Skip to content

Commit c85c55d

Browse files
mapbox profile specific voice instructions (graphhopper#3165)
* navigation profile specific voiceinstruction placement * fix: hiking should have been walking * fix: try to find a mapbox profile for the graphhopper profile * Revert "fix: try to find a mapbox profile for the graphhopper profile" This reverts commit 3db1c5b. * feat: configure DistanceConfig from Profile hints too * chore: add myself (Karol Olszacki) to the contributors list * fix: create NavigationTransportMode enum and use that * fix: make sure we accept all kinds of values for wider compatibility with OSRM/Mapbox/etc * fix: DistanceConfig enhancement (#6) * fix: fold more cases into DistanceConfig, remove the new enum and allow usage of existing one * chore: update the example with gh-centric terms --------- Co-authored-by: Karol Olszacki <karol.olszacki@pl.bosch.com>
1 parent 2445e81 commit c85c55d

File tree

6 files changed

+266
-21
lines changed

6 files changed

+266
-21
lines changed

CONTRIBUTORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Here is an overview:
4949
* jessLryan, max elevation can now be negative
5050
* joe-akeem, improvements like #2158
5151
* JohannesPelzer, improved GPX information and various other things
52+
* karololszacki, introduce `navigation_transport_mode` option for Profiles to easily set which Voice Guidance distances to use
5253
* karussell, one of the core developers
5354
* khuebner, initial turn costs support
5455
* kodonnell, adding support for CH and other algorithms (#60) and penalizing inner-link U-turns (#88)

config-example.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ graphhopper:
3030

3131
profiles:
3232
- name: car
33+
# navigation_transport_mode: "car"
3334
# turn_costs:
3435
# vehicle_types: [motorcar, motor_vehicle]
3536
# u_turn_costs: 60
@@ -39,15 +40,19 @@ graphhopper:
3940
# You can use the following in-built profiles. After you start GraphHopper it will print which encoded values you'll have to add to graph.encoded_values in this config file.
4041
#
4142
# - name: foot
43+
# navigation_transport_mode: "foot"
4244
# custom_model_files: [foot.json, foot_elevation.json]
4345
#
4446
# - name: bike
47+
# navigation_transport_mode: "bike"
4548
# custom_model_files: [bike.json, bike_elevation.json]
4649
#
4750
# - name: racingbike
51+
# navigation_transport_mode: "bike"
4852
# custom_model_files: [racingbike.json, bike_elevation.json]
4953
#
5054
# - name: mtb
55+
# navigation_transport_mode: "bike"
5156
# custom_model_files: [mtb.json, bike_elevation.json]
5257
#
5358
# # See the bus.json for more details.

navigation/src/main/java/com/graphhopper/navigation/DistanceConfig.java

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
*/
1818
package com.graphhopper.navigation;
1919

20+
import com.graphhopper.config.Profile;
21+
import com.graphhopper.routing.util.TransportationMode;
22+
import com.graphhopper.util.Helper;
2023
import com.graphhopper.util.TranslationMap;
2124

2225
import java.util.ArrayList;
@@ -30,22 +33,65 @@ public class DistanceConfig {
3033
final List<VoiceInstructionConfig> voiceInstructions;
3134
final DistanceUtils.Unit unit;
3235

33-
public DistanceConfig(DistanceUtils.Unit unit, TranslationMap translationMap, Locale locale) {
36+
public DistanceConfig(DistanceUtils.Unit unit, TranslationMap translationMap, Locale locale, Profile profile) {
37+
this(unit, translationMap, locale, profile.getHints().getString("navigation_transport_mode", ""));
38+
}
39+
40+
public DistanceConfig(DistanceUtils.Unit unit, TranslationMap translationMap, Locale locale, TransportationMode mode) {
41+
this(unit, translationMap, locale, mode.name());
42+
}
43+
44+
public DistanceConfig(DistanceUtils.Unit unit, TranslationMap translationMap, Locale locale, String mode) {
3445
this.unit = unit;
35-
if (unit == DistanceUtils.Unit.METRIC) {
36-
voiceInstructions = Arrays.asList(
37-
new InitialVoiceInstructionConfig(FOR_HIGHER_DISTANCE_PLURAL.metric, translationMap, locale, 4250, 250, unit),
38-
new FixedDistanceVoiceInstructionConfig(IN_HIGHER_DISTANCE_PLURAL.metric, translationMap, locale, 2000, 2),
39-
new FixedDistanceVoiceInstructionConfig(IN_HIGHER_DISTANCE_SINGULAR.metric, translationMap, locale, 1000, 1),
40-
new ConditionalDistanceVoiceInstructionConfig(IN_LOWER_DISTANCE_PLURAL.metric, translationMap, locale, new int[]{400, 200}, new int[]{400, 200})
41-
);
42-
} else {
43-
voiceInstructions = Arrays.asList(
44-
new InitialVoiceInstructionConfig(FOR_HIGHER_DISTANCE_PLURAL.metric, translationMap, locale, 4250, 250, unit),
45-
new FixedDistanceVoiceInstructionConfig(IN_HIGHER_DISTANCE_PLURAL.imperial, translationMap, locale, 3220, 2),
46-
new FixedDistanceVoiceInstructionConfig(IN_HIGHER_DISTANCE_SINGULAR.imperial, translationMap, locale, 1610, 1),
47-
new ConditionalDistanceVoiceInstructionConfig(IN_LOWER_DISTANCE_PLURAL.imperial, translationMap, locale, new int[]{400, 200}, new int[]{1300, 600})
48-
);
46+
switch (Helper.toLowerCase(mode)) {
47+
case "biking":
48+
case "cycling":
49+
case "cyclist":
50+
case "mtb":
51+
case "racingbike":
52+
case "bike":
53+
if (unit == DistanceUtils.Unit.METRIC) {
54+
voiceInstructions = Arrays.asList(
55+
new ConditionalDistanceVoiceInstructionConfig(IN_LOWER_DISTANCE_PLURAL.metric, translationMap, locale, new int[]{150},
56+
new int[]{150}));
57+
} else {
58+
voiceInstructions = Arrays.asList(
59+
new ConditionalDistanceVoiceInstructionConfig(IN_LOWER_DISTANCE_PLURAL.imperial, translationMap, locale, new int[]{150},
60+
new int[]{500}));
61+
}
62+
break;
63+
case "walking":
64+
case "walk":
65+
case "hiking":
66+
case "hike":
67+
case "foot":
68+
case "pedestrian":
69+
if (unit == DistanceUtils.Unit.METRIC) {
70+
voiceInstructions = Arrays.asList(
71+
new ConditionalDistanceVoiceInstructionConfig(IN_LOWER_DISTANCE_PLURAL.metric, translationMap, locale, new int[]{50},
72+
new int[]{50}));
73+
} else {
74+
voiceInstructions = Arrays.asList(
75+
new ConditionalDistanceVoiceInstructionConfig(IN_LOWER_DISTANCE_PLURAL.imperial, translationMap, locale, new int[]{50},
76+
new int[]{150}));
77+
}
78+
break;
79+
default:
80+
if (unit == DistanceUtils.Unit.METRIC) {
81+
voiceInstructions = Arrays.asList(
82+
new InitialVoiceInstructionConfig(FOR_HIGHER_DISTANCE_PLURAL.metric, translationMap, locale, 4250, 250, unit),
83+
new FixedDistanceVoiceInstructionConfig(IN_HIGHER_DISTANCE_PLURAL.metric, translationMap, locale, 2000, 2),
84+
new FixedDistanceVoiceInstructionConfig(IN_HIGHER_DISTANCE_SINGULAR.metric, translationMap, locale, 1000, 1),
85+
new ConditionalDistanceVoiceInstructionConfig(IN_LOWER_DISTANCE_PLURAL.metric, translationMap, locale, new int[]{400, 200}, new int[]{400, 200})
86+
);
87+
} else {
88+
voiceInstructions = Arrays.asList(
89+
new InitialVoiceInstructionConfig(FOR_HIGHER_DISTANCE_PLURAL.metric, translationMap, locale, 4250, 250, unit),
90+
new FixedDistanceVoiceInstructionConfig(IN_HIGHER_DISTANCE_PLURAL.imperial, translationMap, locale, 3220, 2),
91+
new FixedDistanceVoiceInstructionConfig(IN_HIGHER_DISTANCE_SINGULAR.imperial, translationMap, locale, 1610, 1),
92+
new ConditionalDistanceVoiceInstructionConfig(IN_LOWER_DISTANCE_PLURAL.imperial, translationMap, locale, new int[]{400, 200}, new int[]{1300, 600})
93+
);
94+
}
4995
}
5096

5197
}

navigation/src/main/java/com/graphhopper/navigation/NavigateResource.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ public Response doGet(
144144
} else {
145145
DistanceUtils.Unit unit = voiceUnits.equals("metric") ? DistanceUtils.Unit.METRIC : DistanceUtils.Unit.IMPERIAL;
146146
Locale locale = Helper.getLocale(localeStr);
147-
DistanceConfig config = new DistanceConfig(unit, translationMap, locale);
147+
DistanceConfig config = new DistanceConfig(unit, translationMap, locale, graphHopper.getProfile(ghProfile));
148148
logger.info(logStr);
149149
return Response.ok(NavigateResponseConverter.convertFromGHResponse(ghResponse, translationMap, locale, config)).
150150
header("X-GH-Took", "" + Math.round(took * 1000)).
@@ -214,7 +214,7 @@ public Response doPost(@NotNull GHRequest request, @Context HttpServletRequest h
214214
unit = DistanceUtils.Unit.IMPERIAL;
215215
}
216216

217-
DistanceConfig config = new DistanceConfig(unit, translationMap, request.getLocale());
217+
DistanceConfig config = new DistanceConfig(unit, translationMap, request.getLocale(), graphHopper.getProfile(request.getProfile()));
218218
logger.info(logStr);
219219
return Response.ok(NavigateResponseConverter.convertFromGHResponse(ghResponse, translationMap, request.getLocale(), config)).
220220
header("X-GH-Took", "" + Math.round(took * 1000)).
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.graphhopper.navigation;
2+
3+
import com.graphhopper.config.Profile;
4+
import com.graphhopper.routing.util.TransportationMode;
5+
import org.junit.jupiter.api.Test;
6+
7+
import static org.junit.jupiter.api.Assertions.assertEquals;
8+
9+
public class DistanceConfigTest {
10+
11+
@Test
12+
public void distanceConfigTest() {
13+
// from TransportationMode
14+
DistanceConfig car = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, TransportationMode.CAR);
15+
assertEquals(4, car.voiceInstructions.size());
16+
DistanceConfig foot = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, TransportationMode.FOOT);
17+
assertEquals(1, foot.voiceInstructions.size());
18+
DistanceConfig bike = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, TransportationMode.BIKE);
19+
assertEquals(1, bike.voiceInstructions.size());
20+
DistanceConfig bus = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, TransportationMode.BUS);
21+
assertEquals(4, bus.voiceInstructions.size());
22+
23+
24+
// from Profile
25+
Profile awesomeProfile = new Profile("my_awesome_profile").putHint("navigation_transport_mode", "car");
26+
DistanceConfig carFromProfile = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, awesomeProfile);
27+
assertEquals(4, carFromProfile.voiceInstructions.size());
28+
29+
Profile fastWalkProfile = new Profile("my_fast_walk_profile").putHint("navigation_transport_mode", "foot");
30+
DistanceConfig footFromProfile = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, fastWalkProfile);
31+
assertEquals(1, footFromProfile.voiceInstructions.size());
32+
33+
Profile crazyMtbProfile = new Profile("my_crazy_mtb").putHint("navigation_transport_mode", "bike");
34+
DistanceConfig bikeFromProfile = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, crazyMtbProfile);
35+
assertEquals(1, bikeFromProfile.voiceInstructions.size());
36+
37+
Profile truckProfile = new Profile("my_truck"); // no hint set, so defaults to car
38+
DistanceConfig truckCfg = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, truckProfile);
39+
assertEquals(4, truckCfg.voiceInstructions.size());
40+
41+
42+
// from String
43+
DistanceConfig driving = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, "driving");
44+
assertEquals(4, driving.voiceInstructions.size());
45+
DistanceConfig anything = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, "anything");
46+
assertEquals(4, anything.voiceInstructions.size());
47+
DistanceConfig none = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, "");
48+
assertEquals(4, none.voiceInstructions.size());
49+
DistanceConfig biking = new DistanceConfig(DistanceUtils.Unit.METRIC, null, null, "biking");
50+
assertEquals(1, biking.voiceInstructions.size());
51+
}
52+
53+
}

