package org.opentripplanner.updater.street_notes; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.HashMultimap; import com.google.common.collect.SetMultimap; import com.vividsolutions.jts.geom.Geometry; 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.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.routing.alertpatch.Alert; 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.MatcherAndAlert; import org.opentripplanner.routing.services.notes.NoteMatcher; import org.opentripplanner.routing.services.notes.StreetNotesService; import org.opentripplanner.updater.GraphUpdaterManager; import org.opentripplanner.updater.GraphWriterRunnable; import org.opentripplanner.updater.PollingGraphUpdater; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URL; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.prefs.Preferences; /** * 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 * * @author hannesj */ public abstract class WFSNotePollingGraphUpdater extends PollingGraphUpdater { protected Graph graph; private GraphUpdaterManager updaterManager; private SetMultimap<Edge, MatcherAndAlert> notesForEdge; /** * Set of unique matchers, kept during building phase, used for interning (lots of note/matchers * are identical). */ private Map<T2<NoteMatcher, Alert>, MatcherAndAlert> uniqueMatchers; private URL url; private String featureType; private Query query; private FeatureSource<SimpleFeatureType, SimpleFeature> featureSource; private DynamicStreetNotesSource notesSource = new DynamicStreetNotesSource(); // 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, can be overridden in extending classes private static final NoteMatcher NOTE_MATCHER = StreetNotesService.ALWAYS_MATCHER; private static Logger LOG = LoggerFactory.getLogger(WFSNotePollingGraphUpdater.class); /** * Here the updater can be configured using the properties in the file 'Graph.properties'. * The property frequencySec is already read and used by the abstract base class. */ @Override protected void configurePolling(Graph graph, JsonNode config) throws Exception { url = new URL(config.path("url").asText()); featureType = config.path("featureType").asText(); this.graph = graph; LOG.info("Configured WFS polling updater: frequencySec={}, url={} and featureType={}", frequencySec, url.toString(), featureType); } /** * Here the updater gets to know its parent manager to execute GraphWriterRunnables. */ @Override public void setGraphUpdaterManager(GraphUpdaterManager updaterManager) { this.updaterManager = updaterManager; } /** * Setup the WFS data source and add the DynamicStreetNotesSource to the graph */ @Override public void setup() throws IOException, FactoryException { LOG.info("Setup WFS polling updater"); HashMap<String, Object> connectionParameters = new HashMap<>(); connectionParameters.put(WFSDataStoreFactory.URL.key, url); WFSDataStore data = (new WFSDataStoreFactory()).createDataStore(connectionParameters); query = new Query(featureType); // Read only single feature type from the source query.setCoordinateSystem(CRS.decode("EPSG:4326", true)); // Get coordinates in WGS-84 featureSource = data.getFeatureSource(featureType); graph.streetNotesService.addNotesSource(notesSource); } @Override public void teardown() { LOG.info("Teardown WFS polling updater"); } /** * 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<SimpleFeature> features = featureSource.getFeatures(query).features(); while ( features.hasNext()){ SimpleFeature feature = features.next(); if (feature.getDefaultGeometry() == null) continue; Alert alert = getNote(feature); if (alert == null) continue; Geometry geom = (Geometry) feature.getDefaultGeometry(); Geometry searchArea = geom.buffer(SEARCH_RADIUS_DEG); Collection<Edge> edges = graph.streetIndex.getEdgesForEnvelope(searchArea.getEnvelopeInternal()); for(Edge edge: edges){ if (edge instanceof StreetEdge && !searchArea.disjoint(edge.getGeometry())) { addNote(edge, alert, NOTE_MATCHER); } } } updaterManager.execute(new WFSGraphWriter()); } /** * Parses a SimpleFeature and returns an Alert if the feature should create one. * The alert should be based on the fields specific for the specific WFS feed. */ protected abstract Alert getNote(SimpleFeature feature); /** * Changes the note source to use the newly generated notes */ private class WFSGraphWriter implements GraphWriterRunnable { public void run(Graph graph) { notesSource.setNotes(notesForEdge); } } /** * Methods for writing into notesForEdge * TODO: Should these be extracted into somewhere? */ private void addNote(Edge edge, Alert note, NoteMatcher matcher) { if (LOG.isDebugEnabled()) LOG.debug("Adding note {} to {} with matcher {}", note, edge, matcher); notesForEdge.put(edge, buildMatcherAndAlert(matcher, note)); } /** * Create a MatcherAndAlert, 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 MatcherAndAlert buildMatcherAndAlert(NoteMatcher noteMatcher, Alert note) { T2<NoteMatcher, Alert> key = new T2<>(noteMatcher, note); MatcherAndAlert interned = uniqueMatchers.get(key); if (interned != null) { return interned; } MatcherAndAlert ret = new MatcherAndAlert(noteMatcher, note); uniqueMatchers.put(key, ret); return ret; } }