Repository: graphhopper/graphhopper (java)

Issue:
Add a hash value for profiles
Right now its very easy to:

1) setup some profiles and do the import/preparation of the graph data
2) shut down the server
3) change the profiles in config.yml again
4) restart the server but 'forget' deleting the graph-cache folder
5) get bad routing results because the profiles no longer 'match' the preparations

One easy way to detect this error would be calculating and storing some kind of hash code for each profile.

See this discussion: https://github.com/graphhopper/graphhopper/issues/1999#issuecomment-618910167

Pull request:
check profile if used for LM or CH preparation
This fixes #2025

@easbar the CH and LM stuff is handled differently e.g. CH is coupled to the graph and so the LMConfig is only known at GraphHopper class level. And LM preparation is either loaded or created so this leads to the asymmetric new code (when comparing CH and LM) inside the GraphHopper class. 

It would be probably better to have this code in Landmark.createLandmarks & loadExisting but for this not only the LMConfig is necessary but also profilesByName. Same is for CH: this should be better encapsulated in PrepareContractionHierarchies.doWork and GraphHopperStorage.loadExisting (hmmh, ok this is also not correct and all the shortcuts etc. should be in CHGraphImpl or something or probably in a separate per-CHProfile-object)

Patch:
diff --git a/api/src/main/java/com/graphhopper/config/Profile.java b/api/src/main/java/com/graphhopper/config/Profile.java
index d70099b32..fd11d5dae 100644
--- a/api/src/main/java/com/graphhopper/config/Profile.java
+++ b/api/src/main/java/com/graphhopper/config/Profile.java
@@ -18,6 +18,7 @@
 
 package com.graphhopper.config;
 
+import com.graphhopper.util.Helper;
 import com.graphhopper.util.PMap;
 
 /**
@@ -97,7 +98,7 @@ public class Profile {
 
     @Override
     public String toString() {
-        return "name=" + name + "|vehicle=" + vehicle + "|weighting=" + weighting + "|turnCosts=" + turnCosts + "|hints=" + hints;
+        return createContentString();
     }
 
     @Override
@@ -108,8 +109,17 @@ public class Profile {
         return name.equals(profile.name);
     }
 
+    private String createContentString() {
+        // used to check against stored custom models, see #2026
+        return "name=" + name + "|vehicle=" + vehicle + "|weighting=" + weighting + "|turnCosts=" + turnCosts + "|hints=" + hints;
+    }
+
     @Override
     public int hashCode() {
         return name.hashCode();
     }
+
+    public int getVersion() {
+        return Helper.staticHashCode(createContentString());
+    }
 }
diff --git a/api/src/main/java/com/graphhopper/routing/util/CustomModel.java b/api/src/main/java/com/graphhopper/routing/util/CustomModel.java
index 66c6d9761..430324f0c 100644
--- a/api/src/main/java/com/graphhopper/routing/util/CustomModel.java
+++ b/api/src/main/java/com/graphhopper/routing/util/CustomModel.java
@@ -126,14 +126,13 @@ public class CustomModel {
 
     @Override
     public String toString() {
-        return "CustomModel{" +
-                "distanceInfluence=" + distanceInfluence +
-                ", speedFactor=" + speedFactorMap +
-                ", maxSpeed=" + maxSpeedMap +
-                ", maxSpeedFallback=" + maxSpeedFallback +
-                ", priorityMap=" + priorityMap +
-                ", #areas=" + areas.size() +
-                '}';
+        return createContentString();
+    }
+
+    private String createContentString() {
+        // used to check against stored custom models, see #2026
+        return "distanceInfluence=" + distanceInfluence + "|speedFactor=" + speedFactorMap + "|maxSpeed=" + maxSpeedMap +
+                "|maxSpeedFallback=" + maxSpeedFallback + "|priorityMap=" + priorityMap + "|areas=" + areas;
     }
 
     /**
diff --git a/api/src/main/java/com/graphhopper/util/Helper.java b/api/src/main/java/com/graphhopper/util/Helper.java
index 78b4530fe..dd1ab71aa 100644
--- a/api/src/main/java/com/graphhopper/util/Helper.java
+++ b/api/src/main/java/com/graphhopper/util/Helper.java
@@ -504,4 +504,19 @@ public class Helper {
         }
         return result;
     }
+
+    /**
+     * Produces a static hashcode for a string that is platform independent and still compatible to the default
+     * of openjdk. Do not use for performance critical applications.
+     *
+     * @see String#hashCode()
+     */
+    public static int staticHashCode(String str) {
+        int len = str.length();
+        int val = 0;
+        for (int idx = 0; idx < len; ++idx) {
+            val = 31 * val + str.charAt(idx);
+        }
+        return val;
+    }
 }
diff --git a/api/src/main/java/com/graphhopper/util/PMap.java b/api/src/main/java/com/graphhopper/util/PMap.java
index c91adce5a..9a463f673 100644
--- a/api/src/main/java/com/graphhopper/util/PMap.java
+++ b/api/src/main/java/com/graphhopper/util/PMap.java
@@ -17,11 +17,11 @@
  */
 package com.graphhopper.util;
 
