Repository: graphhopper/graphhopper
Pull request #2303 (2021-05-04 14:50:39)

Implementation of smoothness encoded value.
This is the implementation of the encoded value for the `smoothness`. With commit https://github.com/graphhopper/graphhopper/pull/2303/commits/68932aa7551a97825eafdcabc945d884cfcd9943 the bicycle common flag encoder now use it.

implements #655, supersedes #2108

ratrun: I believe that now the code is working as you suggested in #2108. 
Anyway, I had two problems with failing tests:

1.  The first one is in AbstractBikeFlagEncoderTester, see the changes I had to make. This test looks a bit ugly to me. I think the flagencoders are using more bits now, such that the encoded value does not fit into one integer any longer, so now I'm oring the values of the two 32 bit integeres.
2. The second one are two failing tests in PrepareContractionHierarchiesTest. I have no idea why these two are failing now.

easbar: > The second one are two failing tests in PrepareContractionHierarchiesTest. I have no idea why these two are failing now.

I think these tests were somewhat broken already before and only worked 'accidentally'. Please update your branch to 
https://github.com/graphhopper/graphhopper/commit/b18d576f6c125a6db8fe99e8ec4712c4a31f4598 or newer and the problem should be solved.
ratrun: @easbar Thank you for the fix, I can confirm that it is working again. I have now re-enabled these tests.

Patch:
diff --git a/config-example.yml b/config-example.yml
index bf3afd9c6..efaa2f7f5 100644
--- a/config-example.yml
+++ b/config-example.yml
@@ -17,8 +17,8 @@ graphhopper:
 
   # Add additional information to every edge. Used for path details (#1548), better instructions (#1844) and tunnel/bridge interpolation (#798).
   # Default values are: road_class,road_class_link,road_environment,max_speed,road_access (since #1805)
-  # More are: surface,max_width,max_height,max_weight,max_axle_load,max_length,hazmat,hazmat_tunnel,hazmat_water,toll,track_type,
-  #           mtb_rating,hiking_rating,horse_rating,lanes
+  # More are: surface,smoothness,max_width,max_height,max_weight,max_axle_load,max_length,hazmat,hazmat_tunnel,hazmat_water,toll,track_type,
+  #           mtb_rating, hiking_rating,horse_rating,lanes
   # graph.encoded_values: surface,toll,track_type
 
   ##### Routing Profiles ####
diff --git a/core/src/main/java/com/graphhopper/routing/ev/DefaultEncodedValueFactory.java b/core/src/main/java/com/graphhopper/routing/ev/DefaultEncodedValueFactory.java
index 86004afee..37a833633 100644
--- a/core/src/main/java/com/graphhopper/routing/ev/DefaultEncodedValueFactory.java
+++ b/core/src/main/java/com/graphhopper/routing/ev/DefaultEncodedValueFactory.java
@@ -57,6 +57,8 @@ public class DefaultEncodedValueFactory implements EncodedValueFactory {
             enc = MaxLength.create();
         } else if (Surface.KEY.equals(name)) {
             enc = new EnumEncodedValue<>(Surface.KEY, Surface.class);
+        } else if (Smoothness.KEY.equals(name)) {
+            enc = new EnumEncodedValue<>(Smoothness.KEY, Smoothness.class);
         } else if (Toll.KEY.equals(name)) {
             enc = new EnumEncodedValue<>(Toll.KEY, Toll.class);
         } else if (TrackType.KEY.equals(name)) {
diff --git a/core/src/main/java/com/graphhopper/routing/ev/Smoothness.java b/core/src/main/java/com/graphhopper/routing/ev/Smoothness.java
new file mode 100644
index 000000000..7ca39c05d
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/ev/Smoothness.java
@@ -0,0 +1,55 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package com.graphhopper.routing.ev;
+
+import com.graphhopper.util.Helper;
+
+/**
+ * This enum defines the road smoothness of an edge. If not tagged the value will be MISSING, which is the default. 
+ * All unknown smoothness tags will get OTHER .
+ */
+public enum Smoothness {
+    // Order is important to make it roughly comparable
+    MISSING("missing"),
+    EXCELLENT("excellent"), GOOD("good"), INTERMEDIATE("intermediate"), BAD("bad"), VERY_BAD("very_bad"),
+    HORRIBLE("horrible"), VERY_HORRIBLE("very_horrible"), IMPASSABLE("impassable"),
+    OTHER("other");
+
+    public static final String KEY = "smoothness";
+
+    private final String name;
+
+    Smoothness(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+
+    public static Smoothness find(String name) {
+        if (Helper.isEmpty(name))
+            return MISSING;
+        try {
+            return Smoothness.valueOf(Helper.toUpperCase(name));
+        } catch (IllegalArgumentException ex) {
+            return OTHER;
+        }
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/routing/util/BikeCommonFlagEncoder.java b/core/src/main/java/com/graphhopper/routing/util/BikeCommonFlagEncoder.java
index de9914acf..280f2e39b 100644
--- a/core/src/main/java/com/graphhopper/routing/util/BikeCommonFlagEncoder.java
+++ b/core/src/main/java/com/graphhopper/routing/util/BikeCommonFlagEncoder.java
@@ -47,12 +47,15 @@ abstract public class BikeCommonFlagEncoder extends AbstractFlagEncoder {
     protected final Set<String> unpavedSurfaceTags = new HashSet<>();
     private final Map<String, Integer> trackTypeSpeeds = new HashMap<>();
     private final Map<String, Integer> surfaceSpeeds = new HashMap<>();
+    protected static final double smoothnessFactorPushingSectionThreshold = 0.3d;
+    private final Map<Smoothness, Double> smoothnessFactor = new HashMap<>();
     private final Map<String, Integer> highwaySpeeds = new HashMap<>();
     protected boolean speedTwoDirections;
     DecimalEncodedValue priorityEnc;
     // Car speed limit which switches the preference from UNCHANGED to AVOID_IF_POSSIBLE
     private int avoidSpeedLimit;
     EnumEncodedValue<RouteNetwork> bikeRouteEnc;
+    EnumEncodedValue<Smoothness> smoothnessEnc;
     Map<RouteNetwork, Integer> routeMap = new HashMap<>();
 
     // This is the specific bicycle class
@@ -178,6 +181,9 @@ abstract public class BikeCommonFlagEncoder extends AbstractFlagEncoder {
         routeMap.put(REGIONAL, VERY_NICE.getValue());
         routeMap.put(LOCAL, PREFER.getValue());
 
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.MISSING, 1.0d);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.OTHER, 0.7d);
+
         setAvoidSpeedLimit(71);
     }
 
@@ -199,6 +205,7 @@ abstract public class BikeCommonFlagEncoder extends AbstractFlagEncoder {
         registerNewEncodedValue.add(priorityEnc = new UnsignedDecimalEncodedValue(getKey(prefix, "priority"), 3, PriorityCode.getFactor(1), false));
 
         bikeRouteEnc = getEnumEncodedValue(RouteNetwork.key("bike"), RouteNetwork.class);
+        smoothnessEnc = getEnumEncodedValue(Smoothness.KEY, Smoothness.class);
     }
 
     @Override
@@ -304,6 +311,12 @@ abstract public class BikeCommonFlagEncoder extends AbstractFlagEncoder {
         double wayTypeSpeed = getSpeed(way);
         if (!access.isFerry()) {
             wayTypeSpeed = applyMaxSpeed(way, wayTypeSpeed);
+            Smoothness smoothness = smoothnessEnc.getEnum(false, edgeFlags);
+            if (smoothness != Smoothness.MISSING) {
+               // smoothness handling: Multiply speed with smoothnessFactor
+               Double smoothnessSpeedFactor = smoothnessFactor.get(smoothness);
+               wayTypeSpeed = (smoothnessSpeedFactor <= smoothnessFactorPushingSectionThreshold) ? PUSHING_SECTION_SPEED: (int)Math.round(smoothnessSpeedFactor * wayTypeSpeed);
+            }
             handleSpeed(edgeFlags, way, wayTypeSpeed);
         } else {
             double ferrySpeed = ferrySpeedCalc.getSpeed(way);
@@ -556,6 +569,10 @@ abstract public class BikeCommonFlagEncoder extends AbstractFlagEncoder {
         surfaceSpeeds.put(surface, speed);
     }
 
+    void setSmoothnessSpeedFactor(Smoothness smoothness, double speedfactor) {
+        smoothnessFactor.put(smoothness, speedfactor);
+    }
+
     void addPushingSection(String highway) {
         pushingSectionsHighways.add(highway);
     }
diff --git a/core/src/main/java/com/graphhopper/routing/util/BikeFlagEncoder.java b/core/src/main/java/com/graphhopper/routing/util/BikeFlagEncoder.java
index 2781c2723..a53c2734c 100644
--- a/core/src/main/java/com/graphhopper/routing/util/BikeFlagEncoder.java
+++ b/core/src/main/java/com/graphhopper/routing/util/BikeFlagEncoder.java
@@ -62,6 +62,16 @@ public class BikeFlagEncoder extends BikeCommonFlagEncoder {
         preferHighwayTags.add("residential");
         preferHighwayTags.add("unclassified");
 
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.EXCELLENT, 1.1d);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.GOOD, 1.0d);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.INTERMEDIATE, 0.9d);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.BAD, 0.7d);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.VERY_BAD, 0.6d);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.HORRIBLE, 0.5d);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.VERY_HORRIBLE, 0.4d);
+        // SmoothnessSpeed <= smoothnessFactorPushingSectionThreshold gets mapped to speed PUSHING_SECTION_SPEED
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.IMPASSABLE, smoothnessFactorPushingSectionThreshold);
+
         absoluteBarriers.add("kissing_gate");
         setSpecificClassBicycle("touring");
     }
diff --git a/core/src/main/java/com/graphhopper/routing/util/EncodingManager.java b/core/src/main/java/com/graphhopper/routing/util/EncodingManager.java
index 669d80300..5db754d51 100644
--- a/core/src/main/java/com/graphhopper/routing/util/EncodingManager.java
+++ b/core/src/main/java/com/graphhopper/routing/util/EncodingManager.java
@@ -338,7 +338,8 @@ public class EncodingManager implements EncodedValueLookup {
                         _addRelationTagParser(new OSMBikeNetworkTagParser());
                     if (!em.hasEncodedValue(GetOffBike.KEY))
                         _addEdgeTagParser(new OSMGetOffBikeParser(), false, false);
-
+                    if (!em.hasEncodedValue(Smoothness.KEY))
+                        _addEdgeTagParser(new OSMSmoothnessParser(), false, false);
                 } else if (encoder instanceof FootFlagEncoder) {
                     if (!em.hasEncodedValue(RouteNetwork.key("foot")))
                         _addRelationTagParser(new OSMFootNetworkTagParser());
diff --git a/core/src/main/java/com/graphhopper/routing/util/MountainBikeFlagEncoder.java b/core/src/main/java/com/graphhopper/routing/util/MountainBikeFlagEncoder.java
index 00ecd2b2a..29b4e38f6 100644
--- a/core/src/main/java/com/graphhopper/routing/util/MountainBikeFlagEncoder.java
+++ b/core/src/main/java/com/graphhopper/routing/util/MountainBikeFlagEncoder.java
@@ -128,6 +128,16 @@ public class MountainBikeFlagEncoder extends BikeCommonFlagEncoder {
         preferHighwayTags.add("residential");
         preferHighwayTags.add("unclassified");
 
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.EXCELLENT, 1.1d);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.GOOD, 1.0d);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.INTERMEDIATE, 0.9d);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.BAD, 0.7d);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.VERY_BAD, 0.6d);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.HORRIBLE, 0.5d);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.VERY_HORRIBLE, 0.4d);
+        // SmoothnessSpeed <= smoothnessFactorPushingSectionThreshold gets mapped to speed PUSHING_SECTION_SPEED
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.IMPASSABLE, smoothnessFactorPushingSectionThreshold);
+
         potentialBarriers.add("kissing_gate");
         setSpecificClassBicycle("mtb");
     }
