Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
de.schildbach.pte.NegentweeProvider Maven / Gradle / Ivy
/*
* Copyright the original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package de.schildbach.pte;
import java.io.IOException;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Currency;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import javax.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.google.common.collect.ImmutableSet;
import de.schildbach.pte.dto.Departure;
import de.schildbach.pte.dto.Fare;
import de.schildbach.pte.dto.Line;
import de.schildbach.pte.dto.LineDestination;
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.InternalErrorException;
import de.schildbach.pte.exception.NotFoundException;
import de.schildbach.pte.util.ParserUtils;
import de.schildbach.pte.util.WordUtils;
import okhttp3.HttpUrl;
/**
* @author full-duplex
*/
public class NegentweeProvider extends AbstractNetworkProvider {
private static final String API_BASE = "https://api.9292.nl/0.1/";
private static final String SERVER_PRODUCT = "negentwee";
private static final Language DEFAULT_API_LANG = Language.NL_NL;
private static final TimeZone API_TIMEZONE = TimeZone.getTimeZone("Europe/Amsterdam");
private static final int DEFAULT_MAX_LOCATIONS = 50;
private static final EnumSet trainProducts = EnumSet.of(Product.HIGH_SPEED_TRAIN, Product.REGIONAL_TRAIN,
Product.SUBURBAN_TRAIN);
private final Language language;
private final ResultHeader resultHeader;
public enum Language {
NL_NL("nl-NL"), EN_GB("en-GB");
private final String lang;
private Language(String lang) {
this.lang = lang;
}
@Override
public String toString() {
return this.lang;
}
}
private enum InterchangeTime {
STANDARD, EXTRA;
@Override
public String toString() {
return name().toLowerCase();
}
}
@SuppressWarnings("serial")
private static class QueryParameter implements Serializable {
public String name, value;
private QueryParameter(String name, String value) {
this.name = name;
this.value = value;
}
@Override
public String toString() {
return this.name + "=" + this.value;
}
}
@SuppressWarnings("serial")
private static class TripsContext implements QueryTripsContext {
private String url, earlier, later;
public Location from, to, via;
private TripsContext(HttpUrl url, @Nullable String earlier, @Nullable String later, Location from,
@Nullable Location via, Location to) {
this.url = url.toString();
this.earlier = earlier;
this.later = later;
this.from = from;
this.via = via;
this.to = to;
}
private HttpUrl getQueryEarlier() {
return HttpUrl.parse(this.url).newBuilder(this.earlier).addQueryParameter("before", "4").build();
}
private HttpUrl getQueryLater() {
return HttpUrl.parse(this.url).newBuilder(this.later).addQueryParameter("after", "4").build();
}
@Override
public boolean canQueryEarlier() {
return (earlier != null);
}
@Override
public boolean canQueryLater() {
return (later != null);
}
}
public NegentweeProvider() {
this(DEFAULT_API_LANG);
}
public NegentweeProvider(Language language) {
super(NetworkId.NEGENTWEE);
this.language = language;
this.resultHeader = new ResultHeader(network, SERVER_PRODUCT);
}
private HttpUrl buildApiUrl(String action, List queries) {
HttpUrl.Builder url = HttpUrl.parse(API_BASE).newBuilder().addPathSegments(action).addQueryParameter("lang",
this.language.toString());
for (QueryParameter q : queries) {
url.addQueryParameter(q.name, q.value);
}
return url.build();
}
private Location queryLocationById(String stationId) throws IOException {
HttpUrl url = buildApiUrl("locations/" + stationId, new ArrayList());
final CharSequence page = httpClient.get(url);
try {
JSONObject head = new JSONObject(page.toString());
JSONObject location = head.getJSONObject("location");
return locationFromJSONObject(location);
} catch (final JSONException x) {
throw new IOException("cannot parse: '" + page + "' on " + url, x);
}
}
private Location queryLocationByName(String locationName, Set types) throws IOException {
for (Location location : queryLocationsByName(locationName, types)) {
if (location.name != null && location.name.equals(locationName)) {
return location;
}
}
throw new RuntimeException("Cannot find station with name " + locationName);
}
private List queryLocationsByName(String locationName, Set types) throws IOException {
List queryParameters = new ArrayList<>();
queryParameters.add(new QueryParameter("q", locationName));
// Add types if specified
String locationTypes = locationTypesToQueryParameterString(types);
if (locationTypes.length() > 0)
queryParameters.add(new QueryParameter("type", locationTypes));
HttpUrl url = buildApiUrl("locations", queryParameters);
final CharSequence page = httpClient.get(url);
try {
JSONObject head = new JSONObject(page.toString());
JSONArray locations = head.getJSONArray("locations");
Location[] foundLocations = new Location[locations.length()];
for (int i = 0; i < locations.length(); i++) {
foundLocations[i] = locationFromJSONObject(locations.getJSONObject(i));
}
return Arrays.asList(foundLocations);
} catch (final JSONException x) {
throw new RuntimeException("cannot parse: '" + page + "' on " + url, x);
}
}
private LocationType locationTypeFromTypeString(String type) throws JSONException {
switch (type) {
case "station":
case "stop":
return LocationType.STATION;
case "address":
case "street":
case "streetrange":
case "place":
case "postcode":
return LocationType.ADDRESS;
case "poi":
return LocationType.POI;
case "latlong":
return LocationType.COORD;
default:
throw new JSONException("Unsupported location type: " + type);
}
}
private List locationStringsFromLocationType(LocationType type) {
switch (type) {
case STATION:
return Arrays.asList("station", "stop");
case POI:
return Arrays.asList("poi");
case ADDRESS:
return Arrays.asList("address", "street", "streetrange", "place", "postcode");
case COORD:
return Arrays.asList("latlong");
default:
return Arrays.asList();
}
}
private Set productSetFromTypeString(String type) {
switch (type.toLowerCase()) {
case "train":
return EnumSet.of(Product.HIGH_SPEED_TRAIN, Product.REGIONAL_TRAIN, Product.SUBURBAN_TRAIN);
case "subway":
return EnumSet.of(Product.SUBWAY);
case "tram":
return EnumSet.of(Product.TRAM);
case "bus":
return EnumSet.of(Product.BUS);
case "ferry":
return EnumSet.of(Product.FERRY);
case "walk":
return EnumSet.of(Product.ON_DEMAND);
default:
return EnumSet.noneOf(Product.class);
}
}
private Product productFromMode(String type, String name) {
switch (type.toLowerCase()) {
case "train":
switch (name.toLowerCase()) {
// TODO: Likely not all possible train names, add here if trains are classified incorrectly.
case "thalys":
case "ice":
case "intercity direct":
case "intercity":
return Product.HIGH_SPEED_TRAIN;
case "sprinter":
default:
return Product.REGIONAL_TRAIN;
}
case "tram":
return Product.TRAM;
case "subway":
return Product.SUBWAY;
case "bus":
return Product.BUS;
case "ferry":
return Product.FERRY;
case "walk":
return Product.ON_DEMAND;
}
return null;
}
private String locationToQueryParameterString(Location loc) {
if (loc.hasId()) {
return loc.id;
} else if (loc.hasCoord()) {
return loc.getLatAsDouble() + "," + loc.getLonAsDouble();
} else {
return null;
}
}
// Including these type names will cause the locations API to fail, skip them
private static final ImmutableSet DISALLOWED_TYPE_NAMES = ImmutableSet.of("latlong", "streetrange");
private String locationTypesToQueryParameterString(Set types) {
StringBuilder typeValue = new StringBuilder();
if (!types.contains(LocationType.ANY) && types.size() > 0) {
for (LocationType type : types) {
for (String addition : locationStringsFromLocationType(type)) {
if (DISALLOWED_TYPE_NAMES.contains(addition))
continue;
if (typeValue.length() > 0)
typeValue.append(",");
typeValue.append(addition);
}
}
}
return typeValue.toString();
}
private String formatApiDateTime(Date date) {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HHmm");
formatter.setTimeZone(API_TIMEZONE);
return formatter.format(date.getTime());
}
private Date dateFromJSONObject(JSONObject obj, String key) throws JSONException {
try {
Calendar cal = Calendar.getInstance(API_TIMEZONE);
ParserUtils.parseIsoDateTime(cal, obj.getString(key));
return cal.getTime();
} catch (RuntimeException e) {
return null;
}
}
private Date timeFromJSONObject(JSONObject obj, String key) throws JSONException {
try {
Calendar calParsed = Calendar.getInstance(API_TIMEZONE);
ParserUtils.parseIsoTime(calParsed, obj.getString(key));
// Assume this time is always between NOW-00:05 and NOW+23:55, allowing for a 5 minute delay.
Calendar calNow = Calendar.getInstance();
calNow.add(Calendar.MINUTE, -5);
if (calParsed.before(calNow)) {
calNow.add(Calendar.HOUR, 24);
}
return calParsed.getTime();
} catch (RuntimeException e) {
return null;
}
}
private Date realtimeDateFromJSONObject(JSONObject obj, String key, String realtimeKey) throws JSONException {
return dateFromJSONObject(obj, (!obj.isNull(realtimeKey)) ? realtimeKey : key);
}
private Trip tripFromJSONObject(JSONObject trip, @Nullable Location from, @Nullable Location to,
@Nullable Map disturbances) throws JSONException {
JSONArray legs = trip.getJSONArray("legs");
Date tripDeparture = realtimeDateFromJSONObject(trip, "departure", "realtimeDeparture");
/* Date tripArrival = */ realtimeDateFromJSONObject(trip, "arrival", "realtimeArrival");
// Get journey legs
LinkedList foundLegs = new LinkedList<>();
for (int i = 0; i < legs.length(); i++) {
JSONObject leg = legs.getJSONObject(i);
JSONArray stops = leg.getJSONArray("stops");
JSONObject mode = leg.getJSONObject("mode");
JSONObject operator = leg.optJSONObject("operator");
LinkedList foundPoints = new LinkedList<>();
// First stop
Stop firstStop = stopFromJSONObject(stops.getJSONObject(0));
foundPoints.add(firstStop.location.coord);
// Intermediate stops
LinkedList foundStops = new LinkedList<>();
for (int j = 1; j < stops.length() - 1; j++) {
foundStops.add(stopFromJSONObject(stops.getJSONObject(j)));
foundPoints.add(foundStops.getLast().location.coord);
}
// Last stop
Stop lastStop = stopFromJSONObject(stops.getJSONObject(stops.length() - 1));
foundPoints.add(lastStop.location.coord);
switch (leg.getString("type").toLowerCase()) {
case "scheduled":
Product lineProduct = productFromMode(mode.getString("type"), mode.getString("name"));
StringBuilder legMessage = new StringBuilder();
// Add attributes to leg message
JSONArray legAttributes = leg.getJSONArray("attributes");
for (int k = 0; k < legAttributes.length(); k++) {
JSONObject legAttribute = legAttributes.getJSONObject(k);
if (legMessage.length() > 0)
legMessage.append(", ");
legMessage.append(WordUtils.capitalizeFirst(legAttribute.getString("title")));
}
// Add disturbances to leg message
if (disturbances != null) {
JSONArray legDisturbances = leg.getJSONArray("disturbancePlannerIds");
for (int k = 0; k < legDisturbances.length(); k++) {
String legDisturbanceId = legDisturbances.optString(k);
if (legDisturbanceId != null && disturbances.containsKey(legDisturbanceId)) {
JSONObject legDisturbance = disturbances.get(legDisturbanceId);
if (legMessage.length() > 0)
legMessage.append(" \n \n");
legMessage.append(legDisturbance.getString("title"));
legMessage.append(": \n");
legMessage.append(legDisturbance.getString("effect"));
legMessage.append(" ");
legMessage.append(legDisturbance.getString("measure"));
}
}
}
StringBuilder lineName = new StringBuilder();
lineName.append(mode.getString("name"));
// Service codes have no relevant meaning for trains
if (!leg.isNull("service") && !trainProducts.contains(lineProduct)) {
lineName.append(" ");
lineName.append(leg.getString("service"));
}
foundLegs.add(new Trip.Public(
new Line(leg.getString("service"), (operator != null) ? operator.getString("name") : null,
lineProduct, lineName.toString(), leg.optString("service"),
Standard.STYLES.get(lineProduct), null, null),
new Location(LocationType.STATION, null, null, leg.getString("destination")), firstStop,
lastStop, foundStops, foundPoints, legMessage.length() > 0 ? legMessage.toString() : null));
break;
case "continuous":
// Get leg time from trip or previous leg
Date legDeparture = (i == 0) ? tripDeparture : foundLegs.getLast().getArrivalTime();
Date legArrival = ParserUtils.addMinutes(legDeparture,
ParserUtils.parseMinutesFromTimeString(leg.getString("duration")));
foundLegs.add(new Trip.Individual(Trip.Individual.Type.WALK, firstStop.location, legDeparture,
lastStop.location, legArrival, foundPoints, -1));
break;
default:
throw new JSONException("Unknown leg type: " + leg.getString("type"));
}
}
// Get journey fares
JSONObject fareInfo = trip.getJSONObject("fareInfo");
JSONArray fareLegs = fareInfo.getJSONArray("legs");
Fare[] foundFares = new Fare[fareLegs.length()];
for (int i = 0; i < fareLegs.length(); i++) {
foundFares[i] = fareFromJSONObject(fareLegs.getJSONObject(i));
}
return new Trip(trip.getString("id"), from, to, foundLegs, Arrays.asList(foundFares), null,
trip.getInt("numberOfChanges"));
}
private Stop stopFromJSONObject(JSONObject stop) throws JSONException {
Position plannedPlatform = positionFromJSONObject(stop, "platform");
Position changedPlatform = positionFromJSONObject(stop, "platformChange");
return new Stop(locationFromJSONObject(stop.getJSONObject("location")), dateFromJSONObject(stop, "arrival"),
dateFromJSONObject(stop, "realtimeArrival"), plannedPlatform, changedPlatform, false,
dateFromJSONObject(stop, "departure"), dateFromJSONObject(stop, "realtimeDeparture"), plannedPlatform,
changedPlatform, false);
}
private Fare fareFromJSONObject(JSONObject fareLeg) throws JSONException {
JSONArray fares = fareLeg.getJSONArray("fares");
float farePrice = 0;
for (int j = 0; j < fares.length(); j++) {
JSONObject fare = fares.getJSONObject(j);
// Always get the full non-reduced 2nd class fare price
String fareClass = fare.getString("class");
if (!fare.getBoolean("reduced") && (fareClass.equals("none") || fareClass.equals("second"))) {
farePrice = (fare.getInt("eurocents") / 100);
break;
}
}
return new Fare(fareLeg.getString("operatorString"), Fare.Type.ADULT, Currency.getInstance("EUR"), farePrice,
null, null);
}
private Departure departureFromJSONObject(JSONObject departure) throws JSONException {
JSONObject mode = departure.getJSONObject("mode");
/* String lineName = */ departure.optString("service");
Product lineProduct = productFromMode(mode.getString("type"), mode.getString("name"));
return new Departure(timeFromJSONObject(departure, "time"), timeFromJSONObject(departure, "time"),
new Line(null, departure.getString("operatorName"), lineProduct,
!departure.isNull("service") ? departure.getString("service") : mode.getString("name"), null,
Standard.STYLES.get(lineProduct), null, null),
!departure.isNull("platform") ? new Position(departure.getString("platform")) : null,
new Location(LocationType.STATION, null, null, departure.getString("destinationName")), null,
!departure.isNull("realtimeText") ? departure.optString("realtimeText") : null);
}
private Position positionFromJSONObject(JSONObject obj, String key) throws JSONException {
String position = obj.getString(key);
if (position != null && !position.equals("null")) {
return new Position(position);
} else {
return null;
}
}
private Location locationFromJSONObject(JSONObject location) throws JSONException {
return locationFromJSONObject(location, true);
}
private Location locationFromJSONObject(JSONObject location, boolean addTypePrefix) throws JSONException {
JSONObject latlon = location.getJSONObject("latLong");
JSONObject place = location.optJSONObject("place");
String locationType = location.getString("type");
String locationName = location.optString("name", null);
if (locationName != null) {
if (addTypePrefix && !location.isNull(locationType + "Type") && !locationType.equals("poi")) {
locationName = location.getString(locationType + "Type") + " " + locationName;
}
if (locationType.equals("address")) {
String houseNumber = location.optString("houseNr");
if (!houseNumber.isEmpty()) {
locationName = locationName + " " + houseNumber;
}
}
}
Point locationPoint = Point.fromDouble(latlon.getDouble("lat"), latlon.getDouble("long"));
return new Location(locationTypeFromTypeString(locationType), location.getString("id"), locationPoint,
!(place == null) ? place.optString("name", null) : null, locationName, null);
}
private List solveAmbiguousLocation(Location location) throws IOException {
if (location.hasId()) {
return Arrays.asList(location);
} else if (location.hasCoord()) {
return queryNearbyLocations(EnumSet.of(location.type), location, -1, -1).locations;
} else if (location.hasName()) {
return queryLocationsByName(location.name, EnumSet.of(location.type));
} else {
return null;
}
}
private QueryTripsResult ambiguousQueryTrips(Location from, @Nullable Location via, Location to)
throws IOException {
List ambiguousFrom = solveAmbiguousLocation(from);
if (ambiguousFrom == null || ambiguousFrom.size() <= 0)
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.UNKNOWN_FROM);
List ambiguousTo = solveAmbiguousLocation(to);
if (ambiguousTo == null || ambiguousTo.size() <= 0)
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.UNKNOWN_TO);
List ambiguousVia = null;
if (via != null) {
ambiguousVia = solveAmbiguousLocation(via);
if (ambiguousVia == null || ambiguousVia.size() <= 0)
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.UNKNOWN_VIA);
}
return new QueryTripsResult(this.resultHeader, ambiguousFrom, ambiguousVia, ambiguousTo);
}
private QueryTripsResult queryTrips(HttpUrl url, Location from, @Nullable Location via, Location to)
throws IOException {
final CharSequence page;
try {
page = httpClient.get(url);
} catch (InternalErrorException e) {
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.SERVICE_DOWN);
}
List foundTrips = new ArrayList<>();
String tripsEarlier, tripsLater;
try {
final JSONObject head = new JSONObject(page.toString());
if (head.has("error")) {
switch (head.getString("error")) {
case "WithinWalkingDistance":
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.TOO_CLOSE);
case "DateOutOfRange":
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.INVALID_DATE);
case "UnknownLocations":
String errorDetails = head.getString("details");
if (errorDetails.startsWith("From:")) {
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.UNKNOWN_FROM);
} else if (errorDetails.startsWith("Via:")) {
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.UNKNOWN_VIA);
} else if (errorDetails.startsWith("To:")) {
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.UNKNOWN_TO);
} else {
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.UNRESOLVABLE_ADDRESS);
}
default:
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.NO_TRIPS);
}
}
if (head.has("exception")) {
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.NO_TRIPS);
}
final JSONArray trips = head.optJSONArray("journeys");
final JSONArray disturbances = head.optJSONArray("disturbances");
// Prepare disturbances mapping for leg messages
Map disturbancesMap;
if (disturbances != null && disturbances.length() > 0) {
disturbancesMap = new HashMap<>();
for (int i = 0; i < disturbances.length(); i++) {
JSONObject disturbance = disturbances.getJSONObject(i);
disturbancesMap.put(disturbance.getString("plannerDisturbanceId"), disturbance);
}
} else {
disturbancesMap = null;
}
tripsEarlier = head.optString("earlier");
tripsLater = head.optString("later");
for (int i = 0; i < trips.length(); i++) {
JSONObject trip = trips.getJSONObject(i);
// Skip impossible or cancelled trips
JSONObject realtimeInfo = trip.optJSONObject("realtimeInfo");
if (realtimeInfo != null && ("fatal".equals(realtimeInfo.optString("delays"))
|| "cancellations".equals(realtimeInfo.optString("cancellations"))))
continue;
foundTrips.add(tripFromJSONObject(trip, from, to, disturbancesMap));
}
} catch (final JSONException x) {
throw new RuntimeException("cannot parse: '" + page + "' on " + url, x);
}
return new QueryTripsResult(null, url.toString(), from, via, to,
new TripsContext(url, tripsEarlier, tripsLater, from, via, to), foundTrips);
}
@Override
public Set defaultProducts() {
return EnumSet.of(Product.HIGH_SPEED_TRAIN, Product.REGIONAL_TRAIN, Product.SUBURBAN_TRAIN, Product.SUBWAY,
Product.TRAM, Product.BUS, Product.FERRY);
}
@Override
protected boolean hasCapability(Capability capability) {
switch (capability) {
case SUGGEST_LOCATIONS:
case NEARBY_LOCATIONS:
case DEPARTURES:
case TRIPS:
return true;
default:
return false;
}
}
@Override
public NearbyLocationsResult queryNearbyLocations(Set types, Location location, int maxDistance,
int maxLocations) throws IOException {
// Coordinates are required
if (!location.hasCoord()) {
try {
if (location.hasId()) {
location = queryLocationById(location.id);
} else if (location.hasName()) {
location = queryLocationByName(location.name, EnumSet.of(location.type));
}
} catch (InternalErrorException | NotFoundException | RuntimeException e) {
return new NearbyLocationsResult(this.resultHeader, NearbyLocationsResult.Status.INVALID_ID);
} catch (IOException e) {
return new NearbyLocationsResult(this.resultHeader, NearbyLocationsResult.Status.SERVICE_DOWN);
}
if (location == null || !location.hasCoord()) {
return new NearbyLocationsResult(this.resultHeader, NearbyLocationsResult.Status.INVALID_ID);
}
}
// Default query options
List queryParameters = new ArrayList<>();
queryParameters.add(new QueryParameter("latlong", location.getLatAsDouble() + "," + location.getLonAsDouble()));
queryParameters.add(new QueryParameter("rows",
String.valueOf(Math.min((maxLocations <= 0) ? DEFAULT_MAX_LOCATIONS : maxLocations, 100))));
// Add types if specified
String locationTypes = locationTypesToQueryParameterString(types);
if (locationTypes.length() > 0)
queryParameters.add(new QueryParameter("type", locationTypes));
HttpUrl url = buildApiUrl("locations", queryParameters);
CharSequence page;
try {
page = httpClient.get(url);
} catch (InternalErrorException e) {
return new NearbyLocationsResult(this.resultHeader, NearbyLocationsResult.Status.SERVICE_DOWN);
}
// Parse result into location list
final List foundLocations = new ArrayList<>();
try {
final JSONObject head = new JSONObject(page.toString());
final JSONArray locations = head.optJSONArray("locations");
for (int i = 0; i < locations.length(); i++) {
foundLocations.add(locationFromJSONObject(locations.getJSONObject(i)));
}
} catch (final JSONException x) {
throw new RuntimeException("cannot parse: '" + page + "' on " + url, x);
}
return new NearbyLocationsResult(new ResultHeader(network, SERVER_PRODUCT), foundLocations);
}
@Override
public QueryDeparturesResult queryDepartures(String stationId, @Nullable Date time, int maxDepartures,
boolean equivs) throws IOException {
// The stationId does not need the / character escaped
HttpUrl url = buildApiUrl("locations/" + stationId + "/departure-times", new ArrayList());
final CharSequence page;
try {
page = httpClient.get(url);
} catch (InternalErrorException | NotFoundException e) {
return new QueryDeparturesResult(this.resultHeader, QueryDeparturesResult.Status.INVALID_STATION);
} catch (Exception e) {
return new QueryDeparturesResult(this.resultHeader, QueryDeparturesResult.Status.SERVICE_DOWN);
}
QueryDeparturesResult queryDeparturesResult = new QueryDeparturesResult(this.resultHeader);
try {
JSONObject head = new JSONObject(page.toString());
JSONArray tabs = head.getJSONArray("tabs");
for (int t = 0; t < tabs.length(); t++) {
JSONObject tab = tabs.getJSONObject(t);
JSONArray locations = tab.getJSONArray("locations");
for (int l = 0; l < locations.length(); l++) {
JSONObject location = locations.getJSONObject(l);
// Ignore if equivs is false and stationId is not a strict match
if (!equivs && !location.getString("id").equals(stationId)) {
continue;
}
// Get list of departures
List departuresResult = new ArrayList<>();
List lineDestinationResult = new ArrayList<>();
JSONArray departures = tab.getJSONArray("departures");
for (int i = 0; i < departures.length(); i++) {
JSONObject departure = departures.getJSONObject(i);
JSONObject mode = departure.getJSONObject("mode");
departuresResult.add(departureFromJSONObject(departure));
Product lineProduct = productFromMode(mode.getString("type"), mode.getString("name"));
lineDestinationResult.add(new LineDestination(
new Line(null, departure.getString("operatorName"), lineProduct, mode.getString("name"),
null, Standard.STYLES.get(lineProduct), null, null),
new Location(LocationType.STATION, null, null, null,
departure.getString("destinationName"), EnumSet.of(lineProduct))));
}
// Add to result object
queryDeparturesResult.stationDepartures.add(new StationDepartures(locationFromJSONObject(location),
departuresResult, lineDestinationResult));
}
}
return queryDeparturesResult;
} catch (final JSONException x) {
throw new RuntimeException("cannot parse: '" + page + "' on " + url, x);
}
}
@Override
public SuggestLocationsResult suggestLocations(CharSequence constraint) throws IOException {
HttpUrl url = buildApiUrl("locations", Arrays.asList(new QueryParameter("q", constraint.toString())));
final CharSequence page;
try {
page = httpClient.get(url);
} catch (InternalErrorException e) {
return new SuggestLocationsResult(this.resultHeader, SuggestLocationsResult.Status.SERVICE_DOWN);
}
final List foundLocations = new ArrayList<>();
try {
final JSONObject head = new JSONObject(page.toString());
final JSONArray locations = head.optJSONArray("locations");
if (head.has("error")) {
return new SuggestLocationsResult(this.resultHeader, SuggestLocationsResult.Status.SERVICE_DOWN);
}
for (int i = 0; i < locations.length(); i++) {
JSONObject location = locations.getJSONObject(i);
foundLocations.add(new SuggestedLocation(locationFromJSONObject(location)));
}
} catch (final JSONException x) {
throw new RuntimeException("cannot parse: '" + page + "' on " + url, x);
}
return new SuggestLocationsResult(this.resultHeader, foundLocations);
}
@Override
public QueryTripsResult queryTrips(Location from, @Nullable Location via, Location to, Date date, boolean dep,
@Nullable TripOptions options) throws IOException {
if (!(from.hasId() || from.hasCoord()))
return ambiguousQueryTrips(from, via, to);
if (!(to.hasId() || to.hasCoord()))
return ambiguousQueryTrips(from, via, to);
// Default query options
List queryParameters = new ArrayList<>(
Arrays.asList(new QueryParameter("from", locationToQueryParameterString(from)),
new QueryParameter("to", locationToQueryParameterString(to)),
new QueryParameter("searchType", dep ? "departure" : "arrival"),
new QueryParameter("dateTime", formatApiDateTime(date)),
new QueryParameter("sequence", "1"), new QueryParameter("realtime", "true"),
new QueryParameter("before", "1"), new QueryParameter("after", "5")));
if (via != null) {
if (!(via.hasId() || via.hasCoord()))
return ambiguousQueryTrips(from, via, to);
queryParameters.add(new QueryParameter("via", locationToQueryParameterString(via)));
}
if (options == null)
options = new TripOptions();
if (options.walkSpeed != null && options.walkSpeed == WalkSpeed.SLOW) {
queryParameters.add(new QueryParameter("interchangeTime", InterchangeTime.EXTRA.toString()));
} else {
queryParameters.add(new QueryParameter("interchangeTime", InterchangeTime.STANDARD.toString()));
}
// Add trip product options to query
Set products = options.products;
if (products == null || products.size() == 0) {
products = defaultProducts();
}
queryParameters.add(new QueryParameter("byBus", String.valueOf(products.contains(Product.BUS))));
queryParameters.add(new QueryParameter("byTrain", String.valueOf(products.contains(Product.HIGH_SPEED_TRAIN)
|| products.contains(Product.REGIONAL_TRAIN) || products.contains(Product.SUBURBAN_TRAIN))));
queryParameters.add(new QueryParameter("bySubway", String.valueOf(products.contains(Product.SUBWAY))));
queryParameters.add(new QueryParameter("byTram", String.valueOf(products.contains(Product.TRAM))));
queryParameters.add(new QueryParameter("byFerry", String.valueOf(products.contains(Product.FERRY))));
return queryTrips(buildApiUrl("journeys", queryParameters), from, via, to);
}
@Override
public QueryTripsResult queryMoreTrips(QueryTripsContext context, boolean later) throws IOException {
TripsContext tripContext = (TripsContext) context;
HttpUrl url;
if (later && context.canQueryLater()) {
url = tripContext.getQueryLater();
} else if (!later && context.canQueryEarlier()) {
url = tripContext.getQueryEarlier();
} else {
return new QueryTripsResult(this.resultHeader, QueryTripsResult.Status.NO_TRIPS);
}
return queryTrips(url, tripContext.from, tripContext.via, tripContext.to);
}
}