
org.opentripplanner.ext.fares.impl.HSLFareService Maven / Gradle / Ivy
The newest version!
package org.opentripplanner.ext.fares.impl;
import com.google.common.collect.Sets;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.opentripplanner.ext.fares.model.FareAttribute;
import org.opentripplanner.ext.fares.model.FareRuleSet;
import org.opentripplanner.ext.fares.model.RouteOriginDestination;
import org.opentripplanner.model.plan.Leg;
import org.opentripplanner.model.plan.ScheduledTransitLeg;
import org.opentripplanner.routing.core.FareType;
import org.opentripplanner.transit.model.basic.Money;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This fare service module handles single feed HSL ticket pricing logic.
*/
public class HSLFareService extends DefaultFareService {
private static final Logger LOG = LoggerFactory.getLogger(HSLFareService.class);
// this is not Float.MAX_VALUE to avoid overflow which would then make debugging harder
public static final Money MAX_PRICE = Money.euros(999999f);
@Override
protected boolean shouldCombineInterlinedLegs(
ScheduledTransitLeg previousLeg,
ScheduledTransitLeg currentLeg
) {
return true;
}
@Override
protected Optional getBestFareAndId(
FareType fareType,
List legs,
Collection fareRules
) {
Set zones = new HashSet<>();
ZonedDateTime startTime = legs.get(0).getStartTime();
ZonedDateTime lastRideStartTime = startTime;
Money specialRouteFare = MAX_PRICE;
FareAttribute specialFareAttribute = null;
String agency = null;
boolean singleAgency = true;
// Do not consider fares for legs that do not have fare rules in the same feed
Set fareRuleFeedIds = fareRules
.stream()
.map(fr -> fr.getFareAttribute().getId().getFeedId())
.collect(Collectors.toSet());
Set legFeedIds = legs
.stream()
.map(leg -> leg.getAgency().getId().getFeedId())
.collect(Collectors.toSet());
if (!Sets.difference(legFeedIds, fareRuleFeedIds).isEmpty()) {
return Optional.empty();
}
for (Leg leg : legs) {
lastRideStartTime = leg.getStartTime();
if (agency == null) {
agency = leg.getAgency().getId().getId().toString();
} else if (agency != leg.getAgency().getId().getId().toString()) {
singleAgency = false;
}
/* HSL specific logic: all exception routes start and end from the defined zone set,
but visit temporarily (maybe 1 stop only) an 'external' zone */
Money bestSpecialFare = MAX_PRICE;
Set ruleZones = null;
for (FareRuleSet ruleSet : fareRules) {
if (
ruleSet.hasAgencyDefined() &&
leg.getAgency().getId().getId() != ruleSet.getAgency().getId()
) {
continue;
}
RouteOriginDestination routeOriginDestination = new RouteOriginDestination(
leg.getRoute().getId().toString(),
leg.getFrom().stop.getFirstZoneAsString(),
leg.getTo().stop.getFirstZoneAsString()
);
boolean isSpecialRoute = false;
if (
!ruleSet.getRouteOriginDestinations().isEmpty() &&
ruleSet
.getRouteOriginDestinations()
.toString()
.indexOf(routeOriginDestination.toString()) !=
-1
) {
isSpecialRoute = true;
}
if (
isSpecialRoute ||
(ruleSet.getRoutes().contains(leg.getRoute().getId()) &&
ruleSet.getContains().contains(leg.getFrom().stop.getFirstZoneAsString()) &&
ruleSet.getContains().contains(leg.getTo().stop.getFirstZoneAsString()))
) {
// check validity of this special rule and that it is the cheapest applicable one
FareAttribute attribute = ruleSet.getFareAttribute();
if (
!attribute.isTransferDurationSet() ||
Duration.between(lastRideStartTime, startTime).getSeconds() <
attribute.getTransferDuration()
) {
Money newFare = attribute.getPrice();
if (newFare.lessThan(bestSpecialFare)) {
bestSpecialFare = newFare;
ruleZones = ruleSet.getContains();
if (isSpecialRoute) {
specialRouteFare = bestSpecialFare;
specialFareAttribute = attribute;
}
}
}
}
}
if (ruleZones != null) { // the special case
// evaluate boolean ride.zones AND rule.zones
Set zoneIntersection = new HashSet(
leg.getFareZones().stream().map(z -> z.getId().getId()).toList()
);
zoneIntersection.retainAll(ruleZones); // don't add temporarily visited zones
zones.addAll(zoneIntersection);
} else {
zones.addAll(leg.getFareZones().stream().map(z -> z.getId().getId()).toList());
}
}
FareAttribute bestAttribute = null;
Money bestFare = MAX_PRICE;
long tripTime = Duration.between(startTime, lastRideStartTime).getSeconds();
if (zones.size() > 0) {
// find the best fare that matches this set of rides
for (FareRuleSet ruleSet : fareRules) {
// make sure the rule is applicable by agency requirements
if (
ruleSet.hasAgencyDefined() && (!singleAgency || agency != ruleSet.getAgency().getId())
) {
continue;
}
/* another HSL specific change: We do not set rules for every possible zone combination,
but for the largest zone set allowed for a certain ticket type.
This way we need only a few rules instead of hundreds of rules. Good for speed!
*/
if (ruleSet.getContains().containsAll(zones)) { // contains, not equals !!
FareAttribute attribute = ruleSet.getFareAttribute();
// transfers are evaluated at boarding time
if (attribute.isTransferDurationSet()) {
if (tripTime > attribute.getTransferDuration()) {
LOG.debug(
"transfer time exceeded; {} > {} in fare {}",
tripTime,
attribute.getTransferDuration(),
attribute.getId()
);
continue;
} else {
LOG.debug(
"transfer time OK; {} < {} in fare {}",
tripTime,
attribute.getTransferDuration(),
attribute.getId()
);
}
}
Money newFare = attribute.getPrice();
if (
newFare.lessThan(bestFare) ||
(newFare.equals(bestFare) && ruleSet.getContains().equals(zones))
) {
bestAttribute = attribute;
bestFare = newFare;
}
}
}
} else if (!specialRouteFare.equals(MAX_PRICE) && specialFareAttribute != null) {
bestFare = specialRouteFare;
bestAttribute = specialFareAttribute;
}
LOG.debug("HSL {} best for {}", bestAttribute, legs);
final Money finalBestFare = bestFare;
return Optional.ofNullable(bestAttribute).map(attribute ->
new FareAndId(finalBestFare, attribute.getId())
);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy