
org.opentripplanner.updater.street_note.WFSNotePollingGraphUpdater Maven / Gradle / Ivy
package org.opentripplanner.updater.street_note;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import java.io.IOException;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.geotools.data.FeatureSource;
import org.geotools.data.Query;
import org.geotools.data.wfs.WFSDataStore;
import org.geotools.data.wfs.WFSDataStoreFactory;
import org.geotools.feature.FeatureIterator;
import org.geotools.referencing.CRS;
import org.locationtech.jts.geom.Geometry;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.referencing.FactoryException;
import org.opentripplanner.common.geometry.SphericalDistanceLibrary;
import org.opentripplanner.common.model.T2;
import org.opentripplanner.model.StreetNote;
import org.opentripplanner.routing.edgetype.StreetEdge;
import org.opentripplanner.routing.graph.Edge;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.services.notes.DynamicStreetNotesSource;
import org.opentripplanner.routing.services.notes.MatcherAndStreetNote;
import org.opentripplanner.routing.services.notes.NoteMatcher;
import org.opentripplanner.routing.services.notes.StreetNotesService;
import org.opentripplanner.transit.service.TransitModel;
import org.opentripplanner.updater.GraphWriterRunnable;
import org.opentripplanner.updater.PollingGraphUpdater;
import org.opentripplanner.updater.WriteToGraphCallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A graph updater that reads a WFS-interface and updates a DynamicStreetNotesSource. Useful when
* reading geodata from legacy/external sources, which are not based on OSM and where data has to be
* matched to the street network.
*
* Classes that extend this class should provide getNote which parses the WFS features into notes.
* Also the implementing classes should be added to the GraphUpdaterConfigurator
*
* @see WinkkiPollingGraphUpdater
*/
public abstract class WFSNotePollingGraphUpdater extends PollingGraphUpdater {
private static final Logger LOG = LoggerFactory.getLogger(WFSNotePollingGraphUpdater.class);
/**
* How much should the geometries be padded with in order to be sure they intersect with
* graph edges
*/
private static final double SEARCH_RADIUS_M = 1;
private static final double SEARCH_RADIUS_DEG = SphericalDistanceLibrary.metersToDegrees(
SEARCH_RADIUS_M
);
/** Set the matcher type for the notes */
private static final NoteMatcher NOTE_MATCHER = StreetNotesService.ALWAYS_MATCHER;
private final DynamicStreetNotesSource notesSource = new DynamicStreetNotesSource();
private final FeatureSource featureSource;
private final Query query;
private final Graph graph;
private WriteToGraphCallback saveResultOnGraph;
private SetMultimap notesForEdge;
/**
* Set of unique matchers, kept during building phase, used for interning (lots of note/matchers
* are identical).
*/
private Map, MatcherAndStreetNote> uniqueMatchers;
/**
* The property 'frequencySec' is already read and used by the abstract base class.
*/
public WFSNotePollingGraphUpdater(WFSNotePollingGraphUpdaterParameters config, Graph graph) {
super(config);
try {
LOG.info("Setup WFS polling updater");
URL url = new URL(config.getUrl());
String featureType = config.getFeatureType();
this.graph = graph;
HashMap connectionParameters = new HashMap<>();
connectionParameters.put(WFSDataStoreFactory.URL.key, url);
WFSDataStore data = (new WFSDataStoreFactory()).createDataStore(connectionParameters);
this.query = new Query(featureType); // Read only single feature type from the source
this.query.setCoordinateSystem(CRS.decode("EPSG:4326", true)); // Get coordinates in WGS-84
this.featureSource = data.getFeatureSource(featureType);
graph.streetNotesService.addNotesSource(notesSource);
LOG.info(
"Configured WFS polling updater: frequencySec={}, url={} and featureType={}",
pollingPeriodSeconds(),
url.toString(),
featureType
);
} catch (FactoryException | IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
/**
* Here the updater gets to know its parent manager to execute GraphWriterRunnables.
*/
@Override
public void setGraphUpdaterManager(WriteToGraphCallback saveResultOnGraph) {
this.saveResultOnGraph = saveResultOnGraph;
}
/**
* The function is run periodically by the update manager. The extending class should provide the
* getNote method. It is not implemented here as the requirements for different updaters can be
* vastly different dependent on the data source.
*/
@Override
protected void runPolling() throws IOException {
LOG.info("Run WFS polling updater with hashcode: {}", this.hashCode());
notesForEdge = HashMultimap.create();
uniqueMatchers = new HashMap<>();
FeatureIterator features = featureSource.getFeatures(query).features();
while (features.hasNext()) {
SimpleFeature feature = features.next();
if (feature.getDefaultGeometry() == null) continue;
StreetNote streetNote = getNote(feature);
if (streetNote == null) continue;
Geometry geom = (Geometry) feature.getDefaultGeometry();
Geometry searchArea = geom.buffer(SEARCH_RADIUS_DEG);
Collection edges = graph
.getStreetIndex()
.getEdgesForEnvelope(searchArea.getEnvelopeInternal());
for (Edge edge : edges) {
if (edge instanceof StreetEdge && !searchArea.disjoint(edge.getGeometry())) {
addNote(edge, streetNote, NOTE_MATCHER);
}
}
}
saveResultOnGraph.execute(new WFSGraphWriter());
}
/**
* Parses a SimpleFeature and returns an StreetNote if the feature should create one. The street
* note should be based on the fields specific for the specific WFS feed.
*/
protected abstract StreetNote getNote(SimpleFeature feature);
/**
* Methods for writing into notesForEdge
* TODO: Should these be extracted into somewhere?
*/
private void addNote(Edge edge, StreetNote note, NoteMatcher matcher) {
if (LOG.isDebugEnabled()) LOG.debug(
"Adding note {} to {} with matcher {}",
note,
edge,
matcher
);
notesForEdge.put(edge, buildMatcherAndStreetNote(matcher, note));
}
/**
* Create a MatcherAndStreetNote, interning it if the note and matcher pair is already created.
* Note: we use the default Object.equals() for matchers, as they are mostly already singleton
* instances.
*/
private MatcherAndStreetNote buildMatcherAndStreetNote(NoteMatcher noteMatcher, StreetNote note) {
T2 key = new T2<>(noteMatcher, note);
MatcherAndStreetNote interned = uniqueMatchers.get(key);
if (interned != null) {
return interned;
}
MatcherAndStreetNote ret = new MatcherAndStreetNote(noteMatcher, note);
uniqueMatchers.put(key, ret);
return ret;
}
/**
* Changes the note source to use the newly generated notes
*/
private class WFSGraphWriter implements GraphWriterRunnable {
public void run(Graph graph, TransitModel transitModel) {
notesSource.setNotes(notesForEdge);
}
}
}