diff --git a/mapbox/src/main/java/com/graphhopper/navigation/mapbox/MapboxResponseConverter.java b/mapbox/src/main/java/com/graphhopper/navigation/mapbox/MapboxResponseConverter.java index ff2c851..c392dac 100644 --- a/mapbox/src/main/java/com/graphhopper/navigation/mapbox/MapboxResponseConverter.java +++ b/mapbox/src/main/java/com/graphhopper/navigation/mapbox/MapboxResponseConverter.java @@ -31,6 +31,8 @@ public class MapboxResponseConverter { + private static final int VOICE_INSTRUCTION_MERGE_TRESHHOLD = 100; + /** * Converts a GHResponse into Mapbox compatible json */ @@ -156,7 +158,7 @@ private static ObjectNode putInstruction(InstructionList instructions, int index // Voice and banner instructions are empty for the last element if (index + 1 < instructions.size()) { putVoiceInstructions(instructions, distance, index, locale, translationMap, mapboxResponseConverterTranslationMap, voiceInstructions); - putBannerInstruction(instructions, distance, index, locale, translationMap, bannerInstructions); + putBannerInstructions(instructions, distance, index, locale, translationMap, bannerInstructions); } return instructionJson; @@ -190,6 +192,8 @@ private static void putVoiceInstructions(InstructionList instructions, double di double close = 400; double veryClose = 200; + String thenVoiceInstruction = getThenVoiceInstructionpart(instructions, index, locale, translationMap, mapboxResponseConverterTranslationMap); + if (distance > far) { putSingleVoiceInstruction(far, mapboxResponseConverterTranslationMap.getWithFallBack(locale).tr("in_km", 2) + " " + turnDescription, voiceInstructions); } @@ -197,10 +201,10 @@ private static void putVoiceInstructions(InstructionList instructions, double di putSingleVoiceInstruction(mid, mapboxResponseConverterTranslationMap.getWithFallBack(locale).tr("in_km_singular") + " " + turnDescription, voiceInstructions); } if (distance > close) { - putSingleVoiceInstruction(close, mapboxResponseConverterTranslationMap.getWithFallBack(locale).tr("in_m", 400) + " " + turnDescription, voiceInstructions); + putSingleVoiceInstruction(close, mapboxResponseConverterTranslationMap.getWithFallBack(locale).tr("in_m", 400) + " " + turnDescription + thenVoiceInstruction, voiceInstructions); } else if (distance > veryClose) { // This is an edge case when turning on narrow roads in cities, too close for the close turn, but too far for the direct turn - putSingleVoiceInstruction(veryClose, mapboxResponseConverterTranslationMap.getWithFallBack(locale).tr("in_m", 200) + " " + turnDescription, voiceInstructions) + putSingleVoiceInstruction(veryClose, mapboxResponseConverterTranslationMap.getWithFallBack(locale).tr("in_m", 200) + " " + turnDescription + thenVoiceInstruction, voiceInstructions) ; } @@ -212,7 +216,7 @@ private static void putVoiceInstructions(InstructionList instructions, double di if (index + 2 == instructions.size()) distanceAlongGeometry = Helper.round(Math.min(distance, 25), 1); - putSingleVoiceInstruction(distanceAlongGeometry, turnDescription, voiceInstructions); + putSingleVoiceInstruction(distanceAlongGeometry, turnDescription + thenVoiceInstruction, voiceInstructions); } private static void putSingleVoiceInstruction(double distanceAlongGeometry, String turnDescription, ArrayNode voiceInstructions) { @@ -223,7 +227,32 @@ private static void putSingleVoiceInstruction(double distanceAlongGeometry, Stri voiceInstruction.put("ssmlAnnouncement", "" + turnDescription + ""); } - private static void putBannerInstruction(InstructionList instructions, double distance, int index, Locale locale, TranslationMap translationMap, ArrayNode bannerInstructions) { + /** + * For close turns, it is important to announce the next turn in the earlier instruction. + * e.g.: instruction i+1= turn right, instruction i+2=turn left, with instruction i+1 distance < VOICE_INSTRUCTION_MERGE_TRESHHOLD + * The voice instruction should be like "turn right, then turn left" + * + * For instruction i+1 distance > VOICE_INSTRUCTION_MERGE_TRESHHOLD an empty String will be returned + */ + private static String getThenVoiceInstructionpart(InstructionList instructions, int index, Locale locale, TranslationMap translationMap, TranslationMap mapboxResponseConverterTranslationMap) { + if (instructions.size() > index + 2) { + Instruction firstInstruction = instructions.get(index + 1); + if (firstInstruction.getDistance() < VOICE_INSTRUCTION_MERGE_TRESHHOLD) { + Instruction secondInstruction = instructions.get(index + 2); + if (secondInstruction.getSign() != Instruction.REACHED_VIA) + return ", " + mapboxResponseConverterTranslationMap.getWithFallBack(locale).tr("then") + " " + secondInstruction.getTurnDescription(translationMap.getWithFallBack(locale)); + } + } + + return ""; + } + + /** + * Banner instructions are the turn instructions that are shown to the user in the top bar. + * + * Between two instructions we can show multiple banner instructions, you can control when they pop up using distanceAlongGeometry. + */ + private static void putBannerInstructions(InstructionList instructions, double distance, int index, Locale locale, TranslationMap translationMap, ArrayNode bannerInstructions) { /* A BannerInstruction looks like this distanceAlongGeometry: 107, @@ -242,47 +271,56 @@ private static void putBannerInstruction(InstructionList instructions, double di */ ObjectNode bannerInstruction = bannerInstructions.addObject(); - Instruction nextInstruction = instructions.get(index + 1); //Show from the beginning bannerInstruction.put("distanceAlongGeometry", distance); ObjectNode primary = bannerInstruction.putObject("primary"); - String bannerInstructionName = nextInstruction.getName(); + putSingleBannerInstruction(instructions.get(index + 1), locale, translationMap, primary); + + bannerInstruction.putNull("secondary"); + + if (instructions.size() > index + 2 && instructions.get(index + 2).getSign() != Instruction.REACHED_VIA) { + // Sub shows the instruction after the current one + ObjectNode sub = bannerInstruction.putObject("sub"); + putSingleBannerInstruction(instructions.get(index + 2), locale, translationMap, sub); + } + } + + private static void putSingleBannerInstruction(Instruction instruction, Locale locale, TranslationMap translationMap, ObjectNode singleBannerInstruction) { + String bannerInstructionName = instruction.getName(); if (bannerInstructionName == null || bannerInstructionName.isEmpty()) { // Fix for final instruction and for instructions without name - bannerInstructionName = nextInstruction.getTurnDescription(translationMap.getWithFallBack(locale)); + bannerInstructionName = instruction.getTurnDescription(translationMap.getWithFallBack(locale)); // Uppercase first letter // TODO: should we do this for all cases? Then we might change the spelling of street names though bannerInstructionName = Helper.firstBig(bannerInstructionName); } - primary.put("text", bannerInstructionName); + singleBannerInstruction.put("text", bannerInstructionName); - ArrayNode components = primary.putArray("components"); + ArrayNode components = singleBannerInstruction.putArray("components"); ObjectNode component = components.addObject(); component.put("text", bannerInstructionName); component.put("type", "text"); - primary.put("type", getTurnType(nextInstruction, false)); - String modifier = getModifier(nextInstruction); + singleBannerInstruction.put("type", getTurnType(instruction, false)); + String modifier = getModifier(instruction); if (modifier != null) - primary.put("modifier", modifier); + singleBannerInstruction.put("modifier", modifier); - if (nextInstruction.getSign() == Instruction.USE_ROUNDABOUT) { - if (nextInstruction instanceof RoundaboutInstruction) { - double turnAngle = ((RoundaboutInstruction) nextInstruction).getTurnAngle(); + if (instruction.getSign() == Instruction.USE_ROUNDABOUT) { + if (instruction instanceof RoundaboutInstruction) { + double turnAngle = ((RoundaboutInstruction) instruction).getTurnAngle(); if (Double.isNaN(turnAngle)) { - primary.putNull("degrees"); + singleBannerInstruction.putNull("degrees"); } else { double degree = (Math.abs(turnAngle) * 180) / Math.PI; - primary.put("degrees", degree); + singleBannerInstruction.put("degrees", degree); } } } - - bannerInstruction.putNull("secondary"); } private static void putManeuver(Instruction instruction, ObjectNode instructionJson, Locale locale, TranslationMap translationMap, boolean isFirstInstructionOfLeg) { diff --git a/mapbox/src/main/resources/com/graphhopper/navigation/mapbox/de_DE.txt b/mapbox/src/main/resources/com/graphhopper/navigation/mapbox/de_DE.txt index b16619f..368e658 100644 --- a/mapbox/src/main/resources/com/graphhopper/navigation/mapbox/de_DE.txt +++ b/mapbox/src/main/resources/com/graphhopper/navigation/mapbox/de_DE.txt @@ -1,4 +1,5 @@ in_km_singular=In 1 Kilometer in_km=In %1$s Kilometern in_m=In %1$s Metern -for_km=für %1$s Kilometer \ No newline at end of file +for_km=für %1$s Kilometer +then=dann \ No newline at end of file diff --git a/mapbox/src/main/resources/com/graphhopper/navigation/mapbox/en_US.txt b/mapbox/src/main/resources/com/graphhopper/navigation/mapbox/en_US.txt index 499b017..c1c50ba 100644 --- a/mapbox/src/main/resources/com/graphhopper/navigation/mapbox/en_US.txt +++ b/mapbox/src/main/resources/com/graphhopper/navigation/mapbox/en_US.txt @@ -1,4 +1,5 @@ in_km_singular=In 1 kilometer in_km=In %1$s kilometers in_m=In %1$s meters -for_km=for %1$s kilometer \ No newline at end of file +for_km=for %1$s kilometer +then=then \ No newline at end of file diff --git a/mapbox/src/test/java/com/graphhopper/navigation/mapbox/MapboxResponseConverterTest.java b/mapbox/src/test/java/com/graphhopper/navigation/mapbox/MapboxResponseConverterTest.java index e99d364..446cb6d 100644 --- a/mapbox/src/test/java/com/graphhopper/navigation/mapbox/MapboxResponseConverterTest.java +++ b/mapbox/src/test/java/com/graphhopper/navigation/mapbox/MapboxResponseConverterTest.java @@ -100,7 +100,7 @@ public void basicTest() { assertEquals(1, voiceInstructions.size()); JsonNode voiceInstruction = voiceInstructions.get(0); assertTrue(voiceInstruction.get("distanceAlongGeometry").asDouble() <= instructionDistance); - assertEquals("turn sharp left onto la Callisa", voiceInstruction.get("announcement").asText()); + assertEquals("turn sharp left onto la Callisa, then keep left", voiceInstruction.get("announcement").asText()); JsonNode bannerInstructions = step.get("bannerInstructions"); assertEquals(1, bannerInstructions.size()); @@ -149,7 +149,7 @@ public void voiceInstructionsTest() { assertEquals(2, voiceInstructions.size()); JsonNode voiceInstruction = voiceInstructions.get(0); assertEquals(200, voiceInstruction.get("distanceAlongGeometry").asDouble(), 1); - assertEquals("In 200 meters At roundabout, take exit 2 onto CS-340", voiceInstruction.get("announcement").asText()); + assertEquals("In 200 meters At roundabout, take exit 2 onto CS-340, then At roundabout, take exit 2 onto CG-3", voiceInstruction.get("announcement").asText()); // Step 15 is over 3km long step = steps.get(15);