org.opentripplanner.ext.fares.impl.GtfsFaresV2Service Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of otp Show documentation
Show all versions of otp Show documentation
The OpenTripPlanner multimodal journey planning system
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