From 251748026a90113654ab2be946c98ab47936d0f0 Mon Sep 17 00:00:00 2001 From: takb Date: Mon, 20 Jan 2025 16:00:26 +0100 Subject: [PATCH 1/6] docs: Add custom-models.md and links --- docs/.vitepress/config.js | 4 + .../endpoints/directions/custom-models.md | 76 +++++++++++++++++++ .../endpoints/directions/index.md | 1 + .../configuration/engine/profiles/build.md | 2 +- 4 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 docs/api-reference/endpoints/directions/custom-models.md diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index c9ca0ac805..bf863b4449 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -93,6 +93,10 @@ export default withMermaid(defineVersionedConfig({ text: 'Routing Options', link: '/api-reference/endpoints/directions/routing-options' }, + { + text: 'Custom Models', + link: '/api-reference/endpoints/directions/custom-models' + }, { text: 'Extra info', collapsed: true, diff --git a/docs/api-reference/endpoints/directions/custom-models.md b/docs/api-reference/endpoints/directions/custom-models.md new file mode 100644 index 0000000000..6dbec76233 --- /dev/null +++ b/docs/api-reference/endpoints/directions/custom-models.md @@ -0,0 +1,76 @@ +# Custom Models + +The request body parameter `custom_model` allows setting specific parameters influencing the edge weightings during +route calculation. + +This parameter is available for directions requests only on profiles that have been created using the required encoder +option set at graph build time. **This feature is still in experimental stat and is currently not available on our public API for any profile**. You can use this feature on your [own openrouteservice instance](/run-instance/) by [enabling it for the profile](/run-instance/configuration/engine/profiles/build.md#encoder-options) in the `encoder_options`. + +The `custom_model` parameter is a JSON object, the following example shows the structure: + +```json +{ + "coordinates": [ + [ + 8.681495, + 49.41461 + ], + [ + 8.687872, + 49.420318 + ] + ], + "custom_model": { + "speed": [ + { + "if": true, + "limit_to": 100 + } + ], + "priority": [ + { + "if": "road_class == MOTORWAY", + "multiply_by": 0 + } + ], + "distance_influence": 100 + } +} +``` + +## Available parameters + +| Parameter | Type | Description | +|----------------------|--------|-------------| +| `distance_influence` | Number | | +| `speed` | Array | | +| `priority` | Array | | +| `areas` | Array | | + +## Examples + +### general speed limit of 80 km/h, e.g. for car profiles + +```json +{ + "speed": [ + { + "if": true, + "limit_to": 80 + } + ] +} +``` + +### avoid motorways and tunnels + +```json +{ + "priority": [ + { + "if": "road_class == PRIMARY || road_environment == TUNNEL", + "multiply_by": 0.7 + } + ] +} +``` \ No newline at end of file diff --git a/docs/api-reference/endpoints/directions/index.md b/docs/api-reference/endpoints/directions/index.md index cc41a1eefd..59a9bfb985 100644 --- a/docs/api-reference/endpoints/directions/index.md +++ b/docs/api-reference/endpoints/directions/index.md @@ -7,6 +7,7 @@ Here, however, a few topics are explained in more detail: * The different directions aka routing [requests and return types](requests-and-return-types.md) * How advanced [Routing Options](routing-options.md) can be defined +* How to use [Custom Models](custom-models.md) to in influence edge weightings during route calculation * How [Extra Info](extra-info/index.md) like road surface, track type, OpenStreetMap way ID or additional [Route Attributes](route-attributes.md) can be requested * How geometries in directions responses can be [decoded](geometry-decoding.md) * How [Instruction Types](instruction-types.md) are encoded in the directions response diff --git a/docs/run-instance/configuration/engine/profiles/build.md b/docs/run-instance/configuration/engine/profiles/build.md index 697c13d207..7b9a0a9d25 100644 --- a/docs/run-instance/configuration/engine/profiles/build.md +++ b/docs/run-instance/configuration/engine/profiles/build.md @@ -34,7 +34,7 @@ Properties beneath `ors.engine.profiles..build.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` | +| enable_custom_models | boolean | * | Enables whether the profile is prepared to support custom models. Also see the corresponding parameter `allow_custom_models` in the [service properties](service.md). | `false` | ## `preparation` From fab4e0bb9a776dbdb9a31ea686e86a263f46bcac Mon Sep 17 00:00:00 2001 From: takb Date: Mon, 27 Jan 2025 16:12:01 +0100 Subject: [PATCH 2/6] docs: custom model description --- .../endpoints/directions/custom-models.md | 212 ++++++++++++++++-- .../configuration/engine/profiles/service.md | 2 +- 2 files changed, 200 insertions(+), 14 deletions(-) diff --git a/docs/api-reference/endpoints/directions/custom-models.md b/docs/api-reference/endpoints/directions/custom-models.md index 6dbec76233..35b8648507 100644 --- a/docs/api-reference/endpoints/directions/custom-models.md +++ b/docs/api-reference/endpoints/directions/custom-models.md @@ -4,12 +4,17 @@ The request body parameter `custom_model` allows setting specific parameters inf route calculation. This parameter is available for directions requests only on profiles that have been created using the required encoder -option set at graph build time. **This feature is still in experimental stat and is currently not available on our public API for any profile**. You can use this feature on your [own openrouteservice instance](/run-instance/) by [enabling it for the profile](/run-instance/configuration/engine/profiles/build.md#encoder-options) in the `encoder_options`. +option set at graph build time. **This feature is still in experimental state and is currently not available on our +public API for any profile**. You can use this feature on your [own openrouteservice instance](/run-instance/) +by [enabling it for the profile](/run-instance/configuration/engine/profiles/build.md#encoder-options) in the +`encoder_options`. -The `custom_model` parameter is a JSON object, the following example shows the structure: +The `custom_model` parameter is a JSON object, the following example shows the structure within a request body for the +directions endpoint: ```json { + "preference": "custom", "coordinates": [ [ 8.681495, @@ -40,16 +45,180 @@ The `custom_model` parameter is a JSON object, the following example shows the s ## Available parameters -| Parameter | Type | Description | -|----------------------|--------|-------------| -| `distance_influence` | Number | | -| `speed` | Array | | -| `priority` | Array | | -| `areas` | Array | | +| Parameter | Type | Description | +|----------------------|--------|----------------------------------------------------------------------------------------------| +| `distance_influence` | Number | Time in seconds to require for each kilometer of [detour accepted](#weighting-function) | +| `speed` | Array | Set of [rules](#speed-and-priority-rules) determining [speed factor](#weighting-function) | +| `priority` | Array | Set of [rules](#speed-and-priority-rules) determining [priority factor](#weighting-function) | +| `areas` | Object | Map of [geojson features](#areas) used in area-based [rules](#speed-and-priority-rules) | -## Examples +## Basic principle -### general speed limit of 80 km/h, e.g. for car profiles +Custom models invoke a special weighting function for each edge in the graph and replaces the `shortest` or `fastest` +weightings normally used during calculations with a combined weighting function that takes into account the custom model +parameters. + +The priority weightings of openrouteservice for foot and bike profiles (that represent preference of designated +footpaths or designated bicycle paths over car roads and so on) can be combined "on top" of the custom model weighting. +Therefore, possible values for the `preference` parameter of a route request with a custom model are `custom` and +`recommended`. If a request is made with the `shortest` or `fastest` preference AND a custom model value, the preference +value will be IGNORED. + +## Weighting function + +The weighting function for each edge is calculated as follows: + +``` +weight = [edge distance] / ([edge speed] * speed * priority) + [edge distance] * distance_influence +``` + +where `speed`, `priority` and `distance_influence` are the values derived from the custom model, and `[edge distance]` +and `[edge speed]` are valued stored in the graph. + +The `distance_influence` parameter is a factor that influences the weight of the edge based on the travel distance. If +set to zero, the distance will not be taken into account in the weighting function. If set to a value greater than zero, +the distance will be multiplied by this factor before being added to the weight. The result of this is that a ratio of +preference between distance and speed can be set. The value provided in the custom model is used such that it +corresponds to the required time saved in seconds per kilometer of detour. If you e.g. set a value of `60`, that means a +faster route by travel time is only returned if it saves at least a minute for every additional kilometer of distance +travelled, therefore a detour to take the highway instead of ordinary roads that adds, say, 2 kilometers, is only +returned as "better" route if this saves at least 2 minutes of time. + +The factors `speed` and `priority` are multiplied with the edge speed. Both are calculated based on the rules provided +in the custom model, the difference being that the `speed` factor also changes the travel time computation, while the +`priority` factor is only used to influence the weight of the edge during route computation and does not change the +travel time. Both parameters expect an array of objects, where each object is a rule consisting of a condition and an +operation to be applied. These are described in more detail in the next section. + +## Speed and Priority Rules + +The parameters `speed` and `priority` are arrays of rules that are applied to the edge speed and priority, respectively. +All rules within the arrays are applied in the order they are provided. Rules are defined as objects of the following +structure: + +```json +{ + "condition_key": "condition_value", + "operation_key": "operation_value" +} +``` + +### Operations + +The `operation_key` is a string with two possible values. The `operation_value` is a number. + +| `operation_key` | `operation_value` | +|-----------------|-------------------------------------------------------------| +| `multiply_by` | Factor to multiply with travel speed, e.g. `0`, `0.5`, etc. | +| `limit_to` | Speed limit in km/h, e.g. `70` | + +If the key is `multiply_by`, the value is interpreted as a factor that gets multiplied to the speed stored on the edge +for determining the weight of the edge. + +If the rule is in the `speed` array, the factor is also applied to the speed value used in the travel time calculation, +so a value of `0.5` would mean to travel at half the normal speed on the affected edges. + +If the rule is in the `priority` array, the factor is only applied to the weight of the edge, not to the time +calculation, and you can think of the number as a factor determining how favorable the affected edge is. A `0` would +mean to avoid the edge completely, a `1` would mean indeterminate; any number in between can denote degrees of +favorability, smaller numbers meaning less favorable. + +If the key is `limit_to`, the value is interpreted as a maximum speed that can be travelled on the affected edges. Edges +with a lower travel speed stored in the graph will not be affected by this rule. + +### Conditions + +The `condition_key` is a string with the possible values `if`, `else_if` and `else`. `else_if` and `else` require a +preceding rule with `if`, `else_if` as condition key, and are applied only if the preceding rule's condition did NOT +match. + +The `condition_value` can be a string or boolean value. If it is a string, it can either have the form +`in_[AREA_NAME]` (see [section below](#areas) for details) OR describe a logical statement that can evaluate as true or +false. The logical statement can be a simple comparison of a variable and a value, e.g. `road_class == MOTORWAY`, or a +more complex statement using Java boolean operators, e.g. `max_width <= 2.2 && (road_environment == TUNNEL || roundabout)`. + +The variables that can be used in those statements are called 'encoded values', and different ones are available for +different profiles. The available encoded values can be found in the response to the `status` endpoint, in the array +`profiles..encoded_values`. Below is a list of example 'encoded values' and their possible values that are +available. Note that this list is incomplete and the available variables depend on the profile in question. + +| name | Type | Description | +|--------------------|-----------|---------------------------------------------------------------------------| +| `road_class` | Enum | MOTORWAY, TRUNK, PRIMARY, SECONDARY, TRACK, STEPS, CYCLEWAY, FOOTWAY, ... | +| `road_environment` | Enum | ROAD, FERRY, BRIDGE, TUNNEL, ... | +| `road_access` | Enum | DESTINATION, DELIVERY, PRIVATE, NO, ... | +| `surface` | Enum | PAVED, DIRT, SAND, GRAVEL, ... | +| `smoothness` | Enum | EXCELLENT, GOOD, INTERMEDIATE, ... | +| `toll` | Enum | NO, ALL, HGV | +| `roundabout` | Boolean | Whether edge is part of a roundabout | +| `get_off_bike` | Boolean | Whether edge is marked as "requiring to get off bike" | +| `max_speed` | Numerical | Max speed in km/h | +| `max_height` | Numerical | Max height in m | +| `max_width` | Numerical | Max width in m | + +Enum type encoded values can be used with the `==` and `!=` operators, while numerical encoded values can be used with the +`==`, `!=`, `<`, `<=`, `>`, `>=` operators. Boolean encoded valued do not require an operator, but can be used with the +`==`, `!=` and `!` operators. + +### Areas + +Area-based rules are defined by using a `condition_value` in the form `in_[AREA_NAME]` in the `priority` ond/or `speed` +arrays and providing a corresponding named object in the `areas` object of the custom model. + +Each area object is required to be a GeoJSON `Feature` object. The `bbox` member and other foreign members such as +`properties` are NOT supported. The `geometry` member of the feature object must be of `"type": "Polygon"`. + +The following is an example of a custom model with an area-based rule that sets the priority to zero (avoid completely) +for all edges within the area defined by the polygon: + +```json +{ + "priority": [ + { + "if": "in_custom_area_1", + "multiply_by": 0 + } + ], + "areas": { + "custom_area_1": { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 8.7062144, + 49.4077481 + ], + [ + 8.7068045, + 49.4108196 + ], + [ + 8.7132203, + 49.4117201 + ], + [ + 8.7139713, + 49.4084322 + ], + [ + 8.7062144, + 49.4077481 + ] + ] + ] + } + } + } +} +``` + +## Further Examples + +Below are some examples of custom models that illustrate how the `custom_model` parameter can be used. + +#### A general speed limit of 80 km/h, e.g. for car profiles ```json { @@ -62,15 +231,32 @@ The `custom_model` parameter is a JSON object, the following example shows the s } ``` -### avoid motorways and tunnels +#### Try to avoid motorways and tunnels (even more), avoid applying both rules to motorways within tunnels ```json { "priority": [ { - "if": "road_class == PRIMARY || road_environment == TUNNEL", + "if": "road_class == MOTORWAY", "multiply_by": 0.7 + }, + { + "else_if": "road_environment == TUNNEL", + "multiply_by": 0.5 } ] } -``` \ No newline at end of file +``` + +#### Avoid having to get off the bike + +```json +{ + "priority": [ + { + "if": "get_off_bike", + "multiply_by": 0 + } + ] +} +``` diff --git a/docs/run-instance/configuration/engine/profiles/service.md b/docs/run-instance/configuration/engine/profiles/service.md index 56721cb65f..bdf90f8b19 100644 --- a/docs/run-instance/configuration/engine/profiles/service.md +++ b/docs/run-instance/configuration/engine/profiles/service.md @@ -16,7 +16,7 @@ need to be set specifically for each profile. More parameters relevant at query | 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` | +| 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. | `true` | | execution | object | [Execution settings](#execution) relevant when querying services | | ## `execution` From 0cca1838d61e6021025c1272eda71b64eba847eb Mon Sep 17 00:00:00 2001 From: takb Date: Mon, 27 Jan 2025 16:50:35 +0100 Subject: [PATCH 3/6] test: improve error messages & add some more apitests --- .../ors/apitests/routing/ResultTest.java | 202 +++++++++++++++++- .../heigit/ors/routing/RoutingRequest.java | 2 + 2 files changed, 203 insertions(+), 1 deletion(-) 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 9b49ac4349..a6b66cc3d4 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 @@ -4065,6 +4065,8 @@ void testCustomProfileBlockTunnelsRejectedWhenDisabled() { .post(getEndPointPath() + "/{profile}") .then().log().ifValidationFails() .assertThat() + .body("error.code", is(2018)) + .body("error.message", is("Custom model not available for profile 'cycling-regular'.")) .statusCode(500); } @@ -4090,8 +4092,10 @@ void testCustomProfileBlockTunnelsRejectedWhenDisallowed() { .body(body.toString()) .when() .post(getEndPointPath() + "/{profile}") - .then().log().ifValidationFails() + .then().log().all() .assertThat() + .body("error.code", is(2018)) + .body("error.message", is("Custom model disabled for profile 'foot-hiking'.")) .statusCode(500); } @@ -4235,6 +4239,140 @@ void testCustomProfileDistanceInfluence() { .statusCode(200); } + + @Test + void testCustomProfileLimitSpeed() { + JSONObject body = new JSONObject(); + body.put("coordinates", getParameter("coordinatesCustom2")); + body.put("preference", getParameter("preference")); + body.put("instructions", true); + body.put("elevation", true); + + JSONObject customModel = new JSONObject(); + customModel.put("priority", new JSONArray()); + customModel.put("distance_influence", 0); + JSONObject speed = new JSONObject(); + speed.put("if", true); + speed.put("limit_to", 60); + customModel.put("speed", new JSONArray().put(speed)); + 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, 50f))) + .body("routes[0].summary.duration", is(closeTo(702f, 5f))) + .statusCode(200); + } + + @Test + void testCustomProfileAvoidByMaxSpeed() { + JSONObject body = new JSONObject(); + body.put("coordinates", getParameter("coordinatesCustom2")); + body.put("preference", getParameter("preference")); + body.put("instructions", true); + body.put("elevation", true); + + JSONObject customModel = new JSONObject(); + customModel.put("distance_influence", 0); + JSONObject priority = new JSONObject(); + priority.put("if", "max_speed > 60"); + priority.put("multiply_by", 0.1); + customModel.put("priority", new JSONArray().put(priority)); + 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(11909.7, 50f))) + .body("routes[0].summary.duration", is(closeTo(1677.6, 5f))) + .statusCode(200); + } + + @Test + void testCustomProfileComplexCondition() { + 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(); + customModel.put("distance_influence", 0); + JSONObject priority = new JSONObject(); + priority.put("if", "max_speed > 30 && (road_environment == TUNNEL || roundabout)"); + priority.put("multiply_by", 0.1); + customModel.put("priority", new JSONArray().put(priority)); + 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(3338.7, 50f))) + .body("routes[0].summary.duration", is(closeTo(602.3, 5f))) + .statusCode(200); + } + + @Test + void testCustomProfileAvoidBooleanEncodedValue() { + JSONObject body = new JSONObject(); + JSONArray coordinates = new JSONArray(); + JSONArray coord1 = new JSONArray(); + coord1.put(8.6818009); + coord1.put( 49.397251); + coordinates.put(coord1); + JSONArray coord2 = new JSONArray(); + coord2.put(8.6769783); + coord2.put(49.3962072); + coordinates.put(coord2); + body.put("coordinates", coordinates); + body.put("preference", getParameter("preference")); + body.put("instructions", true); + body.put("elevation", true); + + JSONObject customModel = new JSONObject(); + customModel.put("distance_influence", 0); + JSONObject priority = new JSONObject(); + priority.put("if", "roundabout"); + 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("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(1287.9, 50f))) // big detour to avoid the roundabout + .statusCode(200); + } + @Test void testCustomProfileWithRecommended() { JSONObject body = new JSONObject(); @@ -4309,6 +4447,68 @@ void testCustomProfileWithoutModelDoesntBreak() { //??? .statusCode(200); } + @Test + void testCustomProfileUnavailableEncodedValueFails() { + JSONObject body = new JSONObject(); + body.put("coordinates", getParameter("coordinatesCustom2")); + body.put("preference", getParameter("preference")); + body.put("instructions", true); + body.put("elevation", true); + + JSONObject customModel = new JSONObject(); + customModel.put("distance_influence", 0); + JSONObject priority = new JSONObject(); + priority.put("if", "max_width < 30"); + priority.put("multiply_by", 0.1); + customModel.put("priority", new JSONArray().put(priority)); + 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(false)) + .body("error.code", is(2018)) + .body("error.message", is("Cannot compile expression: in 'priority' entry, invalid expression \"max_width < 30\": encoded value 'max_width' not available")) + .statusCode(500); + } + + @Test + void testCustomProfileInvalidConditionFails() { + JSONObject body = new JSONObject(); + body.put("coordinates", getParameter("coordinatesCustom2")); + body.put("preference", getParameter("preference")); + body.put("instructions", true); + body.put("elevation", true); + + JSONObject customModel = new JSONObject(); + customModel.put("distance_influence", 0); + JSONObject priority = new JSONObject(); + priority.put("if", "äöü this is not a valid condition expression."); + priority.put("multiply_by", 0.1); + customModel.put("priority", new JSONArray().put(priority)); + 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(false)) + .body("error.code", is(2018)) + .body("error.message", is("Cannot compile expression: in 'priority' entry, invalid expression \"äöü this is not a valid condition expression.\"")) + .statusCode(500); + } + 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/RoutingRequest.java b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingRequest.java index 0bb9c148c5..a0db5762a3 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 @@ -828,6 +828,8 @@ private RouteResult[] computeLinearRoute() throws Exception { } } throw new PointNotFoundException(message.toString()); + } else if (gr.getErrors().get(0) instanceof IllegalArgumentException) { + throw new InternalServerException(RoutingErrorCodes.UNSUPPORTED_REQUEST_OPTION, gr.getErrors().get(0).getMessage()); } else { throw new InternalServerException(RoutingErrorCodes.UNKNOWN, gr.getErrors().get(0).getMessage()); } From 555265ca65e5a2f3fcce6bf8dafc5e6c7ba59315 Mon Sep 17 00:00:00 2001 From: takb Date: Wed, 29 Jan 2025 12:05:13 +0100 Subject: [PATCH 4/6] docs: fix wording, weighting formula, status endpoint response listing --- .../endpoints/directions/custom-models.md | 69 ++++++++++--------- docs/api-reference/endpoints/status/index.md | 33 +++++++++ 2 files changed, 69 insertions(+), 33 deletions(-) diff --git a/docs/api-reference/endpoints/directions/custom-models.md b/docs/api-reference/endpoints/directions/custom-models.md index 35b8648507..aed8081f1a 100644 --- a/docs/api-reference/endpoints/directions/custom-models.md +++ b/docs/api-reference/endpoints/directions/custom-models.md @@ -45,12 +45,12 @@ directions endpoint: ## Available parameters -| Parameter | Type | Description | -|----------------------|--------|----------------------------------------------------------------------------------------------| -| `distance_influence` | Number | Time in seconds to require for each kilometer of [detour accepted](#weighting-function) | -| `speed` | Array | Set of [rules](#speed-and-priority-rules) determining [speed factor](#weighting-function) | -| `priority` | Array | Set of [rules](#speed-and-priority-rules) determining [priority factor](#weighting-function) | -| `areas` | Object | Map of [geojson features](#areas) used in area-based [rules](#speed-and-priority-rules) | +| Parameter | Type | Description | +|----------------------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `distance_influence` | Number | Time saved in seconds to require for each kilometer of [detour accepted](#weighting-function), determining [distance influence factor](#weighting-function) | +| `speed` | Array | Set of [rules](#speed-and-priority-rules) determining [speed factor](#weighting-function) | +| `priority` | Array | Set of [rules](#speed-and-priority-rules) determining [priority factor](#weighting-function) | +| `areas` | Object | Map of [GeoJSON Features](#areas) used in area-based [rules](#speed-and-priority-rules) | ## Basic principle @@ -68,27 +68,27 @@ value will be IGNORED. The weighting function for each edge is calculated as follows: -``` -weight = [edge distance] / ([edge speed] * speed * priority) + [edge distance] * distance_influence -``` +weight = distance / (speed * `speed factor` * `priority factor`) + distance * `distance influence factor` -where `speed`, `priority` and `distance_influence` are the values derived from the custom model, and `[edge distance]` -and `[edge speed]` are valued stored in the graph. +where `speed factor`, `priority factor` and `distance influence factor` are values derived from the respective +parameters of the custom model, and distance and speed are values stored in the graph for each edge. -The `distance_influence` parameter is a factor that influences the weight of the edge based on the travel distance. If -set to zero, the distance will not be taken into account in the weighting function. If set to a value greater than zero, -the distance will be multiplied by this factor before being added to the weight. The result of this is that a ratio of -preference between distance and speed can be set. The value provided in the custom model is used such that it -corresponds to the required time saved in seconds per kilometer of detour. If you e.g. set a value of `60`, that means a -faster route by travel time is only returned if it saves at least a minute for every additional kilometer of distance -travelled, therefore a detour to take the highway instead of ordinary roads that adds, say, 2 kilometers, is only -returned as "better" route if this saves at least 2 minutes of time. +The `distance_influence` parameter is used to calculate the `distance influence factor` that influences the weight of +the edge based on the travel distance. If set to zero, the distance will not be taken into account in the weighting +function. If set to a value greater than zero, the distance will be multiplied by the derived factor before being added +to the weight. The result of this is that a ratio of preference between distance and speed can be set. The value +provided in the custom model is used such that it corresponds to the required time saved in seconds per kilometer of +detour. If you e.g. set `distance_influence` to 60, that means a faster route by travel time is only returned if it +saves at least a minute for every additional kilometer of distance travelled, therefore a detour to take the highway +instead of ordinary roads that adds, say, 2 kilometers, is only returned as "better" route if this saves at least 2 +minutes of time. -The factors `speed` and `priority` are multiplied with the edge speed. Both are calculated based on the rules provided -in the custom model, the difference being that the `speed` factor also changes the travel time computation, while the -`priority` factor is only used to influence the weight of the edge during route computation and does not change the -travel time. Both parameters expect an array of objects, where each object is a rule consisting of a condition and an -operation to be applied. These are described in more detail in the next section. +The `speed factor` and the `priority factor` are multiplied with the edge speed. Both are calculated based on the rules +provided in the corresponding parameters `speed` and `priority` in the custom model, the difference being that the +`speed factor` also changes the travel time computation, while the `priority factor` is only used to influence the +weight of the edge during route computation and does not change the travel time. Both parameters expect an array of +objects, where each object is a rule consisting of a condition and an operation to be applied. These are described in +more detail in the next section. ## Speed and Priority Rules @@ -98,8 +98,8 @@ structure: ```json { - "condition_key": "condition_value", - "operation_key": "operation_value" + "operation_key": "operation_value", + "condition_key": "condition_value" } ``` @@ -124,22 +124,24 @@ mean to avoid the edge completely, a `1` would mean indeterminate; any number in favorability, smaller numbers meaning less favorable. If the key is `limit_to`, the value is interpreted as a maximum speed that can be travelled on the affected edges. Edges -with a lower travel speed stored in the graph will not be affected by this rule. +with a higher speed will have their speeds adjusted, edges with a lower travel speed stored in the graph will not be +affected by this rule. ### Conditions The `condition_key` is a string with the possible values `if`, `else_if` and `else`. `else_if` and `else` require a -preceding rule with `if`, `else_if` as condition key, and are applied only if the preceding rule's condition did NOT +preceding rule with `if` or `else_if` as condition key, and are applied only if the preceding rule's condition did NOT match. The `condition_value` can be a string or boolean value. If it is a string, it can either have the form `in_[AREA_NAME]` (see [section below](#areas) for details) OR describe a logical statement that can evaluate as true or false. The logical statement can be a simple comparison of a variable and a value, e.g. `road_class == MOTORWAY`, or a -more complex statement using Java boolean operators, e.g. `max_width <= 2.2 && (road_environment == TUNNEL || roundabout)`. +more complex statement using Java boolean operators, e.g. +`max_width <= 2.2 && (road_environment == TUNNEL || roundabout)`. -The variables that can be used in those statements are called 'encoded values', and different ones are available for -different profiles. The available encoded values can be found in the response to the `status` endpoint, in the array -`profiles..encoded_values`. Below is a list of example 'encoded values' and their possible values that are +The variables that can be used in those statements are called `encoded values`, and different ones are available for +different profiles. The available `encoded values` can be found in the response to the [status endpoint](/api-reference/endpoints/status/), in the array +`profiles..encoded_values`. Below is a list of example `encoded values` and their possible values that are available. Note that this list is incomplete and the available variables depend on the profile in question. | name | Type | Description | @@ -156,7 +158,8 @@ available. Note that this list is incomplete and the available variables depend | `max_height` | Numerical | Max height in m | | `max_width` | Numerical | Max width in m | -Enum type encoded values can be used with the `==` and `!=` operators, while numerical encoded values can be used with the +Enum type encoded values can be used with the `==` and `!=` operators, while numerical encoded values can be used with +the `==`, `!=`, `<`, `<=`, `>`, `>=` operators. Boolean encoded valued do not require an operator, but can be used with the `==`, `!=` and `!` operators. diff --git a/docs/api-reference/endpoints/status/index.md b/docs/api-reference/endpoints/status/index.md index 117809c471..8d3aba1e77 100644 --- a/docs/api-reference/endpoints/status/index.md +++ b/docs/api-reference/endpoints/status/index.md @@ -20,6 +20,7 @@ The GET request http://localhost:8082/ors/v2/status (host and port are dependent The profile names are used as path parameters in API requests and as directory names for the graph directories. Some basic information is shown for each profile: * `encoder_name`: The vehicle type + * `encoded_values`: The list of available encoded values that can be used in [custom models](/api-reference/endpoints/directions/custom-models) * `osm_date`: Timestamp of the osm pbf file that was used for building the graph. This is usually the date of the latest included change. * `graph_build_date`: The date, when graph building was started for this routing profile. @@ -106,6 +107,23 @@ This is an example response: } }, "encoder_name": "driving-car", + "encoded_values": [ + "road_environment", + "car_ors_fastest_with_turn_costs_subnetwork", + "car_ors_fastest_subnetwork", + "car_ors_shortest_with_turn_costs_subnetwork", + "car_ors_shortest_subnetwork", + "car_ors_recommended_with_turn_costs_subnetwork", + "car_ors_recommended_subnetwork", + "roundabout", + "road_class", + "road_class_link", + "max_speed", + "road_access", + "car_ors$access", + "car_ors$average_speed", + "car_ors$turn_cost" + ], "graph_build_date": "2024-10-28T14:42:49Z", "osm_date": "2023-10-11T20:21:48Z", "limits": { @@ -117,6 +135,21 @@ This is an example response: }, "pedestrian": { "encoder_name": "foot-walking", + "encoded_values": [ + "road_environment", + "pedestrian_ors_fastest_subnetwork", + "pedestrian_ors_shortest_subnetwork", + "pedestrian_ors_recommended_subnetwork", + "roundabout", + "road_class", + "road_class_link", + "max_speed", + "road_access", + "foot_network", + "pedestrian_ors$access", + "pedestrian_ors$average_speed", + "pedestrian_ors$priority" + ], "graph_build_date": "2024-10-11T11:08:44Z", "osm_date": "2024-01-22T21:21:14Z", "limits": { From 93073a0f723a9909ffa58e02f3b35d0d83012d5f Mon Sep 17 00:00:00 2001 From: takb Date: Wed, 29 Jan 2025 12:06:09 +0100 Subject: [PATCH 5/6] docs: fix swagger output --- .../routing/RouteRequestCustomModel.java | 8 +++++++- .../routing/RouteRequestCustomModelAreas.java | 11 +++++++++++ .../RouteRequestCustomModelGeoJSONFeature.java | 18 ++++++++++++++++++ ...questCustomModelGeoJSONPolygonGeometry.java | 17 +++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModelAreas.java create mode 100644 ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModelGeoJSONFeature.java create mode 100644 ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModelGeoJSONPolygonGeometry.java 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 e2acf39725..1ae7e8e592 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 @@ -6,6 +6,7 @@ import com.graphhopper.json.Statement; import com.graphhopper.util.CustomModel; import com.graphhopper.util.JsonFeature; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.ArrayList; import java.util.HashMap; @@ -14,21 +15,26 @@ public class RouteRequestCustomModel { @JsonProperty("distance_influence") + @Schema(description = "Parameter determining the influence of the distance between two points on the edge weight") private double distanceInfluence; @JsonProperty("heading_penalty") + @Schema(hidden = true) private double headingPenalty = (double) 300.0F; @JsonProperty("speed") @JsonDeserialize(contentUsing = StatementDeserializer.class) + @Schema(description = "Array of objects describing rules to be applied to the speed of edges") private List speedStatements = new ArrayList<>(); @JsonProperty("priority") @JsonDeserialize(contentUsing = StatementDeserializer.class) + @Schema(description = "Array of objects describing rules to be applied to the priority of edges") private List priorityStatements = new ArrayList<>(); @JsonProperty("areas") - private Map areas = new HashMap(); + @Schema(implementation = RouteRequestCustomModelAreas.class, description = "Map of areas that can be referenced in speed and priority rules") + private Map areas = new HashMap<>(); public CustomModel toGHCustomModel() { CustomModel customModel = new CustomModel(); diff --git a/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModelAreas.java b/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModelAreas.java new file mode 100644 index 0000000000..0933370565 --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModelAreas.java @@ -0,0 +1,11 @@ +package org.heigit.ors.api.requests.routing; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.Map; + +@Schema(type = "object", implementation = RouteRequestCustomModelGeoJSONFeature.class) +public interface RouteRequestCustomModelAreas extends Map { + @Schema(hidden = true) + boolean isEmpty(); +} diff --git a/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModelGeoJSONFeature.java b/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModelGeoJSONFeature.java new file mode 100644 index 0000000000..5ae3824809 --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModelGeoJSONFeature.java @@ -0,0 +1,18 @@ +package org.heigit.ors.api.requests.routing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RouteRequestCustomModelGeoJSONFeature { + @JsonProperty("type") + @Schema(description = "GeoJSON type", defaultValue = "Feature") + public final String type = "Feature"; + + @JsonProperty("geometry") + @Schema(description = "Feature geometry") + public RouteRequestCustomModelGeoJSONPolygonGeometry geometry; +} diff --git a/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModelGeoJSONPolygonGeometry.java b/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModelGeoJSONPolygonGeometry.java new file mode 100644 index 0000000000..171f9d469a --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModelGeoJSONPolygonGeometry.java @@ -0,0 +1,17 @@ +package org.heigit.ors.api.requests.routing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RouteRequestCustomModelGeoJSONPolygonGeometry { + @JsonProperty("type") + @Schema(description = "GeoJSON type", defaultValue = "Polygon") + public final String type = "Polygon"; + + @JsonProperty("coordinates") + public Double[][][] coordinates; +} From 50ff792984ce32a70e65146dba98670a83846e09 Mon Sep 17 00:00:00 2001 From: takb Date: Wed, 29 Jan 2025 12:07:08 +0100 Subject: [PATCH 6/6] test: have one test with inverted statement property order --- .../test/java/org/heigit/ors/apitests/routing/ResultTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a6b66cc3d4..1ff350a273 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 @@ -4354,8 +4354,8 @@ void testCustomProfileAvoidBooleanEncodedValue() { JSONObject customModel = new JSONObject(); customModel.put("distance_influence", 0); JSONObject priority = new JSONObject(); - priority.put("if", "roundabout"); priority.put("multiply_by", 0); + priority.put("if", "roundabout"); customModel.put("priority", new JSONArray().put(priority)); body.put("custom_model", customModel);