Skip to content

Commit 7bee0e4

Browse files
authored
Add config option preparation_profile to allow cross-querying in hybrid mode (graphhopper#1983)
1 parent 3f520c0 commit 7bee0e4

File tree

9 files changed

+169
-22
lines changed

9 files changed

+169
-22
lines changed

api/src/main/java/com/graphhopper/config/LMProfileConfig.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
*/
2929
public class LMProfileConfig {
3030
private String profile = "";
31+
private String preparationProfile = "this";
3132
private double maximumLMWeight = -1;
3233

3334
private LMProfileConfig() {
@@ -47,17 +48,35 @@ void setProfile(String profile) {
4748
this.profile = profile;
4849
}
4950

51+
public boolean usesOtherPreparation() {
52+
return !preparationProfile.equals("this");
53+
}
54+
55+
public String getPreparationProfile() {
56+
return preparationProfile;
57+
}
58+
59+
public LMProfileConfig setPreparationProfile(String preparationProfile) {
60+
validateProfileName(preparationProfile);
61+
if (maximumLMWeight >= 0)
62+
throw new IllegalArgumentException("Using non-default maximum_lm_weight and preparation_profile at the same time is not allowed");
63+
this.preparationProfile = preparationProfile;
64+
return this;
65+
}
66+
5067
public double getMaximumLMWeight() {
5168
return maximumLMWeight;
5269
}
5370

5471
public LMProfileConfig setMaximumLMWeight(double maximumLMWeight) {
72+
if (usesOtherPreparation())
73+
throw new IllegalArgumentException("Using non-default maximum_lm_weight and preparation_profile at the same time is not allowed");
5574
this.maximumLMWeight = maximumLMWeight;
5675
return this;
5776
}
5877

5978
@Override
6079
public String toString() {
61-
return profile + "|maximum_lm_weight=" + maximumLMWeight;
80+
return profile + "|preparation_profile=" + preparationProfile + "|maximum_lm_weight=" + maximumLMWeight;
6281
}
6382
}

config-example.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ graphhopper:
6363
# Hybrid mode:
6464
# Similar to speed mode, the hybrid mode (Landmarks, LM) also speeds up routing by doing calculating auxiliary data
6565
# in advance. Its not as fast as speed mode, but more flexible.
66+
#
67+
# Advanced usage: It is possible to use the same preparation for multiple profiles which saves memory and preparation
68+
# time. To do this use e.g. `preparation_profile: my_other_profile` where `my_other_profile` is the name of another
69+
# profile for which an LM profile exists. Important: This only will give correct routing results if the weights
70+
# calculated for the profile are equal or larger (for every edge) than those calculated for the profile that was used
71+
# for the preparation (`my_other_profile`)
6672
profiles_lm: []
6773

6874
##### Elevation #####

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

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -837,15 +837,26 @@ private void checkProfilesConsistency() {
837837
throw new IllegalArgumentException("CH profile references unknown profile '" + chConfig.getProfile() + "'");
838838
}
839839
}
840-
Set<String> lmProfileSet = new LinkedHashSet<>(lmPreparationHandler.getLMProfileConfigs().size());
840+
Map<String, LMProfileConfig> lmProfileMap = new LinkedHashMap<>(lmPreparationHandler.getLMProfileConfigs().size());
841841
for (LMProfileConfig lmConfig : lmPreparationHandler.getLMProfileConfigs()) {
842-
boolean added = lmProfileSet.add(lmConfig.getProfile());
843-
if (!added) {
844-
throw new IllegalArgumentException("Duplicate LM reference to profile '" + lmConfig.getProfile() + "'");
842+
LMProfileConfig previous = lmProfileMap.put(lmConfig.getProfile(), lmConfig);
843+
if (previous != null) {
844+
throw new IllegalArgumentException("Multiple LM profiles are using the same profile '" + lmConfig.getProfile() + "'");
845845
}
846846
if (!profilesByName.containsKey(lmConfig.getProfile())) {
847847
throw new IllegalArgumentException("LM profile references unknown profile '" + lmConfig.getProfile() + "'");
848848
}
849+
if (lmConfig.usesOtherPreparation() && !profilesByName.containsKey(lmConfig.getPreparationProfile())) {
850+
throw new IllegalArgumentException("LM profile references unknown preparation profile '" + lmConfig.getPreparationProfile() + "'");
851+
}
852+
}
853+
for (LMProfileConfig lmConfig : lmPreparationHandler.getLMProfileConfigs()) {
854+
if (lmConfig.usesOtherPreparation() && !lmProfileMap.containsKey(lmConfig.getPreparationProfile())) {
855+
throw new IllegalArgumentException("Unknown LM preparation profile '" + lmConfig.getPreparationProfile() + "' in LM profile '" + lmConfig.getProfile() + "' cannot be used as preparation_profile");
856+
}
857+
if (lmConfig.usesOtherPreparation() && lmProfileMap.get(lmConfig.getPreparationProfile()).usesOtherPreparation()) {
858+
throw new IllegalArgumentException("Cannot use '" + lmConfig.getPreparationProfile() + "' as preparation_profile for LM profile '" + lmConfig.getProfile() + "', because it uses another profile for preparation itself.");
859+
}
849860
}
850861
}
851862

@@ -861,7 +872,15 @@ public RoutingAlgorithmFactory getAlgorithmFactory(String profile, boolean disab
861872
if (chPreparationHandler.isEnabled() && !disableCH) {
862873
return chPreparationHandler.getAlgorithmFactory(profile);
863874
} else if (lmPreparationHandler.isEnabled() && !disableLM) {
864-
return lmPreparationHandler.getAlgorithmFactory(profile);
875+
for (LMProfileConfig lmp : lmPreparationHandler.getLMProfileConfigs()) {
876+
if (lmp.getProfile().equals(profile)) {
877+
return lmp.usesOtherPreparation()
878+
// cross-querying
879+
? lmPreparationHandler.getAlgorithmFactory(lmp.getPreparationProfile())
880+
: lmPreparationHandler.getAlgorithmFactory(lmp.getProfile());
881+
}
882+
}
883+
throw new IllegalArgumentException("Cannot find LM preparation for the requested profile: '" + profile + "'");
865884
} else {
866885
return new RoutingAlgorithmFactorySimple();
867886
}
@@ -895,11 +914,13 @@ private void initLMPreparationHandler() {
895914
return;
896915

897916
for (LMProfileConfig lmConfig : lmPreparationHandler.getLMProfileConfigs()) {
917+
if (lmConfig.usesOtherPreparation())
918+
continue;
898919
ProfileConfig profile = profilesByName.get(lmConfig.getProfile());
899920
// Note that we have to make sure the weighting used for LM preparation does not include turn costs, because
900921
// the LM preparation is running node-based and the landmark weights will be wrong if there are non-zero
901922
// turn costs, see discussion in #1960
902-
// Running the preparation without turn costs can also be useful to allow e.g. changing the u_turn_costs per
923+
// Running the preparation without turn costs is also useful to allow e.g. changing the u_turn_costs per
903924
// request (we have to use the minimum weight settings (= no turn costs) for the preparation)
904925
Weighting weighting = createWeighting(profile, new PMap(), true);
905926
lmPreparationHandler.addLMProfile(new LMProfile(profile.getName(), weighting));
@@ -1015,14 +1036,12 @@ public List<Path> calcPaths(GHRequest request, GHResponse ghRsp) {
10151036
try {
10161037
if (!request.getVehicle().isEmpty())
10171038
throw new IllegalArgumentException("GHRequest may no longer contain a vehicle, use the profile parameter instead, see #1958");
1018-
// todo: #1980, weighting should be also forbidden
1019-
// if (!request.getWeighting().isEmpty())
1020-
// throw new IllegalArgumentException("GHRequest may no longer contain a weighting, use the profile parameter instead, see #1958");
1039+
if (!request.getWeighting().isEmpty())
1040+
throw new IllegalArgumentException("GHRequest may no longer contain a weighting, use the profile parameter instead, see #1958");
10211041
if (request.getHints().has(Routing.TURN_COSTS))
10221042
throw new IllegalArgumentException("GHRequest may no longer contain the turn_costs=true/false parameter, use the profile parameter instead, see #1958");
1023-
// todo: #1980, edge based should also be forbidden
1024-
// if (request.getHints().has(Routing.EDGE_BASED))
1025-
// throw new IllegalArgumentException("GHRequest may no longer contain the edge_based=true/false parameter, use the profile parameter instead, see #1958");
1043+
if (request.getHints().has(Routing.EDGE_BASED))
1044+
throw new IllegalArgumentException("GHRequest may no longer contain the edge_based=true/false parameter, use the profile parameter instead, see #1958");
10261045

10271046
// todo later: do not allow things like short_fastest.distance_factor or u_turn_costs unless CH is disabled and only under certain conditions for LM
10281047

core/src/main/java/com/graphhopper/routing/lm/LMPreparationHandler.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ public LMPreparationHandler setLMProfileConfigs(Collection<LMProfileConfig> lmPr
130130
this.lmProfileConfigs.clear();
131131
this.maximumWeights.clear();
132132
for (LMProfileConfig config : lmProfileConfigs) {
133+
if (config.usesOtherPreparation())
134+
continue;
133135
maximumWeights.put(config.getProfile(), config.getMaximumLMWeight());
134136
}
135137
this.lmProfileConfigs.addAll(lmProfileConfigs);

core/src/test/java/com/graphhopper/GraphHopperProfileConfigTest.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,63 @@ public void duplicateLMProfile_error() {
160160
public void run() {
161161
hopper.load(GH_LOCATION);
162162
}
163-
}, "Duplicate LM reference to profile 'profile'");
163+
}, "Multiple LM profiles are using the same profile 'profile'");
164164
}
165165

166+
@Test
167+
public void unknownLMPreparationProfile_error() {
168+
final GraphHopper hopper = createHopper(EncodingManager.create("car"));
169+
hopper.setProfiles(new ProfileConfig("profile").setVehicle("car"));
170+
hopper.getLMPreparationHandler().setLMProfileConfigs(
171+
new LMProfileConfig("profile").setPreparationProfile("xyz")
172+
);
173+
assertIllegalArgument(new Runnable() {
174+
@Override
175+
public void run() {
176+
hopper.load(GH_LOCATION);
177+
}
178+
}, "LM profile references unknown preparation profile 'xyz'");
179+
}
180+
181+
@Test
182+
public void lmPreparationProfileChain_error() {
183+
final GraphHopper hopper = createHopper(EncodingManager.create("car,bike,foot"));
184+
hopper.setProfiles(
185+
new ProfileConfig("profile1").setVehicle("car"),
186+
new ProfileConfig("profile2").setVehicle("bike"),
187+
new ProfileConfig("profile3").setVehicle("foot")
188+
);
189+
hopper.getLMPreparationHandler().setLMProfileConfigs(
190+
new LMProfileConfig("profile1"),
191+
new LMProfileConfig("profile2").setPreparationProfile("profile1"),
192+
new LMProfileConfig("profile3").setPreparationProfile("profile2")
193+
);
194+
assertIllegalArgument(new Runnable() {
195+
@Override
196+
public void run() {
197+
hopper.load(GH_LOCATION);
198+
}
199+
}, "Cannot use 'profile2' as preparation_profile for LM profile 'profile3', because it uses another profile for preparation itself.");
200+
}
201+
202+
@Test
203+
public void noLMProfileForPreparationProfile_error() {
204+
final GraphHopper hopper = createHopper(EncodingManager.create("car,bike,foot"));
205+
hopper.setProfiles(
206+
new ProfileConfig("profile1").setVehicle("car"),
207+
new ProfileConfig("profile2").setVehicle("bike"),
208+
new ProfileConfig("profile3").setVehicle("foot")
209+
);
210+
hopper.getLMPreparationHandler().setLMProfileConfigs(
211+
new LMProfileConfig("profile1").setPreparationProfile("profile2")
212+
);
213+
assertIllegalArgument(new Runnable() {
214+
@Override
215+
public void run() {
216+
hopper.load(GH_LOCATION);
217+
}
218+
}, "Unknown LM preparation profile 'profile2' in LM profile 'profile1' cannot be used as preparation_profile");
219+
}
166220

167221
private GraphHopper createHopper(EncodingManager encodingManager) {
168222
final GraphHopper hopper = new GraphHopper();

reader-osm/src/test/java/com/graphhopper/GraphHopperIT.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,6 +1356,54 @@ public void testFlexMode_631() {
13561356
// note: combining hybrid & speed mode is currently not possible and should be avoided: #1082
13571357
}
13581358

1359+
@Test
1360+
public void testCrossQuery() {
1361+
final String profile1 = "p1";
1362+
final String profile2 = "p2";
1363+
final String profile3 = "p3";
1364+
final String vehicle = "car";
1365+
GraphHopper hopper = createGraphHopper(vehicle).
1366+
setOSMFile(MONACO).
1367+
setProfiles(
1368+
new ProfileConfig(profile1).setVehicle("car").setWeighting("short_fastest").putHint("short_fastest.distance_factor", 0.07),
1369+
new ProfileConfig(profile2).setVehicle("car").setWeighting("short_fastest").putHint("short_fastest.distance_factor", 0.10),
1370+
new ProfileConfig(profile3).setVehicle("car").setWeighting("short_fastest").putHint("short_fastest.distance_factor", 0.15)
1371+
).
1372+
setStoreOnFlush(true);
1373+
1374+
hopper.getLMPreparationHandler().
1375+
setLMProfileConfigs(
1376+
// we have an LM setup for each profile, but only one LM preparation that we use for all of them!
1377+
// this works because profile1's weight is the lowest for every edge
1378+
new LMProfileConfig(profile1),
1379+
new LMProfileConfig(profile2).setPreparationProfile(profile1),
1380+
new LMProfileConfig(profile3).setPreparationProfile(profile1)
1381+
).
1382+
setDisablingAllowed(true);
1383+
hopper.importOrLoad();
1384+
1385+
// flex
1386+
testCrossQueryAssert(profile1, hopper, 528.3, 152, true);
1387+
testCrossQueryAssert(profile2, hopper, 636.0, 150, true);
1388+
testCrossQueryAssert(profile3, hopper, 815.4, 146, true);
1389+
1390+
// LM (should be the same as flex, but with less visited nodes!)
1391+
testCrossQueryAssert(profile1, hopper, 528.3, 106, false);
1392+
testCrossQueryAssert(profile2, hopper, 636.0, 78, false);
1393+
// this is actually interesting: the number of visited nodes *increases* once again (while it strictly decreases
1394+
// with rising distance factor for flex): cross-querying 'works', but performs *worse*, because the landmarks
1395+
// were not customized for the weighting in use. Creating a separate LM preparation for profile3 yields 74
1396+
// instead of 124 visited nodes (not shown here)
1397+
testCrossQueryAssert(profile3, hopper, 815.4, 124, false);
1398+
}
1399+
1400+
private void testCrossQueryAssert(String profile, GraphHopper hopper, double expectedWeight, int expectedVisitedNodes, boolean disableLM) {
1401+
GHResponse response = hopper.route(new GHRequest(43.727687, 7.418737, 43.74958, 7.436566).setProfile(profile).putHint("lm.disable", disableLM));
1402+
assertEquals(expectedWeight, response.getBest().getRouteWeight(), 0.1);
1403+
int visitedNodes = response.getHints().getInt("visited_nodes.sum", 0);
1404+
assertEquals(expectedVisitedNodes, visitedNodes);
1405+
}
1406+
13591407
@Test
13601408
public void testCreateWeightingHintsMerging() {
13611409
final String profile = "profile";

tools/src/main/java/com/graphhopper/tools/Measurement.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,11 +304,10 @@ private GraphHopperConfig createConfigFromArgs(PMap args) {
304304
ghConfig.setCHProfiles(chProfiles);
305305
List<LMProfileConfig> lmProfiles = new ArrayList<>();
306306
if (useLM) {
307-
// as currently we do not allow cross-querying LM with turn costs=true/false we have to add both
308-
// profiles and this currently leads to two identical LM preparations
309307
lmProfiles.add(new LMProfileConfig("profile_no_tc"));
310308
if (turnCosts)
311-
lmProfiles.add(new LMProfileConfig("profile_tc"));
309+
// no need for a second LM preparation, we can do cross queries here
310+
lmProfiles.add(new LMProfileConfig("profile_tc").setPreparationProfile("profile_no_tc"));
312311
}
313312
ghConfig.setLMProfiles(lmProfiles);
314313
return ghConfig;

web-bundle/src/main/java/com/graphhopper/resources/RouteResource.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,10 +245,9 @@ private void enableEdgeBasedIfThereAreCurbsides(List<String> curbsides, GHReques
245245

246246
private void removeLegacyParameters(GHRequest request) {
247247
// these parameters should only be used to resolve the profile, but should not be passed to GraphHopper
248-
// todo: #1980, these parameters should be removed as well?!
249-
// request.getHints().setWeighting("");
248+
request.getHints().setWeighting("");
250249
request.getHints().setVehicle("");
251-
// request.getHints().remove("edge_based");
250+
request.getHints().remove("edge_based");
252251
request.getHints().remove("turn_costs");
253252
}
254253

web/src/test/java/com/graphhopper/http/resources/RouteResourceTurnCostsTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,9 @@ private static GraphHopperServerTestConfiguration createConfig() {
6868
new CHProfileConfig("my_car_no_turn_costs")
6969
))
7070
.setLMProfiles(Arrays.asList(
71-
new LMProfileConfig("my_car_turn_costs"),
72-
new LMProfileConfig("my_car_no_turn_costs")
71+
new LMProfileConfig("my_car_no_turn_costs"),
72+
// no need for a second LM preparation: we can just cross query here
73+
new LMProfileConfig("my_car_turn_costs").setPreparationProfile("my_car_no_turn_costs")
7374
));
7475
return config;
7576
}

0 commit comments

Comments
 (0)