org.opentripplanner.routing.graph.Graph Maven / Gradle / Ivy
package org.opentripplanner.routing.graph;
import com.google.common.annotations.VisibleForTesting;
import java.io.Serializable;
import java.time.Instant;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.prefs.Preferences;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.opentripplanner.common.TurnRestriction;
import org.opentripplanner.common.geometry.CompactElevationProfile;
import org.opentripplanner.common.geometry.GraphUtils;
import org.opentripplanner.ext.dataoverlay.configuration.DataOverlayParameterBindings;
import org.opentripplanner.ext.geocoder.LuceneIndex;
import org.opentripplanner.graph_builder.linking.VertexLinker;
import org.opentripplanner.model.calendar.openinghours.OpeningHoursCalendarService;
import org.opentripplanner.routing.edgetype.StreetEdge;
import org.opentripplanner.routing.fares.FareService;
import org.opentripplanner.routing.impl.StreetVertexIndex;
import org.opentripplanner.routing.services.RealtimeVehiclePositionService;
import org.opentripplanner.routing.services.notes.StreetNotesService;
import org.opentripplanner.routing.vehicle_parking.VehicleParkingService;
import org.opentripplanner.routing.vehicle_rental.VehicleRentalStationService;
import org.opentripplanner.routing.vertextype.TransitStopVertex;
import org.opentripplanner.transit.model.framework.Deduplicator;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.service.StopModel;
import org.opentripplanner.util.ElevationUtils;
import org.opentripplanner.util.WorldEnvelope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 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);
public final StreetNotesService streetNotesService = new StreetNotesService();
/* Ideally we could just get rid of vertex labels, but they're used in tests and graph building. */
private final Map vertices = new ConcurrentHashMap<>();
public final transient Deduplicator deduplicator;
public final Instant buildTime = Instant.now();
@Nullable
private final OpeningHoursCalendarService openingHoursCalendarService;
private transient StreetVertexIndex streetIndex;
//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 preferences that were used for graph building. */
public Preferences preferences = null;
// TODO OTP2: This is only enabled with static bike rental
public boolean hasBikeSharing = false;
public boolean hasParkRide = false;
public boolean hasBikeRide = false;
/** True if OSM data was loaded into this Graph. */
public boolean hasStreets = false;
/**
* Have bike parks already been linked to the graph. As the linking happens twice if a base graph
* is used, we store information on whether bike park linking should be skipped.
*/
public boolean hasLinkedBikeParks = false;
/**
* The difference in meters between the WGS84 ellipsoid height and geoid height at the graph's
* center
*/
public Double ellipsoidToGeoidDifference = 0.0;
/**
* Does this graph contain elevation data?
*/
public boolean hasElevation = false;
/**
* If this graph contains elevation data, the minimum value.
*/
public Double minElevation = null;
/**
* If this graph contains elevation data, the maximum value.
*/
public Double maxElevation = null;
/** The distance between elevation samples used in CompactElevationProfile. */
// TODO refactoring transit model: remove and instead always serialize directly from and to the
// static variable in CompactElevationProfile in SerializedGraphObject
private double distanceBetweenElevationSamples;
private transient RealtimeVehiclePositionService vehiclePositionService;
private final VehicleRentalStationService vehicleRentalStationService = new VehicleRentalStationService();
private final VehicleParkingService vehicleParkingService = new VehicleParkingService();
private FareService fareService;
/**
* 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;
/**
* DataOverlay Sandbox module parameter bindings configured in the build-config, and needed when
* creating the data overlay context when routing.
*/
public DataOverlayParameterBindings dataOverlayParameterBindings;
private LuceneIndex luceneIndex;
@Inject
public Graph(
Deduplicator deduplicator,
@Nullable OpeningHoursCalendarService openingHoursCalendarService
) {
this.deduplicator = deduplicator;
this.openingHoursCalendarService = openingHoursCalendarService;
}
public Graph(Deduplicator deduplicator) {
this(deduplicator, null);
}
/** Constructor for deserialization. */
public Graph() {
this(new Deduplicator(), null);
}
/**
* 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) {
streetNotesService.removeStaticNotes(e);
if (e instanceof StreetEdge) {
((StreetEdge) e).removeAllTurnRestrictions();
}
if (e.fromv != null) {
e.fromv
.getIncoming()
.stream()
.filter(StreetEdge.class::isInstance)
.map(StreetEdge.class::cast)
.forEach(otherEdge -> {
for (TurnRestriction turnRestriction : otherEdge.getTurnRestrictions()) {
if (turnRestriction.to == e) {
otherEdge.removeTurnRestriction(turnRestriction);
}
}
});
e.fromv.removeOutgoing(e);
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.
*/
public Collection getVertices() {
return this.vertices.values();
}
public List getVerticesOfType(Class cls) {
return this.getVertices()
.stream()
.filter(cls::isInstance)
.map(cls::cast)
.collect(Collectors.toList());
}
public TransitStopVertex getStopVertexForStopId(FeedScopedId id) {
return streetIndex.findTransitStopVertices(id);
}
/**
* 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;
}
public List getEdgesOfType(Class cls) {
return this.getEdges()
.stream()
.filter(cls::isInstance)
.map(cls::cast)
.collect(Collectors.toList());
}
/**
* Return only the StreetEdges in the graph.
*/
public Collection getStreetEdges() {
return getEdgesOfType(StreetEdge.class);
}
public boolean containsVertex(Vertex v) {
return (v != null) && vertices.get(v.getLabel()) == v;
}
public void remove(Vertex vertex) {
vertices.remove(vertex.getLabel());
}
public void removeIfUnconnected(Vertex v) {
if (v.getDegreeIn() == 0 && v.getDegreeOut() == 0) {
remove(v);
}
}
public Envelope getExtent() {
Envelope env = new Envelope();
for (Vertex v : getVertices()) {
env.expandToInclude(v.getCoordinate());
}
return env;
}
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;
}
/**
* Perform indexing on vertices, edges 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 OTP2 - Indexing the streetIndex is not something that should be delegated outside the
* - graph. This allows a module to index the streetIndex BEFORE another module add
* - something that should go into the index; Hence, inconsistent data.
*/
public void index(StopModel stopModel) {
LOG.info("Index street model...");
streetIndex = new StreetVertexIndex(this, stopModel);
LOG.info("Index street model complete.");
}
@Nullable
public OpeningHoursCalendarService getOpeningHoursCalendarService() {
return this.openingHoursCalendarService;
}
/**
* Get streetIndex, safe to use while routing, but do not use during graph build.
* @see #getStreetIndexSafe(StopModel)
*/
public StreetVertexIndex getStreetIndex() {
return this.streetIndex;
}
/**
* Get streetIndex during graph build, both OSM street data and transit data must be loaded
* before calling this.
*/
public StreetVertexIndex getStreetIndexSafe(StopModel stopModel) {
indexIfNotIndexed(stopModel);
return this.streetIndex;
}
/**
* Get VertexLinker, safe to use while routing, but do not use during graph build.
* @see #getLinkerSafe(StopModel)
*/
public VertexLinker getLinker() {
return streetIndex.getVertexLinker();
}
/**
* Get VertexLinker during graph build, both OSM street data and transit data must be loaded
* before calling this.
*/
public VertexLinker getLinkerSafe(StopModel stopModel) {
indexIfNotIndexed(stopModel);
return streetIndex.getVertexLinker();
}
/**
* 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 void initEllipsoidToGeoidDifference() {
try {
WorldEnvelope env = getEnvelope();
double lat = (env.getLowerLeftLatitude() + env.getUpperRightLatitude()) / 2;
double lon = (env.getLowerLeftLongitude() + env.getUpperRightLongitude()) / 2;
this.ellipsoidToGeoidDifference = ElevationUtils.computeEllipsoidToGeoidDifference(lat, lon);
LOG.info(
"Computed ellipsoid/geoid offset at (" +
lat +
", " +
lon +
") as " +
this.ellipsoidToGeoidDifference
);
} catch (Exception e) {
LOG.error("Error computing ellipsoid/geoid difference");
}
}
public WorldEnvelope getEnvelope() {
return this.envelope;
}
public double getDistanceBetweenElevationSamples() {
return distanceBetweenElevationSamples;
}
public void setDistanceBetweenElevationSamples(double distanceBetweenElevationSamples) {
this.distanceBetweenElevationSamples = distanceBetweenElevationSamples;
CompactElevationProfile.setDistanceBetweenSamplesM(distanceBetweenElevationSamples);
}
public RealtimeVehiclePositionService getVehiclePositionService() {
if (vehiclePositionService == null) {
vehiclePositionService = new RealtimeVehiclePositionService();
}
return vehiclePositionService;
}
public VehicleRentalStationService getVehicleRentalStationService() {
return vehicleRentalStationService;
}
public VehicleParkingService getVehicleParkingService() {
return vehicleParkingService;
}
public FareService getFareService() {
return fareService;
}
public void setFareService(FareService fareService) {
this.fareService = fareService;
}
public LuceneIndex getLuceneIndex() {
return luceneIndex;
}
public void setLuceneIndex(LuceneIndex luceneIndex) {
this.luceneIndex = luceneIndex;
}
private void indexIfNotIndexed(StopModel stopModel) {
if (streetIndex == null) {
index(stopModel);
}
}
}