diff --git a/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java b/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java index 385b4e96e2c..f505d001bc4 100644 --- a/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/fares/impl/OrcaFareServiceTest.java @@ -52,10 +52,9 @@ public class OrcaFareServiceTest { private static final Money ONE_DOLLAR = usDollars(1f); private static final Money TWO_DOLLARS = usDollars(2); - private static final Money FERRY_FARE = usDollars(6.10f); - private static final Money HALF_FERRY_FARE = usDollars(3.05f); - private static final Money ORCA_REGULAR_FARE = usDollars(2.50f); - private static final Money ORCA_SPECIAL_FARE = usDollars(1.50f); + private static final Money FERRY_FARE = usDollars(6.50f); + private static final Money HALF_FERRY_FARE = usDollars(3.25f); + private static final Money ORCA_SPECIAL_FARE = usDollars(1.00f); private static final String FEED_ID = "A"; private static TestOrcaFareService orcaFareService; public static final Money DEFAULT_TEST_RIDE_PRICE = usDollars(3.49f); @@ -218,7 +217,7 @@ void calculateFareThatIncludesNoFreeTransfers() { getLeg(WASHINGTON_STATE_FERRIES_AGENCY_ID, 30, "VashonIsland-Fauntelroy"), getLeg(KITSAP_TRANSIT_AGENCY_ID, 60), getLeg(SKAGIT_TRANSIT_AGENCY_ID, 90), - getLeg(KITSAP_TRANSIT_AGENCY_ID, 120), + getLeg(KITSAP_TRANSIT_AGENCY_ID, 121), getLeg(WASHINGTON_STATE_FERRIES_AGENCY_ID, 150, "Fauntleroy-VashonIsland") ); calculateFare(rides, regular, DEFAULT_TEST_RIDE_PRICE.times(4).plus(FERRY_FARE)); @@ -228,20 +227,17 @@ void calculateFareThatIncludesNoFreeTransfers() { DEFAULT_TEST_RIDE_PRICE.times(3).plus(usDollars(.50f)).plus(HALF_FERRY_FARE) ); calculateFare(rides, FareType.youth, Money.ZERO_USD); - calculateFare( - rides, - FareType.electronicSpecial, - ONE_DOLLAR.plus(DEFAULT_TEST_RIDE_PRICE).plus(ONE_DOLLAR).plus(FERRY_FARE) - ); + // We don't get any fares for the skagit transit leg below here because they don't accept ORCA (electronic) + calculateFare(rides, FareType.electronicSpecial, ONE_DOLLAR.plus(ONE_DOLLAR).plus(FERRY_FARE)); calculateFare( rides, FareType.electronicRegular, - DEFAULT_TEST_RIDE_PRICE.times(3).plus(FERRY_FARE) + DEFAULT_TEST_RIDE_PRICE.times(2).plus(FERRY_FARE) ); calculateFare( rides, FareType.electronicSenior, - ONE_DOLLAR.plus(usDollars(0.5f)).plus(ONE_DOLLAR).plus(HALF_FERRY_FARE) + ONE_DOLLAR.plus(ONE_DOLLAR).plus(HALF_FERRY_FARE) ); calculateFare(rides, FareType.electronicYouth, ZERO_USD); } @@ -308,7 +304,7 @@ void calculateFareForKitsapFastFerryEastAgency() { calculateFare(rides, regular, TWO_DOLLARS); calculateFare(rides, FareType.senior, TWO_DOLLARS); calculateFare(rides, FareType.youth, Money.ZERO_USD); - calculateFare(rides, FareType.electronicSpecial, DEFAULT_TEST_RIDE_PRICE); + calculateFare(rides, FareType.electronicSpecial, ONE_DOLLAR); calculateFare(rides, FareType.electronicRegular, TWO_DOLLARS); calculateFare(rides, FareType.electronicSenior, ONE_DOLLAR); calculateFare(rides, FareType.electronicYouth, Money.ZERO_USD); @@ -336,15 +332,15 @@ void calculateFareForWSFPtToTahlequah() { */ @Test void calculateFareForLightRailLeg() { + var regularFare = usDollars(2.50f); List rides = List.of( getLeg(SOUND_TRANSIT_AGENCY_ID, "1-Line", 0, "Roosevelt Station", "Int'l Dist/Chinatown") ); - - calculateFare(rides, regular, ORCA_REGULAR_FARE); - calculateFare(rides, FareType.senior, ONE_DOLLAR); + calculateFare(rides, regular, regularFare); + calculateFare(rides, FareType.senior, regularFare); calculateFare(rides, FareType.youth, Money.ZERO_USD); calculateFare(rides, FareType.electronicSpecial, ORCA_SPECIAL_FARE); - calculateFare(rides, FareType.electronicRegular, ORCA_REGULAR_FARE); + calculateFare(rides, FareType.electronicRegular, regularFare); calculateFare(rides, FareType.electronicSenior, ONE_DOLLAR); calculateFare(rides, FareType.electronicYouth, Money.ZERO_USD); // Ensure that it works in reverse @@ -352,11 +348,11 @@ void calculateFareForLightRailLeg() { List.of( getLeg(SOUND_TRANSIT_AGENCY_ID, "1-Line", 0, "Int'l Dist/Chinatown", "Roosevelt Station") ); - calculateFare(rides, regular, ORCA_REGULAR_FARE); - calculateFare(rides, FareType.senior, ONE_DOLLAR); + calculateFare(rides, regular, regularFare); + calculateFare(rides, FareType.senior, regularFare); calculateFare(rides, FareType.youth, Money.ZERO_USD); calculateFare(rides, FareType.electronicSpecial, ORCA_SPECIAL_FARE); - calculateFare(rides, FareType.electronicRegular, ORCA_REGULAR_FARE); + calculateFare(rides, FareType.electronicRegular, regularFare); calculateFare(rides, FareType.electronicSenior, ONE_DOLLAR); calculateFare(rides, FareType.electronicYouth, Money.ZERO_USD); } @@ -367,7 +363,7 @@ void calculateFareForSounderLeg() { getLeg(SOUND_TRANSIT_AGENCY_ID, "S Line", 0, "King Street Station", "Auburn Station") ); calculateFare(rides, regular, usDollars(4.25f)); - calculateFare(rides, FareType.senior, ONE_DOLLAR); + calculateFare(rides, FareType.senior, usDollars(4.25f)); calculateFare(rides, FareType.youth, Money.ZERO_USD); calculateFare(rides, FareType.electronicSpecial, ORCA_SPECIAL_FARE); calculateFare(rides, FareType.electronicRegular, usDollars(4.25f)); @@ -379,7 +375,7 @@ void calculateFareForSounderLeg() { getLeg(SOUND_TRANSIT_AGENCY_ID, "N Line", 0, "King Street Station", "Everett Station") ); calculateFare(rides, regular, usDollars(5.00f)); - calculateFare(rides, FareType.senior, ONE_DOLLAR); + calculateFare(rides, FareType.senior, usDollars(5.00f)); calculateFare(rides, FareType.youth, Money.ZERO_USD); calculateFare(rides, FareType.electronicSpecial, ORCA_SPECIAL_FARE); calculateFare(rides, FareType.electronicRegular, usDollars(5.00f)); @@ -400,9 +396,10 @@ void calculateSoundTransitBusFares() { getLeg(KC_METRO_AGENCY_ID, "550", 240) ); calculateFare(rides, regular, usDollars(9.75f)); - calculateFare(rides, FareType.senior, usDollars(3)); + // Sound Transit does not accept senior fares in cash + calculateFare(rides, FareType.senior, usDollars(9.75f)); calculateFare(rides, FareType.youth, Money.ZERO_USD); - calculateFare(rides, FareType.electronicSpecial, usDollars(4.50f)); + calculateFare(rides, FareType.electronicSpecial, usDollars(3f)); calculateFare(rides, FareType.electronicRegular, usDollars(9.75f)); calculateFare(rides, FareType.electronicSenior, usDollars(3.00f)); calculateFare(rides, FareType.electronicYouth, Money.ZERO_USD); @@ -416,7 +413,7 @@ void calculateSoundTransitBusFares() { calculateFare(rides, regular, DEFAULT_TEST_RIDE_PRICE.times(2)); calculateFare(rides, FareType.senior, DEFAULT_TEST_RIDE_PRICE.times(2)); calculateFare(rides, FareType.youth, Money.ZERO_USD); - calculateFare(rides, FareType.electronicSpecial, DEFAULT_TEST_RIDE_PRICE); + calculateFare(rides, FareType.electronicSpecial, usDollars(1f)); calculateFare(rides, FareType.electronicRegular, DEFAULT_TEST_RIDE_PRICE); calculateFare(rides, FareType.electronicSenior, ONE_DOLLAR); calculateFare(rides, FareType.electronicYouth, Money.ZERO_USD); @@ -432,9 +429,9 @@ void calculateCashFreeTransferKCMetro() { getLeg(KC_METRO_AGENCY_ID, 130) ); calculateFare(rides, regular, DEFAULT_TEST_RIDE_PRICE.times(3)); - calculateFare(rides, FareType.senior, usDollars(3.25f)); + calculateFare(rides, FareType.senior, DEFAULT_TEST_RIDE_PRICE.times(2).plus(usDollars(1.25f))); calculateFare(rides, FareType.youth, Money.ZERO_USD); - calculateFare(rides, FareType.electronicSpecial, usDollars(3)); + calculateFare(rides, FareType.electronicSpecial, usDollars(1.25f)); calculateFare(rides, FareType.electronicRegular, DEFAULT_TEST_RIDE_PRICE.times(2)); calculateFare(rides, FareType.electronicSenior, usDollars(1.25f)); // Transfer extended by CT ride calculateFare(rides, FareType.electronicYouth, ZERO_USD); @@ -447,8 +444,9 @@ void calculateTransferExtension() { getLeg(SOUND_TRANSIT_AGENCY_ID, "1-Line", 60, "Roosevelt Station", "Angle Lake Station"), // 3.25, should extend transfer getLeg(SOUND_TRANSIT_AGENCY_ID, "1-Line", 140, "Int'l Dist/Chinatown", "Angle Lake Station") // 3.00, should be free under extended transfer ); - calculateFare(rides, regular, ORCA_REGULAR_FARE.plus(usDollars(3.25f)).plus(usDollars(3.00f))); - calculateFare(rides, FareType.senior, usDollars(3)); + var regularFare = usDollars(2.50f).plus(usDollars(3.25f)).plus(usDollars(3.00f)); + calculateFare(rides, regular, regularFare); + calculateFare(rides, FareType.senior, regularFare); calculateFare(rides, FareType.youth, Money.ZERO_USD); calculateFare(rides, FareType.electronicSpecial, ORCA_SPECIAL_FARE.times(2)); calculateFare(rides, FareType.electronicRegular, usDollars(3.25f)); // transfer extended on second leg diff --git a/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareService.java b/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareService.java index 6bc0ad668cf..724de3e225f 100644 --- a/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareService.java +++ b/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFareService.java @@ -10,8 +10,10 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import javax.annotation.Nullable; import org.opentripplanner.ext.fares.model.FareRuleSet; +import org.opentripplanner.ext.ridehailing.model.Ride; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.model.fare.FareMedium; import org.opentripplanner.model.fare.FareProduct; @@ -34,12 +36,14 @@ public class OrcaFareService extends DefaultFareService { public static final String COMM_TRANS_AGENCY_ID = "29"; public static final String KC_METRO_AGENCY_ID = "1"; public static final String SOUND_TRANSIT_AGENCY_ID = "40"; + public static final String T_LINK_AGENCY_ID = "F1"; public static final String EVERETT_TRANSIT_AGENCY_ID = "97"; public static final String PIERCE_COUNTY_TRANSIT_AGENCY_ID = "3"; public static final String SKAGIT_TRANSIT_AGENCY_ID = "e0e4541a-2714-487b-b30c-f5c6cb4a310f"; public static final String SEATTLE_STREET_CAR_AGENCY_ID = "23"; public static final String WASHINGTON_STATE_FERRIES_AGENCY_ID = "WSF"; public static final String KITSAP_TRANSIT_AGENCY_ID = "kt"; + public static final String WHATCOM_AGENCY_ID = "14"; public static final int ROUTE_TYPE_FERRY = 4; public static final String FEED_ID = "orca"; private static final FareMedium ELECTRONIC_MEDIUM = new FareMedium( @@ -67,9 +71,31 @@ protected enum RideType { SOUND_TRANSIT, SOUND_TRANSIT_BUS, SOUND_TRANSIT_SOUNDER, + SOUND_TRANSIT_T_LINK, SOUND_TRANSIT_LINK, WASHINGTON_STATE_FERRIES, - UNKNOWN, + WHATCOM_LOCAL, + WHATCOM_CROSS_COUNTY, + SKAGIT_LOCAL, + SKAGIT_CROSS_COUNTY, + UNKNOWN; + + /** + * All transit agencies permit free transfers, apart from these. + */ + public boolean permitsFreeTransfers() { + return switch (this) { + case WASHINGTON_STATE_FERRIES, SKAGIT_TRANSIT -> false; + default -> true; + }; + } + + public boolean agencyAcceptsOrca() { + return switch (this) { + case WHATCOM_LOCAL, WHATCOM_CROSS_COUNTY, SKAGIT_CROSS_COUNTY, SKAGIT_LOCAL -> false; + default -> true; + }; + } } static RideType getRideType(String agencyId, Route route) { @@ -127,10 +153,16 @@ static RideType getRideType(String agencyId, Route route) { } case SOUND_TRANSIT_AGENCY_ID -> RideType.SOUND_TRANSIT; case EVERETT_TRANSIT_AGENCY_ID -> RideType.EVERETT_TRANSIT; - case SKAGIT_TRANSIT_AGENCY_ID -> RideType.SKAGIT_TRANSIT; + case SKAGIT_TRANSIT_AGENCY_ID -> Set.of("80X", "90X").contains(route.getShortName()) + ? RideType.SKAGIT_CROSS_COUNTY + : RideType.SKAGIT_LOCAL; case SEATTLE_STREET_CAR_AGENCY_ID -> RideType.SEATTLE_STREET_CAR; case WASHINGTON_STATE_FERRIES_AGENCY_ID -> RideType.WASHINGTON_STATE_FERRIES; + case T_LINK_AGENCY_ID -> RideType.SOUND_TRANSIT_T_LINK; case KITSAP_TRANSIT_AGENCY_ID -> RideType.KITSAP_TRANSIT; + case WHATCOM_AGENCY_ID -> "80X".equals(route.getShortName()) + ? RideType.WHATCOM_CROSS_COUNTY + : RideType.WHATCOM_LOCAL; default -> RideType.UNKNOWN; }; } @@ -183,9 +215,10 @@ private static String cleanStationName(String s) { } /** - * Classify the ride type based on the route information provided. In most cases the agency name is sufficient. In - * some cases the route description and short name are needed to define inner agency ride types. For Kitsap, the - * route data is enough to define the agency, but addition trip id checks are needed to define the fast ferry direction. + * Classify the ride type based on the route information provided. In most cases the agency name + * is sufficient. In some cases the route description and short name are needed to define inner + * agency ride types. For Kitsap, the route data is enough to define the agency, but addition trip + * id checks are needed to define the fast ferry direction. */ private static RideType classify(Route route, String tripId) { var rideType = getRideType(route.getAgency().getId().getId(), route); @@ -217,62 +250,71 @@ private static RideType classify(Route route, String tripId) { } /** - * Define which discount fare should be applied based on the fare type. If the ride type is unknown the discount - * fare can not be applied, use the default fare. + * Define which discount fare should be applied based on the fare type. If the ride type is + * unknown the discount fare can not be applied, use the default fare. */ - private Money getLegFare(FareType fareType, RideType rideType, Money defaultFare, Leg leg) { + private Optional getLegFare( + FareType fareType, + RideType rideType, + Money defaultFare, + Leg leg + ) { if (rideType == null) { - return defaultFare; + return Optional.of(defaultFare); + } + // Filter out agencies that don't accept ORCA from the electronic fare type + if (usesOrca(fareType) && !rideType.agencyAcceptsOrca()) { + return Optional.empty(); } return switch (fareType) { - case youth, electronicYouth -> getYouthFare(); + case youth, electronicYouth -> Optional.of(getYouthFare()); case electronicSpecial -> getLiftFare(rideType, defaultFare, leg.getRoute()); - case electronicSenior, senior -> getSeniorFare( - fareType, - rideType, - defaultFare, - leg.getRoute() - ); + case electronicSenior, senior -> getSeniorFare(fareType, rideType, defaultFare, leg); case regular, electronicRegular -> getRegularFare(fareType, rideType, defaultFare, leg); - default -> defaultFare; + default -> Optional.of(defaultFare); }; } + private static Optional optionalUSD(float amount) { + return Optional.of(usDollars(amount)); + } + /** * Apply regular discount fares. If the ride type cannot be matched the default fare is used. */ - private Money getRegularFare(FareType fareType, RideType rideType, Money defaultFare, Leg leg) { + private Optional getRegularFare( + FareType fareType, + RideType rideType, + Money defaultFare, + Leg leg + ) { Route route = leg.getRoute(); return switch (rideType) { - case KC_WATER_TAXI_VASHON_ISLAND -> usDollars(5.75f); - case KC_WATER_TAXI_WEST_SEATTLE -> usDollars(5f); - case KITSAP_TRANSIT_FAST_FERRY_EASTBOUND -> usDollars(2f); - case KITSAP_TRANSIT_FAST_FERRY_WESTBOUND -> usDollars(10f); - case WASHINGTON_STATE_FERRIES -> getWashingtonStateFerriesFare( - route.getLongName(), - fareType, - defaultFare + case KC_WATER_TAXI_VASHON_ISLAND -> optionalUSD(5.75f); + case KC_WATER_TAXI_WEST_SEATTLE -> optionalUSD(5f); + case KITSAP_TRANSIT_FAST_FERRY_EASTBOUND -> optionalUSD(2f); + case KITSAP_TRANSIT_FAST_FERRY_WESTBOUND -> optionalUSD(10f); + case WASHINGTON_STATE_FERRIES -> Optional.of( + getWashingtonStateFerriesFare(route.getLongName(), fareType, defaultFare) ); - case SOUND_TRANSIT_LINK, SOUND_TRANSIT_SOUNDER -> getSoundTransitFare( - leg, - fareType, - defaultFare, - rideType + case SOUND_TRANSIT_LINK, SOUND_TRANSIT_SOUNDER -> Optional.of( + getSoundTransitFare(leg, defaultFare, rideType) ); - case SOUND_TRANSIT_BUS -> usDollars(3.25f); - default -> defaultFare; + case SOUND_TRANSIT_BUS -> optionalUSD(3.25f); + case WHATCOM_LOCAL, + WHATCOM_CROSS_COUNTY, + SKAGIT_LOCAL, + SKAGIT_CROSS_COUNTY -> fareType.equals(FareType.electronicRegular) + ? Optional.empty() + : Optional.of(defaultFare); + default -> Optional.of(defaultFare); }; } /** - * Calculate the correct Link fare from a "ride" including start and end stations. + * Calculate the correct Link fare from a "ride" including start and end stations. */ - private Money getSoundTransitFare( - Leg leg, - FareType fareType, - Money defaultFare, - RideType rideType - ) { + private Money getSoundTransitFare(Leg leg, Money defaultFare, RideType rideType) { String start = cleanStationName(leg.getFrom().name.toString()); String end = cleanStationName(leg.getTo().name.toString()); // Fares are the same no matter the order of the stations @@ -287,87 +329,89 @@ private Money getSoundTransitFare( .ofNullable(fareModel.get(lookupKey)) .orElseGet(() -> fareModel.get(reverseLookupKey)); - return (fare != null) ? fare.get(fareType) : defaultFare; + return (fare != null) ? fare.get(FareType.regular) : defaultFare; } /** * Apply Orca lift discount fares based on the ride type. */ - private Money getLiftFare(RideType rideType, Money defaultFare, Route route) { + private Optional getLiftFare(RideType rideType, Money defaultFare, Route route) { return switch (rideType) { - case COMM_TRANS_LOCAL_SWIFT -> usDollars(1.25f); - case COMM_TRANS_COMMUTER_EXPRESS -> usDollars(2f); - case KC_WATER_TAXI_VASHON_ISLAND -> usDollars(4.5f); - case KC_WATER_TAXI_WEST_SEATTLE -> usDollars(3.75f); - case KITSAP_TRANSIT -> usDollars(1f); + case COMM_TRANS_LOCAL_SWIFT -> optionalUSD(1.25f); + case COMM_TRANS_COMMUTER_EXPRESS -> optionalUSD(2f); + case KC_WATER_TAXI_VASHON_ISLAND -> optionalUSD(4.5f); + case KC_WATER_TAXI_WEST_SEATTLE -> optionalUSD(3.75f); case KC_METRO, SOUND_TRANSIT, SOUND_TRANSIT_BUS, SOUND_TRANSIT_LINK, SOUND_TRANSIT_SOUNDER, + SOUND_TRANSIT_T_LINK, + KITSAP_TRANSIT, EVERETT_TRANSIT, - SEATTLE_STREET_CAR -> usDollars(1.5f); - case WASHINGTON_STATE_FERRIES -> getWashingtonStateFerriesFare( - route.getLongName(), - FareType.electronicSpecial, - defaultFare + PIERCE_COUNTY_TRANSIT, + SEATTLE_STREET_CAR -> optionalUSD(1.00f); + case WASHINGTON_STATE_FERRIES -> Optional.of( + getWashingtonStateFerriesFare(route.getLongName(), FareType.electronicSpecial, defaultFare) ); - default -> defaultFare; + case KITSAP_TRANSIT_FAST_FERRY_EASTBOUND -> optionalUSD((1f)); + case KITSAP_TRANSIT_FAST_FERRY_WESTBOUND -> optionalUSD((5f)); + default -> Optional.of(defaultFare); }; } /** * Apply senior discount fares based on the fare and ride types. */ - private Money getSeniorFare( + private Optional getSeniorFare( FareType fareType, RideType rideType, Money defaultFare, - Route route + Leg leg ) { + var route = leg.getRoute(); return switch (rideType) { - case COMM_TRANS_LOCAL_SWIFT -> usDollars(1.25f); - case COMM_TRANS_COMMUTER_EXPRESS -> usDollars(2f); - case EVERETT_TRANSIT, SKAGIT_TRANSIT -> usDollars(0.5f); - case PIERCE_COUNTY_TRANSIT, SEATTLE_STREET_CAR, KITSAP_TRANSIT -> fareType.equals( // Pierce, Seattle Streetcar, and Kitsap only provide discounted senior fare for orca. - FareType.electronicSenior - ) - ? usDollars(1f) - : defaultFare; + case COMM_TRANS_LOCAL_SWIFT -> optionalUSD(1.25f); + case COMM_TRANS_COMMUTER_EXPRESS -> optionalUSD(2f); + case EVERETT_TRANSIT, SKAGIT_TRANSIT, WHATCOM_LOCAL, SKAGIT_LOCAL -> optionalUSD(0.5f); case KITSAP_TRANSIT_FAST_FERRY_EASTBOUND -> fareType.equals(FareType.electronicSenior) // Kitsap only provide discounted senior fare for orca. - ? usDollars(1f) - : usDollars(2f); - case KC_WATER_TAXI_VASHON_ISLAND -> usDollars(3f); - case KC_WATER_TAXI_WEST_SEATTLE -> usDollars(2.5f); - case KC_METRO, - SOUND_TRANSIT, + ? optionalUSD(1f) + : optionalUSD(2f); + case KC_WATER_TAXI_VASHON_ISLAND -> optionalUSD(3f); + case KC_WATER_TAXI_WEST_SEATTLE -> optionalUSD(2.5f); + case SOUND_TRANSIT, SOUND_TRANSIT_BUS, SOUND_TRANSIT_LINK, - SOUND_TRANSIT_SOUNDER -> usDollars(1f); + SOUND_TRANSIT_SOUNDER, + SOUND_TRANSIT_T_LINK, + KC_METRO, + PIERCE_COUNTY_TRANSIT, + SEATTLE_STREET_CAR, + KITSAP_TRANSIT -> fareType.equals(FareType.electronicSenior) + ? optionalUSD(1f) + : getRegularFare(fareType, rideType, defaultFare, leg); case KITSAP_TRANSIT_FAST_FERRY_WESTBOUND -> fareType.equals(FareType.electronicSenior) - ? usDollars(5f) - : usDollars(10f); + ? optionalUSD(5f) + : optionalUSD(10f); // Discount specific to Skagit transit and not Orca. - case WASHINGTON_STATE_FERRIES -> getWashingtonStateFerriesFare( - route.getLongName(), - fareType, - defaultFare + case WASHINGTON_STATE_FERRIES -> Optional.of( + getWashingtonStateFerriesFare(route.getLongName(), fareType, defaultFare) ); - default -> defaultFare; + case WHATCOM_CROSS_COUNTY, SKAGIT_CROSS_COUNTY -> optionalUSD(1f); + default -> Optional.of(defaultFare); }; } /** - * Apply youth discount fares based on the ride type. - * Youth ride free in Washington. + * Apply youth discount fares based on the ride type. Youth ride free in Washington. */ private Money getYouthFare() { return Money.ZERO_USD; } /** - * Get the washington state ferries fare matching the route long name and fare type. If no match is found, return - * the default fare. + * Get the washington state ferries fare matching the route long name and fare type. If no match + * is found, return the default fare. */ private Money getWashingtonStateFerriesFare( I18NString routeLongName, @@ -403,8 +447,9 @@ private Money getWashingtonStateFerriesFare( } /** - * Get the ride price for a single leg. If testing, this class is being called directly so the required agency cash - * values are not available therefore the default test price is used instead. + * Get the ride price for a single leg. If testing, this class is being called directly so the + * required agency cash values are not available therefore the default test price is used + * instead. */ protected Optional getRidePrice( Leg leg, @@ -415,12 +460,14 @@ protected Optional getRidePrice( } /** - * Calculate the cost of a journey. Where free transfers are not permitted the cash price is used. If free transfers - * are applicable, the most expensive discount fare across all legs is added to the final cumulative price. - * - * The computed fare for Orca card users takes into account realtime trip updates where available, so that, for - * instance, when a leg on a long itinerary is delayed to begin after the initial two hour window has expired, - * the calculated fare for that trip will be two one-way fares instead of one. + * Calculate the cost of a journey. Where free transfers are not permitted the cash price is used. + * If free transfers are applicable, the most expensive discount fare across all legs is added to + * the final cumulative price. + *

+ * The computed fare for Orca card users takes into account realtime trip updates where available, + * so that, for instance, when a leg on a long itinerary is delayed to begin after the initial two + * hour window has expired, the calculated fare for that trip will be two one-way fares instead of + * one. */ @Override public boolean populateFare( @@ -435,15 +482,22 @@ public boolean populateFare( Money orcaFareDiscount = Money.ZERO_USD; for (Leg leg : legs) { RideType rideType = classify(leg.getRoute(), leg.getTrip().getId().getId()); - boolean ridePermitsFreeTransfers = permitsFreeTransfers(rideType); + assert rideType != null; + boolean ridePermitsFreeTransfers = rideType.permitsFreeTransfers(); if (freeTransferStartTime == null && ridePermitsFreeTransfers) { // The start of a free transfer must be with a transit agency that permits it! freeTransferStartTime = leg.getStartTime(); } - Optional singleLegPrice = getRidePrice(leg, fareType, fareRules); - Money legFare = singleLegPrice - .map(slp -> getLegFare(fareType, rideType, slp, leg)) - .orElse(Money.ZERO_USD); + Optional singleLegPrice = getRidePrice(leg, FareType.regular, fareRules); + Optional optionalLegFare = singleLegPrice.flatMap(slp -> + getLegFare(fareType, rideType, slp, leg) + ); + if (optionalLegFare.isEmpty()) { + // If there is no fare for this leg then skip the rest of the logic. + continue; + } + Money legFare = optionalLegFare.get(); + boolean inFreeTransferWindow = inFreeTransferWindow( freeTransferStartTime, leg.getStartTime() @@ -509,10 +563,11 @@ public boolean populateFare( /** * Adds a leg fare product to the given itinerary fares object - * @param leg The leg to create a fareproduct for - * @param itineraryFares The itinerary fares to store the fare product in - * @param fareType Fare type (split into container and rider category) - * @param totalFare Total fare paid after transfer + * + * @param leg The leg to create a fareproduct for + * @param itineraryFares The itinerary fares to store the fare product in + * @param fareType Fare type (split into container and rider category) + * @param totalFare Total fare paid after transfer * @param transferDiscount Transfer discount applied */ private static void addLegFareProduct( @@ -560,8 +615,6 @@ protected Collection fareRulesForFeed(FareType fareType, String fee /** * Check if trip falls within the transfer time window. - * @param freeTransferStartTime - * @param currentLegStartTime */ private boolean inFreeTransferWindow( ZonedDateTime freeTransferStartTime, @@ -579,21 +632,11 @@ private boolean inFreeTransferWindow( private boolean hasFreeTransfers(FareType fareType, RideType rideType) { // King County Metro allows transfers on cash fare return ( - (permitsFreeTransfers(rideType) && usesOrca(fareType)) || + (rideType.permitsFreeTransfers() && usesOrca(fareType)) || (rideType == RideType.KC_METRO && !usesOrca(fareType)) ); } - /** - * All transit agencies permit free transfers, apart from these. - */ - private boolean permitsFreeTransfers(RideType rideType) { - return switch (rideType) { - case WASHINGTON_STATE_FERRIES, SKAGIT_TRANSIT -> false; - default -> true; - }; - } - /** * Define Orca fare types. */ diff --git a/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFaresData.java b/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFaresData.java index dbb04c552da..44d371590aa 100644 --- a/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFaresData.java +++ b/src/ext/java/org/opentripplanner/ext/fares/impl/OrcaFaresData.java @@ -236,20 +236,20 @@ class OrcaFaresData { // Spaces have been removed from the route name because of inconsistencies in the WSF GTFS route dataset. public static Map> washingtonStateFerriesFares = Map.ofEntries( - sEntry("Seattle-BainbridgeIsland", 9.25f, 4.60f), - sEntry("Seattle-Bremerton", 9.25f, 4.60f), - sEntry("Mukilteo-Clinton", 5.65f, 2.80f), - sEntry("Fauntleroy-VashonIsland", 6.10f, 3.05f), - sEntry("Fauntleroy-Southworth", 7.20f, 3.60f), - sEntry("Edmonds-Kingston", 9.25f, 4.60f), - sEntry("PointDefiance-Tahlequah", 6.10f, 3.05f), - sEntry("Anacortes-FridayHarbor", 14.85f, 7.40f), - sEntry("Anacortes-LopezIsland", 14.85f, 7.40f), - sEntry("Anacortes-OrcasIsland", 14.85f, 7.40f), - sEntry("Anacortes-ShawIsland", 14.85f, 7.40f), - sEntry("Coupeville-PortTownsend", 3.85f, 1.90f), - sEntry("PortTownsend-Coupeville", 3.85f, 1.90f), - sEntry("Southworth-VashonIsland", 6.10f, 3.05f) + sEntry("Seattle-BainbridgeIsland", 9.85f, 4.90f), + sEntry("Seattle-Bremerton", 9.85f, 4.90f), + sEntry("Mukilteo-Clinton", 6f, 3f), + sEntry("Fauntleroy-VashonIsland", 6.50f, 3.25f), + sEntry("Fauntleroy-Southworth", 7.70f, 3.85f), + sEntry("Edmonds-Kingston", 9.85f, 4.90f), + sEntry("PointDefiance-Tahlequah", 6.50f, 3.25f), + sEntry("Anacortes-FridayHarbor", 15.85f, 7.90f), + sEntry("Anacortes-LopezIsland", 15.85f, 7.90f), + sEntry("Anacortes-OrcasIsland", 15.85f, 7.90f), + sEntry("Anacortes-ShawIsland", 15.85f, 7.90f), + sEntry("Coupeville-PortTownsend", 4.10f, 2.05f), + sEntry("PortTownsend-Coupeville", 4.10f, 2.05f), + sEntry("Southworth-VashonIsland", 6.50f, 3.25f) ); private static Map.Entry> entry(