All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opentripplanner.routing.graph.Graph Maven / Gradle / Ivy

There is a newer version: 2.5.0
Show newest version
package org.opentripplanner.routing.graph;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import gnu.trove.list.TDoubleList;
import gnu.trove.list.linked.TDoubleLinkedList;
import org.apache.commons.math3.stat.descriptive.rank.Median;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.opentripplanner.common.MavenVersion;
import org.opentripplanner.common.TurnRestriction;
import org.opentripplanner.common.geometry.CompactElevationProfile;
import org.opentripplanner.common.geometry.GraphUtils;
import org.opentripplanner.common.geometry.SphericalDistanceLibrary;
import org.opentripplanner.common.model.T2;
import org.opentripplanner.ext.flex.trip.FlexTrip;
import org.opentripplanner.graph_builder.DataImportIssueStore;
import org.opentripplanner.graph_builder.issues.NoFutureDates;
import org.opentripplanner.model.Agency;
import org.opentripplanner.model.FeedInfo;
import org.opentripplanner.model.FeedScopedId;
import org.opentripplanner.model.GraphBundle;
import org.opentripplanner.model.GroupOfStations;
import org.opentripplanner.model.FlexStopLocation;
import org.opentripplanner.model.FlexLocationGroup;
import org.opentripplanner.model.MultiModalStation;
import org.opentripplanner.model.Notice;
import org.opentripplanner.model.Operator;
import org.opentripplanner.model.SimpleTransfer;
import org.opentripplanner.model.Station;
import org.opentripplanner.model.Stop;
import org.opentripplanner.model.StopLocation;
import org.opentripplanner.model.TimetableSnapshot;
import org.opentripplanner.model.TimetableSnapshotProvider;
import org.opentripplanner.model.TransitEntity;
import org.opentripplanner.model.TransitMode;
import org.opentripplanner.model.Trip;
import org.opentripplanner.model.TripPattern;
import org.opentripplanner.model.WgsCoordinate;
import org.opentripplanner.model.calendar.CalendarService;
import org.opentripplanner.model.calendar.CalendarServiceData;
import org.opentripplanner.model.calendar.ServiceDate;
import org.opentripplanner.model.calendar.impl.CalendarServiceImpl;
import org.opentripplanner.routing.algorithm.raptor.transit.TransitLayer;
import org.opentripplanner.routing.algorithm.raptor.transit.mappers.TransitLayerUpdater;
import org.opentripplanner.routing.bike_rental.BikeRentalStationService;
import org.opentripplanner.routing.core.TransferTable;
import org.opentripplanner.routing.edgetype.EdgeWithCleanup;
import org.opentripplanner.routing.edgetype.StreetEdge;
import org.opentripplanner.routing.impl.DelegatingTransitAlertServiceImpl;
import org.opentripplanner.routing.impl.StreetVertexIndex;
import org.opentripplanner.routing.services.TransitAlertService;
import org.opentripplanner.routing.services.notes.StreetNotesService;
import org.opentripplanner.routing.trippattern.Deduplicator;
import org.opentripplanner.routing.util.ConcurrentPublished;
import org.opentripplanner.routing.vertextype.TransitStopVertex;
import org.opentripplanner.updater.GraphUpdaterConfigurator;
import org.opentripplanner.updater.GraphUpdaterManager;
import org.opentripplanner.util.WorldEnvelope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.prefs.Preferences;
import java.util.stream.Collectors;

/**
 * A graph is really just one or more indexes into a set of vertexes. It used to keep edgelists for each vertex, but those are in the vertex now.
 */
public class Graph implements Serializable {

    private static final Logger LOG = LoggerFactory.getLogger(Graph.class);

    private static final long serialVersionUID = MavenVersion.VERSION.getUID();

    private final MavenVersion mavenVersion = MavenVersion.VERSION;

    // TODO Remove this field, use Router.routerId ?
    public String routerId;

    private final Map> turnRestrictions = Maps.newHashMap();

    public final StreetNotesService streetNotesService = new StreetNotesService();

    /**
     * Allows a notice element to be attached to an object in the OTP model by its id and then
     * retrieved by the API when navigating from that object. The map key is entity id:
     * {@link TransitEntity#getId()}. The notice is part of the static transit data.
     */
    private final Multimap, Notice> noticesByElement = HashMultimap.create();

    // transit feed validity information in seconds since epoch
    private long transitServiceStarts = Long.MAX_VALUE;

    private long transitServiceEnds = 0;

    private Map, Serializable> services = new HashMap<>();

    private TransferTable transferTable = new TransferTable();

    private GraphBundle bundle;

    /* Ideally we could just get rid of vertex labels, but they're used in tests and graph building. */
    private Map vertices = new ConcurrentHashMap<>();

    private transient CalendarService calendarService;

    public transient StreetVertexIndex streetIndex;

    public transient GraphIndex index;

    public final transient Deduplicator deduplicator = new Deduplicator();

    /**
     * Map from GTFS ServiceIds to integers close to 0. Allows using BitSets instead of {@code Set}.
     * An empty Map is created before the Graph is built to allow registering IDs from multiple feeds.   
     */
    private final Map serviceCodes = Maps.newHashMap();

    private transient TimetableSnapshotProvider timetableSnapshotProvider = null;

    private Collection agencies = new ArrayList<>();

    private Collection operators = new ArrayList<>();

    private Collection feedIds = new HashSet<>();

    private Map feedInfoForId = new HashMap<>();

    private transient TimeZone timeZone = null;

    //Envelope of all OSM and transit vertices. Calculated during build time
    private WorldEnvelope envelope = null;

    //ConvexHull of all the graph vertices. Generated at Graph build time.
    private Geometry convexHull = null;

    /** The density center of the graph for determining the initial geographic extent in the client. */
    private Coordinate center = null;

    /* The preferences that were used for graph building. */
    public Preferences preferences = null;

    /** List of transit modes that are availible in GTFS data used in this graph**/
    private HashSet transitModes = new HashSet<>();

    public boolean hasBikeSharing = false;

    public boolean hasParkRide = false;

    public boolean hasBikeRide = false;

    /**
     * Manages all updaters of this graph. Is created by the GraphUpdaterConfigurator when there are
     * graph updaters defined in the configuration.
     *
     * @see GraphUpdaterConfigurator
     */
    public transient GraphUpdaterManager updaterManager = null;

    public final Date buildTime = new Date();

    /** True if OSM data was loaded into this Graph. */
    public boolean hasStreets = false;

    /** True if GTFS data was loaded into this Graph. */
    public boolean hasTransit = false;

    /** True if direct single-edge transfers were generated between transit stops in this Graph. */
    public boolean hasDirectTransfers = false;

    /**
     * True if frequency-based services exist in this Graph (GTFS frequencies with exact_times = 0).
     */
    public boolean hasFrequencyService = false;

    /**
     * True if schedule-based services exist in this Graph (including GTFS frequencies with
     * exact_times = 1).
     */
    public boolean hasScheduledService = false;

    /**
     * The difference in meters between the WGS84 ellipsoid height and geoid height
     * at the graph's center
     */
    public Double ellipsoidToGeoidDifference = 0.0;

    /** Parent stops **/
    public Map stationById = new HashMap<>();

    /**
     * Optional level above parent stops (only supported in NeTEx)
     */
    public Map multiModalStationById = new HashMap<>();

    /**
     * Optional grouping that can contain both stations and multimodal stations (only supported in
     * NeTEx)
     */
    public Map groupOfStationsById = new HashMap<>();

    /**
     * TripPatterns used to be reached through hop edges, but we're not creating on-board transit
     * vertices/edges anymore.
     */
    public Map tripPatternForId = Maps.newHashMap();

