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

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

There is a newer version: 2.6.0
Show 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 javax.annotation.Nonnull;
import org.opentripplanner.ext.fares.model.FareLegRule;
import org.opentripplanner.ext.fares.model.FareProduct;
import org.opentripplanner.ext.fares.model.FareTransferRule;
import org.opentripplanner.ext.fares.model.LegProducts;
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::toAreadId);
    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)
      .map(FareLegRule::fareProduct)
      .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.product().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.fareProduct().amount().cents() == 0)
    );
  }

  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.toAreadId(), toAreasWithRules)
    );
  }

  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(@Nonnull String 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())
    );
  }
}

/**
 * @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()
          .map(LegProducts.ProductWithTransfer::product)
          .collect(Collectors.toSet())
      )
      .orElse(Set.of());
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy