diff --git a/pom.xml b/pom.xml index d02ff1de8e8..b5c9f414f67 100644 --- a/pom.xml +++ b/pom.xml @@ -79,7 +79,7 @@ 20.1 - 16.5 + 20.1 2.8.11 2.18 @@ -508,7 +508,7 @@ org.geotools - gt-wfs + gt-wfs-ng ${geotools.wfs.version} diff --git a/src/main/java/org/opentripplanner/routing/core/RoutingContext.java b/src/main/java/org/opentripplanner/routing/core/RoutingContext.java index 906549993ec..9a8a26b2370 100644 --- a/src/main/java/org/opentripplanner/routing/core/RoutingContext.java +++ b/src/main/java/org/opentripplanner/routing/core/RoutingContext.java @@ -2,13 +2,12 @@ import com.google.common.collect.Iterables; import org.locationtech.jts.geom.LineString; -import org.opentripplanner.model.Agency; +import org.opentripplanner.api.resource.DebugOutput; +import org.opentripplanner.common.geometry.GeometryUtils; +import org.opentripplanner.model.CalendarService; import org.opentripplanner.model.FeedScopedId; import org.opentripplanner.model.Stop; import org.opentripplanner.model.calendar.ServiceDate; -import org.opentripplanner.model.CalendarService; -import org.opentripplanner.api.resource.DebugOutput; -import org.opentripplanner.common.geometry.GeometryUtils; import org.opentripplanner.routing.algorithm.strategies.EuclideanRemainingWeightHeuristic; import org.opentripplanner.routing.algorithm.strategies.RemainingWeightHeuristic; import org.opentripplanner.routing.algorithm.strategies.TrivialRemainingWeightHeuristic; @@ -23,6 +22,7 @@ import org.opentripplanner.routing.graph.Vertex; import org.opentripplanner.routing.location.StreetLocation; import org.opentripplanner.routing.location.TemporaryStreetLocation; +import org.opentripplanner.routing.roadworks.RoadworksSource; import org.opentripplanner.routing.services.OnBoardDepartService; import org.opentripplanner.routing.vertextype.TemporaryVertex; import org.opentripplanner.routing.vertextype.TransitStop; @@ -31,17 +31,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collection; -import java.util.Comparator; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TimeZone; +import java.util.*; /** * A RoutingContext holds information needed to carry out a search for a particular TraverseOptions, on a specific graph. @@ -361,6 +351,10 @@ public void check() { } } + public RoadworksSource getRoadworksSource() { + return Optional.ofNullable(graph.roadworksSource).orElse(new RoadworksSource()); + } + /** * Cache ServiceDay objects representing which services are running yesterday, today, and tomorrow relative to the search time. This information * is very heavily used (at every transit boarding) and Date operations were identified as a performance bottleneck. Must be called after the diff --git a/src/main/java/org/opentripplanner/routing/edgetype/StreetEdge.java b/src/main/java/org/opentripplanner/routing/edgetype/StreetEdge.java index 82b30039d78..542eeb84f66 100644 --- a/src/main/java/org/opentripplanner/routing/edgetype/StreetEdge.java +++ b/src/main/java/org/opentripplanner/routing/edgetype/StreetEdge.java @@ -561,6 +561,9 @@ private double calculateCarSpeed(RoutingRequest options) { public double calculateSpeed(RoutingRequest options, TraverseMode traverseMode, long timeMillis) { if (traverseMode == null) { return Double.NaN; + } else if (traverseMode.isDriving() && options.getRoutingContext().getRoadworksSource().isBlocked(this)) { + // if the street blocked due to roadworks then the driving speed is set to 0 meters/second, ie. not traversable + return 1; } else if (traverseMode.isDriving()) { // NOTE: Automobiles have variable speeds depending on the edge type return calculateCarSpeed(options); diff --git a/src/main/java/org/opentripplanner/routing/graph/Graph.java b/src/main/java/org/opentripplanner/routing/graph/Graph.java index 63c0e3359cd..8c77eee4ab8 100644 --- a/src/main/java/org/opentripplanner/routing/graph/Graph.java +++ b/src/main/java/org/opentripplanner/routing/graph/Graph.java @@ -49,6 +49,7 @@ import org.opentripplanner.routing.edgetype.TripPattern; import org.opentripplanner.routing.flex.FlexIndex; import org.opentripplanner.routing.impl.DefaultStreetVertexIndexFactory; +import org.opentripplanner.routing.roadworks.RoadworksSource; import org.opentripplanner.routing.services.StreetVertexIndexFactory; import org.opentripplanner.routing.services.StreetVertexIndexService; import org.opentripplanner.routing.services.notes.StreetNotesService; @@ -129,6 +130,8 @@ public class Graph implements Serializable { public transient TimetableSnapshotSource timetableSnapshotSource = null; + public transient RoadworksSource roadworksSource = new RoadworksSource(30082004L); + private transient List graphBuilderAnnotations = new LinkedList(); // initialize for tests private Map> agenciesForFeedId = new HashMap<>(); @@ -1105,4 +1108,6 @@ public void setUseFlexService(boolean useFlexService) { } this.useFlexService = useFlexService; } + + public RoadworksSource getRoadworksSource() { return roadworksSource; } } diff --git a/src/main/java/org/opentripplanner/routing/roadworks/RoadworksSource.java b/src/main/java/org/opentripplanner/routing/roadworks/RoadworksSource.java new file mode 100644 index 00000000000..bf9b5db01ef --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/roadworks/RoadworksSource.java @@ -0,0 +1,34 @@ +package org.opentripplanner.routing.roadworks; + +import com.google.common.collect.Sets; +import org.opentripplanner.routing.edgetype.StreetEdge; +import org.opentripplanner.routing.graph.Edge; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +public class RoadworksSource { + + private Set blockedWayIds; + private Set blockedEdgeIds = new HashSet<>(); + + public RoadworksSource() { + this.blockedWayIds = Sets.newHashSet(); + } + + public RoadworksSource(Long ...blockedWayId) { + this.blockedWayIds = Sets.newHashSet(blockedWayId); + } + + + public boolean isBlocked(StreetEdge edge){ + return blockedWayIds.contains(edge.wayId); + } + + public void addBlockedEdges(Collection edges) { + Set ids = edges.stream().map(Edge::getId).collect(Collectors.toSet()); + blockedEdgeIds.addAll(ids); + } +} diff --git a/src/main/java/org/opentripplanner/updater/GraphUpdaterConfigurator.java b/src/main/java/org/opentripplanner/updater/GraphUpdaterConfigurator.java index 97673cfedcc..0ee2fdcc090 100644 --- a/src/main/java/org/opentripplanner/updater/GraphUpdaterConfigurator.java +++ b/src/main/java/org/opentripplanner/updater/GraphUpdaterConfigurator.java @@ -8,6 +8,7 @@ import org.opentripplanner.updater.car_park.CarParkUpdater; import org.opentripplanner.updater.example.ExampleGraphUpdater; import org.opentripplanner.updater.example.ExamplePollingGraphUpdater; +import org.opentripplanner.updater.roadworks.WFSRoadworksPollingGraphUpdater; import org.opentripplanner.updater.stoptime.MqttGtfsRealtimeUpdater; import org.opentripplanner.updater.stoptime.PollingStoptimeUpdater; import org.opentripplanner.updater.stoptime.WebsocketGtfsRealtimeUpdater; @@ -99,6 +100,9 @@ else if (type.equals("example-polling-updater")) { else if (type.equals("winkki-polling-updater")) { updater = new WinkkiPollingGraphUpdater(); } + else if (type.equals("wfs-roadworks-updater")) { + updater = new WFSRoadworksPollingGraphUpdater(); + } } if (updater == null) { diff --git a/src/main/java/org/opentripplanner/updater/roadworks/WFSRoadworksPollingGraphUpdater.java b/src/main/java/org/opentripplanner/updater/roadworks/WFSRoadworksPollingGraphUpdater.java new file mode 100644 index 00000000000..32c341e4afe --- /dev/null +++ b/src/main/java/org/opentripplanner/updater/roadworks/WFSRoadworksPollingGraphUpdater.java @@ -0,0 +1,153 @@ +package org.opentripplanner.updater.roadworks; + +import com.fasterxml.jackson.databind.JsonNode; +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.routing.graph.Edge; +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.routing.roadworks.RoadworksSource; +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.opentripplanner.updater.street_notes.WinkkiPollingGraphUpdater; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.Serializable; +import java.net.URL; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +/** + * A graph updater that reads a WFS-interface and updates a RoadworksSource. + * 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 + *
+ *    type = wfs-roadworks-updater
+ *    frequencySec = 21600
+ *    url = https://baustellen.strassen.baden-wuerttemberg.de/bis_wfs/wfs&Version=1.1.0&Request=GetCapabilities
+ *    featureType = bis:Baustelle
+ * 
+ * + * @author Leonard Ehrenfried + */ +public class WFSRoadworksPollingGraphUpdater extends PollingGraphUpdater { + protected Graph graph; + + private GraphUpdaterManager updaterManager; + + private URL url; + private String featureType; + private Query query; + + private FeatureSource featureSource; + private RoadworksSource roadworksSource = new RoadworksSource(); + + // 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(WFSRoadworksPollingGraphUpdater.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={}", + pollingPeriodSeconds, 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(Graph graph) throws IOException, FactoryException { + LOG.info("Setup WFS polling updater"); + HashMap connectionParameters = new HashMap<>(); + connectionParameters.put(WFSDataStoreFactory.URL.key, url); + connectionParameters.put(WFSDataStoreFactory.LENIENT.key, true); + 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.roadworksSource = roadworksSource; + } + + @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()); + + FeatureIterator features = featureSource.getFeatures(query).features(); + + List blockedEdges = new LinkedList<>(); + + while ( features.hasNext()){ + SimpleFeature feature = features.next(); + if (feature.getDefaultGeometry() == null) continue; + + Geometry geom = (Geometry) feature.getDefaultGeometry(); + Geometry searchArea = geom.buffer(SEARCH_RADIUS_DEG); + Collection edges = graph.streetIndex.getEdgesForEnvelope(searchArea.getEnvelopeInternal()); + + + blockedEdges.addAll(edges); + } + + roadworksSource.addBlockedEdges(blockedEdges); + + updaterManager.execute(new WFSGraphWriter()); + } + + private class WFSGraphWriter implements GraphWriterRunnable { + public void run(Graph graph) { + graph.roadworksSource = roadworksSource; + } + } + + +} diff --git a/src/main/java/org/opentripplanner/updater/street_notes/WFSNotePollingGraphUpdater.java b/src/main/java/org/opentripplanner/updater/street_notes/WFSNotePollingGraphUpdater.java index 181e50a538e..ee29a3e4277 100644 --- a/src/main/java/org/opentripplanner/updater/street_notes/WFSNotePollingGraphUpdater.java +++ b/src/main/java/org/opentripplanner/updater/street_notes/WFSNotePollingGraphUpdater.java @@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.io.Serializable; import java.net.URL; import java.util.Collection; import java.util.HashMap; @@ -104,7 +105,7 @@ public void setGraphUpdaterManager(GraphUpdaterManager updaterManager) { @Override public void setup(Graph graph) throws IOException, FactoryException { LOG.info("Setup WFS polling updater"); - HashMap connectionParameters = new HashMap<>(); + HashMap 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 diff --git a/src/test/java/org/opentripplanner/ConstantsForTests.java b/src/test/java/org/opentripplanner/ConstantsForTests.java index 6c5efc7db3c..04cd57c19bf 100644 --- a/src/test/java/org/opentripplanner/ConstantsForTests.java +++ b/src/test/java/org/opentripplanner/ConstantsForTests.java @@ -36,6 +36,8 @@ public class ConstantsForTests { public static final String OSLO_MINIMAL_OSM = "src/test/resources/oslo/oslo_osm_minimal.pbf"; + public static final String HERRENBERG_OSM = "src/test/resources/herrenberg/herrenberg.osm.pbf"; + public static final String HSL_MINIMAL_GTFS = "src/test/resources/hsl/hsl_gtfs_minimal.zip"; public static final String VERMONT_GTFS = "/vermont/ruralcommunity-flex-vt-us.zip"; diff --git a/src/test/java/org/opentripplanner/routing/core/RoadworksTest.java b/src/test/java/org/opentripplanner/routing/core/RoadworksTest.java new file mode 100644 index 00000000000..cb39d79c3e5 --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/core/RoadworksTest.java @@ -0,0 +1,120 @@ +/* This program is free software: you can redistribute it and/or + modify it under the terms of the GNU Lesser 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 . */ + +package org.opentripplanner.routing.core; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.geotools.geojson.geom.GeometryJSON; +import org.junit.BeforeClass; +import org.junit.Test; +import org.opentripplanner.ConstantsForTests; +import org.opentripplanner.common.model.GenericLocation; +import org.opentripplanner.graph_builder.GraphBuilder; +import org.opentripplanner.graph_builder.model.GtfsBundle; +import org.opentripplanner.graph_builder.module.GtfsModule; +import org.opentripplanner.graph_builder.module.StreetLinkerModule; +import org.opentripplanner.graph_builder.module.osm.OpenStreetMapModule; +import org.opentripplanner.graph_builder.services.DefaultStreetEdgeFactory; +import org.opentripplanner.openstreetmap.impl.AnyFileBasedOpenStreetMapProviderImpl; +import org.opentripplanner.openstreetmap.services.OpenStreetMapProvider; +import org.opentripplanner.routing.algorithm.AStar; +import org.opentripplanner.routing.edgetype.StreetEdge; +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.routing.impl.DefaultStreetVertexIndexFactory; +import org.opentripplanner.routing.roadworks.RoadworksSource; +import org.opentripplanner.routing.spt.GraphPath; +import org.opentripplanner.routing.spt.ShortestPathTree; +import org.opentripplanner.util.TestUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +public class RoadworksTest { + + private static final Logger LOG = LoggerFactory.getLogger(RoadworksTest.class); + AStar aStar = new AStar(); + + static Graph graph; + static Long blockedWayId = 30082004L; + + GenericLocation zeppelinstr = new GenericLocation(48.59654,8.86100); + GenericLocation hildrizhauserStr = new GenericLocation(48.60008,8.88863); + // needs to be in 2017 because that's when the Oslo GTFS feed is valid. probably want to get a slimmed down Herrenberg feed. + long dateTime = TestUtils.dateInSeconds("Europe/Berlin", 2017, 10, 15, 7, 0, 0); + GeometryJSON geojson = new GeometryJSON(); + + + @BeforeClass + public static void setUp() { + GraphBuilder graphBuilder = new GraphBuilder(); + + List osmProviders = Lists.newArrayList(); + OpenStreetMapProvider osmProvider = new AnyFileBasedOpenStreetMapProviderImpl(new File(ConstantsForTests.HERRENBERG_OSM)); + osmProviders.add(osmProvider); + OpenStreetMapModule osmModule = new OpenStreetMapModule(osmProviders); + osmModule.edgeFactory = new DefaultStreetEdgeFactory(); + osmModule.skipVisibility = true; + graphBuilder.addModule(osmModule); + List gtfsBundles = Lists.newArrayList(); + GtfsBundle gtfsBundle = new GtfsBundle(new File(ConstantsForTests.OSLO_MINIMAL_GTFS)); + gtfsBundles.add(gtfsBundle); + GtfsModule gtfsModule = new GtfsModule(gtfsBundles); + graphBuilder.addModule(gtfsModule); + graphBuilder.addModule(new StreetLinkerModule()); + graphBuilder.serializeGraph = false; + graphBuilder.run(); + + graph = graphBuilder.getGraph(); + graph.roadworksSource = new RoadworksSource(blockedWayId); + graph.index(new DefaultStreetVertexIndexFactory()); + } + + private RoutingRequest buildRoutingRequest(Graph graph) { + RoutingRequest request = new RoutingRequest(); + request.dateTime = dateTime; + request.from = zeppelinstr; + request.to = hildrizhauserStr; + + request.setNumItineraries(1); + request.setRoutingContext(graph); + + request.modes = new TraverseModeSet(TraverseMode.CAR); + return request; + } + + @Test + public void withStreetBlockedDueToRoadworks() { + RoutingRequest options = buildRoutingRequest(graph); + ShortestPathTree tree = aStar.getShortestPathTree(options); + GraphPath path = tree.getPaths().get(0); + + LOG.info(geojson.toString(path.getGeometry())); + + List wayIds = path.edges.stream() + .filter(e -> e instanceof StreetEdge) + .map(e -> (StreetEdge) e) + .map(e -> e.wayId) + .collect(Collectors.toList()); + + assertThat(wayIds, not(hasItem(blockedWayId))); + } + +} diff --git a/src/test/resources/herrenberg/herrenberg.geojson b/src/test/resources/herrenberg/herrenberg.geojson new file mode 100644 index 00000000000..68c8ce754a7 --- /dev/null +++ b/src/test/resources/herrenberg/herrenberg.geojson @@ -0,0 +1,100 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 8.85446548461914, + 48.60079266327784 + ], + [ + 8.844079971313477, + 48.596251662217554 + ], + [ + 8.846397399902342, + 48.592902412395865 + ], + [ + 8.85171890258789, + 48.590915464357714 + ], + [ + 8.852577209472656, + 48.58699810936769 + ], + [ + 8.857383728027344, + 48.58518126194807 + ], + [ + 8.870773315429686, + 48.58892843818417 + ], + [ + 8.873348236083984, + 48.5865439036362 + ], + [ + 8.879613876342773, + 48.58682778269678 + ], + [ + 8.878240585327148, + 48.59051806537385 + ], + [ + 8.88467788696289, + 48.59227795143187 + ], + [ + 8.888626098632812, + 48.595116348170286 + ], + [ + 8.887166976928711, + 48.59772753233707 + ], + [ + 8.889226913452148, + 48.60045210235787 + ], + [ + 8.88845443725586, + 48.601530537394176 + ], + [ + 8.88570785522461, + 48.60204136700653 + ], + [ + 8.882017135620117, + 48.603176525426456 + ], + [ + 8.87643814086914, + 48.605390010965884 + ], + [ + 8.869829177856444, + 48.605390010965884 + ], + [ + 8.86021614074707, + 48.603119768111384 + ], + [ + 8.85446548461914, + 48.60079266327784 + ] + ] + ] + } + } + ] +} diff --git a/src/test/resources/herrenberg/herrenberg.osm.pbf b/src/test/resources/herrenberg/herrenberg.osm.pbf new file mode 100644 index 00000000000..9a326a2729a Binary files /dev/null and b/src/test/resources/herrenberg/herrenberg.osm.pbf differ