Repository: graphhopper/graphhopper
Pull request #1786 (2019-11-11 14:33:48)

Add support for EU tunnel codes
This is a followup PR for the discussion in #1711 which adds the ability to honor restrictions on the transport of hazardous goods through tunnels.

Changes so far:

- added support for alternative tagging schemas `hazmat:adr_tunnel_cat` and `hazmat:tunnel_cat`
- check for the presence of `tunnel=yes` when dealing with hazmat subtags like `hazmat:B` to avoid collisions with tags in [Finland](https://wiki.openstreetmap.org/wiki/Key:hazmat#Finland).

You can run the following overpass turbo query to visualize the ways with supported tag combinations:
```
[out:json][timeout:25];
// gather results
(
  way["hazmat:tunnel_cat"];
  way["hazmat:adr_tunnel_cat"];
  way["hazmat:A"="no"]["tunnel"="yes"];
  way["hazmat:B"="no"]["tunnel"="yes"];
  way["hazmat:C"="no"]["tunnel"="yes"];
  way["hazmat:D"="no"]["tunnel"="yes"];
  way["hazmat:E"="no"]["tunnel"="yes"];
);
// print results
out body;
>;
out skel qt;
```

![ overpass turbo](https://user-images.githubusercontent.com/22315436/68594001-ee6dcc00-0496-11ea-82a6-df02d83f9dd9.png)
![grafik](https://user-images.githubusercontent.com/22315436/68595070-14946b80-0499-11ea-9575-d041d276b1ec.png)




Patch:
diff --git a/config-example.yml b/config-example.yml
index 5f52e6428..cc106f002 100644
--- a/config-example.yml
+++ b/config-example.yml
@@ -15,7 +15,7 @@ graphhopper:
 
   # Add additional information to every edge. Used for path details.
   # If road_environment is added and elevation is enabled then also a tunnel and bridge interpolation is done, see #798.
-  # More options are: surface,max_width,max_height,max_weight,max_axle_load,max_length,hazmat,hazmat_water,toll,track_type
+  # More options are: surface,max_width,max_height,max_weight,max_axle_load,max_length,hazmat,hazmat_tunnel,hazmat_water,toll,track_type
   graph.encoded_values: road_class,road_class_link,road_environment,max_speed,road_access
 
   ##### Elevation #####
diff --git a/core/src/main/java/com/graphhopper/routing/profiles/DefaultEncodedValueFactory.java b/core/src/main/java/com/graphhopper/routing/profiles/DefaultEncodedValueFactory.java
index 2fdea6d57..45216ef11 100644
--- a/core/src/main/java/com/graphhopper/routing/profiles/DefaultEncodedValueFactory.java
+++ b/core/src/main/java/com/graphhopper/routing/profiles/DefaultEncodedValueFactory.java
@@ -61,6 +61,8 @@ public class DefaultEncodedValueFactory implements EncodedValueFactory {
             enc = new EnumEncodedValue<>(TrackType.KEY, TrackType.class);
         } else if (Hazmat.KEY.equals(name)) {
             enc = new EnumEncodedValue<>(Hazmat.KEY, Hazmat.class);
+        } else if (HazmatTunnel.KEY.equals(name)) {
+            enc = new EnumEncodedValue<>(HazmatTunnel.KEY, HazmatTunnel.class);
         } else if (HazmatWater.KEY.equals(name)) {
             enc = new EnumEncodedValue<>(HazmatWater.KEY, HazmatWater.class);
         } else {
diff --git a/core/src/main/java/com/graphhopper/routing/profiles/HazmatTunnel.java b/core/src/main/java/com/graphhopper/routing/profiles/HazmatTunnel.java
new file mode 100644
index 000000000..bde0d8fb6
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/profiles/HazmatTunnel.java
@@ -0,0 +1,33 @@
+package com.graphhopper.routing.profiles;
+
+/**
+ * Defines the degree of restriction for the transport of hazardous goods through tunnels.<br>
+ * If not tagged it will be {@link #A}
+ * 
+ * @see https://wiki.openstreetmap.org/wiki/Key:hazmat#Tunnel_restrictions
+ */
+public enum HazmatTunnel {
+    /** driving with any dangerous goods allowed */
+    A("A"),
+    /** no goods with very large explosion range */
+    B("B"),
+    /** no goods with large explosion or poisoning range */
+    C("C"),
+    /** no goods which threaten a large explosion, poisoning or fire */
+    D("D"),
+    /** forbids all dangerous goods except: UN 2919,3291, 3331, 3359, 3373 */
+    E("E");
+
+    public static final String KEY = "hazmat_tunnel";
+
+    private final String name;
+
+    HazmatTunnel(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+}
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 8ad25f43a..999d788a0 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
@@ -60,6 +60,8 @@ public class DefaultTagParserFactory implements TagParserFactory {
             return new OSMTrackTypeParser();
         else if (name.equals(Hazmat.KEY))
             return new OSMHazmatParser();
+        else if (name.equals(HazmatTunnel.KEY))
+            return new OSMHazmatTunnelParser();
         else if (name.equals(HazmatWater.KEY))
             return new OSMHazmatWaterParser();
         throw new IllegalArgumentException("entry in encoder list not supported " + name);
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMHazmatTunnelParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMHazmatTunnelParser.java
new file mode 100644
index 000000000..667ba8692
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMHazmatTunnelParser.java
@@ -0,0 +1,58 @@
+package com.graphhopper.routing.util.parsers;
+
+import java.util.List;
+
+import com.graphhopper.reader.ReaderWay;
+import com.graphhopper.routing.profiles.EncodedValue;
+import com.graphhopper.routing.profiles.EncodedValueLookup;
+import com.graphhopper.routing.profiles.EnumEncodedValue;
+import com.graphhopper.routing.profiles.HazmatTunnel;
+import com.graphhopper.routing.util.EncodingManager.Access;
+import com.graphhopper.storage.IntsRef;
+
+public class OSMHazmatTunnelParser implements TagParser {
+    
+    private static final String[] TUNNEL_CATEGORY_NAMES;
+    static {
+        HazmatTunnel[] categories = HazmatTunnel.values();
+        TUNNEL_CATEGORY_NAMES = new String[categories.length];
+        for (int i = 0; i < categories.length; i++) {
+            TUNNEL_CATEGORY_NAMES[i] = categories[i].name();
+        }
+    }
+    
+    private final EnumEncodedValue<HazmatTunnel> hazTunnelEnc;
+    
+    public OSMHazmatTunnelParser() {
+        this.hazTunnelEnc = new EnumEncodedValue<>(HazmatTunnel.KEY, HazmatTunnel.class);
+    }
+
+    @Override
+    public void createEncodedValues(EncodedValueLookup lookup,
+                    List<EncodedValue> registerNewEncodedValue) {
+        registerNewEncodedValue.add(hazTunnelEnc);
+    }
+
+    @Override
+    public IntsRef handleWayTags(IntsRef edgeFlags, ReaderWay readerWay, Access access,
+                    long relationFlags) {
+        
+        if (readerWay.hasTag("hazmat:adr_tunnel_cat", TUNNEL_CATEGORY_NAMES)) {
+            HazmatTunnel code = HazmatTunnel.valueOf(readerWay.getTag("hazmat:adr_tunnel_cat"));
+            hazTunnelEnc.setEnum(false, edgeFlags, code);
+        } else if (readerWay.hasTag("hazmat:tunnel_cat", TUNNEL_CATEGORY_NAMES)) {
+            HazmatTunnel code = HazmatTunnel.valueOf(readerWay.getTag("hazmat:tunnel_cat"));
+            hazTunnelEnc.setEnum(false, edgeFlags, code);
+        } else if (readerWay.hasTag("tunnel", "yes")) {
+            HazmatTunnel[] codes = HazmatTunnel.values();
+            for (int i = codes.length - 1; i >= 0; i--) {
+                if (readerWay.hasTag("hazmat:" + codes[i].name(), "no")) {
+                    hazTunnelEnc.setEnum(false, edgeFlags, codes[i]);
+                    break;
+                }
+            }
+        }
+
+        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 13d24fad8..a6aff6d77 100644
--- a/core/src/main/java/com/graphhopper/util/details/PathDetailsBuilderFactory.java
+++ b/core/src/main/java/com/graphhopper/util/details/PathDetailsBuilderFactory.java
@@ -73,7 +73,7 @@ public class PathDetailsBuilderFactory {
                 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<>(HazmatWater.KEY, HazmatWater.class),
+                new MapEntry<>(HazmatTunnel.KEY, HazmatTunnel.class), new MapEntry<>(HazmatWater.KEY, HazmatWater.class),
                 new MapEntry<>(Country.KEY, Country.class))) {
             String key = (String) entry.getKey();
             if (requestedPathDetails.contains(key) && encoder.hasEncodedValue(key))


Test patch:
diff --git a/core/src/test/java/com/graphhopper/routing/util/parsers/OSMHazmatTunnelParserTest.java b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMHazmatTunnelParserTest.java
new file mode 100644
index 000000000..26aade8aa
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMHazmatTunnelParserTest.java
@@ -0,0 +1,179 @@
+package com.graphhopper.routing.util.parsers;
+
+import static com.graphhopper.routing.util.EncodingManager.Access.WAY;
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.graphhopper.reader.ReaderWay;
+import com.graphhopper.routing.profiles.EnumEncodedValue;
+import com.graphhopper.routing.profiles.HazmatTunnel;
+import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.storage.IntsRef;
+
+public class OSMHazmatTunnelParserTest {
+
+    private EncodingManager em;
+    private EnumEncodedValue<HazmatTunnel> hazTunnelEnc;
+    private OSMHazmatTunnelParser parser;
+
+    @Before
+    public void setUp() {
+        parser = new OSMHazmatTunnelParser();
+        em = new EncodingManager.Builder().add(parser).build();
+        hazTunnelEnc = em.getEnumEncodedValue(HazmatTunnel.KEY, HazmatTunnel.class);
+    }
+
+    @Test
+    public void testADRTunnelCat() {
+        IntsRef intsRef = em.createEdgeFlags();
+        ReaderWay readerWay = new ReaderWay(1);
+        readerWay.setTag("hazmat:adr_tunnel_cat", "A");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.A, hazTunnelEnc.getEnum(false, intsRef));
+        
+        intsRef = em.createEdgeFlags();
+        readerWay = new ReaderWay(1);
+        readerWay.setTag("hazmat:adr_tunnel_cat", "B");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.B, hazTunnelEnc.getEnum(false, intsRef));
+
+        intsRef = em.createEdgeFlags();
+        readerWay = new ReaderWay(1);
+        readerWay.setTag("hazmat:adr_tunnel_cat", "C");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.C, hazTunnelEnc.getEnum(false, intsRef));
+
+        intsRef = em.createEdgeFlags();
+        readerWay = new ReaderWay(1);
+        readerWay.setTag("hazmat:adr_tunnel_cat", "D");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.D, hazTunnelEnc.getEnum(false, intsRef));
+
+        intsRef = em.createEdgeFlags();
+        readerWay = new ReaderWay(1);
+        readerWay.setTag("hazmat:adr_tunnel_cat", "E");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.E, hazTunnelEnc.getEnum(false, intsRef));
+    }
+
+    @Test
+    public void testTunnelCat() {
+        IntsRef intsRef = em.createEdgeFlags();
+        ReaderWay readerWay = new ReaderWay(1);
+        readerWay.setTag("hazmat:tunnel_cat", "A");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.A, hazTunnelEnc.getEnum(false, intsRef));
+        
+        intsRef = em.createEdgeFlags();
+        readerWay = new ReaderWay(1);
+        readerWay.setTag("hazmat:tunnel_cat", "B");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.B, hazTunnelEnc.getEnum(false, intsRef));
+
+        intsRef = em.createEdgeFlags();
+        readerWay = new ReaderWay(1);
+        readerWay.setTag("hazmat:tunnel_cat", "C");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.C, hazTunnelEnc.getEnum(false, intsRef));
+
+        intsRef = em.createEdgeFlags();
+        readerWay = new ReaderWay(1);
+        readerWay.setTag("hazmat:tunnel_cat", "D");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.D, hazTunnelEnc.getEnum(false, intsRef));
+
+        intsRef = em.createEdgeFlags();
+        readerWay = new ReaderWay(1);
+        readerWay.setTag("hazmat:tunnel_cat", "E");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.E, hazTunnelEnc.getEnum(false, intsRef));
+    }
+
+    @Test
+    public void testHazmatSubtags() {
+        IntsRef intsRef = em.createEdgeFlags();
+        ReaderWay readerWay = new ReaderWay(1);
+        readerWay.setTag("tunnel", "yes");
+        readerWay.setTag("hazmat:A", "no");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.A, hazTunnelEnc.getEnum(false, intsRef));
+        
+        intsRef = em.createEdgeFlags();
+        readerWay = new ReaderWay(1);
+        readerWay.setTag("tunnel", "yes");
+        readerWay.setTag("hazmat:B", "no");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.B, hazTunnelEnc.getEnum(false, intsRef));
+
+        intsRef = em.createEdgeFlags();
+        readerWay = new ReaderWay(1);
+        readerWay.setTag("tunnel", "yes");
+        readerWay.setTag("hazmat:C", "no");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.C, hazTunnelEnc.getEnum(false, intsRef));
+
+        intsRef = em.createEdgeFlags();
+        readerWay = new ReaderWay(1);
+        readerWay.setTag("tunnel", "yes");
+        readerWay.setTag("hazmat:D", "no");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.D, hazTunnelEnc.getEnum(false, intsRef));
+
+        intsRef = em.createEdgeFlags();
+        readerWay = new ReaderWay(1);
+        readerWay.setTag("tunnel", "yes");
+        readerWay.setTag("hazmat:E", "no");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.E, hazTunnelEnc.getEnum(false, intsRef));
+    }
+    
+    @Test
+    public void testOrder() {
+        IntsRef intsRef = em.createEdgeFlags();
+        ReaderWay readerWay = new ReaderWay(1);
+        readerWay.setTag("tunnel", "yes");
+        readerWay.setTag("hazmat:A", "no");
+        readerWay.setTag("hazmat:B", "no");
+        readerWay.setTag("hazmat:C", "no");
+        readerWay.setTag("hazmat:D", "no");
+        readerWay.setTag("hazmat:E", "no");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.E, hazTunnelEnc.getEnum(false, intsRef));
+        
+        intsRef = em.createEdgeFlags();
+        readerWay = new ReaderWay(1);
+        readerWay.setTag("tunnel", "yes");
+        readerWay.setTag("hazmat:A", "no");
+        readerWay.setTag("hazmat:B", "no");
+        readerWay.setTag("hazmat:C", "no");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.C, hazTunnelEnc.getEnum(false, intsRef));
+        
+        intsRef = em.createEdgeFlags();
+        readerWay = new ReaderWay(1);
+        readerWay.setTag("tunnel", "yes");
+        readerWay.setTag("hazmat:B", "no");
+        readerWay.setTag("hazmat:E", "no");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.E, hazTunnelEnc.getEnum(false, intsRef));
+    }
+    
+    @Test
+    public void testIgnoreNonTunnelSubtags() {
+        IntsRef intsRef = em.createEdgeFlags();
+        ReaderWay readerWay = new ReaderWay(1);
+        readerWay.setTag("hazmat:B", "no");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.A, hazTunnelEnc.getEnum(false, intsRef));
+    }
+    
+    @Test
+    public void testNoNPE() {
+        ReaderWay readerWay = new ReaderWay(1);
+        IntsRef intsRef = em.createEdgeFlags();
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatTunnel.A, hazTunnelEnc.getEnum(false, intsRef));
+    }
+}
\ No newline at end of file