de.schildbach.pte.AbstractNavitiaProvider.java Source code

Java tutorial

Introduction

Here is the source code for de.schildbach.pte.AbstractNavitiaProvider.java

Source

/*
 * 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 <http://www.gnu.org/licenses/>.
 */

package de.schildbach.pte;

import static com.google.common.base.Preconditions.checkNotNull;

import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import javax.annotation.Nullable;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

import com.google.common.base.Strings;

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.NearbyLocationsResult;
import de.schildbach.pte.dto.NearbyLocationsResult.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 okhttp3.HttpUrl;

/**
 * @author Antonio El Khoury
 * @author Andreas Schildbach
 * @author Torsten Grote
 */
public abstract class AbstractNavitiaProvider extends AbstractNetworkProvider {
    protected final static String SERVER_PRODUCT = "navitia";
    protected final static String SERVER_VERSION = "v1";

    protected HttpUrl apiBase = HttpUrl.parse("https://api.navitia.io/").newBuilder().addPathSegment(SERVER_VERSION)
            .build();

    private enum PlaceType {
        ADDRESS, ADMINISTRATIVE_REGION, POI, STOP_POINT, STOP_AREA
    }

    private enum SectionType {
        CROW_FLY, PUBLIC_TRANSPORT, STREET_NETWORK, TRANSFER, WAITING, STAY_IN, ON_DEMAND_TRANSPORT, BSS_RENT, BSS_PUT_BACK, BOARDING, LANDING
    }

    private enum TransferType {
        BIKE, WALKING
    }

    private enum PhysicalMode {
        AIR, BOAT, BUS, BUSRAPIDTRANSIT, COACH, FERRY, FUNICULAR, LOCALTRAIN, LONGDISTANCETRAIN, METRO, RAPIDTRANSIT, SHUTTLE, TAXI, TRAIN, TRAMWAY, TRAM, OTHER
    }

    @SuppressWarnings("serial")
    private static class Context implements QueryTripsContext {
        private final Location from;
        private final 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;
        }

        @Override
        public boolean canQueryLater() {
            return (from != null && to != null && nextQueryUri != null);
        }

        @Override
        public boolean canQueryEarlier() {
            return (from != null && to != null && prevQueryUri != null);
        }

