Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add turn lanes support to navigate endpoint #2997

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ Here is an overview:
* thehereward, code cleanups like #620
* vvikas, ideas for many to many improvements and #616
* zstadler, multiple fixes and car4wd
* abderrahmane, added turn lane information to navigate endpoint
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: as the list has lexicographical order can you move it a bit up :) ?

(btw: will hopefully be able to get some time to test your PR out in the next days)


## Translations

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,22 @@ private static ObjectNode putInstruction(PointList points, InstructionList instr
if (intersectionValue.containsKey("out")) {
intersection.put("out", (int) intersectionValue.get("out"));
}
// lanes
if (!instruction.getInstructionDetails().isEmpty()) {
InstructionDetails lastDetail = instruction.getInstructionDetails().stream()
.sorted(Comparator.comparingDouble(InstructionDetails::getBeforeTurn))
.findFirst()
.get();
Comment on lines +179 to +182
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose the last lane information because in intersections we can't have more than one lane info. I checked the docs and the mapbox API and it had similar results

ArrayNode lanes = intersection.putArray("lanes");
for (LaneInfo li : lastDetail.getLanes()) {
ObjectNode n = lanes.addObject();
n.put("valid", li.isValid());
n.put("active", li.isValid());
ArrayNode indications = n.putArray("indications");
putDirections(instruction, li, indications, n, "active_indication");
}

}
}
}

Expand Down Expand Up @@ -309,7 +325,6 @@ private static void putBannerInstructions(InstructionList instructions, double d
},
secondary: null,
*/

ObjectNode bannerInstruction = bannerInstructions.addObject();

//Show from the beginning
Expand All @@ -319,11 +334,32 @@ private static void putBannerInstructions(InstructionList instructions, double d
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);
if (instructions.get(index + 1).getInstructionDetails().isEmpty()) {
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);
}
} else {
for (InstructionDetails details: instructions.get(index + 1).getInstructionDetails()) {
ObjectNode subBannerInstruction = bannerInstructions.addObject();
subBannerInstruction.put("distanceAlongGeometry", details.getBeforeTurn());

ObjectNode subPrimary = subBannerInstruction.putObject("primary");
putSingleBannerInstruction(instructions.get(index + 1), locale, translationMap, subPrimary);

ObjectNode sub = subBannerInstruction.putObject("sub");
sub.put("text", "");
ArrayNode components = sub.putArray("components");
for (LaneInfo lane : details.getLanes()) {
ObjectNode component = components.addObject();
component.put("text", "");
component.put("type", "lane");
component.put("active", lane.isValid());
ArrayNode directions = component.putArray("directions");
putDirections(instructions.get(index + 1), lane, directions, primary, "active_direction");
Comment on lines +344 to +360
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This requires some explanation, I checked the mapbox navigation API and how it works is:
when there's lane information to show it creates a bannerInstruction with the primary turn only, then it creates a second bannerInstruction with the same primary and a sub that contains the lane information.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, makes sense then. Were you able to test your changes with the mapbox or maplibre SDK?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not yet, but I'll certainly do. I'll set up the environment and try it out.

}
}
}
}

Expand Down Expand Up @@ -469,4 +505,32 @@ public static ObjectNode convertFromGHResponseError(GHResponse ghResponse) {
json.put("message", ghResponse.getErrors().get(0).getMessage());
return json;
}
}

