diff --git a/src/main/java/com/conveyal/r5/streets/BasicTraversalTimeCalculator.java b/src/main/java/com/conveyal/r5/streets/BasicTraversalTimeCalculator.java
index b68ea1764..19d1d4699 100644
--- a/src/main/java/com/conveyal/r5/streets/BasicTraversalTimeCalculator.java
+++ b/src/main/java/com/conveyal/r5/streets/BasicTraversalTimeCalculator.java
@@ -46,11 +46,11 @@ public int traversalTimeSeconds (EdgeStore.Edge currentEdge, StreetMode streetMo
* TODO pull this out into an interface to allow generalization to data from generalized cost tags
*/
@Override
- public int turnTimeSeconds (int fromEdge, int toEdge, StreetMode streetMode) {
+ public int turnTimeSeconds (int fromEdge, int toEdge, StreetMode streetMode, CongestionLevel congestionLevel) {
if (streetMode == StreetMode.CAR) {
double angle = calculateNewTurnAngle(fromEdge, toEdge);
if (angle < 27)
- return STRAIGHT_ON;
+ return straightOnDelay(fromEdge, congestionLevel);
else if (angle < 153)
return driveOnRight ? LEFT_TURN : RIGHT_TURN;
else if (angle < 207)
@@ -58,11 +58,24 @@ else if (angle < 207)
else if (angle < 333)
return driveOnRight ? RIGHT_TURN : LEFT_TURN;
else
- return STRAIGHT_ON;
+ return straightOnDelay(fromEdge, congestionLevel);
}
return 0;
}
+ /**
+ * Get the delay for driving straight over a crossing, depending on fromEdge’s StreetClass, and the time of the day
+ * Based on Jaakkonen (2013)
+ */
+ private int straightOnDelay(int fromEdge, CongestionLevel congestionLevel){
+ EdgeStore.Edge e = layer.edgeStore.getCursor(fromEdge);
+ Byte streetClassCode = e.getStreetClassCode();
+ return CrossingPenalty.getDelay(
+ congestionLevel,
+ JaakkonenStreetClass.fromR5StreetClassCode(streetClassCode)
+ );
+ }
+
/**
* Gets in/out angles from edges and calculates angle between them
* @return angle in degrees from 0-360
diff --git a/src/main/java/com/conveyal/r5/streets/CongestionLevel.java b/src/main/java/com/conveyal/r5/streets/CongestionLevel.java
new file mode 100644
index 000000000..f799b13c4
--- /dev/null
+++ b/src/main/java/com/conveyal/r5/streets/CongestionLevel.java
@@ -0,0 +1,45 @@
+package com.conveyal.r5.streets;
+
+import com.conveyal.r5.profile.ProfileRequest;
+
+import java.time.DayOfWeek;
+
+/**
+ * These congestion levels relate to Jaakkonen (2013)’s assessment of crossing penalties in the Helsinki metropolitan area
+ * See: ..., table 28 on page 61,
+ */
+public enum CongestionLevel {
+ RUSH_HOUR,
+ OFF_PEAK,
+ AVERAGE;
+
+ public static CongestionLevel fromFromTime(int secondsSinceMidnight) {
+ /*
+ * based on https://www.tomtom.com/traffic-index/helsinki-traffic/
+ */
+ if (secondsSinceMidnight < 25_200) // 7:00
+ return CongestionLevel.OFF_PEAK;
+ else if (secondsSinceMidnight < 36_000) // 10:00
+ return CongestionLevel.RUSH_HOUR;
+ else if (secondsSinceMidnight < 50_400) // 14:00
+ return CongestionLevel.AVERAGE;
+ else if (secondsSinceMidnight < 64800) // 18:00
+ return CongestionLevel.RUSH_HOUR;
+ else
+ return CongestionLevel.OFF_PEAK;
+ }
+
+ public static CongestionLevel fromProfileRequest(ProfileRequest profileRequest) {
+ // set the congestion level (rush hour/off peak) depending on week day and time of day
+ if (profileRequest.date != null) {
+ DayOfWeek dayOfWeek = profileRequest.date.getDayOfWeek();
+ if (dayOfWeek.equals(DayOfWeek.SUNDAY) || dayOfWeek.equals(DayOfWeek.SATURDAY))
+ return CongestionLevel.OFF_PEAK;
+ else
+ return CongestionLevel.fromFromTime(profileRequest.fromTime);
+ } else
+ return CongestionLevel.AVERAGE;
+ }
+
+}
+
diff --git a/src/main/java/com/conveyal/r5/streets/CrossingPenalty.java b/src/main/java/com/conveyal/r5/streets/CrossingPenalty.java
new file mode 100644
index 000000000..36ab4a036
--- /dev/null
+++ b/src/main/java/com/conveyal/r5/streets/CrossingPenalty.java
@@ -0,0 +1,48 @@
+package com.conveyal.r5.streets;
+
+import java.util.HashMap;
+
+/**
+ * Crossing penalties according to Jaakkonen (2013)’s analysis for the Helsinki metropolitan area
+ * See: ..., table 28 on page 61,
+ *
+ * Note that delays are in integer seconds, as com.conveyal.r5.streets.BasicTraversalTimeCalculator.turnTimeSeconds
+ * returns full seconds, only (probably a question of performance?)
+ */
+public class CrossingPenalty {
+ public static final HashMap> CROSSING_PENALTIES = new HashMap<>();
+
+ static {
+ CROSSING_PENALTIES.put(
+ CongestionLevel.AVERAGE, new HashMap<>() {
+ {
+ put(JaakkonenStreetClass.CLASS_1_2, 11 /*11.311*/);
+ put(JaakkonenStreetClass.CLASS_3, 9 /*9.439*/);
+ put(JaakkonenStreetClass.CLASS_4_5_6, 9 /*9.362*/);
+ }
+ }
+ );
+ CROSSING_PENALTIES.put(
+ CongestionLevel.OFF_PEAK, new HashMap<>() {
+ {
+ put(JaakkonenStreetClass.CLASS_1_2, 10 /*9.979*/);
+ put(JaakkonenStreetClass.CLASS_3, 7 /*6.650*/);
+ put(JaakkonenStreetClass.CLASS_4_5_6, 8 /*7.752*/);
+ }
+ }
+ );
+ CROSSING_PENALTIES.put(
+ CongestionLevel.RUSH_HOUR, new HashMap<>() {
+ {
+ put(JaakkonenStreetClass.CLASS_1_2, 12 /*12.195*/);
+ put(JaakkonenStreetClass.CLASS_3, 11 /*11.199*/);
+ put(JaakkonenStreetClass.CLASS_4_5_6, 11 /*10.633*/);
+ }
+ }
+ );
+ }
+
+ public static int getDelay(CongestionLevel congestionLevel, JaakkonenStreetClass jaakkonenStreetClass){
+ return CROSSING_PENALTIES.get(congestionLevel).get(jaakkonenStreetClass);
+ }
+}
diff --git a/src/main/java/com/conveyal/r5/streets/EdgeStore.java b/src/main/java/com/conveyal/r5/streets/EdgeStore.java
index 15269f785..fa6b433d6 100644
--- a/src/main/java/com/conveyal/r5/streets/EdgeStore.java
+++ b/src/main/java/com/conveyal/r5/streets/EdgeStore.java
@@ -729,13 +729,15 @@ public StreetRouter.State traverse (
// This was rounding up, now truncating ... maybe change back for consistency?
// int roundedTime = (int) Math.ceil(time);
+ CongestionLevel congestionLevel = CongestionLevel.fromProfileRequest(req);
+
int turnTimeSeconds = 0;
// Negative backEdge means this state is not the result of traversing an edge (it's the start of a search).
if (s0.backEdge >= 0) {
if (req.reverseSearch) {
- turnTimeSeconds = timeCalculator.turnTimeSeconds(getEdgeIndex(), s0.backEdge, streetMode);
+ turnTimeSeconds = timeCalculator.turnTimeSeconds(getEdgeIndex(), s0.backEdge, streetMode, congestionLevel);
} else {
- turnTimeSeconds = timeCalculator.turnTimeSeconds(s0.backEdge, getEdgeIndex(), streetMode);
+ turnTimeSeconds = timeCalculator.turnTimeSeconds(s0.backEdge, getEdgeIndex(), streetMode, congestionLevel);
}
}
// TODO add checks for negative increment values to these functions.
diff --git a/src/main/java/com/conveyal/r5/streets/EdgeTraversalTimes.java b/src/main/java/com/conveyal/r5/streets/EdgeTraversalTimes.java
index 10b44e650..8da57ed8a 100644
--- a/src/main/java/com/conveyal/r5/streets/EdgeTraversalTimes.java
+++ b/src/main/java/com/conveyal/r5/streets/EdgeTraversalTimes.java
@@ -39,7 +39,7 @@ public int traversalTimeSeconds (EdgeStore.Edge currentEdge, StreetMode streetMo
}
@Override
- public int turnTimeSeconds (int fromEdge, int toEdge, StreetMode streetMode) {
+ public int turnTimeSeconds (int fromEdge, int toEdge, StreetMode streetMode, CongestionLevel congestionLevel) {
if (streetMode == StreetMode.WALK) {
return walkTraversalTimes.turnTimeSeconds(fromEdge, toEdge);
} else if (streetMode == StreetMode.BICYCLE) {
diff --git a/src/main/java/com/conveyal/r5/streets/JaakkonenStreetClass.java b/src/main/java/com/conveyal/r5/streets/JaakkonenStreetClass.java
new file mode 100644
index 000000000..815c3efca
--- /dev/null
+++ b/src/main/java/com/conveyal/r5/streets/JaakkonenStreetClass.java
@@ -0,0 +1,28 @@
+package com.conveyal.r5.streets;
+
+import com.conveyal.r5.labeling.StreetClass;
+
+/**
+ * Translates the com.conveyal.r5.labeling.StreetClass (OSM tags) into Jaakkonen (2013)’s street classes,
+ * which are based on ‘functional classes’ in the DigiRoad’s road classification,
+ * see ...
+ * and
+ */
+public enum JaakkonenStreetClass {
+ CLASS_1_2,
+ CLASS_3,
+ CLASS_4_5_6;
+
+ public static JaakkonenStreetClass fromR5StreetClassCode(Byte streetClassCode) {
+ if(streetClassCode.equals(StreetClass.MOTORWAY.code) || streetClassCode.equals(StreetClass.PRIMARY.code)){
+ return JaakkonenStreetClass.CLASS_1_2;
+ } else if (streetClassCode.equals(StreetClass.SECONDARY.code)){
+ return JaakkonenStreetClass.CLASS_3;
+ } else if (streetClassCode.equals(StreetClass.TERTIARY.code) || streetClassCode.equals(StreetClass.OTHER.code)){
+ return JaakkonenStreetClass.CLASS_4_5_6;
+ } else { // catch-all, not really necessary here?
+ return JaakkonenStreetClass.CLASS_4_5_6;
+ }
+ }
+
+}
diff --git a/src/main/java/com/conveyal/r5/streets/MultistageTraversalTimeCalculator.java b/src/main/java/com/conveyal/r5/streets/MultistageTraversalTimeCalculator.java
index 952bb603c..1ae4fc2df 100644
--- a/src/main/java/com/conveyal/r5/streets/MultistageTraversalTimeCalculator.java
+++ b/src/main/java/com/conveyal/r5/streets/MultistageTraversalTimeCalculator.java
@@ -51,8 +51,8 @@ public int traversalTimeSeconds (EdgeStore.Edge currentEdge, StreetMode streetMo
}
@Override
- public int turnTimeSeconds (int fromEdge, int toEdge, StreetMode streetMode) {
- return base.turnTimeSeconds(fromEdge, toEdge, streetMode);
+ public int turnTimeSeconds (int fromEdge, int toEdge, StreetMode streetMode, CongestionLevel congestionLevel) {
+ return base.turnTimeSeconds(fromEdge, toEdge, streetMode, congestionLevel);
}
}
diff --git a/src/main/java/com/conveyal/r5/streets/StreetRouter.java b/src/main/java/com/conveyal/r5/streets/StreetRouter.java
index fa3750e35..0c1bb5270 100644
--- a/src/main/java/com/conveyal/r5/streets/StreetRouter.java
+++ b/src/main/java/com/conveyal/r5/streets/StreetRouter.java
@@ -752,6 +752,8 @@ public State getState (Split split) {
// Start on the forward edge of the pair that was split
EdgeStore.Edge e = streetLayer.edgeStore.getCursor(split.edge);
+ CongestionLevel congestionLevel = CongestionLevel.fromProfileRequest(profileRequest);
+
TIntList edgeList;
if (profileRequest.reverseSearch) {
edgeList = streetLayer.outgoingEdges.get(split.vertex1);
@@ -769,7 +771,7 @@ public State getState (Split split) {
ret.streetMode = s.streetMode;
// figure out the turn cost
- int turnCost = this.timeCalculator.turnTimeSeconds(s.backEdge, split.edge, s.streetMode);
+ int turnCost = this.timeCalculator.turnTimeSeconds(s.backEdge, split.edge, s.streetMode, congestionLevel);
int traversalCost = (int) Math.round(split.distance0_mm / 1000d / e.calculateSpeed(profileRequest, s.streetMode));
// TODO length of perpendicular
@@ -798,7 +800,7 @@ public State getState (Split split) {
}
State ret = new State(-1, split.edge + 1, state);
ret.streetMode = state.streetMode;
- int turnCost = this.timeCalculator.turnTimeSeconds(state.backEdge, split.edge + 1, state.streetMode);
+ int turnCost = this.timeCalculator.turnTimeSeconds(state.backEdge, split.edge + 1, state.streetMode, congestionLevel);
int traversalCost = (int) Math.round(split.distance1_mm / 1000d / e.calculateSpeed(profileRequest, state.streetMode));
ret.distance += split.distance1_mm;
// TODO length of perpendicular
diff --git a/src/main/java/com/conveyal/r5/streets/TraversalTimeCalculator.java b/src/main/java/com/conveyal/r5/streets/TraversalTimeCalculator.java
index 8b06eb56a..89e3c3fe0 100644
--- a/src/main/java/com/conveyal/r5/streets/TraversalTimeCalculator.java
+++ b/src/main/java/com/conveyal/r5/streets/TraversalTimeCalculator.java
@@ -39,7 +39,7 @@ public interface TraversalTimeCalculator extends Serializable {
*
* @return the expected value of the time in seconds to make the turn.
*/
- public int turnTimeSeconds (int fromEdge, int toEdge, StreetMode streetMode);
+ public int turnTimeSeconds (int fromEdge, int toEdge, StreetMode streetMode, CongestionLevel congestionLevel);
}
diff --git a/src/test/java/com/conveyal/r5/streets/BasicTraversalTimeCalculatorTest.java b/src/test/java/com/conveyal/r5/streets/BasicTraversalTimeCalculatorTest.java
index adc1bf376..4d145904e 100644
--- a/src/test/java/com/conveyal/r5/streets/BasicTraversalTimeCalculatorTest.java
+++ b/src/test/java/com/conveyal/r5/streets/BasicTraversalTimeCalculatorTest.java
@@ -43,7 +43,7 @@ public void testAngleSouthernHemisphere() throws Exception {
public void testCost () throws Exception {
setUp(false);
BasicTraversalTimeCalculator calculator = new BasicTraversalTimeCalculator(streetLayer, true);
- assertEquals(calculator.LEFT_TURN, calculator.turnTimeSeconds(ee + 1, es, StreetMode.CAR));
+ assertEquals(calculator.LEFT_TURN, calculator.turnTimeSeconds(ee + 1, es, StreetMode.CAR, CongestionLevel.OFF_PEAK));
}
/**
@@ -58,4 +58,4 @@ public void testJtsAngle () {
double a1 = Angle.angle(new Coordinate(10, 10), new Coordinate(9, 9));
assertTrue(a1 < a0);
}
-}
+}
\ No newline at end of file
diff --git a/src/test/java/com/conveyal/r5/streets/TimeDependentRoutingTest.java b/src/test/java/com/conveyal/r5/streets/TimeDependentRoutingTest.java
index 1c9020562..632cb2761 100644
--- a/src/test/java/com/conveyal/r5/streets/TimeDependentRoutingTest.java
+++ b/src/test/java/com/conveyal/r5/streets/TimeDependentRoutingTest.java
@@ -45,7 +45,7 @@ public int traversalTimeSeconds (EdgeStore.Edge currentEdge, StreetMode streetMo
}
@Override
- public int turnTimeSeconds (int fromEdge, int toEdge, StreetMode streetMode) {
+ public int turnTimeSeconds (int fromEdge, int toEdge, StreetMode streetMode, CongestionLevel congestionLevel) {
return 0;
}
};