        @Override
        public String toString() {
            return getClass().getName() + "[" + from + "|" + to + "|" + prevQueryUri + "|" + nextQueryUri + "]";
        }
    }

    public AbstractNavitiaProvider(final NetworkId network, final HttpUrl apiBase, final String authorization) {
        this(network, authorization);

        this.apiBase = apiBase;
    }

    public AbstractNavitiaProvider(final NetworkId network, final String authorization) {
        super(network);

        if (authorization != null)
            httpClient.setHeader("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 Product product, final String code, final String color) {
        return getLineStyle(product, code, color, null);
    }

    protected Style getLineStyle(final Product product, final String code, final String backgroundColor,
            final String foregroundColor) {
        if (backgroundColor != null) {
            if (foregroundColor == null)
                return new Style(Shape.RECT, Style.parseColor(backgroundColor),
                        computeForegroundColor(backgroundColor));
            return new Style(Shape.RECT, Style.parseColor(backgroundColor), Style.parseColor(foregroundColor));
        } else {
            final Style defaultStyle = Standard.STYLES.get(product);
            return new Style(Shape.RECT, defaultStyle.backgroundColor, defaultStyle.backgroundColor2,
                    defaultStyle.foregroundColor, defaultStyle.borderColor);
        }
    }

    private HttpUrl.Builder url() {
        return apiBase.newBuilder().addPathSegment("coverage").addPathSegment(region());
    }

    private Point parseCoord(final JSONObject coord) throws IOException {
        try {
            final double lat = coord.getDouble("lat");
            final double lon = coord.getDouble("lon");
            return Point.fromDouble(lat, lon);
        } catch (final JSONException jsonExc) {
            throw new ParserException(jsonExc);
        }
    }

    private LocationType getLocationType(PlaceType placeType) {
        switch (placeType) {
        case STOP_POINT: {
            return LocationType.STATION;
        }
        case STOP_AREA: {
            return LocationType.STATION;
        }
        case ADDRESS: {
            return LocationType.ADDRESS;
        }
        case POI: {
            return LocationType.POI;
        }
        default:
            throw new IllegalArgumentException("Unhandled place type: " + placeType);
        }
    }

    /**
     * Some Navitia providers return location names with wrong case. This method can be used to fix the name
     * when locations are parsed.
     * 
     * @param name
     *            The name of the location
     * @return the fixed name of the location
     */
    protected String getLocationName(String name) {
        return name;
    }

    private Location parsePlace(JSONObject location, PlaceType placeType) throws IOException {
        try {
            final LocationType type = getLocationType(placeType);
            String id = null;
            if (placeType != PlaceType.ADDRESS && placeType != PlaceType.POI)
                id = location.getString("id");
            final JSONObject coord = location.getJSONObject("coord");
            final Point point = parseCoord(coord);
            final String name = getLocationName(location.getString("name"));
            String place = null;
            if (location.has("administrative_regions")) {
                JSONArray admin = location.getJSONArray("administrative_regions");
                if (admin.length() > 0)
                    place = Strings.emptyToNull(admin.getJSONObject(0).optString("name"));
            }
            Set<Product> products = null;
            if (location.has("stop_area") && location.getJSONObject("stop_area").has("physical_modes")) {
                products = EnumSet.noneOf(Product.class);
                JSONArray physicalModes = location.getJSONObject("stop_area").getJSONArray("physical_modes");
                for (int i = 0; i < physicalModes.length(); i++) {
                    JSONObject mode = physicalModes.getJSONObject(i);
                    Product product = parseLineProductFromMode(mode.getString("id"));
                    if (product != null)
                        products.add(product);
                }
            }
            return new Location(type, id, point, place, name, products);
        } catch (final JSONException jsonExc) {
            throw new ParserException(jsonExc);
        }
    }

    private Location parseAdministrativeRegion(final JSONObject j) throws IOException {
        try {
            final JSONObject coord = j.getJSONObject("coord");
            final Point point = parseCoord(coord);
            final String name = j.getString("name");
            return new Location(LocationType.POI, null, point, null, name);
        } catch (final JSONException jsonExc) {
            throw new ParserException(jsonExc);
        }
    }

    private Location parseLocation(final JSONObject j) throws IOException {
        try {
            final String type = j.getString("embedded_type");
            final PlaceType placeType = PlaceType.valueOf(type.toUpperCase());
            JSONObject location;

            switch (placeType) {
            case STOP_POINT: {
                location = j.getJSONObject("stop_point");
                break;
            }
            case STOP_AREA: {
                location = j.getJSONObject("stop_area");
                break;
            }
            case ADDRESS: {
                location = j.getJSONObject("address");
                break;
            }
            case POI: {
                location = j.getJSONObject("poi");
                break;
            }
            case ADMINISTRATIVE_REGION: {
                return parseAdministrativeRegion(j.getJSONObject("administrative_region"));
            }
            default:
                throw new IllegalArgumentException("Unhandled place type: " + type);
            }
            return parsePlace(location, placeType);
        } 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<Point> parsePath(final JSONArray coordinates) throws IOException {
        LinkedList<Point> path = new LinkedList<>();

        for (int i = 0; i < coordinates.length(); ++i) {
            try {
                final JSONArray jsonPoint = coordinates.getJSONArray(i);
                final double lon = jsonPoint.getDouble(0);
                final double lat = jsonPoint.getDouble(1);
                final Point point = Point.fromDouble(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<Point> 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<Point> 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<Point> 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, final SectionType type) 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("physical_mode"))
                    modeId = link.getString("id");
            }

            final Product product = type == SectionType.ON_DEMAND_TRANSPORT ? Product.ON_DEMAND
                    : parseLineProductFromMode(modeId);
            final JSONObject displayInfo = section.getJSONObject("display_informations");
            final String network = Strings.emptyToNull(displayInfo.optString("network"));
            final String code = displayInfo.getString("code");
            final String color = Strings.emptyToNull(displayInfo.getString("color"));
            final String name = Strings.emptyToNull(displayInfo.optString("headsign"));
            final Style lineStyle = getLineStyle(product, code, color != null ? "#" + color : null);

            return new Line(lineId, network, product, code, name, 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 = parsePlace(stopPoint, PlaceType.STOP_POINT);

            // 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: {
                // Return null leg if duration is 0.
                if (legInfo.min == 0)
                    return null;

                // 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 ON_DEMAND_TRANSPORT:
            case PUBLIC_TRANSPORT: {
                // Build line.
                final Line line = parseLineFromSection(section, sectionType);

                // Build destination.
                final JSONObject displayInfo = section.getJSONObject("display_informations");
                final String direction = displayInfo.getString("direction");
                final Location destination = new Location(LocationType.ANY, null, null, getLocationName(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<Stop> 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 leg 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<Leg> 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 jsonRoute) throws IOException {
        try {
            final JSONObject jsonLine = jsonRoute.getJSONObject("line");
            final String lineId = jsonLine.getString("id");
            String network = null;
            if (jsonLine.has("network"))
                network = Strings.emptyToNull(jsonLine.getJSONObject("network").optString("name"));
            final JSONObject mode = jsonRoute.getJSONArray("physical_modes").getJSONObject(0);
            final String modeId = mode.getString("id");
            final Product product = parseLineProductFromMode(modeId);
            final String code = jsonLine.getString("code");
            final String name = Strings.emptyToNull(jsonLine.optString("name"));
            final String color = Strings.emptyToNull(jsonLine.getString("color"));
            final String textColor = Strings.emptyToNull(jsonLine.optString("text_color"));
            final Style lineStyle = getLineStyle(product, code, color != null ? "#" + color : null,
                    textColor != null ? "#" + textColor : null);

            return new Line(lineId, network, product, code, name, lineStyle);
        } catch (final JSONException jsonExc) {
            throw new ParserException(jsonExc);
        }
    }

    private @Nullable Product parseLineProductFromMode(final String modeId) {
        final String modeType = modeId.replace("physical_mode:", "");
        final PhysicalMode physicalMode = PhysicalMode.valueOf(modeType.toUpperCase());

        switch (physicalMode) {
        case BUS:
        case BUSRAPIDTRANSIT:
        case COACH:
        case SHUTTLE:
            return Product.BUS;
        case RAPIDTRANSIT:
        case TRAIN:
        case LOCALTRAIN:
        case LONGDISTANCETRAIN:
            return Product.SUBURBAN_TRAIN;
        case TRAMWAY:
        case TRAM:
            return Product.TRAM;
        case METRO:
            return Product.SUBWAY;
        case FERRY:
            return Product.FERRY;
        case FUNICULAR:
            return Product.CABLECAR;
        case TAXI:
            return Product.ON_DEMAND;
        case OTHER:
            return null;
        default:
            throw new IllegalArgumentException("Unhandled physical mode: " + modeId);
        }
    }

    private LineDestination getStationLine(final Line line, final JSONObject jsonDeparture) throws IOException {
        try {
            final JSONObject route = jsonDeparture.getJSONObject("route");
            final JSONObject direction = route.getJSONObject("direction");
            final Location destination = parseLocation(direction);

            return new LineDestination(line, destination);
        } catch (final JSONException jsonExc) {
            throw new ParserException(jsonExc);
        }
    }

    private String getStopAreaId(final String stopPointId) throws IOException {
        final HttpUrl.Builder url = url().addPathSegment("stop_points").addPathSegment(stopPointId);
        url.addQueryParameter("depth", "1");
        final CharSequence page = httpClient.get(url.build());

        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);
        }
    }

    @Override
    protected boolean hasCapability(final Capability capability) {
        if (capability == Capability.SUGGEST_LOCATIONS || capability == Capability.NEARBY_LOCATIONS
                || capability == Capability.DEPARTURES || capability == Capability.TRIPS)
            return true;
        else
            return false;
    }

    @Override
    public NearbyLocationsResult queryNearbyLocations(final EnumSet<LocationType> types, final Location location,
            int maxDistance, final int maxLocations) throws IOException {
        final ResultHeader resultHeader = new ResultHeader(network, SERVER_PRODUCT, SERVER_VERSION, null, 0, null);

        // Build url depending of location type.
        final HttpUrl.Builder url = url();
        if (location.type == LocationType.COORD || location.type == LocationType.ADDRESS
                || location.type == LocationType.ANY) {
            if (!location.hasLocation())
                throw new IllegalArgumentException();
            final double lon = location.lon / 1E6;
            final double lat = location.lat / 1E6;
            url.addPathSegment("coords").addPathSegment(lon + ";" + lat);
        } else if (location.type == LocationType.STATION) {
            if (!location.isIdentified())
                throw new IllegalArgumentException();
            url.addPathSegment("stop_points").addPathSegment(location.id);
        } else if (location.type == LocationType.POI) {
            if (!location.isIdentified())
                throw new IllegalArgumentException();
            url.addPathSegment("pois").addPathSegment(location.id);
        } else {
            throw new IllegalArgumentException("Unhandled location type: " + location.type);
        }
        url.addPathSegment("places_nearby");
        url.addQueryParameter("type[]", "stop_point");
        url.addQueryParameter("distance", Integer.toString(maxDistance == 0 ? 50000 : maxDistance));
        if (maxLocations > 0)
            url.addQueryParameter("count", Integer.toString(maxLocations));
        url.addQueryParameter("depth", "3");
        final CharSequence page = httpClient.get(url.build());

        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 NearbyLocationsResult(resultHeader, Status.INVALID_ID);
            } else {
                final List<Location> 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);
                    stations.add(nearbyLocation);
                }

                return new NearbyLocationsResult(resultHeader, stations);
            }
        } catch (final JSONException jsonExc) {
            throw new ParserException(jsonExc);
        }
    }

    @Override
    public QueryDeparturesResult queryDepartures(final String stationId, final @Nullable Date time,
            final int maxDepartures, final boolean equivs) throws IOException {
        checkNotNull(Strings.emptyToNull(stationId));

        final ResultHeader resultHeader = new ResultHeader(network, SERVER_PRODUCT, SERVER_VERSION, null, 0, null);

        try {
            final QueryDeparturesResult result = new QueryDeparturesResult(resultHeader,
                    QueryDeparturesResult.Status.OK);

            // If equivs is equal to true, get stop_area corresponding
            // to stop_point and query departures.
            final HttpUrl.Builder url = url();
            final String header = stationId.substring(0, stationId.indexOf(":"));
            if (equivs && header.equals("stop_point")) {
                final String stopAreaId = getStopAreaId(stationId);
                url.addPathSegment("stop_areas");
                url.addPathSegment(stopAreaId);
            } else if (header.equals("stop_area")) {
                url.addPathSegment("stop_areas");
                url.addPathSegment(stationId);
            } else {
                url.addPathSegment("stop_points");
                url.addPathSegment(stationId);
            }
            url.addPathSegment("departures");
            url.addQueryParameter("from_datetime", printDate(time));
            url.addQueryParameter("count", Integer.toString(maxDepartures));
            url.addQueryParameter("duration", "86400");
            url.addQueryParameter("depth", "0");

            final CharSequence page = httpClient.get(url.build());

            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);

                // 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 Line line = parseLine(route);

                final JSONObject stopPoint = jsonDeparture.getJSONObject("stop_point");
                final Location location = parsePlace(stopPoint, PlaceType.STOP_POINT);

                // 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<Departure>(),
                            new LinkedList<LineDestination>());
                    result.stationDepartures.add(stationDepartures);
                }
                final LineDestination lineDestination = getStationLine(line, jsonDeparture);
                final List<LineDestination> lines = stationDepartures.lines;
                if (lines != null && !lines.contains(lineDestination))
                    lines.add(lineDestination);
                final Location destination = lineDestination.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.getBodyPeek().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("Cannot parse error content, original exception linked", fnfExc);
            }
        }
    }

    @Override
    public SuggestLocationsResult suggestLocations(final CharSequence constraint) throws IOException {
        final String nameCstr = constraint.toString();

        final HttpUrl.Builder url = url().addPathSegment("places");
        url.addQueryParameter("q", nameCstr);
        url.addQueryParameter("type[]", "stop_area");
        url.addQueryParameter("type[]", "address");
        url.addQueryParameter("type[]", "poi");
        url.addQueryParameter("type[]", "administrative_region");
        url.addQueryParameter("depth", "1");
        final CharSequence page = httpClient.get(url.build());

        try {
            final List<SuggestedLocation> 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);
                    final int priority = place.optInt("quality", 0);

                    // Add location to station list.
                    final Location location = parseLocation(place);
                    locations.add(new SuggestedLocation(location, priority));
                }
            }

            final ResultHeader resultHeader = new ResultHeader(network, SERVER_PRODUCT, SERVER_VERSION, null, 0,
                    null);
            return new SuggestLocationsResult(resultHeader, locations);
        } catch (final JSONException jsonExc) {
            throw new ParserException(jsonExc);
        }
    }

    @Override
    public QueryTripsResult queryTrips(final Location from, final @Nullable Location via, final Location to,
            final Date date, final boolean dep, final @Nullable Set<Product> products,
            final @Nullable Optimize optimize, final @Nullable WalkSpeed walkSpeed,
            final @Nullable Accessibility accessibility, final @Nullable Set<Option> options) throws IOException {
        final ResultHeader resultHeader = new ResultHeader(network, SERVER_PRODUCT, SERVER_VERSION, null, 0, null);

        try {
            if (from != null && from.isIdentified() && to != null && to.isIdentified()) {
                final HttpUrl.Builder url = apiBase.newBuilder().addPathSegment("journeys");
                url.addQueryParameter("from", printLocation(from));
                url.addQueryParameter("to", printLocation(to));
                url.addQueryParameter("datetime", printDate(date));
                url.addQueryParameter("datetime_represents", dep ? "departure" : "arrival");
                url.addQueryParameter("min_nb_journeys", Integer.toString(this.numTripsRequested));
                url.addQueryParameter("depth", "0");

                // Set walking speed.
                if (walkSpeed != null) {
                    final double walkingSpeed;
                    switch (walkSpeed) {
                    case SLOW:
                        walkingSpeed = 1.12 * 0.8;
                        break;
                    case FAST:
                        walkingSpeed = 1.12 * 1.2;
                        break;
                    case NORMAL:
                    default:
                        walkingSpeed = 1.12;
                        break;
                    }

                    url.addQueryParameter("walking_speed", Double.toString(walkingSpeed));
                }

                if (options != null && options.contains(Option.BIKE)) {
                    url.addQueryParameter("first_section_mode", "bike");
                    url.addQueryParameter("last_section_mode", "bike");
                }

                // Set forbidden physical modes.
                if (products != null && !products.equals(Product.ALL)) {
                    url.addQueryParameter("forbidden_uris[]", "physical_mode:Air");
                    url.addQueryParameter("forbidden_uris[]", "physical_mode:Boat");
                    if (!products.contains(Product.REGIONAL_TRAIN)) {
                        url.addQueryParameter("forbidden_uris[]", "physical_mode:Localdistancetrain");
                        url.addQueryParameter("forbidden_uris[]", "physical_mode:Train");
                    }
                    if (!products.contains(Product.SUBURBAN_TRAIN)) {
                        url.addQueryParameter("forbidden_uris[]", "physical_mode:Localtrain");
                        url.addQueryParameter("forbidden_uris[]", "physical_mode:Train");
                        url.addQueryParameter("forbidden_uris[]", "physical_mode:Rapidtransit");
                    }
                    if (!products.contains(Product.SUBWAY)) {
                        url.addQueryParameter("forbidden_uris[]", "physical_mode:Metro");
                    }
                    if (!products.contains(Product.TRAM)) {
                        url.addQueryParameter("forbidden_uris[]", "physical_mode:Tramway");
                    }
                    if (!products.contains(Product.BUS)) {
                        url.addQueryParameter("forbidden_uris[]", "physical_mode:Bus");
                        url.addQueryParameter("forbidden_uris[]", "physical_mode:Busrapidtransit");
                        url.addQueryParameter("forbidden_uris[]", "physical_mode:Coach");
                        url.addQueryParameter("forbidden_uris[]", "physical_mode:Shuttle");
                    }
                    if (!products.contains(Product.FERRY)) {
                        url.addQueryParameter("forbidden_uris[]", "physical_mode:Ferry");
                    }
                    if (!products.contains(Product.CABLECAR)) {
                        url.addQueryParameter("forbidden_uris[]", "physical_mode:Funicular");
                    }
                    if (!products.contains(Product.ON_DEMAND)) {
                        url.addQueryParameter("forbidden_uris[]", "physical_mode:Taxi");
                    }
                }

                final CharSequence page = httpClient.get(url.build());

                try {
                    final JSONObject head = new JSONObject(page.toString());

                    if (head.has("error")) {
                        final JSONObject error = head.getJSONObject("error");
                        final String id = error.getString("id");

                        if (id.equals("no_solution"))
                            return new QueryTripsResult(resultHeader, QueryTripsResult.Status.NO_TRIPS);
                        else
                            throw new IllegalArgumentException("Unhandled error id: " + id);
                    } else {
                        // Fill context.
                        HttpUrl prevQueryUrl = null;
                        HttpUrl nextQueryUrl = null;
                        final JSONArray links = head.getJSONArray("links");
                        for (int i = 0; i < links.length(); ++i) {
                            final JSONObject link = links.getJSONObject(i);
                            final String type = link.getString("type");
                            if (type.equals("prev")) {
                                prevQueryUrl = HttpUrl.parse(link.getString("href"));
                            } else if (type.equals("next")) {
                                nextQueryUrl = HttpUrl.parse(link.getString("href"));
                            }
                        }

                        String prevQueryUrlString = prevQueryUrl != null ? prevQueryUrl.toString() : null;
                        String nextQueryUrlString = nextQueryUrl != null ? nextQueryUrl.toString() : null;

                        final QueryTripsResult result = new QueryTripsResult(resultHeader, url.build().toString(),
                                from, null, to, new Context(from, to, prevQueryUrlString, nextQueryUrlString),
                                new LinkedList<Trip>());

                        parseQueryTripsResult(head, from, to, result);

                        return result;
                    }
                } catch (final JSONException jsonExc) {
                    throw new ParserException(jsonExc);
                }
            } else if (from != null && to != null) {
                List<Location> ambiguousFrom = null, ambiguousTo = null;
                Location newFrom = null, newTo = null;

                if (!from.isIdentified() && from.hasName()) {
                    ambiguousFrom = suggestLocations(from.name).getLocations();
                    if (ambiguousFrom.isEmpty())
                        return new QueryTripsResult(resultHeader, QueryTripsResult.Status.UNKNOWN_FROM);
                    if (ambiguousFrom.size() == 1 && ambiguousFrom.get(0).isIdentified())
                        newFrom = ambiguousFrom.get(0);
                }

                if (!to.isIdentified() && to.hasName()) {
                    ambiguousTo = suggestLocations(to.name).getLocations();
                    if (ambiguousTo.isEmpty())
                        return new QueryTripsResult(resultHeader, QueryTripsResult.Status.UNKNOWN_TO);
                    if (ambiguousTo.size() == 1 && ambiguousTo.get(0).isIdentified())
                        newTo = ambiguousTo.get(0);
                }

                if (newTo != null && newFrom != null)
                    return queryTrips(newFrom, via, newTo, date, dep, products, optimize, walkSpeed, accessibility,
                            options);

                if (ambiguousFrom != null || ambiguousTo != null)
                    return new QueryTripsResult(resultHeader, ambiguousFrom, null, ambiguousTo);
            }
            return new QueryTripsResult(resultHeader, QueryTripsResult.Status.NO_TRIPS);
        } catch (final NotFoundException fnfExc) {
            try {
                final JSONObject head = new JSONObject(fnfExc.getBodyPeek().toString());
                final JSONObject error = head.getJSONObject("error");
                final String id = error.getString("id");

                if (id.equals("unknown_object")) {
                    // Identify unknown object.
                    final String fromString = printLocation(from);
                    final String toString = printLocation(to);

                    final String message = error.getString("message");
                    if (message.equals("Invalid id : " + fromString))
                        return new QueryTripsResult(resultHeader, QueryTripsResult.Status.UNKNOWN_FROM);
                    else if (message.equals("Invalid id : " + toString))
                        return new QueryTripsResult(resultHeader, QueryTripsResult.Status.UNKNOWN_TO);
                    else
                        throw new IllegalArgumentException("Unhandled error message: " + message);
                } else if (id.equals("date_out_of_bounds")) {
                    return new QueryTripsResult(resultHeader, QueryTripsResult.Status.INVALID_DATE);
                } else {
                    throw new IllegalArgumentException("Unhandled error id: " + id);
                }
            } catch (final JSONException jsonExc) {
                throw new ParserException("Cannot parse error content, original exception linked", fnfExc);
            }
        }
    }

    @Override
    public QueryTripsResult queryMoreTrips(final QueryTripsContext contextObj, final boolean later)
            throws IOException {
        final ResultHeader resultHeader = new ResultHeader(network, SERVER_PRODUCT, SERVER_VERSION, null, 0, null);

        final Context context = (Context) contextObj;
        final Location from = context.from;
        final Location to = context.to;
        final HttpUrl queryUrl = HttpUrl.parse(later ? context.nextQueryUri : context.prevQueryUri);
        final CharSequence page = httpClient.get(queryUrl);

        try {
            if (from.isIdentified() && to.isIdentified()) {
                final JSONObject head = new JSONObject(page.toString());

                // Fill context.
                final JSONArray links = head.getJSONArray("links");
                final JSONObject prev = links.getJSONObject(0);
                final HttpUrl prevQueryUrl = HttpUrl.parse(prev.getString("href"));
                final JSONObject next = links.getJSONObject(1);
                final HttpUrl nextQueryUrl = HttpUrl.parse(next.getString("href"));

                final QueryTripsResult result = new QueryTripsResult(resultHeader, queryUrl.toString(), from, null,
                        to, new Context(from, to, prevQueryUrl.toString(), nextQueryUrl.toString()),
                        new LinkedList<Trip>());

                parseQueryTripsResult(head, from, to, result);

                return result;
            } else {
                return new QueryTripsResult(null, QueryTripsResult.Status.NO_TRIPS);
            }
        } catch (final JSONException jsonExc) {
            throw new ParserException(jsonExc);
        }
    }

    @Override
    public Point[] getArea() throws IOException {
        final HttpUrl.Builder url = url();
        final CharSequence page = httpClient.get(url.build());

        try {
            // Get shape string.
            final JSONObject head = new JSONObject(page.toString());
            final JSONArray regions = head.getJSONArray("regions");
            final JSONObject regionInfo = regions.getJSONObject(0);
            final String shape = regionInfo.getString("shape");

            // Parse string using JSON tokenizer for coordinates.
            List<Point> pointList = new ArrayList<>();
            final JSONTokener shapeTokener = new JSONTokener(shape);
            shapeTokener.skipTo('(');
            shapeTokener.next();
            shapeTokener.next();
            char c = shapeTokener.next();
            while (c != ')') {
                // Navitia coordinates are in (longitude, latitude) order.
                final String lonString = shapeTokener.nextTo(' ');
                shapeTokener.next();
                final String latString = shapeTokener.nextTo(",)");
                c = shapeTokener.next();

                // Append new point with (latitude, longitude) order.
                final double lat = Double.parseDouble(latString);
                final double lon = Double.parseDouble(lonString);
                pointList.add(Point.fromDouble(lat, lon));
            }

            // Fill point array.
            final Point[] pointArray = new Point[pointList.size()];
            for (int i = 0; i < pointList.size(); ++i)
                pointArray[i] = pointList.get(i);

            return pointArray;
        } catch (final JSONException jsonExc) {
            throw new ParserException(jsonExc);
        }
    }
}