Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement GTFS Flex safe duration spec draft #5796

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b1ed4ff
Add flex duration factors and offsets
leonardehrenfried Feb 12, 2024
95527bb
Add initial implementation of flex duration factors
leonardehrenfried Feb 14, 2024
ea0960a
Remove stop-time-based factors
leonardehrenfried Mar 15, 2024
a0eb066
Move factors into calcultor
leonardehrenfried Mar 15, 2024
cc76012
Encapsulate factors, add parsing
leonardehrenfried Mar 15, 2024
dca331b
Make factors serializable
leonardehrenfried Mar 15, 2024
721845e
Use safe instead of mean values
leonardehrenfried Apr 5, 2024
abcfa93
Use correct booking info instances
leonardehrenfried Apr 5, 2024
5066052
Implement duration modifier for ScheduledDeviated trip
leonardehrenfried Apr 5, 2024
90b15cd
Rename and test
leonardehrenfried Apr 6, 2024
41b2e50
Extract class for building flex stop times
leonardehrenfried Apr 7, 2024
dec30cd
Remove durationModifier to ScheduledDeviatedTrip
leonardehrenfried Apr 9, 2024
354c968
Add test and docs for DurationModifier
leonardehrenfried Apr 9, 2024
991406e
Cleanup, tests and documentation
leonardehrenfried Apr 9, 2024
6c7d953
Replace DurationModifier with TimePenalty
leonardehrenfried Apr 18, 2024
165e8cf
Merge remote-tracking branch 'upstream/dev-2.x' into flex-duration-fa…
leonardehrenfried Apr 18, 2024
355b287
Update documentation
leonardehrenfried Apr 18, 2024
90148e0
Update docs about experimental fields
leonardehrenfried May 6, 2024
a4dc25b
Revert renaming
leonardehrenfried May 6, 2024
42b36bf
Merge remote-tracking branch 'upstream/dev-2.x' into flex-duration-fa…
leonardehrenfried May 6, 2024
ea67975
Remove extra collection conversion
leonardehrenfried May 8, 2024
695161a
Rename modifiers/factors to time penalty
leonardehrenfried May 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
leonardehrenfried marked this conversation as resolved.
Show resolved Hide resolved

### 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
Loading