Skip to content

Commit dcb2bb6

Browse files
authored
Port of osrm-text-instructions to Java (mapbox#374)
* [wip] TextInstructions * tag it experimental * add optional hook * add missing fiels in step response object * add constructors useful for testing * improved testing * fix checkstyle * add testing fixtures * add all tests * fix for json primitives * fixes compile function * easier to read this way * fix checkstyle
1 parent 33077c6 commit dcb2bb6

337 files changed

Lines changed: 10332 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,9 @@ distance-fixtures:
108108
curl -X POST --header "Content-Type:application/json" -d @mapbox/libjava-services/src/test/fixtures/distance_coordinates.json \
109109
"https://api.mapbox.com/distances/v1/mapbox/driving?access_token=$(MAPBOX_ACCESS_TOKEN)" \
110110
-o mapbox/libjava-services/src/test/fixtures/distance_v1.json
111+
112+
libosrm:
113+
rm -rf mapbox/libjava-services/src/main/resources/translations mapbox/libjava-services/src/test/fixtures/osrm/v5
114+
mkdir -p mapbox/libjava-services/src/main/resources/translations mapbox/libjava-services/src/test/fixtures/osrm/v5
115+
cp -R ../osrm-text-instructions/languages/translations/* mapbox/libjava-services/src/main/resources/translations
116+
cp -R ../osrm-text-instructions/test/fixtures/v5/* mapbox/libjava-services/src/test/fixtures/osrm/v5

mapbox/libjava-services/src/main/java/com/mapbox/services/api/directions/v5/models/IntersectionLanes.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ public class IntersectionLanes {
1111
public IntersectionLanes() {
1212
}
1313

14+
public IntersectionLanes(boolean valid) {
15+
this.valid = valid;
16+
}
17+
1418
/**
1519
* @return Boolean value for whether this lane can be taken to complete the maneuver. For
1620
* instance, if the lane array has four objects and the first two are marked as valid, then the

mapbox/libjava-services/src/main/java/com/mapbox/services/api/directions/v5/models/LegStep.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.mapbox.services.api.directions.v5.models;
22

33
import com.google.gson.annotations.SerializedName;
4+
45
import java.util.List;
56

67
/**
@@ -14,6 +15,8 @@ public class LegStep {
1415
private double duration;
1516
private String geometry;
1617
private String name;
18+
private String ref;
19+
private String destinations;
1720
private String mode;
1821
private String pronunciation;
1922
@SerializedName("rotary_name")
@@ -26,6 +29,16 @@ public class LegStep {
2629
public LegStep() {
2730
}
2831

32+
public LegStep(List<StepIntersection> intersections) {
33+
this.intersections = intersections;
34+
}
35+
36+
public LegStep(String name, String rotaryName, StepManeuver maneuver) {
37+
this.name = name;
38+
this.rotaryName = rotaryName;
39+
this.maneuver = maneuver;
40+
}
41+
2942
/**
3043
* The distance traveled from the maneuver to the next {@link LegStep}.
3144
*
@@ -64,6 +77,24 @@ public String getName() {
6477
return name;
6578
}
6679

80+
/**
81+
* @return String with reference number or code of the way along which the travel proceeds.
82+
* Optionally included, if data is available.
83+
* @since 2.0.0
84+
*/
85+
public String getRef() {
86+
return ref;
87+
}
88+
89+
/**
90+
* @return String with the destinations of the way along which the travel proceeds.
91+
* Optionally included, if data is available.
92+
* @since 2.0.0
93+
*/
94+
public String getDestinations() {
95+
return destinations;
96+
}
97+
6798
/**
6899
* @return String indicating the mode of transportation.
69100
* @since 1.0.0

mapbox/libjava-services/src/main/java/com/mapbox/services/api/directions/v5/models/StepIntersection.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ public class StepIntersection {
1717
public StepIntersection() {
1818
}
1919

20+
public StepIntersection(IntersectionLanes[] lanes) {
21+
this.lanes = lanes;
22+
}
23+
2024
/**
2125
* @return A [longitude, latitude] pair describing the location of the turn.
2226
* @since 1.3.0

mapbox/libjava-services/src/main/java/com/mapbox/services/api/directions/v5/models/StepManeuver.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ public class StepManeuver {
2525
public StepManeuver() {
2626
}
2727

28+
public StepManeuver(String type, String modifier, Integer exit) {
29+
this.type = type;
30+
this.modifier = modifier;
31+
this.exit = exit;
32+
}
33+
2834
/**
2935
* @return double array of [longitude, latitude] for the snapped coordinate.
3036
* @since 1.0.0
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
package com.mapbox.services.api.navigation.v5.osrm;
2+
3+
import com.google.gson.JsonObject;
4+
import com.google.gson.JsonParser;
5+
import com.google.gson.JsonPrimitive;
6+
import com.mapbox.services.Experimental;
7+
import com.mapbox.services.api.directions.v5.models.IntersectionLanes;
8+
import com.mapbox.services.api.directions.v5.models.LegStep;
9+
import com.mapbox.services.commons.utils.TextUtils;
10+
11+
import java.io.InputStream;
12+
import java.io.InputStreamReader;
13+
import java.util.logging.Level;
14+
import java.util.logging.Logger;
15+
16+
/**
17+
* Text instructions from OSRM route responses
18+
* <p>
19+
* This is an experimental API. Experimental APIs are quickly evolving and
20+
* might change or be removed in minor versions.
21+
*
22+
* @since 2.0.0
23+
*/
24+
@Experimental
25+
public class TextInstructions {
26+
27+
private static final Logger logger = Logger.getLogger(TextInstructions.class.getSimpleName());
28+
29+
private TokenizedInstructionHook tokenizedInstructionHook;
30+
31+
private JsonObject rootObject;
32+
private JsonObject versionObject;
33+
34+
public TextInstructions(String language, String version) {
35+
// Load the resource
36+
String localPath = String.format("translations/%s.json", language);
37+
InputStream resource = getClass().getClassLoader().getResourceAsStream(localPath);
38+
if (resource == null) {
39+
throw new RuntimeException("Translation not found for language: " + language);
40+
}
41+
42+
// Parse the JSON content
43+
rootObject = new JsonParser().parse(new InputStreamReader(resource)).getAsJsonObject();
44+
versionObject = rootObject.getAsJsonObject(version);
45+
if (versionObject == null) {
46+
throw new RuntimeException("Version not found for value: " + version);
47+
}
48+
}
49+
50+
public TokenizedInstructionHook getTokenizedInstructionHook() {
51+
return tokenizedInstructionHook;
52+
}
53+
54+
public void setTokenizedInstructionHook(TokenizedInstructionHook tokenizedInstructionHook) {
55+
this.tokenizedInstructionHook = tokenizedInstructionHook;
56+
}
57+
58+
public JsonObject getRootObject() {
59+
return rootObject;
60+
}
61+
62+
public JsonObject getVersionObject() {
63+
return versionObject;
64+
}
65+
66+
public static String capitalizeFirstLetter(String text) {
67+
return text.substring(0, 1).toUpperCase() + text.substring(1);
68+
}
69+
70+
/**
71+
* Transform numbers to their translated ordinalized value
72+
*
73+
* @param number value
74+
* @return translated ordinalized value
75+
*/
76+
public String ordinalize(Integer number) {
77+
try {
78+
return getVersionObject().getAsJsonObject("constants").getAsJsonObject("ordinalize")
79+
.getAsJsonPrimitive(String.valueOf(number)).getAsString();
80+
} catch (Exception exception) {
81+
return "";
82+
}
83+
}
84+
85+
/**
86+
* Transform degrees to their translated compass direction
87+
*
88+
* @param degree value
89+
* @return translated compass direction
90+
*/
91+
public String directionFromDegree(Double degree) {
92+
if (degree == null) {
93+
// step had no bearing_after degree, ignoring
94+
return "";
95+
} else if (degree >= 0 && degree <= 20) {
96+
return getVersionObject().getAsJsonObject("constants").getAsJsonObject("direction")
97+
.getAsJsonPrimitive("north").getAsString();
98+
} else if (degree > 20 && degree < 70) {
99+
return getVersionObject().getAsJsonObject("constants").getAsJsonObject("direction")
100+
.getAsJsonPrimitive("northeast").getAsString();
101+
} else if (degree >= 70 && degree <= 110) {
102+
return getVersionObject().getAsJsonObject("constants").getAsJsonObject("direction")
103+
.getAsJsonPrimitive("east").getAsString();
104+
} else if (degree > 110 && degree < 160) {
105+
return getVersionObject().getAsJsonObject("constants").getAsJsonObject("direction")
106+
.getAsJsonPrimitive("southeast").getAsString();
107+
} else if (degree >= 160 && degree <= 200) {
108+
return getVersionObject().getAsJsonObject("constants").getAsJsonObject("direction")
109+
.getAsJsonPrimitive("south").getAsString();
110+
} else if (degree > 200 && degree < 250) {
111+
return getVersionObject().getAsJsonObject("constants").getAsJsonObject("direction")
112+
.getAsJsonPrimitive("southwest").getAsString();
113+
} else if (degree >= 250 && degree <= 290) {
114+
return getVersionObject().getAsJsonObject("constants").getAsJsonObject("direction")
115+
.getAsJsonPrimitive("west").getAsString();
116+
} else if (degree > 290 && degree < 340) {
117+
return getVersionObject().getAsJsonObject("constants").getAsJsonObject("direction")
118+
.getAsJsonPrimitive("northwest").getAsString();
119+
} else if (degree >= 340 && degree <= 360) {
120+
return getVersionObject().getAsJsonObject("constants").getAsJsonObject("direction")
121+
.getAsJsonPrimitive("north").getAsString();
122+
} else {
123+
throw new RuntimeException("Degree is invalid: " + degree);
124+
}
125+
}
126+
127+
/**
128+
* Reduce any lane combination down to a contracted lane diagram
129+
*
130+
* @param step a route step
131+
*/
132+
public String laneConfig(LegStep step) {
133+
if (step.getIntersections() == null
134+
|| step.getIntersections().size() == 0
135+
|| step.getIntersections().get(0).getLanes() == null
136+
|| step.getIntersections().get(0).getLanes().length == 0) {
137+
throw new RuntimeException("No lanes object");
138+
}
139+
140+
StringBuilder config = new StringBuilder();
141+
Boolean currentLaneValidity = null;
142+
for (IntersectionLanes lane : step.getIntersections().get(0).getLanes()) {
143+
if (currentLaneValidity == null || currentLaneValidity != lane.getValid()) {
144+
if (lane.getValid()) {
145+
config.append("o");
146+
} else {
147+
config.append("x");
148+
}
149+
currentLaneValidity = lane.getValid();
150+
}
151+
}
152+
153+
return config.toString();
154+
}
155+
156+
public String compile(LegStep step) {
157+
if (step.getManeuver() == null) {
158+
throw new RuntimeException("No step maneuver provided.");
159+
}
160+
161+
String type = step.getManeuver().getType();
162+
String modifier = step.getManeuver().getModifier();
163+
String mode = step.getMode();
164+
165+
if (TextUtils.isEmpty(type)) {
166+
throw new RuntimeException("Missing step maneuver type.");
167+
}
168+
169+
if (!type.equals("depart") && !type.equals("arrive") && TextUtils.isEmpty(modifier)) {
170+
throw new RuntimeException("Missing step maneuver modifier.");
171+
}
172+
173+
if (getVersionObject().getAsJsonObject(type) == null) {
174+
// Log for debugging
175+
logger.log(Level.FINE, "Encountered unknown instruction type: " + type);
176+
177+
// OSRM specification assumes turn types can be added without
178+
// major version changes. Unknown types are to be treated as
179+
// type `turn` by clients
180+
type = "turn";
181+
}
182+
183+
// Use special instructions if available, otherwise `defaultinstruction`
184+
JsonObject instructionObject;
185+
JsonObject modeValue = getVersionObject().getAsJsonObject("modes").getAsJsonObject(mode);
186+
if (modeValue != null) {
187+
instructionObject = modeValue;
188+
} else {
189+
JsonObject modifierValue = getVersionObject().getAsJsonObject(type).getAsJsonObject(modifier);
190+
instructionObject = modifierValue == null
191+
? getVersionObject().getAsJsonObject(type).getAsJsonObject("default")
192+
: modifierValue;
193+
}
194+
195+
// Special case handling
196+
JsonPrimitive laneInstruction = null;
197+
switch (type) {
198+
case "use lane":
199+
laneInstruction = getVersionObject().getAsJsonObject("constants")
200+
.getAsJsonObject("lanes").getAsJsonPrimitive(laneConfig(step));
201+
if (laneInstruction == null) {
202+
// If the lane combination is not found, default to continue straight
203+
instructionObject = getVersionObject().getAsJsonObject("use lane")
204+
.getAsJsonObject("no_lanes");
205+
}
206+
break;
207+
case "rotary":
208+
case "roundabout":
209+
if (!TextUtils.isEmpty(step.getRotaryName())
210+
&& step.getManeuver().getExit() != null
211+
&& instructionObject.getAsJsonObject("name_exit") != null) {
212+
instructionObject = instructionObject.getAsJsonObject("name_exit");
213+
} else if (step.getRotaryName() != null && instructionObject.getAsJsonObject("name") != null) {
214+
instructionObject = instructionObject.getAsJsonObject("name");
215+
} else if (step.getManeuver().getExit() != null && instructionObject.getAsJsonObject("exit") != null) {
216+
instructionObject = instructionObject.getAsJsonObject("exit");
217+
} else {
218+
instructionObject = instructionObject.getAsJsonObject("default");
219+
}
220+
break;
221+
default:
222+
// NOOP, since no special logic for that type
223+
}
224+
225+
// Decide way_name with special handling for name and ref
226+
String wayName;
227+
String name = TextUtils.isEmpty(step.getName()) ? "" : step.getName();
228+
String ref = TextUtils.isEmpty(step.getRef()) ? "" : step.getRef().split(";")[0];
229+
230+
// Remove hacks from Mapbox Directions mixing ref into name
231+
if (name.equals(step.getRef())) {
232+
// if both are the same we assume that there used to be an empty name, with the ref being filled in for it
233+
// we only need to retain the ref then
234+
name = "";
235+
}
236+
name = name.replace(" (" + step.getRef() + ")", "");
237+
238+
if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(ref) && !name.equals(ref)) {
239+
wayName = name + " (" + ref + ")";
240+
} else if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(ref)) {
241+
wayName = ref;
242+
} else {
243+
wayName = name;
244+
}
245+
246+
// Decide which instruction string to use
247+
// Destination takes precedence over name
248+
String instruction;
249+
if (!TextUtils.isEmpty(step.getDestinations())
250+
&& instructionObject.getAsJsonPrimitive("destination") != null) {
251+
instruction = instructionObject.getAsJsonPrimitive("destination").getAsString();
252+
} else if (!TextUtils.isEmpty(wayName)
253+
&& instructionObject.getAsJsonPrimitive("name") != null) {
254+
instruction = instructionObject.getAsJsonPrimitive("name").getAsString();
255+
} else {
256+
instruction = instructionObject.getAsJsonPrimitive("default").getAsString();
257+
}
258+
259+
if (getTokenizedInstructionHook() != null) {
260+
instruction = getTokenizedInstructionHook().change(instruction);
261+
}
262+
263+
// Replace tokens
264+
// NOOP if they don't exist
265+
String nthWaypoint = ""; // TODO, add correct waypoint counting
266+
JsonPrimitive modifierValue =
267+
getVersionObject().getAsJsonObject("constants").getAsJsonObject("modifier").getAsJsonPrimitive(modifier);
268+
instruction = instruction
269+
.replace("{way_name}", wayName)
270+
.replace("{destination}", TextUtils.isEmpty(step.getDestinations()) ? "" : step.getDestinations().split(",")[0])
271+
.replace("{exit_number}",
272+
step.getManeuver().getExit() == null ? ordinalize(1) : ordinalize(step.getManeuver().getExit()))
273+
.replace("{rotary_name}", TextUtils.isEmpty(step.getRotaryName()) ? "" : step.getRotaryName())
274+
.replace("{lane_instruction}", laneInstruction == null ? "" : laneInstruction.getAsString())
275+
.replace("{modifier}", modifierValue == null ? "" : modifierValue.getAsString())
276+
.replace("{direction}", directionFromDegree(step.getManeuver().getBearingAfter()))
277+
.replace("{nth}", nthWaypoint)
278+
.replaceAll("\\s+", " "); // remove excess spaces
279+
280+
if (getRootObject().getAsJsonObject("meta").getAsJsonPrimitive("capitalizeFirstLetter").getAsBoolean()) {
281+
instruction = capitalizeFirstLetter(instruction);
282+
}
283+
284+
return instruction;
285+
}
286+
}

0 commit comments

Comments
 (0)