    /** Interlining relationships between trips. */
    public final BiMap interlinedTrips = HashBiMap.create();

    /** Pre-generated transfers between all stops. */
    public final Multimap transfersByStop = HashMultimap.create();

    /** Flex locations are generated by GTFS graph builder, but consumed only after the street graph
     *  is built
     */
    public Map locationsById = new HashMap<>();

    public Map locationGroupsById = new HashMap<>();

    public Map flexTripsById = new HashMap<>();

    /** The distance between elevation samples used in CompactElevationProfile. */
    private double distanceBetweenElevationSamples;

    /** Data model for Raptor routing, with realtime updates applied (if any). */
    private transient TransitLayer transitLayer;

    /** Data model for Raptor routing, with realtime updates applied (if any). */
    private transient ConcurrentPublished realtimeTransitLayer =
        new ConcurrentPublished<>();

    public transient TransitLayerUpdater transitLayerUpdater;

    private transient TransitAlertService transitAlertService;


    /**
     * Hack. I've tried three different ways of generating unique labels.
     * Previously we were just tolerating edge label collisions.
     * For some reason we're repeatedly generating splits on the same edge objects, despite a
     * comment that said it was guaranteed there would only ever be one split per edge. This is
     * going to fail as soon as we load a base OSM graph and build transit on top of it.
     */
    public long nextSplitNumber = 0;

    public Graph(Graph basedOn) {
        this();
        this.bundle = basedOn.getBundle();
    }

    // Constructor for deserialization.
    public Graph() { }

    public TimetableSnapshot getTimetableSnapshot() {
        return timetableSnapshotProvider == null ? null : timetableSnapshotProvider.getTimetableSnapshot();
    }

    /**
     * TODO OTP2 - This should be replaced by proper dependency injection
     */
    @SuppressWarnings("unchecked")
    public  T getOrSetupTimetableSnapshotProvider(Function creator) {
        if (timetableSnapshotProvider == null) {
            timetableSnapshotProvider = creator.apply(this);
        }
        try {
            return (T) timetableSnapshotProvider;
        } catch (ClassCastException e) {
            throw new IllegalArgumentException(
                    "We support only one timetableSnapshotSource, there are two implementation; one for GTFS and one " +
                    "for Netex/Siri. They need to be refactored to work together. This cast will fail if updaters " +
                    "try setup both.",
                    e
            );
        }
    }

    /**
     * Add the given vertex to the graph. Ideally, only vertices should add themselves to the graph,
     * when they are constructed or deserialized.
     *
     * TODO OTP2 - This strategy is error prune, problematic when testing and causes a cyclic
     *           - dependency Graph -> Vertex -> Graph. A better approach is to lett the bigger
     *           - whole (Graph) create and attach its smaller parts (Vertex). A way is to create
     *           - a VertexCollection class, let the graph hold an instance of this collection,
     *           - and create factory methods for each type of Vertex in the VertexCollection.
     */
    public void addVertex(Vertex v) {
        Vertex old = vertices.put(v.getLabel(), v);
        if (old != null) {
            if (old == v)
                LOG.error("repeatedly added the same vertex: {}", v);
            else
                LOG.error("duplicate vertex label in graph (added vertex to graph anyway): {}", v);
        }
    }

    /**
     * Removes an edge from the graph. This method is not thread-safe.
     * @param e The edge to be removed
     */
    public void removeEdge(Edge e) {
        if (e != null) {
            turnRestrictions.remove(e);
            streetNotesService.removeStaticNotes(e);

            if (e instanceof EdgeWithCleanup) ((EdgeWithCleanup) e).detach();

            if (e.fromv != null) {
                e.fromv.removeOutgoing(e);

                for (Edge otherEdge : e.fromv.getIncoming()) {
                    for (TurnRestriction turnRestriction : getTurnRestrictions(otherEdge)) {
                        if (turnRestriction.to == e) {
                            removeTurnRestriction(otherEdge, turnRestriction);
                        }
                    }
                }

                e.fromv = null;
            }

            if (e.tov != null) {
                e.tov.removeIncoming(e);
                e.tov = null;
            }
        }
    }

