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

org.opentripplanner.ext.fares.impl.GtfsFaresV2Service Maven / Gradle / Ivy

The newest version!
package org.opentripplanner.ext.fares.impl;

import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;

import com.google.common.collect.Multimap;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.opentripplanner.ext.fares.model.FareDistance;
import org.opentripplanner.ext.fares.model.FareLegRule;
import org.opentripplanner.ext.fares.model.FareTransferRule;
import org.opentripplanner.ext.fares.model.LegProducts;
import org.opentripplanner.model.fare.FareProduct;
import org.opentripplanner.model.plan.Itinerary;
import org.opentripplanner.model.plan.Leg;
import org.opentripplanner.model.plan.ScheduledTransitLeg;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.site.StopLocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public final class GtfsFaresV2Service implements Serializable {

  private static final Logger LOG = LoggerFactory.getLogger(GtfsFaresV2Service.class);
  private final List legRules;
  private final List transferRules;
  private final Multimap stopAreas;
  private final Set networksWithRules;
  private final Set fromAreasWithRules;
  private final Set toAreasWithRules;

  public GtfsFaresV2Service(
    List legRules,
    List fareTransferRules,
    Multimap stopAreas
  ) {
    this.legRules = legRules;
    this.transferRules = fareTransferRules;
    this.networksWithRules = findNetworksWithRules(legRules);
    this.fromAreasWithRules = findAreasWithRules(legRules, FareLegRule::fromAreaId);
    this.toAreasWithRules = findAreasWithRules(legRules, FareLegRule::toAreaId);
    this.stopAreas = stopAreas;
  }

  public ProductResult getProducts(Itinerary itinerary) {
    var transitLegs = itinerary.getScheduledTransitLegs();

    var allLegProducts = new HashSet();
    for (int i = 0; i < transitLegs.size(); i++) {
      var leg = transitLegs.get(i);
      var nextIndex = i + 1;

      Optional nextLeg = Optional.empty();
      if (nextIndex < transitLegs.size()) {
        nextLeg = Optional.of(transitLegs.get(nextIndex));
      }

      var lp = getLegProduct(leg, nextLeg);
      allLegProducts.add(lp);
    }

    var coveringItinerary = productsCoveringItinerary(itinerary, allLegProducts);

    return new ProductResult(coveringItinerary, allLegProducts);
  }

  private static Set findAreasWithRules(
    List legRules,
    Function getArea
  ) {
    return legRules.stream().map(getArea).filter(Objects::nonNull).collect(Collectors.toSet());
  }

  private static Set findNetworksWithRules(Collection legRules) {
    return legRules
      .stream()
      .map(FareLegRule::networkId)
      .filter(Objects::nonNull)
      .collect(Collectors.toSet());
  }

  private Set productsCoveringItinerary(
    Itinerary itinerary,
    Collection legProducts
  ) {
    var distinctProductWithTransferSets = legProducts
      .stream()
      .map(LegProducts::products)
      .collect(Collectors.toSet());

    return distinctProductWithTransferSets
      .stream()
      .flatMap(p -> p.stream().filter(ps -> coversItinerary(itinerary, ps)))
      .map(LegProducts.ProductWithTransfer::legRule)
      .flatMap(r -> r.fareProducts().stream())
      .collect(Collectors.toSet());
  }

  private boolean coversItinerary(Itinerary i, LegProducts.ProductWithTransfer pwt) {
    var transitLegs = i.getScheduledTransitLegs();
    var allLegsInProductFeed = transitLegs
      .stream()
      .allMatch(leg -> leg.getAgency().getId().getFeedId().equals(pwt.legRule().feedId()));

    return (
      allLegsInProductFeed &&
      (transitLegs.size() == 1 ||
        (pwt.products().stream().anyMatch(p -> p.coversDuration(i.getTransitDuration())) &&
          appliesToAllLegs(pwt.legRule(), transitLegs)) ||
        coversItineraryWithFreeTransfers(i, pwt))
    );
  }

  private boolean appliesToAllLegs(FareLegRule legRule, List transitLegs) {
    return transitLegs.stream().allMatch(leg -> legMatchesRule(leg, legRule));
  }

  private boolean coversItineraryWithFreeTransfers(
    Itinerary i,
    LegProducts.ProductWithTransfer pwt
  ) {
    var feedIdsInItinerary = i
      .getScheduledTransitLegs()
      .stream()
      .map(l -> l.getAgency().getId().getFeedId())
      .collect(Collectors.toSet());

    return (
      feedIdsInItinerary.size() == 1 &&
      pwt
        .transferRules()
        .stream()
        .anyMatch(r -> r.fareProducts().stream().anyMatch(fp -> fp.price().isZero()))
    );
  }

  private boolean legMatchesRule(ScheduledTransitLeg leg, FareLegRule rule) {
    // make sure that you only get rules for the correct feed
    return (
      leg.getAgency().getId().getFeedId().equals(rule.feedId()) &&
      matchesNetworkId(leg, rule) &&
      // apply only those fare leg rules which have the correct area ids
      // if area id is null, the rule applies to all legs UNLESS there is another rule that
      // covers this area
      matchesArea(leg.getFrom().stop, rule.fromAreaId(), fromAreasWithRules) &&
      matchesArea(leg.getTo().stop, rule.toAreaId(), toAreasWithRules) &&
      matchesDistance(leg, rule)
    );
  }

  private LegProducts getLegProduct(
    ScheduledTransitLeg leg,
    Optional nextLeg
  ) {
    var legRules =
      this.legRules.stream().filter(r -> legMatchesRule(leg, r)).collect(Collectors.toSet());

    var transferRulesForLeg = transferRules
      .stream()
      .filter(t -> t.feedId().equals(leg.getAgency().getId().getFeedId()))
      .toList();

    var products = legRules
      .stream()
      .map(rule -> {
        var transferRulesToNextLeg = transferRulesForLeg
          .stream()
          .filter(GtfsFaresV2Service::checkForWildcards)
          .filter(t -> t.fromLegGroup().equals(rule.legGroupId()))
          .filter(t -> transferRuleMatchesNextLeg(nextLeg, t))
          .toList();
        return new LegProducts.ProductWithTransfer(rule, transferRulesToNextLeg);
      })
      .collect(Collectors.toSet());

    return new LegProducts(leg, nextLeg, products);
  }

  private static boolean checkForWildcards(FareTransferRule t) {
    if (Objects.isNull(t.fromLegGroup()) || Objects.isNull(t.toLegGroup())) {
      LOG.error(
        "Transfer rule {} contains a wildcard leg group reference. These are not supported yet.",
        t
      );
      return false;
    } else return true;
  }

  private boolean transferRuleMatchesNextLeg(
    Optional nextLeg,
    FareTransferRule t
  ) {
    return nextLeg
      .map(nLeg -> {
        var maybeFareRule = getFareLegRuleByGroupId(t.toLegGroup());
        return maybeFareRule.map(rule -> legMatchesRule(nLeg, rule)).orElse(false);
      })
      .orElse(false);
  }

  private Optional getFareLegRuleByGroupId(FeedScopedId groupId) {
    return legRules.stream().filter(lr -> groupId.equals(lr.legGroupId())).findAny();
  }

  private boolean matchesArea(StopLocation stop, String areaId, Set areasWithRules) {
    var stopAreas = this.stopAreas.get(stop.getId());
    return (
      (isNull(areaId) && stopAreas.stream().noneMatch(areasWithRules::contains)) ||
      (nonNull(areaId) && stopAreas.contains(areaId))
    );
  }

  /**
   * Get the fare products that match the network_id. If the network id of the product is null it
   * depends on the presence/absence of other rules with that network id.
   */
  private boolean matchesNetworkId(ScheduledTransitLeg leg, FareLegRule rule) {
    var routesNetworkIds = leg
      .getRoute()
      .getGroupsOfRoutes()
      .stream()
      .map(group -> group.getId().getId())
      .filter(Objects::nonNull)
      .toList();

    return (
      (isNull(rule.networkId()) &&
        networksWithRules.stream().noneMatch(routesNetworkIds::contains)) ||
      routesNetworkIds.contains(rule.networkId())
    );
  }

  private boolean matchesDistance(ScheduledTransitLeg leg, FareLegRule rule) {
    // If no valid distance type is given, do not consider distances in fare computation

    FareDistance distance = rule.fareDistance();
    if (distance instanceof FareDistance.Stops ruleDistance) {
      var numStops = leg.getIntermediateStops().size();
      return numStops >= ruleDistance.min() && ruleDistance.max() >= numStops;
    } else if (rule.fareDistance() instanceof FareDistance.LinearDistance ruleDistance) {
      var ruleMax = ruleDistance.max();
      var ruleMin = ruleDistance.min();
      var legDistance = leg.getDirectDistanceMeters();

      return legDistance > ruleMin.toMeters() && legDistance < ruleMax.toMeters();
    } else return true;
  }

  /**
   * @param itineraryProducts The fare products that cover the entire itinerary, like a daily pass.
   * @param legProducts       The fare products that cover only individual legs.
   */
  record ProductResult(Set itineraryProducts, Set legProducts) {
    public Set getProducts(Leg leg) {
      return legProducts
        .stream()
        .filter(lp -> lp.leg().equals(leg))
        .findFirst()
        .map(l ->
          l.products().stream().flatMap(lp -> lp.products().stream()).collect(Collectors.toSet())
        )
        .orElse(Set.of());
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy