Repository: graphhopper/graphhopper
Pull request #2158 (2020-10-17 10:58:24)

Details support for mtb_scale, mtb_uphill_scale, sac_scale and horse_scale
Adds support for mtb:scale, mtb:scale:uphill, sac_scale and horse_scale as enum encoded details.

This PR replaces #1812 since that one hasn't been worked on for a long time and I couldn't get permission for the repository to finish it.

If the implementation is accepted like this in general, I will add a few tests too!

karussell: Thanks for your contribution. My biggest problem with the PR before was the naming and unit. IMO it is confusing to name something xy_scale and then have different conventions and names (HorseScale:NONE,DEMANDING,..., MtbScale:NONE, MTB_0,..., SacScale: NONE, HIKING, ...). I know it is done in OSM but encoded values are not 1:1 copies of OSM tags in general because otherwise e.g. users of the encoded values would have to convert strings like from `maxspeed` into numbers and similar issues.

The encoded values that we store should be thought via its usage. E.g. when it comes to the CustomWeighting this would be simpler:

```yml
priority:
  some_scale:
    "< 5": 0.5
```

than:

```yml
priority:
  some_scale:
    none: 0.5
    name1: 0.5
    name2: 0.5
    name2: 0.5
```
boldtrn: We could use -1 for "none" in this case?
joe-akeem: Not quite sure if I understood you right @karussell 🤔 Is it just about the names? Would it be better to name them `mtb_rating` and `sac_rating` or similar when using `EnumEncodedValue` instead of `IntEncodedValue`?

I understand that it would be better to use `IntEncodedValue` instead of `EnumEncodedValue` because it is easier to compare e.g. with the `<` operator as pointed out above. However, I encountered a problem with using `IntEncodedValue`:

* the default of an `IntEncodedValue` is always `0`. So if the tag is missing, we would automatically end up with  `mtb_scale=0` in Graphhopper. (That's why unfortunately `-1` is not working for "none" @boldtrn)
* According to https://wiki.openstreetmap.org/wiki/Key:mtb:scale however, `0` is the lowest possible MTB Scale which is not the same as "none"!

Of course we could have another interpretation for the OSM `mtb:scale` tag in Graphopper like
* `0` means "none" in Graphhopper while in OSM it means "Difficulty S0"
* `1` means "S0" in  Graphhopper while in OSM it means "Difficulty S1"
* ...

This however I would find very confusing since I would always also look at https://wiki.openstreetmap.org/wiki/Key:mtb:scale to get an idea of how to interpret the values.

The problem only exists with `mtb:scale` and `mtb:scale:uphill` btw. since for them `0` is a valid value in OSM. `sac:scale` starts with "T1" which could be mapped to `IntEncodedValue` value `1` easily and `0` could be used for "none". `horse_scale` is a different story again because it doesn't use numbers in OSM. So we could leave this as an `EnumEncodedValue` or assign some numbers to the enums. This would be less confusing since it is then totally different from OSM.

I'm happy to change the PR but I couldn't think of a better solution that is not confusing to the user when dealing with `mtb_scale` as described above. Any proposals?
karussell: > Would it be better to name them mtb_rating and sac_rating or similar when using EnumEncodedValue instead of IntEncodedValue?

One problem I have is naming, yes. All are named "xy_scale" although they mean slightly different things. So we could give them a different name or use the same values for all (I prefer this but names and values would then be different to OSM although we would preserve the meaning).

> So if the tag is missing, we would automatically end up with mtb_scale=0 in Graphhopper

A missing tag could be detected in the parser and set to any value. (Only for custom scenarios where the graph is used without OSMParsers the default value of 0 would kick in and could make problems.)

> This however I would find very confusing since I would always also look at https://wiki.openstreetmap.org/wiki/Key:mtb:scale to get an idea of how to interpret the values.

If the meaning is changed we must rename the encoded value IMO.

I think we should try to find a more consistent handling, so that our custom weighting can be simpler and more intuitive. E.g. we could define 0 means "missing", and then a value from 1 to 6? Then we can call every encoded values the same like `xy_rating` and nothing should be mixed up with OSM. 

If enums are better or more expressive this would work too (also due to `some_enum.ordinal()`). Here I would still favor to use the same enum for all (mtb, sac, horse etc) which we can name `Rating` to make it clear that it is different to OSM.

See e.g. the RouteNetwork enum that is used for biking and hiking routes and it is ordered from biggest to smallest route.

> horse_scale is a different story again because it doesn't use numbers in OSM

Sure, but is this different for historical reasons or does the difference has a real benefit?

> assign some numbers to the enums.

btw: in Java every enum already has a number (`some_enum.ordinal()`).
boldtrn: > the default of an IntEncodedValue is always 0. So if the tag is missing, we would automatically end up with mtb_scale=0 in Graphhopper. (That's why unfortunately -1 is not working for "none" @boldtrn)

If one sets the value for every edge, this could work, but yes, that not really nice.
joe-akeem: I renamed the enums from `*Scale` to `*Rating` and made the values more uniform. They are also parsed to `int` so they can be weighed with a range as described above.
karussell: Thanks! 

I've tried a common Rating enum, see here https://github.com/graphhopper/graphhopper/pull/2158/commits/4070658a17e40342c278683987c36e79724c086f and then replaced this with an int which I currently prefer: https://github.com/graphhopper/graphhopper/pull/2158/commits/94a34bfd2eaf0ea32cd12396dedb29067567501b

Do you think this could work too?

BTW: I removed MtbUphillRating for now as we do not have elevation changes at the moment which would be necessary for it to be useful. So this would require some more work in a separate PR.
joe-akeem: > Do you think this could work too?

Sure 👍 Although one thing I learnt while playing with this yesterday is that it currently seems not possible to have multiple ranges with `int`. So I think I couldn't set up a profile for my mountain biking preferences which are "I really want everything S2 and below, S3 is OK and everything above is too hard and should be avoided" 🤔 However, it seems like this would work and that should be enough for now:
``` yaml
priority:
  some_scale:
    "< 3": 1
    "*": 0.5
```
karussell: > Sure 

:+1: 

> However, it seems like this would work and that should be enough for now

Yes, this is the way this currently works. However, if everything goes well, we will have a bigger (breaking) change for the custom weighting soon that would allow all boolean and comparison operators.

Patch:
diff --git a/config-example.yml b/config-example.yml
index 22663b6eb..d400bfd6d 100644
--- a/config-example.yml
+++ b/config-example.yml
@@ -17,7 +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
+  # 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
   # graph.encoded_values: surface,toll,track_type
 
   ##### Routing Profiles ####
diff --git a/core/src/main/java/com/graphhopper/routing/ev/HikeRating.java b/core/src/main/java/com/graphhopper/routing/ev/HikeRating.java
new file mode 100644
index 000000000..b8348ede2
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/ev/HikeRating.java
@@ -0,0 +1,9 @@
+package com.graphhopper.routing.ev;
+
+public class HikeRating {
+    public static final String KEY = "hike_rating";
+
+    public static IntEncodedValue create() {
+        return new UnsignedIntEncodedValue(KEY, 3, false);
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/routing/ev/HorseRating.java b/core/src/main/java/com/graphhopper/routing/ev/HorseRating.java
new file mode 100644
index 000000000..5fcbed99f
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/ev/HorseRating.java
@@ -0,0 +1,9 @@
+package com.graphhopper.routing.ev;
+
+public class HorseRating {
+    public static final String KEY = "horse_rating";
+
+    public static IntEncodedValue create() {
+        return new UnsignedIntEncodedValue(KEY, 3, false);
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/routing/ev/MtbRating.java b/core/src/main/java/com/graphhopper/routing/ev/MtbRating.java
new file mode 100644
index 000000000..71d98b53f
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/ev/MtbRating.java
@@ -0,0 +1,9 @@
+package com.graphhopper.routing.ev;
+
+public class MtbRating {
+    public static final String KEY = "mtb_rating";
+
+    public static IntEncodedValue create() {
+        return new UnsignedIntEncodedValue(KEY, 3, false);
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/routing/util/CarFlagEncoder.java b/core/src/main/java/com/graphhopper/routing/util/CarFlagEncoder.java
index 66b2d905b..1980def84 100644
--- a/core/src/main/java/com/graphhopper/routing/util/CarFlagEncoder.java
+++ b/core/src/main/java/com/graphhopper/routing/util/CarFlagEncoder.java
@@ -75,6 +75,7 @@ public class CarFlagEncoder extends AbstractFlagEncoder {
         setSpeedTwoDirections(properties.getBool("speed_two_directions", false));
 
         intendedValues.add("yes");
+        intendedValues.add("designated");
         intendedValues.add("permissive");
 
         potentialBarriers.add("gate");
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 ade86b7b2..0a5f53645 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
@@ -64,6 +64,12 @@ public class DefaultTagParserFactory implements TagParserFactory {
             return new OSMHazmatTunnelParser();
         else if (name.equals(HazmatWater.KEY))
             return new OSMHazmatWaterParser();
+        else if (name.equals(MtbRating.KEY))
+            return new OSMMtbRatingParser();
+        else if (name.equals(HikeRating.KEY))
+            return new OSMHikeRatingParser();
+        else if (name.equals(HorseRating.KEY))
+            return new OSMHorseRatingParser();
         else if (name.equals(Country.KEY))
             throw new IllegalArgumentException("The property spatial_rules.borders_directory is required in the configuration " +
                     "when using 'country' in encoded_values");
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMHikeRatingParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMHikeRatingParser.java
new file mode 100644
index 000000000..cf4191296
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMHikeRatingParser.java
@@ -0,0 +1,64 @@
+/*
+ *  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.HikeRating;
+import com.graphhopper.routing.ev.IntEncodedValue;
+import com.graphhopper.storage.IntsRef;
+
+import java.util.List;
+
+/**
+ * Parses the hiking difficulty. Where hiking corresponds to 1, mountain_hiking to 2 until 6.
+ *
+ * @see <a href="https://wiki.openstreetmap.org/wiki/Key:sac_scale">Key:sac_scale</a> for details on OSM hiking difficulties.
+ */
+public class OSMHikeRatingParser implements TagParser {
+
+    private final IntEncodedValue sacScaleEnc;
+
+    public OSMHikeRatingParser() {
+        this.sacScaleEnc = HikeRating.create();
+    }
+
+    @Override
+    public void createEncodedValues(EncodedValueLookup lookup, List<EncodedValue> link) {
+        link.add(sacScaleEnc);
+    }
+
+    @Override
+    public IntsRef handleWayTags(IntsRef edgeFlags, ReaderWay readerWay, boolean ferry, IntsRef relationFlags) {
+        String scale = readerWay.getTag("sac_scale");
+        int rating = 0;
+        if (scale != null) {
+            if (scale.equals("hiking")) rating = 1;
+            else if (scale.equals("mountain_hiking")) rating = 2;
+            else if (scale.equals("demanding_mountain_hiking")) rating = 3;
+            else if (scale.equals("alpine_hiking")) rating = 4;
+            else if (scale.equals("demanding_alpine_hiking")) rating = 5;
+            else if (scale.equals("difficult_alpine_hiking")) rating = 6;
+        }
+        if (rating != 0)
+            sacScaleEnc.setInt(false, edgeFlags, rating);
+
+        return edgeFlags;
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMHorseRatingParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMHorseRatingParser.java
new file mode 100644
index 000000000..f05599bf5
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMHorseRatingParser.java
@@ -0,0 +1,63 @@
+/*
+ *  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.HorseRating;
+import com.graphhopper.routing.ev.IntEncodedValue;
+import com.graphhopper.storage.IntsRef;
+
+import java.util.List;
+
+/**
+ * Parses the horseback riding difficulty. Where common is mapped to 1, demanding to 2 until 6
+ *
+ * @see <a href="https://wiki.openstreetmap.org/wiki/Key:horse_scale">Key:horse_scale</a> for details on horseback riding difficulties.
+ */
+public class OSMHorseRatingParser implements TagParser {
+
+    private final IntEncodedValue horseScale;
+
+    public OSMHorseRatingParser() {
+        this.horseScale = HorseRating.create();
+    }
+
+    @Override
+    public void createEncodedValues(EncodedValueLookup lookup, List<EncodedValue> link) {
+        link.add(horseScale);
+    }
+
+    @Override
+    public IntsRef handleWayTags(IntsRef edgeFlags, ReaderWay readerWay, boolean ferry, IntsRef relationFlags) {
+        String scale = readerWay.getTag("horse_scale");
+        int rating = 0;
+        if (scale != null) {
+            if (scale.equals("common")) rating = 1;
+            else if (scale.equals("demanding")) rating = 2;
+            else if (scale.equals("difficult")) rating = 3;
+            else if (scale.equals("critical")) rating = 4;
+            else if (scale.equals("dangerous")) rating = 5;
+            else if (scale.equals("impossible")) rating = 6;
+        }
+        if (rating != 0)
+            horseScale.setInt(false, edgeFlags, rating);
+        return edgeFlags;
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMtbRatingParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMtbRatingParser.java
new file mode 100644
index 000000000..ab484a787
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMtbRatingParser.java
@@ -0,0 +1,68 @@
+/*
+ *  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.IntEncodedValue;
+import com.graphhopper.routing.ev.MtbRating;
+import com.graphhopper.storage.IntsRef;
+
+import java.util.List;
+
+/**
+ * Parses the mountain biking difficulty.
+ * A mapping mtb:scale=0 corresponds to 1 and mtb:scale=1 to 2 until 7.
+ *
+ * @see <a href="https://wiki.openstreetmap.org/wiki/Key:mtb:scale">Key:mtb:scale</a> for details on OSM mountain biking difficulties.
+ * @see <a href=""http://www.singletrail-skala.de/>Single Trail Scale</a>
+ */
+public class OSMMtbRatingParser implements TagParser {
+
+    private final IntEncodedValue mtbRatingEnc;
+
+    public OSMMtbRatingParser() {
+        this.mtbRatingEnc = MtbRating.create();
+    }
+
+    @Override
+    public void createEncodedValues(EncodedValueLookup lookup, List<EncodedValue> link) {
+        link.add(mtbRatingEnc);
+    }
+
+    @Override
+    public IntsRef handleWayTags(IntsRef edgeFlags, ReaderWay readerWay, boolean ferry, IntsRef relationFlags) {
+        String scale = readerWay.getTag("mtb:scale");
+        int rating = 0;
+        if (scale != null) {
+            if (scale.length() == 2 && (scale.charAt(1) == '+' || scale.charAt(1) == '-'))
+                scale = scale.substring(0, 1);
+            try {
+                int scaleAsInt = Integer.parseInt(scale);
+                rating = scaleAsInt + 1;
+            } catch (Exception ex) {
+            }
+        }
+
+        if (rating != 0)
+            mtbRatingEnc.setInt(false, edgeFlags, rating);
+
+        return edgeFlags;
+    }
+}
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 b12ab51d1..97fdba964 100644
--- a/core/src/main/java/com/graphhopper/util/details/PathDetailsBuilderFactory.java
+++ b/core/src/main/java/com/graphhopper/util/details/PathDetailsBuilderFactory.java
@@ -17,14 +17,12 @@
  */
 package com.graphhopper.util.details;
 
-import com.graphhopper.coll.MapEntry;
 import com.graphhopper.routing.ev.*;
 import com.graphhopper.routing.weighting.Weighting;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Map;
 
 import static com.graphhopper.routing.util.EncodingManager.getKey;
 import static com.graphhopper.util.Parameters.Details.*;
@@ -76,15 +74,15 @@ public class PathDetailsBuilderFactory {
                 builders.add(new BooleanDetails(key, evl.getBooleanEncodedValue(key)));
         }
 
-        for (Map.Entry entry : Arrays.asList(new MapEntry<>(RoadClass.KEY, RoadClass.class),
-                new MapEntry<>(RoadEnvironment.KEY, RoadEnvironment.class), new MapEntry<>(Surface.KEY, Surface.class),
-                new MapEntry<>(RoadAccess.KEY, RoadAccess.class), new MapEntry<>(Toll.KEY, Toll.class),
-                new MapEntry<>(TrackType.KEY, TrackType.class), new MapEntry<>(Hazmat.KEY, Hazmat.class),
-                new MapEntry<>(HazmatTunnel.KEY, HazmatTunnel.class), new MapEntry<>(HazmatWater.KEY, HazmatWater.class),
-                new MapEntry<>(Country.KEY, Country.class))) {
-            String key = (String) entry.getKey();
+        for (String key : Arrays.asList(RoadClass.KEY, RoadEnvironment.KEY, Surface.KEY, RoadAccess.KEY,
+                Toll.KEY, TrackType.KEY, Hazmat.KEY, HazmatTunnel.KEY, HazmatWater.KEY, Country.KEY)) {
             if (requestedPathDetails.contains(key) && evl.hasEncodedValue(key))
-                builders.add(new EnumDetails(key, evl.getEnumEncodedValue(key, (Class<Enum>) entry.getValue())));
+                builders.add(new EnumDetails<>(key, evl.getEnumEncodedValue(key, Enum.class)));
+        }
+
+        for (String key : Arrays.asList(MtbRating.KEY, HikeRating.KEY, HorseRating.KEY)) {
+            if (requestedPathDetails.contains(key) && evl.hasEncodedValue(key))
+                builders.add(new IntDetails(key, evl.getIntEncodedValue(key)));
         }
 
         if (requestedPathDetails.size() != builders.size()) {


Test patch:
diff --git a/core/src/test/java/com/graphhopper/routing/util/CarFlagEncoderTest.java b/core/src/test/java/com/graphhopper/routing/util/CarFlagEncoderTest.java
index 08a199f93..dc809ebe0 100644
--- a/core/src/test/java/com/graphhopper/routing/util/CarFlagEncoderTest.java
+++ b/core/src/test/java/com/graphhopper/routing/util/CarFlagEncoderTest.java
@@ -90,24 +90,6 @@ public class CarFlagEncoderTest {
         way.setTag("motorcar", "yes");
         assertTrue(encoder.getAccess(way).isWay());
 
-        way.clearTags();
-        way.setTag("route", "ferry");
-        assertTrue(encoder.getAccess(way).isFerry());
-        way.setTag("motorcar", "no");
-        assertTrue(encoder.getAccess(way).canSkip());
-
-        way.clearTags();
-        way.setTag("route", "ferry");
-        way.setTag("foot", "yes");
-        assertTrue(encoder.getAccess(way).canSkip());
-
-        way.clearTags();
-        way.setTag("route", "ferry");
-        way.setTag("access", "no");
-        assertTrue(encoder.getAccess(way).canSkip());
-        way.setTag("vehicle", "yes");
-        assertTrue(encoder.getAccess(way).isFerry());
-
         way.clearTags();
         way.setTag("access", "yes");
         way.setTag("motor_vehicle", "no");
@@ -487,8 +469,11 @@ public class CarFlagEncoderTest {
         way.setTag("railway", "tram");
         // but allow tram to be on the same way
         assertTrue(encoder.getAccess(way).isWay());
+    }
 
-        way = new ReaderWay(1);
+    @Test
+    public void testFerry() {
+        ReaderWay way = new ReaderWay(1);
         way.setTag("route", "shuttle_train");
         way.setTag("motorcar", "yes");
         way.setTag("bicycle", "no");
@@ -527,6 +512,30 @@ public class CarFlagEncoderTest {
         assertTrue(encoder.getAccess(way).isFerry());
         // We have ignored the unrealisitc long duration and take the unknown speed
         assertEquals(2.5, encoder.getFerrySpeed(way), 1e-1);
+
+        way.clearTags();
+        way.setTag("route", "ferry");
+        assertTrue(encoder.getAccess(way).isFerry());
+        way.setTag("motorcar", "no");
+        assertTrue(encoder.getAccess(way).canSkip());
+
+        way.clearTags();
+        way.setTag("route", "ferry");
+        way.setTag("foot", "yes");
+        assertTrue(encoder.getAccess(way).canSkip());
+
+        way.clearTags();
+        way.setTag("route", "ferry");
+        way.setTag("foot", "designated");
+        way.setTag("motor_vehicle", "designated");
+        assertTrue(encoder.getAccess(way).isFerry());
+
+        way.clearTags();
+        way.setTag("route", "ferry");
+        way.setTag("access", "no");
+        assertTrue(encoder.getAccess(way).canSkip());
+        way.setTag("vehicle", "yes");
+        assertTrue(encoder.getAccess(way).isFerry());
     }
 
     @Test