DB: replace HCI-based implementation with new movas provider

This commit is contained in:
Traines 2025-01-09 12:41:13 +00:00 committed by Andreas Schildbach
parent ad5d52dca9
commit 3589b7accc
3 changed files with 780 additions and 55 deletions

View file

@ -17,45 +17,748 @@
package de.schildbach.pte;
import java.io.IOException;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import de.schildbach.pte.dto.Departure;
import de.schildbach.pte.dto.Fare;
import de.schildbach.pte.dto.Line;
import de.schildbach.pte.dto.Location;
import de.schildbach.pte.dto.LocationType;
import de.schildbach.pte.dto.NearbyLocationsResult;
import de.schildbach.pte.dto.Point;
import de.schildbach.pte.dto.Position;
import de.schildbach.pte.dto.Product;
import de.schildbach.pte.dto.QueryDeparturesResult;
import de.schildbach.pte.dto.QueryTripsContext;
import de.schildbach.pte.dto.QueryTripsResult;
import de.schildbach.pte.dto.ResultHeader;
import de.schildbach.pte.dto.StationDepartures;
import de.schildbach.pte.dto.Stop;
import de.schildbach.pte.dto.SuggestLocationsResult;
import de.schildbach.pte.dto.SuggestedLocation;
import de.schildbach.pte.dto.Trip;
import de.schildbach.pte.dto.TripOptions;
import de.schildbach.pte.exception.AbstractHttpException;
import de.schildbach.pte.exception.BlockedException;
import de.schildbach.pte.exception.InternalErrorException;
import de.schildbach.pte.exception.ParserException;
import de.schildbach.pte.util.ParserUtils;
import okhttp3.HttpUrl;
/**
* Provider implementation for Deutsche Bahn (Germany).
* Provider implementation for movas API of Deutsche Bahn (Germany).
*
* @author Andreas Schildbach
*/
public final class DbProvider extends AbstractHafasClientInterfaceProvider {
private static final HttpUrl API_BASE = HttpUrl.parse("https://reiseauskunft.bahn.de/bin/");
private static final Product[] PRODUCTS_MAP = { Product.HIGH_SPEED_TRAIN, // ICE-Züge
Product.HIGH_SPEED_TRAIN, // Intercity- und Eurocityzüge
Product.HIGH_SPEED_TRAIN, // Interregio- und Schnellzüge
Product.REGIONAL_TRAIN, // Nahverkehr, sonstige Züge
Product.SUBURBAN_TRAIN, // S-Bahn
Product.BUS, // Busse
Product.FERRY, // Schiffe
Product.SUBWAY, // U-Bahnen
Product.TRAM, // Straßenbahnen
Product.ON_DEMAND, // Anruf-Sammeltaxi
null, null, null, null };
private static final String DEFAULT_API_CLIENT = "{\"id\":\"DB\",\"v\":\"16040000\",\"type\":\"AND\",\"name\":\"DB Navigator\"}";
public final class DbProvider extends AbstractNetworkProvider {
private final List<Capability> CAPABILITIES = Arrays.asList(
Capability.SUGGEST_LOCATIONS,
Capability.NEARBY_LOCATIONS,
Capability.DEPARTURES,
Capability.TRIPS,
Capability.TRIPS_VIA);
public DbProvider(final String apiAuthorization, final byte[] salt) {
this(DEFAULT_API_CLIENT, apiAuthorization, salt);
private static final HttpUrl API_BASE = HttpUrl.parse("https://app.vendo.noncd.db.de/mob/");
private final ResultHeader resultHeader;
private static final Map<String, Product> PRODUCTS_MAP = new LinkedHashMap<String, Product>() {
{
put("HOCHGESCHWINDIGKEITSZUEGE", Product.HIGH_SPEED_TRAIN);
put("INTERCITYUNDEUROCITYZUEGE", Product.HIGH_SPEED_TRAIN);
put("INTERREGIOUNDSCHNELLZUEGE", Product.HIGH_SPEED_TRAIN);
put("NAHVERKEHRSONSTIGEZUEGE", Product.REGIONAL_TRAIN);
put("SBAHNEN", Product.SUBURBAN_TRAIN);
put("BUSSE", Product.BUS);
put("SCHIFFE", Product.FERRY);
put("UBAHN", Product.SUBWAY);
put("STRASSENBAHN", Product.TRAM);
put("ANRUFPFLICHTIGEVERKEHRE", Product.ON_DEMAND);
}
};
private static final Map<String, Product> SHORT_PRODUCTS_MAP = new LinkedHashMap<String, Product>() {
{
put("ICE", Product.HIGH_SPEED_TRAIN);
put("IC_EC", Product.HIGH_SPEED_TRAIN);
put("IC", Product.HIGH_SPEED_TRAIN);
put("EC", Product.HIGH_SPEED_TRAIN);
put("IR", Product.HIGH_SPEED_TRAIN);
put("RB", Product.REGIONAL_TRAIN);
put("RE", Product.REGIONAL_TRAIN);
put("SBAHN", Product.SUBURBAN_TRAIN);
put("BUS", Product.BUS);
put("SCHIFF", Product.FERRY);
put("UBAHN", Product.SUBWAY);
put("STR", Product.TRAM);
put("ANRUFPFLICHTIGEVERKEHRE", Product.ON_DEMAND);
}
};
private static final Map<String, LocationType> ID_LOCATION_TYPE_MAP = new HashMap<String, LocationType>() {
{
put("1", LocationType.STATION);
put("4", LocationType.POI);
put("2", LocationType.ADDRESS);
}
};
private static final Map<LocationType, String> LOCATION_TYPE_MAP = new HashMap<LocationType, String>() {
{
put(LocationType.ANY, "ALL");
put(LocationType.STATION, "ST");
put(LocationType.POI, "POI");
put(LocationType.ADDRESS, "ADR");
}
};
private static final int DEFAULT_MAX_DEPARTURES = 100;
private static final int DEFAULT_MAX_LOCATIONS = 50;
private static final int DEFAULT_MAX_DISTANCE = 10000;
private final HttpUrl departureEndpoint;
private final HttpUrl tripEndpoint;
private final HttpUrl locationsEndpoint;
private final HttpUrl nearbyEndpoint;
private TimeZone timeZone = TimeZone.getTimeZone("CET");
private static final Pattern P_SPLIT_NAME_FIRST_COMMA = Pattern.compile("([^,]*), (.*)");
private static final Pattern P_SPLIT_NAME_ONE_COMMA = Pattern.compile("([^,]*), ([^,]*)");
public DbProvider() {
super(NetworkId.DB);
this.departureEndpoint = API_BASE.newBuilder().addPathSegments("bahnhofstafel/abfahrt").build();
this.tripEndpoint = API_BASE.newBuilder().addPathSegments("angebote/fahrplan").build();
this.locationsEndpoint = API_BASE.newBuilder().addPathSegments("location/search").build();
this.nearbyEndpoint = API_BASE.newBuilder().addPathSegments("location/nearby").build();
this.resultHeader = new ResultHeader(network, "movas");
}
public DbProvider(final String apiClient, final String apiAuthorization, final byte[] salt) {
super(NetworkId.DB, API_BASE, PRODUCTS_MAP);
setApiVersion("1.15");
setApiExt("DB.R18.06.a");
setApiClient(apiClient);
setApiAuthorization(apiAuthorization);
setRequestChecksumSalt(salt);
private String doRequest(final HttpUrl url, final String body, final String contentType) throws IOException {
// DB API requires these headers
// Content-Type must be exactly as passed below,
// passing it to httpClient.get would add charset suffix
httpClient.setHeader("X-Correlation-ID", "null");
httpClient.setHeader("Accept", contentType);
httpClient.setHeader("Content-Type", contentType);
final CharSequence page = httpClient.get(url, body, null);
return page.toString();
}
private CharSequence formatDate(final Calendar time) {
final int year = time.get(Calendar.YEAR);
final int month = time.get(Calendar.MONTH) + 1;
final int day = time.get(Calendar.DAY_OF_MONTH);
return String.format(Locale.ENGLISH, "%04d-%02d-%02d", year, month, day);
}
private CharSequence formatTime(final Calendar time) {
final int hour = time.get(Calendar.HOUR_OF_DAY);
final int minute = time.get(Calendar.MINUTE);
return String.format(Locale.ENGLISH, "%02d:%02d", hour, minute);
}
private String formatIso8601WOffset(final Date time) {
if (time == null)
return null;
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(time.toInstant().atZone(timeZone.toZoneId()));
}
private Date parseIso8601WOffset(final String time) {
if (time == null)
return null;
return Date.from(Instant.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(time)));
}
private String createLidEntry(final String key, final Object value) {
return key + "=" + value + "@";
}
private String formatLid(final Location loc) {
if (loc.id != null && loc.id.startsWith("A=") && loc.id.contains("@")) {
return loc.id;
}
final String typeId = ID_LOCATION_TYPE_MAP
.entrySet()
.stream()
.filter(e -> e.getValue() == loc.type)
.findFirst()
.map(e -> e.getKey())
.orElse("0");
final StringBuilder out = new StringBuilder();
out.append(createLidEntry("A", typeId));
if (loc.name != null) {
out.append(createLidEntry("O", loc.name));
}
if (loc.coord != null) {
out.append(createLidEntry("X", loc.coord.getLonAs1E6()));
out.append(createLidEntry("Y", loc.coord.getLatAs1E6()));
}
if (loc.id != null) {
out.append(createLidEntry("L", normalizeStationId(loc.id)));
}
return out.toString();
}
private String formatLid(final String stationId) {
return formatLid(new Location(LocationType.STATION, stationId));
}
private Location parseLid(final String loc) {
if (loc == null)
return new Location(LocationType.STATION, null);
final Map<String, String> props = Arrays.stream(loc.split("@"))
.map(chunk -> chunk.split("="))
.filter(e -> e.length == 2)
.collect(Collectors.toMap(e -> e[0], e -> e[1]));
Point coord = null;
try {
coord = Point.from1E6(Integer.parseInt(props.get("Y")), Integer.parseInt(props.get("X")));
} catch (Exception e) {
}
return new Location(
Optional.ofNullable(ID_LOCATION_TYPE_MAP.get(props.get("A"))).orElse(LocationType.ANY),
props.get("L"),
coord,
null,
props.get("O"));
}
private String formatProducts(final Set<Product> products) {
if (products == null)
return "\"ALL\"";
return products.stream()
.flatMap(p -> PRODUCTS_MAP.entrySet().stream().filter(e -> e.getValue() == p))
.map(p -> "\"" + p.getKey() + "\"")
.collect(Collectors.joining(", "));
}
private Set<Product> parseProducts(final JSONArray products) {
if (products == null)
return null;
final Set<Product> out = new HashSet<>();
for (int i = 0; i < products.length(); i++) {
final Product p = PRODUCTS_MAP.get(products.optString(i, null));
if (p != null) {
out.add(p);
}
}
return out;
}
private String formatLocationTypes(Set<LocationType> types) {
if (types == null || types.contains(LocationType.ANY))
return "\"" + LOCATION_TYPE_MAP.get(LocationType.ANY) + "\"";
return types.stream()
.map(t -> LOCATION_TYPE_MAP.get(t))
.filter(t -> t != null)
.map(t -> "\"" + t + "\"")
.collect(Collectors.joining(", "));
}
protected String[] splitPlaceAndName(final String placeAndName, final Pattern p, final int place, final int name) {
if (placeAndName == null)
return new String[] { null, null };
final Matcher m = p.matcher(placeAndName);
if (m.matches())
return new String[] { m.group(place), m.group(name) };
return new String[] { null, placeAndName };
}
protected String[] splitStationName(final String name) {
return splitPlaceAndName(name, P_SPLIT_NAME_ONE_COMMA, 2, 1);
}
protected String[] splitAddress(final String address) {
return splitPlaceAndName(address, P_SPLIT_NAME_FIRST_COMMA, 1, 2);
}
private Location parseLocation(JSONObject loc) {
if (loc == null)
return null;
final String lidStr = loc.optString("locationId", null);
final Location lid = parseLid(lidStr);
final String id = lid.type == LocationType.STATION
? Optional.ofNullable(loc.optString("evaNr", null)).orElse(lid.id)
: lidStr;
Point coord = null;
JSONObject pos = loc.optJSONObject("coordinates");
if (pos == null) {
pos = loc.optJSONObject("position");
}
if (pos != null) {
coord = Point.fromDouble(pos.optDouble("latitude"), pos.optDouble("longitude"));
} else {
coord = lid.coord;
}
return parseLocation(
lid.type,
id,
coord,
loc.optString("name", null),
parseProducts(loc.optJSONArray("products")));
}
private Location parseLocation(final LocationType type, final String id, final Point coord, String name,
final Set<Product> products) {
final String[] placeAndName = type == LocationType.STATION ? splitStationName(name) : splitAddress(name);
return new Location(type, id, coord, placeAndName[0], placeAndName[1], products);
}
private Location parseDirection(final JSONObject dep) {
final String richtung = dep.optString("richtung", null);
if (richtung == null)
return null;
return parseLocation(LocationType.STATION, null, null, richtung, null);
}
private List<Location> parseLocations(final JSONArray locs) throws JSONException {
final List<Location> locations = new ArrayList<>();
for (int i = 0; i < locs.length(); i++) {
final Location l = parseLocation(locs.getJSONObject(i));
if (l != null) {
locations.add(l);
}
}
return locations;
}
private void parseMessages(final JSONArray msgs, final List<String> messages, final Integer minPriority)
throws JSONException {
if (msgs == null)
return;
for (int i = 0; i < msgs.length(); i++) {
final JSONObject msgObj = msgs.getJSONObject(i);
final String msg = msgObj.optString("text", null);
if (msg != null && (minPriority == null || msgObj.optInt("priority", minPriority) < minPriority)) {
messages.add(msg);
}
}
}
private String parseMessages(final JSONObject e) throws JSONException {
final List<String> messages = new ArrayList<>();
parseMessages(e.optJSONArray("echtzeitNotizen"), messages, null);
parseMessages(e.optJSONArray("himNotizen"), messages, null);
// show very important static messages (e.g. on demand tel)
parseMessages(e.optJSONArray("attributNotizen"), messages, 100);
return messages.isEmpty() ? null : String.join(" ", messages);
}
private Line parseLine(final JSONObject e) {
// TODO attrs, messages
final Product p = SHORT_PRODUCTS_MAP.get(e.optString("produktGattung", null));
final String name = Optional.ofNullable(e.optString("langtext", null)).orElse(e.optString("mitteltext", null));
String shortName = e.optString("mitteltext", null);
if (shortName != null && (p == Product.BUS || p == Product.TRAM)) {
shortName = shortName.replaceAll("^[A-Za-z]+ ", "");
}
return new Line(
e.optString("zuglaufId", null),
null,
p,
shortName,
name,
lineStyle(null, p, name));
}
private boolean parseCancelled(JSONObject stop) {
final boolean cancelled = stop.optBoolean("cancelled", false);
if (cancelled)
return true;
final JSONArray notices = stop.optJSONArray("echtzeitNotizen");
if (notices != null) {
for (int i = 0; i < notices.length(); i++) {
final JSONObject notice = notices.optJSONObject(i);
if (notice != null) {
final String text = notice.optString("text", null);
if ("Halt entfällt".equals(text) || "Stop cancelled".equals(text)) {
return true;
}
}
}
}
return false;
}
private Stop parseStop(final JSONObject stop, final Location fallbackLocation) {
final Position gleis = parsePosition(stop.optString("gleis", null));
final Position ezGleis = parsePosition(stop.optString("ezGleis", null));
final boolean cancelled = parseCancelled(stop);
return new Stop(
Optional.ofNullable(parseLocation(stop.optJSONObject("ort"))).orElse(fallbackLocation),
parseIso8601WOffset(stop.optString("ankunftsDatum", null)),
parseIso8601WOffset(stop.optString("ezAnkunftsDatum", null)),
gleis, ezGleis, cancelled,
parseIso8601WOffset(stop.optString("abgangsDatum", null)),
parseIso8601WOffset(stop.optString("ezAbgangsDatum", null)),
gleis, ezGleis, cancelled);
}
private List<Stop> parseStops(final JSONArray stops) throws JSONException {
if (stops == null)
return null;
List<Stop> out = new LinkedList<>();
for (int i = 0; i < stops.length(); i++) {
out.add(parseStop(stops.getJSONObject(i), null));
}
return out;
}
private int[] parseCapacity(final JSONObject e) throws JSONException {
final JSONArray auslastungen = e.optJSONArray("auslastungsInfos");
int[] out = { 0, 0 };
if (auslastungen != null) {
for (int i = 0; i < auslastungen.length(); i++) {
final JSONObject auslastung = auslastungen.getJSONObject(i);
final String klasse = auslastung.optString("klasse");
out["KLASSE_2".equals(klasse) ? 1 : 0] = auslastung.optInt("stufe", 0);
}
if (out[0] == 0 && out[1] == 0) {
return null;
}
return out;
}
return null;
}
private Trip.Leg parseLeg(final JSONObject abschnitt) throws JSONException {
Stop departureStop = null;
Stop arrivalStop = null;
final String typ = abschnitt.optString("typ", null);
final boolean isPublicTransportLeg = "FAHRZEUG".equals(typ);
final List<Stop> intermediateStops = parseStops(abschnitt.optJSONArray("halte"));
if (intermediateStops != null && intermediateStops.size() >= 2 && isPublicTransportLeg) {
final int size = intermediateStops.size();
departureStop = intermediateStops.get(0);
arrivalStop = intermediateStops.get(size - 1);
intermediateStops.remove(size - 1);
intermediateStops.remove(0);
} else {
departureStop = parseStop(abschnitt, parseLocation(abschnitt.optJSONObject("abgangsOrt")));
arrivalStop = parseStop(abschnitt, parseLocation(abschnitt.optJSONObject("ankunftsOrt")));
}
if (isPublicTransportLeg) {
final Line line = parseLine(abschnitt);
final Location destination = parseDirection(abschnitt);
final String message = parseMessages(abschnitt);
return new Trip.Public(line, destination, departureStop, arrivalStop, intermediateStops, null, message);
} else {
final int dist = abschnitt.optInt("distanz");
return new Trip.Individual(
"TRANSFER".equals(typ) ? Trip.Individual.Type.TRANSFER : Trip.Individual.Type.WALK,
departureStop.location,
departureStop.getDepartureTime(),
arrivalStop.location,
departureStop.location.equals(arrivalStop.location)
? departureStop.getDepartureTime()
: arrivalStop.getArrivalTime(),
null, dist);
}
}
private List<Fare> parseFares(final JSONObject verbindungParent) {
List<Fare> fares = new ArrayList<>();
final Optional<JSONObject> ab = Optional.ofNullable(verbindungParent.optJSONObject("angebote"))
.map(angebote -> angebote.optJSONObject("preise"))
.map(preise -> preise.optJSONObject("gesamt"))
.map(gesamt -> gesamt.optJSONObject("ab"));
if (ab.isPresent()) {
fares.add(new Fare(
"ab", Fare.Type.ADULT, ParserUtils.getCurrency(ab.get().optString("waehrung", "EUR")),
(float) ab.get().optDouble("betrag"), null, null));
}
return fares;
}
private String parseErrorCode(AbstractHttpException e) {
String code = null;
try {
final JSONObject res = new JSONObject(e.getBodyPeek().toString());
final JSONObject details = res.optJSONObject("details");
code = res.optString("code", null);
if (details != null) {
code = details.optString("typ", code);
}
} catch (final Exception x) {
// ignore
}
return code;
}
private QueryTripsResult doQueryTrips(Location from, @Nullable Location via, Location to, Date time, boolean dep,
@Nullable Set<Product> products, final boolean bike, final @Nullable String context) throws IOException {
// TODO minUmstiegsdauer instead of walkSpeed ?
// accessibility, optimize not supported
final String deparr = dep ? "ABFAHRT" : "ANKUNFT";
final String productsStr = "\"verkehrsmittel\":[" + formatProducts(products) + "]";
final String viaLocations = via != null
? "\"viaLocations\":[{\"locationId\": \"" + formatLid(via) + "\"," + productsStr + "}],"
: "";
final String bikeStr = bike ? "\"fahrradmitnahme\":true," : "";
final String ctxStr = context != null ? "\"context\": \"" + context + "\"," : "";
final String request = "{\"autonomeReservierung\":false,\"einstiegsTypList\":[\"STANDARD\"],\"klasse\":\"KLASSE_2\"," //
+ "\"reiseHin\":{\"wunsch\":{\"abgangsLocationId\": \"" + formatLid(from) + "\"," //
+ productsStr + "," //
+ viaLocations //
+ bikeStr //
+ ctxStr //
+ "\"zeitWunsch\":{\"reiseDatum\":\"" + formatIso8601WOffset(time) + "\",\"zeitPunktArt\":\"" + deparr //
+ "\"}," //
+ "\"zielLocationId\": \"" + formatLid(to) + "\"}}," //
+ "\"reisendenProfil\":{\"reisende\":[{\"ermaessigungen\":[\"KEINE_ERMAESSIGUNG KLASSENLOS\"],\"reisendenTyp\":\"ERWACHSENER\"}]}," //
+ "\"reservierungsKontingenteVorhanden\":false}";
final HttpUrl url = this.tripEndpoint;
final String contentType = "application/x.db.vendo.mob.verbindungssuche.v8+json";
String page = null;
try {
page = doRequest(url, request, contentType);
final JSONObject res = new JSONObject(page);
final JSONArray verbindungen = res.getJSONArray("verbindungen");
final List<Trip> trips = new ArrayList<>();
for (int i = 0; i < verbindungen.length(); i++) {
final JSONObject verbindungParent = verbindungen.getJSONObject(i);
final JSONObject verbindung = verbindungParent.getJSONObject("verbindung");
final JSONArray abschnitte = verbindung.getJSONArray("verbindungsAbschnitte");
final List<Trip.Leg> legs = new ArrayList<>();
Location tripFrom = null;
Location tripTo = null;
for (int j = 0; j < abschnitte.length(); j++) {
final Trip.Leg leg = parseLeg(abschnitte.getJSONObject(j));
legs.add(leg);
if (j == 0) {
tripFrom = leg.departure;
}
if (j == abschnitte.length() - 1) {
tripTo = leg.arrival;
}
}
final List<Fare> fares = parseFares(verbindungParent);
final int transfers = verbindung.optInt("umstiegeAnzahl", -1);
final int[] capacity = parseCapacity(verbindung);
trips.add(
new Trip(verbindung.optString("kontext").split("#")[0], tripFrom, tripTo, legs, fares, capacity,
transfers == -1 ? null : transfers));
}
if (trips.isEmpty()) {
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.NO_TRIPS);
}
final DbMovasContext ctx = new DbMovasContext(from, via, to, time, dep, products, bike,
res.optString("spaeterContext", null), res.optString("frueherContext", null));
return new QueryTripsResult(this.resultHeader, null, from, via, to, ctx, trips);
} catch (InternalErrorException | BlockedException e) {
final String code = parseErrorCode(e);
if ("MDA-AK-MSG-1001".equals(code)) {
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.INVALID_DATE);
} else if (code != null) {
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.NO_TRIPS);
}
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.SERVICE_DOWN);
} catch (IOException | RuntimeException e) {
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.SERVICE_DOWN);
} catch (final JSONException x) {
throw new ParserException("cannot parse json: '" + page + "' on " + url, x);
}
}
@Override
public NearbyLocationsResult queryNearbyLocations(Set<LocationType> types, Location location, int maxDistance,
int maxLocations) throws IOException {
// TODO POIs not supported (?)
if (maxDistance == 0)
maxDistance = DEFAULT_MAX_DISTANCE;
if (maxLocations == 0)
maxLocations = DEFAULT_MAX_LOCATIONS;
if (location.coord == null) {
return new NearbyLocationsResult(resultHeader, NearbyLocationsResult.Status.INVALID_ID);
}
final String request = "{\"area\":" //
+ "{\"coordinates\":{\"longitude\":" + location.coord.getLonAsDouble() + ",\"latitude\":"
+ location.coord.getLatAsDouble() + "}," //
+ "\"radius\":" + maxDistance + "}," //
+ "\"maxResults\":" + maxLocations + "," //
+ "\"products\":[\"ALL\"]}";
final HttpUrl url = this.nearbyEndpoint;
final String contentType = "application/x.db.vendo.mob.location.v3+json";
String page = null;
try {
page = doRequest(url, request, contentType);
final JSONArray locs = new JSONArray(page);
final List<Location> locations = parseLocations(locs);
return new NearbyLocationsResult(this.resultHeader, locations);
} catch (InternalErrorException | BlockedException e) {
return new NearbyLocationsResult(this.resultHeader, NearbyLocationsResult.Status.INVALID_ID);
} catch (IOException | RuntimeException e) {
return new NearbyLocationsResult(this.resultHeader, NearbyLocationsResult.Status.SERVICE_DOWN);
} catch (final JSONException x) {
throw new ParserException("cannot parse json: '" + page + "' on " + url, x);
}
}
@Override
public QueryDeparturesResult queryDepartures(String stationId, @Nullable Date time, int maxDepartures,
boolean equivs)
throws IOException {
// TODO only 1 hour of results returned, find secret parameter?
if (maxDepartures == 0)
maxDepartures = DEFAULT_MAX_DEPARTURES;
final Calendar c = new GregorianCalendar(timeZone);
c.setTime(time);
final String request = "{\"anfragezeit\": \"" + formatTime(c) + "\"," //
+ "\"datum\": \"" + formatDate(c) + "\"," //
+ "\"ursprungsBahnhofId\": \"" + formatLid(stationId) + "\"," //
+ "\"verkehrsmittel\":[\"ALL\"]}";
final HttpUrl url = this.departureEndpoint;
final String contentType = "application/x.db.vendo.mob.bahnhofstafeln.v2+json";
String page = null;
try {
page = doRequest(url, request, contentType);
final QueryDeparturesResult result = new QueryDeparturesResult(this.resultHeader);
final JSONObject head = new JSONObject(page);
final JSONArray deps = head.getJSONArray("bahnhofstafelAbfahrtPositionen");
int added = 0;
for (int i = 0; i < deps.length(); i++) {
final JSONObject dep = deps.getJSONObject(i);
if (parseCancelled(dep)) {
continue;
}
final Location l = parseLocation(dep.optJSONObject("abfrageOrt"));
if (!equivs && !stationId.equals(l.id)) {
continue;
}
StationDepartures stationDepartures = result.findStationDepartures(l.id);
if (stationDepartures == null) {
stationDepartures = new StationDepartures(l, new ArrayList<Departure>(8), null);
result.stationDepartures.add(stationDepartures);
}
final Stop stop = parseStop(dep, l);
final Departure departure = new Departure(
stop.plannedDepartureTime,
stop.predictedDepartureTime,
parseLine(dep),
Optional.ofNullable(stop.predictedDeparturePosition).orElse(stop.plannedDeparturePosition),
parseDirection(dep),
null,
parseMessages(dep));
stationDepartures.departures.add(departure);
added += 1;
if (added >= maxDepartures) {
break;
}
}
for (final StationDepartures stationDepartures : result.stationDepartures)
Collections.sort(stationDepartures.departures, Departure.TIME_COMPARATOR);
return result;
} catch (InternalErrorException | BlockedException e) {
return new QueryDeparturesResult(this.resultHeader, QueryDeparturesResult.Status.INVALID_STATION);
} catch (IOException | RuntimeException e) {
return new QueryDeparturesResult(this.resultHeader, QueryDeparturesResult.Status.SERVICE_DOWN);
} catch (final JSONException x) {
throw new ParserException("cannot parse json: '" + page + "' on " + url, x);
}
}
@Override
public SuggestLocationsResult suggestLocations(CharSequence constraint, @Nullable Set<LocationType> types,
int maxLocations)
throws IOException {
if (maxLocations == 0)
maxLocations = DEFAULT_MAX_LOCATIONS;
final String request = "{\"searchTerm\": \"" + constraint + "\"," //
+ "\"locationTypes\":[" + formatLocationTypes(types) + "]," //
+ "\"maxResults\":" + maxLocations + "}";
final HttpUrl url = this.locationsEndpoint;
final String contentType = "application/x.db.vendo.mob.location.v3+json";
String page = null;
try {
page = doRequest(url, request, contentType);
final JSONArray locs = new JSONArray(page);
final List<SuggestedLocation> locations = new ArrayList<>();
for (int i = 0; i < locs.length(); i++) {
final JSONObject jsonL = locs.getJSONObject(i);
final Location l = parseLocation(jsonL);
if (l != null) {
locations.add(new SuggestedLocation(l, jsonL.optInt("weight", -i)));
}
}
return new SuggestLocationsResult(this.resultHeader, locations);
} catch (IOException | RuntimeException e) {
e.printStackTrace();
return new SuggestLocationsResult(this.resultHeader, SuggestLocationsResult.Status.SERVICE_DOWN);
} catch (final JSONException x) {
throw new ParserException("cannot parse json: '" + page + "' on " + url, x);
}
}
@Override
public QueryTripsResult queryTrips(Location from, @Nullable Location via, Location to, Date date, boolean dep,
@Nullable TripOptions options) throws IOException {
return doQueryTrips(from, via, to, date, dep,
options != null ? options.products : null,
options != null && options.flags != null && options.flags.contains(TripFlag.BIKE),
null);
}
@Override
public QueryTripsResult queryMoreTrips(QueryTripsContext context, boolean later) throws IOException {
final DbMovasContext ctx = (DbMovasContext) context;
final String ctxToken;
if (later && ctx.canQueryLater()) {
ctxToken = ctx.laterContext;
} else if (!later && ctx.canQueryEarlier()) {
ctxToken = ctx.earlierContext;
} else {
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.NO_TRIPS);
}
return doQueryTrips(ctx.from, ctx.via, ctx.to, ctx.date, ctx.dep, ctx.products, ctx.bike, ctxToken);
}
@Override
protected boolean hasCapability(Capability capability) {
return CAPABILITIES.contains(capability);
}
@Override
@ -63,29 +766,36 @@ public final class DbProvider extends AbstractHafasClientInterfaceProvider {
return Product.ALL;
}
private static final Pattern P_SPLIT_NAME_ONE_COMMA = Pattern.compile("([^,]*), ([^,]*)");
private static class DbMovasContext implements QueryTripsContext {
public final Location from, via, to;
public final Date date;
public final boolean dep;
public final Set<Product> products;
public final boolean bike;
public final String laterContext, earlierContext;
@Override
protected String[] splitStationName(final String name) {
final Matcher m = P_SPLIT_NAME_ONE_COMMA.matcher(name);
if (m.matches())
return new String[] { m.group(2), m.group(1) };
return super.splitStationName(name);
}
public DbMovasContext(final Location from, final @Nullable Location via, final Location to, final Date date,
final boolean dep, final Set<Product> products, final boolean bike, final String laterContext,
final String earlierContext) {
this.from = from;
this.via = via;
this.to = to;
this.date = date;
this.dep = dep;
this.products = products;
this.bike = bike;
this.laterContext = laterContext;
this.earlierContext = earlierContext;
}
@Override
protected String[] splitPOI(final String poi) {
final Matcher m = P_SPLIT_NAME_FIRST_COMMA.matcher(poi);
if (m.matches())
return new String[] { m.group(1), m.group(2) };
return super.splitStationName(poi);
}
@Override
public boolean canQueryLater() {
return laterContext != null;
}
@Override
protected String[] splitAddress(final String address) {
final Matcher m = P_SPLIT_NAME_FIRST_COMMA.matcher(address);
if (m.matches())
return new String[] { m.group(1), m.group(2) };
return super.splitStationName(address);
@Override
public boolean canQueryEarlier() {
return earlierContext != null;
}
}
}