-import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
 
 /**
- * A properties map (String to String) with convenient accessors
+ * A properties map (String to Object) with convenient methods to access the content.
  * <p>
  *
  * @author Peter Karich
@@ -34,20 +34,20 @@ public class PMap {
     }
 
     public PMap(int capacity) {
-        this(new HashMap<>(capacity));
+        this.map = new LinkedHashMap<>(capacity);
     }
 
     public PMap(Map<String, Object> map) {
-        this.map = new HashMap<>(map);
+        this.map = new LinkedHashMap<>(map);
     }
 
     public PMap(PMap map) {
-        this.map = new HashMap<>(map.map);
+        this.map = new LinkedHashMap<>(map.map);
     }
 
     public PMap(String propertiesString) {
         // five chosen as arbitrary initial capacity
-        this.map = new HashMap<>(5);
+        this.map = new LinkedHashMap<>(5);
 
         for (String s : propertiesString.split("\\|")) {
             s = s.trim();
@@ -157,7 +157,7 @@ public class PMap {
      * This method copies the underlying structure into a new Map object
      */
     public Map<String, Object> toMap() {
-        return new HashMap<>(map);
+        return new LinkedHashMap<>(map);
     }
 
     public boolean isEmpty() {
diff --git a/core/src/main/java/com/graphhopper/GraphHopper.java b/core/src/main/java/com/graphhopper/GraphHopper.java
index 5f0a775b9..acf7eaaf2 100644
--- a/core/src/main/java/com/graphhopper/GraphHopper.java
+++ b/core/src/main/java/com/graphhopper/GraphHopper.java
@@ -979,8 +979,15 @@ public class GraphHopper implements GraphHopperAPI {
 
         if (chPreparationHandler.isEnabled())
             chPreparationHandler.createPreparations(ghStorage);
-        if (!isCHPrepared())
+        if (isCHPrepared()) {
+            // check loaded profiles
+            for (CHProfile profile : chPreparationHandler.getCHProfiles()) {
+                if (!getProfileVersion(profile.getProfile()).equals("" + profilesByName.get(profile.getProfile()).getVersion()))
+                    throw new IllegalArgumentException("CH preparation of " + profile.getProfile() + " already exists in storage and doesn't match configuration");
+            }
+        } else {
             prepareCH(closeEarly);
+        }
     }
 
     protected void registerCustomEncodedValues(EncodingManager.Builder emBuilder) {
@@ -1263,16 +1270,24 @@ public class GraphHopper implements GraphHopperAPI {
     }
 
     private boolean isCHPrepared() {
-        return "true".equals(ghStorage.getProperties().get(CH.PREPARE + "done"))
-                // remove old property in >0.9
-                || "true".equals(ghStorage.getProperties().get("prepare.done"));
+        return "true".equals(ghStorage.getProperties().get(CH.PREPARE + "done"));
     }
 
-    private boolean isLMPrepared() {
-        return "true".equals(ghStorage.getProperties().get(Landmark.PREPARE + "done"));
+    private String getProfileVersion(String profile) {
+        return ghStorage.getProperties().get("graph.profiles." + profile + ".version");
+    }
+
+    private void setProfileVersion(String profile, int version) {
+        ghStorage.getProperties().put("graph.profiles." + profile + ".version", version);
     }
 
     protected void prepareCH(boolean closeEarly) {
+        for (CHProfile profile : chPreparationHandler.getCHProfiles()) {
+            if (!getProfileVersion(profile.getProfile()).isEmpty()
+                    && !getProfileVersion(profile.getProfile()).equals("" + profilesByName.get(profile.getProfile()).getVersion()))
+                throw new IllegalArgumentException("CH preparation of " + profile.getProfile() + " already exists in storage and doesn't match configuration");
+        }
+
         boolean tmpPrepare = chPreparationHandler.isEnabled();
         if (tmpPrepare) {
             ensureWriteAccess();
@@ -1286,6 +1301,10 @@ public class GraphHopper implements GraphHopperAPI {
             ghStorage.freeze();
             chPreparationHandler.prepare(ghStorage.getProperties(), closeEarly);
             ghStorage.getProperties().put(CH.PREPARE + "done", true);
+            for (CHProfile profile : chPreparationHandler.getCHProfiles()) {
+                // potentially overwrite existing keys from LM
+                setProfileVersion(profile.getProfile(), profilesByName.get(profile.getProfile()).getVersion());
+            }
         }
     }
 
@@ -1295,10 +1314,20 @@ public class GraphHopper implements GraphHopperAPI {
     protected void loadOrPrepareLM(boolean closeEarly) {
         boolean tmpPrepare = lmPreparationHandler.isEnabled() && !lmPreparationHandler.getPreparations().isEmpty();
         if (tmpPrepare) {
+            for (LMProfile profile : lmPreparationHandler.getLMProfiles()) {
+                if (!getProfileVersion(profile.getProfile()).isEmpty()
+                        && !getProfileVersion(profile.getProfile()).equals("" + profilesByName.get(profile.getProfile()).getVersion()))
+                    throw new IllegalArgumentException("LM preparation of " + profile.getProfile() + " already exists in storage and doesn't match configuration");
+            }
             ensureWriteAccess();
             ghStorage.freeze();
-            if (lmPreparationHandler.loadOrDoWork(ghStorage.getProperties(), closeEarly))
+            if (lmPreparationHandler.loadOrDoWork(ghStorage.getProperties(), closeEarly)) {
                 ghStorage.getProperties().put(Landmark.PREPARE + "done", true);
+                for (LMProfile profile : lmPreparationHandler.getLMProfiles()) {
+                    // potentially overwrite existing keys from CH
+                    setProfileVersion(profile.getProfile(), profilesByName.get(profile.getProfile()).getVersion());
+                }
+            }
         }
     }
 
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 3711ec460..281982a73 100644
--- a/core/src/main/java/com/graphhopper/routing/ev/UnsignedIntEncodedValue.java
+++ b/core/src/main/java/com/graphhopper/routing/ev/UnsignedIntEncodedValue.java
@@ -18,6 +18,7 @@
 package com.graphhopper.routing.ev;
 
 import com.graphhopper.storage.IntsRef;
+import com.graphhopper.util.Helper;
 
 import java.util.Arrays;
 import java.util.Locale;
@@ -178,7 +179,7 @@ public class UnsignedIntEncodedValue implements IntEncodedValue {
 
     @Override
     public int getVersion() {
-        int val = staticHashCode(name);
+        int val = Helper.staticHashCode(name);
         val = 31 * val + (storeTwoDirections ? 1231 : 1237);
         return staticHashCode(val, fwdDataIndex, bwdDataIndex, bits, maxValue, fwdShift, bwdShift, fwdMask, bwdMask);
     }
@@ -227,19 +228,4 @@ public class UnsignedIntEncodedValue implements IntEncodedValue {
         long var2 = Double.doubleToLongBits(val);
         return (int) (var2 ^ var2 >>> 32);
     }
-
-    /**
-     * Produces a static hashcode for a string that is platform independent and still compatible to the default
-     * of openjdk. Do not use for performance critical applications.
-     *
-     * @see String#hashCode()
-     */
-    static int staticHashCode(String str) {
-        int len = str.length();
-        int val = 0;
-        for (int idx = 0; idx < len; ++idx) {
-            val = 31 * val + str.charAt(idx);
-        }
-        return val;
-    }
 }
diff --git a/core/src/main/java/com/graphhopper/routing/lm/LandmarkStorage.java b/core/src/main/java/com/graphhopper/routing/lm/LandmarkStorage.java
index 1f5866fae..e8de0cb66 100644
--- a/core/src/main/java/com/graphhopper/routing/lm/LandmarkStorage.java
+++ b/core/src/main/java/com/graphhopper/routing/lm/LandmarkStorage.java
@@ -36,7 +36,6 @@ import com.graphhopper.routing.weighting.Weighting;
 import com.graphhopper.storage.*;
 import com.graphhopper.util.*;
 import com.graphhopper.util.exceptions.ConnectionNotFoundException;
-import com.graphhopper.util.shapes.BBox;
 import com.graphhopper.util.shapes.GHPoint;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -227,28 +226,6 @@ public class LandmarkStorage implements Storable<LandmarkStorage> {
             landmarkWeightDA.setShort(pointer, (short) SHORT_INFINITY);
         }
 
-        String additionalInfo = "";
-        // guess the factor
-        if (factor <= 0) {
-            // A 'factor' is necessary to store the weight in just a short value but without loosing too much precision.
-            // This factor is rather delicate to pick, we estimate it through the graph boundaries its maximum distance.
-            // For small areas we use max_bounds_dist*X and otherwise we use a big fixed value for this distance.
-            // If we would pick the distance too big for small areas this could lead to (slightly) suboptimal routes as there
-            // will be too big rounding errors. But picking it too small is dangerous regarding performance
-            // e.g. for Germany at least 1500km is very important otherwise speed is at least twice as slow e.g. for just 1000km
-            BBox bounds = graph.getBounds();
-            double distanceInMeter = Helper.DIST_EARTH.calcDist(bounds.maxLat, bounds.maxLon, bounds.minLat, bounds.minLon) * 7;
-            if (distanceInMeter > 50_000 * 7 || /* for tests and convenience we do for now: */ !bounds.isValid())
-                distanceInMeter = 30_000_000;
-
-            double maxWeight = weighting.getMinWeight(distanceInMeter);
-            setMaximumWeight(maxWeight);
-            additionalInfo = ", maxWeight:" + maxWeight + ", from max distance:" + distanceInMeter / 1000f + "km";
-        }
-
-        if (logDetails)
-            LOGGER.info("init landmarks for subnetworks with node count greater than " + minimumNodes + " with factor:" + factor + additionalInfo);
-
         int[] empty = new int[landmarks];
         Arrays.fill(empty, UNSET_SUBNETWORK);
         landmarkIDs.add(empty);
@@ -277,12 +254,30 @@ public class LandmarkStorage implements Storable<LandmarkStorage> {
             LOGGER.info("Calculated " + graphComponents.size() + " subnetworks via tarjan in " + sw.stop().getSeconds() + "s, " + Helper.getMemInfo());
 
         EdgeExplorer tmpExplorer = graph.createEdgeExplorer(new RequireBothDirectionsEdgeFilter(encoder));
+        String additionalInfo = "";
+        // guess the factor
+        if (factor <= 0) {
+            // A 'factor' is necessary to store the weight in just a short value but without loosing too much precision.
+            // This factor is rather delicate to pick, we estimate it from an exploration with some "test landmarks",
+            // see estimateMaxWeight. If we pick the distance too big for small areas this could lead to (slightly)
+            // suboptimal routes as there will be too big rounding errors. But picking it too small is bad for performance
+            // e.g. for Germany at least 1500km is very important otherwise speed is at least twice as slow e.g. for 1000km
+            double maxWeight = estimateMaxWeight(tmpExplorer, graphComponents, blockedEdges);
+            setMaximumWeight(maxWeight);
+            additionalInfo = ", maxWeight:" + maxWeight + " from quick estimation";
+        }
+
+        if (logDetails)
+            LOGGER.info("init landmarks for subnetworks with node count greater than " + minimumNodes + " with factor:" + factor + additionalInfo);
 
         int nodes = 0;
         for (IntArrayList subnetworkIds : graphComponents) {
             nodes += subnetworkIds.size();
             if (subnetworkIds.size() < minimumNodes)
                 continue;
+            if (factor <= 0)
+                throw new IllegalStateException("factor wasn't initialized " + factor + ", subnetworks:"
+                        + graphComponents.size() + ", minimumNodes:" + minimumNodes + ", current size:" + subnetworkIds.size());
 
             int index = subnetworkIds.size() - 1;
             // ensure start node is reachable from both sides and no subnetwork is associated
@@ -335,6 +330,51 @@ public class LandmarkStorage implements Storable<LandmarkStorage> {
         initialized = true;
     }
 
+    /**
+     * This method returns the maximum weight for the graph starting from the landmarks
+     */
+    private double estimateMaxWeight(EdgeExplorer requireBothDirExplorer, List<IntArrayList> graphComponents, IntHashSet blockedEdges) {
+        double maxWeight = 0;
+        int searchedSubnetworks = 0;
+        // the maximum weight can only be an approximation so there is only a tiny improvement when we would do this for
+        // all landmarks. See #2027 (1st commit) where only 1 landmark was sufficient when multiplied with 1.01 at the end
+        // TODO instead of calculating the landmarks again here we could store them in landmarkIDs and do this for all here
+        int[] tmpLandmarkNodeIds = new int[3];
+        for (IntArrayList subnetworkIds : graphComponents) {
+            if (subnetworkIds.size() < minimumNodes)
+                continue;
+
+            searchedSubnetworks++;
+            int index = subnetworkIds.size() - 1;
+            for (; index >= 0; index--) {
+                int nextStartNode = subnetworkIds.get(index);
+                if (GHUtility.count(requireBothDirExplorer.setBaseNode(nextStartNode)) > 0) {
+                    LandmarkExplorer explorer = findLandmarks(tmpLandmarkNodeIds, nextStartNode, blockedEdges);
+                    if (explorer.getFromCount() < minimumNodes)
+                        continue;
+
+                    // starting
+                    for (int lmIdx = 0; lmIdx < tmpLandmarkNodeIds.length; lmIdx++) {
+                        int lmNodeId = tmpLandmarkNodeIds[lmIdx];
+                        explorer = new LandmarkExplorer(graph, this, weighting, traversalMode, true);
+                        explorer.setStartNode(lmNodeId);
+                        explorer.setFilter(blockedEdges, true, true);
+                        explorer.runAlgo();
+                        maxWeight = Math.max(maxWeight, explorer.getLastEntry().weight);
+                    }
+                    break;
+                }
+            }
+        }
+
+        if (maxWeight <= 0 && searchedSubnetworks > 0)
+            throw new IllegalStateException("max weight wasn't set although " + searchedSubnetworks + " subnetworks were searched (total " + graphComponents.size() + "), minimumNodes:" + minimumNodes);
+
+        // we have to increase maxWeight slightly as it is only an approximation towards the maximum weight,
+        // especially when external landmarks are provided, but also because we do not traverse all landmarks
+        return maxWeight * 1.008;
+    }
+
     /**
      * This method creates landmarks for the specified subnetwork (integer list)
      *
@@ -343,7 +383,7 @@ public class LandmarkStorage implements Storable<LandmarkStorage> {
     private boolean createLandmarksForSubnetwork(final int startNode, final byte[] subnetworks, IntHashSet blockedEdges) {
         final int subnetworkId = landmarkIDs.size();
         int[] tmpLandmarkNodeIds = new int[landmarks];
-        int logOffset = Math.max(1, tmpLandmarkNodeIds.length / 2);
+        int logOffset = Math.max(1, landmarks / 2);
         boolean pickedPrecalculatedLandmarks = false;
 
         if (!landmarkSuggestions.isEmpty()) {
@@ -370,40 +410,14 @@ public class LandmarkStorage implements Storable<LandmarkStorage> {
         }
 
         if (pickedPrecalculatedLandmarks) {
-            LOGGER.info("Picked " + tmpLandmarkNodeIds.length + " landmark suggestions, skipped expensive landmark determination");
+            LOGGER.info("Picked " + tmpLandmarkNodeIds.length + " landmark suggestions, skip finding landmarks");
         } else {
-            // 1a) pick landmarks via special weighting for a better geographical spreading
-            Weighting initWeighting = lmSelectionWeighting;
-            LandmarkExplorer explorer = new LandmarkExplorer(graph, this, initWeighting, traversalMode, true);
-            explorer.setStartNode(startNode);
-            explorer.setFilter(blockedEdges, true, true);
-            explorer.runAlgo();
-
+            LandmarkExplorer explorer = findLandmarks(tmpLandmarkNodeIds, startNode, blockedEdges);
             if (explorer.getFromCount() < minimumNodes) {
                 // too small subnetworks are initialized with special id==0
                 explorer.setSubnetworks(subnetworks, UNCLEAR_SUBNETWORK);
                 return false;
             }
-
-            // 1b) we have one landmark, now determine the other landmarks
-            tmpLandmarkNodeIds[0] = explorer.getLastNode();
-            for (int lmIdx = 0; lmIdx < tmpLandmarkNodeIds.length - 1; lmIdx++) {
-                if (Thread.currentThread().isInterrupted()) {
-                    throw new RuntimeException("Thread was interrupted");
-                }
-                explorer = new LandmarkExplorer(graph, this, initWeighting, traversalMode, true);
-                explorer.setFilter(blockedEdges, true, true);
-                // set all current landmarks as start so that the next getLastNode is hopefully a "far away" node
-                for (int j = 0; j < lmIdx + 1; j++) {
-                    explorer.setStartNode(tmpLandmarkNodeIds[j]);
-                }
-                explorer.runAlgo();
-                tmpLandmarkNodeIds[lmIdx + 1] = explorer.getLastNode();
-                if (logDetails && lmIdx % logOffset == 0)
-                    LOGGER.info("Finding landmarks [" + lmConfig + "] in network [" + explorer.getVisitedNodes() + "]. "
-                            + "Progress " + (int) (100.0 * lmIdx / tmpLandmarkNodeIds.length) + "%, " + Helper.getMemInfo());
-            }
-
             if (logDetails)
                 LOGGER.info("Finished searching landmarks for subnetwork " + subnetworkId + " of size " + explorer.getVisitedNodes());
         }
@@ -411,7 +425,7 @@ public class LandmarkStorage implements Storable<LandmarkStorage> {
         // 2) calculate weights for all landmarks -> 'from' and 'to' weight
         for (int lmIdx = 0; lmIdx < tmpLandmarkNodeIds.length; lmIdx++) {
             if (Thread.currentThread().isInterrupted()) {
-                throw new RuntimeException("Thread was interrupted");
+                throw new RuntimeException("Thread was interrupted for landmark " + lmIdx);
             }
             int lmNodeId = tmpLandmarkNodeIds[lmIdx];
             LandmarkExplorer explorer = new LandmarkExplorer(graph, this, weighting, traversalMode, true);
@@ -521,7 +535,7 @@ public class LandmarkStorage implements Storable<LandmarkStorage> {
     final boolean setWeight(long pointer, double value) {
         double tmpVal = value / factor;
         if (tmpVal > Integer.MAX_VALUE)
-            throw new UnsupportedOperationException("Cannot store infinity explicitely, pointer=" + pointer + ", value: " + value);
+            throw new UnsupportedOperationException("Cannot store infinity explicitly, pointer=" + pointer + ", value=" + value + ", factor=" + factor);
 
         if (tmpVal >= SHORT_MAX) {
             landmarkWeightDA.setShort(pointer, (short) SHORT_MAX);
@@ -551,7 +565,8 @@ public class LandmarkStorage implements Storable<LandmarkStorage> {
         if (subnetworkFrom <= UNCLEAR_SUBNETWORK || subnetworkTo <= UNCLEAR_SUBNETWORK)
             return false;
         if (subnetworkFrom != subnetworkTo) {
-            throw new ConnectionNotFoundException("Connection between locations not found. Different subnetworks " + subnetworkFrom + " vs. " + subnetworkTo, new HashMap<String, Object>());
+            throw new ConnectionNotFoundException("Connection between locations not found. Different subnetworks " + subnetworkFrom
+                    + " vs. " + subnetworkTo, new HashMap<String, Object>());
         }
 
         int[] tmpIDs = landmarkIDs.get(subnetworkFrom);
@@ -714,15 +729,44 @@ public class LandmarkStorage implements Storable<LandmarkStorage> {
         return graph.getNodes();
     }
 
+    private LandmarkExplorer findLandmarks(int[] landmarkNodeIdsToReturn, int startNode, IntHashSet blockedEdges) {
+        int logOffset = Math.max(1, landmarkNodeIdsToReturn.length / 2);
+        // 1a) pick landmarks via special weighting for a better geographical spreading
+        Weighting initWeighting = lmSelectionWeighting;
+        LandmarkExplorer explorer = new LandmarkExplorer(graph, this, initWeighting, traversalMode, true);
+        explorer.setStartNode(startNode);
+        explorer.setFilter(blockedEdges, true, true);
+        explorer.runAlgo();
+
+        if (explorer.getFromCount() >= minimumNodes) {
+            // 1b) we have one landmark, now determine the other landmarks
+            landmarkNodeIdsToReturn[0] = explorer.getLastEntry().adjNode;
+            for (int lmIdx = 0; lmIdx < landmarkNodeIdsToReturn.length - 1; lmIdx++) {
+                explorer = new LandmarkExplorer(graph, this, initWeighting, traversalMode, true);
+                explorer.setFilter(blockedEdges, true, true);
+                // set all current landmarks as start so that the next getLastNode is hopefully a "far away" node
+                for (int j = 0; j < lmIdx + 1; j++) {
+                    explorer.setStartNode(landmarkNodeIdsToReturn[j]);
+                }
+                explorer.runAlgo();
+                landmarkNodeIdsToReturn[lmIdx + 1] = explorer.getLastEntry().adjNode;
+                if (logDetails && lmIdx % logOffset == 0)
+                    LOGGER.info("Finding landmarks [" + lmConfig + "] in network [" + explorer.getVisitedNodes() + "]. "
+                            + "Progress " + (int) (100.0 * lmIdx / landmarkNodeIdsToReturn.length) + "%, " + Helper.getMemInfo());
+            }
+        }
+        return explorer;
+    }
+
     /**
      * This class is used to calculate landmark location (equally distributed).
      * It derives from DijkstraBidirectionRef, but is only used as forward or backward search.
      */
     private static class LandmarkExplorer extends DijkstraBidirectionRef {
-        private int lastNode;
         // todo: rename 'from' to 'reverse' (and flip it) ? 'from' is used in many places for node ids and 'reverse' is mostly used for the direction
         private boolean from;
         private final LandmarkStorage lms;
+        private SPTEntry lastEntry;
 
         public LandmarkExplorer(Graph g, LandmarkStorage lms, Weighting weighting, TraversalMode tMode, boolean from) {
             super(g, weighting, tMode);
@@ -754,25 +798,23 @@ public class LandmarkStorage implements Storable<LandmarkStorage> {
             return bestWeightMapFrom.size();
         }
 
-        int getToCount() {
-            return bestWeightMapTo.size();
-        }
-
-        public int getLastNode() {
-            return lastNode;
-        }
-
         public void runAlgo() {
             super.runAlgo();
         }
 
+        SPTEntry getLastEntry() {
+            if (!finished())
+                throw new IllegalStateException("Cannot get max weight if not yet finished");
+            return lastEntry;
+        }
+
         @Override
         public boolean finished() {
             if (from) {
-                lastNode = currFrom.adjNode;
+                lastEntry = currFrom;
                 return finishedFrom;
             } else {
-                lastNode = currTo.adjNode;
+                lastEntry = currTo;
                 return finishedTo;
             }
         }
diff --git a/core/src/main/java/com/graphhopper/storage/CHConfig.java b/core/src/main/java/com/graphhopper/storage/CHConfig.java
index 4c0183baf..71b23ad2e 100644
--- a/core/src/main/java/com/graphhopper/storage/CHConfig.java
+++ b/core/src/main/java/com/graphhopper/storage/CHConfig.java
@@ -2,6 +2,7 @@ package com.graphhopper.storage;
 
 import com.graphhopper.routing.util.TraversalMode;
 import com.graphhopper.routing.weighting.Weighting;
+import com.graphhopper.util.Helper;
 
 import static com.graphhopper.config.Profile.validateProfileName;
 
@@ -69,5 +70,4 @@ public class CHConfig {
     public int hashCode() {
         return getName().hashCode();
     }
-
 }


Test patch:
diff --git a/core/src/test/java/com/graphhopper/routing/lm/PrepareLandmarksTest.java b/core/src/test/java/com/graphhopper/routing/lm/PrepareLandmarksTest.java
index d7f8c0cd5..2485e55fd 100644
--- a/core/src/test/java/com/graphhopper/routing/lm/PrepareLandmarksTest.java
+++ b/core/src/test/java/com/graphhopper/routing/lm/PrepareLandmarksTest.java
@@ -157,7 +157,7 @@ public class PrepareLandmarksTest {
 
         assertEquals(expectedPath.getWeight(), path.getWeight(), .1);
         assertEquals(expectedPath.calcNodes(), path.calcNodes());
-        assertEquals(expectedAlgo.getVisitedNodes() - 133, oneDirAlgoWithLandmarks.getVisitedNodes());
+        assertEquals(expectedAlgo.getVisitedNodes() - 135, oneDirAlgoWithLandmarks.getVisitedNodes());
 
         // landmarks with bidir A*
         RoutingAlgorithm biDirAlgoWithLandmarks = prepare.getRoutingAlgorithmFactory().createAlgo(graph,
@@ -180,7 +180,7 @@ public class PrepareLandmarksTest {
         expectedPath = expectedAlgo.calcPath(fromQR.getClosestNode(), toQR.getClosestNode());
         assertEquals(expectedPath.getWeight(), path.getWeight(), .1);
         assertEquals(expectedPath.calcNodes(), path.calcNodes());
-        assertEquals(expectedAlgo.getVisitedNodes() - 133, qGraphOneDirAlgo.getVisitedNodes());
+        assertEquals(expectedAlgo.getVisitedNodes() - 135, qGraphOneDirAlgo.getVisitedNodes());
     }
 
     @Test
@@ -202,7 +202,7 @@ public class PrepareLandmarksTest {
         assertEquals(Arrays.toString(new int[]{
                 2, 0
         }), Arrays.toString(plm.getLandmarkStorage().getLandmarks(1)));
-        assertEquals(4791, Math.round(plm.getLandmarkStorage().getFromWeight(0, 1) * expectedFactor));
+        assertEquals(4800, Math.round(plm.getLandmarkStorage().getFromWeight(0, 1) * expectedFactor));
 
         dir = new RAMDirectory(fileStr, true);
         plm = new PrepareLandmarks(dir, graph, lmConfig, 2);
@@ -211,7 +211,7 @@ public class PrepareLandmarksTest {
         assertEquals(Arrays.toString(new int[]{
                 2, 0
         }), Arrays.toString(plm.getLandmarkStorage().getLandmarks(1)));
-        assertEquals(4791, Math.round(plm.getLandmarkStorage().getFromWeight(0, 1) * expectedFactor));
+        assertEquals(4800, Math.round(plm.getLandmarkStorage().getFromWeight(0, 1) * expectedFactor));
 
         Helper.removeDir(new File(fileStr));
     }
diff --git a/core/src/test/java/com/graphhopper/storage/GraphHopperStorageCHTest.java b/core/src/test/java/com/graphhopper/storage/GraphHopperStorageCHTest.java
index 811970342..8283e7de7 100644
--- a/core/src/test/java/com/graphhopper/storage/GraphHopperStorageCHTest.java
+++ b/core/src/test/java/com/graphhopper/storage/GraphHopperStorageCHTest.java
@@ -684,5 +684,4 @@ public class GraphHopperStorageCHTest extends GraphHopperStorageTest {
                 .withTurnCosts(true)
                 .build();
     }
-
 }
diff --git a/reader-osm/src/test/java/com/graphhopper/GraphHopperIT.java b/reader-osm/src/test/java/com/graphhopper/GraphHopperIT.java
index 1a9dbf411..4ed0bdee6 100644
--- a/reader-osm/src/test/java/com/graphhopper/GraphHopperIT.java
+++ b/reader-osm/src/test/java/com/graphhopper/GraphHopperIT.java
@@ -1408,13 +1408,13 @@ public class GraphHopperIT {
         testCrossQueryAssert(profile3, hopper, 815.4, 146, true);
 
         // LM (should be the same as flex, but with less visited nodes!)
-        testCrossQueryAssert(profile1, hopper, 528.3, 106, false);
-        testCrossQueryAssert(profile2, hopper, 636.0, 78, false);
+        testCrossQueryAssert(profile1, hopper, 528.3, 74, false);
+        testCrossQueryAssert(profile2, hopper, 636.0, 84, false);
         // this is actually interesting: the number of visited nodes *increases* once again (while it strictly decreases
         // with rising distance factor for flex): cross-querying 'works', but performs *worse*, because the landmarks
         // were not customized for the weighting in use. Creating a separate LM preparation for profile3 yields 74
         // instead of 124 visited nodes (not shown here)
-        testCrossQueryAssert(profile3, hopper, 815.4, 124, false);
+        testCrossQueryAssert(profile3, hopper, 815.4, 128, false);
     }
 
     private void testCrossQueryAssert(String profile, GraphHopper hopper, double expectedWeight, int expectedVisitedNodes, boolean disableLM) {
diff --git a/reader-osm/src/test/java/com/graphhopper/reader/osm/GraphHopperOSMTest.java b/reader-osm/src/test/java/com/graphhopper/reader/osm/GraphHopperOSMTest.java
index 415b1c657..98d07cb49 100644
--- a/reader-osm/src/test/java/com/graphhopper/reader/osm/GraphHopperOSMTest.java
+++ b/reader-osm/src/test/java/com/graphhopper/reader/osm/GraphHopperOSMTest.java
@@ -33,6 +33,7 @@ import com.graphhopper.routing.lm.PrepareLandmarks;
 import com.graphhopper.routing.util.*;
 import com.graphhopper.routing.weighting.FastestWeighting;
 import com.graphhopper.routing.weighting.Weighting;
+import com.graphhopper.routing.weighting.custom.CustomProfile;
 import com.graphhopper.storage.CHConfig;
 import com.graphhopper.storage.GraphBuilder;
 import com.graphhopper.storage.GraphHopperStorage;
@@ -1117,4 +1118,92 @@ public class GraphHopperOSMTest {
                 setEncodingManager(em).
                 setProfiles(profiles);
     }
+
+    @Test
+    public void testLoadingLMAndCHProfiles() {
+        GraphHopper hopper = new GraphHopperOSM()
+                .setGraphHopperLocation(ghLoc)
+                .setDataReaderFile(testOsm)
+                .setEncodingManager(EncodingManager.create("car"))
+                .setProfiles(new Profile("car").setVehicle("car").setWeighting("fastest"));
+        hopper.getLMPreparationHandler().setLMProfiles(new LMProfile("car"));
+        hopper.getCHPreparationHandler().setCHProfiles(new CHProfile("car"));
+        hopper.importOrLoad();
+        hopper.close();
+
+        // load without problem
+        hopper = new GraphHopperOSM()
+                .setEncodingManager(EncodingManager.create("car"))
+                .setProfiles(new Profile("car").setVehicle("car").setWeighting("fastest"));
+        hopper.getLMPreparationHandler().setLMProfiles(new LMProfile("car"));
+        hopper.getCHPreparationHandler().setCHProfiles(new CHProfile("car"));
+        assertTrue(hopper.load(ghLoc));
+        hopper.close();
+
+        // problem: changed weighting in profile although LM preparation was enabled
+        hopper = new GraphHopperOSM()
+                .setEncodingManager(EncodingManager.create("car"))
+                .setProfiles(new Profile("car").setVehicle("car").setWeighting("shortest"));
+        hopper.getLMPreparationHandler().setLMProfiles(new LMProfile("car"));
+        // do not load CH
+        try {
+            assertFalse(hopper.load(ghLoc));
+            fail("load should fail");
+        } catch (Exception ex) {
+            assertEquals("LM preparation of car already exists in storage and doesn't match configuration", ex.getMessage());
+        } finally {
+            hopper.close();
+        }
+
+        // problem: changed weighting in profile although CH preparation was enabled
+        hopper = new GraphHopperOSM()
+                .setEncodingManager(EncodingManager.create("car"))
+                .setProfiles(new Profile("car").setVehicle("car").setWeighting("shortest"));
+        hopper.getCHPreparationHandler().setCHProfiles(new CHProfile("car"));
+        // do not load LM
+        try {
+            assertFalse(hopper.load(ghLoc));
+            fail("load should fail");
+        } catch (Exception ex) {
+            assertEquals("CH preparation of car already exists in storage and doesn't match configuration", ex.getMessage());
+        } finally {
+            hopper.close();
+        }
+    }
+
+    @Test
+    public void testLoadingCustomProfiles() {
+        CustomModel customModel = new CustomModel().setDistanceInfluence(123);
+        GraphHopper hopper = new GraphHopperOSM()
+                .setGraphHopperLocation(ghLoc)
+                .setDataReaderFile(testOsm)
+                .setEncodingManager(EncodingManager.create("car"))
+                .setProfiles(new CustomProfile("car").setCustomModel(customModel));
+        hopper.getLMPreparationHandler().setLMProfiles(new LMProfile("car"));
+        hopper.importOrLoad();
+        hopper.close();
+
+        // load without problem
+        hopper = new GraphHopperOSM()
+                .setEncodingManager(EncodingManager.create("car"))
+                .setProfiles(new CustomProfile("car").setCustomModel(customModel));
+        hopper.getLMPreparationHandler().setLMProfiles(new LMProfile("car"));
+        assertTrue(hopper.load(ghLoc));
+        hopper.close();
+
+        // do not load changed CustomModel
+        customModel.setDistanceInfluence(100);
+        hopper = new GraphHopperOSM()
+                .setEncodingManager(EncodingManager.create("car"))
+                .setProfiles(new CustomProfile("car").setCustomModel(customModel));
+        hopper.getLMPreparationHandler().setLMProfiles(new LMProfile("car"));
+        try {
+            assertFalse(hopper.load(ghLoc));
+            fail("load should fail");
+        } catch (Exception ex) {
+            assertEquals("LM preparation of car already exists in storage and doesn't match configuration", ex.getMessage());
+        } finally {
+            hopper.close();
+        }
+    }
 }