navigation/src/test/java/com/graphhopper/navigation/NavigateResponseConverterTest.java

Lines changed: 144 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.graphhopper.GraphHopper;
88
import com.graphhopper.jackson.ResponsePathSerializer;
99
import com.graphhopper.routing.TestProfiles;
10+
import com.graphhopper.routing.util.TransportationMode;
1011
import com.graphhopper.util.Helper;
1112
import com.graphhopper.util.Parameters;
1213
import com.graphhopper.util.PointList;
@@ -29,9 +30,8 @@ public class NavigateResponseConverterTest {
2930
private static final String osmFile = "../core/files/andorra.osm.gz";
3031
private static GraphHopper hopper;
3132
private static final String profile = "my_car";
32-
3333
private final TranslationMap trMap = hopper.getTranslationMap();
34-
private final DistanceConfig distanceConfig = new DistanceConfig(DistanceUtils.Unit.METRIC, trMap, Locale.ENGLISH);
34+
private final DistanceConfig distanceConfig = new DistanceConfig(DistanceUtils.Unit.METRIC, trMap, Locale.ENGLISH, TransportationMode.CAR);
3535

3636
@BeforeAll
3737
public static void beforeClass() {
@@ -183,7 +183,7 @@ public void voiceInstructionsImperialTest() {
183183
GHResponse rsp = hopper.route(new GHRequest(42.554851, 1.536198, 42.510071, 1.548128).setProfile(profile));
184184

185185
ObjectNode json = NavigateResponseConverter.convertFromGHResponse(rsp, trMap, Locale.ENGLISH,
186-
new DistanceConfig(DistanceUtils.Unit.IMPERIAL, trMap, Locale.ENGLISH));
186+
new DistanceConfig(DistanceUtils.Unit.IMPERIAL, trMap, Locale.ENGLISH, TransportationMode.CAR));
187187

188188
JsonNode steps = json.get("routes").get(0).get("legs").get(0).get("steps");
189189

@@ -212,6 +212,146 @@ public void voiceInstructionsImperialTest() {
212212
assertEquals("keep right", voiceInstruction.get("announcement").asText());
213213
}
214214

215+
@Test
216+
public void voiceInstructionsWalkingMetricTest() {
217+
218+
GHResponse rsp = hopper.route(new GHRequest(42.554851, 1.536198, 42.510071, 1.548128).setProfile(profile));
219+
220+
ObjectNode json = NavigateResponseConverter.convertFromGHResponse(rsp, trMap, Locale.ENGLISH,
221+
new DistanceConfig(DistanceUtils.Unit.METRIC, trMap, Locale.ENGLISH, TransportationMode.FOOT));
222+
223+
JsonNode steps = json.get("routes").get(0).get("legs").get(0).get("steps");
224+
225+
// Step 4 is about 240m long
226+
JsonNode step = steps.get(4);
227+
JsonNode maneuver = step.get("maneuver");
228+
229+
JsonNode voiceInstructions = step.get("voiceInstructions");
230+
assertEquals(2, voiceInstructions.size());
231+
JsonNode voiceInstruction = voiceInstructions.get(0);
232+
assertEquals(50, voiceInstruction.get("distanceAlongGeometry").asDouble(), 1);
233+
assertEquals("In 50 meters At roundabout, take exit 2 onto CS-340, then At roundabout, take exit 2 onto CG-3",
234+
voiceInstruction.get("announcement").asText());
235+
236+
// Step 14 is over 3km long
237+
step = steps.get(14);
238+
maneuver = step.get("maneuver");
239+
240+
voiceInstructions = step.get("voiceInstructions");
241+
assertEquals(2, voiceInstructions.size());
242+
voiceInstruction = voiceInstructions.get(0);
243+
assertEquals(50, voiceInstruction.get("distanceAlongGeometry").asDouble(), 1);
244+
assertEquals("In 50 meters keep right", voiceInstruction.get("announcement").asText());
245+
246+
voiceInstruction = voiceInstructions.get(1);
247+
assertEquals("keep right", voiceInstruction.get("announcement").asText());
248+
}
249+
250+
@Test
251+
public void voiceInstructionsWalkingImperialTest() {
252+
253+
GHResponse rsp = hopper.route(new GHRequest(42.554851, 1.536198, 42.510071, 1.548128).setProfile(profile));
254+
255+
ObjectNode json = NavigateResponseConverter.convertFromGHResponse(rsp, trMap, Locale.ENGLISH,
256+
new DistanceConfig(DistanceUtils.Unit.IMPERIAL, trMap, Locale.ENGLISH, TransportationMode.FOOT));
257+
258+
JsonNode steps = json.get("routes").get(0).get("legs").get(0).get("steps");
259+
260+
// Step 4 is about 240m long
261+
JsonNode step = steps.get(4);
262+
JsonNode maneuver = step.get("maneuver");
263+
264+
JsonNode voiceInstructions = step.get("voiceInstructions");
265+
assertEquals(2, voiceInstructions.size());
266+
JsonNode voiceInstruction = voiceInstructions.get(0);
267+
assertEquals(50, voiceInstruction.get("distanceAlongGeometry").asDouble(), 1);
268+
assertEquals("In 150 feet At roundabout, take exit 2 onto CS-340, then At roundabout, take exit 2 onto CG-3",
269+
voiceInstruction.get("announcement").asText());
270+
271+
// Step 14 is over 3km long
272+
step = steps.get(14);
273+
maneuver = step.get("maneuver");
274+
275+
voiceInstructions = step.get("voiceInstructions");
276+
assertEquals(2, voiceInstructions.size());
277+
voiceInstruction = voiceInstructions.get(0);
278+
assertEquals(50, voiceInstruction.get("distanceAlongGeometry").asDouble(), 1);
279+
assertEquals("In 150 feet keep right", voiceInstruction.get("announcement").asText());
280+
281+
voiceInstruction = voiceInstructions.get(1);
282+
assertEquals("keep right", voiceInstruction.get("announcement").asText());
283+
}
284+
285+
@Test
286+
public void voiceInstructionsCyclingMetricTest() {
287+
288+
GHResponse rsp = hopper.route(new GHRequest(42.554851, 1.536198, 42.510071, 1.548128).setProfile(profile));
289+
290+
ObjectNode json = NavigateResponseConverter.convertFromGHResponse(rsp, trMap, Locale.ENGLISH,
291+
new DistanceConfig(DistanceUtils.Unit.METRIC, trMap, Locale.ENGLISH, TransportationMode.BIKE));
292+
293+
JsonNode steps = json.get("routes").get(0).get("legs").get(0).get("steps");
294+
295+
// Step 4 is about 240m long
296+
JsonNode step = steps.get(4);
297+
JsonNode maneuver = step.get("maneuver");
298+
299+
JsonNode voiceInstructions = step.get("voiceInstructions");
300+
assertEquals(2, voiceInstructions.size());
301+
JsonNode voiceInstruction = voiceInstructions.get(0);
302+
assertEquals(150, voiceInstruction.get("distanceAlongGeometry").asDouble(), 1);
303+
assertEquals("In 150 meters At roundabout, take exit 2 onto CS-340, then At roundabout, take exit 2 onto CG-3",
304+
voiceInstruction.get("announcement").asText());
305+
306+
// Step 14 is over 3km long
307+
step = steps.get(14);
308+
maneuver = step.get("maneuver");
309+
310+
voiceInstructions = step.get("voiceInstructions");
311+
assertEquals(2, voiceInstructions.size());
312+
voiceInstruction = voiceInstructions.get(0);
313+
assertEquals(150, voiceInstruction.get("distanceAlongGeometry").asDouble(), 1);
314+
assertEquals("In 150 meters keep right", voiceInstruction.get("announcement").asText());
315+
316+
voiceInstruction = voiceInstructions.get(1);
317+
assertEquals("keep right", voiceInstruction.get("announcement").asText());
318+
}
319+
320+
@Test
321+
public void voiceInstructionsCyclingImperialTest() {
322+
323+
GHResponse rsp = hopper.route(new GHRequest(42.554851, 1.536198, 42.510071, 1.548128).setProfile(profile));
324+
325+
ObjectNode json = NavigateResponseConverter.convertFromGHResponse(rsp, trMap, Locale.ENGLISH,
326+
new DistanceConfig(DistanceUtils.Unit.IMPERIAL, trMap, Locale.ENGLISH, TransportationMode.BIKE));
327+
328+
JsonNode steps = json.get("routes").get(0).get("legs").get(0).get("steps");
329+
330+
// Step 4 is about 240m long
331+
JsonNode step = steps.get(4);
332+
JsonNode maneuver = step.get("maneuver");
333+
334+
JsonNode voiceInstructions = step.get("voiceInstructions");
335+
assertEquals(2, voiceInstructions.size());
336+
JsonNode voiceInstruction = voiceInstructions.get(0);
337+
assertEquals(150, voiceInstruction.get("distanceAlongGeometry").asDouble(), 1);
338+
assertEquals("In 500 feet At roundabout, take exit 2 onto CS-340, then At roundabout, take exit 2 onto CG-3",
339+
voiceInstruction.get("announcement").asText());
340+
341+
// Step 14 is over 3km long
342+
step = steps.get(14);
343+
maneuver = step.get("maneuver");
344+
345+
voiceInstructions = step.get("voiceInstructions");
346+
assertEquals(2, voiceInstructions.size());
347+
voiceInstruction = voiceInstructions.get(0);
348+
assertEquals(150, voiceInstruction.get("distanceAlongGeometry").asDouble(), 1);
349+
assertEquals("In 500 feet keep right", voiceInstruction.get("announcement").asText());
350+
351+
voiceInstruction = voiceInstructions.get(1);
352+
assertEquals("keep right", voiceInstruction.get("announcement").asText());
353+
}
354+
215355
@Test
216356
@Disabled
217357
public void alternativeRoutesTest() {
@@ -244,7 +384,7 @@ public void voiceInstructionTranslationTest() {
244384
rsp = hopper.route(
245385
new GHRequest(42.554851, 1.536198, 42.510071, 1.548128).setProfile(profile).setLocale(Locale.GERMAN));
246386

247-
DistanceConfig distanceConfigGerman = new DistanceConfig(DistanceUtils.Unit.METRIC, trMap, Locale.GERMAN);
387+
DistanceConfig distanceConfigGerman = new DistanceConfig(DistanceUtils.Unit.METRIC, trMap, Locale.GERMAN, TransportationMode.CAR);
248388

249389
json = NavigateResponseConverter.convertFromGHResponse(rsp, trMap, Locale.GERMAN, distanceConfigGerman);
250390

0 commit comments

Comments
 (0)