View file

@ -24,9 +24,9 @@ import static org.junit.Assert.assertThat;
import java.util.Date;
import java.util.EnumSet;
import org.junit.Ignore;
import org.junit.Test;
import de.schildbach.pte.AbstractHafasClientInterfaceProvider;
import de.schildbach.pte.DbProvider;
import de.schildbach.pte.NetworkProvider.Accessibility;
import de.schildbach.pte.NetworkProvider.WalkSpeed;
@ -45,8 +45,7 @@ import de.schildbach.pte.dto.TripOptions;
*/
public class DbProviderLiveTest extends AbstractProviderLiveTest {
public DbProviderLiveTest() {
super(new DbProvider(secretProperty("db.api_authorization"), AbstractHafasClientInterfaceProvider
.decryptSalt(secretProperty("db.encrypted_salt"), secretProperty("hci.salt_encryption_key"))));
super(new DbProvider());
}
@Test
@ -56,6 +55,8 @@ public class DbProviderLiveTest extends AbstractProviderLiveTest {
}
@Test
@Ignore
// TODO not supported
public void nearbyPOIsByCoordinate() throws Exception {
final NearbyLocationsResult result = queryNearbyLocations(EnumSet.of(LocationType.POI),
Location.coord(Point.fromDouble(52.5304903, 13.3791152)));
@ -67,7 +68,7 @@ public class DbProviderLiveTest extends AbstractProviderLiveTest {
@Test
public void queryDepartures() throws Exception {
final QueryDeparturesResult result = queryDepartures("692991", false);
final QueryDeparturesResult result = queryDepartures("692990", false);
print(result);
}
@ -104,7 +105,7 @@ public class DbProviderLiveTest extends AbstractProviderLiveTest {
final SuggestLocationsResult result = suggestLocations("München, Friedenstraße 2");
print(result);
assertThat(result.getLocations(), hasItem(new Location(LocationType.ADDRESS,
"A=2@O=München - Berg am Laim, Friedenstraße 2@X=11602251@Y=48123949@U=103@L=980857648@B=1@p=1378873973@",
"A=2@O=München - Berg am Laim, Friedenstraße 2@H=2@X=11602251@Y=48123949@U=92@L=980879740@B=1@p=1706613073@",
"München - Berg am Laim", "Friedenstraße 2")));
}
@ -113,6 +114,7 @@ public class DbProviderLiveTest extends AbstractProviderLiveTest {
final Location from = new Location(LocationType.STATION, "8011160", null, "Berlin Hbf");
final Location to = new Location(LocationType.STATION, "8010205", null, "Leipzig Hbf");
final QueryTripsResult result = queryTrips(from, null, to, new Date(), true, null);
assertEquals(QueryTripsResult.Status.OK, result.status);
print(result);
final QueryTripsResult laterResult = queryMoreTrips(result.context, true);
print(laterResult);
@ -131,6 +133,7 @@ public class DbProviderLiveTest extends AbstractProviderLiveTest {
final Location to = new Location(LocationType.STATION, "623234", Point.from1E6(48000221, 11342490), null,
"Tutzinger-Hof-Platz, Starnberg");
final QueryTripsResult result = queryTrips(from, null, to, new Date(), true, null);
assertEquals(QueryTripsResult.Status.OK, result.status);
print(result);
final QueryTripsResult laterResult = queryMoreTrips(result.context, true);
print(laterResult);
@ -141,6 +144,18 @@ public class DbProviderLiveTest extends AbstractProviderLiveTest {
final Location from = new Location(LocationType.STATION, "513729", null, "Schillerplatz, Kaiserslautern");
final Location to = new Location(LocationType.STATION, "403631", null, "Trippstadt Grundschule");
final QueryTripsResult result = queryTrips(from, null, to, new Date(), true, null);
assertEquals(QueryTripsResult.Status.NO_TRIPS, result.status);
print(result);
}
@Test
public void ambiguousTrips() throws Exception {
final Location from = new Location(LocationType.STATION, null, null, "berlin hbf");
final Location to = new Location(LocationType.ADDRESS,
"A=2@O=München - Berg am Laim, Friedenstraße 2@X=11602251@Y=48123949@U=103@L=980857648@B=1@p=1378873973@",
null, "irrelevant");
final QueryTripsResult result = queryTrips(from, null, to, new Date(), true, null);
assertEquals(QueryTripsResult.Status.OK, result.status);
print(result);
}
@ -151,8 +166,10 @@ public class DbProviderLiveTest extends AbstractProviderLiveTest {
final Location to = new Location(LocationType.ADDRESS, null, Point.from1E6(47994243, 11338543), null,
"Starnberg, Possenhofener Straße 13");
final QueryTripsResult result = queryTrips(from, null, to, new Date(), true, null);
assertEquals(QueryTripsResult.Status.OK, result.status);
print(result);
final QueryTripsResult laterResult = queryMoreTrips(result.context, true);
assertEquals(QueryTripsResult.Status.OK, laterResult.status);
print(laterResult);
}
@ -163,8 +180,8 @@ public class DbProviderLiveTest extends AbstractProviderLiveTest {
final Location from = new Location(LocationType.STATION, "8506131", null, "Kreuzlingen");
final Location to = new Location(LocationType.STATION, "8003400", null, "Konstanz");
final QueryTripsResult result = queryTrips(from, null, to, new Date(), true, options);
print(result);
assertEquals(QueryTripsResult.Status.OK, result.status);
print(result);
}
@Test
@ -180,7 +197,7 @@ public class DbProviderLiveTest extends AbstractProviderLiveTest {
final Location location = new Location(LocationType.STATION, "8010205", null, "Leipzig Hbf");
final QueryTripsResult result = queryTrips(location, null, location, new Date(), true, null);
print(result);
assertEquals(QueryTripsResult.Status.TOO_CLOSE, result.status);
assertEquals(QueryTripsResult.Status.NO_TRIPS, result.status);
}
@Test

View file

@ -1,7 +1,5 @@
# Secrets are needed to run some of the live tests.
hci.salt_encryption_key =
db.api_authorization =
db.encrypted_salt =
bvg.api_authorization =
vbb.api_authorization =
vbb.encrypted_salt =