From 19e6e48c699e18e1a8f13e67d8e83cee31b836fb Mon Sep 17 00:00:00 2001 From: Takara Baumbach Date: Wed, 8 Jan 2025 16:57:32 +0100 Subject: [PATCH 01/20] feat: initial enabling of custom model parameter for route requests, requires GH changes to work --- .../api/requests/routing/RouteRequest.java | 18 +++++- .../routing/RouteRequestCustomModel.java | 35 +++++++++++ .../ors/api/services/RoutingService.java | 4 ++ .../ors/apitests/routing/ResultTest.java | 60 +++++++++++++++++++ .../routing/RouteRequestParameterNames.java | 1 + .../ors/routing/RouteSearchParameters.java | 12 +++- .../heigit/ors/routing/RoutingProfile.java | 10 +++- .../heigit/ors/routing/RoutingRequest.java | 4 ++ .../heigit/ors/routing/WeightingMethod.java | 4 ++ .../extensions/ORSWeightingFactory.java | 19 +++++- .../org/heigit/ors/util/ProfileTools.java | 4 ++ 11 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModel.java diff --git a/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequest.java b/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequest.java index 28bce6c676..33012fc7a0 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequest.java +++ b/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequest.java @@ -19,9 +19,10 @@ import io.swagger.v3.oas.annotations.extensions.Extension; import io.swagger.v3.oas.annotations.extensions.ExtensionProperty; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import org.heigit.ors.api.APIEnums; import org.heigit.ors.api.requests.common.APIRequest; import org.heigit.ors.exceptions.ParameterValueException; -import org.heigit.ors.api.APIEnums; import org.heigit.ors.routing.RouteRequestParameterNames; import org.heigit.ors.routing.RoutingErrorCodes; import org.heigit.ors.routing.RoutingProfileType; @@ -331,6 +332,13 @@ Specifies a list of pairs (bearings and deviations) to filter the segments of th @JsonIgnore private boolean hasIgnoreTransfers = false; + @Getter + @Schema(name = PARAM_CUSTOM_MODEL, description = "Specifies custom model for weighting.") + @JsonProperty(PARAM_CUSTOM_MODEL) + private RouteRequestCustomModel customModel; + @JsonIgnore + private boolean hasCustomModel = false; + @JsonCreator public RouteRequest(@JsonProperty(value = PARAM_COORDINATES, required = true) List> coordinates) { this.coordinates = coordinates; @@ -754,4 +762,12 @@ public boolean isPtRequest() { return convertRouteProfileType(profile) == RoutingProfileType.PUBLIC_TRANSPORT; } + public void setCustomModel(RouteRequestCustomModel customModel) { + this.customModel = customModel; + this.hasCustomModel = true; + } + + public boolean hasCustomModel() { + return hasCustomModel; + } } diff --git a/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModel.java b/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModel.java new file mode 100644 index 0000000000..4a64b56fcd --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModel.java @@ -0,0 +1,35 @@ +package org.heigit.ors.api.requests.routing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.graphhopper.jackson.StatementDeserializer; +import com.graphhopper.json.Statement; +import com.graphhopper.util.CustomModel; + +import java.util.ArrayList; +import java.util.List; + +public class RouteRequestCustomModel { + @JsonProperty("distance_influence") + private Double distanceInfluence; + + @JsonProperty("heading_penalty") + private double headingPenalty = (double) 300.0F; + + @JsonProperty("speed") + @JsonDeserialize(contentUsing = StatementDeserializer.class) + private List speedStatements = new ArrayList<>(); + + @JsonProperty("priority") + @JsonDeserialize(contentUsing = StatementDeserializer.class) + private List priorityStatements = new ArrayList<>(); + + public CustomModel toGHCustomModel() { + CustomModel customModel = new CustomModel(); + customModel.setDistanceInfluence(this.distanceInfluence); + customModel.setHeadingPenalty(this.headingPenalty); + this.speedStatements.forEach(customModel::addToSpeed); + this.priorityStatements.forEach(customModel::addToPriority); + return customModel; + } +} diff --git a/ors-api/src/main/java/org/heigit/ors/api/services/RoutingService.java b/ors-api/src/main/java/org/heigit/ors/api/services/RoutingService.java index 3df74d4ab8..4cf301d7b0 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/services/RoutingService.java +++ b/ors-api/src/main/java/org/heigit/ors/api/services/RoutingService.java @@ -205,6 +205,10 @@ else if (request.hasArrival()) params.setScheduleDuaration(request.getScheduleDuration()); } + if (request.hasCustomModel()) { + params.setCustomModel(request.getCustomModel().toGHCustomModel()); + } + params.setConsiderTurnRestrictions(false); routingRequest.setSearchParameters(params); diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java index 3bdb59014a..4d41a82ef2 100644 --- a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java +++ b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java @@ -140,6 +140,20 @@ public ResultTest() { addParameter("coordinatesPTFlipped", coordinatesPTFlipped); addParameter("coordinatesPT2", coordinatesPT2); + JSONArray coordinatesCustom = new JSONArray(); + JSONArray coordinatesCustom1 = new JSONArray(); + coordinatesCustom1.put(8.689885139465334); + coordinatesCustom1.put(49.40667302975234); +// coordinatesCustom1.put(8.660252); +// coordinatesCustom1.put(49.409744); + JSONArray coordinatesCustom2 = new JSONArray(); + coordinatesCustom2.put(8.7184506654739); + coordinatesCustom2.put(49.41430278032613); +// coordinatesCustom2.put(8.626949); +// coordinatesCustom2.put(49.371783); + coordinatesCustom.put(coordinatesCustom1); + coordinatesCustom.put(coordinatesCustom2); + addParameter("coordinatesCustom", coordinatesCustom); JSONArray extraInfo = new JSONArray(); extraInfo.put("surface"); @@ -3972,6 +3986,52 @@ void testPTFail(String coords, String walkingTime, int errorCode, int messageInd .statusCode(404); } + @Test + void testCustomProfile() { + JSONObject body = new JSONObject(); + body.put("coordinates", getParameter("coordinatesCustom")); + body.put("preference", getParameter("preference")); + body.put("instructions", true); + body.put("elevation", true); + +// given() +// .config(JSON_CONFIG_DOUBLE_NUMBERS) +// .headers(CommonHeaders.jsonContent) +// .pathParam("profile", getParameter("carProfile")) +// .body(body.toString()) +// .when() +// .post(getEndPointPath() + "/{profile}") +// .then().log().all() +// .assertThat() +// .body("any { it.key == 'routes' }", is(true)) +// .statusCode(200); + + JSONObject customModel = new JSONObject(); + JSONObject speed = new JSONObject(); + speed.put("if", true); + speed.put("limit_to", 100); + JSONObject priority = new JSONObject(); +// priority.put("if", "road_class == MOTORWAY"); + priority.put("if", "road_environment == TUNNEL"); + priority.put("multiply_by", 0); + customModel.put("speed", new JSONArray().put(speed)); + customModel.put("priority", new JSONArray().put(priority)); + customModel.put("distance_influence", 100); + body.put("custom_model", customModel); + + given() + .config(JSON_CONFIG_DOUBLE_NUMBERS) + .headers(CommonHeaders.jsonContent) + .pathParam("profile", getParameter("carProfile")) + .body(body.toString()) + .when() + .post(getEndPointPath() + "/{profile}") + .then().log().all() + .assertThat() + .body("any { it.key == 'routes' }", is(true)) + .statusCode(200); + } + private JSONArray constructBearings(String coordString) { JSONArray coordinates = new JSONArray(); String[] coordPairs = coordString.split("\\|"); diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/RouteRequestParameterNames.java b/ors-engine/src/main/java/org/heigit/ors/routing/RouteRequestParameterNames.java index ecc671ede4..2030e83512 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/RouteRequestParameterNames.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/RouteRequestParameterNames.java @@ -33,6 +33,7 @@ public interface RouteRequestParameterNames { String PARAM_VEHICLE_TYPE = "vehicle_type"; String PARAM_PROFILE_PARAMS = "profile_params"; String PARAM_AVOID_POLYGONS = "avoid_polygons"; + String PARAM_CUSTOM_MODEL = "custom_model"; // Fields specific to GraphHopper GTFS String PARAM_SCHEDULE = "schedule"; String PARAM_SCHEDULE_DURATION = "schedule_duration"; diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/RouteSearchParameters.java b/ors-engine/src/main/java/org/heigit/ors/routing/RouteSearchParameters.java index 5dc5a9636a..a71859ed4f 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/RouteSearchParameters.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/RouteSearchParameters.java @@ -13,6 +13,7 @@ */ package org.heigit.ors.routing; +import com.graphhopper.util.CustomModel; import lombok.Getter; import lombok.Setter; import org.heigit.ors.routing.graphhopper.extensions.HeavyVehicleAttributes; @@ -73,6 +74,9 @@ public class RouteSearchParameters { private boolean hasWalkingTime = false; private boolean hasScheduleDuration = false; + @Getter + private CustomModel customModel; + public int getProfileType() { return profileType; } @@ -327,7 +331,8 @@ public boolean requiresFullyDynamicWeights() { || hasBearings() || hasContinueStraight() || (getProfileParameters() != null && getProfileParameters().hasWeightings()) - || getAlternativeRoutesCount() > 0; + || getAlternativeRoutesCount() > 0 + || customModel != null; } // time-dependent stuff @@ -417,4 +422,9 @@ public boolean hasSchedule() { public boolean hasScheduleDuration() { return this.hasScheduleDuration; } + + public void setCustomModel(CustomModel customModel) { + this.customModel = customModel; + weightingMethod = WeightingMethod.CUSTOM; + } } diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java index 003c344107..915ff739bd 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java @@ -19,13 +19,14 @@ import com.graphhopper.config.Profile; import com.graphhopper.routing.util.EncodingManager; import com.graphhopper.routing.util.FlagEncoder; +import com.graphhopper.routing.weighting.custom.CustomProfile; import com.graphhopper.storage.ConditionalEdges; import com.graphhopper.storage.GraphHopperStorage; import com.graphhopper.storage.StorableProperties; +import com.graphhopper.util.CustomModel; import com.graphhopper.util.Helper; import com.graphhopper.util.PMap; import com.graphhopper.util.Parameters; -import com.graphhopper.util.shapes.BBox; import lombok.Getter; import org.apache.log4j.Logger; import org.heigit.ors.config.ElevationProperties; @@ -213,6 +214,13 @@ private static ORSGraphHopperConfig createGHSettings(ProfileProperties profile, profiles.put(profileName, new Profile(profileName).setVehicle(vehicle).setWeighting(weighting).setTurnCosts(false)); } + if (true) { // TODO: replace with config flag + if (hasTurnCosts) { + profiles.put(vehicle + "_custom_with_turn_costs", new CustomProfile(vehicle + "_custom_with_turn_costs").setCustomModel(new CustomModel()).setVehicle(vehicle).setTurnCosts(true)); + } + profiles.put(vehicle + "_custom", new CustomProfile(vehicle + "_custom").setCustomModel(new CustomModel()).setVehicle(vehicle).setTurnCosts(false)); + } + ghConfig.putObject(ProfileTools.KEY_PREPARE_CORE_WEIGHTINGS, "no"); if (profile.getBuild().getPreparation() != null) { PreparationProperties preparations = profile.getBuild().getPreparation(); diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingRequest.java b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingRequest.java index ec7d995b16..092774edeb 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingRequest.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingRequest.java @@ -388,6 +388,10 @@ else if (bearings[1] == null) if (props != null && !props.isEmpty()) req.getHints().putAll(props); + if (searchParams.getCustomModel() != null) { + req.setCustomModel(searchParams.getCustomModel()); + } + if (TemporaryUtilShelter.supportWeightingMethod(profileType)) { ProfileTools.setWeightingMethod(req.getHints(), weightingMethod, profileType, TemporaryUtilShelter.hasTimeDependentSpeed(searchParams, searchCntx)); if (routingProfile.requiresTimeDependentAlgorithm(searchParams, searchCntx)) diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/WeightingMethod.java b/ors-engine/src/main/java/org/heigit/ors/routing/WeightingMethod.java index 57fa0e54f1..4ea9136da8 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/WeightingMethod.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/WeightingMethod.java @@ -18,6 +18,7 @@ public class WeightingMethod { public static final int FASTEST = 1; public static final int SHORTEST = 2; public static final int RECOMMENDED = 3; + public static final int CUSTOM = 4; private WeightingMethod() { } @@ -29,6 +30,8 @@ public static int getFromString(String method) { return WeightingMethod.SHORTEST; } else if ("recommended".equalsIgnoreCase(method)) { return WeightingMethod.RECOMMENDED; + } else if ("custom".equalsIgnoreCase(method)) { + return WeightingMethod.CUSTOM; } return WeightingMethod.UNKNOWN; } @@ -38,6 +41,7 @@ public static String getName(int profileType) { case FASTEST -> "fastest"; case SHORTEST -> "shortest"; case RECOMMENDED -> "recommended"; + case CUSTOM -> "custom"; default -> ""; }; } diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSWeightingFactory.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSWeightingFactory.java index e608cebe4d..e5c25a356c 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSWeightingFactory.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSWeightingFactory.java @@ -6,8 +6,11 @@ import com.graphhopper.routing.util.EncodingManager; import com.graphhopper.routing.util.FlagEncoder; import com.graphhopper.routing.weighting.*; +import com.graphhopper.routing.weighting.custom.CustomModelParser; +import com.graphhopper.routing.weighting.custom.CustomProfile; import com.graphhopper.storage.ConditionalEdges; import com.graphhopper.storage.GraphHopperStorage; +import com.graphhopper.util.CustomModel; import com.graphhopper.util.Helper; import com.graphhopper.util.PMap; import com.graphhopper.util.Parameters; @@ -31,6 +34,7 @@ import static com.graphhopper.routing.weighting.TurnCostProvider.NO_TURN_COST_PROVIDER; import static com.graphhopper.routing.weighting.Weighting.INFINITE_U_TURN_COSTS; import static com.graphhopper.util.Helper.toLowerCase; +import static org.heigit.ors.util.ProfileTools.VAL_CUSTOM; /** * This class is a preliminary adaptation of ORSWeightingFactory to the new @@ -82,7 +86,20 @@ public Weighting createWeighting(Profile profile, PMap requestHints, boolean dis throw new IllegalArgumentException("You need to specify a weighting"); Weighting weighting = null; - if ("shortest".equalsIgnoreCase(weightingStr)) { + if (VAL_CUSTOM.equalsIgnoreCase(weightingStr)) { + if (!(profile instanceof CustomProfile)) { + throw new IllegalArgumentException("custom weighting requires a CustomProfile but was profile=" + profile.getName()); + } + + CustomModel queryCustomModel = (CustomModel) requestHints.getObject("custom_model", (Object) null); + CustomProfile customProfile = (CustomProfile) profile; + if (queryCustomModel != null) { + queryCustomModel.checkLMConstraints(customProfile.getCustomModel()); + } + + queryCustomModel = CustomModel.merge(customProfile.getCustomModel(), queryCustomModel); + weighting = CustomModelParser.createWeighting(encoder, this.encodingManager, turnCostProvider, queryCustomModel); + } else if ("shortest".equalsIgnoreCase(weightingStr)) { weighting = new ShortestWeighting(encoder, turnCostProvider); } else if ("fastest".equalsIgnoreCase(weightingStr) || "recommended".equalsIgnoreCase(weightingStr)) { if (encoder.supports(PriorityWeighting.class)) { diff --git a/ors-engine/src/main/java/org/heigit/ors/util/ProfileTools.java b/ors-engine/src/main/java/org/heigit/ors/util/ProfileTools.java index 0640340584..f9e35b1653 100644 --- a/ors-engine/src/main/java/org/heigit/ors/util/ProfileTools.java +++ b/ors-engine/src/main/java/org/heigit/ors/util/ProfileTools.java @@ -7,6 +7,7 @@ public class ProfileTools { public static final String VAL_RECOMMENDED = "recommended"; + public static final String VAL_CUSTOM = "custom"; private static final String KEY_WEIGHTING = "weighting"; private static final String KEY_WEIGHTING_METHOD = "weighting_method"; public static final String KEY_CH_DISABLE = "ch.disable"; @@ -64,6 +65,9 @@ public static void setWeightingMethod(PMap map, int requestWeighting, int profil } } + if (requestWeighting == WeightingMethod.CUSTOM) + weightingMethod = VAL_CUSTOM; + map.putObject(KEY_WEIGHTING_METHOD, weightingMethod); if (hasTimeDependentSpeed) From b47f4c8c37e3d40325f47f4a3887aad3ba1cc7de Mon Sep 17 00:00:00 2001 From: Takara Baumbach Date: Thu, 9 Jan 2025 11:11:22 +0100 Subject: [PATCH 02/20] feat: areas parameter --- .../routing/RouteRequestCustomModel.java | 9 ++- .../ors/apitests/routing/ResultTest.java | 60 +++++++++++++++++++ .../heigit/ors/routing/RoutingProfile.java | 4 +- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModel.java b/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModel.java index 4a64b56fcd..e2acf39725 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModel.java +++ b/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModel.java @@ -5,13 +5,16 @@ import com.graphhopper.jackson.StatementDeserializer; import com.graphhopper.json.Statement; import com.graphhopper.util.CustomModel; +import com.graphhopper.util.JsonFeature; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class RouteRequestCustomModel { @JsonProperty("distance_influence") - private Double distanceInfluence; + private double distanceInfluence; @JsonProperty("heading_penalty") private double headingPenalty = (double) 300.0F; @@ -24,12 +27,16 @@ public class RouteRequestCustomModel { @JsonDeserialize(contentUsing = StatementDeserializer.class) private List priorityStatements = new ArrayList<>(); + @JsonProperty("areas") + private Map areas = new HashMap(); + public CustomModel toGHCustomModel() { CustomModel customModel = new CustomModel(); customModel.setDistanceInfluence(this.distanceInfluence); customModel.setHeadingPenalty(this.headingPenalty); this.speedStatements.forEach(customModel::addToSpeed); this.priorityStatements.forEach(customModel::addToPriority); + customModel.setAreas(this.areas); return customModel; } } diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java index 4d41a82ef2..2a4a9d942e 100644 --- a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java +++ b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java @@ -4032,6 +4032,66 @@ void testCustomProfile() { .statusCode(200); } + @Test + void testCustomProfileAreas() { + JSONObject body = new JSONObject(); + body.put("coordinates", getParameter("coordinatesCustom")); + body.put("preference", getParameter("preference")); + body.put("instructions", true); + body.put("elevation", true); + + JSONObject customModel = new JSONObject(); + JSONObject priority = new JSONObject(); + priority.put("if", "in_custom1"); + priority.put("multiply_by", 0); + customModel.put("priority", new JSONArray().put(priority)); + + JSONObject areas = new JSONObject(); + JSONObject area1 = new JSONObject(); + area1.put("type", "Feature"); + area1.put("id", "something"); + JSONObject area1props = new JSONObject(); + area1.put("properties", area1props); + JSONObject area1geo = new JSONObject(); + area1geo.put("type", "Polygon"); + JSONArray area1coords = new JSONArray(); + JSONArray coordinate1 = new JSONArray(); + coordinate1.put(8.7062144); + coordinate1.put(49.4077481); + area1coords.put(coordinate1); + JSONArray coordinate2 = new JSONArray(); + coordinate2.put(8.7068045); + coordinate2.put(49.4108196); + area1coords.put(coordinate2); + JSONArray coordinate3 = new JSONArray(); + coordinate3.put(8.7132203); + coordinate3.put(49.4117201); + area1coords.put(coordinate3); + JSONArray coordinate4 = new JSONArray(); + coordinate4.put(8.7139713); + coordinate4.put(49.4084322); + area1coords.put(coordinate4); + area1coords.put(coordinate1); + area1geo.put("coordinates", new JSONArray().put(area1coords)); + area1.put("geometry", area1geo); + areas.put("custom1", area1); + customModel.put("areas", areas); + + body.put("custom_model", customModel); + + given() + .config(JSON_CONFIG_DOUBLE_NUMBERS) + .headers(CommonHeaders.jsonContent) + .pathParam("profile", getParameter("carProfile")) + .body(body.toString()) + .when().log().all() + .post(getEndPointPath() + "/{profile}") + .then().log().all() + .assertThat() + .body("any { it.key == 'routes' }", is(true)) + .statusCode(200); + } + private JSONArray constructBearings(String coordString) { JSONArray coordinates = new JSONArray(); String[] coordPairs = coordString.split("\\|"); diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java index 915ff739bd..16993409e4 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java @@ -216,9 +216,9 @@ private static ORSGraphHopperConfig createGHSettings(ProfileProperties profile, if (true) { // TODO: replace with config flag if (hasTurnCosts) { - profiles.put(vehicle + "_custom_with_turn_costs", new CustomProfile(vehicle + "_custom_with_turn_costs").setCustomModel(new CustomModel()).setVehicle(vehicle).setTurnCosts(true)); + profiles.put(vehicle + "_custom_with_turn_costs", new CustomProfile(vehicle + "_custom_with_turn_costs").setCustomModel(new CustomModel().setDistanceInfluence(0)).setVehicle(vehicle).setTurnCosts(true)); } - profiles.put(vehicle + "_custom", new CustomProfile(vehicle + "_custom").setCustomModel(new CustomModel()).setVehicle(vehicle).setTurnCosts(false)); + profiles.put(vehicle + "_custom", new CustomProfile(vehicle + "_custom").setCustomModel(new CustomModel().setDistanceInfluence(0)).setVehicle(vehicle).setTurnCosts(false)); } ghConfig.putObject(ProfileTools.KEY_PREPARE_CORE_WEIGHTINGS, "no"); From e8d77a54cf75b2552a39c1b3e1e1ee456ce63530 Mon Sep 17 00:00:00 2001 From: Sascha Fendrich Date: Thu, 9 Jan 2025 12:19:44 +0100 Subject: [PATCH 03/20] test: streamline custom profile API test --- .../ors/apitests/routing/ResultTest.java | 19 ++----------------- pom.xml | 4 ++-- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java index 2a4a9d942e..be6a11929d 100644 --- a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java +++ b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java @@ -3994,27 +3994,11 @@ void testCustomProfile() { body.put("instructions", true); body.put("elevation", true); -// given() -// .config(JSON_CONFIG_DOUBLE_NUMBERS) -// .headers(CommonHeaders.jsonContent) -// .pathParam("profile", getParameter("carProfile")) -// .body(body.toString()) -// .when() -// .post(getEndPointPath() + "/{profile}") -// .then().log().all() -// .assertThat() -// .body("any { it.key == 'routes' }", is(true)) -// .statusCode(200); - + // This custom model blocks tunnels JSONObject customModel = new JSONObject(); - JSONObject speed = new JSONObject(); - speed.put("if", true); - speed.put("limit_to", 100); JSONObject priority = new JSONObject(); -// priority.put("if", "road_class == MOTORWAY"); priority.put("if", "road_environment == TUNNEL"); priority.put("multiply_by", 0); - customModel.put("speed", new JSONArray().put(speed)); customModel.put("priority", new JSONArray().put(priority)); customModel.put("distance_influence", 100); body.put("custom_model", customModel); @@ -4029,6 +4013,7 @@ void testCustomProfile() { .then().log().all() .assertThat() .body("any { it.key == 'routes' }", is(true)) + .body("routes[0].summary.distance", is(closeTo(3338f, 40f))) .statusCode(200); } diff --git a/pom.xml b/pom.xml index 835fc36bde..8307b8de92 100644 --- a/pom.xml +++ b/pom.xml @@ -49,8 +49,8 @@ yyyy-MM-dd'T'HH:mm:ss'Z' - v4.9.5 - + + 4.9-SNAPSHOT 1.18.34 2.0.13 2.22.1 From 9905596639bfcb782d3f2fee725bfcf7d855d5da Mon Sep 17 00:00:00 2001 From: Sascha Fendrich Date: Thu, 9 Jan 2025 13:10:39 +0100 Subject: [PATCH 04/20] feat: add list of encoded values to StatusAPI In order to enable custom models one needs to be able to query which encoded values are active for which profile in ORS. --- .../java/org/heigit/ors/api/controllers/StatusAPI.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ors-api/src/main/java/org/heigit/ors/api/controllers/StatusAPI.java b/ors-api/src/main/java/org/heigit/ors/api/controllers/StatusAPI.java index 40df670424..bdd7794462 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/controllers/StatusAPI.java +++ b/ors-api/src/main/java/org/heigit/ors/api/controllers/StatusAPI.java @@ -15,6 +15,7 @@ package org.heigit.ors.api.controllers; +import com.graphhopper.routing.ev.EncodedValue; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import org.heigit.ors.api.config.EndpointsProperties; @@ -24,6 +25,7 @@ import org.heigit.ors.routing.RoutingProfile; import org.heigit.ors.routing.RoutingProfileManager; import org.heigit.ors.routing.RoutingProfileManagerStatus; +import org.json.JSONArray; import org.json.JSONObject; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -101,6 +103,11 @@ public ResponseEntity getStatus(HttpServletRequest request) throws Exception { if (profile.getBuild().getExtStorages() != null && !profile.getBuild().getExtStorages().isEmpty()) jProfileProps.put("storages", profile.getBuild().getExtStorages()); + var profile_evs = rp.getGraphhopper().getEncodingManager().getEncodedValues(); + if (profile_evs != null && !profile_evs.isEmpty()) { + JSONArray jEVs = new JSONArray(profile_evs.stream().map(EncodedValue::getName).toArray()); + jProfileProps.put("encoded_values",jEVs); + } jProfiles.put(profile.getProfileName(), jProfileProps); } From 32b0353eff62d057e30f663ce7c227dbdd0d94db Mon Sep 17 00:00:00 2001 From: Sascha Fendrich Date: Fri, 10 Jan 2025 13:54:42 +0100 Subject: [PATCH 05/20] test: add test for custom model blocking highway --- .../ors/apitests/routing/ResultTest.java | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java index be6a11929d..756d04fe38 100644 --- a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java +++ b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java @@ -3987,14 +3987,13 @@ void testPTFail(String coords, String walkingTime, int errorCode, int messageInd } @Test - void testCustomProfile() { + void testCustomProfileBlockTunnels() { JSONObject body = new JSONObject(); body.put("coordinates", getParameter("coordinatesCustom")); body.put("preference", getParameter("preference")); body.put("instructions", true); body.put("elevation", true); - // This custom model blocks tunnels JSONObject customModel = new JSONObject(); JSONObject priority = new JSONObject(); priority.put("if", "road_environment == TUNNEL"); @@ -4017,6 +4016,49 @@ void testCustomProfile() { .statusCode(200); } + @Test + void testCustomProfileBlockHighway() { + JSONObject body = new JSONObject(); + JSONArray coordinates = new JSONArray(); + JSONArray coord1 = new JSONArray(); + + coord1.put(8.64751); + coord1.put(49.41316); + coordinates.put(coord1); + JSONArray coord2 = new JSONArray(); + + coord2.put(8.623651); + coord2.put(49.371185); + coordinates.put(coord2); + + body.put("coordinates", coordinates); + body.put("preference", getParameter("preference")); + body.put("instructions", true); + body.put("elevation", true); + + JSONObject customModel = new JSONObject(); + JSONObject priority = new JSONObject(); + priority.put("if", "road_class == MOTORWAY"); + priority.put("multiply_by", 0); + customModel.put("priority", new JSONArray().put(priority)); + customModel.put("distance_influence", 100); + body.put("custom_model", customModel); + + given() + .config(JSON_CONFIG_DOUBLE_NUMBERS) + .headers(CommonHeaders.jsonContent) + .pathParam("profile", getParameter("carProfile")) + .body(body.toString()) + .when() + .post(getEndPointPath() + "/{profile}") + .then().log().all() + .assertThat() + .body("any { it.key == 'routes' }", is(true)) + .body("routes[0].summary.distance", is(closeTo(9039f, 80f))) + .body("routes[0].summary.duration", is(closeTo(895f, 9f))) + .statusCode(200); + } + @Test void testCustomProfileAreas() { JSONObject body = new JSONObject(); From 7484a8691df7132189d14b4c6473af4176252921 Mon Sep 17 00:00:00 2001 From: Takara Baumbach Date: Mon, 13 Jan 2025 10:27:44 +0100 Subject: [PATCH 06/20] test: add test for custom model using distance_influence --- .../ors/apitests/routing/ResultTest.java | 82 +++++++++++++++---- pom.xml | 4 +- 2 files changed, 66 insertions(+), 20 deletions(-) diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java index 756d04fe38..08770faa17 100644 --- a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java +++ b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java @@ -140,20 +140,27 @@ public ResultTest() { addParameter("coordinatesPTFlipped", coordinatesPTFlipped); addParameter("coordinatesPT2", coordinatesPT2); - JSONArray coordinatesCustom = new JSONArray(); JSONArray coordinatesCustom1 = new JSONArray(); - coordinatesCustom1.put(8.689885139465334); - coordinatesCustom1.put(49.40667302975234); -// coordinatesCustom1.put(8.660252); -// coordinatesCustom1.put(49.409744); + JSONArray coordinateCustom1 = new JSONArray(); + coordinateCustom1.put(8.689885139465334); + coordinateCustom1.put(49.40667302975234); + JSONArray coordinateCustom2 = new JSONArray(); + coordinateCustom2.put(8.7184506654739); + coordinateCustom2.put(49.41430278032613); + coordinatesCustom1.put(coordinateCustom1); + coordinatesCustom1.put(coordinateCustom2); + addParameter("coordinatesCustom1", coordinatesCustom1); + JSONArray coordinatesCustom2 = new JSONArray(); - coordinatesCustom2.put(8.7184506654739); - coordinatesCustom2.put(49.41430278032613); -// coordinatesCustom2.put(8.626949); -// coordinatesCustom2.put(49.371783); - coordinatesCustom.put(coordinatesCustom1); - coordinatesCustom.put(coordinatesCustom2); - addParameter("coordinatesCustom", coordinatesCustom); + JSONArray coordinateCustom3 = new JSONArray(); + coordinateCustom3.put(8.669232130050661); + coordinateCustom3.put(49.40850204416985); + JSONArray coordinateCustom4 = new JSONArray(); + coordinateCustom4.put(8.625125885009767); + coordinateCustom4.put(49.37098664229148); + coordinatesCustom2.put(coordinateCustom3); + coordinatesCustom2.put(coordinateCustom4); + addParameter("coordinatesCustom2", coordinatesCustom2); JSONArray extraInfo = new JSONArray(); extraInfo.put("surface"); @@ -3989,7 +3996,7 @@ void testPTFail(String coords, String walkingTime, int errorCode, int messageInd @Test void testCustomProfileBlockTunnels() { JSONObject body = new JSONObject(); - body.put("coordinates", getParameter("coordinatesCustom")); + body.put("coordinates", getParameter("coordinatesCustom1")); body.put("preference", getParameter("preference")); body.put("instructions", true); body.put("elevation", true); @@ -3999,7 +4006,6 @@ void testCustomProfileBlockTunnels() { priority.put("if", "road_environment == TUNNEL"); priority.put("multiply_by", 0); customModel.put("priority", new JSONArray().put(priority)); - customModel.put("distance_influence", 100); body.put("custom_model", customModel); given() @@ -4062,17 +4068,17 @@ void testCustomProfileBlockHighway() { @Test void testCustomProfileAreas() { JSONObject body = new JSONObject(); - body.put("coordinates", getParameter("coordinatesCustom")); + body.put("coordinates", getParameter("coordinatesCustom1")); body.put("preference", getParameter("preference")); body.put("instructions", true); body.put("elevation", true); + // This custom model blocks a certain area JSONObject customModel = new JSONObject(); JSONObject priority = new JSONObject(); priority.put("if", "in_custom1"); priority.put("multiply_by", 0); customModel.put("priority", new JSONArray().put(priority)); - JSONObject areas = new JSONObject(); JSONObject area1 = new JSONObject(); area1.put("type", "Feature"); @@ -4103,7 +4109,46 @@ void testCustomProfileAreas() { area1.put("geometry", area1geo); areas.put("custom1", area1); customModel.put("areas", areas); + body.put("custom_model", customModel); + + given() + .config(JSON_CONFIG_DOUBLE_NUMBERS) + .headers(CommonHeaders.jsonContent) + .pathParam("profile", getParameter("carProfile")) + .body(body.toString()) + .when().log().ifValidationFails() + .post(getEndPointPath() + "/{profile}") + .then().log().ifValidationFails() + .assertThat() + .body("any { it.key == 'routes' }", is(true)) + .body("routes[0].summary.distance", is(closeTo(3338f, 40f))) + .statusCode(200); + } + + @Test + void testCustomProfileDistanceInfluence() { + JSONObject body = new JSONObject(); + body.put("coordinates", getParameter("coordinatesCustom2")); + body.put("preference", getParameter("preference")); + body.put("instructions", true); + body.put("elevation", true); + + given() + .config(JSON_CONFIG_DOUBLE_NUMBERS) + .headers(CommonHeaders.jsonContent) + .pathParam("profile", getParameter("carProfile")) + .body(body.toString()) + .when().log().ifValidationFails() + .post(getEndPointPath() + "/{profile}") + .then().log().ifValidationFails() + .assertThat() + .body("any { it.key == 'routes' }", is(true)) + .body("routes[0].summary.distance", is(closeTo(9746, 50f))) + .statusCode(200); + JSONObject customModel = new JSONObject(); + customModel.put("priority", new JSONArray()); + customModel.put("distance_influence", 150); body.put("custom_model", customModel); given() @@ -4111,11 +4156,12 @@ void testCustomProfileAreas() { .headers(CommonHeaders.jsonContent) .pathParam("profile", getParameter("carProfile")) .body(body.toString()) - .when().log().all() + .when().log().ifValidationFails() .post(getEndPointPath() + "/{profile}") - .then().log().all() + .then().log().ifValidationFails() .assertThat() .body("any { it.key == 'routes' }", is(true)) + .body("routes[0].summary.distance", is(closeTo(7648f, 50f))) .statusCode(200); } diff --git a/pom.xml b/pom.xml index 8307b8de92..194bcf7c61 100644 --- a/pom.xml +++ b/pom.xml @@ -49,8 +49,8 @@ yyyy-MM-dd'T'HH:mm:ss'Z' - - 4.9-SNAPSHOT + v4.9.5 + 1.18.34 2.0.13 2.22.1 From 5620c1540ad449f8bdb806d8cddbbcd3350fa791 Mon Sep 17 00:00:00 2001 From: Takara Baumbach Date: Mon, 13 Jan 2025 11:04:28 +0100 Subject: [PATCH 07/20] chore: remove unnecessary params in testCustomProfileAreas --- .../test/java/org/heigit/ors/apitests/routing/ResultTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java index 08770faa17..399ba018fc 100644 --- a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java +++ b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java @@ -4082,9 +4082,6 @@ void testCustomProfileAreas() { JSONObject areas = new JSONObject(); JSONObject area1 = new JSONObject(); area1.put("type", "Feature"); - area1.put("id", "something"); - JSONObject area1props = new JSONObject(); - area1.put("properties", area1props); JSONObject area1geo = new JSONObject(); area1geo.put("type", "Polygon"); JSONArray area1coords = new JSONArray(); From a7780dc1ba883cc61a32b88555e3b71219b82f7c Mon Sep 17 00:00:00 2001 From: Sascha Fendrich Date: Mon, 13 Jan 2025 14:55:31 +0100 Subject: [PATCH 08/20] test: output request only if validation fails --- .../test/java/org/heigit/ors/apitests/routing/ResultTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java index 399ba018fc..3752751311 100644 --- a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java +++ b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java @@ -4015,7 +4015,7 @@ void testCustomProfileBlockTunnels() { .body(body.toString()) .when() .post(getEndPointPath() + "/{profile}") - .then().log().all() + .then().log().ifValidationFails() .assertThat() .body("any { it.key == 'routes' }", is(true)) .body("routes[0].summary.distance", is(closeTo(3338f, 40f))) @@ -4057,7 +4057,7 @@ void testCustomProfileBlockHighway() { .body(body.toString()) .when() .post(getEndPointPath() + "/{profile}") - .then().log().all() + .then().log().ifValidationFails() .assertThat() .body("any { it.key == 'routes' }", is(true)) .body("routes[0].summary.distance", is(closeTo(9039f, 80f))) From 902a9cd5bdfcb869375fdac8916b16ca7cb46a2a Mon Sep 17 00:00:00 2001 From: Takara Baumbach Date: Tue, 14 Jan 2025 13:58:33 +0100 Subject: [PATCH 09/20] build: use GH v4.9.6 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 194bcf7c61..578cd445de 100644 --- a/pom.xml +++ b/pom.xml @@ -49,7 +49,7 @@ yyyy-MM-dd'T'HH:mm:ss'Z' - v4.9.5 + v4.9.6 1.18.34 2.0.13 From 965a530e54233587f6d7afce70ff6161dea577d5 Mon Sep 17 00:00:00 2001 From: Sascha Fendrich Date: Wed, 15 Jan 2025 10:29:29 +0100 Subject: [PATCH 10/20] refactor: delombok core class We do not want to use lombok in core classes just to spare a setter or getter. --- .../java/org/heigit/ors/routing/RoutingProfile.java | 11 ++++++++--- .../org/heigit/ors/routing/RoutingProfileManager.java | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java index 16993409e4..d3896cbed0 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java @@ -27,7 +27,6 @@ import com.graphhopper.util.Helper; import com.graphhopper.util.PMap; import com.graphhopper.util.Parameters; -import lombok.Getter; import org.apache.log4j.Logger; import org.heigit.ors.config.ElevationProperties; import org.heigit.ors.config.EngineProperties; @@ -66,9 +65,7 @@ public class RoutingProfile { private static int profileIdentifier = 0; private final Integer[] mRoutePrefs; - @Getter private String profileName; - @Getter private ProfileProperties profileProperties; private EngineProperties engineProperties; private String graphVersion; @@ -461,4 +458,12 @@ public boolean equals(Object o) { public int hashCode() { return mGraphHopper.getGraphHopperStorage().getDirectory().getLocation().hashCode(); } + + public String name() { + return this.profileName; + } + + public ProfileProperties getProfileProperties() { + return this.profileProperties; + } } diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfileManager.java b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfileManager.java index 873348789c..715eb5522f 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfileManager.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfileManager.java @@ -91,7 +91,7 @@ public void initialize(EngineProperties config, String graphVersion) { try { RoutingProfile rp = future.get(); nCompletedTasks++; - routingProfiles.put(rp.getProfileName(), rp); + routingProfiles.put(rp.name(), rp); } catch (ExecutionException e) { LOGGER.debug(e); if (ExceptionUtils.indexOfThrowable(e, FileNotFoundException.class) != -1) { From c9b7125e3de59572d633b3b918231611338b7f65 Mon Sep 17 00:00:00 2001 From: Sascha Fendrich Date: Wed, 15 Jan 2025 13:19:03 +0100 Subject: [PATCH 11/20] feat: add configuration flag for enabling custom models This commit adds a graph-build-time configuration flag to enable custom models for each profile. By default the flag is disabled. --- ors-api/src/main/resources/application.yml | 10 +++++++ .../heigit/ors/apitests/ORSStartupTest.java | 2 +- .../ors/apitests/routing/ResultTest.java | 27 +++++++++++++++++++ .../src/test/resources/application-test.yml | 11 ++++++++ .../profile/EncoderOptionsProperties.java | 8 +++++- .../heigit/ors/routing/RoutingProfile.java | 2 +- 6 files changed, 57 insertions(+), 3 deletions(-) diff --git a/ors-api/src/main/resources/application.yml b/ors-api/src/main/resources/application.yml index 5ae54f1d0e..b53e571f8d 100644 --- a/ors-api/src/main/resources/application.yml +++ b/ors-api/src/main/resources/application.yml @@ -179,6 +179,7 @@ ors: turn_costs: true block_fords: false use_acceleration: true + enable_custom_models: false preparation: min_network_size: 200 methods: @@ -215,6 +216,7 @@ ors: turn_costs: true block_fords: false use_acceleration: true + enable_custom_models: false preparation: min_network_size: 200 methods: @@ -248,6 +250,7 @@ ors: consider_elevation: true turn_costs: true block_fords: false + enable_custom_models: false ext_storages: WayCategory: WaySurfaceType: @@ -260,6 +263,7 @@ ors: consider_elevation: true turn_costs: true block_fords: false + enable_custom_models: false ext_storages: WayCategory: WaySurfaceType: @@ -272,6 +276,7 @@ ors: consider_elevation: true turn_costs: true block_fords: false + enable_custom_models: false ext_storages: WayCategory: WaySurfaceType: @@ -284,6 +289,7 @@ ors: consider_elevation: true turn_costs: true block_fords: false + enable_custom_models: false ext_storages: WayCategory: WaySurfaceType: @@ -294,6 +300,7 @@ ors: build: encoder_options: block_fords: false + enable_custom_models: false ext_storages: WayCategory: WaySurfaceType: @@ -304,6 +311,7 @@ ors: build: encoder_options: block_fords: false + enable_custom_models: false ext_storages: WayCategory: WaySurfaceType: @@ -314,6 +322,7 @@ ors: build: encoder_options: block_fords: true + enable_custom_models: false ext_storages: WayCategory: WaySurfaceType: @@ -327,6 +336,7 @@ ors: build: encoder_options: block_fords: false + enable_custom_models: false elevation: true gtfs_file: ./src/test/files/vrn_gtfs_cut.zip service: diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/ORSStartupTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/ORSStartupTest.java index 8e14e921ed..1cf61d319c 100644 --- a/ors-api/src/test/java/org/heigit/ors/apitests/ORSStartupTest.java +++ b/ors-api/src/test/java/org/heigit/ors/apitests/ORSStartupTest.java @@ -30,7 +30,7 @@ void testGraphBuildInfoFilesWrittenCorrectly() throws ParseException, ORSGraphFi assertNull(profileProperties.getBuild().getGtfsFile(), "GTFS file path settings should not be set in the graph_build_info"); assertTrue(profileProperties.getRepo().isEmpty(), "Repo settings should not be set in the graph_build_info"); assertTrue(profileProperties.getService().getExecution().isEmpty(), "Execution settings should not be set in the graph_build_info"); - assertEquals("turn_costs=true|block_fords=false|use_acceleration=true|maximum_grade_level=1|conditional_access=true|conditional_speed=true", profileProperties.getBuild().getEncoderOptions().toString(), "Encoder options should be set in the graph_build_info"); + assertEquals("turn_costs=true|block_fords=false|use_acceleration=true|maximum_grade_level=1|conditional_access=true|conditional_speed=true|enable_custom_models=true", profileProperties.getBuild().getEncoderOptions().toString(), "Encoder options should be set in the graph_build_info"); assertFalse(profileProperties.getBuild().getPreparation().isEmpty(), "Preparation settings should be set in the graph_build_info"); assertTrue(profileProperties.getBuild().getPreparation().getMethods().getCore().getEnabled(), "Preparation settings should contain enabled core method"); assertFalse(profileProperties.getBuild().getExtStorages().isEmpty(), "ExtStorages settings should be set in the graph_build_info"); diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java index 3752751311..0784cb955b 100644 --- a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java +++ b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java @@ -4022,6 +4022,33 @@ void testCustomProfileBlockTunnels() { .statusCode(200); } + @Test + void testCustomProfileBlockTunnelsRejectedWhenDisabled() { + JSONObject body = new JSONObject(); + body.put("coordinates", getParameter("coordinatesCustom1")); + body.put("preference", getParameter("preference")); + body.put("instructions", true); + body.put("elevation", true); + + JSONObject customModel = new JSONObject(); + JSONObject priority = new JSONObject(); + priority.put("if", "road_environment == TUNNEL"); + priority.put("multiply_by", 0); + customModel.put("priority", new JSONArray().put(priority)); + body.put("custom_model", customModel); + + given() + .config(JSON_CONFIG_DOUBLE_NUMBERS) + .headers(CommonHeaders.jsonContent) + .pathParam("profile", getParameter("bikeProfile")) + .body(body.toString()) + .when() + .post(getEndPointPath() + "/{profile}") + .then().log().ifValidationFails() + .assertThat() + .statusCode(500); + } + @Test void testCustomProfileBlockHighway() { JSONObject body = new JSONObject(); diff --git a/ors-api/src/test/resources/application-test.yml b/ors-api/src/test/resources/application-test.yml index a48032043c..b27bafc309 100644 --- a/ors-api/src/test/resources/application-test.yml +++ b/ors-api/src/test/resources/application-test.yml @@ -52,6 +52,7 @@ ors: maximum_grade_level: 1 conditional_access: true conditional_speed: true + enable_custom_models: true preparation: min_network_size: 200 methods: @@ -106,6 +107,7 @@ ors: turn_costs: true block_fords: false use_acceleration: true + enable_custom_models: false preparation: min_network_size: 200 methods: @@ -161,6 +163,7 @@ ors: consider_elevation: false turn_costs: true block_fords: false + enable_custom_models: false preparation: min_network_size: 200 methods: @@ -187,6 +190,7 @@ ors: consider_elevation: false turn_costs: true block_fords: false + enable_custom_models: false ext_storages: WayCategory: WaySurfaceType: @@ -202,6 +206,7 @@ ors: consider_elevation: false turn_costs: false block_fords: false + enable_custom_models: false ext_storages: WayCategory: WaySurfaceType: @@ -215,6 +220,7 @@ ors: consider_elevation: false turn_costs: true block_fords: false + enable_custom_models: false ext_storages: WayCategory: WaySurfaceType: @@ -227,6 +233,7 @@ ors: interpolate_bridges_and_tunnels: false encoder_options: block_fords: false + enable_custom_models: false ext_storages: GreenIndex: filepath: ./src/test/files/green_streets_hd.csv @@ -246,6 +253,7 @@ ors: build: encoder_options: block_fords: false + enable_custom_models: false ext_storages: GreenIndex: filepath: ./src/test/files/green_streets_hd.csv @@ -263,6 +271,7 @@ ors: build: encoder_options: block_fords: false + enable_custom_models: false ext_storages: Wheelchair: kerbs_on_crossings: true @@ -277,6 +286,7 @@ ors: build: encoder_options: block_fords: false + enable_custom_models: false gtfs_file: ./src/test/files/vrn_gtfs_cut.zip service: maximum_visited_nodes: 15000 @@ -291,6 +301,7 @@ ors: maximum_grade_level: 1 conditional_access: false conditional_speed: false + enable_custom_models: false preparation: min_network_size: 200 diff --git a/ors-engine/src/main/java/org/heigit/ors/config/profile/EncoderOptionsProperties.java b/ors-engine/src/main/java/org/heigit/ors/config/profile/EncoderOptionsProperties.java index 2137b388f1..21049ea395 100644 --- a/ors-engine/src/main/java/org/heigit/ors/config/profile/EncoderOptionsProperties.java +++ b/ors-engine/src/main/java/org/heigit/ors/config/profile/EncoderOptionsProperties.java @@ -34,6 +34,8 @@ public class EncoderOptionsProperties { private Boolean conditionalAccess; @JsonProperty("conditional_speed") private Boolean conditionalSpeed; + @JsonProperty("enable_custom_models") + private Boolean enableCustomModels; public EncoderOptionsProperties() { } @@ -44,7 +46,7 @@ public EncoderOptionsProperties(String ignored) { @JsonIgnore public boolean isEmpty() { - return blockFords == null && considerElevation == null && turnCosts == null && useAcceleration == null && maximumGradeLevel == null && preferredSpeedFactor == null && problematicSpeedFactor == null && conditionalAccess == null && conditionalSpeed == null; + return blockFords == null && considerElevation == null && turnCosts == null && useAcceleration == null && maximumGradeLevel == null && preferredSpeedFactor == null && problematicSpeedFactor == null && conditionalAccess == null && conditionalSpeed == null && enableCustomModels == null; } @JsonIgnore @@ -77,6 +79,9 @@ public String toString() { if (conditionalSpeed != null) { out.add("conditional_speed=" + conditionalSpeed); } + if (enableCustomModels != null) { + out.add("enable_custom_models=" + enableCustomModels); + } return String.join("|", out); } @@ -90,6 +95,7 @@ public void merge(EncoderOptionsProperties other) { problematicSpeedFactor = ofNullable(this.problematicSpeedFactor).orElse(other.problematicSpeedFactor); conditionalAccess = ofNullable(this.conditionalAccess).orElse(other.conditionalAccess); conditionalSpeed = ofNullable(this.conditionalSpeed).orElse(other.conditionalSpeed); + enableCustomModels = ofNullable(this.enableCustomModels).orElse(other.enableCustomModels); } } diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java index d3896cbed0..e1f30314fb 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java @@ -211,7 +211,7 @@ private static ORSGraphHopperConfig createGHSettings(ProfileProperties profile, profiles.put(profileName, new Profile(profileName).setVehicle(vehicle).setWeighting(weighting).setTurnCosts(false)); } - if (true) { // TODO: replace with config flag + if (profile.getBuild().getEncoderOptions().getEnableCustomModels()) { if (hasTurnCosts) { profiles.put(vehicle + "_custom_with_turn_costs", new CustomProfile(vehicle + "_custom_with_turn_costs").setCustomModel(new CustomModel().setDistanceInfluence(0)).setVehicle(vehicle).setTurnCosts(true)); } From d722abbe9c205161bda5df07936396a325c0f51c Mon Sep 17 00:00:00 2001 From: Sascha Fendrich Date: Thu, 16 Jan 2025 13:15:46 +0100 Subject: [PATCH 12/20] feat: add custom model error response This commit includes substantial refactoring to enable its primary goal of adding an appropriate errer response if a user requests a custom model on a profile that is configured to disallow custom models. Co-authored-by: Jochen Haeussler --- docs/api-reference/error-codes.md | 1 + .../ors/api/services/RoutingService.java | 271 ++++++++++++------ .../heigit/ors/routing/RoutingErrorCodes.java | 1 + .../ors/routing/RoutingProfileManager.java | 95 +----- .../heigit/ors/routing/RoutingRequest.java | 9 + 5 files changed, 202 insertions(+), 175 deletions(-) diff --git a/docs/api-reference/error-codes.md b/docs/api-reference/error-codes.md index 0c540c26ff..76ff639d49 100644 --- a/docs/api-reference/error-codes.md +++ b/docs/api-reference/error-codes.md @@ -45,6 +45,7 @@ Endpoints. | 2015 | Entry not reached. | | 2016 | No route between entry and exit found. | | 2017 | Maximum number of nodes exceeded. | +| 2018 | Unsupported request option. | | 2099 | Unknown internal error. | ### Isochrones API diff --git a/ors-api/src/main/java/org/heigit/ors/api/services/RoutingService.java b/ors-api/src/main/java/org/heigit/ors/api/services/RoutingService.java index 4cf301d7b0..fe601043bb 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/services/RoutingService.java +++ b/ors-api/src/main/java/org/heigit/ors/api/services/RoutingService.java @@ -1,11 +1,14 @@ package org.heigit.ors.api.services; +import com.graphhopper.util.DistanceCalc; +import com.graphhopper.util.DistanceCalcEarth; import org.heigit.ors.api.APIEnums; import org.heigit.ors.api.config.ApiEngineProperties; import org.heigit.ors.api.config.EndpointsProperties; import org.heigit.ors.api.requests.routing.RouteRequest; import org.heigit.ors.api.requests.routing.RouteRequestRoundTripOptions; import org.heigit.ors.common.StatusCode; +import org.heigit.ors.config.profile.ProfileProperties; import org.heigit.ors.exceptions.*; import org.heigit.ors.localization.LocalizationManager; import org.heigit.ors.routing.*; @@ -28,6 +31,80 @@ public RoutingService(EndpointsProperties endpointsProperties, ApiEngineProperti this.apiEngineProperties = apiEngineProperties; } + public static void validateRouteProfileForRequest(RoutingRequest req) throws InternalServerException, ServerLimitExceededException, ParameterValueException { + boolean oneToMany = false; + RouteSearchParameters searchParams = req.getSearchParameters(); + String profileName = searchParams.getProfileName(); + + boolean fallbackAlgorithm = searchParams.requiresFullyDynamicWeights(); + boolean dynamicWeights = searchParams.requiresDynamicPreprocessedWeights(); + boolean useAlternativeRoutes = searchParams.getAlternativeRoutesCount() > 1; + + RoutingProfile rp = req.profile(); + + if (rp == null) + throw new InternalServerException(RoutingErrorCodes.UNKNOWN, "Unable to get an appropriate routing profile for the name " + profileName + "."); + + ProfileProperties profileProperties = rp.getProfileConfiguration(); + + if (profileProperties.getService().getMaximumDistance() != null + || dynamicWeights && profileProperties.getService().getMaximumDistanceDynamicWeights() != null + || profileProperties.getService().getMaximumWayPoints() != null + || fallbackAlgorithm && profileProperties.getService().getMaximumDistanceAvoidAreas() != null + ) { + Coordinate[] coords = req.getCoordinates(); + int nCoords = coords.length; + if (profileProperties.getService().getMaximumWayPoints() > 0 && !oneToMany && nCoords > profileProperties.getService().getMaximumWayPoints()) { + throw new ServerLimitExceededException(RoutingErrorCodes.REQUEST_EXCEEDS_SERVER_LIMIT, "The specified number of waypoints must not be greater than " + profileProperties.getService().getMaximumWayPoints() + "."); + } + + if (profileProperties.getService().getMaximumDistance() != null + || dynamicWeights && profileProperties.getService().getMaximumDistanceDynamicWeights() != null + || fallbackAlgorithm && profileProperties.getService().getMaximumDistanceAvoidAreas() != null + ) { + DistanceCalc distCalc = DistanceCalcEarth.DIST_EARTH; + + List skipSegments = req.getSkipSegments(); + Coordinate c0 = coords[0]; + Coordinate c1; + double totalDist = 0.0; + + if (oneToMany) { + for (int i = 1; i < nCoords; i++) { + c1 = coords[i]; + totalDist = distCalc.calcDist(c0.y, c0.x, c1.y, c1.x); + } + } else { + for (int i = 1; i < nCoords; i++) { + c1 = coords[i]; + if (!skipSegments.contains(i)) { // ignore skipped segments + totalDist += distCalc.calcDist(c0.y, c0.x, c1.y, c1.x); + } + c0 = c1; + } + } + + if (profileProperties.getService().getMaximumDistance() != null && totalDist > profileProperties.getService().getMaximumDistance()) + throw new ServerLimitExceededException(RoutingErrorCodes.REQUEST_EXCEEDS_SERVER_LIMIT, "The approximated route distance must not be greater than %s meters.".formatted(profileProperties.getService().getMaximumDistance())); + if (dynamicWeights && profileProperties.getService().getMaximumDistanceDynamicWeights() != null && totalDist > profileProperties.getService().getMaximumDistanceDynamicWeights()) + throw new ServerLimitExceededException(RoutingErrorCodes.REQUEST_EXCEEDS_SERVER_LIMIT, "By dynamic weighting, the approximated distance of a route segment must not be greater than %s meters.".formatted(profileProperties.getService().getMaximumDistanceDynamicWeights())); + if (fallbackAlgorithm && profileProperties.getService().getMaximumDistanceAvoidAreas() != null && totalDist > profileProperties.getService().getMaximumDistanceAvoidAreas()) + throw new ServerLimitExceededException(RoutingErrorCodes.REQUEST_EXCEEDS_SERVER_LIMIT, "With these options, the approximated route distance must not be greater than %s meters.".formatted(profileProperties.getService().getMaximumDistanceAvoidAreas())); + if (useAlternativeRoutes && profileProperties.getService().getMaximumDistanceAlternativeRoutes() != null && totalDist > profileProperties.getService().getMaximumDistanceAlternativeRoutes()) + throw new ServerLimitExceededException(RoutingErrorCodes.REQUEST_EXCEEDS_SERVER_LIMIT, "The approximated route distance must not be greater than %s meters for use with the alternative Routes algorithm.".formatted(profileProperties.getService().getMaximumDistanceAlternativeRoutes())); + } + } + + if (searchParams.hasMaximumSpeed() && profileProperties.getBuild().getMaximumSpeedLowerBound() != null) { + if (searchParams.getMaximumSpeed() < profileProperties.getBuild().getMaximumSpeedLowerBound()) { + throw new ParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequestParameterNames.PARAM_MAXIMUM_SPEED, String.valueOf(searchParams.getMaximumSpeed()), "The maximum speed must not be lower than " + profileProperties.getBuild().getMaximumSpeedLowerBound() + " km/h."); + } + if (RoutingProfileCategory.getFromEncoder(rp.getGraphhopper().getEncodingManager()) != RoutingProfileCategory.DRIVING) { + throw new ParameterValueException(RoutingErrorCodes.INCOMPATIBLE_PARAMETERS, "The maximum speed feature can only be used with cars and heavy vehicles."); + } + } + } + @Override double getMaximumAvoidPolygonArea() { return this.endpointsProperties.getRouting().getMaximumAvoidPolygonArea(); @@ -38,10 +115,19 @@ public RoutingService(EndpointsProperties endpointsProperties, ApiEngineProperti return this.endpointsProperties.getRouting().getMaximumAvoidPolygonExtent(); } - public RouteResult[] generateRouteFromRequest(RouteRequest request) throws StatusCodeException { - RoutingRequest routingRequest = this.convertRouteRequest(request); - + public RouteResult[] generateRouteFromRequest(RouteRequest routeApiRequest) throws StatusCodeException { try { + RoutingRequest routingRequest = convertRouteRequest(routeApiRequest); + RoutingProfile profile = parseRoutingProfile(routeApiRequest.getProfileName()); + routingRequest.setRoutingProfile(profile); + validateRouteProfileForRequest(routingRequest); + if (routeApiRequest.hasCustomModel()) { + if (!profile.getProfileProperties().getBuild().getEncoderOptions().getEnableCustomModels()) { + throw new StatusCodeException(StatusCode.INTERNAL_SERVER_ERROR, RoutingErrorCodes.UNSUPPORTED_REQUEST_OPTION, + "Custom model not supported for profile '" + profile.name() + "'."); + } + } + return RoutingProfileManager.getInstance().computeRoute(routingRequest); } catch (StatusCodeException e) { throw e; @@ -50,163 +136,163 @@ public RouteResult[] generateRouteFromRequest(RouteRequest request) throws Statu } } - public RoutingRequest convertRouteRequest(RouteRequest request) throws StatusCodeException { + public RoutingRequest convertRouteRequest(RouteRequest routeApiRequest) throws StatusCodeException { RoutingRequest routingRequest = new RoutingRequest(); - boolean isRoundTrip = request.hasRouteOptions() && request.getRouteOptions().hasRoundTripOptions(); - routingRequest.setCoordinates(convertCoordinates(request.getCoordinates(), isRoundTrip)); - routingRequest.setGeometryFormat(convertGeometryFormat(request.getResponseType())); + boolean isRoundTrip = routeApiRequest.hasRouteOptions() && routeApiRequest.getRouteOptions().hasRoundTripOptions(); + routingRequest.setCoordinates(convertCoordinates(routeApiRequest.getCoordinates(), isRoundTrip)); + routingRequest.setGeometryFormat(convertGeometryFormat(routeApiRequest.getResponseType())); - if (request.hasUseElevation()) - routingRequest.setIncludeElevation(request.getUseElevation()); + if (routeApiRequest.hasUseElevation()) + routingRequest.setIncludeElevation(routeApiRequest.getUseElevation()); - if (request.hasContinueStraightAtWaypoints()) - routingRequest.setContinueStraight(request.getContinueStraightAtWaypoints()); + if (routeApiRequest.hasContinueStraightAtWaypoints()) + routingRequest.setContinueStraight(routeApiRequest.getContinueStraightAtWaypoints()); - if (request.hasIncludeGeometry()) - routingRequest.setIncludeGeometry(convertIncludeGeometry(request)); + if (routeApiRequest.hasIncludeGeometry()) + routingRequest.setIncludeGeometry(convertIncludeGeometry(routeApiRequest)); - if (request.hasIncludeManeuvers()) - routingRequest.setIncludeManeuvers(request.getIncludeManeuvers()); + if (routeApiRequest.hasIncludeManeuvers()) + routingRequest.setIncludeManeuvers(routeApiRequest.getIncludeManeuvers()); - if (request.hasIncludeInstructions()) - routingRequest.setIncludeInstructions(request.getIncludeInstructionsInResponse()); + if (routeApiRequest.hasIncludeInstructions()) + routingRequest.setIncludeInstructions(routeApiRequest.getIncludeInstructionsInResponse()); - if (request.hasIncludeRoundaboutExitInfo()) - routingRequest.setIncludeRoundaboutExits(request.getIncludeRoundaboutExitInfo()); + if (routeApiRequest.hasIncludeRoundaboutExitInfo()) + routingRequest.setIncludeRoundaboutExits(routeApiRequest.getIncludeRoundaboutExitInfo()); - if (request.hasAttributes()) - routingRequest.setAttributes(convertAttributes(request)); + if (routeApiRequest.hasAttributes()) + routingRequest.setAttributes(convertAttributes(routeApiRequest)); - if (request.hasExtraInfo()) { - routingRequest.setExtraInfo(convertExtraInfo(request)); - for (APIEnums.ExtraInfo extra : request.getExtraInfo()) { + if (routeApiRequest.hasExtraInfo()) { + routingRequest.setExtraInfo(convertExtraInfo(routeApiRequest)); + for (APIEnums.ExtraInfo extra : routeApiRequest.getExtraInfo()) { if (extra.compareTo(APIEnums.ExtraInfo.COUNTRY_INFO) == 0) { routingRequest.setIncludeCountryInfo(true); } } } - if (request.hasLanguage()) - routingRequest.setLanguage(convertLanguage(request.getLanguage())); + if (routeApiRequest.hasLanguage()) + routingRequest.setLanguage(convertLanguage(routeApiRequest.getLanguage())); - if (request.hasInstructionsFormat()) - routingRequest.setInstructionsFormat(convertInstructionsFormat(request.getInstructionsFormat())); + if (routeApiRequest.hasInstructionsFormat()) + routingRequest.setInstructionsFormat(convertInstructionsFormat(routeApiRequest.getInstructionsFormat())); - if (request.hasUnits()) - routingRequest.setUnits(convertUnits(request.getUnits())); + if (routeApiRequest.hasUnits()) + routingRequest.setUnits(convertUnits(routeApiRequest.getUnits())); - if (request.hasSimplifyGeometry()) { - routingRequest.setGeometrySimplify(request.getSimplifyGeometry()); - if (request.hasExtraInfo() && request.getSimplifyGeometry()) { + if (routeApiRequest.hasSimplifyGeometry()) { + routingRequest.setGeometrySimplify(routeApiRequest.getSimplifyGeometry()); + if (routeApiRequest.hasExtraInfo() && routeApiRequest.getSimplifyGeometry()) { throw new IncompatibleParameterException(RoutingErrorCodes.INCOMPATIBLE_PARAMETERS, RouteRequest.PARAM_SIMPLIFY_GEOMETRY, "true", RouteRequest.PARAM_EXTRA_INFO, "*"); } } - if (request.hasSkipSegments()) { - routingRequest.setSkipSegments(processSkipSegments(request)); + if (routeApiRequest.hasSkipSegments()) { + routingRequest.setSkipSegments(processSkipSegments(routeApiRequest)); } - if (request.hasId()) - routingRequest.setId(request.getId()); + if (routeApiRequest.hasId()) + routingRequest.setId(routeApiRequest.getId()); - if (request.hasMaximumSpeed()) { - routingRequest.setMaximumSpeed(request.getMaximumSpeed()); + if (routeApiRequest.hasMaximumSpeed()) { + routingRequest.setMaximumSpeed(routeApiRequest.getMaximumSpeed()); } int profileType = -1; - int coordinatesLength = request.getCoordinates().size(); + int coordinatesLength = routeApiRequest.getCoordinates().size(); RouteSearchParameters params = new RouteSearchParameters(); - params.setProfileName(request.getProfileName()); + params.setProfileName(routeApiRequest.getProfileName()); - if (request.hasExtraInfo()) { - routingRequest.setExtraInfo(convertExtraInfo(request));//todo remove duplicate? - params.setExtraInfo(convertExtraInfo(request)); + if (routeApiRequest.hasExtraInfo()) { + routingRequest.setExtraInfo(convertExtraInfo(routeApiRequest));//todo remove duplicate? + params.setExtraInfo(convertExtraInfo(routeApiRequest)); } - if (request.hasSuppressWarnings()) - params.setSuppressWarnings(request.getSuppressWarnings()); + if (routeApiRequest.hasSuppressWarnings()) + params.setSuppressWarnings(routeApiRequest.getSuppressWarnings()); try { - profileType = convertRouteProfileType(request.getProfile()); + profileType = convertRouteProfileType(routeApiRequest.getProfile()); params.setProfileType(profileType); } catch (Exception e) { throw new ParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequest.PARAM_PROFILE); } - APIEnums.RoutePreference preference = request.hasRoutePreference() ? request.getRoutePreference() : APIEnums.RoutePreference.RECOMMENDED; - params.setWeightingMethod(convertWeightingMethod(request, preference)); + APIEnums.RoutePreference preference = routeApiRequest.hasRoutePreference() ? routeApiRequest.getRoutePreference() : APIEnums.RoutePreference.RECOMMENDED; + params.setWeightingMethod(convertWeightingMethod(routeApiRequest, preference)); - if (request.hasBearings()) - params.setBearings(convertBearings(request.getBearings(), coordinatesLength)); + if (routeApiRequest.hasBearings()) + params.setBearings(convertBearings(routeApiRequest.getBearings(), coordinatesLength)); - if (request.hasContinueStraightAtWaypoints()) - params.setContinueStraight(request.getContinueStraightAtWaypoints()); + if (routeApiRequest.hasContinueStraightAtWaypoints()) + params.setContinueStraight(routeApiRequest.getContinueStraightAtWaypoints()); - if (request.hasMaximumSearchRadii()) - params.setMaximumRadiuses(convertMaxRadii(request.getMaximumSearchRadii(), coordinatesLength, profileType)); + if (routeApiRequest.hasMaximumSearchRadii()) + params.setMaximumRadiuses(convertMaxRadii(routeApiRequest.getMaximumSearchRadii(), coordinatesLength, profileType)); - if (request.hasUseContractionHierarchies()) { - params.setFlexibleMode(convertSetFlexibleMode(request.getUseContractionHierarchies())); - params.setOptimized(request.getUseContractionHierarchies()); + if (routeApiRequest.hasUseContractionHierarchies()) { + params.setFlexibleMode(convertSetFlexibleMode(routeApiRequest.getUseContractionHierarchies())); + params.setOptimized(routeApiRequest.getUseContractionHierarchies()); } - if (request.hasRouteOptions()) { - params = processRouteRequestOptions(request, params); + if (routeApiRequest.hasRouteOptions()) { + params = processRouteRequestOptions(routeApiRequest, params); } - if (request.hasAlternativeRoutes()) { - if (request.getCoordinates().size() > 2) { + if (routeApiRequest.hasAlternativeRoutes()) { + if (routeApiRequest.getCoordinates().size() > 2) { throw new IncompatibleParameterException(RoutingErrorCodes.INCOMPATIBLE_PARAMETERS, RouteRequest.PARAM_ALTERNATIVE_ROUTES, "(number of waypoints > 2)"); } - if (request.getAlternativeRoutes().hasTargetCount()) { - params.setAlternativeRoutesCount(request.getAlternativeRoutes().getTargetCount()); + if (routeApiRequest.getAlternativeRoutes().hasTargetCount()) { + params.setAlternativeRoutesCount(routeApiRequest.getAlternativeRoutes().getTargetCount()); int countLimit = endpointsProperties.getRouting().getMaximumAlternativeRoutes(); - if (countLimit > 0 && request.getAlternativeRoutes().getTargetCount() > countLimit) { - throw new ParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequest.PARAM_ALTERNATIVE_ROUTES, Integer.toString(request.getAlternativeRoutes().getTargetCount()), "The target alternative routes count has to be equal to or less than " + countLimit); + if (countLimit > 0 && routeApiRequest.getAlternativeRoutes().getTargetCount() > countLimit) { + throw new ParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequest.PARAM_ALTERNATIVE_ROUTES, Integer.toString(routeApiRequest.getAlternativeRoutes().getTargetCount()), "The target alternative routes count has to be equal to or less than " + countLimit); } } - if (request.getAlternativeRoutes().hasWeightFactor()) - params.setAlternativeRoutesWeightFactor(request.getAlternativeRoutes().getWeightFactor()); - if (request.getAlternativeRoutes().hasShareFactor()) - params.setAlternativeRoutesShareFactor(request.getAlternativeRoutes().getShareFactor()); + if (routeApiRequest.getAlternativeRoutes().hasWeightFactor()) + params.setAlternativeRoutesWeightFactor(routeApiRequest.getAlternativeRoutes().getWeightFactor()); + if (routeApiRequest.getAlternativeRoutes().hasShareFactor()) + params.setAlternativeRoutesShareFactor(routeApiRequest.getAlternativeRoutes().getShareFactor()); } - if (request.hasDeparture() && request.hasArrival()) + if (routeApiRequest.hasDeparture() && routeApiRequest.hasArrival()) throw new IncompatibleParameterException(RoutingErrorCodes.INCOMPATIBLE_PARAMETERS, RouteRequest.PARAM_DEPARTURE, RouteRequest.PARAM_ARRIVAL); - else if (request.hasDeparture()) - params.setDeparture(request.getDeparture()); - else if (request.hasArrival()) - params.setArrival(request.getArrival()); + else if (routeApiRequest.hasDeparture()) + params.setDeparture(routeApiRequest.getDeparture()); + else if (routeApiRequest.hasArrival()) + params.setArrival(routeApiRequest.getArrival()); - if (request.hasMaximumSpeed()) { - params.setMaximumSpeed(request.getMaximumSpeed()); + if (routeApiRequest.hasMaximumSpeed()) { + params.setMaximumSpeed(routeApiRequest.getMaximumSpeed()); } // propagate GTFS-parameters to params to convert to ptRequest in RoutingProfile.computeRoute - if (request.hasSchedule()) { - params.setSchedule(request.getSchedule()); + if (routeApiRequest.hasSchedule()) { + params.setSchedule(routeApiRequest.getSchedule()); } - if (request.hasWalkingTime()) { - params.setWalkingTime(request.getWalkingTime()); + if (routeApiRequest.hasWalkingTime()) { + params.setWalkingTime(routeApiRequest.getWalkingTime()); } - if (request.hasScheduleRows()) { - params.setScheduleRows(request.getScheduleRows()); + if (routeApiRequest.hasScheduleRows()) { + params.setScheduleRows(routeApiRequest.getScheduleRows()); } - if (request.hasIgnoreTransfers()) { - params.setIgnoreTransfers(request.isIgnoreTransfers()); + if (routeApiRequest.hasIgnoreTransfers()) { + params.setIgnoreTransfers(routeApiRequest.isIgnoreTransfers()); } - if (request.hasScheduleDuration()) { - params.setScheduleDuaration(request.getScheduleDuration()); + if (routeApiRequest.hasScheduleDuration()) { + params.setScheduleDuaration(routeApiRequest.getScheduleDuration()); } - if (request.hasCustomModel()) { - params.setCustomModel(request.getCustomModel().toGHCustomModel()); + if (routeApiRequest.hasCustomModel()) { + params.setCustomModel(routeApiRequest.getCustomModel().toGHCustomModel()); } params.setConsiderTurnRestrictions(false); @@ -216,6 +302,13 @@ else if (request.hasArrival()) return routingRequest; } + private static RoutingProfile parseRoutingProfile(String profileName) throws InternalServerException { + RoutingProfile rp = RoutingProfileManager.getInstance().getRoutingProfile(profileName); + if (rp == null) + throw new InternalServerException(RoutingErrorCodes.UNKNOWN, "Unable to find routing profile named '" + profileName + "'."); + return rp; + } + private Coordinate[] convertCoordinates(List> coordinates, boolean allowSingleCoordinate) throws ParameterValueException { if (!allowSingleCoordinate && coordinates.size() < 2) throw new ParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequest.PARAM_COORDINATES); diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingErrorCodes.java b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingErrorCodes.java index 62b90cc7b2..96447e19ec 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingErrorCodes.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingErrorCodes.java @@ -44,6 +44,7 @@ public class RoutingErrorCodes { public static final int PT_ROUTE_NOT_FOUND = 2016; public static final int PT_MAX_VISITED_NODES_EXCEEDED = 2017; + public static final int UNSUPPORTED_REQUEST_OPTION = 2018; public static final int UNKNOWN = 2099; private RoutingErrorCodes() { diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfileManager.java b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfileManager.java index 715eb5522f..7bedcbabdd 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfileManager.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfileManager.java @@ -15,8 +15,6 @@ import com.graphhopper.GHResponse; import com.graphhopper.util.AngleCalc; -import com.graphhopper.util.DistanceCalc; -import com.graphhopper.util.DistanceCalcEarth; import com.graphhopper.util.PointList; import com.graphhopper.util.exceptions.MaximumNodesExceededException; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -165,10 +163,10 @@ private void fail(String message) { RoutingProfileManagerStatus.setShutdown(true); } - public RouteResult[] computeRoundTripRoute(RoutingRequest req) throws Exception { + public static RouteResult[] computeRoundTripRoute(RoutingRequest req) throws Exception { List routes = new ArrayList<>(); - RoutingProfile rp = getRouteProfileForRequest(req, false); + RoutingProfile rp = req.profile(); RouteSearchParameters searchParams = req.getSearchParameters(); ProfileProperties profileProperties = rp.getProfileConfiguration(); @@ -243,7 +241,7 @@ public RouteResult[] computeRoundTripRoute(RoutingRequest req) throws Exception return new RouteResultBuilder().createRouteResults(routes, req, new List[]{extraInfos}); } - public RouteResult[] computeRoute(RoutingRequest req) throws Exception { + public static RouteResult[] computeRoute(RoutingRequest req) throws Exception { if (req.getSearchParameters().getRoundTripLength() > 0) { return computeRoundTripRoute(req); } else { @@ -251,11 +249,11 @@ public RouteResult[] computeRoute(RoutingRequest req) throws Exception { } } - public RouteResult[] computeLinearRoute(RoutingRequest req) throws Exception { + public static RouteResult[] computeLinearRoute(RoutingRequest req) throws Exception { List skipSegments = req.getSkipSegments(); List routes = new ArrayList<>(); - RoutingProfile rp = getRouteProfileForRequest(req, false); + RoutingProfile rp = req.profile(); RouteSearchParameters searchParams = req.getSearchParameters(); Coordinate[] coords = req.getCoordinates(); @@ -295,7 +293,7 @@ public RouteResult[] computeLinearRoute(RoutingRequest req) throws Exception { radiuses[1] = searchParams.getMaximumRadiuses()[i]; } else { try { - int maximumSnappingRadius = getRoutingProfile(profileName).getProfileConfiguration().getService().getMaximumSnappingRadius(); + int maximumSnappingRadius = req.profile().getProfileConfiguration().getService().getMaximumSnappingRadius(); radiuses = new double[2]; radiuses[0] = maximumSnappingRadius; radiuses[1] = maximumSnappingRadius; @@ -368,7 +366,7 @@ public RouteResult[] computeLinearRoute(RoutingRequest req) throws Exception { // -1 is used to indicate the use of internal limits instead of specifying it in the request. // we should therefore let them know that they are already using the limit. if (pointRadius == -1) { - pointRadius = getRoutingProfile(profileName).getProfileConfiguration().getService().getMaximumSnappingRadius(); + pointRadius = req.profile().getProfileConfiguration().getService().getMaximumSnappingRadius(); message.append("Could not find routable point within the maximum possible radius of %.1f meters of specified coordinate %d: %s.".formatted( pointRadius, pointReference, @@ -450,7 +448,7 @@ public RouteResult[] computeLinearRoute(RoutingRequest req) throws Exception { * @param routes Should hold all the routes that have been calculated, not only the direct routes. * @return will return routes object with enriched direct routes if any we're found in the same order as the input object. */ - private List enrichDirectRoutesTime(List routes) { + private static List enrichDirectRoutesTime(List routes) { List graphhopperRoutes = new ArrayList<>(); List directRoutes = new ArrayList<>(); long graphHopperTravelTime = 0; @@ -488,7 +486,7 @@ private List enrichDirectRoutesTime(List routes) { return routes; } - private double getHeadingDirection(GHResponse resp) { + private static double getHeadingDirection(GHResponse resp) { PointList points = resp.getBest().getPoints(); int nPoints = points.size(); if (nPoints > 1) { @@ -506,80 +504,5 @@ private double getHeadingDirection(GHResponse resp) { return 0; } - public RoutingProfile getRouteProfileForRequest(RoutingRequest req, boolean oneToMany) throws InternalServerException, ServerLimitExceededException, ParameterValueException { - RouteSearchParameters searchParams = req.getSearchParameters(); - String profileName = searchParams.getProfileName(); - - boolean fallbackAlgorithm = searchParams.requiresFullyDynamicWeights(); - boolean dynamicWeights = searchParams.requiresDynamicPreprocessedWeights(); - boolean useAlternativeRoutes = searchParams.getAlternativeRoutesCount() > 1; - - RoutingProfile rp = getRoutingProfile(profileName); - - if (rp == null) - throw new InternalServerException(RoutingErrorCodes.UNKNOWN, "Unable to get an appropriate routing profile for the name " + profileName + "."); - - ProfileProperties profileProperties = rp.getProfileConfiguration(); - - if (profileProperties.getService().getMaximumDistance() != null - || dynamicWeights && profileProperties.getService().getMaximumDistanceDynamicWeights() != null - || profileProperties.getService().getMaximumWayPoints() != null - || fallbackAlgorithm && profileProperties.getService().getMaximumDistanceAvoidAreas() != null - ) { - Coordinate[] coords = req.getCoordinates(); - int nCoords = coords.length; - if (profileProperties.getService().getMaximumWayPoints() > 0 && !oneToMany && nCoords > profileProperties.getService().getMaximumWayPoints()) { - throw new ServerLimitExceededException(RoutingErrorCodes.REQUEST_EXCEEDS_SERVER_LIMIT, "The specified number of waypoints must not be greater than " + profileProperties.getService().getMaximumWayPoints() + "."); - } - - if (profileProperties.getService().getMaximumDistance() != null - || dynamicWeights && profileProperties.getService().getMaximumDistanceDynamicWeights() != null - || fallbackAlgorithm && profileProperties.getService().getMaximumDistanceAvoidAreas() != null - ) { - DistanceCalc distCalc = DistanceCalcEarth.DIST_EARTH; - - List skipSegments = req.getSkipSegments(); - Coordinate c0 = coords[0]; - Coordinate c1; - double totalDist = 0.0; - - if (oneToMany) { - for (int i = 1; i < nCoords; i++) { - c1 = coords[i]; - totalDist = distCalc.calcDist(c0.y, c0.x, c1.y, c1.x); - } - } else { - for (int i = 1; i < nCoords; i++) { - c1 = coords[i]; - if (!skipSegments.contains(i)) { // ignore skipped segments - totalDist += distCalc.calcDist(c0.y, c0.x, c1.y, c1.x); - } - c0 = c1; - } - } - - if (profileProperties.getService().getMaximumDistance() != null && totalDist > profileProperties.getService().getMaximumDistance()) - throw new ServerLimitExceededException(RoutingErrorCodes.REQUEST_EXCEEDS_SERVER_LIMIT, "The approximated route distance must not be greater than %s meters.".formatted(profileProperties.getService().getMaximumDistance())); - if (dynamicWeights && profileProperties.getService().getMaximumDistanceDynamicWeights() != null && totalDist > profileProperties.getService().getMaximumDistanceDynamicWeights()) - throw new ServerLimitExceededException(RoutingErrorCodes.REQUEST_EXCEEDS_SERVER_LIMIT, "By dynamic weighting, the approximated distance of a route segment must not be greater than %s meters.".formatted(profileProperties.getService().getMaximumDistanceDynamicWeights())); - if (fallbackAlgorithm && profileProperties.getService().getMaximumDistanceAvoidAreas() != null && totalDist > profileProperties.getService().getMaximumDistanceAvoidAreas()) - throw new ServerLimitExceededException(RoutingErrorCodes.REQUEST_EXCEEDS_SERVER_LIMIT, "With these options, the approximated route distance must not be greater than %s meters.".formatted(profileProperties.getService().getMaximumDistanceAvoidAreas())); - if (useAlternativeRoutes && profileProperties.getService().getMaximumDistanceAlternativeRoutes() != null && totalDist > profileProperties.getService().getMaximumDistanceAlternativeRoutes()) - throw new ServerLimitExceededException(RoutingErrorCodes.REQUEST_EXCEEDS_SERVER_LIMIT, "The approximated route distance must not be greater than %s meters for use with the alternative Routes algorithm.".formatted(profileProperties.getService().getMaximumDistanceAlternativeRoutes())); - } - } - - if (searchParams.hasMaximumSpeed() && profileProperties.getBuild().getMaximumSpeedLowerBound() != null) { - if (searchParams.getMaximumSpeed() < profileProperties.getBuild().getMaximumSpeedLowerBound()) { - throw new ParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequestParameterNames.PARAM_MAXIMUM_SPEED, String.valueOf(searchParams.getMaximumSpeed()), "The maximum speed must not be lower than " + profileProperties.getBuild().getMaximumSpeedLowerBound() + " km/h."); - } - if (RoutingProfileCategory.getFromEncoder(rp.getGraphhopper().getEncodingManager()) != RoutingProfileCategory.DRIVING) { - throw new ParameterValueException(RoutingErrorCodes.INCOMPATIBLE_PARAMETERS, "The maximum speed feature can only be used with cars and heavy vehicles."); - } - } - - return rp; - } - } diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingRequest.java b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingRequest.java index 092774edeb..4a1acb3eeb 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingRequest.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingRequest.java @@ -57,6 +57,7 @@ public class RoutingRequest extends ServiceRequest { private List skipSegments = new ArrayList<>(); private boolean includeCountryInfo = false; private double maximumSpeed; + private RoutingProfile routingProfile; private String responseFormat = "json"; // Fields specific to GraphHopper GTFS @@ -70,6 +71,14 @@ public RoutingRequest() { searchParameters = new RouteSearchParameters(); } + public RoutingProfile profile() { + return routingProfile; + } + + public void setRoutingProfile(RoutingProfile profile) { + this.routingProfile = profile; + } + public Coordinate[] getCoordinates() { return coordinates; } From 54ead0aab579f78af1d50e5a872632b947c77d97 Mon Sep 17 00:00:00 2001 From: takb Date: Fri, 17 Jan 2025 10:21:54 +0100 Subject: [PATCH 13/20] fix: weighting chain for custom + recommended --- .../java/org/heigit/ors/api/APIEnums.java | 3 +- .../ors/api/services/RoutingService.java | 5 +- .../ors/apitests/routing/ResultTest.java | 92 +++++++++++++++++++ .../src/test/resources/application-test.yml | 2 +- .../ors/routing/RouteSearchParameters.java | 6 +- .../extensions/ORSWeightingFactory.java | 26 ++---- .../weighting/ORSPriorityWeighting.java | 22 ++++- .../heigit/ors/util/TemporaryUtilShelter.java | 3 +- 8 files changed, 130 insertions(+), 29 deletions(-) diff --git a/ors-api/src/main/java/org/heigit/ors/api/APIEnums.java b/ors-api/src/main/java/org/heigit/ors/api/APIEnums.java index 8e6dd99686..53e5d32ca9 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/APIEnums.java +++ b/ors-api/src/main/java/org/heigit/ors/api/APIEnums.java @@ -269,7 +269,8 @@ public String toString() { public enum RoutePreference { FASTEST("fastest"), SHORTEST("shortest"), - RECOMMENDED("recommended"); + RECOMMENDED("recommended"), + CUSTOM("custom"); private final String value; diff --git a/ors-api/src/main/java/org/heigit/ors/api/services/RoutingService.java b/ors-api/src/main/java/org/heigit/ors/api/services/RoutingService.java index fe601043bb..5a0b766c26 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/services/RoutingService.java +++ b/ors-api/src/main/java/org/heigit/ors/api/services/RoutingService.java @@ -415,8 +415,11 @@ private List processSkipSegments(RouteRequest request) throws Parameter } private int convertWeightingMethod(RouteRequest request, APIEnums.RoutePreference preferenceIn) throws UnknownParameterValueException { - if (request.getProfile().equals(APIEnums.Profile.DRIVING_CAR) && preferenceIn.equals(APIEnums.RoutePreference.RECOMMENDED)) + if (request.getProfile().equals(APIEnums.Profile.DRIVING_CAR) && preferenceIn.equals(APIEnums.RoutePreference.RECOMMENDED)) { + if (request.getCustomModel() != null) + return WeightingMethod.CUSTOM; return WeightingMethod.FASTEST; + } int weightingMethod = WeightingMethod.getFromString(preferenceIn.toString()); if (weightingMethod == WeightingMethod.UNKNOWN) throw new UnknownParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequest.PARAM_PREFERENCE, preferenceIn.toString()); diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java index 0784cb955b..4abfbbe5f1 100644 --- a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java +++ b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java @@ -162,6 +162,24 @@ public ResultTest() { coordinatesCustom2.put(coordinateCustom4); addParameter("coordinatesCustom2", coordinatesCustom2); + JSONArray coordinatesCustom3 = new JSONArray(); + JSONArray coordinateCustom5 = new JSONArray(); + coordinateCustom5.put(8.687862753868105); + coordinateCustom5.put(49.41309522267728); + JSONArray coordinateCustom6 = new JSONArray(); + coordinateCustom6.put(8.691891431808473); + coordinateCustom6.put(49.41331858818114); + coordinatesCustom3.put(coordinateCustom5); + coordinatesCustom3.put(coordinateCustom6); + addParameter("coordinatesCustom3", coordinatesCustom3); + +// 8.6947238445282, 49.41176896906394 +// 8.7036609649658, 49.41281775942496 + + // 8.687862753868105, 49.41309522267728 + // 8.691891431808473, 49.41331858818114 + + JSONArray extraInfo = new JSONArray(); extraInfo.put("surface"); extraInfo.put("suitability"); @@ -4189,6 +4207,80 @@ void testCustomProfileDistanceInfluence() { .statusCode(200); } + @Test + void testCustomProfileWithRecommended() { + JSONObject body = new JSONObject(); + body.put("coordinates", getParameter("coordinatesCustom3")); + body.put("preference", "recommended"); + body.put("instructions", true); + body.put("elevation", true); + + JSONObject customModel = new JSONObject(); + customModel.put("distance_influence", 0); + body.put("custom_model", customModel); + + given() + .config(JSON_CONFIG_DOUBLE_NUMBERS) + .headers(CommonHeaders.jsonContent) + .pathParam("profile", getParameter("footProfile")) + .body(body.toString()) + .when().log().ifValidationFails() + .post(getEndPointPath() + "/{profile}") + .then().log().ifValidationFails() + .assertThat() + .body("any { it.key == 'routes' }", is(true)) + .body("routes[0].summary.distance", is(closeTo( 306.6f, 50f))) + .statusCode(200); + } + + @Test + void testCustomProfileWithRecommendedCarDoesntBreak() { + JSONObject body = new JSONObject(); + body.put("coordinates", getParameter("coordinatesCustom2")); + body.put("preference", "recommended"); + body.put("instructions", true); + body.put("elevation", true); + + JSONObject customModel = new JSONObject(); + customModel.put("distance_influence", 0); + body.put("custom_model", customModel); + + given() + .config(JSON_CONFIG_DOUBLE_NUMBERS) + .headers(CommonHeaders.jsonContent) + .pathParam("profile", getParameter("carProfile")) + .body(body.toString()) + .when().log().ifValidationFails() + .post(getEndPointPath() + "/{profile}") + .then().log().ifValidationFails() + .assertThat() + .body("any { it.key == 'routes' }", is(true)) + .body("routes[0].summary.distance", is(closeTo(9746.7f, 50f))) + .statusCode(200); + } + + @Test + void testCustomProfileWithoutModelDoesntBreak() { //??? + JSONObject body = new JSONObject(); + body.put("coordinates", getParameter("coordinatesCustom2")); + body.put("preference", "custom"); + body.put("instructions", true); + body.put("elevation", true); + + given() + .config(JSON_CONFIG_DOUBLE_NUMBERS) + .headers(CommonHeaders.jsonContent) + .pathParam("profile", getParameter("carProfile")) + .body(body.toString()) + .when().log().ifValidationFails() + .post(getEndPointPath() + "/{profile}") + .then().log().all() + .assertThat() + .body("any { it.key == 'routes' }", is(true)) + .body("routes[0].summary.distance", is(closeTo(9746.7f, 5f))) + .statusCode(200); + } + private JSONArray constructBearings(String coordString) { JSONArray coordinates = new JSONArray(); String[] coordPairs = coordString.split("\\|"); diff --git a/ors-api/src/test/resources/application-test.yml b/ors-api/src/test/resources/application-test.yml index b27bafc309..f23c37e7bd 100644 --- a/ors-api/src/test/resources/application-test.yml +++ b/ors-api/src/test/resources/application-test.yml @@ -233,7 +233,7 @@ ors: interpolate_bridges_and_tunnels: false encoder_options: block_fords: false - enable_custom_models: false + enable_custom_models: true ext_storages: GreenIndex: filepath: ./src/test/files/green_streets_hd.csv diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/RouteSearchParameters.java b/ors-engine/src/main/java/org/heigit/ors/routing/RouteSearchParameters.java index a71859ed4f..06e7625e61 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/RouteSearchParameters.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/RouteSearchParameters.java @@ -74,6 +74,7 @@ public class RouteSearchParameters { private boolean hasWalkingTime = false; private boolean hasScheduleDuration = false; + @Setter @Getter private CustomModel customModel; @@ -422,9 +423,4 @@ public boolean hasSchedule() { public boolean hasScheduleDuration() { return this.hasScheduleDuration; } - - public void setCustomModel(CustomModel customModel) { - this.customModel = customModel; - weightingMethod = WeightingMethod.CUSTOM; - } } diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSWeightingFactory.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSWeightingFactory.java index e5c25a356c..cab241d495 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSWeightingFactory.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSWeightingFactory.java @@ -35,6 +35,7 @@ import static com.graphhopper.routing.weighting.Weighting.INFINITE_U_TURN_COSTS; import static com.graphhopper.util.Helper.toLowerCase; import static org.heigit.ors.util.ProfileTools.VAL_CUSTOM; +import static org.heigit.ors.util.ProfileTools.VAL_RECOMMENDED; /** * This class is a preliminary adaptation of ORSWeightingFactory to the new @@ -85,14 +86,13 @@ public Weighting createWeighting(Profile profile, PMap requestHints, boolean dis if (weightingStr.isEmpty()) throw new IllegalArgumentException("You need to specify a weighting"); - Weighting weighting = null; - if (VAL_CUSTOM.equalsIgnoreCase(weightingStr)) { - if (!(profile instanceof CustomProfile)) { + Weighting weighting; + if (VAL_CUSTOM.equalsIgnoreCase(weightingStr) || profile instanceof CustomProfile) { + if (!(profile instanceof CustomProfile customProfile)) { throw new IllegalArgumentException("custom weighting requires a CustomProfile but was profile=" + profile.getName()); } - CustomModel queryCustomModel = (CustomModel) requestHints.getObject("custom_model", (Object) null); - CustomProfile customProfile = (CustomProfile) profile; + CustomModel queryCustomModel = requestHints.getObject("custom_model", null); if (queryCustomModel != null) { queryCustomModel.checkLMConstraints(customProfile.getCustomModel()); } @@ -101,18 +101,12 @@ public Weighting createWeighting(Profile profile, PMap requestHints, boolean dis weighting = CustomModelParser.createWeighting(encoder, this.encodingManager, turnCostProvider, queryCustomModel); } else if ("shortest".equalsIgnoreCase(weightingStr)) { weighting = new ShortestWeighting(encoder, turnCostProvider); - } else if ("fastest".equalsIgnoreCase(weightingStr) || "recommended".equalsIgnoreCase(weightingStr)) { - if (encoder.supports(PriorityWeighting.class)) { - weighting = new ORSPriorityWeighting(encoder, hints, turnCostProvider); - } else { - weighting = new ORSFastestWeighting(encoder, hints, turnCostProvider); - } } else { - if (encoder.supports(PriorityWeighting.class)) { - weighting = new FastestSafeWeighting(encoder, hints, turnCostProvider); - } else { - weighting = new ORSFastestWeighting(encoder, hints, turnCostProvider); - } + weighting = new ORSFastestWeighting(encoder, hints, turnCostProvider); + } + + if ("recommended".equalsIgnoreCase(weightingStr) && encoder.supports(PriorityWeighting.class)) { + weighting = new ORSPriorityWeighting(encoder, turnCostProvider, weighting); } weighting = applySoftWeightings(hints, encoder, weighting); diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/weighting/ORSPriorityWeighting.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/weighting/ORSPriorityWeighting.java index f260964a43..0799d6513b 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/weighting/ORSPriorityWeighting.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/weighting/ORSPriorityWeighting.java @@ -15,7 +15,9 @@ import com.graphhopper.routing.ev.DecimalEncodedValue; import com.graphhopper.routing.util.FlagEncoder; +import com.graphhopper.routing.weighting.AbstractWeighting; import com.graphhopper.routing.weighting.TurnCostProvider; +import com.graphhopper.routing.weighting.Weighting; import com.graphhopper.util.EdgeIteratorState; import com.graphhopper.util.PMap; import org.heigit.ors.routing.graphhopper.extensions.flagencoders.FlagEncoderKeys; @@ -23,19 +25,21 @@ import static com.graphhopper.routing.util.EncodingManager.getKey; -public class ORSPriorityWeighting extends ORSFastestWeighting { +public class ORSPriorityWeighting extends AbstractWeighting { private static final double PRIORITY_BEST = PriorityCode.BEST.getValue(); private static final double PRIORITY_UNCHANGED = PriorityCode.UNCHANGED.getValue(); private final DecimalEncodedValue priorityEncoder; + private final Weighting superWeighting; - public ORSPriorityWeighting(FlagEncoder encoder, PMap map, TurnCostProvider tcp) { - super(encoder, map, tcp); + public ORSPriorityWeighting(FlagEncoder encoder, TurnCostProvider tcp, Weighting superWeighting) { + super(encoder, tcp); + this.superWeighting = superWeighting; priorityEncoder = encoder.getDecimalEncodedValue(getKey(encoder, FlagEncoderKeys.PRIORITY_KEY)); } @Override public double calcEdgeWeight(EdgeIteratorState edgeState, boolean reverse, long edgeEnterTime) { - double weight = super.calcEdgeWeight(edgeState, reverse, edgeEnterTime); + double weight = superWeighting.calcEdgeWeight(edgeState, reverse, edgeEnterTime); if (Double.isInfinite(weight)) return Double.POSITIVE_INFINITY; @@ -48,6 +52,16 @@ public double calcEdgeWeight(EdgeIteratorState edgeState, boolean reverse, long return weight * factor; } + @Override + public double getMinWeight(double distance) { + return 0; + } + + @Override + public double calcEdgeWeight(EdgeIteratorState edgeState, boolean reverse) { + return calcEdgeWeight(edgeState, reverse, -1); + } + @Override public boolean equals(Object obj) { if (obj == null) diff --git a/ors-engine/src/main/java/org/heigit/ors/util/TemporaryUtilShelter.java b/ors-engine/src/main/java/org/heigit/ors/util/TemporaryUtilShelter.java index c5b7a8e9d1..33ff630a04 100644 --- a/ors-engine/src/main/java/org/heigit/ors/util/TemporaryUtilShelter.java +++ b/ors-engine/src/main/java/org/heigit/ors/util/TemporaryUtilShelter.java @@ -125,7 +125,8 @@ else if (profileType == RoutingProfileType.WHEELCHAIR) { } } - String localProfileName = ProfileTools.makeProfileName(encoderName, WeightingMethod.getName(searchParams.getWeightingMethod()), + String effectiveWeightingMethod = searchParams.getCustomModel() != null ? WeightingMethod.getName(WeightingMethod.CUSTOM) : WeightingMethod.getName(searchParams.getWeightingMethod()); + String localProfileName = ProfileTools.makeProfileName(encoderName, effectiveWeightingMethod, Boolean.TRUE.equals(routingProfile.getProfileProperties().getBuild().getEncoderOptions().getTurnCosts())); String profileNameCH = ProfileTools.makeProfileName(encoderName, WeightingMethod.getName(searchParams.getWeightingMethod()), false); RouteSearchContext searchCntx = new RouteSearchContext(routingProfile.getGraphhopper(), flagEncoder, localProfileName, profileNameCH); From 0e9efb3b434138a5a43126983def19a060bcd4a1 Mon Sep 17 00:00:00 2001 From: Sascha Fendrich Date: Mon, 20 Jan 2025 12:04:30 +0100 Subject: [PATCH 14/20] feat: add configuration flag to disable custom models The new configuration flag `allow_custom_models` allows one to disable requests containing custom models even if the graph is prepared to support custom requests, e.g., if someone downloads a prebuilt graph but does not want to permit custom request due to their computational cost. --- .../configuration/engine/profiles/build.md | 1 + .../configuration/engine/profiles/service.md | 25 ++++++++--------- .../ors/api/services/RoutingService.java | 6 ++++- ors-api/src/main/resources/application.yml | 1 + .../ors/apitests/routing/ResultTest.java | 27 +++++++++++++++++++ .../src/test/resources/application-test.yml | 2 ++ .../ors/config/profile/ServiceProperties.java | 2 ++ 7 files changed, 51 insertions(+), 13 deletions(-) diff --git a/docs/run-instance/configuration/engine/profiles/build.md b/docs/run-instance/configuration/engine/profiles/build.md index 1f997de3eb..3557ebcd22 100644 --- a/docs/run-instance/configuration/engine/profiles/build.md +++ b/docs/run-instance/configuration/engine/profiles/build.md @@ -34,6 +34,7 @@ Properties beneath `ors.engine.profiles.*.encoder_options`: | problematic_speed_factor | number | wheelchair | Travel speeds on edges classified as problematic for wheelchair users are multiplied by this factor, use to set slow traveling speeds on such ways | `0.7` | | turn_costs | boolean | car, hgv, bike-* | Should turn restrictions be respected | `true` | | use_acceleration | boolean | car, hgv | Models how a vehicle would accelerate on the road segment to the maximum allowed speed. In practice it reduces speed on shorter road segments such as ones between nearby intersections in a city | `true` | +| enable_custom_models | boolean | * | Enables whether the profile is prepared to support custom models. | `false` | ## `preparation` diff --git a/docs/run-instance/configuration/engine/profiles/service.md b/docs/run-instance/configuration/engine/profiles/service.md index f11ded27b0..5c61fe2b26 100644 --- a/docs/run-instance/configuration/engine/profiles/service.md +++ b/docs/run-instance/configuration/engine/profiles/service.md @@ -4,18 +4,19 @@ Properties beneath `ors.engine.profiles..service` represent parameters need to be set specifically for each profile. More parameters relevant at query time can be found in the [ `ors.endpoints`](/api-reference/endpoints/index.md) section. -| key | type | description | default value | -|-------------------------------------|---------|-----------------------------------------------------------------------------------------------------------|---------------| -| maximum_distance | number | The maximum allowed total distance of a route | `100000` | -| maximum_distance_dynamic_weights | number | The maximum allowed distance between two way points when dynamic weights are used | `100000` | -| maximum_distance_avoid_areas | number | The maximum allowed distance between two way points when areas to be avoided are provided | `100000` | -| maximum_distance_alternative_routes | number | The maximum allowed total distance of a route for the alternative routes algorithm | `100000` | -| maximum_distance_round_trip_routes | number | The maximum allowed total distance of a route for the round trip algorithm | `100000` | -| maximum_way_points | number | The maximum number of way points in a request | `50` | -| maximum_snapping_radius | number | Maximum distance around a given coordinate to find connectable edges | `400` | -| maximum_visited_nodes | number | Only for `public-transport` profile: maximum allowed number of visited nodes in shortest path computation | `1000000` | -| force_turn_costs | boolean | Should turn restrictions be obeyed | `false` | -| execution | object | [Execution settings](#execution) relevant when querying services | | +| key | type | description | default value | +|-------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| +| maximum_distance | number | The maximum allowed total distance of a route | `100000` | +| maximum_distance_dynamic_weights | number | The maximum allowed distance between two way points when dynamic weights are used | `100000` | +| maximum_distance_avoid_areas | number | The maximum allowed distance between two way points when areas to be avoided are provided | `100000` | +| maximum_distance_alternative_routes | number | The maximum allowed total distance of a route for the alternative routes algorithm | `100000` | +| maximum_distance_round_trip_routes | number | The maximum allowed total distance of a route for the round trip algorithm | `100000` | +| maximum_way_points | number | The maximum number of way points in a request | `50` | +| maximum_snapping_radius | number | Maximum distance around a given coordinate to find connectable edges | `400` | +| maximum_visited_nodes | number | Only for `public-transport` profile: maximum allowed number of visited nodes in shortest path computation | `1000000` | +| force_turn_costs | boolean | Should turn restrictions be obeyed | `false` | +| allow_custom_models | boolean | Allows custom model requests on this profile. Requires that `encoder_options.enable_custom_models` is set to true in the [build](build.md#encoder_options) section of this profile. | `false` | +| execution | object | [Execution settings](#execution) relevant when querying services | | ## `execution` diff --git a/ors-api/src/main/java/org/heigit/ors/api/services/RoutingService.java b/ors-api/src/main/java/org/heigit/ors/api/services/RoutingService.java index 5a0b766c26..9dcd60c5ea 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/services/RoutingService.java +++ b/ors-api/src/main/java/org/heigit/ors/api/services/RoutingService.java @@ -124,7 +124,11 @@ public RouteResult[] generateRouteFromRequest(RouteRequest routeApiRequest) thro if (routeApiRequest.hasCustomModel()) { if (!profile.getProfileProperties().getBuild().getEncoderOptions().getEnableCustomModels()) { throw new StatusCodeException(StatusCode.INTERNAL_SERVER_ERROR, RoutingErrorCodes.UNSUPPORTED_REQUEST_OPTION, - "Custom model not supported for profile '" + profile.name() + "'."); + "Custom model not available for profile '" + profile.name() + "'."); + } + if (!profile.getProfileProperties().getService().getAllowCustomModels()) { + throw new StatusCodeException(StatusCode.INTERNAL_SERVER_ERROR, RoutingErrorCodes.UNSUPPORTED_REQUEST_OPTION, + "Custom model disabled for profile '" + profile.name() + "'."); } } diff --git a/ors-api/src/main/resources/application.yml b/ors-api/src/main/resources/application.yml index b53e571f8d..c59ff641b3 100644 --- a/ors-api/src/main/resources/application.yml +++ b/ors-api/src/main/resources/application.yml @@ -161,6 +161,7 @@ ors: maximum_distance_round_trip_routes: 100000 maximum_visited_nodes: 1000000 force_turn_costs: false + allow_custom_models: true execution: methods: lm: diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java index 4abfbbe5f1..ed0e536224 100644 --- a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java +++ b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java @@ -4067,6 +4067,33 @@ void testCustomProfileBlockTunnelsRejectedWhenDisabled() { .statusCode(500); } + @Test + void testCustomProfileBlockTunnelsRejectedWhenDisallowed() { + JSONObject body = new JSONObject(); + body.put("coordinates", getParameter("coordinatesCustom1")); + body.put("preference", getParameter("preference")); + body.put("instructions", true); + body.put("elevation", true); + + JSONObject customModel = new JSONObject(); + JSONObject priority = new JSONObject(); + priority.put("if", "road_environment == TUNNEL"); + priority.put("multiply_by", 0); + customModel.put("priority", new JSONArray().put(priority)); + body.put("custom_model", customModel); + + given() + .config(JSON_CONFIG_DOUBLE_NUMBERS) + .headers(CommonHeaders.jsonContent) + .pathParam("profile", getParameter("footProfile")) + .body(body.toString()) + .when() + .post(getEndPointPath() + "/{profile}") + .then().log().ifValidationFails() + .assertThat() + .statusCode(500); + } + @Test void testCustomProfileBlockHighway() { JSONObject body = new JSONObject(); diff --git a/ors-api/src/test/resources/application-test.yml b/ors-api/src/test/resources/application-test.yml index f23c37e7bd..b7c97c2295 100644 --- a/ors-api/src/test/resources/application-test.yml +++ b/ors-api/src/test/resources/application-test.yml @@ -247,6 +247,8 @@ ors: WaySurfaceType: HillIndex: TrailDifficulty: + service: + allow_custom_models: false foot-hiking: enabled: true encoder_name: foot-hiking diff --git a/ors-engine/src/main/java/org/heigit/ors/config/profile/ServiceProperties.java b/ors-engine/src/main/java/org/heigit/ors/config/profile/ServiceProperties.java index 4d2e3cfe7b..0b930f4df4 100644 --- a/ors-engine/src/main/java/org/heigit/ors/config/profile/ServiceProperties.java +++ b/ors-engine/src/main/java/org/heigit/ors/config/profile/ServiceProperties.java @@ -18,6 +18,7 @@ public class ServiceProperties { private Integer maximumSnappingRadius; private Integer maximumVisitedNodes; private Boolean forceTurnCosts; + private Boolean allowCustomModels; private ExecutionProperties execution = new ExecutionProperties(); public ServiceProperties() { @@ -37,6 +38,7 @@ public void merge(ServiceProperties other) { maximumSnappingRadius = ofNullable(this.maximumSnappingRadius).orElse(other.maximumSnappingRadius); maximumVisitedNodes = ofNullable(this.maximumVisitedNodes).orElse(other.maximumVisitedNodes); forceTurnCosts = ofNullable(forceTurnCosts).orElse(other.forceTurnCosts); + allowCustomModels = ofNullable(allowCustomModels).orElse(other.allowCustomModels); execution.merge(other.execution); } } From e23dd225d1a5e4242105a54453d7200780edf8d9 Mon Sep 17 00:00:00 2001 From: takb Date: Mon, 20 Jan 2025 11:53:22 +0000 Subject: [PATCH 15/20] chore(config): automatic conversion of application.yml to ors-config.yml and ors-config.env --- ors-config.env | 11 +++++++++++ ors-config.yml | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/ors-config.env b/ors-config.env index ca2d4ffa65..d382cb07ef 100644 --- a/ors-config.env +++ b/ors-config.env @@ -96,11 +96,13 @@ ors.engine.profile_default.build.source_file=ors-api/src/test/files/heidelberg.t #ors.engine.profile_default.service.maximum_distance_round_trip_routes=100000 #ors.engine.profile_default.service.maximum_visited_nodes=1000000 #ors.engine.profile_default.service.force_turn_costs=false +#ors.engine.profile_default.service.allow_custom_models=true #ors.engine.profile_default.service.execution.methods.lm.active_landmarks=8 #ors.engine.profiles.driving-car.encoder_name=driving-car #ors.engine.profiles.driving-car.build.encoder_options.turn_costs=true #ors.engine.profiles.driving-car.build.encoder_options.block_fords=false #ors.engine.profiles.driving-car.build.encoder_options.use_acceleration=true +#ors.engine.profiles.driving-car.build.encoder_options.enable_custom_models=false #ors.engine.profiles.driving-car.build.preparation.min_network_size=200 #ors.engine.profiles.driving-car.build.preparation.methods.ch.enabled=true #ors.engine.profiles.driving-car.build.preparation.methods.ch.threads=1 @@ -123,6 +125,7 @@ ors.engine.profiles.driving-car.enabled=true #ors.engine.profiles.driving-hgv.build.encoder_options.turn_costs=true #ors.engine.profiles.driving-hgv.build.encoder_options.block_fords=false #ors.engine.profiles.driving-hgv.build.encoder_options.use_acceleration=true +#ors.engine.profiles.driving-hgv.build.encoder_options.enable_custom_models=false #ors.engine.profiles.driving-hgv.build.preparation.min_network_size=200 #ors.engine.profiles.driving-hgv.build.preparation.methods.ch.enabled=true #ors.engine.profiles.driving-hgv.build.preparation.methods.ch.threads=1 @@ -142,6 +145,7 @@ ors.engine.profiles.driving-car.enabled=true #ors.engine.profiles.cycling-regular.build.encoder_options.consider_elevation=true #ors.engine.profiles.cycling-regular.build.encoder_options.turn_costs=true #ors.engine.profiles.cycling-regular.build.encoder_options.block_fords=false +#ors.engine.profiles.cycling-regular.build.encoder_options.enable_custom_models=false #ors.engine.profiles.cycling-regular.build.ext_storages.WayCategory= #ors.engine.profiles.cycling-regular.build.ext_storages.WaySurfaceType= #ors.engine.profiles.cycling-regular.build.ext_storages.HillIndex= @@ -150,6 +154,7 @@ ors.engine.profiles.driving-car.enabled=true #ors.engine.profiles.cycling-mountain.build.encoder_options.consider_elevation=true #ors.engine.profiles.cycling-mountain.build.encoder_options.turn_costs=true #ors.engine.profiles.cycling-mountain.build.encoder_options.block_fords=false +#ors.engine.profiles.cycling-mountain.build.encoder_options.enable_custom_models=false #ors.engine.profiles.cycling-mountain.build.ext_storages.WayCategory= #ors.engine.profiles.cycling-mountain.build.ext_storages.WaySurfaceType= #ors.engine.profiles.cycling-mountain.build.ext_storages.HillIndex= @@ -158,6 +163,7 @@ ors.engine.profiles.driving-car.enabled=true #ors.engine.profiles.cycling-road.build.encoder_options.consider_elevation=true #ors.engine.profiles.cycling-road.build.encoder_options.turn_costs=true #ors.engine.profiles.cycling-road.build.encoder_options.block_fords=false +#ors.engine.profiles.cycling-road.build.encoder_options.enable_custom_models=false #ors.engine.profiles.cycling-road.build.ext_storages.WayCategory= #ors.engine.profiles.cycling-road.build.ext_storages.WaySurfaceType= #ors.engine.profiles.cycling-road.build.ext_storages.HillIndex= @@ -166,24 +172,28 @@ ors.engine.profiles.driving-car.enabled=true #ors.engine.profiles.cycling-electric.build.encoder_options.consider_elevation=true #ors.engine.profiles.cycling-electric.build.encoder_options.turn_costs=true #ors.engine.profiles.cycling-electric.build.encoder_options.block_fords=false +#ors.engine.profiles.cycling-electric.build.encoder_options.enable_custom_models=false #ors.engine.profiles.cycling-electric.build.ext_storages.WayCategory= #ors.engine.profiles.cycling-electric.build.ext_storages.WaySurfaceType= #ors.engine.profiles.cycling-electric.build.ext_storages.HillIndex= #ors.engine.profiles.cycling-electric.build.ext_storages.TrailDifficulty= #ors.engine.profiles.foot-walking.encoder_name=foot-walking #ors.engine.profiles.foot-walking.build.encoder_options.block_fords=false +#ors.engine.profiles.foot-walking.build.encoder_options.enable_custom_models=false #ors.engine.profiles.foot-walking.build.ext_storages.WayCategory= #ors.engine.profiles.foot-walking.build.ext_storages.WaySurfaceType= #ors.engine.profiles.foot-walking.build.ext_storages.HillIndex= #ors.engine.profiles.foot-walking.build.ext_storages.TrailDifficulty= #ors.engine.profiles.foot-hiking.encoder_name=foot-hiking #ors.engine.profiles.foot-hiking.build.encoder_options.block_fords=false +#ors.engine.profiles.foot-hiking.build.encoder_options.enable_custom_models=false #ors.engine.profiles.foot-hiking.build.ext_storages.WayCategory= #ors.engine.profiles.foot-hiking.build.ext_storages.WaySurfaceType= #ors.engine.profiles.foot-hiking.build.ext_storages.HillIndex= #ors.engine.profiles.foot-hiking.build.ext_storages.TrailDifficulty= #ors.engine.profiles.wheelchair.encoder_name=wheelchair #ors.engine.profiles.wheelchair.build.encoder_options.block_fords=true +#ors.engine.profiles.wheelchair.build.encoder_options.enable_custom_models=false #ors.engine.profiles.wheelchair.build.ext_storages.WayCategory= #ors.engine.profiles.wheelchair.build.ext_storages.WaySurfaceType= #ors.engine.profiles.wheelchair.build.ext_storages.Wheelchair.kerbs_on_crossings=true @@ -191,6 +201,7 @@ ors.engine.profiles.driving-car.enabled=true #ors.engine.profiles.wheelchair.service.maximum_snapping_radius=50 #ors.engine.profiles.public-transport.encoder_name=public-transport #ors.engine.profiles.public-transport.build.encoder_options.block_fords=false +#ors.engine.profiles.public-transport.build.encoder_options.enable_custom_models=false #ors.engine.profiles.public-transport.build.elevation=true #ors.engine.profiles.public-transport.build.gtfs_file=./src/test/files/vrn_gtfs_cut.zip #ors.engine.profiles.public-transport.service.maximum_visited_nodes=1000000 diff --git a/ors-config.yml b/ors-config.yml index 5fe664fd7c..fe647c87af 100644 --- a/ors-config.yml +++ b/ors-config.yml @@ -157,6 +157,7 @@ ors: # maximum_distance_round_trip_routes: 100000 # maximum_visited_nodes: 1000000 # force_turn_costs: false +# allow_custom_models: true # execution: # methods: # lm: @@ -174,6 +175,7 @@ ors: # turn_costs: true # block_fords: false # use_acceleration: true +# enable_custom_models: false # preparation: # min_network_size: 200 # methods: @@ -210,6 +212,7 @@ ors: # turn_costs: true # block_fords: false # use_acceleration: true +# enable_custom_models: false # preparation: # min_network_size: 200 # methods: @@ -243,6 +246,7 @@ ors: # consider_elevation: true # turn_costs: true # block_fords: false +# enable_custom_models: false # ext_storages: # WayCategory: # WaySurfaceType: @@ -255,6 +259,7 @@ ors: # consider_elevation: true # turn_costs: true # block_fords: false +# enable_custom_models: false # ext_storages: # WayCategory: # WaySurfaceType: @@ -267,6 +272,7 @@ ors: # consider_elevation: true # turn_costs: true # block_fords: false +# enable_custom_models: false # ext_storages: # WayCategory: # WaySurfaceType: @@ -279,6 +285,7 @@ ors: # consider_elevation: true # turn_costs: true # block_fords: false +# enable_custom_models: false # ext_storages: # WayCategory: # WaySurfaceType: @@ -289,6 +296,7 @@ ors: # build: # encoder_options: # block_fords: false +# enable_custom_models: false # ext_storages: # WayCategory: # WaySurfaceType: @@ -299,6 +307,7 @@ ors: # build: # encoder_options: # block_fords: false +# enable_custom_models: false # ext_storages: # WayCategory: # WaySurfaceType: @@ -309,6 +318,7 @@ ors: # build: # encoder_options: # block_fords: true +# enable_custom_models: false # ext_storages: # WayCategory: # WaySurfaceType: @@ -322,6 +332,7 @@ ors: # build: # encoder_options: # block_fords: false +# enable_custom_models: false # elevation: true # gtfs_file: ./src/test/files/vrn_gtfs_cut.zip # service: From e8d68f9b0faad5620e68a8854fbead473f89a92b Mon Sep 17 00:00:00 2001 From: takb Date: Mon, 20 Jan 2025 12:54:11 +0100 Subject: [PATCH 16/20] docs: CHANGELOGS --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e004b89fc..22e1e77748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,8 +54,8 @@ RELEASING: ### Added - take into account tunnel categories B, C, D, and E when restricting roads for the transport of dangerous goods via the `hazmat` flag in vehicle parameters ([#1879](https://github.com/GIScience/openrouteservice/pull/1879)) - Ukrainian translation ([#1883](https://github.com/GIScience/openrouteservice/pull/1883)) -- add new functionality to download new routing graphs from a remote - repository ([#1889](https://github.com/GIScience/openrouteservice/pull/1889)) +- add new functionality to download new routing graphs from a remote repository ([#1889](https://github.com/GIScience/openrouteservice/pull/1889)) +- add support for custom model for directions requests ([#1950](https://github.com/GIScience/openrouteservice/pull/1950)) ### Changed - update docs dependency: VitePress ([#1872](https://github.com/GIScience/openrouteservice/pull/1872)) - adjust documentation for export endpoint ([#1872](https://github.com/GIScience/openrouteservice/pull/1872)) From 2c46c1ff8abb7f33c156fc918b9422f018952262 Mon Sep 17 00:00:00 2001 From: takb Date: Mon, 20 Jan 2025 13:31:20 +0100 Subject: [PATCH 17/20] docs: Improve swagger schema --- .../ors/api/requests/routing/RouteRequest.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequest.java b/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequest.java index 33012fc7a0..895854d737 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequest.java +++ b/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequest.java @@ -333,7 +333,22 @@ Specifies a list of pairs (bearings and deviations) to filter the segments of th private boolean hasIgnoreTransfers = false; @Getter - @Schema(name = PARAM_CUSTOM_MODEL, description = "Specifies custom model for weighting.") + @Schema(name = PARAM_CUSTOM_MODEL, description = "Specifies custom model for weighting.", example = """ + {\ + "speed": [\ + {\ + "if": true,\ + "limit_to": 100\ + }\ + ],\ + "priority": [\ + {\ + "if": "road_class == MOTORWAY",\ + "multiply_by": 0\ + }\ + ],\ + "distance_influence": 100\ + }""") @JsonProperty(PARAM_CUSTOM_MODEL) private RouteRequestCustomModel customModel; @JsonIgnore From 3d44154ae53c0ec1b322646732e2356dbb9cf047 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jan 2025 21:40:12 +0000 Subject: [PATCH 18/20] build(deps): bump katex in the npm_and_yarn group Bumps the npm_and_yarn group with 1 update: [katex](https://github.com/KaTeX/KaTeX). Updates `katex` from 0.16.11 to 0.16.21 - [Release notes](https://github.com/KaTeX/KaTeX/releases) - [Changelog](https://github.com/KaTeX/KaTeX/blob/main/CHANGELOG.md) - [Commits](https://github.com/KaTeX/KaTeX/compare/v0.16.11...v0.16.21) --- updated-dependencies: - dependency-name: katex dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- package-lock.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index aeaaeeb4b9..80a324694a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2465,15 +2465,14 @@ } }, "node_modules/katex": { - "version": "0.16.11", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz", - "integrity": "sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==", + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", "dev": true, "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" ], - "license": "MIT", "dependencies": { "commander": "^8.3.0" }, From 990741b9a8820a957d1e070d3ace3dd3da6bd25b Mon Sep 17 00:00:00 2001 From: Jochen Haeussler Date: Fri, 17 Jan 2025 17:13:43 +0100 Subject: [PATCH 19/20] docs(configuration): fix missing 'build' in paths also unify the placeholder for profiles in properties and make it readable more informative --- CHANGELOG.md | 2 ++ .../configuration/engine/profiles/build.md | 21 ++++++++++--------- .../configuration/engine/profiles/repo.md | 4 ++-- .../configuration/engine/profiles/service.md | 14 +++++++------ .../configuration/how-to-configure.md | 2 +- docs/run-instance/data.md | 8 ++++--- .../graph-repo-client/index.md | 4 ++-- 7 files changed, 31 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22e1e77748..18d921e68a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,8 @@ RELEASING: ### Fixed - NPE in error handling ([#1925](https://github.com/GIScience/openrouteservice/pull/1925)) +- Add missing 'build' in documentation for profile + properties ([#1947](https://github.com/GIScience/openrouteservice/issues//1947)) ### Security diff --git a/docs/run-instance/configuration/engine/profiles/build.md b/docs/run-instance/configuration/engine/profiles/build.md index 3557ebcd22..697c13d207 100644 --- a/docs/run-instance/configuration/engine/profiles/build.md +++ b/docs/run-instance/configuration/engine/profiles/build.md @@ -1,6 +1,6 @@ -# `ors.engine.profiles..build` +# `ors.engine.profiles..build` -Properties beneath `ors.engine.profiles..build` are used to define the parameters for building the routing +Properties beneath `ors.engine.profiles..build` are used to define the parameters for building the routing graphs for the specified profile. | key | type | description | default value | @@ -19,11 +19,11 @@ graphs for the specified profile. | gtfs_file | string | Only for `public-transport` profile: location of GTFS data; can either be a zip-file or the unzipped folder | _NA_ | | encoder_options | string | For details see [encoder_options](#encoder-options) below | | | preparation | object | [Preparation settings](#preparation) for building the routing graphs | | -| ext_storages | object | [External storages](#ext_storages) for returning extra information | | +| ext_storages | object | [External storages](#ext-storages) for returning extra information | | ## `encoder_options` -Properties beneath `ors.engine.profiles.*.encoder_options`: +Properties beneath `ors.engine.profiles..build.encoder_options`: | key | type | profiles | description | example value | |--------------------------|---------|------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| @@ -38,7 +38,7 @@ Properties beneath `ors.engine.profiles.*.encoder_options`: ## `preparation` -Properties beneath `ors.engine.profiles.*.preparation`: +Properties beneath `ors.engine.profiles..build.preparation`: | key | type | description | default value | |------------------|--------|-------------------------------------------------------------------|---------------| @@ -49,7 +49,7 @@ Properties beneath `ors.engine.profiles.*.preparation`: Settings for preprocessing contraction hierarchies -Properties beneath `ors.engine.profiles.*.preparation.methods.ch`: +Properties beneath `ors.engine.profiles..build.preparation.methods.ch`: | key | type | description | example value | |------------|---------|----------------------------------------------------------|------------------------| @@ -61,7 +61,7 @@ Properties beneath `ors.engine.profiles.*.preparation.methods.ch`: Settings for preprocessing A* with landmarks -Properties beneath `ors.engine.profiles.*.preparation.methods.lm`: +Properties beneath `ors.engine.profiles..build.preparation.methods.lm`: | key | type | description | default value | |------------|---------|---------------------------------------------------------------------------------------------------------------------------|------------------------| @@ -74,7 +74,7 @@ Properties beneath `ors.engine.profiles.*.preparation.methods.lm`: Settings for preprocessing core routing with landmarks -Properties beneath `ors.engine.profiles.*.preparation.methods.core`: +Properties beneath `ors.engine.profiles..build.preparation.methods.core`: | key | type | description | example value | |------------|---------|-----------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------| @@ -111,7 +111,7 @@ parameter rely on the `HeavyVehicle` storage. ::: -Properties beneath `ors.engine.profiles.*.ext_storages`: +Properties beneath `ors.engine.profiles..build.ext_storages`: | key | type | description | example value | |------------------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------| @@ -153,7 +153,8 @@ route response. ### `Borders` -Properties beneath `ors.engine.profiles.*.ext_storages.Borders` allows to define restriction of routes to not cross +Properties beneath `ors.engine.profiles..build.ext_storages.Borders` allows to define restriction of +routes to not cross country borders, compatible with any profile type. | key | type | description | example value | diff --git a/docs/run-instance/configuration/engine/profiles/repo.md b/docs/run-instance/configuration/engine/profiles/repo.md index a97dd077e7..6b3a746217 100644 --- a/docs/run-instance/configuration/engine/profiles/repo.md +++ b/docs/run-instance/configuration/engine/profiles/repo.md @@ -1,6 +1,6 @@ -# `ors.engine.profiles..repo` +# `ors.engine.profiles..repo` -Properties beneath `ors.engine.profiles..repo` are used to address a graph in a graph repository. +Properties beneath `ors.engine.profiles..repo` are used to address a graph in a graph repository. If *all* of these properties are set, and if graph management is enabled [ors.engine.graph_management.enabled](/run-instance/configuration/engine/graph-management.md), diff --git a/docs/run-instance/configuration/engine/profiles/service.md b/docs/run-instance/configuration/engine/profiles/service.md index 5c61fe2b26..56721cb65f 100644 --- a/docs/run-instance/configuration/engine/profiles/service.md +++ b/docs/run-instance/configuration/engine/profiles/service.md @@ -1,6 +1,7 @@ -# `ors.engine.profiles..service` +# `ors.engine.profiles..service` -Properties beneath `ors.engine.profiles..service` represent parameters relevant when querying services that +Properties beneath `ors.engine.profiles..service` represent parameters relevant when querying services +that need to be set specifically for each profile. More parameters relevant at query time can be found in the [ `ors.endpoints`](/api-reference/endpoints/index.md) section. @@ -20,13 +21,14 @@ need to be set specifically for each profile. More parameters relevant at query ## `execution` -Properties beneath `ors.engine.profiles..service.execution` represent options specific for certain algorithms. +Properties beneath `ors.engine.profiles..service.execution` represent options specific for certain +algorithms. ### `methods.astar` Parameters for beeline approximation in the A* routing algorithm. -Properties beneath `ors.engine.profiles..service.execution.methods.astar`: +Properties beneath `ors.engine.profiles..service.execution.methods.astar`: | key | type | description | default value | |---------------|--------|---------------------------------------------------------------------------------------------------------------------------------------------|-------------------------| @@ -37,7 +39,7 @@ Properties beneath `ors.engine.profiles..service.execution.methods.asta Settings for using landmarks in routing. -Properties beneath `ors.engine.profiles..service.execution.methods.lm`: +Properties beneath `ors.engine.profiles..service.execution.methods.lm`: | key | type | description | default value | |------------------|--------|--------------------------------------------------|---------------| @@ -47,7 +49,7 @@ Properties beneath `ors.engine.profiles..service.execution.methods.lm`: Settings for using landmarks in routing using the Core-ALT algorithm. -Properties beneath `ors.engine.profiles..service.execution.methods.core`: +Properties beneath `ors.engine.profiles..service.execution.methods.core`: | key | type | description | example value | |------------------|--------|--------------------------------------------------|---------------| diff --git a/docs/run-instance/configuration/how-to-configure.md b/docs/run-instance/configuration/how-to-configure.md index efde60f2d5..d6fbc0ca93 100644 --- a/docs/run-instance/configuration/how-to-configure.md +++ b/docs/run-instance/configuration/how-to-configure.md @@ -286,7 +286,7 @@ The internal value for the specific profile wins over the user's `profile_defaul ## Graph build properties loaded from graph directories -The configured profile properties in `ors.engine.profiles..build` (and `ors.engine.profile_default.build`) +The configured profile properties in `ors.engine.profiles..build` (and `ors.engine.profile_default.build`) are used to _build_ a graph. openrouteservice stores these build properties in the file `graphs//graph_build_info.yml` when a graph is computed. diff --git a/docs/run-instance/data.md b/docs/run-instance/data.md index 14d93ae176..19dcb1f7a8 100644 --- a/docs/run-instance/data.md +++ b/docs/run-instance/data.md @@ -26,18 +26,20 @@ Note, that while the dataset was published in 2023, the most recent data contain ### Borders Data relating to the avoid borders features is derived from administrative boundaries features in OpenStreetMap. Information about open borders is obtained from [Wikipedia](https://en.wikipedia.org/wiki/Open_border). -Configuration parameters: [`ors.engine.profiles.*.ext_storages.Borders`](configuration/engine/profiles/build.md#borders) +Configuration parameters: [ +`ors.engine.profiles..ext_storages.Borders`](configuration/engine/profiles/build.md#borders) ### GTFS The public transport profile integrates [GTFS](https://developers.google.com/transit/gtfs) data for the public transit part. GTFS feeds can be obtained e.g. from sites like https://gtfs.de/ (for Germany), or from local public transport operators. -Configuration parameters: [`ors.engine.profiles.*.gtfs_file`](configuration/engine/profiles/build.md) +Configuration parameters: [`ors.engine.profiles..gtfs_file`](configuration/engine/profiles/build.md) ### Green & Quiet The data used to identify green and quiet routes were derived from research projects in the GIScience research group at Heidelberg University. More information about these can be found on the GIScience news blog [here](https://giscienceblog.uni-heidelberg.de/2017/07/03/healthy-routing-prefering-green-areas-added-to-openrouteserviceorg/) and [here](http://giscienceblog.uni-heidelberg.de/2017/07/10/reducing-stress-by-avoiding-noise-with-quiet-routing-in-openrouteservice/) -Configuration parameters: [`ors.engine.profiles.*.ext_storages`](configuration/engine/profiles/build.md#ext_storages) +Configuration parameters: [ +`ors.engine.profiles..ext_storages`](configuration/engine/profiles/build.md#ext_storages) ## Output Files diff --git a/docs/technical-details/graph-repo-client/index.md b/docs/technical-details/graph-repo-client/index.md index 1c8c889b41..fcb3270611 100644 --- a/docs/technical-details/graph-repo-client/index.md +++ b/docs/technical-details/graph-repo-client/index.md @@ -196,7 +196,7 @@ go in touch with us to discuss if we can calculate the required graphs for you a The local directory, where openrouteservice stores and loads graphs, is defined by the configuration property `graph_path` -(in `ors.engine.profile_default` or in a single profile in `ors.engine.profiles..graph_path`). +(in `ors.engine.profile_default` or in a single profile in `ors.engine.profiles..graph_path`). The graphs are located in sub directories named like their routing profiles. E.g. with `ors.engine.profile_default.graph_path: /home/ors/graphs` and the enabled profiles `car` and `bike`, @@ -338,7 +338,7 @@ The general graph management is configured in the config properties beneatch [ To define a graph in the openrouteservice config, configuration parameters similar to the elements of the [repository structure](#repository-structure) are used -(see also [`ors.engine.profiles..repo`](/run-instance/configuration/engine/profiles/repo.md)): +(see also [`ors.engine.profiles..repo`](/run-instance/configuration/engine/profiles/repo.md)): | Repo Path Parameter | Configuration Parameter | |----------------------------|---------------------------------------------------------------------------------------| From d2e9dcda712d5201170d65dadf61a602516acd53 Mon Sep 17 00:00:00 2001 From: Sascha Fendrich Date: Mon, 20 Jan 2025 14:11:51 +0100 Subject: [PATCH 20/20] Fix: failing API test Two API tests interfered after rebase du to usage of same routing profile under different conditions. --- .../java/org/heigit/ors/apitests/routing/ResultTest.java | 3 ++- ors-api/src/test/resources/application-test.yml | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java index ed0e536224..9b49ac4349 100644 --- a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java +++ b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java @@ -191,6 +191,7 @@ public ResultTest() { addParameter("bikeProfile", "cycling-regular"); addParameter("carProfile", "driving-car"); addParameter("footProfile", "foot-walking"); + addParameter("hikeProfile", "foot-hiking"); addParameter("ptProfile", "public-transport"); addParameter("carCustomProfile", "driving-car-no-preparations"); } @@ -4085,7 +4086,7 @@ void testCustomProfileBlockTunnelsRejectedWhenDisallowed() { given() .config(JSON_CONFIG_DOUBLE_NUMBERS) .headers(CommonHeaders.jsonContent) - .pathParam("profile", getParameter("footProfile")) + .pathParam("profile", getParameter("hikeProfile")) .body(body.toString()) .when() .post(getEndPointPath() + "/{profile}") diff --git a/ors-api/src/test/resources/application-test.yml b/ors-api/src/test/resources/application-test.yml index b7c97c2295..28aabdc2f5 100644 --- a/ors-api/src/test/resources/application-test.yml +++ b/ors-api/src/test/resources/application-test.yml @@ -247,15 +247,13 @@ ors: WaySurfaceType: HillIndex: TrailDifficulty: - service: - allow_custom_models: false foot-hiking: enabled: true encoder_name: foot-hiking build: encoder_options: block_fords: false - enable_custom_models: false + enable_custom_models: true ext_storages: GreenIndex: filepath: ./src/test/files/green_streets_hd.csv @@ -267,6 +265,8 @@ ors: WaySurfaceType: HillIndex: TrailDifficulty: + service: + allow_custom_models: false wheelchair: enabled: true encoder_name: wheelchair