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

Relax rejection of GTFS flex trips that also contain continuous stopping #6231

Open
wants to merge 6 commits into
base: dev-2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package org.opentripplanner.ext.flex;

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

import org.opentripplanner._support.geometry.Polygons;
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;
import org.opentripplanner.utils.time.TimeUtils;

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,74 @@ 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);
/**
* 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;
}

public static StopTime regularStopTime(String arrivalTime, String departureTime) {
return regularStopTime(TimeUtils.time(arrivalTime), TimeUtils.time(departureTime));
/**
* Returns an invalid combination of a flex area and continuous pick up.
*/
public static StopTime areaWithContinuousPickup(String time) {
var st = area(time, time);
st.setFlexContinuousPickup(PickDrop.COORDINATE_WITH_DRIVER);
return st;
}

public static StopTime regularStopTime(int arrivalTime, int departureTime) {
/**
* Returns an invalid combination of a flex area and continuous drop off.
*/
public static StopTime areaWithContinuousDropOff(String time) {
var st = area(time, time);
st.setFlexContinuousDropOff(PickDrop.COORDINATE_WITH_DRIVER);
return st;
}

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

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

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 regularStopWithContinuousPickup(String time) {
var st = regularStop(TimeUtils.time(time), TimeUtils.time(time));
st.setFlexContinuousPickup(PickDrop.COORDINATE_WITH_DRIVER);
return st;
}

public static StopTime regularStopWithContinuousDropOff(String time) {
var st = regularStop(TimeUtils.time(time), TimeUtils.time(time));
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));
}


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

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.ext.flex.FlexStopTimesForTest.regularStop;
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 @@ -19,9 +19,9 @@ class ScheduledFlexPathCalculatorTest {
.of(id("123"))
.withStopTimes(
List.of(
regularStopTime("10:00", "10:01"),
regularStop("10:00", "10:01"),
area("10:10", "10:20"),
regularStopTime("10:25", "10:26"),
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.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;
import org.opentripplanner.utils.time.ServiceDateUtils;

/**
* 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
Loading