diff --git a/CHANGELOG.md b/CHANGELOG.md index f9e1c72ef6..18d921e68a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,8 +56,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)) 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/docs/run-instance/configuration/engine/profiles/build.md b/docs/run-instance/configuration/engine/profiles/build.md index 04bcb9e304..697c13d207 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..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` | ## `preparation` diff --git a/docs/run-instance/configuration/engine/profiles/service.md b/docs/run-instance/configuration/engine/profiles/service.md index 7c9dd775e7..56721cb65f 100644 --- a/docs/run-instance/configuration/engine/profiles/service.md +++ b/docs/run-instance/configuration/engine/profiles/service.md @@ -5,18 +5,19 @@ 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. -| 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/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/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); } 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..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 @@ -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,28 @@ 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.", 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 + private boolean hasCustomModel = false; + @JsonCreator public RouteRequest(@JsonProperty(value = PARAM_COORDINATES, required = true) List> coordinates) { this.coordinates = coordinates; @@ -754,4 +777,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..e2acf39725 --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/requests/routing/RouteRequestCustomModel.java @@ -0,0 +1,42 @@ +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 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; + + @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<>(); + + @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/main/java/org/heigit/ors/api/services/RoutingService.java b/ors-api/src/main/java/org/heigit/ors/api/services/RoutingService.java index 3df74d4ab8..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 @@ -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,23 @@ 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 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() + "'."); + } + } + return RoutingProfileManager.getInstance().computeRoute(routingRequest); } catch (StatusCodeException e) { throw e; @@ -50,159 +140,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 (routeApiRequest.hasWalkingTime()) { + params.setWalkingTime(routeApiRequest.getWalkingTime()); } - if (request.hasWalkingTime()) { - params.setWalkingTime(request.getWalkingTime()); + if (routeApiRequest.hasScheduleRows()) { + params.setScheduleRows(routeApiRequest.getScheduleRows()); } - if (request.hasScheduleRows()) { - params.setScheduleRows(request.getScheduleRows()); + if (routeApiRequest.hasIgnoreTransfers()) { + params.setIgnoreTransfers(routeApiRequest.isIgnoreTransfers()); } - if (request.hasIgnoreTransfers()) { - params.setIgnoreTransfers(request.isIgnoreTransfers()); + if (routeApiRequest.hasScheduleDuration()) { + params.setScheduleDuaration(routeApiRequest.getScheduleDuration()); } - if (request.hasScheduleDuration()) { - params.setScheduleDuaration(request.getScheduleDuration()); + if (routeApiRequest.hasCustomModel()) { + params.setCustomModel(routeApiRequest.getCustomModel().toGHCustomModel()); } params.setConsiderTurnRestrictions(false); @@ -212,6 +306,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); @@ -318,8 +419,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/main/resources/application.yml b/ors-api/src/main/resources/application.yml index 5ae54f1d0e..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: @@ -179,6 +180,7 @@ ors: turn_costs: true block_fords: false use_acceleration: true + enable_custom_models: false preparation: min_network_size: 200 methods: @@ -215,6 +217,7 @@ ors: turn_costs: true block_fords: false use_acceleration: true + enable_custom_models: false preparation: min_network_size: 200 methods: @@ -248,6 +251,7 @@ ors: consider_elevation: true turn_costs: true block_fords: false + enable_custom_models: false ext_storages: WayCategory: WaySurfaceType: @@ -260,6 +264,7 @@ ors: consider_elevation: true turn_costs: true block_fords: false + enable_custom_models: false ext_storages: WayCategory: WaySurfaceType: @@ -272,6 +277,7 @@ ors: consider_elevation: true turn_costs: true block_fords: false + enable_custom_models: false ext_storages: WayCategory: WaySurfaceType: @@ -284,6 +290,7 @@ ors: consider_elevation: true turn_costs: true block_fords: false + enable_custom_models: false ext_storages: WayCategory: WaySurfaceType: @@ -294,6 +301,7 @@ ors: build: encoder_options: block_fords: false + enable_custom_models: false ext_storages: WayCategory: WaySurfaceType: @@ -304,6 +312,7 @@ ors: build: encoder_options: block_fords: false + enable_custom_models: false ext_storages: WayCategory: WaySurfaceType: @@ -314,6 +323,7 @@ ors: build: encoder_options: block_fords: true + enable_custom_models: false ext_storages: WayCategory: WaySurfaceType: @@ -327,6 +337,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 3bdb59014a..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 @@ -140,6 +140,45 @@ public ResultTest() { addParameter("coordinatesPTFlipped", coordinatesPTFlipped); addParameter("coordinatesPT2", coordinatesPT2); + JSONArray coordinatesCustom1 = new JSONArray(); + 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(); + 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 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"); @@ -152,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"); } @@ -3972,6 +4012,303 @@ void testPTFail(String coords, String walkingTime, int errorCode, int messageInd .statusCode(404); } + @Test + void testCustomProfileBlockTunnels() { + 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("carProfile")) + .body(body.toString()) + .when() + .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 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 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("hikeProfile")) + .body(body.toString()) + .when() + .post(getEndPointPath() + "/{profile}") + .then().log().ifValidationFails() + .assertThat() + .statusCode(500); + } + + @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().ifValidationFails() + .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(); + 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"); + 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().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() + .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(7648f, 50f))) + .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 a48032043c..28aabdc2f5 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: true 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: true ext_storages: GreenIndex: filepath: ./src/test/files/green_streets_hd.csv @@ -257,12 +265,15 @@ ors: WaySurfaceType: HillIndex: TrailDifficulty: + service: + allow_custom_models: false wheelchair: enabled: true encoder_name: wheelchair build: encoder_options: block_fords: false + enable_custom_models: false ext_storages: Wheelchair: kerbs_on_crossings: true @@ -277,6 +288,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 +303,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-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: 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/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); } } 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..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 @@ -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,10 @@ public class RouteSearchParameters { private boolean hasWalkingTime = false; private boolean hasScheduleDuration = false; + @Setter + @Getter + private CustomModel customModel; + public int getProfileType() { return profileType; } @@ -327,7 +332,8 @@ public boolean requiresFullyDynamicWeights() { || hasBearings() || hasContinueStraight() || (getProfileParameters() != null && getProfileParameters().hasWeightings()) - || getAlternativeRoutesCount() > 0; + || getAlternativeRoutesCount() > 0 + || customModel != null; } // time-dependent stuff 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/RoutingProfile.java b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java index 003c344107..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 @@ -19,14 +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; import org.heigit.ors.config.EngineProperties; @@ -65,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; @@ -213,6 +211,13 @@ private static ORSGraphHopperConfig createGHSettings(ProfileProperties profile, profiles.put(profileName, new Profile(profileName).setVehicle(vehicle).setWeighting(weighting).setTurnCosts(false)); } + 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)); + } + 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"); if (profile.getBuild().getPreparation() != null) { PreparationProperties preparations = profile.getBuild().getPreparation(); @@ -453,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..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; @@ -91,7 +89,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) { @@ -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 ec7d995b16..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; } @@ -388,6 +397,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..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 @@ -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,8 @@ 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; +import static org.heigit.ors.util.ProfileTools.VAL_RECOMMENDED; /** * This class is a preliminary adaptation of ORSWeightingFactory to the new @@ -81,21 +86,27 @@ 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 ("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); + 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()); } - } else { - if (encoder.supports(PriorityWeighting.class)) { - weighting = new FastestSafeWeighting(encoder, hints, turnCostProvider); - } else { - weighting = new ORSFastestWeighting(encoder, hints, turnCostProvider); + + CustomModel queryCustomModel = requestHints.getObject("custom_model", null); + 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 { + 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/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) 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); diff --git a/pom.xml b/pom.xml index 835fc36bde..578cd445de 100644 --- a/pom.xml +++ b/pom.xml @@ -49,8 +49,8 @@ yyyy-MM-dd'T'HH:mm:ss'Z' - v4.9.5 - + v4.9.6 + 1.18.34 2.0.13 2.22.1