/* * Copyright 2014-2015 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.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; import de.schildbach.pte.dto.Departure; 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.NearbyStationsResult; import de.schildbach.pte.dto.NearbyStationsResult.Status; 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.Style; import de.schildbach.pte.dto.Style.Shape; import de.schildbach.pte.dto.SuggestLocationsResult; import de.schildbach.pte.dto.SuggestedLocation; import de.schildbach.pte.dto.Trip; import de.schildbach.pte.dto.Trip.Individual; import de.schildbach.pte.dto.Trip.Leg; import de.schildbach.pte.dto.Trip.Public; import de.schildbach.pte.exception.NotFoundException; import de.schildbach.pte.exception.ParserException; import de.schildbach.pte.util.ParserUtils; import de.schildbach.pte.util.WordUtils; /** * @author Antonio El Khoury * @author Andreas Schildbach */ public abstract class AbstractNavitiaProvider extends AbstractNetworkProvider { protected final static String SERVER_PRODUCT = "navitia"; protected final static String SERVER_VERSION = "v1"; protected static final String API_BASE = "http://api.navitia.io/" + SERVER_VERSION + "/"; private enum PlaceType { ADDRESS, ADMINISTRATIVE_REGION, POI, STOP_POINT, STOP_AREA } private enum SectionType { CROW_FLY, PUBLIC_TRANSPORT, STREET_NETWORK, TRANSFER, WAITING } private enum TransferType { BIKE, WALKING } private enum CommercialMode { BUS, TRAIN, TRAM, TRAMWAY, METRO, FERRY, CABLECAR, RAPIDTRANSIT, FUNICULAR, DEFAULT_COMMERCIAL_MODE } private static class Context implements QueryTripsContext { private Location from; private Location to; private final String prevQueryUri; private final String nextQueryUri; private Context(final Location from, final Location to, final String prevQueryUri, final String nextQueryUri) { this.from = from; this.to = to; this.prevQueryUri = prevQueryUri; this.nextQueryUri = nextQueryUri; } public boolean canQueryLater() { return (from != null && to != null && nextQueryUri != null); } public boolean canQueryEarlier() { return (from != null && to != null && prevQueryUri != null); } @Override public String toString() { return getClass().getName() + "[" + from + "|" + to + "|" + prevQueryUri + "|" + nextQueryUri + "]"; } } private final String authorization; public AbstractNavitiaProvider(final String authorization) { this.authorization = authorization; } protected abstract String region(); protected int computeForegroundColor(final String lineColor) { int bgColor = Style.parseColor(lineColor); return Style.deriveForegroundColor(bgColor); } protected Style getLineStyle(final char product, final String code, final String color) { return new Style(Shape.RECT, Style.parseColor(color), computeForegroundColor(color)); } private String uri() { return API_BASE + "coverage/" + region() + "/"; } private String tripUri() { return API_BASE; } private Point parseCoord(final JSONObject coord) throws IOException { try { final float lat = (float) coord.getDouble("lat"); final float lon = (float) coord.getDouble("lon"); return new Point(lat, lon); } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } } private Location parseStopPoint(final JSONObject stopPoint) throws IOException { try { final String id = stopPoint.getString("id"); final JSONObject coord = stopPoint.getJSONObject("coord"); final Point point = parseCoord(coord); final String name = WordUtils.capitalizeFully(stopPoint.getString("name")); return new Location(LocationType.STATION, id, point.lat, point.lon, null, name); } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } } private Location parseLocation(final JSONObject place) throws IOException { try { final String type = place.getString("embedded_type"); final PlaceType placeType = PlaceType.valueOf(type.toUpperCase()); switch (placeType) { case STOP_POINT: { final JSONObject stopPoint = place.getJSONObject("stop_point"); return parseStopPoint(stopPoint); } case STOP_AREA: { final JSONObject stopArea = place.getJSONObject("stop_area"); return parseStopPoint(stopArea); } case ADDRESS: { final JSONObject address = place.getJSONObject("address"); final String id = address.getString("id"); final JSONObject coord = address.getJSONObject("coord"); final Point point = parseCoord(coord); final String name = WordUtils.capitalizeFully(place.getString("name")); return new Location(LocationType.ADDRESS, id, point.lat, point.lon, null, name); } case POI: { final JSONObject poi = place.getJSONObject("poi"); final String id = poi.getString("id"); final JSONObject coord = poi.getJSONObject("coord"); final Point point = parseCoord(coord); final String name = WordUtils.capitalizeFully(poi.getString("name")); return new Location(LocationType.ADDRESS, id, point.lat, point.lon, null, name); } default: throw new IllegalArgumentException("Unhandled place type: " + type); } } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } } private String printLocation(final Location location) { if (location.hasId()) return location.id; else if (location.hasLocation()) return (double) (location.lon) / 1E6 + ";" + (double) (location.lat) / 1E6; else return ""; } private Date parseDate(final String dateString) throws ParseException { return new SimpleDateFormat("yyyyMMdd'T'HHmmss").parse(dateString); } private String printDate(final Date date) { return new SimpleDateFormat("yyyyMMdd'T'HHmmss").format(date); } private LinkedList parsePath(final JSONArray coordinates) throws IOException { LinkedList path = new LinkedList(); for (int i = 0; i < coordinates.length(); ++i) { try { final JSONArray jsonPoint = coordinates.getJSONArray(i); final int lon = (int) (jsonPoint.getDouble(0) * 1E6); final int lat = (int) (jsonPoint.getDouble(1) * 1E6); final Point point = new Point(lat, lon); path.add(point); } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } } return path; } private class LegInfo { public final Location departure; public final Date departureTime; public final Location arrival; public final Date arrivalTime; public final List path; public final int distance; public final int min; public LegInfo(final Location departure, final Date departureTime, final Location arrival, final Date arrivalTime, final List path, final int distance, final int min) { this.departure = departure; this.departureTime = departureTime; this.arrival = arrival; this.arrivalTime = arrivalTime; this.path = path; this.distance = distance; this.min = min; } } private LegInfo parseLegInfo(final JSONObject section) throws IOException { try { final String type = section.getString("type"); if (!type.equals("waiting")) { // Build departure location. final JSONObject sectionFrom = section.getJSONObject("from"); final Location departure = parseLocation(sectionFrom); // Build departure time. final String departureDateTime = section.getString("departure_date_time"); final Date departureTime = parseDate(departureDateTime); // Build arrival location. final JSONObject sectionTo = section.getJSONObject("to"); final Location arrival = parseLocation(sectionTo); // Build arrival time. final String arrivalDateTime = section.getString("arrival_date_time"); final Date arrivalTime = parseDate(arrivalDateTime); // Build path and distance. Check first that geojson // object exists. LinkedList path = null; int distance = 0; if (section.has("geojson")) { final JSONObject jsonPath = section.getJSONObject("geojson"); final JSONArray coordinates = jsonPath.getJSONArray("coordinates"); path = parsePath(coordinates); final JSONArray properties = jsonPath.getJSONArray("properties"); for (int i = 0; i < properties.length(); ++i) { final JSONObject property = properties.getJSONObject(i); if (property.has("length")) { distance = property.getInt("length"); break; } } } // Build duration in min. final int min = section.getInt("duration") / 60; return new LegInfo(departure, departureTime, arrival, arrivalTime, path, distance, min); } else { return null; } } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } catch (final ParseException parseExc) { throw new ParserException(parseExc); } } private Line parseLineFromSection(final JSONObject section) throws IOException { try { final JSONArray links = section.getJSONArray("links"); String lineId = null; String modeId = null; for (int i = 0; i < links.length(); ++i) { final JSONObject link = links.getJSONObject(i); final String linkType = link.getString("type"); if (linkType.equals("line")) lineId = link.getString("id"); else if (linkType.equals("commercial_mode")) modeId = link.getString("id"); } final char product = parseLineProductFromMode(modeId); final JSONObject displayInfo = section.getJSONObject("display_informations"); final String code = displayInfo.getString("code"); final String lineLabel = product + code; final String colorHex = displayInfo.getString("color"); final String color = colorHex.equals("000000") ? "#FFFFFF" : "#" + colorHex; final Style lineStyle = getLineStyle(product, code, color); return new Line(lineId, lineLabel, lineStyle); } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } } private Stop parseStop(final JSONObject stopDateTime) throws IOException { try { // Build location. final JSONObject stopPoint = stopDateTime.getJSONObject("stop_point"); final Location location = parseStopPoint(stopPoint); // Build planned arrival time. final Date plannedArrivalTime = parseDate(stopDateTime.getString("arrival_date_time")); // Build planned arrival position. final Position plannedArrivalPosition = null; // Build planned departure time. final Date plannedDepartureTime = parseDate(stopDateTime.getString("departure_date_time")); // Build planned departure position. final Position plannedDeparturePosition = null; return new Stop(location, plannedArrivalTime, plannedArrivalPosition, plannedDepartureTime, plannedDeparturePosition); } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } catch (final ParseException parseExc) { throw new ParserException(parseExc); } } private Leg parseLeg(final JSONObject section) throws IOException { try { // Build common leg info. final LegInfo legInfo = parseLegInfo(section); if (legInfo == null) return null; final String type = section.getString("type"); final SectionType sectionType = SectionType.valueOf(type.toUpperCase()); switch (sectionType) { case CROW_FLY: { // Build type. final Individual.Type individualType = Individual.Type.WALK; return new Individual(individualType, legInfo.departure, legInfo.departureTime, legInfo.arrival, legInfo.arrivalTime, legInfo.path, legInfo.distance); } case PUBLIC_TRANSPORT: { // Build line. final Line line = parseLineFromSection(section); // Build destination. final JSONObject displayInfo = section.getJSONObject("display_informations"); final String direction = displayInfo.getString("direction"); final Location destination = new Location(LocationType.STATION, direction, direction, direction); final JSONArray stopDateTimes = section.getJSONArray("stop_date_times"); final int nbStopDateTime = stopDateTimes.length(); // Build departure stop. final Stop departureStop = parseStop(stopDateTimes.getJSONObject(0)); // Build arrival stop. final Stop arrivalStop = parseStop(stopDateTimes.getJSONObject(nbStopDateTime - 1)); // Build intermediate stops. final LinkedList intermediateStops = new LinkedList(); for (int i = 1; i < nbStopDateTime - 1; ++i) { final Stop intermediateStop = parseStop(stopDateTimes.getJSONObject(i)); intermediateStops.add(intermediateStop); } // Build message. final String message = null; return new Public(line, destination, departureStop, arrivalStop, intermediateStops, legInfo.path, message); } case STREET_NETWORK: { final String modeType = section.getString("mode"); final TransferType transferType = TransferType.valueOf(modeType.toUpperCase()); // Build type. final Individual.Type individualType; switch (transferType) { case BIKE: individualType = Individual.Type.BIKE; break; case WALKING: individualType = Individual.Type.WALK; break; default: throw new IllegalArgumentException("Unhandled transfer type: " + modeType); } return new Individual(individualType, legInfo.departure, legInfo.departureTime, legInfo.arrival, legInfo.arrivalTime, legInfo.path, legInfo.distance); } case TRANSFER: { // Build type. final Individual.Type individualType = Individual.Type.WALK; return new Individual(individualType, legInfo.departure, legInfo.departureTime, legInfo.arrival, legInfo.arrivalTime, legInfo.path, legInfo.distance); } case WAITING: { return null; // Do not add leg in case of waiting on the peer. } default: throw new IllegalArgumentException("Unhandled place type: " + type); } } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } } private void parseQueryTripsResult(final JSONObject head, final Location from, final Location to, final QueryTripsResult result) throws IOException { try { // Fill trips. final JSONArray journeys = head.getJSONArray("journeys"); for (int i = 0; i < journeys.length(); ++i) { final JSONObject journey = journeys.getJSONObject(i); final int changeCount = journey.getInt("nb_transfers"); // Build leg list. final List legs = new LinkedList(); final JSONArray sections = journey.getJSONArray("sections"); for (int j = 0; j < sections.length(); ++j) { final JSONObject section = sections.getJSONObject(j); final Leg leg = parseLeg(section); if (leg != null) legs.add(leg); } result.trips.add(new Trip(null, from, to, legs, null, null, changeCount)); } } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } } private Line parseLine(final JSONObject jsonLine) throws IOException { try { final String lineId = jsonLine.getString("id"); final char product = parseLineProduct(jsonLine); final String code = jsonLine.getString("code"); final String lineLabel = product + code; final String color = "#" + jsonLine.getString("color"); final Style lineStyle = getLineStyle(product, code, color); return new Line(lineId, lineLabel, lineStyle); } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } } private Map lineProductCache = new WeakHashMap(); private char parseLineProductFromMode(final String modeId) { final String modeType = modeId.replace("commercial_mode:", ""); final CommercialMode commercialMode = CommercialMode.valueOf(modeType.toUpperCase()); switch (commercialMode) { case BUS: return 'B'; case RAPIDTRANSIT: case TRAIN: return 'S'; case TRAM: case TRAMWAY: return 'T'; case METRO: return 'U'; case FERRY: return 'F'; case FUNICULAR: case CABLECAR: return 'C'; case DEFAULT_COMMERCIAL_MODE: default: throw new IllegalArgumentException("Unhandled place type: " + modeId); } } private char parseLineProduct(final JSONObject line) throws IOException { try { final String lineId = line.getString("id"); final Character cachedProduct = lineProductCache.get(lineId); if (cachedProduct != null) return cachedProduct; final JSONObject mode = line.getJSONObject("commercial_mode"); final String modeId = mode.getString("id"); final char product = parseLineProductFromMode(modeId); lineProductCache.put(lineId, product); return product; } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } } private LineDestination parseLineDestination(final JSONObject route) throws IOException { try { // Build line. final JSONObject jsonLine = route.getJSONObject("line"); final Line line = parseLine(jsonLine); // Build line destination. final JSONObject direction = route.getJSONObject("direction"); Location destination = parseLocation(direction); return new LineDestination(line, destination); } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } } private List getStationLines(final String stopPointId) throws IOException { final String uri = uri() + "stop_points/" + ParserUtils.urlEncode(stopPointId) + "/routes?depth=2"; final CharSequence page = ParserUtils.scrape(uri, authorization); try { final JSONObject head = new JSONObject(page.toString()); final JSONArray routes = head.getJSONArray("routes"); final List lineDestinations = new LinkedList(); for (int i = 0; i < routes.length(); ++i) { final JSONObject route = routes.getJSONObject(i); final LineDestination lineDestination = parseLineDestination(route); lineDestinations.add(lineDestination); } return lineDestinations; } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } } private String getStopAreaId(final String stopPointId) throws IOException { final String uri = uri() + "stop_points/" + ParserUtils.urlEncode(stopPointId) + "?depth=1"; final CharSequence page = ParserUtils.scrape(uri, authorization); try { final JSONObject head = new JSONObject(page.toString()); final JSONArray stopPoints = head.getJSONArray("stop_points"); final JSONObject stopPoint = stopPoints.getJSONObject(0); final JSONObject stopArea = stopPoint.getJSONObject("stop_area"); return stopArea.getString("id"); } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } } private boolean isStationActive(final Location location) throws IOException { try { final String stationId = location.id; final StringBuilder queryUri = new StringBuilder(); queryUri.append(uri()); queryUri.append("stop_points/" + stationId + "/"); final Calendar now = Calendar.getInstance(timeZone); final String dateTime = printDate(now.getTime()); // Look for at least one departure in less than an hour. queryUri.append("departures?from_datetime=" + dateTime + "&count=" + 1 + "&duration=3600" + "&depth=0"); final CharSequence page = ParserUtils.scrape(queryUri.toString(), authorization); final JSONObject head = new JSONObject(page.toString()); final JSONArray departures = head.getJSONArray("departures"); if (departures.length() == 0) return false; else return true; } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } } @Override protected boolean hasCapability(final Capability capability) { if (capability == Capability.SUGGEST_LOCATIONS || capability == Capability.NEARBY_STATIONS || capability == Capability.DEPARTURES || capability == Capability.TRIPS) return true; else return false; } public NearbyStationsResult queryNearbyStations(final Location location, final int maxDistance, final int maxStations) throws IOException { final ResultHeader resultHeader = new ResultHeader(SERVER_PRODUCT, SERVER_VERSION, 0, null); // Check that Location object has coordinates. if (!location.isIdentified()) throw new IllegalArgumentException(); // Build query uri depending of location type. final String queryUriType; if (location.type == LocationType.ADDRESS) { final double lon = location.lon / 1E6; final double lat = location.lat / 1E6; queryUriType = "coords/" + lon + ";" + lat + "/"; } else if (location.type == LocationType.STATION) { queryUriType = "stop_point/" + location.id + "/"; } else if (location.type == LocationType.POI) { queryUriType = "poi/" + location.id + "/"; } else { throw new IllegalArgumentException("Unhandled location type: " + location.type); } final String queryUri = uri() + queryUriType + "places_nearby?type[]=stop_point" + "&distance=" + maxDistance + "&count=" + maxStations + "&depth=0"; final CharSequence page = ParserUtils.scrape(queryUri, authorization); // System.out.println(queryUri); try { final JSONObject head = new JSONObject(page.toString()); final JSONObject pagination = head.getJSONObject("pagination"); final int nbResults = pagination.getInt("total_result"); // If no result is available, location id must be // faulty. if (nbResults == 0) { return new NearbyStationsResult(resultHeader, Status.INVALID_STATION); } else { final List stations = new ArrayList(); final JSONArray places = head.getJSONArray("places_nearby"); // Cycle through nearby stations. for (int i = 0; i < places.length(); ++i) { final JSONObject place = places.getJSONObject(i); // Add location to station list only if // station is active, i.e. at least one // departure exists within one hour. final Location nearbyLocation = parseLocation(place); if (isStationActive(nearbyLocation)) stations.add(nearbyLocation); } return new NearbyStationsResult(resultHeader, stations); } } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } } public QueryDeparturesResult queryDepartures(final String stationId, final Date time, final int maxDepartures, final boolean equivs) throws IOException { final ResultHeader resultHeader = new ResultHeader(SERVER_PRODUCT, SERVER_VERSION, 0, null); try { final QueryDeparturesResult result = new QueryDeparturesResult(resultHeader, QueryDeparturesResult.Status.OK); final String dateTime = printDate(time); // If equivs is equal to true, get stop_area corresponding // to stop_point and query departures. final StringBuilder queryUri = new StringBuilder(); queryUri.append(uri()); if (equivs) { final String stopAreaId = getStopAreaId(stationId); queryUri.append("stop_areas/" + stopAreaId + "/"); } else { queryUri.append("stop_points/" + stationId + "/"); } queryUri.append("departures?from_datetime=" + dateTime + "&count=" + maxDepartures + "&duration=3600" + "&depth=0"); final CharSequence page = ParserUtils.scrape(queryUri.toString(), authorization); // System.out.println(queryUri); final JSONObject head = new JSONObject(page.toString()); final JSONArray departures = head.getJSONArray("departures"); // Fill departures in StationDepartures. for (int i = 0; i < departures.length(); ++i) { final JSONObject jsonDeparture = departures.getJSONObject(i); final JSONObject stopPoint = jsonDeparture.getJSONObject("stop_point"); final Location location = parseStopPoint(stopPoint); // If stop point has already been added, retrieve it // from result, otherwise add it and add station // lines. StationDepartures stationDepartures = result.findStationDepartures(location.id); if (stationDepartures == null) { stationDepartures = new StationDepartures(location, new LinkedList(), new LinkedList()); result.stationDepartures.add(stationDepartures); final List lineDestinations = getStationLines(location.id); for (LineDestination lineDestination : lineDestinations) stationDepartures.lines.add(lineDestination); } // Build departure date. final JSONObject stopDateTime = jsonDeparture.getJSONObject("stop_date_time"); final String departureDateTime = stopDateTime.getString("departure_date_time"); final Date plannedTime = parseDate(departureDateTime); // Build line. final JSONObject route = jsonDeparture.getJSONObject("route"); final JSONObject jsonLine = route.getJSONObject("line"); final Line line = parseLine(jsonLine); final Location destination = findLineDestination(stationDepartures.lines, line).destination; // Add departure to list. final Departure departure = new Departure(plannedTime, null, line, null, destination, null, null); stationDepartures.departures.add(departure); } return result; } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } catch (final ParseException parseExc) { throw new ParserException(parseExc); } catch (final NotFoundException fnfExc) { try { final JSONObject head = new JSONObject(fnfExc.scrapeErrorStream().toString()); final JSONObject error = head.getJSONObject("error"); final String id = error.getString("id"); if (id.equals("unknown_object")) return new QueryDeparturesResult(resultHeader, QueryDeparturesResult.Status.INVALID_STATION); else throw new IllegalArgumentException("Unhandled error id: " + id); } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } } } private LineDestination findLineDestination(final List lineDestinations, final Line line) { for (final LineDestination lineDestination : lineDestinations) if (lineDestination.line.equals(line)) return lineDestination; return null; } public SuggestLocationsResult suggestLocations(final CharSequence constraint) throws IOException { final String nameCstr = constraint.toString(); final String queryUri = uri() + "places?q=" + ParserUtils.urlEncode(nameCstr) + "&type[]=stop_area&type[]=address" + "&depth=1"; final CharSequence page = ParserUtils.scrape(queryUri, authorization); // System.out.println(queryUri); try { final List locations = new ArrayList(); final JSONObject head = new JSONObject(page.toString()); if (head.has("places")) { final JSONArray places = head.getJSONArray("places"); for (int i = 0; i < places.length(); ++i) { final JSONObject place = places.getJSONObject(i); // Add location to station list. final Location location = parseLocation(place); locations.add(new SuggestedLocation(location)); } } final ResultHeader resultHeader = new ResultHeader(SERVER_PRODUCT, SERVER_VERSION, 0, null); return new SuggestLocationsResult(resultHeader, locations); } catch (final JSONException jsonExc) { throw new ParserException(jsonExc); } } public QueryTripsResult queryTrips(final Location from, final Location via, final Location to, final Date date, final boolean dep, final Collection products, final WalkSpeed walkSpeed, final Accessibility accessibility, final Set