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 20 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,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 DurationModifierCalculatorTest {

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 DurationModifierCalculator(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 DurationModifierCalculator(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,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,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 noModifier() {
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 withModifier() {
var trip = UnscheduledTrip
.of(id("1"))
.withStopTimes(List.of(STOP_TIME))
.withDurationModifier(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
10 changes: 8 additions & 2 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 @@ -34,10 +35,15 @@ public class FlexTripsMapper {
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));

if (UnscheduledTrip.isUnscheduledTrip(stopTimes)) {
var modifier = builder.getFlexDurationFactors().getOrDefault(trip, TimePenalty.ZERO);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we stick to call to calling them modifiers instead of factors? Or is there some difference?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, sorry, I did several renames of this field and made a mess. Lets discuss in the dev meeting what this field should be called.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets use timePenalty here - modifier is too wide. Factor is not correct, it is more than a factor.

result.add(
UnscheduledTrip.of(trip.getId()).withTrip(trip).withStopTimes(stopTimes).build()
UnscheduledTrip
.of(trip.getId())
.withTrip(trip)
.withStopTimes(stopTimes)
.withDurationModifier(modifier)
.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
@@ -0,0 +1,32 @@
package org.opentripplanner.ext.flex.flexpathcalculator;

import javax.annotation.Nullable;
import org.opentripplanner.routing.api.request.framework.TimePenalty;
import org.opentripplanner.street.model.vertex.Vertex;

/**
* A calculator to delegates the main computation to another instance and applies a duration
* modifier afterward.
*/
public class DurationModifierCalculator implements FlexPathCalculator {

private final FlexPathCalculator delegate;
private final TimePenalty factors;

public DurationModifierCalculator(FlexPathCalculator delegate, TimePenalty penalty) {
this.delegate = delegate;
this.factors = penalty;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

}

@Nullable
@Override
public FlexPath calculateFlexPath(Vertex fromv, Vertex tov, int fromStopIndex, int toStopIndex) {
var path = delegate.calculateFlexPath(fromv, tov, fromStopIndex, toStopIndex);

if (path == null) {
return null;
} else {
return path.withDurationModifier(factors);
}
}
}
Loading
Loading