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

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

package org.opentripplanner.routing.impl;

import java.io.Serializable;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Currency;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.opentripplanner.model.FeedScopedId;
import org.opentripplanner.model.FareAttribute;
import org.opentripplanner.model.Stop;
import org.opentripplanner.routing.core.Fare;
import org.opentripplanner.routing.core.Fare.FareType;
import org.opentripplanner.routing.core.FareComponent;
import org.opentripplanner.routing.core.FareRuleSet;
import org.opentripplanner.routing.core.Money;
import org.opentripplanner.routing.core.State;
import org.opentripplanner.routing.core.WrappedCurrency;
import org.opentripplanner.routing.edgetype.HopEdge;
import org.opentripplanner.routing.graph.Edge;
import org.opentripplanner.routing.services.FareService;
import org.opentripplanner.routing.spt.GraphPath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** A set of edges on a single route, with associated information for calculating fares */
class Ride {

    String feedId;

    String agency; // route agency

    FeedScopedId route;

    FeedScopedId trip;
    
    Set zones;

    String startZone;

    String endZone;

    long startTime;

    long endTime;

    // in DefaultFareServiceImpl classifier is just the TraverseMode
    // it can be used differently in custom fare services
    public Object classifier;

    public Stop firstStop;

    public Stop lastStop;

    public Ride() {
        zones = new HashSet();
    }

    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("Ride");
        if (startZone != null) {
            builder.append("(from zone ");
            builder.append(startZone);
        }
        if (endZone != null) {
            builder.append(" to zone ");
            builder.append(endZone);
        }
        builder.append(" on route ");
        builder.append(route);
        if (zones.size() > 0) {
            builder.append(" through zones ");
            boolean first = true;
            for (String zone : zones) {
                if (first) {
                    first = false;
                } else {
                    builder.append(",");
                }
                builder.append(zone);
            }
        }
        builder.append(" at ");
        builder.append(startTime);
        if (classifier != null) {
            builder.append(", classified by ");
            builder.append(classifier.toString());
        }
        builder.append(")");
        return builder.toString();
    }
}

/** Holds information for doing the graph search on fares */
class FareSearch {
    // Cell [i,j] holds the best (lowest) cost for a trip from rides[i] to rides[j]
    float[][] resultTable;

    // Cell [i,j] holds the index of the ride to pass through for the best cost
    // This is used for reconstructing which rides are grouped together
    int[][] next;

    // Cell [i,j] holds the id of the fare that corresponds to the relevant cost
    // we can't just use FareAndId for resultTable because you need to sum them
    FeedScopedId[][] fareIds;

    // Cell [i] holds the index of the last ride that ride[i] has a fare to
    // If it's -1, the ride does not have fares to anywhere
    int[] endOfComponent;

    FareSearch(int size) {
        resultTable = new float[size][size];
        next = new int[size][size];
        fareIds = new FeedScopedId[size][size];
        endOfComponent = new int[size];
        Arrays.fill(endOfComponent, -1);
    }
}

/** Holds fare and corresponding fareId */
class FareAndId {
    float fare;
    FeedScopedId fareId;

    FareAndId(float fare, FeedScopedId fareId) {
        this.fare = fare;
        this.fareId = fareId;
    }
}

/**
 * This fare service module handles the cases that GTFS handles within a single feed.
 * It cannot necessarily handle multi-feed graphs, because a rule-less fare attribute
 * might be applied to rides on routes in another feed, for example.
 * For more interesting fare structures like New York's MTA, or cities with multiple
 * feeds and inter-feed transfer rules, you get to implement your own FareService. 
 * See this thread on gtfs-changes explaining the proper interpretation of fares.txt:
 * http://groups.google.com/group/gtfs-changes/browse_thread/thread/8a4a48ae1e742517/4f81b826cb732f3b
 */
public class DefaultFareServiceImpl implements FareService, Serializable {

    private static final long serialVersionUID = 20120229L;

    private static final Logger LOG = LoggerFactory.getLogger(DefaultFareServiceImpl.class);

    /** For each fare type (regular, student, etc...) the collection of rules that apply. */
    protected Map> fareRulesPerType;

    public DefaultFareServiceImpl() {
        fareRulesPerType = new HashMap<>();
    }

    public void addFareRules(FareType fareType, Collection fareRules) {
        fareRulesPerType.put(fareType, new ArrayList<>(fareRules));
    }