    /* Fetching vertices by label is convenient in tests and such, but avoid using in general. */
    @VisibleForTesting
    public Vertex getVertex(String label) {
        return vertices.get(label);
    }

    /**
     * Get all the vertices in the graph.
     * @return
     */
    public Collection getVertices() {
        return this.vertices.values();
    }

    /**
     * Return all the edges in the graph. Derived from vertices on demand.
     */
    public Collection getEdges() {
        Set edges = new HashSet<>();
        for (Vertex v : this.getVertices()) {
            edges.addAll(v.getOutgoing());
        }
        return edges;
    }

    /**
     * Add a {@link TurnRestriction} to the {@link TurnRestriction} {@link List} belonging to an
     * {@link Edge}. This method is not thread-safe.
     * @param edge
     * @param turnRestriction
     */
    public void addTurnRestriction(Edge edge, TurnRestriction turnRestriction) {
        if (edge == null || turnRestriction == null) return;
        List turnRestrictions = this.turnRestrictions.get(edge);
        if (turnRestrictions == null) {
            turnRestrictions = Lists.newArrayList();
            this.turnRestrictions.put(edge, turnRestrictions);
        }
        turnRestrictions.add(turnRestriction);
    }

    /**
     * Remove a {@link TurnRestriction} from the {@link TurnRestriction} {@link List} belonging to
     * an {@link Edge}. This method is not thread-safe.
     * @param edge
     * @param turnRestriction
     */
    public void removeTurnRestriction(Edge edge, TurnRestriction turnRestriction) {
        if (edge == null || turnRestriction == null) return;
        List turnRestrictions = this.turnRestrictions.get(edge);
        if (turnRestrictions != null && turnRestrictions.contains(turnRestriction)) {
            if (turnRestrictions.size() < 2) {
                this.turnRestrictions.remove(edge);
            } else {
                turnRestrictions.remove(turnRestriction);
            }
        }
    }

    /**
     * Get the {@link TurnRestriction} {@link List} that belongs to an {@link Edge} and return an
     * immutable copy. This method is thread-safe when used by itself, but not if addTurnRestriction
     * or removeTurnRestriction is called concurrently.
     * @param edge
     * @return The {@link TurnRestriction} {@link List} that belongs to the {@link Edge}
     */
    public List getTurnRestrictions(Edge edge) {
        if (edge != null) {
            List turnRestrictions = this.turnRestrictions.get(edge);
            if (turnRestrictions != null) {
                return ImmutableList.copyOf(turnRestrictions);
            }
        }
        return Collections.emptyList();
    }

    /**
     * Return only the StreetEdges in the graph.
     * @return
     */
    public Collection getStreetEdges() {
        Collection allEdges = this.getEdges();
        return Lists.newArrayList(Iterables.filter(allEdges, StreetEdge.class));
    }

    public TransitLayer getTransitLayer() {
        return transitLayer;
    }

    public void setTransitLayer(
        TransitLayer transitLayer
    ) {
        this.transitLayer = transitLayer;
    }

    public TransitLayer getRealtimeTransitLayer() {
        return realtimeTransitLayer.get();
    }

    public boolean hasRealtimeTransitLayer() {
        return realtimeTransitLayer != null;
    }

    public void setRealtimeTransitLayer(
        TransitLayer realtimeTransitLayer
    ) {
        this.realtimeTransitLayer.publish(realtimeTransitLayer);
    }

    public boolean containsVertex(Vertex v) {
        return (v != null) && vertices.get(v.getLabel()) == v;
    }

    @SuppressWarnings("unchecked")
    public  T putService(Class serviceType, T service) {
        return (T) services.put(serviceType, service);
    }

