Repository: graphhopper/graphhopper
Pull request #1711 (2019-09-03 08:37:42)

Add support for various hazmat tags
This PR adds support for parsing `hazmat`, `hazmat:water` ~~and EU tunnel codes (`hazmat:B`, `hazmat:C`,...)~~. This brings us one step further to deliver more realistic routing for commercial vehicles.

I decided to split them up into several enums/parsers because they serve different concepts. The more general `hazmat` tag is quite clear and prohibits any kind of hazardous materials without the need to categorize the types of goods.

The `hazmat:water` tag prohibits the transport of goods which might threaten water protection areas. This is a critical feature for trucks carrying e.g oil.

~~The last addition is the support of EU tunnel codes tagged with `hazmat:B` down to `hazmat:E`. I know that they add a bit of complexity and seem to be tagged rather sparsely but they dictate which tunnels a truck is able to pass in the EU. As tunnels provide major shortcuts around natural obstacles these tags become critical because we might broadly underestimate the route for a truck.~~

Edit: Tunnel codes will be subject of a future PR as discussed with @karussell 

karussell: Thanks! Is it possible that two of these EncodedValues are set for one edge e.g. hazmat water and hazmat tunnel? Or is there a chance to combine them e.g. Hazmat=YES,TUNNEL_A,...,WATER,...?
otbutz: @karussell In a tunnel we should check first of all for existing tunnel tags as they are the most specific information we're going to get. As water protection *areas* are a bit vague i'd trust the tunnel tags more if they're present.

According to taginfo there are only 348 cases of ways being tagged with a combination of `hazmat` and `hazmat:water`. We could combine `Hazmat` and `HazmatWater` but we'd be mixing a general yes/no decision with a broadly used special case which some users may like to ignore.

I was thinking about several hazmat categories(water,explosives,...) but there are regulations targeting the type of hazard like eg `hazmat:1` (which is therefore not part of this PR) and the potential impact they expose like eg `hazmat:B`.

With the current approach it would be enough to know the restriction code of the vehicle and if the load is allowed to be carried through water protection areas.

Examples:
* Restriction code `A` and water friendly goods: no hazmat restrictions
* Restriction code `D` and water friendly goods: all tunnels with category `A`,`B` or `C` and every other way not tagged with `hazmat=no`
* Restriction code `D` and non water friendly goods: all tunnels with category `A`,`B` or `C` and every other way not tagged with `hazmat=no` or `hazmat:water=no`
karussell: Thanks for the explanation. Should the encoded values that are only for the EU prefixed with `eu`, e.g. `eu_hazmat_tunnel`? Or is this unnecessary complexity as in e.g. the US the schema is different and does not overlap?
otbutz: According to the wiki they don't overlap as US seems to use the `hazmat:1`, `hazmat:2` etc scheme.
karussell: Not really something missing, but I'm still unsure about all the new introduced encoded values. I would for now delay the inclusion in 0.13 and merge it early for 0.14-SNAPSHOT. I hope this is ok.
otbutz: No problem with that  :+1: 
karussell: There is not much missing to merge this PR. Can you remove HazmatTunnel from this PR and add "permissive" to HazmatWater?
otbutz: @karussell i updated my PR :)

Patch:
diff --git a/config-example.yml b/config-example.yml
index d547db815..5f52e6428 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,toll,track_type
+  # More options are: surface,max_width,max_height,max_weight,max_axle_load,max_length,hazmat,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 e12b6b2a0..2fdea6d57 100644
--- a/core/src/main/java/com/graphhopper/routing/profiles/DefaultEncodedValueFactory.java
+++ b/core/src/main/java/com/graphhopper/routing/profiles/DefaultEncodedValueFactory.java
@@ -59,6 +59,10 @@ public class DefaultEncodedValueFactory implements EncodedValueFactory {
             enc = new EnumEncodedValue<>(Toll.KEY, Toll.class);
         } else if (TrackType.KEY.equals(name)) {
             enc = new EnumEncodedValue<>(TrackType.KEY, TrackType.class);
+        } else if (Hazmat.KEY.equals(name)) {
+            enc = new EnumEncodedValue<>(Hazmat.KEY, Hazmat.class);
+        } else if (HazmatWater.KEY.equals(name)) {
+            enc = new EnumEncodedValue<>(HazmatWater.KEY, HazmatWater.class);
         } else {
             throw new IllegalArgumentException("DefaultEncodedValueFactory cannot find EncodedValue " + name);
         }
