diff --git a/core/src/main/java/com/graphhopper/routing/ev/DecimalEncodedValueImpl.java b/core/src/main/java/com/graphhopper/routing/ev/DecimalEncodedValueImpl.java index 6584da72e3d..1de0cbdfe83 100644 --- a/core/src/main/java/com/graphhopper/routing/ev/DecimalEncodedValueImpl.java +++ b/core/src/main/java/com/graphhopper/routing/ev/DecimalEncodedValueImpl.java @@ -17,6 +17,8 @@ */ package com.graphhopper.routing.ev; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import com.graphhopper.storage.IntsRef; /** @@ -68,6 +70,23 @@ public DecimalEncodedValueImpl(String name, int bits, double minValue, double fa throw new IllegalArgumentException("defaultIsInfinity cannot be true when minValue is negative"); } + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + DecimalEncodedValueImpl(@JsonProperty("name") String name, + @JsonProperty("bits") int bits, + @JsonProperty("min_value") int minValue, + @JsonProperty("max_value") int maxValue, + @JsonProperty("negate_reverse_direction") boolean negateReverseDirection, + @JsonProperty("store_two_directions") boolean storeTwoDirections, + @JsonProperty("factor") double factor, + @JsonProperty("default_is_infinity") boolean defaultIsInfinity, + @JsonProperty("use_maximum_as_infinity") boolean useMaximumAsInfinity) { + // we need this constructor for Jackson + super(name, bits, minValue, maxValue, negateReverseDirection, storeTwoDirections); + this.factor = factor; + this.defaultIsInfinity = defaultIsInfinity; + this.useMaximumAsInfinity = useMaximumAsInfinity; + } + @Override public void setDecimal(boolean reverse, IntsRef ref, double value) { if (!isInitialized()) diff --git a/core/src/main/java/com/graphhopper/routing/ev/EncodedValue.java b/core/src/main/java/com/graphhopper/routing/ev/EncodedValue.java index b7fc629397b..073bedb1eb2 100644 --- a/core/src/main/java/com/graphhopper/routing/ev/EncodedValue.java +++ b/core/src/main/java/com/graphhopper/routing/ev/EncodedValue.java @@ -17,11 +17,14 @@ */ package com.graphhopper.routing.ev; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + /** * This interface defines how to store and read values from a list of integers * * @see com.graphhopper.storage.IntsRef */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "className") public interface EncodedValue { /** diff --git a/core/src/main/java/com/graphhopper/routing/ev/EncodedValueSerializer.java b/core/src/main/java/com/graphhopper/routing/ev/EncodedValueSerializer.java new file mode 100644 index 00000000000..d34c4086803 --- /dev/null +++ b/core/src/main/java/com/graphhopper/routing/ev/EncodedValueSerializer.java @@ -0,0 +1,62 @@ +/* + * Licensed to GraphHopper GmbH under one or more contributor + * license agreements. See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + * + * GraphHopper GmbH licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.graphhopper.routing.ev; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.node.ObjectNode; + +public class EncodedValueSerializer { + private final static ObjectMapper MAPPER = new ObjectMapper(); + + static { + MAPPER.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); + MAPPER.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + MAPPER.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); + } + + public static String serializeEncodedValue(EncodedValue encodedValue) { + try { + JsonNode tree = MAPPER.valueToTree(encodedValue); + ((ObjectNode) tree).put("version", encodedValue.getVersion()); + return MAPPER.writeValueAsString(tree); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Could not serialize encoded value: " + encodedValue + ", error: " + e.getMessage()); + } + } + + public static EncodedValue deserializeEncodedValue(String serializedEncodedValue) { + try { + JsonNode jsonNode = MAPPER.readTree(serializedEncodedValue); + int storedVersion = jsonNode.get("version").asInt(); + ((ObjectNode) jsonNode).remove("version"); + EncodedValue encodedValue = MAPPER.treeToValue(jsonNode, EncodedValue.class); + if (storedVersion != encodedValue.getVersion()) + throw new IllegalStateException("Version does not match. Cannot properly read encoded value: " + encodedValue.getName() + ". " + + "You need to use the same version of GraphHopper you used to import the data"); + return encodedValue; + } catch (JsonProcessingException e) { + throw new IllegalStateException("Could not deserialize encoded value: " + serializedEncodedValue + ", error: " + e.getMessage()); + } + } +} diff --git a/core/src/main/java/com/graphhopper/routing/ev/EnumEncodedValue.java b/core/src/main/java/com/graphhopper/routing/ev/EnumEncodedValue.java index b2a71a8eb1f..f5d8bc19a04 100644 --- a/core/src/main/java/com/graphhopper/routing/ev/EnumEncodedValue.java +++ b/core/src/main/java/com/graphhopper/routing/ev/EnumEncodedValue.java @@ -17,6 +17,9 @@ */ package com.graphhopper.routing.ev; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import com.graphhopper.storage.IntsRef; import java.util.Arrays; @@ -25,7 +28,10 @@ * This class allows to store distinct values via an enum. I.e. it stores just the indices */ public final class EnumEncodedValue extends IntEncodedValueImpl { + @JsonIgnore private final E[] arr; + // needed for Jackson + private final Class enumType; public EnumEncodedValue(String name, Class enumType) { this(name, enumType, false); @@ -33,6 +39,21 @@ public EnumEncodedValue(String name, Class enumType) { public EnumEncodedValue(String name, Class enumType, boolean storeTwoDirections) { super(name, 32 - Integer.numberOfLeadingZeros(enumType.getEnumConstants().length - 1), storeTwoDirections); + this.enumType = enumType; + arr = enumType.getEnumConstants(); + } + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + EnumEncodedValue(@JsonProperty("name") String name, + @JsonProperty("bits") int bits, + @JsonProperty("min_value") int minValue, + @JsonProperty("max_value") int maxValue, + @JsonProperty("negate_reverse_direction") boolean negateReverseDirection, + @JsonProperty("store_two_directions") boolean storeTwoDirections, + @JsonProperty("enum_type") Class enumType) { + // we need this constructor for Jackson + super(name, bits, minValue, maxValue, negateReverseDirection, storeTwoDirections); + this.enumType = enumType; arr = enumType.getEnumConstants(); } diff --git a/core/src/main/java/com/graphhopper/routing/ev/IntEncodedValueImpl.java b/core/src/main/java/com/graphhopper/routing/ev/IntEncodedValueImpl.java index 4682b47e904..09f7114fb39 100644 --- a/core/src/main/java/com/graphhopper/routing/ev/IntEncodedValueImpl.java +++ b/core/src/main/java/com/graphhopper/routing/ev/IntEncodedValueImpl.java @@ -17,6 +17,9 @@ */ package com.graphhopper.routing.ev; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import com.graphhopper.routing.util.EncodingManager; import com.graphhopper.storage.IntsRef; import com.graphhopper.util.Helper; @@ -35,22 +38,29 @@ * and is used to save storage space. */ public class IntEncodedValueImpl implements IntEncodedValue { - private final String name; + private final boolean storeTwoDirections; + final int bits; + final boolean negateReverseDirection; + final int minValue; + final int maxValue; + // the following fields will be set by the init() method and we do not store them on disk, because they will be + // set again when we create the EncodingManager /** * There are multiple int values possible per edge. Here we specify the index into this integer array. */ + @JsonIgnore private int fwdDataIndex; + @JsonIgnore private int bwdDataIndex; - private final boolean storeTwoDirections; - final int bits; - final boolean negateReverseDirection; - final int minValue; - final int maxValue; + @JsonIgnore int fwdShift = -1; + @JsonIgnore int bwdShift = -1; + @JsonIgnore int fwdMask; + @JsonIgnore int bwdMask; /** @@ -94,6 +104,22 @@ public IntEncodedValueImpl(String name, int bits, int minValue, boolean negateRe this.negateReverseDirection = negateReverseDirection; } + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + IntEncodedValueImpl(@JsonProperty("name") String name, + @JsonProperty("bits") int bits, + @JsonProperty("min_value") int minValue, + @JsonProperty("max_value") int maxValue, + @JsonProperty("negate_reverse_direction") boolean negateReverseDirection, + @JsonProperty("store_two_directions") boolean storeTwoDirections) { + // we need this constructor for Jackson + this.name = name; + this.storeTwoDirections = storeTwoDirections; + this.bits = bits; + this.negateReverseDirection = negateReverseDirection; + this.minValue = minValue; + this.maxValue = maxValue; + } + @Override public final int init(EncodedValue.InitializerConfig init) { if (isInitialized()) diff --git a/core/src/main/java/com/graphhopper/routing/ev/SimpleBooleanEncodedValue.java b/core/src/main/java/com/graphhopper/routing/ev/SimpleBooleanEncodedValue.java index a856c15a8b2..095f1507cb8 100644 --- a/core/src/main/java/com/graphhopper/routing/ev/SimpleBooleanEncodedValue.java +++ b/core/src/main/java/com/graphhopper/routing/ev/SimpleBooleanEncodedValue.java @@ -17,13 +17,14 @@ */ package com.graphhopper.routing.ev; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import com.graphhopper.storage.IntsRef; /** * This class implements a simple boolean storage via an UnsignedIntEncodedValue with 1 bit. */ public final class SimpleBooleanEncodedValue extends IntEncodedValueImpl implements BooleanEncodedValue { - public SimpleBooleanEncodedValue(String name) { this(name, false); } @@ -32,6 +33,19 @@ public SimpleBooleanEncodedValue(String name, boolean storeBothDirections) { super(name, 1, storeBothDirections); } + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + SimpleBooleanEncodedValue( + @JsonProperty("name") String name, + @JsonProperty("bits") int bits, + @JsonProperty("minValue") int minValue, + @JsonProperty("maxValue") int maxValue, + @JsonProperty("negateReverseDirection") boolean negateReverseDirection, + @JsonProperty("storeTwoDirections") boolean storeTwoDirections + ) { + // we need this constructor for Jackson + super(name, bits, minValue, maxValue, negateReverseDirection, storeTwoDirections); + } + @Override public final void setBool(boolean reverse, IntsRef ref, boolean value) { setInt(reverse, ref, value ? 1 : 0); diff --git a/core/src/main/java/com/graphhopper/routing/ev/StringEncodedValue.java b/core/src/main/java/com/graphhopper/routing/ev/StringEncodedValue.java index 23754453e59..c70c207fa37 100644 --- a/core/src/main/java/com/graphhopper/routing/ev/StringEncodedValue.java +++ b/core/src/main/java/com/graphhopper/routing/ev/StringEncodedValue.java @@ -1,13 +1,10 @@ package com.graphhopper.routing.ev; -import com.carrotsearch.hppc.ObjectIntHashMap; -import com.carrotsearch.hppc.ObjectIntMap; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import com.graphhopper.storage.IntsRef; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; +import java.util.*; /** * This class holds a List of up to {@link #maxValues} encountered Strings and stores @@ -19,7 +16,7 @@ public final class StringEncodedValue extends IntEncodedValueImpl { private final int maxValues; private final List values; - private final ObjectIntMap indexMap; + private final Map indexMap; public StringEncodedValue(String name, int expectedValueCount) { this(name, expectedValueCount, false); @@ -30,7 +27,7 @@ public StringEncodedValue(String name, int expectedValueCount, boolean storeTwoD this.maxValues = roundUp(expectedValueCount); this.values = new ArrayList<>(maxValues); - this.indexMap = new ObjectIntHashMap<>(maxValues); + this.indexMap = new HashMap<>(maxValues); } public StringEncodedValue(String name, int bits, List values, boolean storeTwoDirections) { @@ -42,19 +39,40 @@ public StringEncodedValue(String name, int bits, List values, boolean st + values.size() + " > " + maxValues); this.values = new ArrayList<>(values); - this.indexMap = new ObjectIntHashMap<>(values.size()); + this.indexMap = new HashMap<>(values.size()); int index = 1; for (String value : values) { indexMap.put(value, index++); } } + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + StringEncodedValue( + @JsonProperty("name") String name, + @JsonProperty("bits") int bits, + @JsonProperty("min_value") int minValue, + @JsonProperty("max_value") int maxValue, + @JsonProperty("negate_reverse_direction") boolean negateReverseDirection, + @JsonProperty("store_two_directions") boolean storeTwoDirections, + @JsonProperty("max_values") int maxValues, + @JsonProperty("values") List values, + @JsonProperty("index_map") HashMap indexMap) { + // we need this constructor for Jackson + super(name, bits, minValue, maxValue, negateReverseDirection, storeTwoDirections); + if (values.size() > maxValues) + throw new IllegalArgumentException("Number of values is higher than the maximum value count: " + + values.size() + " > " + maxValues); + this.maxValues = maxValues; + this.values = values; + this.indexMap = indexMap; + } + public final void setString(boolean reverse, IntsRef ref, String value) { if (value == null) { super.setInt(reverse, ref, 0); return; } - int index = indexMap.get(value); + int index = indexMap.getOrDefault(value, 0); if (index == 0) { if (values.size() == maxValues) throw new IllegalStateException("Maximum number of values reached for " + getName() + ": " + maxValues); @@ -87,7 +105,7 @@ private static int roundUp(int value) { * @return the non-zero index of the String or 0 if it couldn't be found */ public int indexOf(String value) { - return indexMap.get(value); + return indexMap.getOrDefault(value, 0); } /** diff --git a/core/src/test/java/com/graphhopper/routing/ev/EncodedValueSerializerTest.java b/core/src/test/java/com/graphhopper/routing/ev/EncodedValueSerializerTest.java new file mode 100644 index 00000000000..97849f09ce1 --- /dev/null +++ b/core/src/test/java/com/graphhopper/routing/ev/EncodedValueSerializerTest.java @@ -0,0 +1,77 @@ +/* + * Licensed to GraphHopper GmbH under one or more contributor + * license agreements. See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + * + * GraphHopper GmbH licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.graphhopper.routing.ev; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class EncodedValueSerializerTest { + @Test + public void serializationAndDeserialization() { + List encodedValues = new ArrayList<>(); + // add enum, int, decimal and boolean encoded values + encodedValues.add(new EnumEncodedValue<>(RoadClass.KEY, RoadClass.class)); + encodedValues.add(Lanes.create()); + encodedValues.add(MaxWidth.create()); + encodedValues.add(GetOffBike.create()); + StringEncodedValue namesEnc = new StringEncodedValue("names", 3, Arrays.asList("jim", "joe", "kate"), false); + encodedValues.add(namesEnc); + + // serialize + List serializedEVs = new ArrayList<>(); + for (EncodedValue e : encodedValues) + serializedEVs.add(EncodedValueSerializer.serializeEncodedValue(e)); + + // deserialize + List deserializedEVs = new ArrayList<>(); + for (String s : serializedEVs) + deserializedEVs.add(EncodedValueSerializer.deserializeEncodedValue(s)); + + // look, it's all there! + EnumEncodedValue deserializedRoadClass = (EnumEncodedValue) deserializedEVs.get(0); + IntEncodedValue deserializedLanes = (IntEncodedValue) deserializedEVs.get(1); + DecimalEncodedValue deserializedMaxWidth = (DecimalEncodedValue) deserializedEVs.get(2); + BooleanEncodedValue deserializedGetOffBike = (BooleanEncodedValue) deserializedEVs.get(3); + StringEncodedValue deserializedNames = (StringEncodedValue) deserializedEVs.get(4); + assertEquals("road_class", deserializedRoadClass.getName()); + assertTrue(Arrays.toString(deserializedRoadClass.getValues()).contains("motorway")); + assertEquals("lanes", deserializedLanes.getName()); + assertEquals("max_width", deserializedMaxWidth.getName()); + assertEquals("get_off_bike", deserializedGetOffBike.getName()); + assertEquals("names", deserializedNames.getName()); + assertTrue(deserializedNames.getValues().contains("jim")); + } + + @Test + void wrongVersion() { + String serializedEV = "{\"className\":\"com.graphhopper.routing.ev.EnumEncodedValue\",\"name\":\"road_class\",\"bits\":5," + + "\"min_value\":0,\"max_value\":31,\"negate_reverse_direction\":false,\"store_two_directions\":false," + + "\"enum_type\":\"com.graphhopper.routing.ev.RoadClass\",\"version\":"; + // this fails, because the version is wrong + IllegalStateException e = assertThrows(IllegalStateException.class, () -> EncodedValueSerializer.deserializeEncodedValue(serializedEV + "404}")); + assertTrue(e.getMessage().contains("Version does not match"), e.getMessage()); + // this works + assertEquals("road_class", EncodedValueSerializer.deserializeEncodedValue(serializedEV + "979560347}").getName()); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/graphhopper/routing/ev/StringEncodedValueTest.java b/core/src/test/java/com/graphhopper/routing/ev/StringEncodedValueTest.java index 580661b3375..82b0f8082d0 100644 --- a/core/src/test/java/com/graphhopper/routing/ev/StringEncodedValueTest.java +++ b/core/src/test/java/com/graphhopper/routing/ev/StringEncodedValueTest.java @@ -31,7 +31,7 @@ public void testInitRoundUp() { assertEquals(0, init.dataIndex); assertEquals(0, init.shift); } - + @Test public void testInitSingle() { StringEncodedValue prop = new StringEncodedValue("country", 1); @@ -41,7 +41,7 @@ public void testInitSingle() { assertEquals(0, init.dataIndex); assertEquals(0, init.shift); } - + @Test public void testInitTooManyEntries() { List values = Arrays.asList("aut", "deu", "che", "fra"); @@ -52,17 +52,17 @@ public void testInitTooManyEntries() { 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 values = Arrays.asList("aut", "deu", "che"); @@ -71,15 +71,15 @@ public void testEquals() { 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()); @@ -87,37 +87,37 @@ public void testLookup() { 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");