    protected List createRides(GraphPath path) {
        List rides = new LinkedList();
        Ride ride = null;
        for (State state : path.states) {
            Edge edge = state.getBackEdge();
            if ( ! (edge instanceof HopEdge))
                continue;
            HopEdge hEdge = (HopEdge) edge;
            if (ride == null || ! state.getRoute().equals(ride.route)) {
                ride = new Ride();
                rides.add(ride);
                ride.startZone = hEdge.getBeginStop().getZoneId();
                ride.zones.add(ride.startZone);
                ride.agency = state.getBackTrip().getRoute().getAgency().getId();
                ride.route = state.getRoute();
                ride.startTime = state.getBackState().getTimeSeconds();
                ride.firstStop = hEdge.getBeginStop();
                ride.trip = state.getTripId();
                ride.feedId = hEdge.getFeedId();
            }
            ride.lastStop = hEdge.getEndStop();
            ride.endZone  = ride.lastStop.getZoneId();
            ride.zones.add(ride.endZone);
            ride.endTime  = state.getTimeSeconds();
            // in default fare service, classify rides by mode 
            ride.classifier = state.getBackMode();
        }
        return rides;
    }

    @Override
    public Fare getCost(GraphPath path) {

        List rides = createRides(path);
        // If there are no rides, there's no fare.
        if (rides.size() == 0) {
            return null;
        }

        Fare fare = new Fare();
        boolean hasFare = false;
        for (Map.Entry> kv : fareRulesPerType.entrySet()) {
            FareType fareType = kv.getKey();
            Collection fareRules = kv.getValue();
            // Get the currency from the first fareAttribute, assuming that all tickets use the same currency.
            Currency currency = null;
            if (fareRules.size() > 0) {
                currency = Currency.getInstance(fareRules.iterator().next().getFareAttribute().getCurrencyType());
            }
            hasFare = populateFare(fare, currency, fareType, rides, fareRules);
        }
        return hasFare ? fare : null;
    }

    protected static Money getMoney(Currency currency, float cost) {
        int fractionDigits = 2;
        if (currency != null)
            fractionDigits = currency.getDefaultFractionDigits();
        int cents = (int) Math.round(cost * Math.pow(10, fractionDigits));
        return new Money(new WrappedCurrency(currency), cents);
    }

    private FareSearch performSearch(FareType fareType, List rides,
            Collection fareRules) {
        FareSearch r = new FareSearch(rides.size());

        // Dynamic algorithm to calculate fare cost.
        // This is a modified Floyd-Warshall algorithm, a key thing to remember is that
        // rides are already edges, so when comparing "via" routes, i -> k is connected
        // to k+1 -> j.
        for (int i = 0; i < rides.size(); i++) {
            // each diagonal
            for (int j = 0; j < rides.size() - i; j++) {
                FareAndId best = getBestFareAndId(fareType, rides.subList(j, j + i + 1), fareRules);
                float cost = best.fare;
                if (cost < 0) {
                    LOG.error("negative cost for a ride sequence");
                    cost = Float.POSITIVE_INFINITY;
                }
                if (cost < Float.POSITIVE_INFINITY) {
                    r.endOfComponent[j] = j + i;
                    r.next[j][j + i] = j + i;
                }
                r.resultTable[j][j + i] = cost;
                r.fareIds[j][j + i] = best.fareId;
                for (int k = 0; k < i; k++) {
                    float via = addFares(rides.subList(j, j + k + 1), rides.subList(j + k + 1, j + i + 1),
                            r.resultTable[j][j + k], r.resultTable[j + k + 1][j + i]);
                    if (r.resultTable[j][j + i] > via) {
                        r.resultTable[j][j + i] = via;
                        r.endOfComponent[j] = j + i;
                        r.next[j][j + i] = r.next[j][j + k];
                    }
                }
            }
        }
        return r;
    }

    protected float addFares(List ride0, List ride1, float cost0, float cost1) {
        return cost0 + cost1;
    }

    protected float getLowestCost(FareType fareType, List rides,
            Collection fareRules) {
        FareSearch r = performSearch(fareType, rides, fareRules);
        return r.resultTable[0][rides.size()-1];
    }

