Skip to content

Commit

Permalink
Allow combination of flex and continuous stopping under certain condi…
Browse files Browse the repository at this point in the history
…tions
  • Loading branch information
leonardehrenfried committed Nov 2, 2024
1 parent 94e07bc commit 021811a
Show file tree
Hide file tree
Showing 9 changed files with 375 additions and 271 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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.PickDrop;
import org.opentripplanner.model.StopTime;
import org.opentripplanner.transit.model._data.TimetableRepositoryForTest;
import org.opentripplanner.transit.model.site.RegularStop;
import org.opentripplanner.transit.model.site.StopLocation;
import org.opentripplanner.transit.model.timetable.Trip;

public class FlexStopTimesForTest {

Expand All @@ -18,6 +18,8 @@ public class FlexStopTimesForTest {
.build();
private static final RegularStop REGULAR_STOP = TEST_MODEL.stop("stop").build();

private static final Trip TRIP = TimetableRepositoryForTest.trip("flex").build();

public static StopTime area(String startTime, String endTime) {
return area(AREA_STOP, endTime, startTime);
}
Expand All @@ -27,26 +29,42 @@ public static StopTime area(StopLocation areaStop, String endTime, String startT
stopTime.setStop(areaStop);
stopTime.setFlexWindowStart(TimeUtils.time(startTime));
stopTime.setFlexWindowEnd(TimeUtils.time(endTime));
stopTime.setTrip(TRIP);
return stopTime;
}

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

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

public static StopTime regularStopTime(int arrivalTime, int departureTime) {
public static StopTime regularStopWithContinuousStopping(String time) {
var st = regularStop(TimeUtils.time(time), TimeUtils.time(time));
st.setFlexContinuousPickup(PickDrop.COORDINATE_WITH_DRIVER);
st.setFlexContinuousDropOff(PickDrop.COORDINATE_WITH_DRIVER);
return st;
}

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

public static StopTime regularDeparture(String departureTime) {
return regularStopTime(MISSING_VALUE, TimeUtils.time(departureTime));
/**
* Returns an invalid combination of a flex area and continuous stopping.
*/
public static StopTime areaWithContinuousStopping(String time) {
var st = area(time, time);
st.setFlexContinuousPickup(PickDrop.COORDINATE_WITH_DRIVER);
st.setFlexContinuousDropOff(PickDrop.COORDINATE_WITH_DRIVER);
return st;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

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.TimetableRepositoryForTest.id;
Expand All @@ -11,6 +10,7 @@
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.trip.ScheduledDeviatedTrip;

class ScheduledFlexPathCalculatorTest {
Expand All @@ -19,9 +19,9 @@ class ScheduledFlexPathCalculatorTest {
.of(id("123"))
.withStopTimes(
List.of(
regularStopTime("10:00", "10:01"),
FlexStopTimesForTest.regularStop("10:00", "10:01"),
area("10:10", "10:20"),
regularStopTime("10:25", "10:26"),
FlexStopTimesForTest.regularStop("10:25", "10:26"),
area("10:40", "10:50")
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
package org.opentripplanner.ext.flex.trip;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.opentripplanner.test.support.PolylineAssert.assertThatPolylinesAreEqual;

import java.time.LocalDateTime;
import java.time.Month;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.locationtech.jts.geom.Coordinate;
import org.opentripplanner.TestOtpModel;
import org.opentripplanner.TestServerContext;
import org.opentripplanner._support.time.ZoneIds;
import org.opentripplanner.ext.fares.DecorateWithFare;
import org.opentripplanner.ext.flex.FlexIntegrationTestData;
import org.opentripplanner.ext.flex.FlexParameters;
import org.opentripplanner.ext.flex.FlexRouter;
import org.opentripplanner.framework.application.OTPFeature;
import org.opentripplanner.framework.geometry.EncodedPolyline;
import org.opentripplanner.framework.i18n.I18NString;
import org.opentripplanner.framework.time.ServiceDateUtils;
import org.opentripplanner.graph_builder.module.ValidateAndInterpolateStopTimesForEachTrip;
import org.opentripplanner.model.GenericLocation;
import org.opentripplanner.model.StopTime;
import org.opentripplanner.model.plan.Itinerary;
import org.opentripplanner.routing.algorithm.raptoradapter.router.AdditionalSearchDays;
import org.opentripplanner.routing.algorithm.raptoradapter.router.TransitRouter;
import org.opentripplanner.routing.api.request.RouteRequest;
import org.opentripplanner.routing.api.request.request.filter.AllowAllTransitFilter;
import org.opentripplanner.routing.framework.DebugTimingAggregator;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.graphfinder.NearbyStop;
import org.opentripplanner.standalone.api.OtpServerRequestContext;
import org.opentripplanner.street.model.vertex.StreetLocation;
import org.opentripplanner.street.search.request.StreetSearchRequest;
import org.opentripplanner.street.search.state.State;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService;
import org.opentripplanner.transit.model.site.AreaStop;
import org.opentripplanner.transit.service.DefaultTransitService;
import org.opentripplanner.transit.service.TimetableRepository;

/**
* This tests that the feed for the Cobb County Flex service is processed correctly. This service
* contains both flex zones but also scheduled stops. Inside the zone, passengers can get on or off
* anywhere, so there it works more like a taxi.
* <p>
* Read about the details at: https://www.cobbcounty.org/transportation/cobblinc/routes-and-schedules/flex
*/
class ScheduledDeviatedTripIntegrationTest {

static Graph graph;
static TimetableRepository timetableRepository;

float delta = 0.01f;

@Test
void parseCobbCountyAsScheduledDeviatedTrip() {
var flexTrips = timetableRepository.getAllFlexTrips();
assertFalse(flexTrips.isEmpty());
assertEquals(72, flexTrips.size());

assertEquals(
Set.of(ScheduledDeviatedTrip.class),
flexTrips.stream().map(FlexTrip::getClass).collect(Collectors.toSet())
);

var trip = getFlexTrip();
var stop = trip
.getStops()
.stream()
.filter(s -> s.getId().getId().equals("cujv"))
.findFirst()
.orElseThrow();
assertEquals(33.85465, stop.getLat(), delta);
assertEquals(-84.60039, stop.getLon(), delta);

var flexZone = trip
.getStops()
.stream()
.filter(s -> s.getId().getId().equals("zone_3"))
.findFirst()
.orElseThrow();
assertEquals(33.825846635310214, flexZone.getLat(), delta);
assertEquals(-84.63430143459385, flexZone.getLon(), delta);
}

@Test
void calculateDirectFare() {
OTPFeature.enableFeatures(Map.of(OTPFeature.FlexRouting, true));
var trip = getFlexTrip();

var from = getNearbyStop(trip, "from-stop");
var to = getNearbyStop(trip, "to-stop");

var router = new FlexRouter(
graph,
new DefaultTransitService(timetableRepository),
FlexParameters.defaultValues(),
OffsetDateTime.parse("2021-11-12T10:15:24-05:00").toInstant(),
null,
1,
1,
List.of(from),
List.of(to)
);

var filter = new DecorateWithFare(graph.getFareService());

var itineraries = router
.createFlexOnlyItineraries(false)
.stream()
.peek(filter::decorate)
.toList();

var itinerary = itineraries.getFirst();

assertFalse(itinerary.getFares().getLegProducts().isEmpty());

OTPFeature.enableFeatures(Map.of(OTPFeature.FlexRouting, false));
}

/**
* Trips which consist of flex and fixed-schedule stops should work in transit mode.
* <p>
* The flex stops will show up as intermediate stops (without a departure/arrival time) but you
* cannot board or alight.
*/
@Test
void flexTripInTransitMode() {
var feedId = timetableRepository.getFeedIds().iterator().next();

var serverContext = TestServerContext.createServerContext(graph, timetableRepository);

// from zone 3 to zone 2
var from = GenericLocation.fromStopId("Transfer Point for Route 30", feedId, "cujv");
var to = GenericLocation.fromStopId(
"Zone 1 - PUBLIX Super Market,Zone 1 Collection Point",
feedId,
"yz85"
);

var itineraries = getItineraries(from, to, serverContext);

assertEquals(2, itineraries.size());

var itin = itineraries.get(0);
var leg = itin.getLegs().get(0);

assertEquals("cujv", leg.getFrom().stop.getId().getId());
assertEquals("yz85", leg.getTo().stop.getId().getId());

var intermediateStops = leg.getIntermediateStops();
assertEquals(1, intermediateStops.size());
assertEquals("zone_1", intermediateStops.get(0).place.stop.getId().getId());

EncodedPolyline legGeometry = EncodedPolyline.encode(leg.getLegGeometry());
assertThatPolylinesAreEqual(
legGeometry.points(),
"kfsmEjojcOa@eBRKfBfHR|ALjBBhVArMG|OCrEGx@OhAKj@a@tAe@hA]l@MPgAnAgw@nr@cDxCm@t@c@t@c@x@_@~@]pAyAdIoAhG}@lE{AzHWhAtt@t~Aj@tAb@~AXdBHn@FlBC`CKnA_@nC{CjOa@dCOlAEz@E|BRtUCbCQ~CWjD??qBvXBl@kBvWOzAc@dDOx@sHv]aIG?q@@c@ZaB\\mA"
);
}

/**
* We add flex trips, that can potentially not have a departure and arrival time, to the trip.
* <p>
* Normally these trip times are interpolated/repaired during the graph build but for flex this is
* exactly what we don't want. Here we check that the interpolation process is skipped.
*
* @see ValidateAndInterpolateStopTimesForEachTrip#interpolateStopTimes(List)
*/
@Test
void shouldNotInterpolateFlexTimes() {
var feedId = timetableRepository.getFeedIds().iterator().next();
var pattern = timetableRepository.getTripPatternForId(new FeedScopedId(feedId, "090z:0:01"));

assertEquals(3, pattern.numberOfStops());

var tripTimes = pattern.getScheduledTimetable().getTripTimes(0);
var arrivalTime = tripTimes.getArrivalTime(1);

assertEquals(StopTime.MISSING_VALUE, arrivalTime);
}

@BeforeAll
static void setup() {
TestOtpModel model = FlexIntegrationTestData.cobbFlexGtfs();
graph = model.graph();
timetableRepository = model.timetableRepository();
}

private static List<Itinerary> getItineraries(
GenericLocation from,
GenericLocation to,
OtpServerRequestContext serverContext
) {
var zoneId = ZoneIds.NEW_YORK;
RouteRequest request = new RouteRequest();
request.journey().transit().setFilters(List.of(AllowAllTransitFilter.of()));
var dateTime = LocalDateTime.of(2021, Month.DECEMBER, 16, 12, 0).atZone(zoneId);
request.setDateTime(dateTime.toInstant());
request.setFrom(from);
request.setTo(to);

var transitStartOfTime = ServiceDateUtils.asStartOfService(request.dateTime(), zoneId);
var additionalSearchDays = AdditionalSearchDays.defaults(dateTime);
var result = TransitRouter.route(
request,
serverContext,
TransitGroupPriorityService.empty(),
transitStartOfTime,
additionalSearchDays,
new DebugTimingAggregator()
);

return result.getItineraries();
}

private static NearbyStop getNearbyStop(FlexTrip<?, ?> trip, String id) {
// getStops() returns a set of stops and the order doesn't correspond to the stop times
// of the trip
var stopLocation = trip
.getStops()
.stream()
.filter(s -> s instanceof AreaStop)
.findFirst()
.orElseThrow();

return new NearbyStop(
stopLocation,
0,
List.of(),
new State(
new StreetLocation(id, new Coordinate(0, 0), I18NString.of(id)),
StreetSearchRequest.of().build()
)
);
}

private static FlexTrip<?, ?> getFlexTrip() {
var feedId = timetableRepository.getFeedIds().iterator().next();
var tripId = new FeedScopedId(feedId, "a326c618-d42c-4bd1-9624-c314fbf8ecd8");
return timetableRepository.getFlexTrip(tripId);
}
}
Loading

0 comments on commit 021811a

Please sign in to comment.