Repository: graphhopper/graphhopper
Pull request #2326 (2021-05-18 08:42:53)

Use bbox intersection logic for rectangles
If a polygon describes a rectangle, we don't need to load the way geometry to check for an intersection.
This should be especially beneficial for large areas, for which we previously had to load all intersecting edge geometries.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e4503e64b..66edb4954 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
 ### 4.0 [not yet released]
 
 - added smoothness encoded value, used to determine bike speed (#2303)
+- maps: custom_model is now included in URL (#2328)
+- maps/isochrone: works for different profiles now (#2332)  
+- there is no stable tag anymore, either use master or one of the release branches like 2.x, 3.x, ...  
+- PointList#getSize() -> PointList#size()
 - migrated tests from junit 4 to 5 (#2324)
 
 ### 3.0 [17 May 2021]
diff --git a/README.md b/README.md
index 0c72dac50..a40026fe7 100644
--- a/README.md
+++ b/README.md
@@ -55,6 +55,11 @@ you'll see a map of Berlin. You should be able to right click on the map to crea
 
 For more details about the installation, see [here](./docs/web/quickstart.md).
 
+### Docker
+
+The Docker images created by the community from the `master` branch can be found [here](https://hub.docker.com/r/israelhikingmap/graphhopper)
+(currently daily). See the [Dockerfile](https://github.com/IsraelHikingMap/graphhopper-docker-image-push) for more details.
+
 ## GraphHopper Maps
 
 To see the road routing feature of GraphHopper in action please go to [GraphHopper Maps](https://graphhopper.com/maps).
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomModelParser.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomModelParser.java
index b61851f6f..3f759e8f5 100644
--- a/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomModelParser.java
+++ b/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomModelParser.java
@@ -428,7 +428,7 @@ public class CustomModelParser {
         List<List<Statement>> blocks = splitIntoBlocks(speedStatements);
         for (List<Statement> block : blocks)
             result = getMaxSpeedForBlock(block, result);
-        if (maxSpeed <= 0)
+        if (result <= 0)
             throw new IllegalStateException("max speed is <= 0");
         return result;
     }
diff --git a/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomWeightingHelper.java b/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomWeightingHelper.java
index 5657e2efb..5b1d279e2 100644
--- a/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomWeightingHelper.java
+++ b/core/src/main/java/com/graphhopper/routing/weighting/custom/CustomWeightingHelper.java
@@ -59,8 +59,10 @@ public class CustomWeightingHelper {
 
     public static boolean in(Polygon p, EdgeIteratorState edge) {
         BBox bbox = GHUtility.createBBox(edge);
-        if (p.getBounds().intersects(bbox))
-            return p.intersects(edge.fetchWayGeometry(FetchMode.ALL).makeImmutable()); // TODO PERF: cache bbox and edge wayGeometry for multiple area
-        return false;
+        if (!p.getBounds().intersects(bbox))
+            return false;
+        if (p.isRectangle())
+            return true;
+        return p.intersects(edge.fetchWayGeometry(FetchMode.ALL).makeImmutable()); // TODO PERF: cache bbox and edge wayGeometry for multiple area
     }
 }
diff --git a/core/src/main/java/com/graphhopper/storage/GraphEdgeIdFinder.java b/core/src/main/java/com/graphhopper/storage/GraphEdgeIdFinder.java
index fbc15a996..894e6e954 100644
--- a/core/src/main/java/com/graphhopper/storage/GraphEdgeIdFinder.java
+++ b/core/src/main/java/com/graphhopper/storage/GraphEdgeIdFinder.java
@@ -226,6 +226,8 @@ public class GraphEdgeIdFinder {
 
                 Shape shape = blockedShapes.get(shapeIdx);
                 if (shape.getBounds().intersects(bbox)) {
+                    if (shape instanceof Polygon && ((Polygon) shape).isRectangle())
+                        return true;
                     if (pointList == null)
                         pointList = edgeState.fetchWayGeometry(FetchMode.ALL).makeImmutable();
                     if (shape.intersects(pointList))
diff --git a/core/src/main/java/com/graphhopper/util/shapes/Polygon.java b/core/src/main/java/com/graphhopper/util/shapes/Polygon.java
index 5ec42c6e4..7aa9a7447 100644
--- a/core/src/main/java/com/graphhopper/util/shapes/Polygon.java
+++ b/core/src/main/java/com/graphhopper/util/shapes/Polygon.java
@@ -37,10 +37,12 @@ public class Polygon implements Shape {
 
     private final GeometryFactory factory = new GeometryFactory();
     public final PreparedGeometry prepPolygon;
+    public final boolean rectangle;
     public final Envelope envelope;
 
     public Polygon(PreparedPolygon prepPolygon) {
         this.prepPolygon = prepPolygon;
+        this.rectangle = prepPolygon.getGeometry().isRectangle();
         this.envelope = prepPolygon.getGeometry().getEnvelopeInternal();
     }
 
@@ -57,6 +59,7 @@ public class Polygon implements Shape {
         }
         coordinates[lats.length] = coordinates[0];
         this.prepPolygon = new PreparedPolygon(factory.createPolygon(new PackedCoordinateSequence.Double(coordinates, 2)));
+        this.rectangle = prepPolygon.getGeometry().isRectangle();
         this.envelope = prepPolygon.getGeometry().getEnvelopeInternal();
     }
 
@@ -99,6 +102,10 @@ public class Polygon implements Shape {
     public double getMaxLon() {
         return envelope.getMaxX();
     }
+    
+    public boolean isRectangle() {
+        return rectangle;
+    }
 
     @Override
     public String toString() {
diff --git a/docs/core/translations.md b/docs/core/translations.md
index fe0c25022..12678f997 100644
--- a/docs/core/translations.md
+++ b/docs/core/translations.md
@@ -30,7 +30,7 @@ want to try your changes or want to speed up the integration you can do the foll
 
  * Make GraphHopper working on your computer, where you need to git clone the repository - see [here](./quickstart-from-source.md) for more information.
  * If you created a new language then add it in lexicographical order to TranslationMap.LOCALES (core/src/main/java/com/graphhopper/util) and to the script: core/files/update-translations.sh
- * Do `cd graphhopper/core; curl 'https://docs.google.com/spreadsheets/d/10HKSFmxGVEIO92loVQetVmjXT0qpf3EA2jxuQSSYTdU/export?format=tsv&id=10HKSFmxGVEIO92loVQetVmjXT0qpf3EA2jxuQSSYTdU&gid=0' > tmp.tsv`
+ * Do `cd graphhopper/core; curl -L 'https://docs.google.com/spreadsheets/d/10HKSFmxGVEIO92loVQetVmjXT0qpf3EA2jxuQSSYTdU/export?format=tsv&id=10HKSFmxGVEIO92loVQetVmjXT0qpf3EA2jxuQSSYTdU&gid=0' > tmp.tsv`
  * Then `./files/update-translations.sh tmp.tsv && rm tmp.tsv`
  * Now you can see your changes via `git diff`. Make sure that is the only one with `git status`
  * Now execute `mvn clean test` to see if you did not miss arguments in your translation (see point 2 in the questions above) and start
diff --git a/hmm-lib/pom.xml b/hmm-lib/pom.xml
index 23ebbc60f..cd4c313a8 100644
--- a/hmm-lib/pom.xml
+++ b/hmm-lib/pom.xml
@@ -58,14 +58,6 @@
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
     </properties>
 
-    <dependencies>
-        <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
-            <scope>test</scope>
-        </dependency>
-    </dependencies>
-
     <build>
         <plugins>
             <plugin>
diff --git a/map-matching/pom.xml b/map-matching/pom.xml
index a8c5cf25c..bea9275a1 100644
--- a/map-matching/pom.xml
+++ b/map-matching/pom.xml
@@ -28,11 +28,6 @@
             <artifactId>hmm-lib-external</artifactId>
             <version>${project.parent.version}</version>
         </dependency>
-        <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
-            <scope>test</scope>
-        </dependency>
         <dependency>
             <groupId>ch.qos.logback</groupId>
             <artifactId>logback-classic</artifactId>
diff --git a/web-bundle/src/main/resources/com/graphhopper/maps/isochrone/index.html b/web-bundle/src/main/resources/com/graphhopper/maps/isochrone/index.html
index 9298c1805..500063b46 100644
--- a/web-bundle/src/main/resources/com/graphhopper/maps/isochrone/index.html
+++ b/web-bundle/src/main/resources/com/graphhopper/maps/isochrone/index.html
@@ -45,6 +45,11 @@
         <label for='show-graph'> show GH graph</label>
     </div>
     <div id='iso-menu'>
+        <select id='isochrone-profile' type='text' v-model='isochroneProfile' @change='updateIsochrone'>
+            <option v-for='profile in isochroneProfiles' v-bind:value='profile'>
+                {{ profile }}
+            </option>
+        </select>
         <input id='isochrone-point' type='text' v-model='isochronePoint' @keyup.enter='updateIsochrone'>
         <input id='isochrone-radius' type='number' v-model='isochroneRadius' @change='updateIsochrone'>
         <label for='isochrone-radius'> iso radius (s)</label>
diff --git a/web-bundle/src/main/resources/com/graphhopper/maps/isochrone/index.js b/web-bundle/src/main/resources/com/graphhopper/maps/isochrone/index.js
index be1142f29..11e335bb5 100644
--- a/web-bundle/src/main/resources/com/graphhopper/maps/isochrone/index.js
+++ b/web-bundle/src/main/resources/com/graphhopper/maps/isochrone/index.js
@@ -23,7 +23,9 @@ var menu = new Vue({
         showGraph: false,
         isochroneRadius: 600,
         showSpt: false,
-        isochronePoint: undefined
+        isochronePoint: undefined,
+        isochroneProfile: undefined,
+        isochroneProfiles: []
     },
     methods: {
         changeLayer: function (event) {
@@ -74,9 +76,16 @@ var vectorStyle = 'https://api.maptiler.com/maps/basic/style.json?key=' + mapTil
 
 fetch('/info')
     .then(response => response.json())
-    .then(json => _drawMap(json.bbox))
+    .then(json => {
+        _updateProfiles(json.profiles);
+        _drawMap(json.bbox);
+        })
     .catch(e => console.error('Could not receive bbox from GH server', e));
 
+function _updateProfiles(profiles) {
+    menu.isochroneProfiles = profiles.map(p => p.name)
+    menu.isochroneProfile = menu.isochroneProfiles[0]
+}
 // the mapbox map object used in various places here
 var map;
 
@@ -158,7 +167,8 @@ function fetchAndDrawSPT(point) {
     var counter = 0;
     var coordinates = [];
     var radius = menu.isochroneRadius;
-    Papa.parse("http://" + window.location.host + "/spt?profile=car&point=" + point.lat + "," + point.lng + "&columns=prev_longitude,prev_latitude,longitude,latitude,distance,time&time_limit=" + radius, {
+    var profile = menu.isochroneProfile;
+    Papa.parse("http://" + window.location.host + "/spt?profile=" + profile + "&point=" + point.lat + "," + point.lng + "&columns=prev_longitude,prev_latitude,longitude,latitude,distance,time&time_limit=" + radius, {
         download: true,
         worker: true,
         step: function (results) {
@@ -191,7 +201,8 @@ function fetchAndDrawSPT(point) {
 
 function fetchAndDrawIsoline(point) {
     var radius = menu.isochroneRadius;
-    fetch("/isochrone?profile=car&point=" + point.lat + "," + point.lng + "&time_limit=" + radius)
+    var profile = menu.isochroneProfile;
+    fetch("/isochrone?profile=" + profile + "&point=" + point.lat + "," + point.lng + "&time_limit=" + radius)
         .then(response => response.json())
         .then(data => {
             console.log('isoline took: ' + data.info.took + 'ms');
diff --git a/web-bundle/src/main/resources/com/graphhopper/maps/js/graphhopper/GHRequest.js b/web-bundle/src/main/resources/com/graphhopper/maps/js/graphhopper/GHRequest.js
index 658343aaa..4e0146d26 100644
--- a/web-bundle/src/main/resources/com/graphhopper/maps/js/graphhopper/GHRequest.js
+++ b/web-bundle/src/main/resources/com/graphhopper/maps/js/graphhopper/GHRequest.js
@@ -196,7 +196,7 @@ GHRequest.prototype.createGPXURL = function (withRoute, withTrack, withWayPoints
 };
 
 GHRequest.prototype.createHistoryURL = function () {
-    var skip = {"key": true};
+    var skip = {"key": true, "custom_model": true};
     return this.createPath("?" + this.createPointParams(true), skip) + "&use_miles=" + !!this.useMiles + (this.selectedDetail ? "&selected_detail=" + this.selectedDetail : "");
 };
 
diff --git a/web-bundle/src/main/resources/com/graphhopper/maps/js/instructions.js b/web-bundle/src/main/resources/com/graphhopper/maps/js/instructions.js
index a222be658..839e16d63 100644
--- a/web-bundle/src/main/resources/com/graphhopper/maps/js/instructions.js
+++ b/web-bundle/src/main/resources/com/graphhopper/maps/js/instructions.js
@@ -96,8 +96,6 @@ module.exports.create = function (mapLayer, path, urlForHistory, request) {
 
     hiddenDiv.append("<span>" + infoStr + "</span>");
 
-    var exportLink = $("#export-link a");
-    exportLink.attr('href', urlForHistory);
     var osmRouteLink = $("<br/><a>view on OSM</a>");
 
     var osmVehicle = request.getVehicle();
diff --git a/web-bundle/src/main/resources/com/graphhopper/maps/js/main-template.js b/web-bundle/src/main/resources/com/graphhopper/maps/js/main-template.js
index 784ca12d6..ef81d3ece 100644
--- a/web-bundle/src/main/resources/com/graphhopper/maps/js/main-template.js
+++ b/web-bundle/src/main/resources/com/graphhopper/maps/js/main-template.js
@@ -75,9 +75,7 @@ $(document).ready(function (e) {
     var cmEditor = customModelEditor.create({}, function (element) {
         $("#custom-model-editor").append(element);
     });
-    // todo: the idea was to highlight everything so if we start typing the example is overwritten. But unfortunately
-    // this does not work. And not even sure this is so useful?
-    showCustomModelExample();
+
     cmEditor.validListener = function(valid) {
         $("#custom-model-search-button").prop('disabled', !valid);
     };
@@ -128,6 +126,20 @@ $(document).ready(function (e) {
         return false;
     });
 
+    $("#export-link").click(function (e) {
+        try {
+          e.preventDefault();
+          var url = location.href;
+          if(url.indexOf("?") > 0)
+            url = url.substring(0, url.indexOf("?")) + ghRequest.createHistoryURL() + "&layer=" + encodeURIComponent(tileLayers.activeLayerName);
+          if(ghRequest.cmEditorActive) {
+            var text = cmEditor.value.replaceAll("&","%26");
+            url += "&custom_model=" + new URLSearchParams(JSON.stringify(JSON.parse(text))).toString();
+          }
+          navigator.clipboard.writeText(url).then(() => { alert('Link copied to clipboard'); });
+        } catch(e) { console.warn(e); }
+    });
+
     var sendCustomData = function () {
         ghRequest.ignoreCustomErrors = false;
         mySubmit();
@@ -164,13 +176,22 @@ $(document).ready(function (e) {
 
     var urlParams = urlTools.parseUrlWithHisto();
 
-    var customURL = urlParams.load_custom;
-    if(urlParams.load_custom)
+    var customModelJSON = urlParams.custom_model;
+    if(customModelJSON) {
         toggleCustomModelBox(false);
-    if(customURL && ghenv.environment === 'development')
-        $.ajax(customURL).
-            done(function(data) { cmEditor.value = data; sendCustomData(); }).
-            fail(function(err)  { console.log("Cannot load custom URL " + customURL); });
+        cmEditor.value = customModelJSON; // if json parsing fails we still have the partial custom model in the box
+        try {
+            var tmpObj = JSON.parse(customModelJSON);
+            cmEditor.value = JSON.stringify(tmpObj, null, 2);
+        } catch(e) {
+            console.warn('cannot pretty print custom model: ' + e);
+        }
+
+    } else {
+        // todo: the idea was to highlight everything so if we start typing the example is overwritten. But unfortunately
+        // this does not work. And not even sure this is so useful?
+        showCustomModelExample();
+    }
 
     $.when(ghRequest.fetchTranslationMap(urlParams.locale), ghRequest.getInfo())
             .then(function (arg1, arg2) {
@@ -899,12 +920,15 @@ function doCustomRequest(request, routeResultsDiv) {
         elevation: ghRequest.api_params.elevation,
         profile: ghRequest.api_params.profile,
         custom_model: customModel,
+        locale: ghRequest.api_params.locale,
         "ch.disable": true,
         details: details
     }
+    var reqURL = host + "/route";
+    if(ghRequest.api_params.key) reqURL += "?key=" + ghRequest.api_params.key;
 
     $.ajax({
-        url: host + "/route",
+        url: reqURL,
         type: "POST",
         contentType: 'application/json; charset=utf-8',
         dataType: "json",
diff --git a/web-bundle/src/main/resources/com/graphhopper/maps/js/map.js b/web-bundle/src/main/resources/com/graphhopper/maps/js/map.js
index 48772f078..33094713c 100644
--- a/web-bundle/src/main/resources/com/graphhopper/maps/js/map.js
+++ b/web-bundle/src/main/resources/com/graphhopper/maps/js/map.js
@@ -145,9 +145,6 @@ function initMap(bounds, setStartCoord, setIntermediateCoord, setEndCoord, selec
     map.on('baselayerchange', function (a) {
         if (a.name) {
             tileLayers.activeLayerName = a.name;
-            $("#export-link a").attr('href', function (i, v) {
-                return v.replace(/(layer=)([\w\s]+)/, '$1' + tileLayers.activeLayerName);
-            });
         }
     });
 
@@ -364,7 +361,7 @@ function getColorMapping(detail) {
             var palette = ['#332288', '#88ccee', '#44aa99', '#117733', '#999933', '#ddcc77', '#cc6677', '#882255', '#aa4499'];
             var missingColor = '#dddddd';
             var index = values.indexOf(data) % palette.length;
-            var color = data === 'missing' || data === 'unclassified'
+            var color = data === 'missing' || data === 'unclassified' || data === 'Undefined'
                 ? missingColor
                 : palette[index];
             return {


Test patch:
diff --git a/core/src/test/java/com/graphhopper/routing/DirectedRoutingTest.java b/core/src/test/java/com/graphhopper/routing/DirectedRoutingTest.java
index 59346769e..6ef473520 100644
--- a/core/src/test/java/com/graphhopper/routing/DirectedRoutingTest.java
+++ b/core/src/test/java/com/graphhopper/routing/DirectedRoutingTest.java
@@ -183,7 +183,7 @@ public class DirectedRoutingTest {
     private static class RepeatedFixtureProvider implements ArgumentsProvider {
         @Override
         public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
-            return Stream.generate(() -> new FixtureProvider().provideArguments(context)).flatMap(s -> s).limit(10);
+            return Stream.generate(() -> new FixtureProvider().provideArguments(context)).limit(10).flatMap(s -> s);
         }
     }
 
diff --git a/core/src/test/java/com/graphhopper/routing/RandomizedRoutingTest.java b/core/src/test/java/com/graphhopper/routing/RandomizedRoutingTest.java
index 840628b83..bca7c5e80 100644
--- a/core/src/test/java/com/graphhopper/routing/RandomizedRoutingTest.java
+++ b/core/src/test/java/com/graphhopper/routing/RandomizedRoutingTest.java
@@ -109,12 +109,11 @@ public class RandomizedRoutingTest {
         private RoutingCHGraph routingCHGraph;
         private PrepareLandmarks lm;
 
-        public Fixture(Algo algo, boolean prepareCH, boolean prepareLM, TraversalMode traversalMode) {
+        Fixture(Algo algo, boolean prepareCH, boolean prepareLM, TraversalMode traversalMode) {
             this.algo = algo;
             this.prepareCH = prepareCH;
             this.prepareLM = prepareLM;
             this.traversalMode = traversalMode;
-
             maxTurnCosts = 10;
             dir = new RAMDirectory();
             // todo: this test only works with speedTwoDirections=false (as long as loops are enabled), otherwise it will
@@ -287,7 +286,7 @@ public class RandomizedRoutingTest {
     private static class RepeatedFixtureProvider implements ArgumentsProvider {
         @Override
         public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
-            return Stream.generate(() -> new FixtureProvider().provideArguments(context)).flatMap(s -> s).limit(5);
+            return Stream.generate(() -> new FixtureProvider().provideArguments(context)).limit(5).flatMap(s -> s);
         }
     }
 
diff --git a/core/src/test/java/com/graphhopper/search/StringIndexTest.java b/core/src/test/java/com/graphhopper/search/StringIndexTest.java
index 46911b5b0..c13adbce2 100644
--- a/core/src/test/java/com/graphhopper/search/StringIndexTest.java
+++ b/core/src/test/java/com/graphhopper/search/StringIndexTest.java
@@ -222,31 +222,34 @@ public class StringIndexTest {
         assertNull(index.get(pointerB, ""));
     }
 
-    @RepeatedTest(100)
+    @RepeatedTest(20)
     public void testRandom() {
-        StringIndex index = create();
         long seed = new Random().nextLong();
-        System.out.println("StringIndexText.testRandom seed:" + seed);
-        Random random = new Random(seed);
-        List<String> keys = createRandomList(random, "_key", 1000);
-        List<String> values = createRandomList(random, "_value", 5000);
-
-        int size = 20000;
-        LongArrayList pointers = new LongArrayList(size);
-        for (int i = 0; i < size; i++) {
-            Map<String, String> map = createRandomMap(random, keys, values);
-            long pointer = index.add(map);
-            try {
-                assertEquals(map.size(), index.getAll(pointer).size(), "" + i);
-            } catch (Exception ex) {
-                throw new RuntimeException(i + " " + map + ", " + pointer, ex);
+        try {
+            StringIndex index = create();
+            Random random = new Random(seed);
+            List<String> keys = createRandomList(random, "_key", 1000);
+            List<String> values = createRandomList(random, "_value", 5000);
+
+            int size = 20000;
+            LongArrayList pointers = new LongArrayList(size);
+            for (int i = 0; i < size; i++) {
+                Map<String, String> map = createRandomMap(random, keys, values);
+                long pointer = index.add(map);
+                try {
+                    assertEquals(map.size(), index.getAll(pointer).size(), "" + i);
+                } catch (Exception ex) {
+                    throw new RuntimeException(i + " " + map + ", " + pointer, ex);
+                }
+                pointers.add(pointer);
             }
-            pointers.add(pointer);
-        }
 
-        for (int i = 0; i < size; i++) {
-            Map<String, String> map = index.getAll(pointers.get(i));
-            assertTrue(map.size() > 0, i + " " + map);
+            for (int i = 0; i < size; i++) {
+                Map<String, String> map = index.getAll(pointers.get(i));
+                assertTrue(map.size() > 0, i + " " + map);
+            }
+        } catch (Throwable t) {
+            throw new RuntimeException("seed:" + seed + ", error:" + t);
         }
     }