    /**
     * Builds the Fare object for the given currency, fareType and fareRules.
     * 

* Besides calculating the lowest fare, we also break down the fare and which routes * correspond to which components. Note that even if we cannot get a lowest fare * (if some rides don't have fare rules), there will still be a breakdown for those * parts which have fares. *

* As an example, given the rides A-B and B-C. Where A-B and B-C have fares of 10 * each, 2 fare detail objects are added, one with fare 10 for A-B and one with fare 10 * for B-C. *

* If we add the rule for A-C with a fare of 15, we will get 1 fare detail object * with fare 15, which lists both A-B and B-C as routes involved. *

* If our only rule were A-B with a fare of 10, we would have no lowest fare, but * we will still have one fare detail with fare 10 for the route A-B. B-C will not * just not be listed at all. */ protected boolean populateFare(Fare fare, Currency currency, FareType fareType, List rides, Collection fareRules) { FareSearch r = performSearch(fareType, rides, fareRules); List details = new ArrayList(); int count = 0; int start = 0; int end = rides.size() - 1; while(start <= end) { // skip parts where no fare is present, we want to return something // even if not all legs have fares while(start <= end && r.endOfComponent[start] < 0) { ++start; } if(start > end) { break; } int via = r.next[start][r.endOfComponent[start]]; float cost = r.resultTable[start][via]; FeedScopedId fareId = r.fareIds[start][via]; FareComponent detail = new FareComponent(fareId, getMoney(currency, cost)); for(int i = start; i <= via; ++i) { detail.addRoute(rides.get(i).route); } details.add(detail); ++count; start = via + 1; } fare.addFare(fareType, getMoney(currency, r.resultTable[0][rides.size()-1])); fare.addFareDetails(fareType, details); return count > 0; } protected float calculateCost(FareType fareType, List rides, Collection fareRules) { return getBestFareAndId(fareType, rides, fareRules).fare; } private FareAndId getBestFareAndId(FareType fareType, List rides, Collection fareRules) { Set zones = new HashSet(); Set routes = new HashSet(); Set agencies = new HashSet(); Set trips = new HashSet(); int transfersUsed = -1; Ride firstRide = rides.get(0); long startTime = firstRide.startTime; String startZone = firstRide.startZone; String endZone = firstRide.endZone; String feedId = firstRide.feedId; long lastRideStartTime = firstRide.startTime; long lastRideEndTime = firstRide.endTime; for (Ride ride : rides) { if ( ! ride.feedId.equals(feedId)) { LOG.debug("skipped multi-feed ride sequence {}", rides); return new FareAndId(Float.POSITIVE_INFINITY, null); } lastRideStartTime = ride.startTime; lastRideEndTime = ride.endTime; endZone = ride.endZone; agencies.add(ride.agency); routes.add(ride.route); zones.addAll(ride.zones); trips.add(ride.trip); transfersUsed += 1; } FareAttribute bestAttribute = null; float bestFare = Float.POSITIVE_INFINITY; long tripTime = lastRideStartTime - startTime; long journeyTime = lastRideEndTime - startTime; // find the best fare that matches this set of rides for (FareRuleSet ruleSet : fareRules) { FareAttribute attribute = ruleSet.getFareAttribute(); // fares also don't really have an agency id, they will have the per-feed default id // check only if the fare is not mapped to an agency if (!ruleSet.hasAgencyDefined() && !attribute.getId().getAgencyId().equals(feedId)) continue; if (ruleSet.matches(agencies, startZone, endZone, zones, routes, trips)) { // TODO Maybe move the code below in FareRuleSet::matches() ? if (attribute.isTransfersSet() && attribute.getTransfers() < transfersUsed) { continue; } // assume transfers are evaluated at boarding time, // as trimet does if (attribute.isTransferDurationSet() && tripTime > attribute.getTransferDuration()) { continue; } if (attribute.isJourneyDurationSet() && journeyTime > attribute.getJourneyDuration()) { continue; } float newFare = getFarePrice(attribute, fareType); if (newFare < bestFare) { bestAttribute = attribute; bestFare = newFare; } } } LOG.debug("{} best for {}", bestAttribute, rides); if (bestFare == Float.POSITIVE_INFINITY) { LOG.debug("No fare for a ride sequence: {}", rides); } return new FareAndId(bestFare, bestAttribute == null ? null : bestAttribute.getId()); } private float getFarePrice(FareAttribute fare, FareType type) { switch(type) { case senior: if (fare.getSeniorPrice() >= 0) { return fare.getSeniorPrice(); } break; case youth: if (fare.getYouthPrice() >= 0) { return fare.getYouthPrice(); } break; case regular: default: break; } return fare.getPrice(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy