Repository: graphhopper/graphhopper
Pull request #2204 (2020-12-08 16:08:25)

StringEncodedValues
* minor code cleanup: add generic parameters to Enum references
* add StringEncodedValue which encodes Strings based on their index within a predefined list
* StringDetails for retrieval via PathDetails

karussell: Would it make sense to make this dynamic? I.e. only provide a maximum number of strings at construction time and feed strings as they come while import? This way we could easier introduce new tags. Hmmh, but it would also mean that a smaller area is fine and the larger area requires a higher number and breaks.
otbutz: > Would it make sense to make this dynamic? I.e. only provide a maximum number of strings at construction time and feed strings as they come while import? This way we could easier introduce new tags. Hmmh, but it would also mean that a smaller area is fine and the larger area requires a higher number and breaks.

I though a bit about dynamic approaches but encountered a few problems:
* we need the number of bits at construction time so we can't really safe space
* if we'd solve this somehow and only encode values we encounter while importing, we would still need to store the encountered values. Otherwise it would be impossible to recreate the StringEncodedValue when loading from an already existing graph.

I dodged the problem in my custom area PR by providing two filter mechanisms(bbox+id) which should allow to conveniently limit the number of areas without having to deal with GeoJSON file itself.
otbutz: Anything missing?
karussell: > we need the number of bits at construction time so we can't really safe space

If bits are full we could either throw an exception or everything goes into a special "OTHER" string (?)

> we would still need to store the encountered values

Yes, it would be necessary to serialize this into the StorableProperties.

But let us do this later in a separate issue (if required at all). 

Can you add an integration test where you collect e.g. "surface" or some other string tag via one of our test OSM PBFs? Because it might be useful to introduce some automatic normalization features like white space removal or lower case conversion etc? Hmmh, maybe we don't add such a test here and have a real integration test using the new Janino scripting (an important optimization there would be to do the comparison based on the index not on the String).
otbutz: > If bits are full we could either throw an exception

From my custom area PR:

```yaml
   custom_area_files:
     - location: core/files/spatialrules/countries.geojson
       id_field: "ISO3166-1:alpha3"
       encoded_value: country
       filter: deu,aut
       max_bbox: -180,180,-90,90
```

Would you add an extra option to define the maximum number of encoded entries?

> or everything goes into a special "OTHER" string (?)

I fear that this might cause erratic behavior and problems which are hard to reproduce.

e.g if we encode countries and reach our maximum number of entries, the resulting EV would completely depend on the order of the processed ways

> Yes, it would be necessary to serialize this into the StorableProperties.

Don't we store the toString representation of the EncodedValues? Maybe we should extend this to a load/store mechanic.

> Can you add an integration test where you collect e.g. "surface" or some other string tag via one of our test OSM PBFs? Because it might be useful to introduce some automatic normalization features like white space removal or lower case conversion etc?

The first usecase would be custom areas and i think it's ok to expect the IDs in the GeoJSON to be already normalized.

>Hmmh, maybe we don't add such a test here and have a real integration test using the new Janino scripting (an important optimization there would be to do the comparison based on the index not on the String).

Not sure if feasible but you could lookup the String within the EncodedValue array once and rely on reference checks later on if you want to avoid String.equals()
otbutz: We would also need to switch to a hashmap based approach as the Strings passed in via setString() are unordered, so we can't rely on binary search.
otbutz: Something like this?

```java
public final class StringEncodedValue extends UnsignedIntEncodedValue {
    private final List<String> values;
    private final ObjectIntMap<String> indexMap;
    private final int maxEntries;
    
    public StringEncodedValue(String name, int maxEntries) {
        this(name, maxEntries, false);
    }
    
    public StringEncodedValue(String name, int maxEntries, List<String> values, boolean storeTwoDirections) {
        super(name, 32 - Integer.numberOfLeadingZeros(maxEntries), storeTwoDirections);
        
        this.values = new ArrayList<>(values);
        this.indexMap = new ObjectIntHashMap<>(values.size());
        int index = 1;
        for (String value : values) {
            indexMap.put(value, index++);
        }
        this.maxEntries = maxEntries;
    }
    
    public StringEncodedValue(String name, int maxEntries, boolean storeTwoDirections) {
        super(name, 32 - Integer.numberOfLeadingZeros(maxEntries), storeTwoDirections);
        
        this.values = new ArrayList<>(maxEntries);
        this.indexMap = new ObjectIntHashMap<>(maxEntries);
        this.maxEntries = maxEntries;
    }

    public final void setString(boolean reverse, IntsRef ref, String value) {
        int index = indexMap.get(value);
        if (index == 0) {
            if (values.size() == maxEntries) {
                throw new IllegalStateException("Maximum number of entries reached for " + getName() + ": " + maxEntries);
            }
            values.add(value);
            index = values.size();
            indexMap.put(value, index);
        }
        super.setInt(reverse, ref, index);
    }

    public final String getString(boolean reverse, IntsRef ref) {
        int value = super.getInt(reverse, ref);
        if (value == 0) {
            return null;
        }
        return values.get(value);
    }

    public List<String> getValues() {
        return Collections.unmodifiableList(values);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof StringEncodedValue)) {
            return false;
        }
        StringEncodedValue other = (StringEncodedValue) obj;
        return Objects.equals(values, other.values);
    }

    @Override
    public int getVersion() {
        return 31 * super.getVersion() + staticHashCode(values);
    }
}
```
karussell: Yes, thanks :) 

An additional indexOf method would be nice, so we can easier get the int (for fast usage inside Weighting.calc)
otbutz: Changed my implementation and added a few unittests

Patch:
diff --git a/README.md b/README.md
index 101e4997b..63b4022f6 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ like finding and fixing bugs and improving our documentation or translations!
 
 To get started you can try [GraphHopper Maps](README.md#graphhopper-maps), read through our documentation and install the GraphHopper Web Service locally.
 
- * stable 2.x: [documentation](https://github.com/graphhopper/graphhopper/blob/2.x/docs/index.md), [web service jar](https://graphhopper.com/public/releases/graphhopper-web-2.2.jar), [announcement](https://www.graphhopper.com/blog/2020/09/30/graphhopper-routing-engine-2-0-released/)
+ * stable 2.x: [documentation](https://github.com/graphhopper/graphhopper/blob/2.x/docs/index.md), [web service jar](https://graphhopper.com/public/releases/graphhopper-web-2.3.jar), [announcement](https://www.graphhopper.com/blog/2020/09/30/graphhopper-routing-engine-2-0-released/)
  * unstable master: [documentation](https://github.com/graphhopper/graphhopper/blob/master/docs/index.md)
 
 <details><summary>Click to see older releases</summary>
@@ -45,7 +45,7 @@ To install the [GraphHopper Maps](https://graphhopper.com/maps/) and the web ser
 
 ```bash
 # download and install a JVM that supports at least Java 8 (https://adoptopenjdk.net)
-wget https://graphhopper.com/public/releases/graphhopper-web-2.0.jar https://raw.githubusercontent.com/graphhopper/graphhopper/stable/config-example.yml http://download.geofabrik.de/europe/germany/berlin-latest.osm.pbf
+wget https://graphhopper.com/public/releases/graphhopper-web-2.3.jar https://raw.githubusercontent.com/graphhopper/graphhopper/stable/config-example.yml http://download.geofabrik.de/europe/germany/berlin-latest.osm.pbf
 java -Ddw.graphhopper.datareader.file=berlin-latest.osm.pbf -jar *.jar server config-example.yml
 ```
 
diff --git a/core/src/main/java/com/graphhopper/routing/ev/EncodedValueLookup.java b/core/src/main/java/com/graphhopper/routing/ev/EncodedValueLookup.java
index 52a8d2f0b..c67f696c6 100644
--- a/core/src/main/java/com/graphhopper/routing/ev/EncodedValueLookup.java
+++ b/core/src/main/java/com/graphhopper/routing/ev/EncodedValueLookup.java
@@ -31,7 +31,9 @@ public interface EncodedValueLookup {
 
     DecimalEncodedValue getDecimalEncodedValue(String key);
 
-    <T extends Enum> EnumEncodedValue<T> getEnumEncodedValue(String key, Class<T> enumType);
+    <T extends Enum<?>> EnumEncodedValue<T> getEnumEncodedValue(String key, Class<T> enumType);
+    
+    StringEncodedValue getStringEncodedValue(String key);
 
     boolean hasEncodedValue(String key);
 }
diff --git a/core/src/main/java/com/graphhopper/routing/ev/StringEncodedValue.java b/core/src/main/java/com/graphhopper/routing/ev/StringEncodedValue.java
new file mode 100644
index 000000000..515ea23f0
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/routing/ev/StringEncodedValue.java
@@ -0,0 +1,123 @@
+package com.graphhopper.routing.ev;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import com.carrotsearch.hppc.ObjectIntHashMap;
+import com.carrotsearch.hppc.ObjectIntMap;
+import com.graphhopper.storage.IntsRef;
+
+/**
+ * This class holds a List of up to {@link #maxValues} encountered Strings and stores
+ * <i>index+1</i> to indicate a string is set or <i>0</i> if no value is assigned
+ * 
+ * @author Peter Karich
+ * @author Thomas Butz
+ */
+public final class StringEncodedValue extends UnsignedIntEncodedValue {
+    private final int maxValues;
+    private final List<String> values;
+    private final ObjectIntMap<String> indexMap;
+
+    public StringEncodedValue(String name, int expectedValueCount) {
+        this(name, expectedValueCount, false);
+    }
+
+    public StringEncodedValue(String name, int expectedValueCount, boolean storeTwoDirections) {
+        super(name, 32 - Integer.numberOfLeadingZeros(expectedValueCount), storeTwoDirections);
+
+        this.maxValues = roundUp(expectedValueCount);
+        this.values = new ArrayList<>(maxValues);
+        this.indexMap = new ObjectIntHashMap<>(maxValues);
+    }
+
+    public StringEncodedValue(String name, int bits, List<String> values, boolean storeTwoDirections) {
+        super(name, bits, storeTwoDirections);
+
+        this.maxValues = (1 << bits) - 1;
+        if (values.size() > maxValues) {
+            throw new IllegalArgumentException(
+                            "Number of values is higher than the maximum value count: "
+                                            + values.size() + " > " + maxValues);
+        }
+        this.values = new ArrayList<>(values);
+        this.indexMap = new ObjectIntHashMap<>(values.size());
+        int index = 1;
+        for (String value : values) {
+            indexMap.put(value, index++);
+        }
+    }
+
+    public final void setString(boolean reverse, IntsRef ref, String value) {
+        if (value == null) {
+            super.setInt(reverse, ref, 0);
+            return;
+        }
+        int index = indexMap.get(value);
+        if (index == 0) {
+            if (values.size() == maxValues) {
+                throw new IllegalStateException("Maximum number of values reached for " + getName()
+                                + ": " + maxValues);
+            }
+            values.add(value);
+            index = values.size();
+            indexMap.put(value, index);
+        }
+        super.setInt(reverse, ref, index);
+    }
+
+    public final String getString(boolean reverse, IntsRef ref) {
+        int value = super.getInt(reverse, ref);
+        if (value == 0) {
+            return null;
+        }
+        return values.get(value - 1);
+    }
+
+    /**
+     * @param value
+     *            the value to be rounded
+     * @return the value rounded to the highest integer with the same number of leading zeros
+     */
+    private static int roundUp(int value) {
+        return -1 >>> Integer.numberOfLeadingZeros(value);
+    }
+
+    /**
+     * @param value
+     *            the String to retrieve the index
+     * @return the non-zero index of the String or <i>0</i> if it couldn't be found
+     */
+    public int indexOf(String value) {
+        return indexMap.get(value);
+    }
+
+    /**
+     * @return an unmodifiable List of the current values
+     */
+    public List<String> getValues() {
+        return Collections.unmodifiableList(values);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof StringEncodedValue)) {
+            return false;
+        }
+        StringEncodedValue other = (StringEncodedValue) obj;
+        if (this.bits != other.bits) {
+            return false;
+        }
+        return Objects.equals(values, other.values);
+    }
+
+    @Override
+    public int getVersion() {
+        return 31 * super.getVersion() + staticHashCode(values);
+    }
+}
diff --git a/core/src/main/java/com/graphhopper/routing/ev/UnsignedIntEncodedValue.java b/core/src/main/java/com/graphhopper/routing/ev/UnsignedIntEncodedValue.java
index 4fb107ba9..62185fca6 100644
--- a/core/src/main/java/com/graphhopper/routing/ev/UnsignedIntEncodedValue.java
+++ b/core/src/main/java/com/graphhopper/routing/ev/UnsignedIntEncodedValue.java
@@ -22,7 +22,7 @@ import com.graphhopper.storage.IntsRef;
 import com.graphhopper.util.Helper;
 
 import java.util.Arrays;
-import java.util.Locale;
+import java.util.Collection;
 import java.util.Objects;
 
 /**
@@ -207,7 +207,7 @@ public class UnsignedIntEncodedValue implements IntEncodedValue {
      * Produces a static hashcode for an Enum arrays that is platform independent and still compatible to the default
      * of openjdk.
      */
-    static int staticHashCode(Enum... vals) {
+    static int staticHashCode(Enum<?>... vals) {
         if (vals == null)
             return 0;
         int len = vals.length;
@@ -219,6 +219,37 @@ public class UnsignedIntEncodedValue implements IntEncodedValue {
         return val;
     }
 
+    /**
+     * Produces a static hashcode for an String arrays that is platform independent and still compatible to the default
+     * of openjdk.
+     */
+    static int staticHashCode(String... vals) {
+        if (vals == null)
+            return 0;
+        int len = vals.length;
+        int val = 1;
+        for (int idx = 0; idx < len; ++idx) {
+            val = 31 * val + Helper.staticHashCode(vals[idx]);
+        }
+
+        return val;
+    }
+    
+    /**
+     * Produces a static hashcode for a collection of Strings that is platform independent and still compatible to the default
+     * of openjdk.
+     */
+    static int staticHashCode(Collection<String> vals) {
+        if (vals == null)
+            return 0;
+        int val = 1;
+        for (String str : vals) {
+            val = 31 * val + Helper.staticHashCode(str);
+        }
+
+        return val;
+    }
+
     /**
      * Produces a static hashcode for an integer arrays that is platform independent and still compatible to the default
      * of openjdk
diff --git a/core/src/main/java/com/graphhopper/routing/querygraph/VirtualEdgeIterator.java b/core/src/main/java/com/graphhopper/routing/querygraph/VirtualEdgeIterator.java
index 181d3ce54..aeecd2b62 100644
--- a/core/src/main/java/com/graphhopper/routing/querygraph/VirtualEdgeIterator.java
+++ b/core/src/main/java/com/graphhopper/routing/querygraph/VirtualEdgeIterator.java
@@ -21,6 +21,7 @@ import com.graphhopper.routing.ev.BooleanEncodedValue;
 import com.graphhopper.routing.ev.DecimalEncodedValue;
 import com.graphhopper.routing.ev.EnumEncodedValue;
 import com.graphhopper.routing.ev.IntEncodedValue;
+import com.graphhopper.routing.ev.StringEncodedValue;
 import com.graphhopper.routing.util.EdgeFilter;
 import com.graphhopper.storage.IntsRef;
 import com.graphhopper.util.EdgeIterator;
@@ -201,32 +202,57 @@ class VirtualEdgeIterator implements EdgeIterator {
     }
 
     @Override
-    public <T extends Enum> EdgeIteratorState set(EnumEncodedValue<T> property, T value) {
+    public <T extends Enum<?>> EdgeIteratorState set(EnumEncodedValue<T> property, T value) {
         getCurrentEdge().set(property, value);
         return this;
     }
 
     @Override
-    public <T extends Enum> T get(EnumEncodedValue<T> property) {
+    public <T extends Enum<?>> T get(EnumEncodedValue<T> property) {
         return getCurrentEdge().get(property);
     }
 
     @Override
-    public <T extends Enum> EdgeIteratorState setReverse(EnumEncodedValue<T> property, T value) {
+    public <T extends Enum<?>> EdgeIteratorState setReverse(EnumEncodedValue<T> property, T value) {
         getCurrentEdge().setReverse(property, value);
         return this;
     }
 
     @Override
-    public <T extends Enum> T getReverse(EnumEncodedValue<T> property) {
+    public <T extends Enum<?>> T getReverse(EnumEncodedValue<T> property) {
         return getCurrentEdge().getReverse(property);
     }
 
     @Override
-    public <T extends Enum> EdgeIteratorState set(EnumEncodedValue<T> property, T fwd, T bwd) {
+    public <T extends Enum<?>> EdgeIteratorState set(EnumEncodedValue<T> property, T fwd, T bwd) {
         getCurrentEdge().set(property, fwd, bwd);
         return this;
     }
+    
+    @Override
+    public String get(StringEncodedValue property) {
+        return getCurrentEdge().get(property);
+    }
+    
+    @Override
+    public EdgeIteratorState set(StringEncodedValue property, String value) {
+        return getCurrentEdge().set(property, value);
+    }
+    
+    @Override
+    public String getReverse(StringEncodedValue property) {
+        return getCurrentEdge().getReverse(property);
+    }
+    
+    @Override
+    public EdgeIteratorState setReverse(StringEncodedValue property, String value) {
+        return getCurrentEdge().setReverse(property, value);
+    }
+    
+    @Override
+    public EdgeIteratorState set(StringEncodedValue property, String fwd, String bwd) {
+        return getCurrentEdge().set(property, fwd, bwd);
+    }
 
     @Override
     public String getName() {
diff --git a/core/src/main/java/com/graphhopper/routing/querygraph/VirtualEdgeIteratorState.java b/core/src/main/java/com/graphhopper/routing/querygraph/VirtualEdgeIteratorState.java
index 44fad7489..7a1a93afc 100644
--- a/core/src/main/java/com/graphhopper/routing/querygraph/VirtualEdgeIteratorState.java
+++ b/core/src/main/java/com/graphhopper/routing/querygraph/VirtualEdgeIteratorState.java
@@ -21,6 +21,7 @@ import com.graphhopper.routing.ev.BooleanEncodedValue;
 import com.graphhopper.routing.ev.DecimalEncodedValue;
 import com.graphhopper.routing.ev.EnumEncodedValue;
 import com.graphhopper.routing.ev.IntEncodedValue;
+import com.graphhopper.routing.ev.StringEncodedValue;
 import com.graphhopper.storage.IntsRef;
 import com.graphhopper.util.EdgeIteratorState;
 import com.graphhopper.util.FetchMode;
@@ -242,35 +243,66 @@ public class VirtualEdgeIteratorState implements EdgeIteratorState {
     }
 
     @Override
-    public <T extends Enum> T get(EnumEncodedValue<T> property) {
+    public <T extends Enum<?>> T get(EnumEncodedValue<T> property) {
         return property.getEnum(reverse, edgeFlags);
     }
 
     @Override
-    public <T extends Enum> EdgeIteratorState set(EnumEncodedValue<T> property, T value) {
+    public <T extends Enum<?>> EdgeIteratorState set(EnumEncodedValue<T> property, T value) {
         property.setEnum(reverse, edgeFlags, value);
         return this;
     }
 
     @Override
-    public <T extends Enum> T getReverse(EnumEncodedValue<T> property) {
+    public <T extends Enum<?>> T getReverse(EnumEncodedValue<T> property) {
         return property.getEnum(!reverse, edgeFlags);
     }
 
     @Override
-    public <T extends Enum> EdgeIteratorState setReverse(EnumEncodedValue<T> property, T value) {
+    public <T extends Enum<?>> EdgeIteratorState setReverse(EnumEncodedValue<T> property, T value) {
         property.setEnum(!reverse, edgeFlags, value);
         return this;
     }
 
     @Override
-    public <T extends Enum> EdgeIteratorState set(EnumEncodedValue<T> property, T fwd, T bwd) {
+    public <T extends Enum<?>> EdgeIteratorState set(EnumEncodedValue<T> property, T fwd, T bwd) {
         if (!property.isStoreTwoDirections())
             throw new IllegalArgumentException("EncodedValue " + property.getName() + " supports only one direction");
         property.setEnum(reverse, edgeFlags, fwd);
         property.setEnum(!reverse, edgeFlags, bwd);
         return this;
     }
+    
+    @Override
+    public String get(StringEncodedValue property) {
+        return property.getString(reverse, edgeFlags);
+    }
+    
+    @Override
+    public EdgeIteratorState set(StringEncodedValue property, String value) {
+        property.setString(reverse, edgeFlags, value);
+        return this;
+    }
+    
+    @Override
+    public String getReverse(StringEncodedValue property) {
+        return property.getString(!reverse, edgeFlags);
+    }
+    
+    @Override
+    public EdgeIteratorState setReverse(StringEncodedValue property, String value) {
+        property.setString(!reverse, edgeFlags, value);
+        return this;
+    }
+    
+    @Override
+    public EdgeIteratorState set(StringEncodedValue property, String fwd, String bwd) {
+        if (!property.isStoreTwoDirections())
+            throw new IllegalArgumentException("EncodedValue " + property.getName() + " supports only one direction");
+        property.setString(reverse, edgeFlags, fwd);
+        property.setString(!reverse, edgeFlags, bwd);
+        return this;
+    }
 
     @Override
     public String getName() {
diff --git a/core/src/main/java/com/graphhopper/routing/util/AbstractFlagEncoder.java b/core/src/main/java/com/graphhopper/routing/util/AbstractFlagEncoder.java
index d2422290e..1720cf617 100644
--- a/core/src/main/java/com/graphhopper/routing/util/AbstractFlagEncoder.java
+++ b/core/src/main/java/com/graphhopper/routing/util/AbstractFlagEncoder.java
@@ -410,9 +410,14 @@ public abstract class AbstractFlagEncoder implements FlagEncoder {
     }
 
     @Override
-    public <T extends Enum> EnumEncodedValue<T> getEnumEncodedValue(String key, Class<T> enumType) {
+    public <T extends Enum<?>> EnumEncodedValue<T> getEnumEncodedValue(String key, Class<T> enumType) {
         return encodedValueLookup.getEnumEncodedValue(key, enumType);
     }
+    
+    @Override
+    public StringEncodedValue getStringEncodedValue(String key) {
+        return encodedValueLookup.getStringEncodedValue(key);
+    }
 
     public void setEncodedValueLookup(EncodedValueLookup encodedValueLookup) {
         this.encodedValueLookup = encodedValueLookup;
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 dbb5b7fcf..b1a7cfbcf 100644
--- a/core/src/main/java/com/graphhopper/routing/util/EncodingManager.java
+++ b/core/src/main/java/com/graphhopper/routing/util/EncodingManager.java
@@ -772,10 +772,15 @@ public class EncodingManager implements EncodedValueLookup {
         return getEncodedValue(key, DecimalEncodedValue.class);
     }
 
-    @Override
     @SuppressWarnings("unchecked")
-    public <T extends Enum> EnumEncodedValue<T> getEnumEncodedValue(String key, Class<T> type) {
-        return (EnumEncodedValue<T>) getEncodedValue(key, EnumEncodedValue.class);
+    @Override
+    public <T extends Enum<?>> EnumEncodedValue<T> getEnumEncodedValue(String key, Class<T> type) {
+        return getEncodedValue(key, EnumEncodedValue.class);
+    }
+
+    @Override
+    public StringEncodedValue getStringEncodedValue(String key) {
+        return getEncodedValue(key, StringEncodedValue.class);
     }
 
     @Override
diff --git a/core/src/main/java/com/graphhopper/storage/BaseGraph.java b/core/src/main/java/com/graphhopper/storage/BaseGraph.java
index b4b86fe3c..ec45ef0da 100644
--- a/core/src/main/java/com/graphhopper/storage/BaseGraph.java
+++ b/core/src/main/java/com/graphhopper/storage/BaseGraph.java
@@ -21,6 +21,7 @@ import com.graphhopper.routing.ev.BooleanEncodedValue;
 import com.graphhopper.routing.ev.DecimalEncodedValue;
 import com.graphhopper.routing.ev.EnumEncodedValue;
 import com.graphhopper.routing.ev.IntEncodedValue;
+import com.graphhopper.routing.ev.StringEncodedValue;
 import com.graphhopper.routing.util.AllEdgesIterator;
 import com.graphhopper.routing.util.EdgeFilter;
 import com.graphhopper.routing.util.EncodingManager;
@@ -1209,31 +1210,31 @@ class BaseGraph implements Graph {
         }
 
         @Override
-        public <T extends Enum> T get(EnumEncodedValue<T> property) {
+        public <T extends Enum<?>> T get(EnumEncodedValue<T> property) {
             return property.getEnum(reverse, getFlags());
         }
 
         @Override
-        public <T extends Enum> EdgeIteratorState set(EnumEncodedValue<T> property, T value) {
+        public <T extends Enum<?>> EdgeIteratorState set(EnumEncodedValue<T> property, T value) {
             property.setEnum(reverse, getFlags(), value);
             baseGraph.writeFlags(edgePointer, getFlags());
             return this;
         }
 
         @Override
-        public <T extends Enum> T getReverse(EnumEncodedValue<T> property) {
+        public <T extends Enum<?>> T getReverse(EnumEncodedValue<T> property) {
             return property.getEnum(!reverse, getFlags());
         }
 
         @Override
-        public <T extends Enum> EdgeIteratorState setReverse(EnumEncodedValue<T> property, T value) {
+        public <T extends Enum<?>> EdgeIteratorState setReverse(EnumEncodedValue<T> property, T value) {
             property.setEnum(!reverse, getFlags(), value);
             baseGraph.writeFlags(edgePointer, getFlags());
             return this;
         }
 
         @Override
-        public <T extends Enum> EdgeIteratorState set(EnumEncodedValue<T> property, T fwd, T bwd) {
+        public <T extends Enum<?>> EdgeIteratorState set(EnumEncodedValue<T> property, T fwd, T bwd) {
             if (!property.isStoreTwoDirections())
                 throw new IllegalArgumentException("EncodedValue " + property.getName() + " supports only one direction");
             property.setEnum(reverse, getFlags(), fwd);
@@ -1241,6 +1242,40 @@ class BaseGraph implements Graph {
             baseGraph.writeFlags(edgePointer, getFlags());
             return this;
         }
+        
+        @Override
+        public String get(StringEncodedValue property) {
+            return property.getString(reverse, getFlags());
+        }
+        
+        @Override
+        public EdgeIteratorState set(StringEncodedValue property, String value) {
+            property.setString(reverse, getFlags(), value);
+            baseGraph.writeFlags(edgePointer, getFlags());
+            return this;
+        }
+        
+        @Override
+        public String getReverse(StringEncodedValue property) {
+            return property.getString(!reverse, getFlags());
+        }
+        
+        @Override
+        public EdgeIteratorState setReverse(StringEncodedValue property, String value) {
+            property.setString(!reverse, getFlags(), value);
+            baseGraph.writeFlags(edgePointer, getFlags());
+            return this;
+        }
+        
+        @Override
+        public EdgeIteratorState set(StringEncodedValue property, String fwd, String bwd) {
+            if (!property.isStoreTwoDirections())
+                throw new IllegalArgumentException("EncodedValue " + property.getName() + " supports only one direction");
+            property.setString(reverse, getFlags(), fwd);
+            property.setString(!reverse, getFlags(), bwd);
+            baseGraph.writeFlags(edgePointer, getFlags());
+            return this;
+        }
 
         @Override
         public final EdgeIteratorState copyPropertiesFrom(EdgeIteratorState edge) {
diff --git a/core/src/main/java/com/graphhopper/storage/CHGraphImpl.java b/core/src/main/java/com/graphhopper/storage/CHGraphImpl.java
index 066cf770e..4a7a154fb 100644
--- a/core/src/main/java/com/graphhopper/storage/CHGraphImpl.java
+++ b/core/src/main/java/com/graphhopper/storage/CHGraphImpl.java
@@ -23,6 +23,7 @@ import com.graphhopper.routing.ev.BooleanEncodedValue;
 import com.graphhopper.routing.ev.DecimalEncodedValue;
 import com.graphhopper.routing.ev.EnumEncodedValue;
 import com.graphhopper.routing.ev.IntEncodedValue;
+import com.graphhopper.routing.ev.StringEncodedValue;
 import com.graphhopper.routing.util.AllCHEdgesIterator;
 import com.graphhopper.routing.util.EdgeFilter;
 import com.graphhopper.storage.BaseGraph.AllEdgeIterator;
@@ -907,34 +908,64 @@ public class CHGraphImpl implements CHGraph, Storable<CHGraph> {
         }
 
         @Override
-        public <T extends Enum> T get(EnumEncodedValue<T> property) {
+        public <T extends Enum<?>> T get(EnumEncodedValue<T> property) {
             checkShortcut(false, "get(EnumEncodedValue<T>)");
             return edgeIterable.get(property);
         }
 
         @Override
-        public <T extends Enum> EdgeIteratorState set(EnumEncodedValue<T> property, T value) {
+        public <T extends Enum<?>> EdgeIteratorState set(EnumEncodedValue<T> property, T value) {
             checkShortcut(false, "set(EnumEncodedValue<T>, T)");
             return edgeIterable.set(property, value);
         }
 
         @Override
-        public <T extends Enum> T getReverse(EnumEncodedValue<T> property) {
+        public <T extends Enum<?>> T getReverse(EnumEncodedValue<T> property) {
             checkShortcut(false, "getReverse(EnumEncodedValue<T>)");
             return edgeIterable.getReverse(property);
         }
 
         @Override
-        public <T extends Enum> EdgeIteratorState setReverse(EnumEncodedValue<T> property, T value) {
+        public <T extends Enum<?>> EdgeIteratorState setReverse(EnumEncodedValue<T> property, T value) {
             checkShortcut(false, "setReverse(EnumEncodedValue<T>, T)");
             return edgeIterable.setReverse(property, value);
         }
 
         @Override
-        public <T extends Enum> EdgeIteratorState set(EnumEncodedValue<T> property, T fwd, T bwd) {
+        public <T extends Enum<?>> EdgeIteratorState set(EnumEncodedValue<T> property, T fwd, T bwd) {
             checkShortcut(false, "set(EnumEncodedValue<T>, T, T)");
             return edgeIterable.set(property, fwd, bwd);
         }
+        
+        @Override
+        public String get(StringEncodedValue property) {
+            checkShortcut(false, "get(StringEncodedValue)");
+            return edgeIterable.get(property);
+        }
+        
+        @Override
+        public EdgeIteratorState set(StringEncodedValue property, String value) {
+            checkShortcut(false, "set(StringEncodedValue, String)");
+            return edgeIterable.set(property, value);
+        }
+        
+        @Override
+        public String getReverse(StringEncodedValue property) {
+            checkShortcut(false, "getReverse(StringEncodedValue)");
+            return edgeIterable.getReverse(property);
+        }
+        
+        @Override
+        public EdgeIteratorState setReverse(StringEncodedValue property, String value) {
+            checkShortcut(false, "setReverse(StringEncodedValue, String)");
+            return edgeIterable.setReverse(property, value);
+        }
+        
+        @Override
+        public EdgeIteratorState set(StringEncodedValue property, String fwd, String bwd) {
+            checkShortcut(false, "set(StringEncodedValue, String, String)");
+            return edgeIterable.set(property, fwd, bwd);
+        }
 
         @Override
         public EdgeIteratorState detach(boolean reverseArg) {
diff --git a/core/src/main/java/com/graphhopper/util/EdgeIteratorState.java b/core/src/main/java/com/graphhopper/util/EdgeIteratorState.java
index bcab8d4a8..9038ed960 100644
--- a/core/src/main/java/com/graphhopper/util/EdgeIteratorState.java
+++ b/core/src/main/java/com/graphhopper/util/EdgeIteratorState.java
@@ -182,15 +182,25 @@ public interface EdgeIteratorState {
 
     EdgeIteratorState set(DecimalEncodedValue property, double fwd, double bwd);
 
-    <T extends Enum> T get(EnumEncodedValue<T> property);
+    <T extends Enum<?>> T get(EnumEncodedValue<T> property);
 
-    <T extends Enum> EdgeIteratorState set(EnumEncodedValue<T> property, T value);
+    <T extends Enum<?>> EdgeIteratorState set(EnumEncodedValue<T> property, T value);
 
-    <T extends Enum> T getReverse(EnumEncodedValue<T> property);
+    <T extends Enum<?>> T getReverse(EnumEncodedValue<T> property);
 
-    <T extends Enum> EdgeIteratorState setReverse(EnumEncodedValue<T> property, T value);
+    <T extends Enum<?>> EdgeIteratorState setReverse(EnumEncodedValue<T> property, T value);
 
-    <T extends Enum> EdgeIteratorState set(EnumEncodedValue<T> property, T fwd, T bwd);
+    <T extends Enum<?>> EdgeIteratorState set(EnumEncodedValue<T> property, T fwd, T bwd);
+    
+    String get(StringEncodedValue property);
+    
+    EdgeIteratorState set(StringEncodedValue property, String value);
+    
+    String getReverse(StringEncodedValue property);
+    
+    EdgeIteratorState setReverse(StringEncodedValue property, String value);
+    
+    EdgeIteratorState set(StringEncodedValue property, String fwd, String bwd);
 
     String getName();
 
diff --git a/core/src/main/java/com/graphhopper/util/GHUtility.java b/core/src/main/java/com/graphhopper/util/GHUtility.java
index cb42da17a..118c3df4d 100644
--- a/core/src/main/java/com/graphhopper/util/GHUtility.java
+++ b/core/src/main/java/com/graphhopper/util/GHUtility.java
@@ -523,12 +523,12 @@ public class GHUtility {
             }
 
             @Override
-            public <T extends Enum> T get(EnumEncodedValue<T> property) {
+            public <T extends Enum<?>> T get(EnumEncodedValue<T> property) {
                 return property.getEnum(false, flags);
             }
 
             @Override
-            public <T extends Enum> T getReverse(EnumEncodedValue<T> property) {
+            public <T extends Enum<?>> T getReverse(EnumEncodedValue<T> property) {
                 return property.getEnum(true, flags);
             }
 
@@ -887,27 +887,52 @@ public class GHUtility {
         }
 
         @Override
-        public <T extends Enum> T get(EnumEncodedValue<T> property) {
+        public <T extends Enum<?>> T get(EnumEncodedValue<T> property) {
             throw new UnsupportedOperationException("Not supported. Edge is empty.");
         }
 
         @Override
-        public <T extends Enum> EdgeIteratorState set(EnumEncodedValue<T> property, T value) {
+        public <T extends Enum<?>> EdgeIteratorState set(EnumEncodedValue<T> property, T value) {
             throw new UnsupportedOperationException("Not supported. Edge is empty.");
         }
 
         @Override
-        public <T extends Enum> T getReverse(EnumEncodedValue<T> property) {
+        public <T extends Enum<?>> T getReverse(EnumEncodedValue<T> property) {
             throw new UnsupportedOperationException("Not supported. Edge is empty.");
         }
 
         @Override
-        public <T extends Enum> EdgeIteratorState setReverse(EnumEncodedValue<T> property, T value) {
+        public <T extends Enum<?>> EdgeIteratorState setReverse(EnumEncodedValue<T> property, T value) {
             throw new UnsupportedOperationException("Not supported. Edge is empty.");
         }
 
         @Override
-        public <T extends Enum> EdgeIteratorState set(EnumEncodedValue<T> property, T fwd, T bwd) {
+        public <T extends Enum<?>> EdgeIteratorState set(EnumEncodedValue<T> property, T fwd, T bwd) {
+            throw new UnsupportedOperationException("Not supported. Edge is empty.");
+        }
+        
+        @Override
+        public String get(StringEncodedValue property) {
+            throw new UnsupportedOperationException("Not supported. Edge is empty.");
+        }
+        
+        @Override
+        public EdgeIteratorState set(StringEncodedValue property, String value) {
+            throw new UnsupportedOperationException("Not supported. Edge is empty.");
+        }
+        
+        @Override
+        public String getReverse(StringEncodedValue property) {
+            throw new UnsupportedOperationException("Not supported. Edge is empty.");
+        }
+        
+        @Override
+        public EdgeIteratorState setReverse(StringEncodedValue property, String value) {
+            throw new UnsupportedOperationException("Not supported. Edge is empty.");
+        }
+        
+        @Override
+        public EdgeIteratorState set(StringEncodedValue property, String fwd, String bwd) {
             throw new UnsupportedOperationException("Not supported. Edge is empty.");
         }
 
diff --git a/core/src/main/java/com/graphhopper/util/details/StringDetails.java b/core/src/main/java/com/graphhopper/util/details/StringDetails.java
new file mode 100644
index 000000000..bdeabceac
--- /dev/null
+++ b/core/src/main/java/com/graphhopper/util/details/StringDetails.java
@@ -0,0 +1,48 @@
+/*
+ *  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.util.details;
+
+import com.graphhopper.routing.ev.StringEncodedValue;
+import com.graphhopper.util.EdgeIteratorState;
+
+public class StringDetails extends AbstractPathDetailsBuilder {
+
+    private final StringEncodedValue ev;
+    private String currentVal = null;
+
+    public StringDetails(String name, StringEncodedValue ev) {
+        super(name);
+        this.ev = ev;
+    }
+
+    @Override
+    protected Object getCurrentValue() {
+        return currentVal;
+    }
+
+    @Override
+    public boolean isEdgeDifferentToLastEdge(EdgeIteratorState edge) {
+        String val = edge.get(ev);
+        // we can use the reference equality here
+        if (val != currentVal) {
+            this.currentVal = val;
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/reader-gtfs/src/main/java/com/graphhopper/gtfs/WrapperGraph.java b/reader-gtfs/src/main/java/com/graphhopper/gtfs/WrapperGraph.java
index f6c8f5639..beeb7c732 100644
--- a/reader-gtfs/src/main/java/com/graphhopper/gtfs/WrapperGraph.java
+++ b/reader-gtfs/src/main/java/com/graphhopper/gtfs/WrapperGraph.java
@@ -25,6 +25,7 @@ import com.graphhopper.routing.ev.BooleanEncodedValue;
 import com.graphhopper.routing.ev.DecimalEncodedValue;
 import com.graphhopper.routing.ev.EnumEncodedValue;
 import com.graphhopper.routing.ev.IntEncodedValue;
+import com.graphhopper.routing.ev.StringEncodedValue;
 import com.graphhopper.routing.querygraph.VirtualEdgeIteratorState;
 import com.graphhopper.routing.util.AllEdgesIterator;
 import com.graphhopper.routing.util.EdgeFilter;
@@ -264,27 +265,52 @@ public class WrapperGraph implements Graph {
             }
 
             @Override
-            public <T extends Enum> T get(EnumEncodedValue<T> property) {
+            public <T extends Enum<?>> T get(EnumEncodedValue<T> property) {
                 throw new UnsupportedOperationException();
             }
 
             @Override
-            public <T extends Enum> T getReverse(EnumEncodedValue<T> property) {
+            public <T extends Enum<?>> T getReverse(EnumEncodedValue<T> property) {
                 throw new UnsupportedOperationException();
             }
 
             @Override
-            public <T extends Enum> EdgeIteratorState set(EnumEncodedValue<T> property, T value) {
+            public <T extends Enum<?>> EdgeIteratorState set(EnumEncodedValue<T> property, T value) {
                 throw new UnsupportedOperationException();
             }
 
             @Override
-            public <T extends Enum> EdgeIteratorState setReverse(EnumEncodedValue<T> property, T value) {
+            public <T extends Enum<?>> EdgeIteratorState setReverse(EnumEncodedValue<T> property, T value) {
                 throw new UnsupportedOperationException();
             }
 
             @Override
-            public <T extends Enum> EdgeIteratorState set(EnumEncodedValue<T> property, T fwd, T bwd) {
+            public <T extends Enum<?>> EdgeIteratorState set(EnumEncodedValue<T> property, T fwd, T bwd) {
+                throw new UnsupportedOperationException();
+            }
+            
+            @Override
+            public String get(StringEncodedValue property) {
+                throw new UnsupportedOperationException();
+            }
+            
+            @Override
+            public EdgeIteratorState set(StringEncodedValue property, String value) {
+                throw new UnsupportedOperationException();
+            }
+            
+            @Override
+            public String getReverse(StringEncodedValue property) {
+                throw new UnsupportedOperationException();
+            }
+            
+            @Override
+            public EdgeIteratorState setReverse(StringEncodedValue property, String value) {
+                throw new UnsupportedOperationException();
+            }
+            
+            @Override
+            public EdgeIteratorState set(StringEncodedValue property, String fwd, String bwd) {
                 throw new UnsupportedOperationException();
             }
 
@@ -500,29 +526,57 @@ public class WrapperGraph implements Graph {
                     }
 
                     @Override
-                    public <T extends Enum> T get(EnumEncodedValue<T> property) {
+                    public <T extends Enum<?>> T get(EnumEncodedValue<T> property) {
                         return current.get(property);
                     }
 
                     @Override
-                    public <T extends Enum> EdgeIteratorState set(EnumEncodedValue<T> property, T value) {
+                    public <T extends Enum<?>> EdgeIteratorState set(EnumEncodedValue<T> property, T value) {
                         current.set(property, value);
                         return this;
                     }
 
                     @Override
-                    public <T extends Enum> T getReverse(EnumEncodedValue<T> property) {
+                    public <T extends Enum<?>> T getReverse(EnumEncodedValue<T> property) {
                         return current.getReverse(property);
                     }
 
                     @Override
-                    public <T extends Enum> EdgeIteratorState setReverse(EnumEncodedValue<T> property, T value) {
+                    public <T extends Enum<?>> EdgeIteratorState setReverse(EnumEncodedValue<T> property, T value) {
                         current.setReverse(property, value);
                         return this;
                     }
 
                     @Override
-                    public <T extends Enum> EdgeIteratorState set(EnumEncodedValue<T> property, T fwd, T bwd) {
+                    public <T extends Enum<?>> EdgeIteratorState set(EnumEncodedValue<T> property, T fwd, T bwd) {
+                        current.set(property, fwd, bwd);
+                        return this;
+                    }
+                    
+                    @Override
+                    public String get(StringEncodedValue property) {
+                        return current.get(property);
+                    }
+                    
+                    @Override
+                    public EdgeIteratorState set(StringEncodedValue property, String value) {
+                        current.set(property, value);
+                        return this;
+                    }
+                    
+                    @Override
+                    public String getReverse(StringEncodedValue property) {
+                        return current.getReverse(property);
+                    }
+                    
+                    @Override
+                    public EdgeIteratorState setReverse(StringEncodedValue property, String value) {
+                        current.setReverse(property, value);
+                        return this;
+                    }
+                    
+                    @Override
+                    public EdgeIteratorState set(StringEncodedValue property, String fwd, String bwd) {
                         current.set(property, fwd, bwd);
                         return this;
                     }


Test patch:
diff --git a/core/src/test/java/com/graphhopper/routing/ev/StringEncodedValueTest.java b/core/src/test/java/com/graphhopper/routing/ev/StringEncodedValueTest.java
new file mode 100644
index 000000000..1c356e3a9
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/routing/ev/StringEncodedValueTest.java
@@ -0,0 +1,131 @@
+package com.graphhopper.routing.ev;
+
+import com.graphhopper.storage.IntsRef;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class StringEncodedValueTest {
+
+    @Test
+    public void testInitExact() {
+        // 3+1 values -> 2 bits
+        StringEncodedValue prop = new StringEncodedValue("country", 3);
+        EncodedValue.InitializerConfig init = new EncodedValue.InitializerConfig();
+        assertEquals(2, prop.init(init));
+        assertEquals(2, prop.bits);
+        assertEquals(0, init.dataIndex);
+        assertEquals(0, init.shift);
+    }
+
+    @Test
+    public void testInitRoundUp() {
+        // 33+1 values -> 6 bits
+        StringEncodedValue prop = new StringEncodedValue("country", 33);
+        EncodedValue.InitializerConfig init = new EncodedValue.InitializerConfig();
+        assertEquals(6, prop.init(init));
+        assertEquals(6, prop.bits);
+        assertEquals(0, init.dataIndex);
+        assertEquals(0, init.shift);
+    }
+    
+    @Test
+    public void testInitSingle() {
+        StringEncodedValue prop = new StringEncodedValue("country", 1);
+        EncodedValue.InitializerConfig init = new EncodedValue.InitializerConfig();
+        assertEquals(1, prop.init(init));
+        assertEquals(1, prop.bits);
+        assertEquals(0, init.dataIndex);
+        assertEquals(0, init.shift);
+    }
+    
+    @Test
+    public void testInitTooManyEntries() {
+        List<String> values = Arrays.asList("aut", "deu", "che", "fra");
+        try {
+            new StringEncodedValue("country", 2, values, false);
+            fail("The encoded value should only allow 3 entries");
+        } catch (IllegalArgumentException e) {
+            assertTrue(e.getMessage().startsWith("Number of values is higher than the maximum value count"));
+        }
+    }
+    
+    @Test
+    public void testNull() {
+        StringEncodedValue prop = new StringEncodedValue("country", 3);
+        prop.init(new EncodedValue.InitializerConfig());
+        
+        IntsRef ref = new IntsRef(1);
+        prop.setString(false, ref, null);
+        assertEquals(0, prop.getValues().size());
+    }
+    
+    @Test
+    public void testEquals() {
+        List<String> values = Arrays.asList("aut", "deu", "che");
+        StringEncodedValue small = new StringEncodedValue("country", 3, values, false);
+        small.init(new EncodedValue.InitializerConfig());
+
+        StringEncodedValue big = new StringEncodedValue("country", 4, values, false);
+        big.init(new EncodedValue.InitializerConfig());
+        
+        assertNotEquals(small, big);
+    }
+    
+    @Test
+    public void testLookup() {
+        StringEncodedValue prop = new StringEncodedValue("country", 3);
+        prop.init(new EncodedValue.InitializerConfig());
+        
+        IntsRef ref = new IntsRef(1);
+        assertEquals(null, prop.getString(false, ref));
+        assertEquals(0, prop.getValues().size());
+
+        prop.setString(false, ref, "aut");
+        assertEquals("aut", prop.getString(false, ref));
+        assertEquals(1, prop.getValues().size());
+        
+        prop.setString(false, ref, "deu");
+        assertEquals("deu", prop.getString(false, ref));
+        assertEquals(2, prop.getValues().size());
+        
+        prop.setString(false, ref, "che");
+        assertEquals("che", prop.getString(false, ref));
+        assertEquals(3, prop.getValues().size());
+        
+        prop.setString(false, ref, "deu");
+        assertEquals("deu", prop.getString(false, ref));
+        assertEquals(3, prop.getValues().size());
+    }
+    
+    @Test
+    public void testStoreTooManyEntries() {
+        StringEncodedValue prop = new StringEncodedValue("country", 3);
+        prop.init(new EncodedValue.InitializerConfig());
+        
+        IntsRef ref = new IntsRef(1);
+        assertEquals(null, prop.getString(false, ref));
+
+        prop.setString(false, ref, "aut");
+        assertEquals("aut", prop.getString(false, ref));
+        
+        prop.setString(false, ref, "deu");
+        assertEquals("deu", prop.getString(false, ref));
+        
+        prop.setString(false, ref, "che");
+        assertEquals("che", prop.getString(false, ref));
+        
+        try {
+            prop.setString(false, ref, "xyz");
+            fail("The encoded value should only allow a limited number of values");
+        } catch (IllegalStateException e) {
+            assertTrue(e.getMessage().startsWith("Maximum number of values reached for"));
+        }
+    }
+}
\ No newline at end of file