Skip to content

Commit d852592

Browse files
authored
Use edge-based subnetwork removal if turn costs are enabled (graphhopper#2044)
1 parent db29f6d commit d852592

File tree

6 files changed

+236
-55
lines changed

6 files changed

+236
-55
lines changed
66.6 KB
Binary file not shown.

core/src/main/java/com/graphhopper/GraphHopper.java

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import com.graphhopper.routing.lm.LMPreparationHandler;
3838
import com.graphhopper.routing.querygraph.QueryGraph;
3939
import com.graphhopper.routing.subnetwork.PrepareRoutingSubnetworks;
40+
import com.graphhopper.routing.subnetwork.PrepareRoutingSubnetworks.PrepareJob;
4041
import com.graphhopper.routing.template.AlternativeRoutingTemplate;
4142
import com.graphhopper.routing.template.RoundTripRoutingTemplate;
4243
import com.graphhopper.routing.template.RoutingTemplate;
@@ -1334,15 +1335,28 @@ protected void loadOrPrepareLM(boolean closeEarly) {
13341335
* Internal method to clean up the graph.
13351336
*/
13361337
protected void cleanUp() {
1337-
int prevNodeCount = ghStorage.getNodes();
1338-
PrepareRoutingSubnetworks preparation = new PrepareRoutingSubnetworks(ghStorage, encodingManager.fetchEdgeEncoders());
1338+
PrepareRoutingSubnetworks preparation = new PrepareRoutingSubnetworks(ghStorage, buildSubnetworkRemovalJobs());
13391339
preparation.setMinNetworkSize(minNetworkSize);
13401340
preparation.doWork();
1341-
int currNodeCount = ghStorage.getNodes();
1342-
logger.info("edges: " + Helper.nf(ghStorage.getEdges()) + ", nodes " + Helper.nf(currNodeCount)
1343-
+ ", there were " + Helper.nf(preparation.getMaxSubnetworks())
1344-
+ " subnetworks. removed them => " + Helper.nf(prevNodeCount - currNodeCount)
1345-
+ " less nodes");
1341+
logger.info("nodes: " + Helper.nf(ghStorage.getNodes()) + ", edges: " + Helper.nf(ghStorage.getEdges()));
1342+
}
1343+
1344+
private List<PrepareJob> buildSubnetworkRemovalJobs() {
1345+
List<FlagEncoder> encoders = encodingManager.fetchEdgeEncoders();
1346+
List<PrepareJob> jobs = new ArrayList<>();
1347+
for (FlagEncoder encoder : encoders) {
1348+
// for encoders with turn costs we do an edge-based subnetwork removal, because they *might* be used with
1349+
// a profile with turn_costs=true
1350+
if (encoder.supportsTurnCosts()) {
1351+
// u-turn costs are zero as we only want to make sure the graph is fully connected assuming finite
1352+
// u-turn costs
1353+
TurnCostProvider turnCostProvider = new DefaultTurnCostProvider(encoder, ghStorage.getTurnCostStorage(), 0);
1354+
jobs.add(new PrepareJob(encoder.toString(), encoder.getAccessEnc(), turnCostProvider));
1355+
} else {
1356+
jobs.add(new PrepareJob(encoder.toString(), encoder.getAccessEnc(), null));
1357+
}
1358+
}
1359+
return jobs;
13461360
}
13471361

13481362
protected void flush() {

core/src/main/java/com/graphhopper/routing/subnetwork/PrepareRoutingSubnetworks.java

Lines changed: 131 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,12 @@
2121
import com.carrotsearch.hppc.BitSetIterator;
2222
import com.carrotsearch.hppc.IntArrayList;
2323
import com.carrotsearch.hppc.IntIndexedContainer;
24+
import com.carrotsearch.hppc.cursors.IntCursor;
2425
import com.graphhopper.routing.ev.BooleanEncodedValue;
2526
import com.graphhopper.routing.util.DefaultEdgeFilter;
26-
import com.graphhopper.routing.util.FlagEncoder;
27+
import com.graphhopper.routing.weighting.TurnCostProvider;
2728
import com.graphhopper.storage.GraphHopperStorage;
28-
import com.graphhopper.util.EdgeExplorer;
29-
import com.graphhopper.util.EdgeIterator;
30-
import com.graphhopper.util.Helper;
31-
import com.graphhopper.util.StopWatch;
29+
import com.graphhopper.util.*;
3230
import org.slf4j.Logger;
3331
import org.slf4j.LoggerFactory;
3432

@@ -38,29 +36,31 @@
3836
/**
3937
* Removes nodes/edges which are not part of the 'main' network(s). I.e. mostly nodes with no edges at all but
4038
* also small subnetworks which could be bugs in OSM data or 'islands' or indicate otherwise disconnected areas
41-
* e.g. via barriers or one way problems - see #86.
42-
* <p>
39+
* e.g. via barriers or one way problems - see #86. Subnetworks are removed by disabling access to the corresponding
40+
* edges for a given access encoded value. It is important to search for strongly connected components here (i.e.
41+
* consider that the graph is directed). For example, small areas like parking lots are sometimes connected to the whole
42+
* network through a single one-way road (a mapping error) and have to be removed because otherwise the routing fails
43+
* when starting from such a parking lot.
4344
*
4445
* @author Peter Karich
4546
* @author easbar
4647
*/
4748
public class PrepareRoutingSubnetworks {
4849
private final Logger logger = LoggerFactory.getLogger(getClass());
4950
private final GraphHopperStorage ghStorage;
50-
private final List<FlagEncoder> encoders;
51-
private final List<BooleanEncodedValue> accessEncList;
51+
private final List<PrepareJob> prepareJobs;
5252
private int minNetworkSize = 200;
53-
private int subnetworks = -1;
5453

55-
public PrepareRoutingSubnetworks(GraphHopperStorage ghStorage, List<FlagEncoder> encoders) {
54+
public PrepareRoutingSubnetworks(GraphHopperStorage ghStorage, List<PrepareJob> prepareJobs) {
5655
this.ghStorage = ghStorage;
57-
this.encoders = encoders;
58-
this.accessEncList = new ArrayList<>();
59-
for (FlagEncoder flagEncoder : encoders) {
60-
accessEncList.add(flagEncoder.getAccessEnc());
61-
}
56+
this.prepareJobs = prepareJobs;
6257
}
6358

59+
/**
60+
* The subnetwork removal removes components with less than {@link #minNetworkSize} nodes from the graph if it is
61+
* run node-based. For edge-based subnetwork removal it removes components with less than 2*{@link #minNetworkSize}
62+
* (directed) edges.
63+
*/
6464
public PrepareRoutingSubnetworks setMinNetworkSize(int minNetworkSize) {
6565
this.minNetworkSize = minNetworkSize;
6666
return this;
@@ -73,15 +73,16 @@ public void doWork() {
7373
}
7474
StopWatch sw = new StopWatch().start();
7575
logger.info("Start removing subnetworks (prepare.min_network_size:" + minNetworkSize + ") " + Helper.getMemInfo());
76+
logger.info("Subnetwork removal jobs: " + prepareJobs);
7677
logger.info("Graph nodes: " + Helper.nf(ghStorage.getNodes()));
7778
logger.info("Graph edges: " + Helper.nf(ghStorage.getEdges()));
78-
for (FlagEncoder encoder : encoders) {
79-
logger.info("--- vehicle: '" + encoder.toString() + "'");
80-
removeSmallSubNetworks(encoder.getAccessEnc());
79+
for (PrepareJob job : prepareJobs) {
80+
logger.info("--- vehicle: '" + job.name + "'");
81+
removeSmallSubNetworks(job.accessEnc, job.turnCostProvider);
8182
}
8283
markNodesRemovedIfUnreachable();
8384
optimize();
84-
logger.info("Finished finding and removing subnetworks for " + encoders.size() + " vehicles, took: " + sw.stop().getSeconds() + "s, " + Helper.getMemInfo());
85+
logger.info("Finished finding and removing subnetworks for " + prepareJobs.size() + " vehicles, took: " + sw.stop().getSeconds() + "s, " + Helper.getMemInfo());
8586
}
8687

8788
private void optimize() {
@@ -90,21 +91,21 @@ private void optimize() {
9091
logger.info("Optimized storage after subnetwork removal, took: " + sw.stop().getSeconds() + "s," + Helper.getMemInfo());
9192
}
9293

93-
public int getMaxSubnetworks() {
94-
return subnetworks;
95-
}
96-
9794
/**
98-
* Removes components with less than {@link #minNetworkSize} nodes from the graph by disabling access to the nodes
99-
* of the removed components (for the given access encoded value). It is important to search for strongly connected
100-
* components here (i.e. consider that the graph is directed). For example, small areas like parking lots are
101-
* sometimes connected to the whole network through a single one-way road. This is clearly a (mapping) error - but
102-
* it causes the routing to fail when starting from the parking lot (and there is no way out from it).
103-
* The biggest component is always kept regardless of its size.
95+
* The biggest component is always kept regardless of its size. For edge-based routing with turn restrictions the
96+
* subnetwork search has to consider the turn restrictions as well to make sure components that are not reachable
97+
* due to turn restrictions are also removed.
10498
*
10599
* @return number of removed edges
106100
*/
107-
int removeSmallSubNetworks(BooleanEncodedValue accessEnc) {
101+
int removeSmallSubNetworks(BooleanEncodedValue accessEnc, TurnCostProvider turnCostProvider) {
102+
if (turnCostProvider == null)
103+
return removeSmallSubNetworksNodeBased(accessEnc);
104+
else
105+
return removeSmallSubNetworksEdgeBased(accessEnc, turnCostProvider);
106+
}
107+
108+
private int removeSmallSubNetworksNodeBased(BooleanEncodedValue accessEnc) {
108109
// partition graph into strongly connected components using Tarjan's algorithm
109110
StopWatch sw = new StopWatch().start();
110111
TarjanSCC tarjan = new TarjanSCC(ghStorage, accessEnc, false);
@@ -115,7 +116,7 @@ int removeSmallSubNetworks(BooleanEncodedValue accessEnc) {
115116
logger.info("Found " + ccs.getTotalComponents() + " subnetworks (" + numSingleNodeComponents + " single nodes and "
116117
+ components.size() + " components with more than one node, total nodes: " + ccs.getNodes() + "), took: " + sw.stop().getSeconds() + "s");
117118

118-
// remove all small networks except the biggest (even when its smaller than the given min_network_size)
119+
// remove all small networks, but keep the biggest (even when its smaller than the given min_network_size)
119120
sw = new StopWatch().start();
120121
int removedComponents = 0;
121122
int removedEdges = 0;
@@ -151,17 +152,14 @@ int removeSmallSubNetworks(BooleanEncodedValue accessEnc) {
151152
throw new IllegalStateException("Too many total edges were removed: " + removedEdges + " out of " + ghStorage.getEdges() + "\n" +
152153
"The maximum number of removed edges is: " + allowedRemoved);
153154

154-
subnetworks = ccs.getTotalComponents() - removedComponents;
155155
logger.info("Removed " + removedComponents + " subnetworks (biggest removed: " + biggestRemoved + " nodes) -> " +
156-
subnetworks + " subnetwork(s) left (smallest: " + smallestRemaining + ", biggest: " + ccs.getBiggestComponent().size() + " nodes)"
156+
(ccs.getTotalComponents() - removedComponents) + " subnetwork(s) left (smallest: " + smallestRemaining + ", biggest: " + ccs.getBiggestComponent().size() + " nodes)"
157157
+ ", total removed edges: " + removedEdges + ", took: " + sw.stop().getSeconds() + "s");
158158
return removedEdges;
159159
}
160160

161161
/**
162162
* Makes all edges of the given component (the given set of node ids) inaccessible for the given access encoded value.
163-
* So far we are not removing the edges entirely from the graph (we could probably do this for edges that are blocked
164-
* for *all* vehicles similar to {@link #markNodesRemovedIfUnreachable})
165163
*/
166164
int blockEdgesForComponent(EdgeExplorer explorer, BooleanEncodedValue accessEnc, IntIndexedContainer component) {
167165
int removedEdges = 0;
@@ -183,8 +181,83 @@ private int blockEdgesForNode(EdgeExplorer explorer, BooleanEncodedValue accessE
183181
return removedEdges;
184182
}
185183

184+
private int removeSmallSubNetworksEdgeBased(BooleanEncodedValue accessEnc, TurnCostProvider turnCostProvider) {
185+
// partition graph into strongly connected components using Tarjan's algorithm
186+
StopWatch sw = new StopWatch().start();
187+
EdgeBasedTarjanSCC tarjan = new EdgeBasedTarjanSCC(ghStorage, accessEnc, turnCostProvider, false);
188+
EdgeBasedTarjanSCC.ConnectedComponents ccs = tarjan.findComponents();
189+
List<IntArrayList> components = ccs.getComponents();
190+
BitSet singleEdgeComponents = ccs.getSingleEdgeComponents();
191+
long numSingleEdgeComponents = singleEdgeComponents.cardinality();
192+
logger.info("Found " + ccs.getTotalComponents() + " subnetworks (" + numSingleEdgeComponents + " single edges and "
193+
+ components.size() + " components with more than one edge, total nodes: " + ccs.getEdgeKeys() + "), took: " + sw.stop().getSeconds() + "s");
194+
195+
// n edge-keys roughly equal n/2 edges and components with n/2 edges approximately have n/2 nodes
196+
// we could actually count the nodes to make this more consistent, but is it really needed?
197+
final int minNetworkSizeEdges = 2 * minNetworkSize;
198+
199+
// remove all small networks, but keep the biggest (even when its smaller than the given min_network_size)
200+
sw = new StopWatch().start();
201+
int removedComponents = 0;
202+
int removedEdgeKeys = 0;
203+
int smallestRemaining = ccs.getBiggestComponent().size();
204+
int biggestRemoved = 0;
205+
206+
for (IntArrayList component : components) {
207+
if (component == ccs.getBiggestComponent())
208+
continue;
209+
210+
if (component.size() < minNetworkSizeEdges) {
211+
for (IntCursor cursor : component) {
212+
removedEdgeKeys += removeEdgeWithKey(cursor.value, accessEnc);
213+
}
214+
removedComponents++;
215+
biggestRemoved = Math.max(biggestRemoved, component.size());
216+
} else {
217+
smallestRemaining = Math.min(smallestRemaining, component.size());
218+
}
219+
}
220+
221+
if (minNetworkSizeEdges > 0) {
222+
BitSetIterator iter = singleEdgeComponents.iterator();
223+
for (int edgeKey = iter.nextSetBit(); edgeKey >= 0; edgeKey = iter.nextSetBit()) {
224+
removedEdgeKeys += removeEdgeWithKey(edgeKey, accessEnc);
225+
removedComponents++;
226+
biggestRemoved = Math.max(biggestRemoved, 1);
227+
}
228+
} else if (numSingleEdgeComponents > 0) {
229+
smallestRemaining = Math.min(smallestRemaining, 1);
230+
}
231+
232+
int allowedRemoved = ghStorage.getEdges() / 2;
233+
if (removedEdgeKeys / 2 > allowedRemoved)
234+
throw new IllegalStateException("Too many total (directed) edges were removed: " + removedEdgeKeys + " out of " + (2 * ghStorage.getEdges()) + "\n" +
235+
"The maximum number of removed edges is: " + (2 * allowedRemoved));
236+
237+
logger.info("Removed " + removedComponents + " subnetworks (biggest removed: " + biggestRemoved + " edges) -> " +
238+
(ccs.getTotalComponents() - removedComponents) + " subnetwork(s) left (smallest: " + smallestRemaining + ", biggest: " + ccs.getBiggestComponent().size() + " edges)"
239+
+ ", total removed edges: " + removedEdgeKeys + ", took: " + sw.stop().getSeconds() + "s");
240+
return removedEdgeKeys;
241+
}
242+
243+
private int removeEdgeWithKey(int edgeKey, BooleanEncodedValue accessEnc) {
244+
int edgeId = EdgeBasedTarjanSCC.getEdgeFromKey(edgeKey);
245+
EdgeIteratorState edge = ghStorage.getEdgeIteratorState(edgeId, Integer.MIN_VALUE);
246+
if (edgeKey % 2 == 0 && edge.get(accessEnc)) {
247+
edge.set(accessEnc, false);
248+
return 1;
249+
}
250+
if (edgeKey % 2 != 0 && edge.getReverse(accessEnc)) {
251+
edge.setReverse(accessEnc, false);
252+
return 1;
253+
}
254+
return 0;
255+
}
256+
186257
/**
187-
* Removes nodes if all edges are not accessible. I.e. removes zero degree nodes.
258+
* Removes nodes if all edges are not accessible. I.e. removes zero degree nodes. Note that so far we are not
259+
* removing any edges entirely from the graph (we could probably do this for edges that are blocked for *all*
260+
* vehicles.
188261
*/
189262
void markNodesRemovedIfUnreachable() {
190263
EdgeExplorer edgeExplorer = ghStorage.createEdgeExplorer();
@@ -207,6 +280,10 @@ boolean detectNodeRemovedForAllEncoders(EdgeExplorer edgeExplorerAllEdges, int n
207280
// we could implement a 'fast check' for several previously marked removed nodes via GHBitSet
208281
// removedNodesPerVehicle. The problem is that we would need long-indices but BitSet only supports int (due to nodeIndex*numberOfEncoders)
209282

283+
List<BooleanEncodedValue> accessEncList = new ArrayList<>();
284+
for (PrepareJob job : prepareJobs) {
285+
accessEncList.add(job.accessEnc);
286+
}
210287
// if no edges are reachable return true
211288
EdgeIterator iter = edgeExplorerAllEdges.setBaseNode(nodeIndex);
212289
while (iter.next()) {
@@ -219,4 +296,21 @@ boolean detectNodeRemovedForAllEncoders(EdgeExplorer edgeExplorerAllEdges, int n
219296

220297
return true;
221298
}
299+
300+
public static class PrepareJob {
301+
private final String name;
302+
private final BooleanEncodedValue accessEnc;
303+
private final TurnCostProvider turnCostProvider;
304+
305+
public PrepareJob(String name, BooleanEncodedValue accessEnc, TurnCostProvider turnCostProvider) {
306+
this.name = name;
307+
this.accessEnc = accessEnc;
308+
this.turnCostProvider = turnCostProvider;
309+
}
310+
311+
@Override
312+
public String toString() {
313+
return name + "|" + (turnCostProvider == null ? "node-based" : "edge-based");
314+
}
315+
}
222316
}

0 commit comments

Comments
 (0)