Skip to content

Commit

Permalink
Merge pull request #5796 from ibi-group/flex-duration-factors
Browse files Browse the repository at this point in the history
Implement GTFS Flex safe duration spec draft
  • Loading branch information
leonardehrenfried authored May 17, 2024
2 parents e0c163c + 695161a commit ef0d16b
Show file tree
Hide file tree
Showing 25 changed files with 444 additions and 86 deletions.
15 changes: 13 additions & 2 deletions doc-templates/Flex.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,19 @@

To enable this turn on `FlexRouting` as a feature in `otp-config.json`.

The GTFS feeds should conform to the
[GTFS-Flex v2 draft PR](https://github.com/google/transit/pull/388)
The GTFS feeds must conform to the final, approved version of the draft which has been
merged into the [mainline specification](https://gtfs.org/schedule/reference/) in March 2024.

### Experimental features

This sandbox feature also has experimental support for the following fields:

- `safe_duration_factor`
- `safe_duration_offset`

These features are currently [undergoing specification](https://github.com/MobilityData/gtfs-flex/pull/79)
and their definition might change. OTP's implementation will be also be changed so be careful
when relying on this feature.

## Configuration

Expand Down
15 changes: 13 additions & 2 deletions docs/sandbox/Flex.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,19 @@

To enable this turn on `FlexRouting` as a feature in `otp-config.json`.

The GTFS feeds should conform to the
[GTFS-Flex v2 draft PR](https://github.com/google/transit/pull/388)
The GTFS feeds must conform to the final, approved version of the draft which has been
merged into the [mainline specification](https://gtfs.org/schedule/reference/) in March 2024.

### Experimental features

This sandbox feature also has experimental support for the following fields:

- `safe_duration_factor`
- `safe_duration_offset`

These features are currently [undergoing specification](https://github.com/MobilityData/gtfs-flex/pull/79)
and their definition might change. OTP's implementation will be also be changed so be careful
when relying on this feature.

## Configuration

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.opentripplanner.ext.flex;

import static org.opentripplanner.model.StopTime.MISSING_VALUE;

import org.opentripplanner._support.geometry.Polygons;
import org.opentripplanner.framework.time.TimeUtils;
import org.opentripplanner.model.StopTime;
import org.opentripplanner.transit.model._data.TransitModelForTest;
import org.opentripplanner.transit.model.site.RegularStop;
import org.opentripplanner.transit.model.site.StopLocation;

public class FlexStopTimesForTest {

private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of();
private static final StopLocation AREA_STOP = TEST_MODEL.areaStopForTest("area", Polygons.BERLIN);
private static final RegularStop REGULAR_STOP = TEST_MODEL.stop("stop").build();

public static StopTime area(String startTime, String endTime) {
return area(AREA_STOP, endTime, startTime);
}

public static StopTime area(StopLocation areaStop, String endTime, String startTime) {
var stopTime = new StopTime();
stopTime.setStop(areaStop);
stopTime.setFlexWindowStart(TimeUtils.time(startTime));
stopTime.setFlexWindowEnd(TimeUtils.time(endTime));
return stopTime;
}

public static StopTime regularArrival(String arrivalTime) {
return regularStopTime(TimeUtils.time(arrivalTime), MISSING_VALUE);
}

public static StopTime regularStopTime(String arrivalTime, String departureTime) {
return regularStopTime(TimeUtils.time(arrivalTime), TimeUtils.time(departureTime));
}

public static StopTime regularStopTime(int arrivalTime, int departureTime) {
var stopTime = new StopTime();
stopTime.setStop(REGULAR_STOP);
stopTime.setArrivalTime(arrivalTime);
stopTime.setDepartureTime(departureTime);
return stopTime;
}

public static StopTime regularDeparture(String departureTime) {
return regularStopTime(MISSING_VALUE, TimeUtils.time(departureTime));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.opentripplanner.ext.flex.flexpathcalculator;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.time.Duration;
import java.util.List;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.opentripplanner._support.geometry.LineStrings;
import org.opentripplanner.routing.api.request.framework.TimePenalty;

class FlexPathTest {

private static final int THIRTY_MINS_IN_SECONDS = (int) Duration.ofMinutes(30).toSeconds();
private static final FlexPath PATH = new FlexPath(
10_000,
THIRTY_MINS_IN_SECONDS,
() -> LineStrings.SIMPLE
);

static List<Arguments> cases() {
return List.of(
Arguments.of(TimePenalty.ZERO, THIRTY_MINS_IN_SECONDS),
Arguments.of(TimePenalty.of(Duration.ofMinutes(10), 1), 2400),
Arguments.of(TimePenalty.of(Duration.ofMinutes(10), 1.5f), 3300),
Arguments.of(TimePenalty.of(Duration.ZERO, 3), 5400)
);
}

@ParameterizedTest
@MethodSource("cases")
void calculate(TimePenalty mod, int expectedSeconds) {
var modified = PATH.withDurationModifier(mod);
assertEquals(expectedSeconds, modified.durationSeconds);
assertEquals(LineStrings.SIMPLE, modified.getGeometry());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.opentripplanner.ext.flex.flexpathcalculator;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.opentripplanner.ext.flex.FlexStopTimesForTest.area;
import static org.opentripplanner.ext.flex.FlexStopTimesForTest.regularStopTime;
import static org.opentripplanner.street.model._data.StreetModelForTest.V1;
import static org.opentripplanner.street.model._data.StreetModelForTest.V2;
import static org.opentripplanner.transit.model._data.TransitModelForTest.id;

import java.time.Duration;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.opentripplanner._support.geometry.LineStrings;
import org.opentripplanner.ext.flex.trip.ScheduledDeviatedTrip;

class ScheduledFlexPathCalculatorTest {

private static final ScheduledDeviatedTrip TRIP = ScheduledDeviatedTrip
.of(id("123"))
.withStopTimes(
List.of(
regularStopTime("10:00", "10:01"),
area("10:10", "10:20"),
regularStopTime("10:25", "10:26"),
area("10:40", "10:50")
)
)
.build();

@Test
void calculateTime() {
var c = (FlexPathCalculator) (fromv, tov, fromStopIndex, toStopIndex) ->
new FlexPath(10_000, (int) Duration.ofMinutes(10).toSeconds(), () -> LineStrings.SIMPLE);
var calc = new ScheduledFlexPathCalculator(c, TRIP);
var path = calc.calculateFlexPath(V1, V2, 0, 1);
assertEquals(Duration.ofMinutes(19), Duration.ofSeconds(path.durationSeconds));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.opentripplanner.ext.flex.flexpathcalculator;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.opentripplanner._support.geometry.LineStrings;
import org.opentripplanner.routing.api.request.framework.TimePenalty;
import org.opentripplanner.street.model._data.StreetModelForTest;

class TimePenaltyCalculatorTest {

private static final int THIRTY_MINS_IN_SECONDS = (int) Duration.ofMinutes(30).toSeconds();

@Test
void calculate() {
FlexPathCalculator delegate = (fromv, tov, fromStopIndex, toStopIndex) ->
new FlexPath(10_000, THIRTY_MINS_IN_SECONDS, () -> LineStrings.SIMPLE);

var mod = TimePenalty.of(Duration.ofMinutes(10), 1.5f);
var calc = new TimePenaltyCalculator(delegate, mod);
var path = calc.calculateFlexPath(StreetModelForTest.V1, StreetModelForTest.V2, 0, 5);
assertEquals(3300, path.durationSeconds);
}

@Test
void nullValue() {
FlexPathCalculator delegate = (fromv, tov, fromStopIndex, toStopIndex) -> null;
var mod = TimePenalty.of(Duration.ofMinutes(10), 1.5f);
var calc = new TimePenaltyCalculator(delegate, mod);
var path = calc.calculateFlexPath(StreetModelForTest.V1, StreetModelForTest.V2, 0, 5);
assertNull(path);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.opentripplanner.ext.flex.trip;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.opentripplanner.street.model._data.StreetModelForTest.V1;
import static org.opentripplanner.street.model._data.StreetModelForTest.V2;
import static org.opentripplanner.transit.model._data.TransitModelForTest.id;

import java.time.Duration;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.opentripplanner._support.geometry.LineStrings;
import org.opentripplanner.ext.flex.FlexStopTimesForTest;
import org.opentripplanner.ext.flex.flexpathcalculator.FlexPath;
import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator;
import org.opentripplanner.model.StopTime;
import org.opentripplanner.routing.api.request.framework.TimePenalty;

class UnscheduledDrivingDurationTest {

static final FlexPathCalculator STATIC_CALCULATOR = (fromv, tov, fromStopIndex, toStopIndex) ->
new FlexPath(10_000, (int) Duration.ofMinutes(10).toSeconds(), () -> LineStrings.SIMPLE);
private static final StopTime STOP_TIME = FlexStopTimesForTest.area("10:00", "18:00");

@Test
void noPenalty() {
var trip = UnscheduledTrip.of(id("1")).withStopTimes(List.of(STOP_TIME)).build();

var calculator = trip.flexPathCalculator(STATIC_CALCULATOR);
var path = calculator.calculateFlexPath(V1, V2, 0, 0);
assertEquals(600, path.durationSeconds);
}

@Test
void withPenalty() {
var trip = UnscheduledTrip
.of(id("1"))
.withStopTimes(List.of(STOP_TIME))
.withTimePenalty(TimePenalty.of(Duration.ofMinutes(2), 1.5f))
.build();

var calculator = trip.flexPathCalculator(STATIC_CALCULATOR);
var path = calculator.calculateFlexPath(V1, V2, 0, 0);
assertEquals(1020, path.durationSeconds);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.opentripplanner.ext.flex.FlexStopTimesForTest.area;
import static org.opentripplanner.ext.flex.FlexStopTimesForTest.regularArrival;
import static org.opentripplanner.ext.flex.FlexStopTimesForTest.regularDeparture;
import static org.opentripplanner.ext.flex.trip.UnscheduledTrip.isUnscheduledTrip;
import static org.opentripplanner.ext.flex.trip.UnscheduledTripTest.TestCase.tc;
import static org.opentripplanner.model.PickDrop.NONE;
Expand Down Expand Up @@ -48,11 +51,10 @@ class UnscheduledTripTest {
private static final int T15_00 = TimeUtils.hm2time(15, 0);

private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of();
private static final StopLocation AREA_STOP = TEST_MODEL.areaStopForTest("area", Polygons.BERLIN);

private static final RegularStop REGULAR_STOP = TEST_MODEL.stop("stop").build();

private static final StopLocation AREA_STOP = TEST_MODEL.areaStopForTest("area", Polygons.BERLIN);

@Nested
class IsUnscheduledTrip {

Expand Down Expand Up @@ -661,35 +663,6 @@ private static String timeToString(int time) {
return TimeUtils.timeToStrCompact(time, MISSING_VALUE, "MISSING_VALUE");
}

private static StopTime area(String startTime, String endTime) {
return area(AREA_STOP, endTime, startTime);
}

@Nonnull
private static StopTime area(StopLocation areaStop, String endTime, String startTime) {
var stopTime = new StopTime();
stopTime.setStop(areaStop);
stopTime.setFlexWindowStart(TimeUtils.time(startTime));
stopTime.setFlexWindowEnd(TimeUtils.time(endTime));
return stopTime;
}

private static StopTime regularDeparture(String departureTime) {
return regularStopTime(MISSING_VALUE, TimeUtils.time(departureTime));
}

private static StopTime regularArrival(String arrivalTime) {
return regularStopTime(TimeUtils.time(arrivalTime), MISSING_VALUE);
}

private static StopTime regularStopTime(int arrivalTime, int departureTime) {
var stopTime = new StopTime();
stopTime.setStop(REGULAR_STOP);
stopTime.setArrivalTime(arrivalTime);
stopTime.setDepartureTime(departureTime);
return stopTime;
}

@Nonnull
private static NearbyStop nearbyStop(AreaStop stop) {
return new NearbyStop(stop, 1000, List.of(), null);
Expand Down
13 changes: 9 additions & 4 deletions src/ext/java/org/opentripplanner/ext/flex/FlexTripsMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.opentripplanner.model.StopTime;
import org.opentripplanner.model.TripStopTimes;
import org.opentripplanner.model.impl.OtpTransitServiceBuilder;
import org.opentripplanner.routing.api.request.framework.TimePenalty;
import org.opentripplanner.transit.model.timetable.Trip;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -32,12 +33,16 @@ public class FlexTripsMapper {
ProgressTracker progress = ProgressTracker.track("Create flex trips", 500, tripSize);

for (Trip trip : stopTimesByTrip.keys()) {
/* Fetch the stop times for this trip. Copy the list since it's immutable. */
List<StopTime> stopTimes = new ArrayList<>(stopTimesByTrip.get(trip));

var stopTimes = stopTimesByTrip.get(trip);
if (UnscheduledTrip.isUnscheduledTrip(stopTimes)) {
var timePenalty = builder.getFlexTimePenalty().getOrDefault(trip, TimePenalty.NONE);
result.add(
UnscheduledTrip.of(trip.getId()).withTrip(trip).withStopTimes(stopTimes).build()
UnscheduledTrip
.of(trip.getId())
.withTrip(trip)
.withStopTimes(stopTimes)
.withTimePenalty(timePenalty)
.build()
);
} else if (ScheduledDeviatedTrip.isScheduledFlexTrip(stopTimes)) {
result.add(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ public int getGeneralizedCost() {
return generalizedCost;
}

@Override
public void addAlert(TransitAlert alert) {
transitAlerts.add(alert);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package org.opentripplanner.ext.flex.flexpathcalculator;

import java.time.Duration;
import java.util.function.Supplier;
import javax.annotation.concurrent.Immutable;
import org.locationtech.jts.geom.LineString;
import org.opentripplanner.framework.lang.IntUtils;
import org.opentripplanner.routing.api.request.framework.TimePenalty;

/**
* This class contains the results from a FlexPathCalculator.
*/
@Immutable
public class FlexPath {

private final Supplier<LineString> geometrySupplier;
Expand All @@ -22,7 +27,7 @@ public class FlexPath {
*/
public FlexPath(int distanceMeters, int durationSeconds, Supplier<LineString> geometrySupplier) {
this.distanceMeters = distanceMeters;
this.durationSeconds = durationSeconds;
this.durationSeconds = IntUtils.requireNotNegative(durationSeconds);
this.geometrySupplier = geometrySupplier;
}

Expand All @@ -32,4 +37,16 @@ public LineString getGeometry() {
}
return geometry;
}

/**
* Returns an (immutable) copy of this path with the duration modified.
*/
public FlexPath withDurationModifier(TimePenalty mod) {
if (mod.isZero()) {
return this;
} else {
int updatedDuration = (int) mod.calculate(Duration.ofSeconds(durationSeconds)).toSeconds();
return new FlexPath(distanceMeters, updatedDuration, geometrySupplier);
}
}
}
Loading

0 comments on commit ef0d16b

Please sign in to comment.