diff --git a/src/de/schildbach/pte/MvvProvider.java b/src/de/schildbach/pte/MvvProvider.java index 67a208d32..0a01f6adb 100644 --- a/src/de/schildbach/pte/MvvProvider.java +++ b/src/de/schildbach/pte/MvvProvider.java @@ -17,7 +17,12 @@ package de.schildbach.pte; +import java.io.IOException; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -27,19 +32,24 @@ import com.google.common.base.Charsets; import de.schildbach.pte.dto.Line; +import de.schildbach.pte.dto.Location; import de.schildbach.pte.dto.Point; import de.schildbach.pte.dto.Position; import de.schildbach.pte.dto.Product; +import de.schildbach.pte.dto.QueryTripsContext; +import de.schildbach.pte.dto.QueryTripsResult; import de.schildbach.pte.dto.Style; import de.schildbach.pte.dto.Style.Shape; +import de.schildbach.pte.dto.Trip; +import de.schildbach.pte.dto.TripOptions; import okhttp3.HttpUrl; /** * @author Andreas Schildbach */ public class MvvProvider extends AbstractEfaProvider { - private static final HttpUrl API_BASE = HttpUrl.parse("https://efa.mvv-muenchen.de/mobile/"); + private static final HttpUrl API_BASE = HttpUrl.parse("https://efa.mvv-muenchen.de/ng/"); public MvvProvider() { this(API_BASE); @@ -50,7 +60,7 @@ public MvvProvider(final HttpUrl apiBase) { setIncludeRegionId(false); setRequestUrlEncoding(Charsets.UTF_8); setStyles(STYLES); - setSessionCookieName("SIDefaalt"); // SIDefa + setSessionCookieName("SIDefa"); } @Override @@ -157,4 +167,117 @@ protected Position parsePosition(final String position) { public Point[] getArea() { return new Point[] { Point.fromDouble(48.140377, 11.560643) }; } + + /* + MVV's new EFA uses load balancing. Therefore, stateful API functionality only works + correctly if we coincidentally hit the same server again. The session ID cookie does include + the server ID that the session was created on, but apparently the load balancer does not + respect this. + + There were attempts to ask MVV to fix this issue, but they did not offer any help: + https://github.com/schildbach/public-transport-enabler/pull/414#issuecomment-954032588 + + Thus, we implement queryMoreTrips in a stateless manner by adjusting + the departure/arrival times ourselves. This is the same algorithm that is + also used in the Javascript code on the mobile MVV website at + https://m.mvv-muenchen.de/mvvMobile5/de/index.html#trips + */ + + private static class MvvContext implements QueryTripsContext { + public Location from; + public Location via; + public Location to; + public TripOptions options; + public QueryTripsResult result; + public List trips; + + @Override + public boolean canQueryLater() { + return true; + } + + @Override + public boolean canQueryEarlier() { + return true; + } + } + + private boolean requestingMoreTrips; + + @Override + public QueryTripsResult queryTrips(final Location from, final @Nullable Location via, final Location to, + final Date date, final boolean dep, final @Nullable TripOptions options) throws IOException { + QueryTripsResult result = super.queryTrips(from, via, to, date, dep, options); + + if (result.status == QueryTripsResult.Status.OK) { + MvvContext context = new MvvContext(); + context.from = from; + context.to = to; + context.via = via; + context.options = options; + context.trips = result.trips; + result = new QueryTripsResult(result.header, result.queryUri, result.from, + result.via, result.to, context, result.trips); + } + return result; + } + + @Override + public QueryTripsResult queryMoreTrips(final QueryTripsContext contextObj, final boolean later) throws IOException { + if (!(contextObj instanceof MvvContext)) { + throw new IllegalArgumentException("needs an MvvContext"); + } + MvvContext context = (MvvContext) contextObj; + + // get departure time of last trip / arrival time of last trip as reference time + int tripIndex; + if (later) { + Trip lastTrip = context.trips.get(context.trips.size() - 1); + // if the last included trip is a walking route, use the previous one + boolean lastTripIsIndividual = + lastTrip.legs.size() == 1 && lastTrip.legs.get(0) instanceof Trip.Individual; + tripIndex = context.trips.size() - (lastTripIsIndividual ? 2 : 1); + } else { + tripIndex = 0; + } + Trip refTrip = context.trips.get(tripIndex); + Date refTime = later ? refTrip.getFirstDepartureTime() : refTrip.getLastArrivalTime(); + + // adjust time by one minute so that we don't get the same trip again + refTime = addMinutesToDate(refTime, later ? 1 : -1); + + requestingMoreTrips = true; // set special options for more trips request + try { + QueryTripsResult result = super.queryTrips(context.from, context.via, context.to, + refTime, later, context.options); + + if (result.status == QueryTripsResult.Status.OK) { + context.trips.addAll(later ? context.trips.size() - 1 : 0, result.trips); + result = new QueryTripsResult(result.header, result.queryUri, result.from, + result.via, result.to, context, result.trips); + } + + return result; + } finally { + requestingMoreTrips = false; // reset options + } + } + + @Override + protected void appendTripRequestParameters(HttpUrl.Builder url, Location from, @Nullable Location via, Location to, Date time, boolean dep, @Nullable TripOptions options) { + super.appendTripRequestParameters(url, from, via, to, time, dep, options); + + if (requestingMoreTrips) { + // ensure that the first displayed trip is after the given departure time / + // last displayed trip is before the given arrival time + url.addEncodedQueryParameter("calcOneDirection", "1"); + } + } + + private Date addMinutesToDate(Date initial, int minutes) { + Calendar c = new GregorianCalendar(timeZone); + c.setTime(initial); + c.add(Calendar.MINUTE, minutes); + return c.getTime(); + } } diff --git a/test/de/schildbach/pte/live/MvvProviderLiveTest.java b/test/de/schildbach/pte/live/MvvProviderLiveTest.java index ce53c10f9..bb198b2f0 100644 --- a/test/de/schildbach/pte/live/MvvProviderLiveTest.java +++ b/test/de/schildbach/pte/live/MvvProviderLiveTest.java @@ -17,6 +17,7 @@ package de.schildbach.pte.live; +import static org.hamcrest.CoreMatchers.anyOf; import static org.hamcrest.CoreMatchers.hasItem; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; @@ -46,7 +47,8 @@ public MvvProviderLiveTest() { @Test public void nearbyStations() throws Exception { - final NearbyLocationsResult result = queryNearbyStations(new Location(LocationType.STATION, "350")); + final NearbyLocationsResult result = + queryNearbyStations(new Location(LocationType.STATION, "91000350")); print(result); } @@ -68,13 +70,9 @@ public void nearbyLocationsByCoordinate() throws Exception { @Test public void queryDeparturesMarienplatz() throws Exception { - final QueryDeparturesResult result1 = queryDepartures("2", false); + final QueryDeparturesResult result1 = queryDepartures("91000002", false); assertEquals(QueryDeparturesResult.Status.OK, result1.status); print(result1); - - final QueryDeparturesResult result2 = queryDepartures("1000002", false); - assertEquals(QueryDeparturesResult.Status.OK, result2.status); - print(result2); } @Test @@ -99,14 +97,14 @@ public void suggestLocationsIncomplete() throws Exception { public void suggestLocationsWithUmlaut() throws Exception { final SuggestLocationsResult result = suggestLocations("Grüntal"); print(result); - assertThat(result.getLocations(), hasItem(new Location(LocationType.STATION, "1000619"))); + assertThat(result.getLocations(), hasItem(new Location(LocationType.STATION, "91000619"))); } @Test public void suggestLocationsFraunhofer() throws Exception { final SuggestLocationsResult result = suggestLocations("fraunhofer"); print(result); - assertThat(result.getLocations(), hasItem(new Location(LocationType.STATION, "1000150"))); + assertThat(result.getLocations(), hasItem(new Location(LocationType.STATION, "91000150"))); } @Test @@ -134,22 +132,30 @@ public void suggestLocationsMarienplatz() throws Exception { public void suggestAddress() throws Exception { final SuggestLocationsResult result = suggestLocations("München, Maximilianstr. 1"); print(result); - assertThat(result.getLocations(), hasItem(new Location(LocationType.ADDRESS, - "streetID:3239:1:9162000:9162000:Maximilianstraße:München:Maximilianstraße::Maximilianstraße:80539:ANY:DIVA_ADDRESS:4468763:826437:MVTT:MVV"))); + assertThat(result.getLocations(), anyOf( + hasItem(new Location(LocationType.ADDRESS, + "streetID:1500000040::9162000:-1:Maximilianstraße:München:Maximilianstraße::Maximilianstraße: 80539 80538:ANY:DIVA_STREET:1289423:5870091:MRCV:BAY")), + hasItem(new Location(LocationType.ADDRESS, + "streetID:1500000040::9162000:-1:Maximilianstraße:München:Maximilianstraße::Maximilianstraße: 80539 80538:ANY:DIVA_STREET:1289423:5870091:MRCV:bay")) + )); } @Test public void suggestStreet() throws Exception { final SuggestLocationsResult result = suggestLocations("München, Maximilianstr."); print(result); - assertThat(result.getLocations(), hasItem(new Location(LocationType.ADDRESS, - "streetID:3239::9162000:-1:Maximilianstraße:München:Maximilianstraße::Maximilianstraße: 80539 80538:ANY:DIVA_STREET:4469138:826553:MVTT:MVV"))); + assertThat(result.getLocations(), anyOf( + hasItem(new Location(LocationType.ADDRESS, + "streetID:1500000040::9162000:-1:Maximilianstraße:München:Maximilianstraße::Maximilianstraße: 80539 80538:ANY:DIVA_STREET:1289423:5870091:MRCV:BAY")), + hasItem(new Location(LocationType.ADDRESS, + "streetID:1500000040::9162000:-1:Maximilianstraße:München:Maximilianstraße::Maximilianstraße: 80539 80538:ANY:DIVA_STREET:1289423:5870091:MRCV:bay")) + )); } @Test public void shortTrip() throws Exception { - final Location from = new Location(LocationType.STATION, "2", "München", "Marienplatz"); - final Location to = new Location(LocationType.STATION, "10", "München", "Pasing"); + final Location from = new Location(LocationType.STATION, "91000002", "München", "Marienplatz"); + final Location to = new Location(LocationType.STATION, "91000010", "München", "Pasing"); final QueryTripsResult result = queryTrips(from, null, to, new Date(), true, null); print(result); final QueryTripsResult laterResult = queryMoreTrips(result.context, true); @@ -162,7 +168,7 @@ public void shortTrip() throws Exception { public void longTrip() throws Exception { final Location from = new Location(LocationType.STATION, "1005530", Point.from1E6(48002924, 11340144), "Starnberg", "Agentur für Arbeit"); - final Location to = new Location(LocationType.STATION, null, null, "Ackermannstraße"); + final Location to = new Location(LocationType.STATION, "91000309", null, "Ackermannstraße"); final QueryTripsResult result = queryTrips(from, null, to, new Date(), true, null); print(result); } @@ -180,7 +186,7 @@ public void tripBetweenCoordinates() throws Exception { @Test public void tripBetweenCoordinateAndStation() throws Exception { final Location from = new Location(LocationType.ADDRESS, null, Point.from1E6(48238341, 11478230)); - final Location to = new Location(LocationType.ANY, null, null, "Ostbahnhof"); + final Location to = new Location(LocationType.ANY, null, null, "München, Ostbahnhof"); final QueryTripsResult result = queryTrips(from, null, to, new Date(), true, null); print(result); final QueryTripsResult laterResult = queryMoreTrips(result.context, true); @@ -217,7 +223,7 @@ public void tripBetweenStreets() throws Exception { @Test public void tripBetweenStationAndAddress() throws Exception { - final Location from = new Location(LocationType.STATION, "1220", null, "Josephsburg"); + final Location from = new Location(LocationType.STATION, "91001220", null, "Josephsburg"); final Location to = new Location(LocationType.ADDRESS, null, Point.from1E6(48188018, 11574239), null, "München Frankfurter Ring 35"); final QueryTripsResult result = queryTrips(from, null, to, new Date(), true, null); @@ -228,7 +234,7 @@ public void tripBetweenStationAndAddress() throws Exception { @Test public void tripInvalidStation() throws Exception { - final Location valid = new Location(LocationType.STATION, "2", "München", "Marienplatz"); + final Location valid = new Location(LocationType.STATION, "91000002", "München", "Marienplatz"); final Location invalid = new Location(LocationType.STATION, "99999", null, null); final QueryTripsResult result1 = queryTrips(valid, null, invalid, new Date(), true, null); assertEquals(QueryTripsResult.Status.UNKNOWN_TO, result1.status);