diff --git a/core/src/main/java/com/graphhopper/routing/util/RacingBikeFlagEncoder.java b/core/src/main/java/com/graphhopper/routing/util/RacingBikeFlagEncoder.java
index ad14809b1..4bdda45ab 100644
--- a/core/src/main/java/com/graphhopper/routing/util/RacingBikeFlagEncoder.java
+++ b/core/src/main/java/com/graphhopper/routing/util/RacingBikeFlagEncoder.java
@@ -113,6 +113,15 @@ public class RacingBikeFlagEncoder extends BikeCommonFlagEncoder {
         addPushingSection("pedestrian");
         addPushingSection("steps");
 
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.EXCELLENT, 1.2d);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.GOOD, 1.0d);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.INTERMEDIATE, 0.9d);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.BAD, 0.7d);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.VERY_BAD, smoothnessFactorPushingSectionThreshold);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.HORRIBLE, smoothnessFactorPushingSectionThreshold);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.VERY_HORRIBLE, smoothnessFactorPushingSectionThreshold);
+        setSmoothnessSpeedFactor(com.graphhopper.routing.ev.Smoothness.IMPASSABLE, smoothnessFactorPushingSectionThreshold);
+
         routeMap.put(INTERNATIONAL, BEST.getValue());
         routeMap.put(NATIONAL, BEST.getValue());
         routeMap.put(REGIONAL, VERY_NICE.getValue());
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/DefaultTagParserFactory.java b/core/src/main/java/com/graphhopper/routing/util/parsers/DefaultTagParserFactory.java
index 32f589a25..22b632471 100644
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/DefaultTagParserFactory.java
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/DefaultTagParserFactory.java
@@ -54,6 +54,8 @@ public class DefaultTagParserFactory implements TagParserFactory {
             return new OSMMaxLengthParser();
         else if (name.equals(Surface.KEY))
             return new OSMSurfaceParser();
+        else if (name.equals(Smoothness.KEY))
+            return new OSMSmoothnessParser();
         else if (name.equals(Toll.KEY))
             return new OSMTollParser();
         else if (name.equals(TrackType.KEY))
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMSmoothnessParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMSmoothnessParser.java
new file mode 100644
index 000000000..ae328d80f
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMSmoothnessParser.java
@@ -0,0 +1,58 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package com.graphhopper.routing.util.parsers;
+
+import com.graphhopper.reader.ReaderWay;
+import com.graphhopper.routing.ev.EncodedValue;
+import com.graphhopper.routing.ev.EncodedValueLookup;
+import com.graphhopper.routing.ev.EnumEncodedValue;
+import com.graphhopper.routing.ev.Smoothness;
+import com.graphhopper.storage.IntsRef;
+
+import java.util.List;
+
+import static com.graphhopper.routing.ev.Smoothness.*;
+
+public class OSMSmoothnessParser implements TagParser {
+
+    private final EnumEncodedValue<Smoothness> smoothnessEnc;
+
+    public OSMSmoothnessParser() {
+        this(new EnumEncodedValue<>(KEY, Smoothness.class));
+    }
+
+    public OSMSmoothnessParser(EnumEncodedValue<Smoothness> smoothnessEnc) {
+        this.smoothnessEnc = smoothnessEnc;
+    }
+
+    @Override
+    public void createEncodedValues(EncodedValueLookup lookup, List<EncodedValue> list) {
+        list.add(smoothnessEnc);
+    }
+
+    @Override
+    public IntsRef handleWayTags(IntsRef edgeFlags, ReaderWay readerWay, boolean ferry, IntsRef relationFlags) {
+        String smoothnessTag = readerWay.getTag("smoothness");
+        Smoothness smoothness = Smoothness.find(smoothnessTag);
+        if (smoothness == MISSING)
+            return edgeFlags;
+
+        smoothnessEnc.setEnum(false, edgeFlags, smoothness);
+        return edgeFlags;
+    }
+}
\ No newline at end of file
diff --git a/core/src/main/java/com/graphhopper/util/details/PathDetailsBuilderFactory.java b/core/src/main/java/com/graphhopper/util/details/PathDetailsBuilderFactory.java
index 7392a8eaf..b24423d49 100644
--- a/core/src/main/java/com/graphhopper/util/details/PathDetailsBuilderFactory.java
+++ b/core/src/main/java/com/graphhopper/util/details/PathDetailsBuilderFactory.java
@@ -74,7 +74,7 @@ public class PathDetailsBuilderFactory {
                 builders.add(new BooleanDetails(key, evl.getBooleanEncodedValue(key)));
         }
 
-        for (String key : Arrays.asList(RoadClass.KEY, RoadEnvironment.KEY, Surface.KEY, RoadAccess.KEY,
+        for (String key : Arrays.asList(RoadClass.KEY, RoadEnvironment.KEY, Surface.KEY, Smoothness.KEY, RoadAccess.KEY,
                 BikeNetwork.KEY, FootNetwork.KEY, Toll.KEY, TrackType.KEY, Hazmat.KEY, HazmatTunnel.KEY,
                 HazmatWater.KEY, Country.KEY)) {
             if (requestedPathDetails.contains(key) && evl.hasEncodedValue(key))
diff --git a/docs/core/custom-models.md b/docs/core/custom-models.md
index 6f4c28e85..3283d1adf 100644
--- a/docs/core/custom-models.md
+++ b/docs/core/custom-models.md
@@ -69,6 +69,7 @@ encoded values are the following (some of their possible values are given in bra
 - road_environment: (ROAD, FERRY, BRIDGE, TUNNEL, ...)
 - road_access: (DESTINATION, DELIVERY, PRIVATE, NO, ...)
 - surface: (PAVED, DIRT, SAND, GRAVEL, ...)
+- smoothness: (EXCELLENT, GOOD, INTERMEDIATE, ...)
 - toll: (NO, ALL, HGV)
 
 To learn about all available encoded values you can query the `/info` endpoint.
diff --git a/web-bundle/src/main/js/custom-model-editor/demo/index.html b/web-bundle/src/main/js/custom-model-editor/demo/index.html
index 4926b1ad7..d6f446414 100644
--- a/web-bundle/src/main/js/custom-model-editor/demo/index.html
+++ b/web-bundle/src/main/js/custom-model-editor/demo/index.html
@@ -26,6 +26,7 @@
         "road_environment": {type: 'enum', values: ["OTHER", "ROAD", "FERRY", "TUNNEL", "BRIDGE", "FORD", "SHUTTLE_TRAIN"].sort()},
         "road_access": {type: 'enum', values: ["YES", "DESTINATION", "CUSTOMERS", "DELIVERY", "FORESTRY", "AGRICULTURAL", "PRIVATE", "OTHER", "NO"].sort()},
         "surface": {type: 'enum', values: ["MISSING", "PAVED", "ASPHALT", "CONCRETE", "PAVING_STONES", "COBBLESTONE", "UNPAVED", "COMPACTED", "FINE_GRAVEL", "GRAVEL", "GROUND", "DIRT", "GRASS", "SAND", "OTHER"].sort()},
+        "smoothness": {type: 'enum', values: ["MISSING", "EXCELLENT", "GOOD", "INTERMEDIATE", "BAD", "VERY_BAD", "HORRIBLE", "VERY_HORRIBLE", "IMPASSABLE", "OTHER"].sort()},
         "toll": {type: 'enum', values: ["NO", "ALL", "HGV"].sort()}
     };
     const editor = GHCustomModelEditor.create({}, (element) => {


Test patch:
diff --git a/core/src/test/java/com/graphhopper/GraphHopperTest.java b/core/src/test/java/com/graphhopper/GraphHopperTest.java
index 1c8b8e0e2..689251c4c 100644
--- a/core/src/test/java/com/graphhopper/GraphHopperTest.java
+++ b/core/src/test/java/com/graphhopper/GraphHopperTest.java
@@ -422,7 +422,7 @@ public class GraphHopperTest {
 
         assertEquals(3, rsp.getAll().size());
         // via ramsenthal
-        assertEquals(2863, rsp.getAll().get(0).getTime() / 1000);
+        assertEquals(2865, rsp.getAll().get(0).getTime() / 1000);
         // via unterwaiz
         assertEquals(3318, rsp.getAll().get(1).getTime() / 1000);
         // via eselslohe -> theta; BTW: here smaller time as 2nd alternative due to priority influences time order
diff --git a/core/src/test/java/com/graphhopper/routing/util/AbstractBikeFlagEncoderTester.java b/core/src/test/java/com/graphhopper/routing/util/AbstractBikeFlagEncoderTester.java
index cf1ab3e0d..b195c9e5a 100644
--- a/core/src/test/java/com/graphhopper/routing/util/AbstractBikeFlagEncoderTester.java
+++ b/core/src/test/java/com/graphhopper/routing/util/AbstractBikeFlagEncoderTester.java
@@ -70,7 +70,8 @@ public abstract class AbstractBikeFlagEncoderTester {
     }
 
     protected double getSpeedFromFlags(ReaderWay way) {
-        IntsRef flags = encoder.handleWayTags(encodingManager.createEdgeFlags(), way, WAY);
+        IntsRef relFlags = encodingManager.createRelationFlags();
+        IntsRef flags = encodingManager.handleWayTags(way, new EncodingManager.AcceptWay().put(encoder.toString(), WAY), relFlags);
         return avgSpeedEnc.getDecimal(false, flags);
     }
 
@@ -255,13 +256,13 @@ public abstract class AbstractBikeFlagEncoderTester {
         way.setTag("railway", "platform");
         IntsRef relFlags = encodingManager.createRelationFlags();
         IntsRef flags = encoder.handleWayTags(encodingManager.createEdgeFlags(), way, encoder.getAccess(way));
-        assertNotEquals(0, flags.ints[0]);
+        assertNotEquals(true, flags.isEmpty());
 
         way = new ReaderWay(1);
         way.setTag("highway", "track");
         way.setTag("railway", "platform");
         flags = encoder.handleWayTags(encodingManager.createEdgeFlags(), way, encoder.getAccess(way));
-        assertNotEquals(0, flags.ints[0]);
+        assertNotEquals(true, flags.isEmpty());
 
         way = new ReaderWay(1);
         way.setTag("highway", "track");
@@ -269,7 +270,7 @@ public abstract class AbstractBikeFlagEncoderTester {
         way.setTag("bicycle", "no");
 
         flags = encoder.handleWayTags(encodingManager.createEdgeFlags(), way, encoder.getAccess(way));
-        assertEquals(0, flags.ints[0]);
+        assertEquals(true, flags.isEmpty());
     }
 
     @Test
diff --git a/core/src/test/java/com/graphhopper/routing/util/BikeFlagEncoderTest.java b/core/src/test/java/com/graphhopper/routing/util/BikeFlagEncoderTest.java
index 90126a205..d8f133c10 100644
--- a/core/src/test/java/com/graphhopper/routing/util/BikeFlagEncoderTest.java
+++ b/core/src/test/java/com/graphhopper/routing/util/BikeFlagEncoderTest.java
@@ -290,6 +290,42 @@ public class BikeFlagEncoderTest extends AbstractBikeFlagEncoderTester {
         assertEquals(18, encoder.getSpeed(way));
     }
 
+    @Test
+    public void testSmoothness() {
+        ReaderWay way = new ReaderWay(1);
+        way.setTag("highway", "residential");
+        way.setTag("smoothness", "excellent");
+        assertEquals(20, getSpeedFromFlags(way), 0.01);
+
+        way.setTag("smoothness", "bad");
+        assertEquals(14, getSpeedFromFlags(way), 0.01);
+
+        way.setTag("smoothness", "impassable");
+        assertEquals(PUSHING_SECTION_SPEED, getSpeedFromFlags(way), 0.01);
+
+        way.setTag("smoothness", "unknown");
+        assertEquals(14, getSpeedFromFlags(way), 0.01);
+
+        way.clearTags();
+        way.setTag("highway", "residential");
+        way.setTag("surface", "ground");
+        assertEquals(12, getSpeedFromFlags(way), 0.01);
+
+        way.setTag("smoothness", "bad");
+        assertEquals(8, getSpeedFromFlags(way), 0.01);
+
+        way.clearTags();
+        way.setTag("highway", "track");
+        way.setTag("tracktype", "grade5");
+        assertEquals(PUSHING_SECTION_SPEED, getSpeedFromFlags(way), 0.01);
+
+        way.setTag("smoothness", "bad");
+        assertEquals(PUSHING_SECTION_SPEED, getSpeedFromFlags(way), 0.01);
+
+        way.setTag("smoothness", "impassable");
+        assertEquals(PUSHING_SECTION_SPEED, getSpeedFromFlags(way), 0.01);
+    }
+
     @Test
     public void testCycleway() {
         ReaderWay way = new ReaderWay(1);
diff --git a/core/src/test/java/com/graphhopper/routing/util/MountainBikeFlagEncoderTest.java b/core/src/test/java/com/graphhopper/routing/util/MountainBikeFlagEncoderTest.java
index bd9399216..6f67da261 100644
--- a/core/src/test/java/com/graphhopper/routing/util/MountainBikeFlagEncoderTest.java
+++ b/core/src/test/java/com/graphhopper/routing/util/MountainBikeFlagEncoderTest.java
@@ -20,6 +20,7 @@ package com.graphhopper.routing.util;
 import com.graphhopper.reader.ReaderNode;
 import com.graphhopper.reader.ReaderRelation;
 import com.graphhopper.reader.ReaderWay;
+import static com.graphhopper.routing.util.BikeCommonFlagEncoder.PUSHING_SECTION_SPEED;
 import com.graphhopper.storage.IntsRef;
 import com.graphhopper.util.GHUtility;
 import com.graphhopper.util.PMap;
@@ -82,6 +83,42 @@ public class MountainBikeFlagEncoderTest extends AbstractBikeFlagEncoderTester {
         assertEquals(16, encoder.getSpeed(way));
         assertPriority(PREFER.getValue(), way);
     }
+    
+    @Test
+    public void testSmoothness() {
+        ReaderWay way = new ReaderWay(1);
+        way.setTag("highway", "residential");
+        way.setTag("smoothness", "excellent");
+        assertEquals(18, getSpeedFromFlags(way), 0.01);
+
+        way.setTag("smoothness", "bad");
+        assertEquals(12, getSpeedFromFlags(way), 0.01);
+
+        way.setTag("smoothness", "impassable");
+        assertEquals(PUSHING_SECTION_SPEED, getSpeedFromFlags(way), 0.01);
+
+        way.setTag("smoothness", "unknown");
+        assertEquals(12, getSpeedFromFlags(way), 0.01);
+
+        way.clearTags();
+        way.setTag("highway", "residential");
+        way.setTag("surface", "ground");
+        assertEquals(16, getSpeedFromFlags(way), 0.01);
+
+        way.setTag("smoothness", "bad");
+        assertEquals(12, getSpeedFromFlags(way), 0.01);
+
+        way.clearTags();
+        way.setTag("highway", "track");
+        way.setTag("tracktype", "grade5");
+        assertEquals(6, getSpeedFromFlags(way), 0.01);
+
+        way.setTag("smoothness", "bad");
+        assertEquals(PUSHING_SECTION_SPEED, getSpeedFromFlags(way), 0.01);
+
+        way.setTag("smoothness", "impassable");
+        assertEquals(PUSHING_SECTION_SPEED, getSpeedFromFlags(way), 0.01);
+    }
 
     @Test
     @Override
diff --git a/core/src/test/java/com/graphhopper/routing/util/RacingBikeFlagEncoderTest.java b/core/src/test/java/com/graphhopper/routing/util/RacingBikeFlagEncoderTest.java
index 92dd72143..47ec90058 100644
--- a/core/src/test/java/com/graphhopper/routing/util/RacingBikeFlagEncoderTest.java
+++ b/core/src/test/java/com/graphhopper/routing/util/RacingBikeFlagEncoderTest.java
@@ -123,7 +123,44 @@ public class RacingBikeFlagEncoderTest extends AbstractBikeFlagEncoderTester {
         way.setTag("highway", "primary");
         way.setTag("surface", "unknownpavement");
         assertEquals(PUSHING_SECTION_SPEED, getSpeedFromFlags(way), 1e-1);
+    }
+
+    @Test
+    public void testSmoothness() {
+        ReaderWay way = new ReaderWay(1);
+        way.setTag("highway", "residential");
+        assertEquals(16, getSpeedFromFlags(way), 0.01);
+
+        way.setTag("smoothness", "excellent");
+        assertEquals(20, getSpeedFromFlags(way), 0.01);
+
+        way.setTag("smoothness", "bad");
+        assertEquals(12, getSpeedFromFlags(way), 0.01);
+
+        way.setTag("smoothness", "impassable");
+        assertEquals(PUSHING_SECTION_SPEED, getSpeedFromFlags(way), 0.01);
+        
+        way.setTag("smoothness", "unknown");
+        assertEquals(12, getSpeedFromFlags(way), 0.01);
+
+        way.clearTags();
+        way.setTag("highway", "residential");
+        way.setTag("surface", "ground");
+        assertEquals(2, getSpeedFromFlags(way), 0.01);
+
+        way.setTag("smoothness", "bad");
+        assertEquals(2, getSpeedFromFlags(way), 0.01);
+
+        way.clearTags();
+        way.setTag("highway", "track");
+        way.setTag("tracktype", "grade5");
+        assertEquals(PUSHING_SECTION_SPEED, getSpeedFromFlags(way), 0.01);
+
+        way.setTag("smoothness", "bad");
+        assertEquals(PUSHING_SECTION_SPEED, getSpeedFromFlags(way), 0.01);
 
+        way.setTag("smoothness", "impassable");
+        assertEquals(PUSHING_SECTION_SPEED, getSpeedFromFlags(way), 0.01);
     }
 
     @Test
diff --git a/core/src/test/java/com/graphhopper/routing/util/parsers/OSMSmoothnessParserTest.java b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMSmoothnessParserTest.java
new file mode 100644
index 000000000..387346d54
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMSmoothnessParserTest.java
@@ -0,0 +1,41 @@
+package com.graphhopper.routing.util.parsers;
+
+import com.graphhopper.reader.ReaderWay;
+import com.graphhopper.routing.ev.EnumEncodedValue;
+import com.graphhopper.routing.ev.Smoothness;
+import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.storage.IntsRef;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class OSMSmoothnessParserTest {
+    private EncodingManager em;
+    private EnumEncodedValue<Smoothness> smoothnessEnc;
+    private OSMSmoothnessParser parser;
+
+    @Before
+    public void setUp() {
+        parser = new OSMSmoothnessParser();
+        em = new EncodingManager.Builder().add(parser).build();
+        smoothnessEnc = em.getEnumEncodedValue(Smoothness.KEY, Smoothness.class);
+    }
+
+    @Test
+    public void testSimpleTags() {
+        IntsRef relFlags = em.createRelationFlags();
+
+        ReaderWay readerWay = new ReaderWay(1);
+        IntsRef intsRef = em.createEdgeFlags();
+        readerWay.setTag("highway", "primary");
+        parser.handleWayTags(intsRef, readerWay, false, relFlags);
+        assertEquals(Smoothness.MISSING, smoothnessEnc.getEnum(false, intsRef));
+
+        readerWay.setTag("smoothness", "bad");
+        parser.handleWayTags(intsRef, readerWay, false, relFlags);
+        assertEquals(Smoothness.BAD, smoothnessEnc.getEnum(false, intsRef));
+        assertTrue(Smoothness.BAD.ordinal() < Smoothness.VERY_BAD.ordinal());
+    }
+}
\ No newline at end of file