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; } };