public static void putDirections(Instruction instruction, LaneInfo lane, ArrayNode directions,
ObjectNode laneNode, String activeDirectionFieldName) {
for (String direction: lane.getDirections()){
if (direction.equals("continue") || direction.equals("none")) {
direction = "straight";
}
direction = direction.replace("_", " ");
directions.add(direction);
if (lane.isValid()) {
if (lane.getDirections().size() == 1) {
laneNode.put(activeDirectionFieldName, direction);
} else {
if (direction.startsWith("merge")) {
return;
}
String modifier = getModifier(instruction);
if (modifier != null) {
String[] dir = modifier.split(" ");
String[] laneDir = direction.split(" ");
if (dir[dir.length - 1].equals(laneDir[laneDir.length - 1])) {
laneNode.put(activeDirectionFieldName, direction);
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.graphhopper.navigation;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeCreator;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import com.graphhopper.GHRequest;
import com.graphhopper.GHResponse;
import com.graphhopper.GraphHopper;
import com.graphhopper.GraphHopperConfig;
import com.graphhopper.config.Profile;
import com.graphhopper.routing.TestProfiles;
import com.graphhopper.util.Helper;
import com.graphhopper.util.PMap;
import com.graphhopper.util.TranslationMap;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

class NavigationResponseConverterTurnLanesTest {

private static final String graphFolder = "target/graphhopper-turn-lanes-test-car";
private static final String osmFile = "../core/files/bautzen.osm";
private static GraphHopper hopper;
private static final String profile = "my_car";

private final TranslationMap trMap = hopper.getTranslationMap();
private final DistanceConfig distanceConfig = new DistanceConfig(DistanceUtils.Unit.METRIC, trMap, Locale.ENGLISH);

@BeforeAll
public static void beforeClass() {
// make sure we are using fresh files with correct vehicle
Helper.removeDir(new File(graphFolder));
hopper = new GraphHopper().setStoreOnFlush(true);
hopper = hopper.init(new GraphHopperConfig()
.putObject("graph.location", graphFolder)
.putObject("datareader.file", osmFile)
.putObject("import.osm.ignored_highways", "footway,cycleway,path,pedestrian,steps")
.putObject("datareader.turn_lanes_profiles", profile)
.setProfiles(List.of(TestProfiles.accessAndSpeed(profile, "car")))
).importOrLoad();
}

@AfterAll
public static void afterClass() {
Helper.removeDir(new File(graphFolder));
}

@Test
public void intersectionsTest() {
GHResponse rsp = hopper.route(new GHRequest(51.186861,14.412755,51.18958,14.41242).
setProfile(profile).setPathDetails(Arrays.asList("intersection")));

ObjectNode json = NavigateResponseConverter.convertFromGHResponse(rsp, trMap, Locale.ENGLISH, distanceConfig);
JsonNode lanes = json.get("routes").get(0).get("legs").get(0).get("steps")
.get(1).get("intersections").get(0).get("lanes");

assertEquals(2, lanes.size());

JsonNode firstLane = lanes.get(0);
assertEquals(1, firstLane.get("indications").size());
assertEquals("left", firstLane.get("indications").get(0).asText());
assertEquals(true, firstLane.get("valid").asBoolean());
assertEquals(true, firstLane.get("active").asBoolean());
assertEquals("left", firstLane.get("active_indication").asText());

JsonNode secondLane = lanes.get(1);
assertEquals(1, secondLane.get("indications").size());
assertEquals("straight", secondLane.get("indications").get(0).asText());
assertEquals(false, secondLane.get("valid").asBoolean());
assertEquals(false, secondLane.get("active").asBoolean());
}

@Test
public void bannerInstructionsTest() {
GHResponse rsp = hopper.route(new GHRequest(51.186861,14.412755,51.18958,14.41242).
setProfile(profile).setPathDetails(Arrays.asList("intersection")));

ObjectNode json = NavigateResponseConverter.convertFromGHResponse(rsp, trMap, Locale.ENGLISH, distanceConfig);
JsonNode bannerInstructions = json.get("routes").get(0).get("legs").get(0).get("steps")
.get(0).get("bannerInstructions");

assertEquals(2, bannerInstructions.size());

JsonNode firstBannerInstruction = bannerInstructions.get(0);

assertEquals("Turn left and take A 4 toward Dresden", firstBannerInstruction.get("primary").get("text").asText());
assertEquals("left", firstBannerInstruction.get("primary").get("modifier").asText());
assertEquals("left", firstBannerInstruction.get("primary").get("active_direction").asText());
assertEquals("turn", firstBannerInstruction.get("primary").get("type").asText());
assertEquals("[{\"text\":\"Turn left and take A 4 toward Dresden\",\"type\":\"text\"}]", firstBannerInstruction.get("primary").get("components").toString());
assertNull(firstBannerInstruction.get("sub"));
assertEquals(597, firstBannerInstruction.get("distanceAlongGeometry").asDouble(), 0.001);

JsonNode secondBannerInstruction = bannerInstructions.get(1);
assertEquals("Turn left and take A 4 toward Dresden", secondBannerInstruction.get("primary").get("text").asText());
assertEquals("left", secondBannerInstruction.get("primary").get("modifier").asText());
assertEquals("turn", secondBannerInstruction.get("primary").get("type").asText());
assertEquals("[{\"text\":\"Turn left and take A 4 toward Dresden\",\"type\":\"text\"}]", secondBannerInstruction.get("primary").get("components").toString());
assertEquals(161.593, secondBannerInstruction.get("distanceAlongGeometry").asDouble(), 0.001);

assertEquals("", secondBannerInstruction.get("sub").get("text").asText());
assertEquals("[{\"text\":\"\",\"type\":\"lane\",\"active\":true,\"directions\":[\"left\"]},{\"text\":\"\",\"type\":\"lane\",\"active\":false,\"directions\":[\"straight\"]}]",
secondBannerInstruction.get("sub").get("components").toString());
}
}