    public boolean hasService(Class serviceType) {
        return services.containsKey(serviceType);
    }

    @SuppressWarnings("unchecked")
    public  T getService(Class serviceType) {
        return (T) services.get(serviceType);
    }

    public  T getService(Class serviceType, boolean autoCreate) {
        T t = (T)services.get(serviceType);
        if (t == null && autoCreate) {
            try {
                t = serviceType.getDeclaredConstructor().newInstance();
            }
            catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | InstantiationException e) {
                throw new RuntimeException(e);
            }
            services.put(serviceType, t);
        }
        return t;
    }

    public void remove(Vertex vertex) {
        vertices.remove(vertex.getLabel());
    }

    public void removeVertexAndEdges(Vertex vertex) {
        if (!containsVertex(vertex)) {
            throw new IllegalStateException("attempting to remove vertex that is not in graph.");
        }

        /*
         * Note: We have to handle the removal of looping edges (for example RentABikeOn/OffEdge),
         * we use a set to prevent having multiple times the same edge.
         */
        Set edges = new HashSet(vertex.getDegreeIn() + vertex.getDegreeOut());
        edges.addAll(vertex.getIncoming());
        edges.addAll(vertex.getOutgoing());

        for (Edge edge : edges) {
            removeEdge(edge);
        }

        this.remove(vertex);
    }

    public Envelope getExtent() {
        Envelope env = new Envelope();
        for (Vertex v : getVertices()) {
            env.expandToInclude(v.getCoordinate());
        }
        return env;
    }

    public TransferTable getTransferTable() {
        return transferTable;
    }

    // Infer the time period covered by the transit feed
    public void updateTransitFeedValidity(
            CalendarServiceData data,
            DataImportIssueStore issueStore
    ) {
        long now = new Date().getTime() / 1000;
        final long SEC_IN_DAY = 24 * 60 * 60;
        HashSet agenciesWithFutureDates = new HashSet();
        HashSet agencies = new HashSet();
        for (FeedScopedId sid : data.getServiceIds()) {
            agencies.add(sid.getFeedId());
            for (ServiceDate sd : data.getServiceDatesForServiceId(sid)) {
                // Adjust for timezone, assuming there is only one per graph.
                long t = sd.getAsDate(getTimeZone()).getTime() / 1000;
                if (t > now) {
                    agenciesWithFutureDates.add(sid.getFeedId());
                }
                // assume feed is unreliable after midnight on last service day
                long u = t + SEC_IN_DAY;
                if (t < this.transitServiceStarts)
                    this.transitServiceStarts = t;
                if (u > this.transitServiceEnds)
                    this.transitServiceEnds = u;
            }
        }
        for (String agency : agencies) {
            if (!agenciesWithFutureDates.contains(agency)) {
                issueStore.add(new NoFutureDates(agency));
            }
        }
    }

    // Check to see if we have transit information for a given date
    public boolean transitFeedCovers(long t) {
        return t >= this.transitServiceStarts && t < this.transitServiceEnds;
    }

    public GraphBundle getBundle() {
        return bundle;
    }

    public void setBundle(GraphBundle bundle) {
        this.bundle = bundle;
    }

    public int countVertices() {
        return vertices.size();
    }

    /**
     * Find the total number of edges in this Graph. There are assumed to be no Edges in an incoming edge list that are not in an outgoing edge list.
     * 
     * @return number of outgoing edges in the graph
     */
    public int countEdges() {
        int ne = 0;
        for (Vertex v : getVertices()) {
            ne += v.getDegreeOut();
        }
        return ne;
    }

    private void readObject(ObjectInputStream inputStream) throws ClassNotFoundException, IOException {
        inputStream.defaultReadObject();
    }

    /**
     * Adds mode of transport to transit modes in graph
     * @param mode
     */
    public void addTransitMode(TransitMode mode) {
        transitModes.add(mode);
    }

    public HashSet getTransitModes() {
        return transitModes;
    }

    /**
     * Perform indexing on vertices, edges, and timetables, and create transient data structures.
     * This used to be done in readObject methods upon deserialization, but stand-alone mode now
     * allows passing graphs from graphbuilder to server in memory, without a round trip through
     * serialization. 
     * TODO: do we really need a factory for different street vertex indexes?
     */
    public void index () {
        LOG.info("Index graph...");
        streetIndex = new StreetVertexIndex(this);
        LOG.debug("Rebuilding edge and vertex indices.");
        for (TripPattern tp : tripPatternForId.values()) {
            // Skip frequency-based patterns which have no timetable (null)
            if (tp != null) tp.scheduledTimetable.finish();
        }
        // TODO: Move this ^ stuff into the graph index
        this.index = new GraphIndex(this);
        LOG.info("Index graph complete.");
    }
    
    /**
     * Compares the OTP version number stored in the graph with that of the currently running instance. Logs warnings explaining that mismatched
     * versions can cause problems.
     * 
     * @return false if Maven versions match (even if commit ids do not match), true if Maven version of graph does not match this version of OTP or
     *         graphs are otherwise obviously incompatible.
     */
    boolean graphVersionMismatch() {
        MavenVersion v = MavenVersion.VERSION;
        MavenVersion gv = this.mavenVersion;
        LOG.info("Graph version: {}", gv);
        LOG.info("OTP version:   {}", v);
        if (!v.equals(gv)) {
            LOG.error("This graph was built with a different version of OTP. Please rebuild it.");
            return true; // do not allow graph use
        } else if (!v.commit.equals(gv.commit)) {
            if (v.qualifier.equals("SNAPSHOT")) {
                LOG.warn("This graph was built with the same SNAPSHOT version of OTP, but a "
                        + "different commit. Please rebuild the graph if you experience incorrect "
                        + "behavior. ");
                return false; // graph might still work
            } else {
                LOG.error("Commit mismatch in non-SNAPSHOT version. This implies a problem with "
                        + "the build or release process.");
                return true; // major problem
            }
        } else {
            // no version mismatch, no commit mismatch
            LOG.info("This graph was built with the currently running version and commit of OTP.");
            return false;
        }
    }

    public CalendarService getCalendarService() {
        if (calendarService == null) {
            CalendarServiceData data = this.getService(CalendarServiceData.class);
            if (data != null) {
                this.calendarService = new CalendarServiceImpl(data);
            }
        }
        return this.calendarService;
    }

    public int removeEdgelessVertices() {
        int removed = 0;
        List toRemove = new LinkedList();
        for (Vertex v : this.getVertices())
            if (v.getDegreeOut() + v.getDegreeIn() == 0)
                toRemove.add(v);
        // avoid concurrent vertex map modification
        for (Vertex v : toRemove) {
            this.remove(v);
            removed += 1;
            LOG.trace("removed edgeless vertex {}", v);
        }
        return removed;
    }

    public Collection getFeedIds() {
        return feedIds;
    }

    public Collection getAgencies() {
        return agencies;
    }

    public FeedInfo getFeedInfo(String feedId) {
        return feedInfoForId.get(feedId);
    }

    public void addAgency(String feedId, Agency agency) {
        agencies.add(agency);
        this.feedIds.add(feedId);
    }

    public void addFeedInfo(FeedInfo info) {
        this.feedInfoForId.put(info.getId().toString(), info);
    }

    /**
     * Returns the time zone for the first agency in this graph. This is used to interpret times in API requests. The JVM default time zone cannot be
     * used because we support multiple graphs on one server via the routerId. Ideally we would want to interpret times in the time zone of the
     * geographic location where the origin/destination vertex or board/alight event is located. This may become necessary when we start making graphs
     * with long distance train, boat, or air services.
     */
    public TimeZone getTimeZone() {
        if (timeZone == null) {
            if (agencies == null || agencies.size() == 0) {
                timeZone = TimeZone.getTimeZone("GMT");
                LOG.warn("graph contains no agencies (yet); API request times will be interpreted as GMT.");
            } else {
                CalendarService cs = this.getCalendarService();
                for (Agency agency : agencies) {
                    TimeZone tz = cs.getTimeZoneForAgencyId(agency.getId());
                    if (timeZone == null) {
                        LOG.debug("graph time zone set to {}", tz);
                        timeZone = tz;
                    } else if (!timeZone.equals(tz)) {
                        LOG.error("agency time zone differs from graph time zone: {}", tz);
                    }
                }
            }
        }
        return timeZone;
    }

    public Collection getOperators() {
        return operators;
    }

    /**
     * The timezone is cached by the graph. If you've done something to the graph that has the
     * potential to change the time zone, you should call this to ensure it is reset. 
     */
    public void clearTimeZone () {
        this.timeZone = null;
    }

    /**
     * Calculates envelope out of all OSM coordinates
     *
     * Transit stops are added to the envelope as they are added to the graph
     */
    public void calculateEnvelope() {
        this.envelope = new WorldEnvelope();

        for (Vertex v : this.getVertices()) {
            Coordinate c = v.getCoordinate();
            this.envelope.expandToInclude(c);
        }
    }

    /**
     * Calculates convexHull of all the vertices during build time
     */
    public void calculateConvexHull() {
        convexHull = GraphUtils.makeConvexHull(this);
    }

    /**
     * @return calculated convexHull;
     */
    public Geometry getConvexHull() {
        return convexHull;

    }

    /**
     * Expands envelope to include given point
     *
     * If envelope is empty it creates it (This can happen with a graph without OSM data)
     * Used when adding stops to OSM envelope
     *
     * @param  x  the value to lower the minimum x to or to raise the maximum x to
     * @param  y  the value to lower the minimum y to or to raise the maximum y to
     */
    public void expandToInclude(double x, double y) {
        //Envelope can be empty if graph building is run without OSM data
        if (this.envelope == null) {
            calculateEnvelope();
        }
        this.envelope.expandToInclude(x, y);
    }

    public WorldEnvelope getEnvelope() {
        return this.envelope;
    }

    /**
     * Calculates Transit center from median of coordinates of all transitStops if graph
     * has transit. If it doesn't it isn't calculated. (mean walue of min, max latitude and longitudes are used)
     *
     * Transit center is saved in center variable
     *
     * This speeds up calculation, but problem is that median needs to have all of latitudes/longitudes
     * in memory, this can become problematic in large installations. It works without a problem on New York State.
     */
    public void calculateTransitCenter() {
        if (hasTransit) {

            TDoubleList latitudes = new TDoubleLinkedList();
            TDoubleList longitudes = new TDoubleLinkedList();
            Median median = new Median();

            getVertices().stream()
                .filter(v -> v instanceof TransitStopVertex)
                .forEach(v -> {
                    latitudes.add(v.getLat());
                    longitudes.add(v.getLon());
                });

            median.setData(latitudes.toArray());
            double medianLatitude = median.evaluate();
            median = new Median();
            median.setData(longitudes.toArray());
            double medianLongitude = median.evaluate();

            this.center = new Coordinate(medianLongitude, medianLatitude);
        }
    }

    public Optional getCenter() {
        return Optional.ofNullable(center);
    }

    public long getTransitServiceStarts() {
        return transitServiceStarts;
    }

    public long getTransitServiceEnds() {
        return transitServiceEnds;
    }

    public Multimap, Notice> getNoticesByElement() {
        return noticesByElement;
    }

    public void addNoticeAssignments(Multimap, Notice> noticesByElement) {
        this.noticesByElement.putAll(noticesByElement);
    }

    public double getDistanceBetweenElevationSamples() {
        return distanceBetweenElevationSamples;
    }

    public void setDistanceBetweenElevationSamples(double distanceBetweenElevationSamples) {
        this.distanceBetweenElevationSamples = distanceBetweenElevationSamples;
        CompactElevationProfile.setDistanceBetweenSamplesM(distanceBetweenElevationSamples);
    }

    public TransitAlertService getTransitAlertService() {
        if (transitAlertService == null) {
            transitAlertService = new DelegatingTransitAlertServiceImpl(this);
        }
        return transitAlertService;
    }

    private Collection getStopsForId(FeedScopedId id) {

        // GroupOfStations
        GroupOfStations groupOfStations = groupOfStationsById.get(id);
        if (groupOfStations != null) {
            return groupOfStations.getChildStops();
        }

        // Multimodal station
        MultiModalStation multiModalStation = multiModalStationById.get(id);
        if (multiModalStation != null) {
            return multiModalStation.getChildStops();
        }

        // Station
        Station station = stationById.get(id);
        if (station != null) {
            return station.getChildStops();
        }
        // Single stop
        Stop stop = index.getStopForId(id);
        if (stop != null) {
            return Collections.singleton(stop);
        }

        return null;
    }

    /**
     *
     * @param id Id of Stop, Station, MultiModalStation or GroupOfStations
     * @return The associated TransitStopVertex or all underlying TransitStopVertices
     */
    public Set getStopVerticesById(FeedScopedId id) {
        Collection stops = getStopsForId(id);

        if (stops == null) {
            return null;
        }

        return stops.stream().map(index.getStopVertexForStop()::get).collect(Collectors.toSet());
    }

    /** An OBA Service Date is a local date without timezone, only year month and day. */
    public BitSet getServicesRunningForDate(ServiceDate date) {
        BitSet services = new BitSet(calendarService.getServiceIds().size());
        for (FeedScopedId serviceId : calendarService.getServiceIdsOnDate(date)) {
            int n = serviceCodes.get(serviceId);
            if (n < 0) continue;
            services.set(n);
        }
        return services;
    }

    public BikeRentalStationService getBikerentalStationService() {
        return getService(BikeRentalStationService.class);
    }

    public Collection getNoticesByEntity(TransitEntity entity) {
        Collection res = getNoticesByElement().get(entity);
        return res == null ? Collections.emptyList() : res;
    }

    public TripPattern getTripPatternForId(FeedScopedId id) {
        return tripPatternForId.get(id);
    }

    public Collection getTripPatterns() {
        return tripPatternForId.values();
    }

    public Collection getNotices() {
        return getNoticesByElement().values();
    }

    /** Get all stops within a given bounding box. */
    public Collection getStopsByBoundingBox(double minLat, double minLon, double maxLat, double maxLon) {
        Envelope envelope = new Envelope(
                new Coordinate(minLon, minLat),
                new Coordinate(maxLon, maxLat)
        );
        return streetIndex
            .getTransitStopForEnvelope(envelope)
            .stream()
            .map(TransitStopVertex::getStop)
            .collect(Collectors.toList());
    }

    /** Get all stops within a given radius. Unit: meters. */
    public List> getStopsInRadius(WgsCoordinate center, double radius) {
        Coordinate coord = new Coordinate(center.longitude(), center.latitude());
        return streetIndex.getNearbyTransitStops(coord, radius).stream()
                .map(v -> new T2<>(v.getStop(), SphericalDistanceLibrary.fastDistance(v.getCoordinate(), coord)))
                .filter(t -> t.second < radius)
                .collect(Collectors.toList());
    }

    public Station getStationById(FeedScopedId id) {
        return stationById.get(id);
    }

    public MultiModalStation getMultiModalStation(FeedScopedId id) {
        return multiModalStationById.get(id);
    }

    public Collection getStations() {
        return stationById.values();
    }

    public Map getServiceCodes() {
        return serviceCodes;
    }

    public Collection getTransfersByStop(StopLocation stop) {
        return transfersByStop.get(stop);
    }
}