diff --git a/core/src/main/java/com/graphhopper/routing/profiles/Hazmat.java b/core/src/main/java/com/graphhopper/routing/profiles/Hazmat.java
new file mode 100644
index 000000000..668c1326f
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/profiles/Hazmat.java
@@ -0,0 +1,22 @@
+package com.graphhopper.routing.profiles;
+
+/**
+ * Defines general restrictions for the transport of hazardous materials.<br>
+ * If not tagged it will be {@link #YES}
+ */
+public enum Hazmat {
+    YES("yes"), NO("no");
+
+    public static final String KEY = "hazmat";
+
+    private final String name;
+
+    Hazmat(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/routing/profiles/HazmatWater.java b/core/src/main/java/com/graphhopper/routing/profiles/HazmatWater.java
new file mode 100644
index 000000000..e5fa4cb95
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/profiles/HazmatWater.java
@@ -0,0 +1,22 @@
+package com.graphhopper.routing.profiles;
+
+/**
+ * Defines general restrictions for the transport of goods through water protection areas.<br>
+ * If not tagged it will be {@link #YES}
+ */
+public enum HazmatWater {
+    YES("yes"), PERMISSIVE("permissive"), NO("no");
+
+    public static final String KEY = "hazmat_water";
+
+    private final String name;
+
+    HazmatWater(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 3dcb28530..8ad25f43a 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
@@ -58,6 +58,10 @@ public class DefaultTagParserFactory implements TagParserFactory {
             return new OSMTollParser();
         else if (name.equals(TrackType.KEY))
             return new OSMTrackTypeParser();
+        else if (name.equals(Hazmat.KEY))
+            return new OSMHazmatParser();
+        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/OSMHazmatParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMHazmatParser.java
new file mode 100644
index 000000000..105653f6d
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMHazmatParser.java
@@ -0,0 +1,36 @@
+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.Hazmat;
+import com.graphhopper.routing.util.EncodingManager.Access;
+import com.graphhopper.storage.IntsRef;
+
+public class OSMHazmatParser implements TagParser {
+    
+    private final EnumEncodedValue<Hazmat> hazEnc;
+    
+    public OSMHazmatParser() {
+        this.hazEnc = new EnumEncodedValue<>(Hazmat.KEY, Hazmat.class);
+    }
+
+    @Override
+    public void createEncodedValues(EncodedValueLookup lookup,
+                    List<EncodedValue> registerNewEncodedValue) {
+        registerNewEncodedValue.add(hazEnc);
+    }
+
+    @Override
+    public IntsRef handleWayTags(IntsRef edgeFlags, ReaderWay readerWay, Access access,
+                    long relationFlags) {
+        if (readerWay.hasTag("hazmat", "no")) {
+            hazEnc.setEnum(false, edgeFlags, Hazmat.NO);
+        }
+        return edgeFlags;
+    }
+
+}
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMHazmatWaterParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMHazmatWaterParser.java
new file mode 100644
index 000000000..dc226ebf2
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMHazmatWaterParser.java
@@ -0,0 +1,38 @@
+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.HazmatWater;
+import com.graphhopper.routing.util.EncodingManager.Access;
+import com.graphhopper.storage.IntsRef;
+
+public class OSMHazmatWaterParser implements TagParser {
+    
+    private final EnumEncodedValue<HazmatWater> hazWaterEnc;
+    
+    public OSMHazmatWaterParser() {
+        this.hazWaterEnc = new EnumEncodedValue<>(HazmatWater.KEY, HazmatWater.class);
+    }
+
+    @Override
+    public void createEncodedValues(EncodedValueLookup lookup,
+                    List<EncodedValue> registerNewEncodedValue) {
+        registerNewEncodedValue.add(hazWaterEnc);
+    }
+
+    @Override
+    public IntsRef handleWayTags(IntsRef edgeFlags, ReaderWay readerWay, Access access,
+                    long relationFlags) {
+        if (readerWay.hasTag("hazmat:water", "no")) {
+            hazWaterEnc.setEnum(false, edgeFlags, HazmatWater.NO);
+        } else if (readerWay.hasTag("hazmat:water", "permissive")) {
+            hazWaterEnc.setEnum(false, edgeFlags, HazmatWater.PERMISSIVE);
+        }
+        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 e50e4cdf8..71e9919c0 100644
--- a/core/src/main/java/com/graphhopper/util/details/PathDetailsBuilderFactory.java
+++ b/core/src/main/java/com/graphhopper/util/details/PathDetailsBuilderFactory.java
@@ -63,7 +63,9 @@ public class PathDetailsBuilderFactory {
         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<>(Country.KEY, Country.class))) {
+                new MapEntry<>(TrackType.KEY, TrackType.class), new MapEntry<>(Hazmat.KEY, Hazmat.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))
                 builders.add(new EnumDetails(key, encoder.getEnumEncodedValue(key, (Class<Enum>) entry.getValue())));


Test patch:
diff --git a/core/src/test/java/com/graphhopper/routing/util/parsers/OSMHazmatParserTest.java b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMHazmatParserTest.java
new file mode 100644
index 000000000..b3a7c8ea4
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMHazmatParserTest.java
@@ -0,0 +1,54 @@
+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.Hazmat;
+import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.storage.IntsRef;
+
+public class OSMHazmatParserTest {
+
+    private EncodingManager em;
+    private EnumEncodedValue<Hazmat> hazEnc;
+    private OSMHazmatParser parser;
+
+    @Before
+    public void setUp() {
+        parser = new OSMHazmatParser();
+        em = new EncodingManager.Builder().add(parser).build();
+        hazEnc = em.getEnumEncodedValue(Hazmat.KEY, Hazmat.class);
+    }
+
+    @Test
+    public void testSimpleTags() {
+        ReaderWay readerWay = new ReaderWay(1);
+        IntsRef intsRef = em.createEdgeFlags();
+        readerWay.setTag("hazmat", "no");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(Hazmat.NO, hazEnc.getEnum(false, intsRef));
+
+        intsRef = em.createEdgeFlags();
+        readerWay.setTag("hazmat", "yes");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(Hazmat.YES, hazEnc.getEnum(false, intsRef));
+        
+        intsRef = em.createEdgeFlags();
+        readerWay.setTag("hazmat", "designated");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(Hazmat.YES, hazEnc.getEnum(false, intsRef));
+    }
+    
+    @Test
+    public void testNoNPE() {
+        ReaderWay readerWay = new ReaderWay(1);
+        IntsRef intsRef = em.createEdgeFlags();
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(Hazmat.YES, hazEnc.getEnum(false, intsRef));
+    }
+}
\ No newline at end of file
diff --git a/core/src/test/java/com/graphhopper/routing/util/parsers/OSMHazmatWaterParserTest.java b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMHazmatWaterParserTest.java
new file mode 100644
index 000000000..e8992cf8b
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMHazmatWaterParserTest.java
@@ -0,0 +1,54 @@
+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.HazmatWater;
+import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.storage.IntsRef;
+
+public class OSMHazmatWaterParserTest {
+
+    private EncodingManager em;
+    private EnumEncodedValue<HazmatWater> hazWaterEnc;
+    private OSMHazmatWaterParser parser;
+
+    @Before
+    public void setUp() {
+        parser = new OSMHazmatWaterParser();
+        em = new EncodingManager.Builder().add(parser).build();
+        hazWaterEnc = em.getEnumEncodedValue(HazmatWater.KEY, HazmatWater.class);
+    }
+
+    @Test
+    public void testSimpleTags() {
+        ReaderWay readerWay = new ReaderWay(1);
+        IntsRef intsRef = em.createEdgeFlags();
+        readerWay.setTag("hazmat:water", "no");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatWater.NO, hazWaterEnc.getEnum(false, intsRef));
+
+        intsRef = em.createEdgeFlags();
+        readerWay.setTag("hazmat:water", "yes");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatWater.YES, hazWaterEnc.getEnum(false, intsRef));
+
+        intsRef = em.createEdgeFlags();
+        readerWay.setTag("hazmat:water", "permissive");
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatWater.PERMISSIVE, hazWaterEnc.getEnum(false, intsRef));
+    }
+    
+    @Test
+    public void testNoNPE() {
+        ReaderWay readerWay = new ReaderWay(1);
+        IntsRef intsRef = em.createEdgeFlags();
+        parser.handleWayTags(intsRef, readerWay, WAY, 0);
+        assertEquals(HazmatWater.YES, hazWaterEnc.getEnum(false, intsRef));
+    }
+}
\ No newline at end of file