From 5ca2c56628771c8e5d7066bae489919693780308 Mon Sep 17 00:00:00 2001 From: Sascha Fendrich Date: Thu, 10 Aug 2023 10:56:03 +0200 Subject: [PATCH 01/18] feat: add snapping endpoint This commit adds a new service and endpoints that returns given locations snapped to the nearest edge in the graph. --- .../ors/api/controllers/SnappingAPI.java | 163 ++++++++++++++++++ .../requests/snapping/SnappingApiRequest.java | 66 +++++++ .../responses/matrix/json/JSON2DSources.java | 2 +- .../responses/snapping/SnappingResponse.java | 12 ++ .../snapping/json/JsonSnappingResponse.java | 37 ++++ .../ors/api/services/SnappingService.java | 73 ++++++++ .../ors/apitests/snapping/ParamsTest.java | 48 ++++++ .../java/org/heigit/ors/routing/APIEnums.java | 26 +++ .../ors/routing/RoutingProfileManager.java | 15 +- .../ors/snapping/SnappingErrorCodes.java | 18 ++ .../heigit/ors/snapping/SnappingRequest.java | 49 ++++++ .../heigit/ors/snapping/SnappingResult.java | 15 ++ 12 files changed, 512 insertions(+), 12 deletions(-) create mode 100644 ors-api/src/main/java/org/heigit/ors/api/controllers/SnappingAPI.java create mode 100644 ors-api/src/main/java/org/heigit/ors/api/requests/snapping/SnappingApiRequest.java create mode 100644 ors-api/src/main/java/org/heigit/ors/api/responses/snapping/SnappingResponse.java create mode 100644 ors-api/src/main/java/org/heigit/ors/api/responses/snapping/json/JsonSnappingResponse.java create mode 100644 ors-api/src/main/java/org/heigit/ors/api/services/SnappingService.java create mode 100644 ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java create mode 100644 ors-engine/src/main/java/org/heigit/ors/snapping/SnappingErrorCodes.java create mode 100644 ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java create mode 100644 ors-engine/src/main/java/org/heigit/ors/snapping/SnappingResult.java diff --git a/ors-api/src/main/java/org/heigit/ors/api/controllers/SnappingAPI.java b/ors-api/src/main/java/org/heigit/ors/api/controllers/SnappingAPI.java new file mode 100644 index 0000000000..308bdaff3c --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/controllers/SnappingAPI.java @@ -0,0 +1,163 @@ +/* + * This file is part of Openrouteservice. + * + * Openrouteservice is free software; you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 + * of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with this library; + * if not, see . + */ + +package org.heigit.ors.api.controllers; + +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import org.heigit.ors.api.errors.CommonResponseEntityExceptionHandler; +import org.heigit.ors.api.requests.snapping.SnappingApiRequest; +import org.heigit.ors.api.responses.snapping.json.JsonSnappingResponse; +import org.heigit.ors.api.services.SnappingService; +import org.heigit.ors.exceptions.*; +import org.heigit.ors.routing.APIEnums; +import org.heigit.ors.snapping.SnappingErrorCodes; +import org.heigit.ors.snapping.SnappingResult; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageConversionException; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.*; + +@RestController +@Tag(name = "Snapping Service", description = "Snap coordinates to the graph edges.") +@RequestMapping("/v2/snap") +@ApiResponse(responseCode = "400", description = "The request is incorrect and therefore can not be processed.") +@ApiResponse(responseCode = "404", description = "An element could not be found. If possible, a more detailed error code is provided.") +@ApiResponse(responseCode = "405", description = "The specified HTTP method is not supported. For more details, refer to the EndPoint documentation.") +@ApiResponse(responseCode = "413", description = "The request is larger than the server is able to process, the data provided in the request exceeds the capacity limit.") +@ApiResponse(responseCode = "500", description = "An unexpected error was encountered and a more detailed error code is provided.") +@ApiResponse(responseCode = "501", description = "Indicates that the server does not support the functionality needed to fulfill the request.") +@ApiResponse(responseCode = "503", description = "The server is currently unavailable due to overload or maintenance.") +public class SnappingAPI { + static final CommonResponseEntityExceptionHandler errorHandler = new CommonResponseEntityExceptionHandler(SnappingErrorCodes.BASE); + + private final SnappingService snappingService; + + public SnappingAPI(SnappingService snappingService) { + this.snappingService = snappingService; + } + + // generic catch methods - when extra info is provided in the url, the other methods are accessed. + @GetMapping + @Operation(hidden = true) + public void getGetMapping() throws MissingParameterException { + throw new MissingParameterException(SnappingErrorCodes.MISSING_PARAMETER, "profile"); + } + + @PostMapping + @Operation(hidden = true) + public String getPostMapping(@RequestBody SnappingApiRequest request) throws MissingParameterException { + throw new MissingParameterException(SnappingErrorCodes.MISSING_PARAMETER, "profile"); + } + + // Matches any response type that has not been defined + @PostMapping(value = "/{profile}/*") + @Operation(hidden = true) + public void getInvalidResponseType() throws StatusCodeException { + throw new StatusCodeException(HttpServletResponse.SC_NOT_ACCEPTABLE, SnappingErrorCodes.UNSUPPORTED_EXPORT_FORMAT, "This response format is not supported"); + } + + // Functional request methods + @PostMapping(value = "/{profile}") + @Operation( + description = """ + Returns a list of points snapped to the nearest edge in the graph. In case an appropriate + snapping point cannot be found within the specified search radius, "null" is returned. + """, + summary = "Snapping Service" + ) + @ApiResponse( + responseCode = "200", + description = "Standard response for successfully processed requests. Returns JSON.", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = JsonSnappingResponse.class) + ) + }) + public JsonSnappingResponse getDefault(@Parameter(description = "Specifies the route profile.", required = true, example = "driving-car") @PathVariable APIEnums.Profile profile, + @Parameter(description = "The request payload", required = true) @RequestBody SnappingApiRequest request) throws StatusCodeException { + return getJsonSnapping(profile, request); + } + + @PostMapping(value = "/{profile}/json", produces = {"application/json;charset=UTF-8"}) + @Operation( + description = """ + Returns a list of points snapped to the nearest edge in the graph. In case an appropriate + snapping point cannot be found within the specified search radius, \"null\" is returned. + """, + summary = "Snapping Service JSON" + ) + @ApiResponse( + responseCode = "200", + description = "JSON Response.", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = JsonSnappingResponse.class) + ) + }) + public JsonSnappingResponse getJsonSnapping( + @Parameter(description = "Specifies the profile.", required = true, example = "driving-car") @PathVariable APIEnums.Profile profile, + @Parameter(description = "The request payload", required = true) @RequestBody SnappingApiRequest request) throws StatusCodeException { + request.setProfile(profile); + request.setResponseType(APIEnums.SnappingResponseType.JSON); + + SnappingResult result = snappingService.generateSnappingFromRequest(request); + + return new JsonSnappingResponse(result); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity handleMissingParams(final MissingServletRequestParameterException e) { + return errorHandler.handleStatusCodeException(new MissingParameterException(SnappingErrorCodes.MISSING_PARAMETER, e.getParameterName())); + } + + @ExceptionHandler({HttpMessageNotReadableException.class, HttpMessageConversionException.class, Exception.class}) + public ResponseEntity handleReadingBodyException(final Exception e) { + final Throwable cause = e.getCause(); + if (cause instanceof UnrecognizedPropertyException exception) { + return errorHandler.handleUnknownParameterException(new UnknownParameterException(SnappingErrorCodes.UNKNOWN_PARAMETER, exception.getPropertyName())); + } else if (cause instanceof InvalidFormatException exception) { + return errorHandler.handleStatusCodeException(new ParameterValueException(SnappingErrorCodes.INVALID_PARAMETER_FORMAT, exception.getValue().toString())); + } else if (cause instanceof InvalidDefinitionException exception) { + return errorHandler.handleStatusCodeException(new ParameterValueException(SnappingErrorCodes.INVALID_PARAMETER_VALUE, exception.getPath().get(0).getFieldName())); + } else if (cause instanceof MismatchedInputException exception) { + return errorHandler.handleStatusCodeException(new ParameterValueException(SnappingErrorCodes.INVALID_PARAMETER_FORMAT, exception.getPath().get(0).getFieldName())); + } else if (cause instanceof ConversionFailedException exception) { + return errorHandler.handleStatusCodeException(new ParameterValueException(SnappingErrorCodes.INVALID_PARAMETER_VALUE, (String) exception.getValue())); + } else { + // Check if we are missing the body as a whole + if (e.getLocalizedMessage().startsWith("Required request body is missing")) { + return errorHandler.handleStatusCodeException(new EmptyElementException(SnappingErrorCodes.MISSING_PARAMETER, "Request body could not be read")); + } + return errorHandler.handleGenericException(e); + } + } + + @ExceptionHandler(StatusCodeException.class) + public ResponseEntity handleException(final StatusCodeException e) { + return errorHandler.handleStatusCodeException(e); + } +} diff --git a/ors-api/src/main/java/org/heigit/ors/api/requests/snapping/SnappingApiRequest.java b/ors-api/src/main/java/org/heigit/ors/api/requests/snapping/SnappingApiRequest.java new file mode 100644 index 0000000000..82cf905593 --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/requests/snapping/SnappingApiRequest.java @@ -0,0 +1,66 @@ +package org.heigit.ors.api.requests.snapping; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import org.heigit.ors.api.requests.common.APIRequest; +import org.heigit.ors.routing.APIEnums; + +import java.util.List; + +@Schema(name = "SnappingRequest", description = "Snapping service endpoint.") +@JsonInclude(JsonInclude.Include.NON_DEFAULT) +public class SnappingApiRequest extends APIRequest { + public static final String PARAM_PROFILE = "profile"; + public static final String PARAM_LOCATIONS = "locations"; + public static final String PARAM_MAXIMUM_SEARCH_RADIUS = "maximum_search_radius"; + public static final String PARAM_FORMAT = "format"; + + @Schema(name = PARAM_PROFILE, hidden = true) + private APIEnums.Profile profile; + + @Schema(name = PARAM_LOCATIONS, description = "The locations to be snapped as array of `longitude/latitude` pairs.", + example = "[[8.681495,49.41461],[8.686507,49.41943]]", + requiredMode = Schema.RequiredMode.REQUIRED) + @JsonProperty(PARAM_LOCATIONS) + private List> locations; //apparently, this has to be a non-primitive type… + + @Schema(name = PARAM_FORMAT, hidden = true) + @JsonProperty(PARAM_FORMAT) + private APIEnums.SnappingResponseType responseType = APIEnums.SnappingResponseType.JSON; + + @Schema(name = PARAM_MAXIMUM_SEARCH_RADIUS, description = "Maximum radius in meters around given coordinates to search for graph edges.", + example ="300", requiredMode = Schema.RequiredMode.REQUIRED) + @JsonProperty(PARAM_MAXIMUM_SEARCH_RADIUS) + private double maximumSearchRadius; + + @JsonCreator + public SnappingApiRequest(@JsonProperty(value = PARAM_LOCATIONS, required = true) List> locations) { + this.locations = locations; + } + + public APIEnums.Profile getProfile() { + return profile; + } + + public void setProfile(APIEnums.Profile profile) { + this.profile = profile; + } + + public double getMaximumSearchRadius() { + return maximumSearchRadius; + } + + public List> getLocations() { + return locations; + } + + public void setLocations(List> locations) { + this.locations = locations; + } + + public void setResponseType(APIEnums.SnappingResponseType responseType) { + this.responseType = responseType; + } +} diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/matrix/json/JSON2DSources.java b/ors-api/src/main/java/org/heigit/ors/api/responses/matrix/json/JSON2DSources.java index d15b88f182..fda891f653 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/responses/matrix/json/JSON2DSources.java +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/matrix/json/JSON2DSources.java @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_DEFAULT) public class JSON2DSources extends JSONLocation { - JSON2DSources(ResolvedLocation source, boolean includeResolveLocations) { + public JSON2DSources(ResolvedLocation source, boolean includeResolveLocations) { super(source, includeResolveLocations); } diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/SnappingResponse.java b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/SnappingResponse.java new file mode 100644 index 0000000000..25f42ad85d --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/SnappingResponse.java @@ -0,0 +1,12 @@ +package org.heigit.ors.api.responses.snapping; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.heigit.ors.snapping.SnappingResult; +public class SnappingResponse { + @JsonIgnore + private final SnappingResult result; + + public SnappingResponse(SnappingResult result) { + this.result = result; + } +} diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/json/JsonSnappingResponse.java b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/json/JsonSnappingResponse.java new file mode 100644 index 0000000000..eb219d5713 --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/json/JsonSnappingResponse.java @@ -0,0 +1,37 @@ +package org.heigit.ors.api.responses.snapping.json; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.swagger.v3.oas.annotations.media.Schema; +import org.heigit.ors.api.responses.matrix.json.JSON2DSources; +import org.heigit.ors.api.responses.matrix.json.JSONIndividualMatrixResponse; +import org.heigit.ors.api.responses.matrix.json.JSONLocation; +import org.heigit.ors.api.responses.snapping.SnappingResponse; +import org.heigit.ors.matrix.ResolvedLocation; +import org.heigit.ors.snapping.SnappingResult; + +import java.util.ArrayList; +import java.util.List; + +@Schema(name = "SnappingResponse", description = "The Snapping Response contains the snapped coordinates.") +public class JsonSnappingResponse extends SnappingResponse { + @Schema(description = "The snapped locations as coordinates and snapping distance.") + @JsonProperty("locations") + List locations; + public JsonSnappingResponse(SnappingResult result) { + super(result); + locations = constructLocations(result); + } + + private List constructLocations(SnappingResult result) { + List locs = new ArrayList(); + for (ResolvedLocation location: result.getLocations()) { + if (location == null) { + locs.add(null); + } else { + locs.add(new JSON2DSources(location, true)); + } + } + return locs; + } +} diff --git a/ors-api/src/main/java/org/heigit/ors/api/services/SnappingService.java b/ors-api/src/main/java/org/heigit/ors/api/services/SnappingService.java new file mode 100644 index 0000000000..a9ca36b0c3 --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/services/SnappingService.java @@ -0,0 +1,73 @@ +package org.heigit.ors.api.services; + +import com.graphhopper.GraphHopper; +import org.heigit.ors.common.StatusCode; +import org.heigit.ors.exceptions.ParameterValueException; +import org.heigit.ors.exceptions.PointNotFoundException; +import org.heigit.ors.exceptions.StatusCodeException; +import org.heigit.ors.snapping.SnappingErrorCodes; +import org.heigit.ors.routing.RoutingProfile; +import org.heigit.ors.routing.RoutingProfileManager; +import org.heigit.ors.snapping.SnappingRequest; +import org.heigit.ors.api.requests.snapping.SnappingApiRequest; +import org.heigit.ors.snapping.SnappingResult; +import org.locationtech.jts.geom.Coordinate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class SnappingService extends ApiService { + + public SnappingResult generateSnappingFromRequest(SnappingApiRequest snappingApiRequest) throws StatusCodeException { + SnappingRequest snappingRequest = this.convertSnappingRequest(snappingApiRequest); + + try { + RoutingProfileManager rpm = RoutingProfileManager.getInstance(); + RoutingProfile rp = rpm.getProfiles().getRouteProfile(snappingRequest.getProfileType()); + GraphHopper gh = rp.getGraphhopper(); + return snappingRequest.computeResult(gh); + } catch (PointNotFoundException e) { + throw new StatusCodeException(StatusCode.NOT_FOUND, SnappingErrorCodes.POINT_NOT_FOUND, e.getMessage()); + } catch (StatusCodeException e) { + throw e; + } catch (Exception e) { + throw new StatusCodeException(StatusCode.INTERNAL_SERVER_ERROR, SnappingErrorCodes.UNKNOWN); + } + } + + private SnappingRequest convertSnappingRequest(SnappingApiRequest snappingApiRequest) throws StatusCodeException { + int profileType = -1; + try { + profileType = convertRouteProfileType(snappingApiRequest.getProfile()); + } catch (Exception e) { + throw new ParameterValueException(SnappingErrorCodes.INVALID_PARAMETER_VALUE, SnappingApiRequest.PARAM_PROFILE); + } + + SnappingRequest snappingRequest = new SnappingRequest(profileType, + convertLocations(snappingApiRequest.getLocations()), snappingApiRequest.getMaximumSearchRadius()); + + if (snappingApiRequest.hasId()) + snappingRequest.setId(snappingApiRequest.getId()); + return snappingRequest; + + } + + private Coordinate[] convertLocations(List> locations) throws StatusCodeException { + Coordinate[] coordinates = new Coordinate[locations.size()]; + int i = 0; // apparently stream().map() does not work with exceptions + for (var location: locations) { + coordinates[i] = convertLocation(location); + i++; + } + return coordinates; + } + + private static Coordinate convertLocation(List location) throws StatusCodeException { + if (location.size() != 2) { + throw new ParameterValueException(SnappingErrorCodes.INVALID_PARAMETER_VALUE, SnappingApiRequest.PARAM_LOCATIONS); + } + return new Coordinate(location.get(0), location.get(1)); + } +} diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java new file mode 100644 index 0000000000..a48d738ad4 --- /dev/null +++ b/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java @@ -0,0 +1,48 @@ +package org.heigit.ors.apitests.snapping; + +import org.heigit.ors.apitests.common.EndPointAnnotation; +import org.heigit.ors.apitests.common.ServiceTest; +import org.heigit.ors.apitests.common.VersionAnnotation; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; +import static org.heigit.ors.apitests.utils.CommonHeaders.jsonContent; + +@EndPointAnnotation(name = "snap") +@VersionAnnotation(version = "v2") +class ParamsTest extends ServiceTest { + + public ParamsTest() { + JSONArray coordsShort = new JSONArray(); + JSONArray coord1 = new JSONArray(); + coord1.put(8.680916); + coord1.put(49.410973); + coordsShort.put(coord1); + JSONArray coord2 = new JSONArray(); + coord2.put(8.687782); + coord2.put(49.424597); + coordsShort.put(coord2); + addParameter("coordinates", coordsShort); + } + @Test + void basicTest () { + JSONObject body = new JSONObject(); + body.put("locations", getParameter("coordinates")); + body.put("maximum_search_radius", "300"); + given() + .headers(jsonContent) + .pathParam("profile", "driving-car") + .body(body.toString()) + .when() + .log().ifValidationFails() + .post(getEndPointPath() + "/{profile}/json") + .then() + .log().ifValidationFails() + .body("any { it.key == 'locations' }", is(true)) + .statusCode(200); + + } +} diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/APIEnums.java b/ors-engine/src/main/java/org/heigit/ors/routing/APIEnums.java index 15063c6279..d3860f43b4 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/APIEnums.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/APIEnums.java @@ -172,6 +172,32 @@ public String toString() { } } + @Schema(name = "Snapping response type", description = "Format of the snapping response.") + public enum SnappingResponseType { + JSON("json"); + + private final String value; + + SnappingResponseType(String value) { + this.value = value; + } + + @JsonCreator + public static SnappingResponseType forValue(String v) throws ParameterValueException { + for (SnappingResponseType enumItem : SnappingResponseType.values()) { + if (enumItem.value.equals(v)) + return enumItem; + } + throw new ParameterValueException(GenericErrorCodes.INVALID_PARAMETER_VALUE, "format", v); + } + + @Override + @JsonValue + public String toString() { + return value; + } + } + @Schema(name = "Vehicle type", description = "Definition of the vehicle type.") public enum VehicleType { HGV("hgv"), 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 3617dbc27f..eed5e6857e 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 @@ -14,10 +14,7 @@ package org.heigit.ors.routing; 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.*; import com.graphhopper.util.exceptions.ConnectionNotFoundException; import com.graphhopper.util.exceptions.MaximumNodesExceededException; import org.apache.log4j.Logger; @@ -29,16 +26,11 @@ import org.heigit.ors.isochrones.IsochroneMap; import org.heigit.ors.isochrones.IsochroneSearchParameters; import org.heigit.ors.mapmatching.MapMatchingRequest; -import org.heigit.ors.matrix.MatrixErrorCodes; -import org.heigit.ors.matrix.MatrixRequest; -import org.heigit.ors.matrix.MatrixResult; +import org.heigit.ors.matrix.*; import org.heigit.ors.routing.configuration.RouteProfileConfiguration; import org.heigit.ors.routing.configuration.RoutingManagerConfiguration; import org.heigit.ors.routing.pathprocessors.ExtraInfoProcessor; -import org.heigit.ors.util.FormatUtility; -import org.heigit.ors.util.RuntimeUtility; -import org.heigit.ors.util.StringUtility; -import org.heigit.ors.util.TimeUtility; +import org.heigit.ors.util.*; import org.locationtech.jts.geom.Coordinate; import java.util.ArrayList; @@ -614,4 +606,5 @@ public ExportResult computeExport(ExportRequest req) throws Exception { throw new InternalServerException(ExportErrorCodes.UNKNOWN, "Unable to find an appropriate routing profile."); return rp.computeExport(req); } + } diff --git a/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingErrorCodes.java b/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingErrorCodes.java new file mode 100644 index 0000000000..a620ab4450 --- /dev/null +++ b/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingErrorCodes.java @@ -0,0 +1,18 @@ +package org.heigit.ors.snapping; + +public class SnappingErrorCodes { + public static final int BASE = 8000; + public static final int INVALID_JSON_FORMAT = 8000; + public static final int MISSING_PARAMETER = 8001; + public static final int INVALID_PARAMETER_FORMAT = 8002; + public static final int INVALID_PARAMETER_VALUE = 8003; + public static final int UNKNOWN_PARAMETER = 8004; + public static final int UNSUPPORTED_EXPORT_FORMAT = 8006; + + public static final int POINT_NOT_FOUND = 8010; + public static final int UNKNOWN = 8099; + + private SnappingErrorCodes() { + } + +} diff --git a/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java b/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java new file mode 100644 index 0000000000..38766fbcbb --- /dev/null +++ b/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java @@ -0,0 +1,49 @@ +package org.heigit.ors.snapping; + +import com.graphhopper.GraphHopper; +import com.graphhopper.routing.util.AccessFilter; +import com.graphhopper.routing.util.FlagEncoder; +import com.graphhopper.storage.RoutingCHGraph; +import com.graphhopper.util.PMap; +import org.heigit.ors.common.ServiceRequest; +import org.heigit.ors.matrix.MatrixSearchContext; +import org.heigit.ors.matrix.MatrixSearchContextBuilder; +import org.heigit.ors.routing.RoutingProfileType; +import org.heigit.ors.routing.WeightingMethod; +import org.heigit.ors.util.ProfileTools; +import org.locationtech.jts.geom.Coordinate; + +public class SnappingRequest extends ServiceRequest { + private final int profileType; + private final Coordinate[] locations; + private final double maximumSearchRadius; + + public SnappingRequest(int profileType, Coordinate[] locations, double maximumSearchRadius) { + this.profileType = profileType; + this.locations = locations; + this.maximumSearchRadius = maximumSearchRadius; + } + + public SnappingResult computeResult(GraphHopper gh) throws Exception { + String encoderName = RoutingProfileType.getEncoderName(profileType); + FlagEncoder flagEncoder = gh.getEncodingManager().getEncoder(encoderName); + PMap hintsMap = new PMap(); + int weightingMethod = WeightingMethod.RECOMMENDED; // Only needed to create the profile string + ProfileTools.setWeightingMethod(hintsMap, weightingMethod, profileType, false); + ProfileTools.setWeighting(hintsMap, weightingMethod, profileType, false); + String CHProfileName = ProfileTools.makeProfileName(encoderName, hintsMap.getString("weighting", ""), false); + String profileName = CHProfileName; + + // TODO: replace usage of matrix search context by snapping-specific class + RoutingCHGraph routingCHGraph = gh.getGraphHopperStorage().getRoutingCHGraph(profileName); + MatrixSearchContextBuilder builder = new MatrixSearchContextBuilder(gh.getGraphHopperStorage(), gh.getLocationIndex(), AccessFilter.allEdges(flagEncoder.getAccessEnc()), true); + MatrixSearchContext mtxSearchCntx = builder.create(routingCHGraph.getBaseGraph(), routingCHGraph, routingCHGraph.getWeighting(), profileName, locations, locations, maximumSearchRadius); + SnappingResult mtxResult = new SnappingResult(mtxSearchCntx.getSources().getLocations()); + return mtxResult; + } + + public int getProfileType() { + return profileType; + } + +} diff --git a/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingResult.java b/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingResult.java new file mode 100644 index 0000000000..86d4dd8ca0 --- /dev/null +++ b/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingResult.java @@ -0,0 +1,15 @@ +package org.heigit.ors.snapping; + +import org.heigit.ors.matrix.ResolvedLocation; + +public class SnappingResult { + private final ResolvedLocation[] locations; + + public SnappingResult(ResolvedLocation[] locations) { + this.locations = locations; + } + + public ResolvedLocation[] getLocations() { + return locations; + } +} From 577ad23e7a92fb01404527e116370ce540701da2 Mon Sep 17 00:00:00 2001 From: Sascha Fendrich Date: Thu, 10 Aug 2023 11:03:54 +0200 Subject: [PATCH 02/18] docs: add changelog entry for snapping service --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d00b0640b..39d622a649 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ RELEASING: ## [Unreleased] ### Added +- snapping service endpoints for returning nearest points on the graph ([#1519](https://github.com/GIScience/openrouteservice/issues/1519)) - workflow for RPM packaging ([#1490](https://github.com/GIScience/openrouteservice/pull/1490)) - workflow for graph building with GitHub environments ([#1468](https://github.com/GIScience/openrouteservice/pull/1468)) - environment variables for adjusting folders and paths during graph build using docker: ([#1468](https://github.com/GIScience/openrouteservice/pull/1468)) From 3589e462736f72dfa7aff18af8690188b9a847c1 Mon Sep 17 00:00:00 2001 From: Amandus Butzer Date: Mon, 2 Oct 2023 14:07:14 +0200 Subject: [PATCH 03/18] refactor: extract EngineInfo class to common EngineInfo was a subclass of: - IsochronesResponseInfo - MatrixResponseInfo - RouteResponseInfo It is now a common class for all responses --- .../common/engineinfo/EngineInfo.java | 36 +++++++++++++++++++ .../isochrones/IsochronesResponseInfo.java | 32 +---------------- .../responses/matrix/MatrixResponseInfo.java | 33 +---------------- .../responses/routing/RouteResponseInfo.java | 32 +---------------- 4 files changed, 39 insertions(+), 94 deletions(-) create mode 100644 ors-api/src/main/java/org/heigit/ors/api/responses/common/engineinfo/EngineInfo.java diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/common/engineinfo/EngineInfo.java b/ors-api/src/main/java/org/heigit/ors/api/responses/common/engineinfo/EngineInfo.java new file mode 100644 index 0000000000..27c1ad7be9 --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/common/engineinfo/EngineInfo.java @@ -0,0 +1,36 @@ +package org.heigit.ors.api.responses.common.engineinfo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import org.json.JSONObject; + +@Schema(description = "Information about the openrouteservice engine used") +public class EngineInfo { + @Schema(description = "The backend version of the openrouteservice that was queried", example = "8.0") + @JsonProperty("version") + private final String version; + @Schema(description = "The date that the service was last updated", example = "2019-02-07T14:28:11Z") + @JsonProperty("build_date") + private final String buildDate; + @Schema(description = "The date that the graph data was last updated", example = "2019-02-07T14:28:11Z") + @JsonProperty("graph_date") + private String graphDate; + + public EngineInfo(JSONObject infoIn) { + version = infoIn.getString("version"); + buildDate = infoIn.getString("build_date"); + graphDate = "0000-00-00T00:00:00Z"; + } + + public String getVersion() { + return version; + } + + public String getBuildDate() { + return buildDate; + } + + public void setGraphDate(String graphDate) { + this.graphDate = graphDate; + } +} diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/isochrones/IsochronesResponseInfo.java b/ors-api/src/main/java/org/heigit/ors/api/responses/isochrones/IsochronesResponseInfo.java index 01c5b21130..e655615a46 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/responses/isochrones/IsochronesResponseInfo.java +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/isochrones/IsochronesResponseInfo.java @@ -23,10 +23,10 @@ import org.heigit.ors.api.EndpointsProperties; import org.heigit.ors.api.SystemMessageProperties; import org.heigit.ors.api.requests.isochrones.IsochronesRequest; +import org.heigit.ors.api.responses.common.engineinfo.EngineInfo; import org.heigit.ors.api.util.AppInfo; import org.heigit.ors.api.util.SystemMessage; import org.heigit.ors.config.AppConfig; -import org.json.JSONObject; @Schema(name = "IsochronesResponseInfo", description = "Information about the request") @JsonInclude(JsonInclude.Include.NON_DEFAULT) @@ -85,34 +85,4 @@ public void setGraphDate(String graphDate) { engineInfo.setGraphDate(graphDate); } - @Schema(description = "Information about the version of the openrouteservice that was used to generate the isochrones") - private static class EngineInfo { - @Schema(description = "The backend version of the openrouteservice that was queried", example = "5.0") - @JsonProperty("version") - private final String version; - @Schema(description = "The date that the service was last updated", example = "2019-02-07T14:28:11Z") - @JsonProperty("build_date") - private final String buildDate; - @Schema(description = "The date that the graph data was last updated", example = "2019-02-07T14:28:11Z") - @JsonProperty("graph_date") - private String graphDate; - - public EngineInfo(JSONObject infoIn) { - version = infoIn.getString("version"); - buildDate = infoIn.getString("build_date"); - graphDate = "0000-00-00T00:00:00Z"; - } - - public String getVersion() { - return version; - } - - public String getBuildDate() { - return buildDate; - } - - public void setGraphDate(String graphDate) { - this.graphDate = graphDate; - } - } } diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/matrix/MatrixResponseInfo.java b/ors-api/src/main/java/org/heigit/ors/api/responses/matrix/MatrixResponseInfo.java index 0ff7b2381e..5452b4bb10 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/responses/matrix/MatrixResponseInfo.java +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/matrix/MatrixResponseInfo.java @@ -23,10 +23,10 @@ import org.heigit.ors.api.EndpointsProperties; import org.heigit.ors.api.SystemMessageProperties; import org.heigit.ors.api.requests.matrix.MatrixRequest; +import org.heigit.ors.api.responses.common.engineinfo.EngineInfo; import org.heigit.ors.api.util.AppInfo; import org.heigit.ors.api.util.SystemMessage; import org.heigit.ors.config.AppConfig; -import org.json.JSONObject; @Schema(description = "Information about the request") @JsonInclude(JsonInclude.Include.NON_DEFAULT) @@ -85,37 +85,6 @@ public void setGraphDate(String graphDate) { engineInfo.setGraphDate(graphDate); } - @Schema(description = "Information about the version of the openrouteservice that was used to generate the matrix") - private static class EngineInfo { - @Schema(description = "The backend version of the openrouteservice that was queried", example = "5.0") - @JsonProperty("version") - private final String version; - @Schema(description = "The date that the service was last updated", example = "2019-02-07T14:28:11Z") - @JsonProperty("build_date") - private final String buildDate; - @Schema(description = "The date that the graph data was last updated", example = "2019-02-07T14:28:11Z") - @JsonProperty("graph_date") - private String graphDate; - - public EngineInfo(JSONObject infoIn) { - version = infoIn.getString("version"); - buildDate = infoIn.getString("build_date"); - graphDate = "0000-00-00T00:00:00Z"; - } - - public String getVersion() { - return version; - } - - public String getBuildDate() { - return buildDate; - } - - public void setGraphDate(String graphDate) { - this.graphDate = graphDate; - } - } - public String getAttribution() { return attribution; } diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/routing/RouteResponseInfo.java b/ors-api/src/main/java/org/heigit/ors/api/responses/routing/RouteResponseInfo.java index 50c51bc123..758b340d80 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/responses/routing/RouteResponseInfo.java +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/routing/RouteResponseInfo.java @@ -23,10 +23,10 @@ import org.heigit.ors.api.EndpointsProperties; import org.heigit.ors.api.SystemMessageProperties; import org.heigit.ors.api.requests.routing.RouteRequest; +import org.heigit.ors.api.responses.common.engineinfo.EngineInfo; import org.heigit.ors.api.util.AppInfo; import org.heigit.ors.api.util.SystemMessage; import org.heigit.ors.config.AppConfig; -import org.json.JSONObject; @Schema(description = "Information about the request") @JsonInclude(JsonInclude.Include.NON_DEFAULT) @@ -85,34 +85,4 @@ public void setGraphDate(String graphDate) { engineInfo.setGraphDate(graphDate); } - @Schema(description = "Information about the version of the openrouteservice that was used to generate the route") - private static class EngineInfo { - @Schema(description = "The backend version of the openrouteservice that was queried", example = "5.0") - @JsonProperty("version") - private final String version; - @Schema(description = "The date that the service was last updated", example = "2019-02-07T14:28:11Z") - @JsonProperty("build_date") - private final String buildDate; - @Schema(description = "The date that the graph data was last updated", example = "2019-02-07T14:28:11Z") - @JsonProperty("graph_date") - private String graphDate; - - public EngineInfo(JSONObject infoIn) { - version = infoIn.getString("version"); - buildDate = infoIn.getString("build_date"); - graphDate = "0000-00-00T00:00:00Z"; - } - - public void setGraphDate(String graphDate) { - this.graphDate = graphDate; - } - - public String getVersion() { - return version; - } - - public String getBuildDate() { - return buildDate; - } - } } From 57dd325aac2cdc25a65cb7afca49502d7a65e326 Mon Sep 17 00:00:00 2001 From: Amandus Butzer Date: Mon, 2 Oct 2023 15:48:21 +0200 Subject: [PATCH 04/18] feat: expose graphDate in SnappingResult --- .../main/java/org/heigit/ors/snapping/SnappingRequest.java | 4 ++-- .../main/java/org/heigit/ors/snapping/SnappingResult.java | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java b/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java index 38766fbcbb..53b6419b3b 100644 --- a/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java +++ b/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java @@ -33,13 +33,13 @@ public SnappingResult computeResult(GraphHopper gh) throws Exception { ProfileTools.setWeighting(hintsMap, weightingMethod, profileType, false); String CHProfileName = ProfileTools.makeProfileName(encoderName, hintsMap.getString("weighting", ""), false); String profileName = CHProfileName; + String graphDate = gh.getGraphHopperStorage().getProperties().get("datareader.import.date"); // TODO: replace usage of matrix search context by snapping-specific class RoutingCHGraph routingCHGraph = gh.getGraphHopperStorage().getRoutingCHGraph(profileName); MatrixSearchContextBuilder builder = new MatrixSearchContextBuilder(gh.getGraphHopperStorage(), gh.getLocationIndex(), AccessFilter.allEdges(flagEncoder.getAccessEnc()), true); MatrixSearchContext mtxSearchCntx = builder.create(routingCHGraph.getBaseGraph(), routingCHGraph, routingCHGraph.getWeighting(), profileName, locations, locations, maximumSearchRadius); - SnappingResult mtxResult = new SnappingResult(mtxSearchCntx.getSources().getLocations()); - return mtxResult; + return new SnappingResult(mtxSearchCntx.getSources().getLocations(), graphDate); } public int getProfileType() { diff --git a/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingResult.java b/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingResult.java index 86d4dd8ca0..52c9af14d6 100644 --- a/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingResult.java +++ b/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingResult.java @@ -4,12 +4,15 @@ public class SnappingResult { private final ResolvedLocation[] locations; - - public SnappingResult(ResolvedLocation[] locations) { + private String graphDate = ""; + public SnappingResult(ResolvedLocation[] locations, String graphDate) { this.locations = locations; + this.graphDate = graphDate; } public ResolvedLocation[] getLocations() { return locations; } + + public String getGraphDate() { return graphDate; } } From bedffeeede8444311e129d9b0e4ae767b28103eb Mon Sep 17 00:00:00 2001 From: Amandus Butzer Date: Mon, 2 Oct 2023 15:57:01 +0200 Subject: [PATCH 05/18] feat: extend JsonSnappingResponse with metadata object as in our other services, the snapping endpoint is now also returning the metadata object containing info about the service and request made. - add SnappingResponseInfo class - extend SnappingAPI class with endpointsProperties and systemMessageProperties needed for generic response metadata --- .../ors/api/controllers/SnappingAPI.java | 11 +++- .../snapping/SnappingResponseInfo.java | 66 +++++++++++++++++++ .../snapping/json/JsonSnappingResponse.java | 14 +++- 3 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 ors-api/src/main/java/org/heigit/ors/api/responses/snapping/SnappingResponseInfo.java diff --git a/ors-api/src/main/java/org/heigit/ors/api/controllers/SnappingAPI.java b/ors-api/src/main/java/org/heigit/ors/api/controllers/SnappingAPI.java index 308bdaff3c..a13e4ab46b 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/controllers/SnappingAPI.java +++ b/ors-api/src/main/java/org/heigit/ors/api/controllers/SnappingAPI.java @@ -26,10 +26,13 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; +import org.heigit.ors.api.EndpointsProperties; +import org.heigit.ors.api.SystemMessageProperties; import org.heigit.ors.api.errors.CommonResponseEntityExceptionHandler; import org.heigit.ors.api.requests.snapping.SnappingApiRequest; import org.heigit.ors.api.responses.snapping.json.JsonSnappingResponse; import org.heigit.ors.api.services.SnappingService; +import org.heigit.ors.api.util.AppConfigMigration; import org.heigit.ors.exceptions.*; import org.heigit.ors.routing.APIEnums; import org.heigit.ors.snapping.SnappingErrorCodes; @@ -54,9 +57,13 @@ public class SnappingAPI { static final CommonResponseEntityExceptionHandler errorHandler = new CommonResponseEntityExceptionHandler(SnappingErrorCodes.BASE); + private final EndpointsProperties endpointsProperties; + private final SystemMessageProperties systemMessageProperties; private final SnappingService snappingService; - public SnappingAPI(SnappingService snappingService) { + public SnappingAPI(EndpointsProperties endpointsProperties, SystemMessageProperties systemMessageProperties, SnappingService snappingService) { + this.endpointsProperties = AppConfigMigration.overrideEndpointsProperties(endpointsProperties); + this.systemMessageProperties = systemMessageProperties; this.snappingService = snappingService; } @@ -126,7 +133,7 @@ public JsonSnappingResponse getJsonSnapping( SnappingResult result = snappingService.generateSnappingFromRequest(request); - return new JsonSnappingResponse(result); + return new JsonSnappingResponse(result, request, systemMessageProperties, endpointsProperties); } @ExceptionHandler(MissingServletRequestParameterException.class) diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/SnappingResponseInfo.java b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/SnappingResponseInfo.java new file mode 100644 index 0000000000..70ef133878 --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/SnappingResponseInfo.java @@ -0,0 +1,66 @@ +package org.heigit.ors.api.responses.snapping; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.graphhopper.util.Helper; +import io.swagger.v3.oas.annotations.media.Schema; +import org.heigit.ors.api.EndpointsProperties; +import org.heigit.ors.api.SystemMessageProperties; +import org.heigit.ors.api.requests.snapping.SnappingApiRequest; +import org.heigit.ors.api.responses.common.engineinfo.EngineInfo; +import org.heigit.ors.api.util.AppInfo; +import org.heigit.ors.api.util.SystemMessage; +import org.heigit.ors.config.AppConfig; + +@Schema(description = "Information about the request") +@JsonInclude(JsonInclude.Include.NON_DEFAULT) +public class SnappingResponseInfo { + @Schema(description = "Copyright and attribution information", example = "openrouteservice.org | OpenStreetMap contributors") + @JsonProperty("attribution") + private String attribution; + @Schema(description = "The MD5 hash of the OSM planet file that was used for generating graphs", example = "c0327ba6") + @JsonProperty("osm_file_md5_hash") + private String osmFileMD5Hash; + @Schema(description = "The service that was requested", example = "snap") + @JsonProperty("service") + private final String service; + @Schema(description = "Time that the request was made (UNIX Epoch time)", example = "1549549847974") + @JsonProperty("timestamp") + private final long timeStamp; + + @Schema(description = "The information that was used for generating the request") + @JsonProperty("query") + private final SnappingApiRequest request; + + @Schema(description = "Information about the snapping service") + @JsonProperty("engine") + private final EngineInfo engineInfo; + + @Schema(description = "System message", example = "A message string configured in the service") + @JsonProperty("system_message") + private final String systemMessage; + + public SnappingResponseInfo(SnappingApiRequest request, SystemMessageProperties systemMessageProperties, EndpointsProperties endpointsProperties) { + service = "snap"; + timeStamp = System.currentTimeMillis(); + + if (AppConfig.hasValidMD5Hash()) + osmFileMD5Hash = AppConfig.getMD5Hash(); + + if (!Helper.isEmpty(endpointsProperties.getMatrix().getAttribution())) + attribution = endpointsProperties.getMatrix().getAttribution(); + + engineInfo = new EngineInfo(AppInfo.getEngineInfo()); + + this.request = request; + + this.systemMessage = SystemMessage.getSystemMessage(request, systemMessageProperties); + } + + @JsonIgnore + public void setGraphDate(String graphDate) { + engineInfo.setGraphDate(graphDate); + } + +} diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/json/JsonSnappingResponse.java b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/json/JsonSnappingResponse.java index eb219d5713..24625cb492 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/json/JsonSnappingResponse.java +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/json/JsonSnappingResponse.java @@ -1,12 +1,14 @@ package org.heigit.ors.api.responses.snapping.json; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonUnwrapped; import io.swagger.v3.oas.annotations.media.Schema; +import org.heigit.ors.api.EndpointsProperties; +import org.heigit.ors.api.SystemMessageProperties; +import org.heigit.ors.api.requests.snapping.SnappingApiRequest; import org.heigit.ors.api.responses.matrix.json.JSON2DSources; -import org.heigit.ors.api.responses.matrix.json.JSONIndividualMatrixResponse; import org.heigit.ors.api.responses.matrix.json.JSONLocation; import org.heigit.ors.api.responses.snapping.SnappingResponse; +import org.heigit.ors.api.responses.snapping.SnappingResponseInfo; import org.heigit.ors.matrix.ResolvedLocation; import org.heigit.ors.snapping.SnappingResult; @@ -18,9 +20,15 @@ public class JsonSnappingResponse extends SnappingResponse { @Schema(description = "The snapped locations as coordinates and snapping distance.") @JsonProperty("locations") List locations; - public JsonSnappingResponse(SnappingResult result) { + + @JsonProperty("metadata") + @Schema(description = "Information about the service and request") + SnappingResponseInfo responseInformation; + public JsonSnappingResponse(SnappingResult result, SnappingApiRequest request, SystemMessageProperties systemMessageProperties, EndpointsProperties endpointsProperties) { super(result); locations = constructLocations(result); + responseInformation = new SnappingResponseInfo(request, systemMessageProperties, endpointsProperties); + responseInformation.setGraphDate(result.getGraphDate()); } private List constructLocations(SnappingResult result) { From d5a5d45c353ae7a72bf59cb540b0d30cd622ab25 Mon Sep 17 00:00:00 2001 From: Julian Psotta Date: Tue, 3 Oct 2023 00:47:24 +0200 Subject: [PATCH 06/18] feat(status): Add snap service to config and status page --- .../heigit/ors/api/EndpointsProperties.java | 32 ++++++++++++++++++- .../heigit/ors/api/controllers/StatusAPI.java | 2 ++ .../snapping/SnappingResponseInfo.java | 4 +-- .../ors/api/util/AppConfigMigration.java | 9 ++++++ ors-api/src/main/resources/application.yml | 3 ++ 5 files changed, 47 insertions(+), 3 deletions(-) diff --git a/ors-api/src/main/java/org/heigit/ors/api/EndpointsProperties.java b/ors-api/src/main/java/org/heigit/ors/api/EndpointsProperties.java index f8e69c54b3..973bea7c20 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/EndpointsProperties.java +++ b/ors-api/src/main/java/org/heigit/ors/api/EndpointsProperties.java @@ -17,7 +17,7 @@ public class EndpointsProperties { private EndpointRoutingProperties routing; private EndpointMatrixProperties matrix; private EndpointIsochroneProperties isochrone; - + private EndpointSnapProperties snap; private String swaggerDocumentationUrl; public void setSwaggerDocumentationUrl(String swaggerDocumentationUrl) { @@ -60,6 +60,14 @@ public void setIsochrone(EndpointIsochroneProperties isochrone) { this.isochrone = isochrone; } + public EndpointSnapProperties getSnap() { + return snap; + } + + public void setSnap(EndpointSnapProperties snap) { + this.snap = snap; + } + public static class EndpointDefaultProperties { private String attribution; @@ -433,4 +441,26 @@ public void setAttribution(String attribution) { } } } + + public static class EndpointSnapProperties { + private boolean enabled; + private String attribution; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getAttribution() { + return attribution; + } + + public void setAttribution(String attribution) { + this.attribution = attribution; + } + + } } 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 f4287f8ffc..5ce7c9268b 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 @@ -71,6 +71,8 @@ public ResponseEntity fetchHealth(HttpServletRequest request) throws Exception { list.add("isochrones"); if (endpointsProperties.getMatrix().isEnabled()) list.add("matrix"); + if (endpointsProperties.getSnap().isEnabled()) + list.add("snap"); jInfo.put("services", list); jInfo.put("languages", LocalizationManager.getInstance().getLanguages()); diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/SnappingResponseInfo.java b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/SnappingResponseInfo.java index 70ef133878..73ae526361 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/SnappingResponseInfo.java +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/SnappingResponseInfo.java @@ -48,8 +48,8 @@ public SnappingResponseInfo(SnappingApiRequest request, SystemMessageProperties if (AppConfig.hasValidMD5Hash()) osmFileMD5Hash = AppConfig.getMD5Hash(); - if (!Helper.isEmpty(endpointsProperties.getMatrix().getAttribution())) - attribution = endpointsProperties.getMatrix().getAttribution(); + if (!Helper.isEmpty(endpointsProperties.getSnap().getAttribution())) + attribution = endpointsProperties.getSnap().getAttribution(); engineInfo = new EngineInfo(AppInfo.getEngineInfo()); diff --git a/ors-api/src/main/java/org/heigit/ors/api/util/AppConfigMigration.java b/ors-api/src/main/java/org/heigit/ors/api/util/AppConfigMigration.java index 2b0fafaedb..7841b05ef2 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/util/AppConfigMigration.java +++ b/ors-api/src/main/java/org/heigit/ors/api/util/AppConfigMigration.java @@ -165,6 +165,15 @@ public static EndpointsProperties overrideEndpointsProperties(EndpointsPropertie } isochrones.getStatisticsProviders().putAll(statisticsProviderPropertiesMap); +// ### Snap ### + EndpointsProperties.EndpointSnapProperties snap = endpoints.getSnap(); + value = config.getServiceParameter("snap", "enabled"); + if (value != null) + snap.setEnabled(Boolean.parseBoolean(value)); + value = config.getServiceParameter("snap", "attribution"); + if (value != null) + snap.setAttribution(value); + // ### Matrix ### EndpointsProperties.EndpointMatrixProperties matrix = endpoints.getMatrix(); value = config.getServiceParameter(SERVICE_NAME_MATRIX, "enabled"); diff --git a/ors-api/src/main/resources/application.yml b/ors-api/src/main/resources/application.yml index cf87085ca0..9f74896395 100644 --- a/ors-api/src/main/resources/application.yml +++ b/ors-api/src/main/resources/application.yml @@ -96,6 +96,9 @@ ors: maximum_range_time: - profiles: driving-car, driving-hgv value: 10800 + Snap: + enabled: true + attribution: openrouteservice.org, OpenStreetMap contributors ##### ORS engine settings ##### engine: From 8252cd5d25aa7079a52a001efccbd4e430c0dca0 Mon Sep 17 00:00:00 2001 From: Amandus Butzer Date: Fri, 6 Oct 2023 14:34:12 +0200 Subject: [PATCH 07/18] docs: add info for snap service to config docs --- docs/installation/Configuration.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/installation/Configuration.md b/docs/installation/Configuration.md index 09e82c669f..89a9327cc3 100644 --- a/docs/installation/Configuration.md +++ b/docs/installation/Configuration.md @@ -74,7 +74,7 @@ The messages property expects a list of elements where each has the following: | time_after | ISO 8601 datetime string | message sent if server local time is after given point in time | | api_version | 1 or 2 | message sent if API version requested through matches value | | api_format | String with output formats ("json", "geojson", "gpx"), comma separated | message sent if requested output format matches value | -| request_service | String with service names ("routing", "matrix", "isochrones"), comma separated | message sent if requested service matches one of the given names | +| request_service | String with service names ("routing", "matrix", "isochrones", "snap"), comma separated | message sent if requested service matches one of the given names | | request_profile | String with profile names, comma separated | message sent if requested profile matches one of the given names | | request_preference | String with preference (weightings for routing, metrics for matrix, rangetype for isochrones) names, comma separated | message sent if requested preference matches one of the given names | @@ -127,6 +127,7 @@ The top level element. | ors.services.routing | object | settings for routing and its profiles | [routing](#orsservicesrouting) | | ors.services.isochrones | object | settings for isochrones restrictions | [isochrones](#orsservicesisochrones) | | ors.services.matrix | object | settings for matrix restrictions | [matrix](#orsservicesmatrix) | +| ors.services.snap | object | settings for snap | [matrix](#orsservicesmatrix) | --- @@ -360,6 +361,14 @@ The top level element. | allow_resolve_locations | number | Specifies whether the name of a nearest street to the location can be resolved or not. Default value is true | `true` | | attribution | string | Specifies whether the name of a nearest street to the location can be resolved or not. Default value is true | `"openrouteservice.org, OpenStreetMap contributors"` | +--- +#### ors.services.snap + +| key | type | description | example value | +|-------------------------|---------|----------------------------------------------------------------|------------------------------------------------------| +| enabled | boolean | Enables or disables (true/false) the end-point (default: true) | `true` | +| attribution | string | Attribution added to the response metadata | `"openrouteservice.org, OpenStreetMap contributors"` | + --- #### ors.logging From d77eac9a1ac7f7bacd44602dbac100e1fbde50ad Mon Sep 17 00:00:00 2001 From: aoles Date: Wed, 4 Oct 2023 13:06:49 +0200 Subject: [PATCH 08/18] refactor(snapping): remove dependency on CH graph --- .../org/heigit/ors/snapping/SnappingRequest.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java b/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java index 53b6419b3b..f4af183eb3 100644 --- a/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java +++ b/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java @@ -3,13 +3,15 @@ import com.graphhopper.GraphHopper; import com.graphhopper.routing.util.AccessFilter; import com.graphhopper.routing.util.FlagEncoder; -import com.graphhopper.storage.RoutingCHGraph; +import com.graphhopper.routing.weighting.Weighting; +import com.graphhopper.storage.GraphHopperStorage; import com.graphhopper.util.PMap; import org.heigit.ors.common.ServiceRequest; import org.heigit.ors.matrix.MatrixSearchContext; import org.heigit.ors.matrix.MatrixSearchContextBuilder; import org.heigit.ors.routing.RoutingProfileType; import org.heigit.ors.routing.WeightingMethod; +import org.heigit.ors.routing.graphhopper.extensions.ORSWeightingFactory; import org.heigit.ors.util.ProfileTools; import org.locationtech.jts.geom.Coordinate; @@ -31,14 +33,14 @@ public SnappingResult computeResult(GraphHopper gh) throws Exception { int weightingMethod = WeightingMethod.RECOMMENDED; // Only needed to create the profile string ProfileTools.setWeightingMethod(hintsMap, weightingMethod, profileType, false); ProfileTools.setWeighting(hintsMap, weightingMethod, profileType, false); - String CHProfileName = ProfileTools.makeProfileName(encoderName, hintsMap.getString("weighting", ""), false); - String profileName = CHProfileName; - String graphDate = gh.getGraphHopperStorage().getProperties().get("datareader.import.date"); + String profileName = ProfileTools.makeProfileName(encoderName, hintsMap.getString("weighting", ""), false); + GraphHopperStorage ghStorage = gh.getGraphHopperStorage(); + String graphDate = ghStorage.getProperties().get("datareader.import.date"); // TODO: replace usage of matrix search context by snapping-specific class - RoutingCHGraph routingCHGraph = gh.getGraphHopperStorage().getRoutingCHGraph(profileName); - MatrixSearchContextBuilder builder = new MatrixSearchContextBuilder(gh.getGraphHopperStorage(), gh.getLocationIndex(), AccessFilter.allEdges(flagEncoder.getAccessEnc()), true); - MatrixSearchContext mtxSearchCntx = builder.create(routingCHGraph.getBaseGraph(), routingCHGraph, routingCHGraph.getWeighting(), profileName, locations, locations, maximumSearchRadius); + MatrixSearchContextBuilder builder = new MatrixSearchContextBuilder(ghStorage, gh.getLocationIndex(), AccessFilter.allEdges(flagEncoder.getAccessEnc()), true); + Weighting weighting = new ORSWeightingFactory(ghStorage, gh.getEncodingManager()).createWeighting(gh.getProfile(profileName), hintsMap, false); + MatrixSearchContext mtxSearchCntx = builder.create(ghStorage.getBaseGraph(), null, weighting, profileName, locations, locations, maximumSearchRadius); return new SnappingResult(mtxSearchCntx.getSources().getLocations(), graphDate); } From e6a6a6c42120ef40b74e13bc7950e4870fbea1ec Mon Sep 17 00:00:00 2001 From: Jochen Haeussler Date: Wed, 4 Oct 2023 14:46:27 +0200 Subject: [PATCH 09/18] refactor(snapping): moved computing method to the service class --- .../ors/api/services/SnappingService.java | 36 ++++++++++++++++-- .../heigit/ors/snapping/SnappingRequest.java | 37 ++++--------------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/ors-api/src/main/java/org/heigit/ors/api/services/SnappingService.java b/ors-api/src/main/java/org/heigit/ors/api/services/SnappingService.java index a9ca36b0c3..79db38c827 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/services/SnappingService.java +++ b/ors-api/src/main/java/org/heigit/ors/api/services/SnappingService.java @@ -1,18 +1,28 @@ package org.heigit.ors.api.services; import com.graphhopper.GraphHopper; +import com.graphhopper.routing.util.AccessFilter; +import com.graphhopper.routing.util.FlagEncoder; +import com.graphhopper.routing.weighting.Weighting; +import com.graphhopper.storage.GraphHopperStorage; +import com.graphhopper.util.PMap; +import org.heigit.ors.api.requests.snapping.SnappingApiRequest; import org.heigit.ors.common.StatusCode; import org.heigit.ors.exceptions.ParameterValueException; import org.heigit.ors.exceptions.PointNotFoundException; import org.heigit.ors.exceptions.StatusCodeException; -import org.heigit.ors.snapping.SnappingErrorCodes; +import org.heigit.ors.matrix.MatrixSearchContext; +import org.heigit.ors.matrix.MatrixSearchContextBuilder; import org.heigit.ors.routing.RoutingProfile; import org.heigit.ors.routing.RoutingProfileManager; +import org.heigit.ors.routing.RoutingProfileType; +import org.heigit.ors.routing.WeightingMethod; +import org.heigit.ors.routing.graphhopper.extensions.ORSWeightingFactory; +import org.heigit.ors.snapping.SnappingErrorCodes; import org.heigit.ors.snapping.SnappingRequest; -import org.heigit.ors.api.requests.snapping.SnappingApiRequest; import org.heigit.ors.snapping.SnappingResult; +import org.heigit.ors.util.ProfileTools; import org.locationtech.jts.geom.Coordinate; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @@ -27,7 +37,7 @@ public SnappingResult generateSnappingFromRequest(SnappingApiRequest snappingApi RoutingProfileManager rpm = RoutingProfileManager.getInstance(); RoutingProfile rp = rpm.getProfiles().getRouteProfile(snappingRequest.getProfileType()); GraphHopper gh = rp.getGraphhopper(); - return snappingRequest.computeResult(gh); + return computeResult(snappingRequest, gh); } catch (PointNotFoundException e) { throw new StatusCodeException(StatusCode.NOT_FOUND, SnappingErrorCodes.POINT_NOT_FOUND, e.getMessage()); } catch (StatusCodeException e) { @@ -70,4 +80,22 @@ private static Coordinate convertLocation(List location) throws StatusCo } return new Coordinate(location.get(0), location.get(1)); } + + public SnappingResult computeResult(SnappingRequest snappingRequest, GraphHopper gh) throws Exception { + String encoderName = RoutingProfileType.getEncoderName(snappingRequest.getProfileType()); + FlagEncoder flagEncoder = gh.getEncodingManager().getEncoder(encoderName); + PMap hintsMap = new PMap(); + int weightingMethod = WeightingMethod.RECOMMENDED; // Only needed to create the profile string + ProfileTools.setWeightingMethod(hintsMap, weightingMethod, snappingRequest.getProfileType(), false); + ProfileTools.setWeighting(hintsMap, weightingMethod, snappingRequest.getProfileType(), false); + String profileName = ProfileTools.makeProfileName(encoderName, hintsMap.getString("weighting", ""), false); + GraphHopperStorage ghStorage = gh.getGraphHopperStorage(); + String graphDate = ghStorage.getProperties().get("datareader.import.date"); + + // TODO: replace usage of matrix search context by snapping-specific class + MatrixSearchContextBuilder builder = new MatrixSearchContextBuilder(ghStorage, gh.getLocationIndex(), AccessFilter.allEdges(flagEncoder.getAccessEnc()), true); + Weighting weighting = new ORSWeightingFactory(ghStorage, gh.getEncodingManager()).createWeighting(gh.getProfile(profileName), hintsMap, false); + MatrixSearchContext mtxSearchCntx = builder.create(ghStorage.getBaseGraph(), null, weighting, profileName, snappingRequest.getLocations(), snappingRequest.getLocations(), snappingRequest.getMaximumSearchRadius()); + return new SnappingResult(mtxSearchCntx.getSources().getLocations(), graphDate); + } } diff --git a/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java b/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java index f4af183eb3..4c970d48d0 100644 --- a/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java +++ b/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java @@ -1,18 +1,6 @@ package org.heigit.ors.snapping; -import com.graphhopper.GraphHopper; -import com.graphhopper.routing.util.AccessFilter; -import com.graphhopper.routing.util.FlagEncoder; -import com.graphhopper.routing.weighting.Weighting; -import com.graphhopper.storage.GraphHopperStorage; -import com.graphhopper.util.PMap; import org.heigit.ors.common.ServiceRequest; -import org.heigit.ors.matrix.MatrixSearchContext; -import org.heigit.ors.matrix.MatrixSearchContextBuilder; -import org.heigit.ors.routing.RoutingProfileType; -import org.heigit.ors.routing.WeightingMethod; -import org.heigit.ors.routing.graphhopper.extensions.ORSWeightingFactory; -import org.heigit.ors.util.ProfileTools; import org.locationtech.jts.geom.Coordinate; public class SnappingRequest extends ServiceRequest { @@ -26,26 +14,15 @@ public SnappingRequest(int profileType, Coordinate[] locations, double maximumSe this.maximumSearchRadius = maximumSearchRadius; } - public SnappingResult computeResult(GraphHopper gh) throws Exception { - String encoderName = RoutingProfileType.getEncoderName(profileType); - FlagEncoder flagEncoder = gh.getEncodingManager().getEncoder(encoderName); - PMap hintsMap = new PMap(); - int weightingMethod = WeightingMethod.RECOMMENDED; // Only needed to create the profile string - ProfileTools.setWeightingMethod(hintsMap, weightingMethod, profileType, false); - ProfileTools.setWeighting(hintsMap, weightingMethod, profileType, false); - String profileName = ProfileTools.makeProfileName(encoderName, hintsMap.getString("weighting", ""), false); - GraphHopperStorage ghStorage = gh.getGraphHopperStorage(); - String graphDate = ghStorage.getProperties().get("datareader.import.date"); - - // TODO: replace usage of matrix search context by snapping-specific class - MatrixSearchContextBuilder builder = new MatrixSearchContextBuilder(ghStorage, gh.getLocationIndex(), AccessFilter.allEdges(flagEncoder.getAccessEnc()), true); - Weighting weighting = new ORSWeightingFactory(ghStorage, gh.getEncodingManager()).createWeighting(gh.getProfile(profileName), hintsMap, false); - MatrixSearchContext mtxSearchCntx = builder.create(ghStorage.getBaseGraph(), null, weighting, profileName, locations, locations, maximumSearchRadius); - return new SnappingResult(mtxSearchCntx.getSources().getLocations(), graphDate); - } - public int getProfileType() { return profileType; } + public Coordinate[] getLocations() { + return locations; + } + + public double getMaximumSearchRadius() { + return maximumSearchRadius; + } } From 2958559ca279f1b6d8d370d6c8640e6677bc099d Mon Sep 17 00:00:00 2001 From: Amandus Butzer Date: Wed, 4 Oct 2023 11:23:33 +0200 Subject: [PATCH 10/18] feat: add Getter for name of matrix JSONLocation schema --- .../heigit/ors/api/responses/matrix/json/JSONLocation.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/matrix/json/JSONLocation.java b/ors-api/src/main/java/org/heigit/ors/api/responses/matrix/json/JSONLocation.java index 91428496fe..e1c3ca90e4 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/responses/matrix/json/JSONLocation.java +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/matrix/json/JSONLocation.java @@ -63,4 +63,8 @@ public Double getSnappedDistance() { public Double[] getLocation() { return new Double[0]; } + + public String getName() { + return name; + } } From 9b96e4eb50706f391971bc089265681e01a22083 Mon Sep 17 00:00:00 2001 From: Amandus Butzer Date: Wed, 4 Oct 2023 15:08:17 +0200 Subject: [PATCH 11/18] docs: add unit to snapped_distance of JSONLocation schema --- .../org/heigit/ors/api/responses/matrix/json/JSONLocation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/matrix/json/JSONLocation.java b/ors-api/src/main/java/org/heigit/ors/api/responses/matrix/json/JSONLocation.java index e1c3ca90e4..f4344c0dea 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/responses/matrix/json/JSONLocation.java +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/matrix/json/JSONLocation.java @@ -44,7 +44,7 @@ public class JSONLocation { @JsonFormat(shape = JsonFormat.Shape.STRING) protected String name; - @Schema(description = "Distance between the `source/destination` Location and the used point on the routing graph.", example = "1.2") + @Schema(description = "Distance between the `source/destination` Location and the used point on the routing graph in meters.", example = "1.2") @JsonProperty(value = "snapped_distance") @JsonFormat(shape = JsonFormat.Shape.NUMBER_FLOAT, pattern = "%.2d") private final Double snappedDistance; From 5d3bf40f59d2b573d41dff9bfb1a63d114fe5bae Mon Sep 17 00:00:00 2001 From: Julian Psotta Date: Mon, 2 Oct 2023 11:51:26 +0200 Subject: [PATCH 12/18] test(snapping): Add parametrized exception test Co-authored-by: Jochen Haeussler --- .../ors/apitests/snapping/ParamsTest.java | 132 +++++++++++++++++- 1 file changed, 131 insertions(+), 1 deletion(-) diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java index a48d738ad4..67b7db181a 100644 --- a/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java +++ b/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java @@ -1,20 +1,46 @@ package org.heigit.ors.apitests.snapping; +import org.hamcrest.Matchers; import org.heigit.ors.apitests.common.EndPointAnnotation; import org.heigit.ors.apitests.common.ServiceTest; import org.heigit.ors.apitests.common.VersionAnnotation; import org.json.JSONArray; import org.json.JSONObject; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.is; import static org.heigit.ors.apitests.utils.CommonHeaders.jsonContent; +import static org.heigit.ors.common.StatusCode.*; +import static org.heigit.ors.snapping.SnappingErrorCodes.*; @EndPointAnnotation(name = "snap") @VersionAnnotation(version = "v2") class ParamsTest extends ServiceTest { + /** + * This function creates a {@link JSONArray} with fake coordinates. + * The size depends on maximumSize. + * + * @param maximumSize number of maximum coordinates in the {@link JSONArray} + * @return {@link JSONArray} + */ + private static JSONArray fakeLocations(int maximumSize) { + JSONArray overloadedLocations = new JSONArray(); + for (int i = 0; i < maximumSize; i++) { + JSONArray location = new JSONArray(); + location.put(0.0); + location.put(0.0); + overloadedLocations.put(location); + } + return overloadedLocations; + } + public ParamsTest() { JSONArray coordsShort = new JSONArray(); JSONArray coord1 = new JSONArray(); @@ -27,8 +53,9 @@ public ParamsTest() { coordsShort.put(coord2); addParameter("coordinates", coordsShort); } + @Test - void basicTest () { + void basicTest() { JSONObject body = new JSONObject(); body.put("locations", getParameter("coordinates")); body.put("maximum_search_radius", "300"); @@ -45,4 +72,107 @@ void basicTest () { .statusCode(200); } + + /** + * Provides a stream of test arguments for testing the Snapping Endpoint with various scenarios. + *

+ * The scenarios include: + * 1. Single fake location to check exception handling for single locations. + * 2. Ten fake locations to check exception handling for multiple locations. + * 3. Broken fake location to check exception handling for invalid locations. + * 4. Wrong profile to check exception handling for invalid profiles. + * 5. Unknown parameter to check exception handling for unknown parameters. + *

+ * Each test case is represented as an instance of the Arguments class, containing the following parameters: + * - The routing profile type (String). + * - The locations (JSONArray). + * - Expected error code for the test case (SnappingErrorCodes). + * - Expected HTTP status code for the test case (StatusCode). + * - Body parameter for testing (JSONObject). + * + * @return A stream of Arguments instances for testing the Snapping Endpoint with different scenarios. + */ + public static Stream snappingEndpointExceptionTestProvider() { + // Create fake locations for testing + JSONArray oneFakeLocation = fakeLocations(1); + JSONArray tenFakeLocations = fakeLocations(10); + + // Create a broken fake location with invalid coordinates + JSONArray brokenFakeLocation = new JSONArray(); + brokenFakeLocation.put(0.0); + brokenFakeLocation.put(0.0); + + JSONArray invalidCoords = new JSONArray(); + JSONArray invalidCoord1 = new JSONArray(); + invalidCoord1.put(8.680916); + invalidCoords.put(invalidCoord1); + + // Create correct test locations with valid coordinates + JSONArray correctTestLocations = new JSONArray(); + JSONArray coord1 = new JSONArray(); + coord1.put(8.680916); + coord1.put(49.410973); + correctTestLocations.put(coord1); + JSONArray coord2 = new JSONArray(); + coord2.put(8.687782); + coord2.put(49.424597); + correctTestLocations.put(coord2); + // Return a stream of test arguments + return Stream.of( + Arguments.of(INVALID_PARAMETER_FORMAT, BAD_REQUEST, "json", "driving-car", new JSONObject()), + // Check exception for one fake location to ensure single locations are checked + Arguments.of(POINT_NOT_FOUND, NOT_FOUND, "json", "driving-car", new JSONObject() + .put("locations", oneFakeLocation)), + // Check exception for ten fake locations to ensure multiple locations are checked + Arguments.of(POINT_NOT_FOUND, NOT_FOUND, "json", "driving-car", new JSONObject() + .put("locations", tenFakeLocations)), + // Check exception for broken location to ensure invalid locations are checked + Arguments.of(INVALID_PARAMETER_FORMAT, BAD_REQUEST, "json", "driving-car", new JSONObject() + .put("locations", brokenFakeLocation)), + // Check exception for wrong profile to ensure invalid profiles are checked + Arguments.of(INVALID_PARAMETER_VALUE, BAD_REQUEST, "json", "driving-foo", new JSONObject() + .put("locations", correctTestLocations)), + // Check exception for unknown parameter to ensure unknown parameters are checked + Arguments.of(UNKNOWN_PARAMETER, BAD_REQUEST, "json", "driving-car", new JSONObject() + .put("locations", correctTestLocations).put("unknown", "unknown")), + // Check exception for invalid locations parameter (only one ccordinate) + Arguments.of(INVALID_PARAMETER_VALUE, BAD_REQUEST, "json", "driving-car", new JSONObject() + .put("locations", invalidCoords).put("maximum_search_radius", "300")), + // Check exception for invalid locations parameter (only one ccordinate) + Arguments.of(INVALID_PARAMETER_FORMAT, BAD_REQUEST, "json", "driving-car", new JSONObject() + .put("locations", "noJsonArray").put("maximum_search_radius", "300")), + // Check exception for invalid maximum_search_radius + Arguments.of(INVALID_PARAMETER_FORMAT, BAD_REQUEST, "json", "driving-car", new JSONObject() + .put("locations", correctTestLocations).put("maximum_search_radius", "notANumber")), + // Check exception for missing locations parameter + Arguments.of(INVALID_PARAMETER_FORMAT, BAD_REQUEST, "json", "driving-car", new JSONObject() + .put("maximum_search_radius", "300") + )); + } + + /** + * Parameterized test method for testing various exception scenarios in the Snapping Endpoint. + * + * @param expectedErrorCode The expected error code for the test case (SnappingErrorCodes). + * @param expectedStatusCode The expected HTTP status code for the test case (StatusCode). + * @param profile The routing profile type (String). + * @param body The request body (JSONObject). + */ + @ParameterizedTest + @MethodSource("snappingEndpointExceptionTestProvider") + void testSnappingExceptions(int expectedErrorCode, int expectedStatusCode, String endPoint, String profile, JSONObject body) { + + given() + .headers(jsonContent) + .pathParam("profile", profile) + .body(body.toString()) + .when() + .log().ifValidationFails() + .post(getEndPointPath() + "/{profile}/" + endPoint) + .then() + .log().ifValidationFails() + .assertThat() + .body("error.code", Matchers.is(expectedErrorCode)) + .statusCode(expectedStatusCode); + } } From cbf7f5e98c12a361b6f01733b0a7bb83eed42cf2 Mon Sep 17 00:00:00 2001 From: Julian Psotta Date: Tue, 3 Oct 2023 00:17:21 +0200 Subject: [PATCH 13/18] test(snapping): refactor basicTest to testSnappingSuccess - use argument stream for parametrization - add multiple test cases --- .../ors/apitests/snapping/ParamsTest.java | 119 +++++++++++++++--- 1 file changed, 102 insertions(+), 17 deletions(-) diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java index 67b7db181a..48bd76fbec 100644 --- a/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java +++ b/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java @@ -1,5 +1,6 @@ package org.heigit.ors.apitests.snapping; +import io.restassured.response.ValidatableResponse; import org.hamcrest.Matchers; import org.heigit.ors.apitests.common.EndPointAnnotation; import org.heigit.ors.apitests.common.ServiceTest; @@ -11,24 +12,28 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import java.util.stream.Stream; import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.is; +import static org.assertj.core.api.FactoryBasedNavigableListAssert.assertThat; +import static org.hamcrest.Matchers.*; import static org.heigit.ors.apitests.utils.CommonHeaders.jsonContent; import static org.heigit.ors.common.StatusCode.*; import static org.heigit.ors.snapping.SnappingErrorCodes.*; +import static org.junit.jupiter.api.Assertions.*; @EndPointAnnotation(name = "snap") @VersionAnnotation(version = "v2") class ParamsTest extends ServiceTest { /** - * This function creates a {@link JSONArray} with fake coordinates. - * The size depends on maximumSize. + * Generates a JSONArray with fake locations for testing purposes. * - * @param maximumSize number of maximum coordinates in the {@link JSONArray} - * @return {@link JSONArray} + * @param maximumSize The maximum size of the JSONArray. + * @return A JSONArray containing fake locations with the specified size. */ private static JSONArray fakeLocations(int maximumSize) { JSONArray overloadedLocations = new JSONArray(); @@ -41,7 +46,12 @@ private static JSONArray fakeLocations(int maximumSize) { return overloadedLocations; } - public ParamsTest() { + /** + * Generates a JSONArray with valid locations for testing purposes. + * + * @return A JSONArray containing valid coordinates for testing. + */ + public static JSONArray validLocations() { JSONArray coordsShort = new JSONArray(); JSONArray coord1 = new JSONArray(); coord1.put(8.680916); @@ -49,30 +59,105 @@ public ParamsTest() { coordsShort.put(coord1); JSONArray coord2 = new JSONArray(); coord2.put(8.687782); - coord2.put(49.424597); + coord2.put(49.4246); coordsShort.put(coord2); - addParameter("coordinates", coordsShort); + return coordsShort; } - @Test - void basicTest() { - JSONObject body = new JSONObject(); - body.put("locations", getParameter("coordinates")); - body.put("maximum_search_radius", "300"); - given() + /** + * Provides a stream of test arguments for testing successful scenarios in the Snapping Endpoint. + *

+ * Each test case is represented as an instance of the Arguments class, containing the following parameters: + * - The request body (JSONObject). + * - Boolean flag indicating whether an empty result is expected. + * - Boolean flag indicating whether a partially empty result is expected. + * - The endpoint type (String). + * - The routing profile type (String). + * + * @return A stream of Arguments instances for testing successful scenarios in the Snapping Endpoint. + */ + public static Stream snappingEndpointSuccessTestProvider() { + return Stream.of( + Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "1"), true, false, "json", "driving-hgv"), + Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "10"), false, true, "json", "driving-hgv"), + Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "300"), false, false, "json", "driving-hgv"), + Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "400"), false, false, "json", "driving-hgv"), + Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "1000"), false, false, "json", "driving-hgv") + ); + } + + /** + * Parameterized test method for testing various scenarios in the Snapping Endpoint. + * + * @param body The request body (JSONObject). + * @param emptyResult Boolean flag indicating whether an empty result is expected. + * @param partiallyEmptyResult Boolean flag indicating whether a partially empty result is expected. + * @param endPoint The endpoint type (String). + * @param profile The routing profile type (String). + */ + @ParameterizedTest + @MethodSource("snappingEndpointSuccessTestProvider") + void testSnappingSuccess(JSONObject body, Boolean emptyResult, Boolean partiallyEmptyResult, String endPoint, String profile) { + + ValidatableResponse result = given() .headers(jsonContent) - .pathParam("profile", "driving-car") + .pathParam("profile", profile) .body(body.toString()) .when() .log().ifValidationFails() - .post(getEndPointPath() + "/{profile}/json") + .post(getEndPointPath() + "/{profile}/" + endPoint) .then() .log().ifValidationFails() - .body("any { it.key == 'locations' }", is(true)) .statusCode(200); + // Check if the response contains the expected keys + result.body("any { it.key == 'locations' }", is(true)); + result.body("any { it.key == 'metadata' }", is(true)); + result.body("metadata.containsKey('attribution')", is(true)); + result.body("metadata.service", is("snap")); + result.body("metadata.containsKey('timestamp')", is(true)); + result.body("metadata.containsKey('query')", is(true)); + result.body("metadata.containsKey('engine')", is(true)); + result.body("metadata.containsKey('system_message')", is(true)); + result.body("metadata.query.locations.size()", is(2)); + result.body("metadata.query.locations[0].size()", is(2)); + result.body("metadata.query.locations[1].size()", is(2)); + result.body("metadata.query.profile", is(profile)); + result.body("metadata.query.format", is(endPoint)); + result.body("metadata.query.maximum_search_radius", is(Float.parseFloat(body.get("maximum_search_radius").toString()))); + + + boolean foundValidLocation = false; + boolean foundInvalidLocation = false; + + // Iterate over the locations array and check the types of the values + ArrayList locations = result.extract().jsonPath().get("locations"); + for (int i = 0; i < locations.size(); i++) { + // if empty result is expected, check if the locations array is empty + if (emptyResult) { + assertNull(result.extract().jsonPath().get("locations[" + i + "].location[0]")); + foundValidLocation = true; + foundInvalidLocation = true; + } else if (partiallyEmptyResult && !foundInvalidLocation && result.extract().jsonPath().get("locations[" + i + "]") == null) { + foundInvalidLocation = true; + } else { + // Type expectations + assertEquals(Float.class, result.extract().jsonPath().get("locations[" + i + "].location[0]").getClass()); + assertEquals(Float.class, result.extract().jsonPath().get("locations[" + i + "].location[1]").getClass()); + assertEquals(Float.class, result.extract().jsonPath().get("locations[" + i + "].snapped_distance").getClass()); + // If name is in the response, check the type + if (result.extract().jsonPath().get("locations[" + i + "].name") != null) + assertEquals(String.class, result.extract().jsonPath().get("locations[" + i + "].name").getClass()); + foundValidLocation = true; + } + } + + assertTrue(foundValidLocation); + if (partiallyEmptyResult) + assertTrue(foundInvalidLocation); } + /** * Provides a stream of test arguments for testing the Snapping Endpoint with various scenarios. *

From 7c24f36a6743b4411ca9a2fca56b0b48eca58214 Mon Sep 17 00:00:00 2001 From: Jochen Haeussler Date: Wed, 4 Oct 2023 11:51:42 +0200 Subject: [PATCH 14/18] test(snapping): add endpoint and profile conditionally - add more tests with missing endpoint/profile --- .../ors/apitests/snapping/ParamsTest.java | 72 ++++++++++++++----- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java index 48bd76fbec..63bd52d096 100644 --- a/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java +++ b/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java @@ -1,27 +1,27 @@ package org.heigit.ors.apitests.snapping; import io.restassured.response.ValidatableResponse; +import io.restassured.specification.RequestSpecification; +import org.apache.commons.lang3.StringUtils; import org.hamcrest.Matchers; import org.heigit.ors.apitests.common.EndPointAnnotation; import org.heigit.ors.apitests.common.ServiceTest; import org.heigit.ors.apitests.common.VersionAnnotation; import org.json.JSONArray; import org.json.JSONObject; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; import java.util.stream.Stream; import static io.restassured.RestAssured.given; -import static org.assertj.core.api.FactoryBasedNavigableListAssert.assertThat; -import static org.hamcrest.Matchers.*; +import static jakarta.servlet.http.HttpServletResponse.SC_NOT_ACCEPTABLE; +import static org.hamcrest.Matchers.is; import static org.heigit.ors.apitests.utils.CommonHeaders.jsonContent; -import static org.heigit.ors.common.StatusCode.*; +import static org.heigit.ors.common.StatusCode.BAD_REQUEST; +import static org.heigit.ors.common.StatusCode.NOT_FOUND; import static org.heigit.ors.snapping.SnappingErrorCodes.*; import static org.junit.jupiter.api.Assertions.*; @@ -78,11 +78,14 @@ public static JSONArray validLocations() { */ public static Stream snappingEndpointSuccessTestProvider() { return Stream.of( + Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "-1"), true, false, "json", "driving-hgv"), + Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "0"), true, false, "json", "driving-hgv"), Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "1"), true, false, "json", "driving-hgv"), Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "10"), false, true, "json", "driving-hgv"), Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "300"), false, false, "json", "driving-hgv"), Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "400"), false, false, "json", "driving-hgv"), - Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "1000"), false, false, "json", "driving-hgv") + Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "1000"), false, false, "json", "driving-hgv"), + Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "1000"), false, false, null, "driving-hgv") ); } @@ -99,13 +102,24 @@ public static Stream snappingEndpointSuccessTestProvider() { @MethodSource("snappingEndpointSuccessTestProvider") void testSnappingSuccess(JSONObject body, Boolean emptyResult, Boolean partiallyEmptyResult, String endPoint, String profile) { - ValidatableResponse result = given() - .headers(jsonContent) - .pathParam("profile", profile) + RequestSpecification requestSpecification = given() + .headers(jsonContent); + + if (profile != null) + requestSpecification = requestSpecification.pathParam("profile", profile); + + String url = getEndPointPath(); + if (StringUtils.isNotBlank(profile)) + url = url + "/{profile}"; + + if (StringUtils.isNotBlank(endPoint)) + url = url + "/" + endPoint; + + ValidatableResponse result = requestSpecification .body(body.toString()) .when() .log().ifValidationFails() - .post(getEndPointPath() + "/{profile}/" + endPoint) + .post(url) .then() .log().ifValidationFails() .statusCode(200); @@ -123,8 +137,12 @@ void testSnappingSuccess(JSONObject body, Boolean emptyResult, Boolean partially result.body("metadata.query.locations[0].size()", is(2)); result.body("metadata.query.locations[1].size()", is(2)); result.body("metadata.query.profile", is(profile)); - result.body("metadata.query.format", is(endPoint)); - result.body("metadata.query.maximum_search_radius", is(Float.parseFloat(body.get("maximum_search_radius").toString()))); + + if (body.get("maximum_search_radius") != "0") + result.body("metadata.query.maximum_search_radius", is(Float.parseFloat(body.get("maximum_search_radius").toString()))); + + if (StringUtils.isNotBlank(endPoint)) + result.body("metadata.query.format", is(endPoint)); boolean foundValidLocation = false; @@ -204,6 +222,15 @@ public static Stream snappingEndpointExceptionTestProvider() { correctTestLocations.put(coord2); // Return a stream of test arguments return Stream.of( + //Check exception for missing profile and return type + Arguments.of(MISSING_PARAMETER, BAD_REQUEST, null, null, new JSONObject() + .put("locations", correctTestLocations).put("maximum_search_radius", "300")), + //Check exception for missing profile - "json" is interpreted as profile + Arguments.of(INVALID_PARAMETER_VALUE, BAD_REQUEST, "json", null, new JSONObject() + .put("locations", correctTestLocations).put("maximum_search_radius", "300")), + //Check exception for missing profile - "json" is interpreted as profile + Arguments.of(UNSUPPORTED_EXPORT_FORMAT, SC_NOT_ACCEPTABLE, "badExportFormat", "driving-car", new JSONObject() + .put("locations", correctTestLocations).put("maximum_search_radius", "300")), Arguments.of(INVALID_PARAMETER_FORMAT, BAD_REQUEST, "json", "driving-car", new JSONObject()), // Check exception for one fake location to ensure single locations are checked Arguments.of(POINT_NOT_FOUND, NOT_FOUND, "json", "driving-car", new JSONObject() @@ -247,13 +274,24 @@ public static Stream snappingEndpointExceptionTestProvider() { @MethodSource("snappingEndpointExceptionTestProvider") void testSnappingExceptions(int expectedErrorCode, int expectedStatusCode, String endPoint, String profile, JSONObject body) { - given() - .headers(jsonContent) - .pathParam("profile", profile) + RequestSpecification requestSpecification = given() + .headers(jsonContent); + + if (profile != null) + requestSpecification = requestSpecification.pathParam("profile", profile); + + String url = getEndPointPath(); + if (StringUtils.isNotBlank(profile)) + url = url + "/{profile}"; + + if (StringUtils.isNotBlank(endPoint)) + url = url + "/" + endPoint; + + requestSpecification .body(body.toString()) .when() .log().ifValidationFails() - .post(getEndPointPath() + "/{profile}/" + endPoint) + .post(url) .then() .log().ifValidationFails() .assertThat() From 965f931216c4ef3137022024c2887979c0495387 Mon Sep 17 00:00:00 2001 From: Amandus Butzer Date: Wed, 4 Oct 2023 15:07:04 +0200 Subject: [PATCH 15/18] feat(snapping): add GeoJSON endpoint - add schema classes for GeoJSON response - add endpoint to SnappingAPI - add GeoJSON to APIEnums --- .../ors/api/controllers/SnappingAPI.java | 32 +++++++- .../snapping/geojson/GeoJSONFeature.java | 45 +++++++++++ .../geojson/GeoJSONFeatureProperties.java | 42 +++++++++++ .../geojson/GeoJSONPointGeometry.java | 31 ++++++++ .../geojson/GeoJSONSnappingResponse.java | 75 +++++++++++++++++++ .../java/org/heigit/ors/routing/APIEnums.java | 3 +- 6 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONFeature.java create mode 100644 ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONFeatureProperties.java create mode 100644 ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONPointGeometry.java create mode 100644 ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONSnappingResponse.java diff --git a/ors-api/src/main/java/org/heigit/ors/api/controllers/SnappingAPI.java b/ors-api/src/main/java/org/heigit/ors/api/controllers/SnappingAPI.java index a13e4ab46b..f848a96c17 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/controllers/SnappingAPI.java +++ b/ors-api/src/main/java/org/heigit/ors/api/controllers/SnappingAPI.java @@ -30,6 +30,7 @@ import org.heigit.ors.api.SystemMessageProperties; import org.heigit.ors.api.errors.CommonResponseEntityExceptionHandler; import org.heigit.ors.api.requests.snapping.SnappingApiRequest; +import org.heigit.ors.api.responses.snapping.geojson.GeoJSONSnappingResponse; import org.heigit.ors.api.responses.snapping.json.JsonSnappingResponse; import org.heigit.ors.api.services.SnappingService; import org.heigit.ors.api.util.AppConfigMigration; @@ -113,7 +114,7 @@ public JsonSnappingResponse getDefault(@Parameter(description = "Specifies the r @Operation( description = """ Returns a list of points snapped to the nearest edge in the graph. In case an appropriate - snapping point cannot be found within the specified search radius, \"null\" is returned. + snapping point cannot be found within the specified search radius, "null" is returned. """, summary = "Snapping Service JSON" ) @@ -136,6 +137,35 @@ public JsonSnappingResponse getJsonSnapping( return new JsonSnappingResponse(result, request, systemMessageProperties, endpointsProperties); } +@PostMapping(value = "/{profile}/geojson", produces = {"application/json;charset=UTF-8"}) + @Operation( + description = """ + Returns a GeoJSON FeatureCollection of points snapped to the nearest edge in the graph. + In case an appropriate snapping point cannot be found within the specified search radius, + it is omitted from the features array. The features provide the 'source_id' property, to match + the results with the input location array (IDs start at 0). + """, + summary = "Snapping Service GeoJSON" + ) + @ApiResponse( + responseCode = "200", + description = "GeoJSON Response", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = GeoJSONSnappingResponse.class) + ) + }) + public GeoJSONSnappingResponse getGeoJSONSnapping( + @Parameter(description = "Specifies the profile.", required = true, example = "driving-car") @PathVariable APIEnums.Profile profile, + @Parameter(description = "The request payload", required = true) @RequestBody SnappingApiRequest request) throws StatusCodeException { + request.setProfile(profile); + request.setResponseType(APIEnums.SnappingResponseType.GEOJSON); + + SnappingResult result = snappingService.generateSnappingFromRequest(request); + + return new GeoJSONSnappingResponse(result, request, systemMessageProperties, endpointsProperties); + } + @ExceptionHandler(MissingServletRequestParameterException.class) public ResponseEntity handleMissingParams(final MissingServletRequestParameterException e) { return errorHandler.handleStatusCodeException(new MissingParameterException(SnappingErrorCodes.MISSING_PARAMETER, e.getParameterName())); diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONFeature.java b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONFeature.java new file mode 100644 index 0000000000..8e48ef6973 --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONFeature.java @@ -0,0 +1,45 @@ +package org.heigit.ors.api.responses.snapping.geojson; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import org.heigit.ors.api.responses.matrix.json.JSON2DSources; + +public class GeoJSONFeature { + @JsonProperty("type") + @Schema(description = "GeoJSON type", defaultValue = "Feature") + public final String type = "Feature"; + + @JsonProperty("properties") + @Schema(description = "Feature properties") + public GeoJSONFeatureProperties props; + + public GeoJSONFeature(JSON2DSources source) { + this.geometry = new GeoJSONPointGeometry(source); + this.props = new GeoJSONFeatureProperties(source); + } + + @JsonProperty("geometry") + @Schema(description = "Feature geometry") + public GeoJSONPointGeometry geometry; + + public String getType() { + return type; + } + + public GeoJSONFeatureProperties getProps() { + return props; + } + + public void setProps(GeoJSONFeatureProperties props) { + this.props = props; + } + + public GeoJSONPointGeometry getGeometry() { + return geometry; + } + + public void setGeometry(GeoJSONPointGeometry geometry) { + this.geometry = geometry; + } + +} diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONFeatureProperties.java b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONFeatureProperties.java new file mode 100644 index 0000000000..7977bd4775 --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONFeatureProperties.java @@ -0,0 +1,42 @@ +package org.heigit.ors.api.responses.snapping.geojson; + +import com.fasterxml.jackson.annotation.JsonProperty; +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 org.heigit.ors.api.responses.matrix.json.JSON2DSources; + +public class GeoJSONFeatureProperties { + @JsonProperty("name") + @Schema(description = "\"Name of the street the closest accessible point is situated on. Only for `resolve_locations=true` and only if name is available.", + extensions = {@Extension(name = "validWhen", properties = { + @ExtensionProperty(name = "ref", value = "resolve_locations"), + @ExtensionProperty(name = "value", value = "true", parseValue = true)} + )}, + example = "Gerhart-Hauptmann-Straße") + public String name; + @JsonProperty("snapped_distance") + @Schema(description = "Distance between the `source/destination` Location and the used point on the routing graph in meters.", + example = "0.02") + public double dist; + public GeoJSONFeatureProperties(JSON2DSources source) { + this.dist = source.getSnappedDistance(); + this.name = source.getName(); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getDist() { + return dist; + } + + public void setDist(double dist) { + this.dist = dist; + } +} diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONPointGeometry.java b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONPointGeometry.java new file mode 100644 index 0000000000..b38325c251 --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONPointGeometry.java @@ -0,0 +1,31 @@ +package org.heigit.ors.api.responses.snapping.geojson; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import org.heigit.ors.api.responses.matrix.json.JSON2DSources; + +public class GeoJSONPointGeometry { + @JsonProperty("type") + @Schema(description = "GeoJSON type", defaultValue = "Point") + public final String type = "Point"; + + @JsonProperty("coordinates") + @Schema(description = "Lon/Lat coordinates of the snapped location", example = "[8.681495,49.41461]") + public Double[] coordinates; + + public GeoJSONPointGeometry(JSON2DSources source) { + this.coordinates = source.getLocation(); + } + + public String getType() { + return type; + } + + public Double[] getCoordinates() { + return coordinates; + } + + public void setCoordinates(Double[] coordinates) { + this.coordinates = coordinates; + } +} diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONSnappingResponse.java b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONSnappingResponse.java new file mode 100644 index 0000000000..e3d6651816 --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONSnappingResponse.java @@ -0,0 +1,75 @@ +package org.heigit.ors.api.responses.snapping.geojson; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.graphhopper.util.shapes.BBox; +import io.swagger.v3.oas.annotations.media.Schema; +import org.heigit.ors.api.EndpointsProperties; +import org.heigit.ors.api.SystemMessageProperties; +import org.heigit.ors.api.requests.snapping.SnappingApiRequest; +import org.heigit.ors.api.responses.common.boundingbox.BoundingBox; +import org.heigit.ors.api.responses.common.boundingbox.BoundingBoxBase; +import org.heigit.ors.api.responses.matrix.json.JSON2DSources; +import org.heigit.ors.api.responses.routing.json.JSONBoundingBox; +import org.heigit.ors.api.responses.snapping.SnappingResponse; +import org.heigit.ors.api.responses.snapping.SnappingResponseInfo; +import org.heigit.ors.snapping.SnappingResult; +import org.heigit.ors.util.GeomUtility; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Schema(name = "GeoJSONSnappingResponse", description = "The GeoJSON Snapping Response contains the snapped coordinates in GeoJSON format.") +public class GeoJSONSnappingResponse extends SnappingResponse { + + @JsonIgnore + protected BoundingBox bbox; + + @JsonProperty("type") + @Schema(description = "GeoJSON type", defaultValue = "FeatureCollection") + public final String type = "FeatureCollection"; + + @JsonProperty("bbox") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + @Schema(description = "Bounding box that covers all returned snapping points", example = "[49.414057, 8.680894, 49.420514, 8.690123]") + public double[] getBBoxAsArray() { + return bbox.getAsArray(); + } + + @JsonProperty("features") + @Schema(description = "Information about the service and request") + public List features; + + @JsonProperty("metadata") + @Schema(description = "Information about the service and request") + SnappingResponseInfo responseInformation; + + public GeoJSONSnappingResponse(SnappingResult result, SnappingApiRequest request, SystemMessageProperties systemMessageProperties, EndpointsProperties endpointsProperties) { + super(result); + this.features = new ArrayList<>(); + List bBoxes = new ArrayList<>(); + Arrays.stream(result.getLocations()) + .forEach(resolvedLocation -> { + if (resolvedLocation == null) { + this.features.add(null); + } else { + // create BBox for each point to use existing generateBoundingFromMultiple function + double x = resolvedLocation.getCoordinate().x; + double y = resolvedLocation.getCoordinate().y; + bBoxes.add(new BBox(x,x,y,y)); + this.features.add(new GeoJSONFeature(new JSON2DSources(resolvedLocation, true))); + } + }); + BBox[] boxes = bBoxes.toArray(new BBox[0]); + if (boxes.length > 0) { + this.bbox = new BoundingBoxBase(GeomUtility.generateBoundingFromMultiple(boxes)); + } else { + this.bbox = new JSONBoundingBox(new BBox(0,0,0,0)); + } + + responseInformation = new SnappingResponseInfo(request, systemMessageProperties, endpointsProperties); + responseInformation.setGraphDate(result.getGraphDate()); + } +} diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/APIEnums.java b/ors-engine/src/main/java/org/heigit/ors/routing/APIEnums.java index d3860f43b4..aae356f3c4 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/APIEnums.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/APIEnums.java @@ -174,7 +174,8 @@ public String toString() { @Schema(name = "Snapping response type", description = "Format of the snapping response.") public enum SnappingResponseType { - JSON("json"); + JSON("json"), + GEOJSON("geojson"); private final String value; From 4280e4d25675a409272f7ba3587f978faca90f4b Mon Sep 17 00:00:00 2001 From: Jochen Haeussler Date: Thu, 5 Oct 2023 12:45:02 +0200 Subject: [PATCH 16/18] test(snapping): refactored ParamsTest and covered geojson --- .../ors/apitests/snapping/ParamsTest.java | 311 ++++++++++++------ 1 file changed, 205 insertions(+), 106 deletions(-) diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java index 63bd52d096..62e64c4f4a 100644 --- a/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java +++ b/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java @@ -1,7 +1,6 @@ package org.heigit.ors.apitests.snapping; import io.restassured.response.ValidatableResponse; -import io.restassured.specification.RequestSpecification; import org.apache.commons.lang3.StringUtils; import org.hamcrest.Matchers; import org.heigit.ors.apitests.common.EndPointAnnotation; @@ -9,6 +8,7 @@ import org.heigit.ors.apitests.common.VersionAnnotation; import org.json.JSONArray; import org.json.JSONObject; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -51,17 +51,26 @@ private static JSONArray fakeLocations(int maximumSize) { * * @return A JSONArray containing valid coordinates for testing. */ - public static JSONArray validLocations() { - JSONArray coordsShort = new JSONArray(); + private static JSONArray validLocations() { + // Create correct test locations with valid coordinates + JSONArray correctTestLocations = new JSONArray(); JSONArray coord1 = new JSONArray(); coord1.put(8.680916); coord1.put(49.410973); - coordsShort.put(coord1); + correctTestLocations.put(coord1); JSONArray coord2 = new JSONArray(); coord2.put(8.687782); - coord2.put(49.4246); - coordsShort.put(coord2); - return coordsShort; + coord2.put(49.424597); + correctTestLocations.put(coord2); + return correctTestLocations; + } + + + private static JSONObject validBody() { + JSONObject body = new JSONObject() + .put("locations", validLocations()) + .put("maximum_search_radius", "300"); + return body; } /** @@ -78,44 +87,127 @@ public static JSONArray validLocations() { */ public static Stream snappingEndpointSuccessTestProvider() { return Stream.of( - Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "-1"), true, false, "json", "driving-hgv"), - Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "0"), true, false, "json", "driving-hgv"), - Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "1"), true, false, "json", "driving-hgv"), - Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "10"), false, true, "json", "driving-hgv"), - Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "300"), false, false, "json", "driving-hgv"), - Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "400"), false, false, "json", "driving-hgv"), - Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "1000"), false, false, "json", "driving-hgv"), - Arguments.of(new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "1000"), false, false, null, "driving-hgv") + Arguments.of(true, false, "driving-hgv", new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "-1")), + Arguments.of(true, false, "driving-hgv", new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "0")), + Arguments.of(true, false, "driving-hgv", new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "1")), + Arguments.of(false, true, "driving-hgv", new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "10")), + Arguments.of(false, false, "driving-hgv", new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "300")), + Arguments.of(false, false, "driving-hgv", new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "400")), + Arguments.of(false, false, "driving-hgv", new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "1000")), + Arguments.of(false, false, "driving-hgv", new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "1000")) ); } /** * Parameterized test method for testing various scenarios in the Snapping Endpoint. * - * @param body The request body (JSONObject). - * @param emptyResult Boolean flag indicating whether an empty result is expected. - * @param partiallyEmptyResult Boolean flag indicating whether a partially empty result is expected. - * @param endPoint The endpoint type (String). - * @param profile The routing profile type (String). + * @param expectEmptyResult Boolean flag indicating whether an empty result is expected. + * @param expectPartiallyEmptyResult Boolean flag indicating whether a partially empty result is expected. + * @param body The request body (JSONObject). + * @param profile The routing profile type (String). */ @ParameterizedTest @MethodSource("snappingEndpointSuccessTestProvider") - void testSnappingSuccess(JSONObject body, Boolean emptyResult, Boolean partiallyEmptyResult, String endPoint, String profile) { + void testSnappingSuccessJson(Boolean expectEmptyResult, Boolean expectPartiallyEmptyResult, String profile, JSONObject body) { + String endPoint = "json"; + ValidatableResponse result = doRequestAndExceptSuccess(body, profile, endPoint); + validateJsonResponse(expectEmptyResult, expectPartiallyEmptyResult, result); + } - RequestSpecification requestSpecification = given() - .headers(jsonContent); + @Test + void testMissingPathParameterFormat_defaultsToJson() { + ValidatableResponse result = doRequestAndExceptSuccess(validBody(), "driving-hgv", null); + validateJsonResponse(false, false, result); + } - if (profile != null) - requestSpecification = requestSpecification.pathParam("profile", profile); + private static void validateJsonResponse(Boolean expectEmptyResult, Boolean expectPartiallyEmptyResult, ValidatableResponse result) { + boolean foundValidLocation = false; + boolean foundInvalidLocation = false; + + result.body("any { it.key == 'locations' }", is(true)); - String url = getEndPointPath(); - if (StringUtils.isNotBlank(profile)) - url = url + "/{profile}"; + // Iterate over the locations array and check the types of the values + ArrayList locations = result.extract().jsonPath().get("locations"); + for (int i = 0; i < locations.size(); i++) { + // if empty result is expected, check if the locations array is empty + if (expectEmptyResult) { + assertNull(result.extract().jsonPath().get("locations[" + i + "].location[0]")); + foundValidLocation = true; + foundInvalidLocation = true; + } else if (expectPartiallyEmptyResult && !foundInvalidLocation && result.extract().jsonPath().get("locations[" + i + "]") == null) { + foundInvalidLocation = true; + } else { + // Type expectations + assertEquals(Float.class, result.extract().jsonPath().get("locations[" + i + "].location[0]").getClass()); + assertEquals(Float.class, result.extract().jsonPath().get("locations[" + i + "].location[1]").getClass()); + assertEquals(Float.class, result.extract().jsonPath().get("locations[" + i + "].snapped_distance").getClass()); + // If name is in the response, check the type + if (result.extract().jsonPath().get("locations[" + i + "].name") != null) + assertEquals(String.class, result.extract().jsonPath().get("locations[" + i + "].name").getClass()); + foundValidLocation = true; + } + } + assertTrue(foundValidLocation); + if (expectPartiallyEmptyResult) + assertTrue(foundInvalidLocation); + } + + /** + * Parameterized test method for testing various scenarios in the Snapping Endpoint. + * + * @param expectEmptyResult Boolean flag indicating whether an empty result is expected. + * @param expectPartiallyEmptyResult Boolean flag indicating whether a partially empty result is expected. + * @param body The request body (JSONObject). + * @param profile The routing profile type (String). + */ + @ParameterizedTest + @MethodSource("snappingEndpointSuccessTestProvider") + void testSnappingSuccessGeojson(Boolean expectEmptyResult, Boolean expectPartiallyEmptyResult, String profile, JSONObject body) { + String endPoint = "geojson"; + ValidatableResponse result = doRequestAndExceptSuccess(body, profile, endPoint); + + boolean foundValidLocation = false; + boolean foundInvalidLocation = false; + + result.body("any { it.key == 'features' }", is(true)); + result.body("any { it.key == 'type' }", is(true)); + + // Iterate over the features array and check the types of the values + ArrayList features = result.extract().jsonPath().get("features"); + for (int i = 0; i < features.size(); i++) { + // if empty result is expected, check if the features array is empty + if (expectEmptyResult) { + assertNull(result.extract().jsonPath().get("features[" + i + "].features[0]")); + foundValidLocation = true; + foundInvalidLocation = true; + } else if (expectPartiallyEmptyResult && !foundInvalidLocation && result.extract().jsonPath().get("features[" + i + "]") == null) { + foundInvalidLocation = true; + } else { + // Type expectations + assertEquals(Float.class, result.extract().jsonPath().get("features[" + i + "].geometry.coordinates[0]").getClass()); + assertEquals(Float.class, result.extract().jsonPath().get("features[" + i + "].geometry.coordinates[1]").getClass()); + assertEquals(Float.class, result.extract().jsonPath().get("features[" + i + "].properties.snapped_distance").getClass()); + // If name is in the response, check the type + if (result.extract().jsonPath().get("features[" + i + "].properties.name") != null) + assertEquals(String.class, result.extract().jsonPath().get("features[" + i + "].properties.name").getClass()); + foundValidLocation = true; + } + } + + assertTrue(foundValidLocation); + if (expectPartiallyEmptyResult) + assertTrue(foundInvalidLocation); + } + + private ValidatableResponse doRequestAndExceptSuccess(JSONObject body, String profile, String endPoint) { + String url = getEndPointPath() + "/{profile}"; if (StringUtils.isNotBlank(endPoint)) - url = url + "/" + endPoint; + url = "%s/%s".formatted(url, endPoint); - ValidatableResponse result = requestSpecification + ValidatableResponse result = given() + .headers(jsonContent) + .pathParam("profile", profile) .body(body.toString()) .when() .log().ifValidationFails() @@ -125,7 +217,6 @@ void testSnappingSuccess(JSONObject body, Boolean emptyResult, Boolean partially .statusCode(200); // Check if the response contains the expected keys - result.body("any { it.key == 'locations' }", is(true)); result.body("any { it.key == 'metadata' }", is(true)); result.body("metadata.containsKey('attribution')", is(true)); result.body("metadata.service", is("snap")); @@ -138,41 +229,13 @@ void testSnappingSuccess(JSONObject body, Boolean emptyResult, Boolean partially result.body("metadata.query.locations[1].size()", is(2)); result.body("metadata.query.profile", is(profile)); - if (body.get("maximum_search_radius") != "0") - result.body("metadata.query.maximum_search_radius", is(Float.parseFloat(body.get("maximum_search_radius").toString()))); - if (StringUtils.isNotBlank(endPoint)) result.body("metadata.query.format", is(endPoint)); - - boolean foundValidLocation = false; - boolean foundInvalidLocation = false; - - // Iterate over the locations array and check the types of the values - ArrayList locations = result.extract().jsonPath().get("locations"); - for (int i = 0; i < locations.size(); i++) { - // if empty result is expected, check if the locations array is empty - if (emptyResult) { - assertNull(result.extract().jsonPath().get("locations[" + i + "].location[0]")); - foundValidLocation = true; - foundInvalidLocation = true; - } else if (partiallyEmptyResult && !foundInvalidLocation && result.extract().jsonPath().get("locations[" + i + "]") == null) { - foundInvalidLocation = true; - } else { - // Type expectations - assertEquals(Float.class, result.extract().jsonPath().get("locations[" + i + "].location[0]").getClass()); - assertEquals(Float.class, result.extract().jsonPath().get("locations[" + i + "].location[1]").getClass()); - assertEquals(Float.class, result.extract().jsonPath().get("locations[" + i + "].snapped_distance").getClass()); - // If name is in the response, check the type - if (result.extract().jsonPath().get("locations[" + i + "].name") != null) - assertEquals(String.class, result.extract().jsonPath().get("locations[" + i + "].name").getClass()); - foundValidLocation = true; - } + if (body.get("maximum_search_radius") != "0") { + result.body("metadata.query.maximum_search_radius", is(Float.parseFloat(body.get("maximum_search_radius").toString()))); } - - assertTrue(foundValidLocation); - if (partiallyEmptyResult) - assertTrue(foundInvalidLocation); + return result; } @@ -210,54 +273,36 @@ public static Stream snappingEndpointExceptionTestProvider() { invalidCoord1.put(8.680916); invalidCoords.put(invalidCoord1); - // Create correct test locations with valid coordinates - JSONArray correctTestLocations = new JSONArray(); - JSONArray coord1 = new JSONArray(); - coord1.put(8.680916); - coord1.put(49.410973); - correctTestLocations.put(coord1); - JSONArray coord2 = new JSONArray(); - coord2.put(8.687782); - coord2.put(49.424597); - correctTestLocations.put(coord2); + JSONArray correctTestLocations = validLocations(); // Return a stream of test arguments return Stream.of( - //Check exception for missing profile and return type - Arguments.of(MISSING_PARAMETER, BAD_REQUEST, null, null, new JSONObject() - .put("locations", correctTestLocations).put("maximum_search_radius", "300")), - //Check exception for missing profile - "json" is interpreted as profile - Arguments.of(INVALID_PARAMETER_VALUE, BAD_REQUEST, "json", null, new JSONObject() - .put("locations", correctTestLocations).put("maximum_search_radius", "300")), - //Check exception for missing profile - "json" is interpreted as profile - Arguments.of(UNSUPPORTED_EXPORT_FORMAT, SC_NOT_ACCEPTABLE, "badExportFormat", "driving-car", new JSONObject() - .put("locations", correctTestLocations).put("maximum_search_radius", "300")), - Arguments.of(INVALID_PARAMETER_FORMAT, BAD_REQUEST, "json", "driving-car", new JSONObject()), + Arguments.of(INVALID_PARAMETER_FORMAT, BAD_REQUEST, "driving-car", new JSONObject()), // Check exception for one fake location to ensure single locations are checked - Arguments.of(POINT_NOT_FOUND, NOT_FOUND, "json", "driving-car", new JSONObject() + Arguments.of(POINT_NOT_FOUND, NOT_FOUND, "driving-car", new JSONObject() .put("locations", oneFakeLocation)), // Check exception for ten fake locations to ensure multiple locations are checked - Arguments.of(POINT_NOT_FOUND, NOT_FOUND, "json", "driving-car", new JSONObject() + Arguments.of(POINT_NOT_FOUND, NOT_FOUND, "driving-car", new JSONObject() .put("locations", tenFakeLocations)), // Check exception for broken location to ensure invalid locations are checked - Arguments.of(INVALID_PARAMETER_FORMAT, BAD_REQUEST, "json", "driving-car", new JSONObject() + Arguments.of(INVALID_PARAMETER_FORMAT, BAD_REQUEST, "driving-car", new JSONObject() .put("locations", brokenFakeLocation)), // Check exception for wrong profile to ensure invalid profiles are checked - Arguments.of(INVALID_PARAMETER_VALUE, BAD_REQUEST, "json", "driving-foo", new JSONObject() + Arguments.of(INVALID_PARAMETER_VALUE, BAD_REQUEST, "driving-foo", new JSONObject() .put("locations", correctTestLocations)), // Check exception for unknown parameter to ensure unknown parameters are checked - Arguments.of(UNKNOWN_PARAMETER, BAD_REQUEST, "json", "driving-car", new JSONObject() + Arguments.of(UNKNOWN_PARAMETER, BAD_REQUEST, "driving-car", new JSONObject() .put("locations", correctTestLocations).put("unknown", "unknown")), // Check exception for invalid locations parameter (only one ccordinate) - Arguments.of(INVALID_PARAMETER_VALUE, BAD_REQUEST, "json", "driving-car", new JSONObject() + Arguments.of(INVALID_PARAMETER_VALUE, BAD_REQUEST, "driving-car", new JSONObject() .put("locations", invalidCoords).put("maximum_search_radius", "300")), // Check exception for invalid locations parameter (only one ccordinate) - Arguments.of(INVALID_PARAMETER_FORMAT, BAD_REQUEST, "json", "driving-car", new JSONObject() + Arguments.of(INVALID_PARAMETER_FORMAT, BAD_REQUEST, "driving-car", new JSONObject() .put("locations", "noJsonArray").put("maximum_search_radius", "300")), // Check exception for invalid maximum_search_radius - Arguments.of(INVALID_PARAMETER_FORMAT, BAD_REQUEST, "json", "driving-car", new JSONObject() + Arguments.of(INVALID_PARAMETER_FORMAT, BAD_REQUEST, "driving-car", new JSONObject() .put("locations", correctTestLocations).put("maximum_search_radius", "notANumber")), // Check exception for missing locations parameter - Arguments.of(INVALID_PARAMETER_FORMAT, BAD_REQUEST, "json", "driving-car", new JSONObject() + Arguments.of(INVALID_PARAMETER_FORMAT, BAD_REQUEST, "driving-car", new JSONObject() .put("maximum_search_radius", "300") )); } @@ -272,30 +317,84 @@ public static Stream snappingEndpointExceptionTestProvider() { */ @ParameterizedTest @MethodSource("snappingEndpointExceptionTestProvider") - void testSnappingExceptions(int expectedErrorCode, int expectedStatusCode, String endPoint, String profile, JSONObject body) { - - RequestSpecification requestSpecification = given() - .headers(jsonContent); - - if (profile != null) - requestSpecification = requestSpecification.pathParam("profile", profile); - - String url = getEndPointPath(); - if (StringUtils.isNotBlank(profile)) - url = url + "/{profile}"; + void testSnappingExceptionsJson(int expectedErrorCode, int expectedStatusCode, String profile, JSONObject body) { + doRequestAndExpectError(expectedErrorCode, expectedStatusCode, profile, "json", body); + } - if (StringUtils.isNotBlank(endPoint)) - url = url + "/" + endPoint; + /** + * Parameterized test method for testing various exception scenarios in the Snapping Endpoint. + * + * @param expectedErrorCode The expected error code for the test case (SnappingErrorCodes). + * @param expectedStatusCode The expected HTTP status code for the test case (StatusCode). + * @param profile The routing profile type (String). + * @param body The request body (JSONObject). + */ + @ParameterizedTest + @MethodSource("snappingEndpointExceptionTestProvider") + void testSnappingExceptionsGeojson(int expectedErrorCode, int expectedStatusCode, String profile, JSONObject body) { + doRequestAndExpectError(expectedErrorCode, expectedStatusCode, profile, "geojson", body); + } - requestSpecification + void doRequestAndExpectError(int expectedErrorCode, int expectedStatusCode, String profile, String endPoint, JSONObject body) { + given() + .headers(jsonContent) + .pathParam("profile", profile) .body(body.toString()) .when() .log().ifValidationFails() - .post(url) + .post(getEndPointPath() + "/{profile}/" + endPoint) .then() .log().ifValidationFails() .assertThat() .body("error.code", Matchers.is(expectedErrorCode)) .statusCode(expectedStatusCode); } + + @Test + void testMissingPathParametersProfileAndFormat() { + JSONObject body = validBody(); + + given() + .headers(jsonContent) + .body(body.toString()) + .when() + .log().ifValidationFails() + .post(getEndPointPath()) + .then() + .log().ifValidationFails() + .assertThat() + .body("error.code", Matchers.is(MISSING_PARAMETER)) + .statusCode(BAD_REQUEST); + } + + @Test + void testMissingPathParameterProfile() { + given() + .headers(jsonContent) + .body(validBody().toString()) + .when() + .log().ifValidationFails() + .post(getEndPointPath() + "/json") + .then() + .log().ifValidationFails() + .assertThat() + .body("error.code", Matchers.is(INVALID_PARAMETER_VALUE)) + .statusCode(BAD_REQUEST); + } + + @Test + void testBadExportFormat() { + given() + .headers(jsonContent) + .body(validBody().toString()) + .when() + .log().ifValidationFails() + .post(getEndPointPath() + "/driving-car/xml") + .then() + .log().ifValidationFails() + .assertThat() + .body("error.code", Matchers.is(UNSUPPORTED_EXPORT_FORMAT)) + .statusCode(SC_NOT_ACCEPTABLE); + } + } From cf3b6b0396a77c333c1cf5e29f4838791a370fc1 Mon Sep 17 00:00:00 2001 From: Jochen Haeussler Date: Thu, 5 Oct 2023 16:35:24 +0200 Subject: [PATCH 17/18] feat(snapping): add source_id to geojson response --- .../snapping/geojson/GeoJSONFeature.java | 4 +- .../geojson/GeoJSONFeatureProperties.java | 16 +- .../geojson/GeoJSONSnappingResponse.java | 25 ++- .../ors/apitests/snapping/ParamsTest.java | 164 +++++++++--------- 4 files changed, 110 insertions(+), 99 deletions(-) diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONFeature.java b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONFeature.java index 8e48ef6973..ef54eae38a 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONFeature.java +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONFeature.java @@ -13,9 +13,9 @@ public class GeoJSONFeature { @Schema(description = "Feature properties") public GeoJSONFeatureProperties props; - public GeoJSONFeature(JSON2DSources source) { + public GeoJSONFeature(int sourceId, JSON2DSources source) { this.geometry = new GeoJSONPointGeometry(source); - this.props = new GeoJSONFeatureProperties(source); + this.props = new GeoJSONFeatureProperties(sourceId, source); } @JsonProperty("geometry") diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONFeatureProperties.java b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONFeatureProperties.java index 7977bd4775..05e443b189 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONFeatureProperties.java +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONFeatureProperties.java @@ -19,7 +19,13 @@ public class GeoJSONFeatureProperties { @Schema(description = "Distance between the `source/destination` Location and the used point on the routing graph in meters.", example = "0.02") public double dist; - public GeoJSONFeatureProperties(JSON2DSources source) { + + @JsonProperty("source_id") + @Schema(description = "Index of the requested location") + public int sourceId; + + public GeoJSONFeatureProperties(int sourceId, JSON2DSources source) { + this.sourceId = sourceId; this.dist = source.getSnappedDistance(); this.name = source.getName(); } @@ -39,4 +45,12 @@ public double getDist() { public void setDist(double dist) { this.dist = dist; } + + public int getSourceId() { + return sourceId; + } + + public void setSourceId(int sourceId) { + this.sourceId = sourceId; + } } diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONSnappingResponse.java b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONSnappingResponse.java index e3d6651816..54f852cb83 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONSnappingResponse.java +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/snapping/geojson/GeoJSONSnappingResponse.java @@ -14,11 +14,11 @@ import org.heigit.ors.api.responses.routing.json.JSONBoundingBox; import org.heigit.ors.api.responses.snapping.SnappingResponse; import org.heigit.ors.api.responses.snapping.SnappingResponseInfo; +import org.heigit.ors.matrix.ResolvedLocation; import org.heigit.ors.snapping.SnappingResult; import org.heigit.ors.util.GeomUtility; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; @Schema(name = "GeoJSONSnappingResponse", description = "The GeoJSON Snapping Response contains the snapped coordinates in GeoJSON format.") @@ -50,18 +50,17 @@ public GeoJSONSnappingResponse(SnappingResult result, SnappingApiRequest request super(result); this.features = new ArrayList<>(); List bBoxes = new ArrayList<>(); - Arrays.stream(result.getLocations()) - .forEach(resolvedLocation -> { - if (resolvedLocation == null) { - this.features.add(null); - } else { - // create BBox for each point to use existing generateBoundingFromMultiple function - double x = resolvedLocation.getCoordinate().x; - double y = resolvedLocation.getCoordinate().y; - bBoxes.add(new BBox(x,x,y,y)); - this.features.add(new GeoJSONFeature(new JSON2DSources(resolvedLocation, true))); - } - }); + for (int sourceId = 0; sourceId < result.getLocations().length; sourceId++){ + ResolvedLocation resolvedLocation = result.getLocations()[sourceId]; + if (resolvedLocation != null) { + // create BBox for each point to use existing generateBoundingFromMultiple function + double x = resolvedLocation.getCoordinate().x; + double y = resolvedLocation.getCoordinate().y; + bBoxes.add(new BBox(x,x,y,y)); + this.features.add(new GeoJSONFeature(sourceId, new JSON2DSources(resolvedLocation, true))); + } + } + BBox[] boxes = bBoxes.toArray(new BBox[0]); if (boxes.length > 0) { this.bbox = new BoundingBoxBase(GeomUtility.generateBoundingFromMultiple(boxes)); diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java index 62e64c4f4a..1d47d399e8 100644 --- a/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java +++ b/ors-api/src/test/java/org/heigit/ors/apitests/snapping/ParamsTest.java @@ -14,16 +14,20 @@ import org.junit.jupiter.params.provider.MethodSource; import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.stream.Stream; import static io.restassured.RestAssured.given; import static jakarta.servlet.http.HttpServletResponse.SC_NOT_ACCEPTABLE; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.is; import static org.heigit.ors.apitests.utils.CommonHeaders.jsonContent; import static org.heigit.ors.common.StatusCode.BAD_REQUEST; import static org.heigit.ors.common.StatusCode.NOT_FOUND; import static org.heigit.ors.snapping.SnappingErrorCodes.*; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; @EndPointAnnotation(name = "snap") @VersionAnnotation(version = "v2") @@ -38,39 +42,48 @@ class ParamsTest extends ServiceTest { private static JSONArray fakeLocations(int maximumSize) { JSONArray overloadedLocations = new JSONArray(); for (int i = 0; i < maximumSize; i++) { - JSONArray location = new JSONArray(); - location.put(0.0); - location.put(0.0); - overloadedLocations.put(location); + overloadedLocations.put(invalidLocation()); } return overloadedLocations; } + private static JSONArray location2m() { + JSONArray coord2 = new JSONArray(); + coord2.put(8.687782); + coord2.put(49.424597); + return coord2; + } + + private static JSONArray location94m() { + JSONArray coord1 = new JSONArray(); + coord1.put(8.680916); + coord1.put(49.410973); + return coord1; + } + + private static JSONArray invalidLocation() { + JSONArray coord1 = new JSONArray(); + coord1.put(0.0); + coord1.put(0.0); + return coord1; + } + /** * Generates a JSONArray with valid locations for testing purposes. * * @return A JSONArray containing valid coordinates for testing. */ - private static JSONArray validLocations() { - // Create correct test locations with valid coordinates + private static JSONArray createLocations(JSONArray... locations) { JSONArray correctTestLocations = new JSONArray(); - JSONArray coord1 = new JSONArray(); - coord1.put(8.680916); - coord1.put(49.410973); - correctTestLocations.put(coord1); - JSONArray coord2 = new JSONArray(); - coord2.put(8.687782); - coord2.put(49.424597); - correctTestLocations.put(coord2); + correctTestLocations.putAll(locations); return correctTestLocations; } private static JSONObject validBody() { - JSONObject body = new JSONObject() - .put("locations", validLocations()) + return new JSONObject() + .put("locations", createLocations(location94m(), location2m())) .put("maximum_search_radius", "300"); - return body; } /** @@ -87,55 +100,56 @@ private static JSONObject validBody() { */ public static Stream snappingEndpointSuccessTestProvider() { return Stream.of( - Arguments.of(true, false, "driving-hgv", new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "-1")), - Arguments.of(true, false, "driving-hgv", new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "0")), - Arguments.of(true, false, "driving-hgv", new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "1")), - Arguments.of(false, true, "driving-hgv", new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "10")), - Arguments.of(false, false, "driving-hgv", new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "300")), - Arguments.of(false, false, "driving-hgv", new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "400")), - Arguments.of(false, false, "driving-hgv", new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "1000")), - Arguments.of(false, false, "driving-hgv", new JSONObject().put("locations", validLocations()).put("maximum_search_radius", "1000")) + Arguments.of(Arrays.asList(false, false), "driving-hgv", new JSONObject() + .put("locations", createLocations(location94m(), location2m())).put("maximum_search_radius", "-1")), + Arguments.of(Arrays.asList(false, false), "driving-hgv", new JSONObject() + .put("locations", createLocations(location94m(), location2m())).put("maximum_search_radius", "0")), + Arguments.of(Arrays.asList(false, false), "driving-hgv", new JSONObject() + .put("locations", createLocations(location94m(), location2m())).put("maximum_search_radius", "1")), + Arguments.of(Arrays.asList(false, true), "driving-hgv", new JSONObject() + .put("locations", createLocations(location94m(), location2m())).put("maximum_search_radius", "10")), + Arguments.of(Arrays.asList(true, true), "driving-hgv", new JSONObject() + .put("locations", createLocations(location94m(), location2m())).put("maximum_search_radius", "300")), + Arguments.of(Arrays.asList(true, false, true), "driving-hgv", new JSONObject() + .put("locations", createLocations(location2m(), location94m(), location2m())).put("maximum_search_radius", "10")), + Arguments.of(Arrays.asList(true, true), "driving-hgv", new JSONObject() + .put("locations", createLocations(location94m(), location2m())).put("maximum_search_radius", "400")), + Arguments.of(Arrays.asList(true, true), "driving-hgv", new JSONObject() + .put("locations", createLocations(location94m(), location2m())).put("maximum_search_radius", "1000")) ); } /** * Parameterized test method for testing various scenarios in the Snapping Endpoint. * - * @param expectEmptyResult Boolean flag indicating whether an empty result is expected. - * @param expectPartiallyEmptyResult Boolean flag indicating whether a partially empty result is expected. - * @param body The request body (JSONObject). - * @param profile The routing profile type (String). + * @param expectedSnapped Boolean flags indicating if the locations are expected to be snapped. + * @param body The request body (JSONObject). + * @param profile The routing profile type (String). */ @ParameterizedTest @MethodSource("snappingEndpointSuccessTestProvider") - void testSnappingSuccessJson(Boolean expectEmptyResult, Boolean expectPartiallyEmptyResult, String profile, JSONObject body) { + void testSnappingSuccessJson(List expectedSnapped, String profile, JSONObject body) { String endPoint = "json"; ValidatableResponse result = doRequestAndExceptSuccess(body, profile, endPoint); - validateJsonResponse(expectEmptyResult, expectPartiallyEmptyResult, result); + validateJsonResponse(expectedSnapped, result); } @Test void testMissingPathParameterFormat_defaultsToJson() { - ValidatableResponse result = doRequestAndExceptSuccess(validBody(), "driving-hgv", null); - validateJsonResponse(false, false, result); + JSONObject body = validBody(); + ValidatableResponse result = doRequestAndExceptSuccess(body, "driving-hgv", null); + validateJsonResponse(Arrays.asList(true, true), result); } - private static void validateJsonResponse(Boolean expectEmptyResult, Boolean expectPartiallyEmptyResult, ValidatableResponse result) { - boolean foundValidLocation = false; - boolean foundInvalidLocation = false; - + private static void validateJsonResponse(List expectedSnappedList, ValidatableResponse result) { result.body("any { it.key == 'locations' }", is(true)); - // Iterate over the locations array and check the types of the values - ArrayList locations = result.extract().jsonPath().get("locations"); - for (int i = 0; i < locations.size(); i++) { - // if empty result is expected, check if the locations array is empty - if (expectEmptyResult) { + // Iterate over the snappedLocations array and check the types of the values + ArrayList snappedLocations = result.extract().jsonPath().get("locations"); + for (int i = 0; i < snappedLocations.size(); i++) { + boolean expectedSnapped = expectedSnappedList.get(i); + if (!expectedSnapped) { assertNull(result.extract().jsonPath().get("locations[" + i + "].location[0]")); - foundValidLocation = true; - foundInvalidLocation = true; - } else if (expectPartiallyEmptyResult && !foundInvalidLocation && result.extract().jsonPath().get("locations[" + i + "]") == null) { - foundInvalidLocation = true; } else { // Type expectations assertEquals(Float.class, result.extract().jsonPath().get("locations[" + i + "].location[0]").getClass()); @@ -144,60 +158,44 @@ private static void validateJsonResponse(Boolean expectEmptyResult, Boolean expe // If name is in the response, check the type if (result.extract().jsonPath().get("locations[" + i + "].name") != null) assertEquals(String.class, result.extract().jsonPath().get("locations[" + i + "].name").getClass()); - foundValidLocation = true; } } - - assertTrue(foundValidLocation); - if (expectPartiallyEmptyResult) - assertTrue(foundInvalidLocation); } /** * Parameterized test method for testing various scenarios in the Snapping Endpoint. * - * @param expectEmptyResult Boolean flag indicating whether an empty result is expected. - * @param expectPartiallyEmptyResult Boolean flag indicating whether a partially empty result is expected. + * @param expectedSnapped Boolean flags indicating if the locations are expected to be snapped. * @param body The request body (JSONObject). * @param profile The routing profile type (String). */ @ParameterizedTest @MethodSource("snappingEndpointSuccessTestProvider") - void testSnappingSuccessGeojson(Boolean expectEmptyResult, Boolean expectPartiallyEmptyResult, String profile, JSONObject body) { + void testSnappingSuccessGeojson(List expectedSnapped, String profile, JSONObject body) { String endPoint = "geojson"; ValidatableResponse result = doRequestAndExceptSuccess(body, profile, endPoint); - boolean foundValidLocation = false; - boolean foundInvalidLocation = false; - result.body("any { it.key == 'features' }", is(true)); result.body("any { it.key == 'type' }", is(true)); - - // Iterate over the features array and check the types of the values - ArrayList features = result.extract().jsonPath().get("features"); - for (int i = 0; i < features.size(); i++) { - // if empty result is expected, check if the features array is empty - if (expectEmptyResult) { - assertNull(result.extract().jsonPath().get("features[" + i + "].features[0]")); - foundValidLocation = true; - foundInvalidLocation = true; - } else if (expectPartiallyEmptyResult && !foundInvalidLocation && result.extract().jsonPath().get("features[" + i + "]") == null) { - foundInvalidLocation = true; - } else { - // Type expectations - assertEquals(Float.class, result.extract().jsonPath().get("features[" + i + "].geometry.coordinates[0]").getClass()); - assertEquals(Float.class, result.extract().jsonPath().get("features[" + i + "].geometry.coordinates[1]").getClass()); - assertEquals(Float.class, result.extract().jsonPath().get("features[" + i + "].properties.snapped_distance").getClass()); - // If name is in the response, check the type - if (result.extract().jsonPath().get("features[" + i + "].properties.name") != null) - assertEquals(String.class, result.extract().jsonPath().get("features[" + i + "].properties.name").getClass()); - foundValidLocation = true; + List expectedSourceIds = new ArrayList<>(); + for (int i = 0; i < expectedSnapped.size(); i++) { + if (expectedSnapped.get(i)) { + expectedSourceIds.add(i); } } - assertTrue(foundValidLocation); - if (expectPartiallyEmptyResult) - assertTrue(foundInvalidLocation); + ArrayList features = result.extract().jsonPath().get("features"); + assertThat(features).hasSize(expectedSourceIds.size()); + for (int i = 0; i < features.size(); i++) { + assertEquals(Float.class, result.extract().jsonPath().get("features[" + i + "].geometry.coordinates[0]").getClass()); + assertEquals(Float.class, result.extract().jsonPath().get("features[" + i + "].geometry.coordinates[1]").getClass()); + assertEquals(Float.class, result.extract().jsonPath().get("features[" + i + "].properties.snapped_distance").getClass()); + assertEquals(Integer.class, result.extract().jsonPath().get("features[" + i + "].properties.source_id").getClass()); + assertEquals(result.extract().jsonPath().get("features[" + i + "].properties.source_id"), expectedSourceIds.get(i)); + // If name is in the response, check the type + if (result.extract().jsonPath().get("features[" + i + "].properties.name") != null) + assertEquals(String.class, result.extract().jsonPath().get("features[" + i + "].properties.name").getClass()); + } } private ValidatableResponse doRequestAndExceptSuccess(JSONObject body, String profile, String endPoint) { @@ -224,7 +222,7 @@ private ValidatableResponse doRequestAndExceptSuccess(JSONObject body, String pr result.body("metadata.containsKey('query')", is(true)); result.body("metadata.containsKey('engine')", is(true)); result.body("metadata.containsKey('system_message')", is(true)); - result.body("metadata.query.locations.size()", is(2)); + result.body("metadata.query.locations.size()", is(((JSONArray) body.get("locations")).toList().size())); result.body("metadata.query.locations[0].size()", is(2)); result.body("metadata.query.locations[1].size()", is(2)); result.body("metadata.query.profile", is(profile)); @@ -273,7 +271,7 @@ public static Stream snappingEndpointExceptionTestProvider() { invalidCoord1.put(8.680916); invalidCoords.put(invalidCoord1); - JSONArray correctTestLocations = validLocations(); + JSONArray correctTestLocations = createLocations(location94m(), location2m()); // Return a stream of test arguments return Stream.of( Arguments.of(INVALID_PARAMETER_FORMAT, BAD_REQUEST, "driving-car", new JSONObject()), From efb7f761ad092b4a72aa39b9f4a56fc837b80765 Mon Sep 17 00:00:00 2001 From: Amandus Butzer Date: Fri, 6 Oct 2023 14:38:49 +0200 Subject: [PATCH 18/18] docs: add attribution description & fix table --- docs/installation/Configuration.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/installation/Configuration.md b/docs/installation/Configuration.md index 89a9327cc3..def583a656 100644 --- a/docs/installation/Configuration.md +++ b/docs/installation/Configuration.md @@ -62,11 +62,12 @@ descriptions of each block follows below. ### Properties in the `messages` block The messages property expects a list of elements where each has the following: -| key | type | description | example value | + +| key | type | description | example value | |-----------|---------|-------------------------------------------------------------------|----------------------| -| active | boolean | Enables or disables this message | `true` | -| text | string | The message text | `"The message text"` | -| condition | list | omittable; may contain any of the conditions from the table below | | +| active | boolean | Enables or disables this message | `true` | +| text | string | The message text | `"The message text"` | +| condition | list | omittable; may contain any of the conditions from the table below | | | condition | value | description | |--------------------|----------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------| @@ -141,7 +142,7 @@ The top level element. | routing_name | string | Specifies the gpx `name` tag that is returned in a gpx response | `"openrouteservice"` | | sources | list | the osm file to be used, formats supported are `.osm`, `.osm.gz`, `.osm.zip` and `.pbf` | `["heidelberg.osm.gz"]` | | init_threads | number | The number of threads used to initialize (build/load) graphs. Higher numbers requires more RAM. | `2` | -| attribution | string | | `"openrouteservice.org, OpenStreetMap contributors"` | +| attribution | string | Attribution added to the response metadata | `"openrouteservice.org, OpenStreetMap contributors"` | | elevation_preprocessed | boolean | Enables or disables reading ele tags for nodes. Default value is false. If enabled, GH's elevation lookup is prevented and all nodes without ele tag will default to 0. Experimental, for use with the ORS preprocessor | `false` | | profiles | object | | [profiles](#orsservicesroutingprofiles) | @@ -359,7 +360,7 @@ The top level element. | maximum_search_radius | number | Maximum allowed distance between the requested coordinate and a point on the nearest road. The value is measured in meters | `5000` | | maximum_visited_nodes | number | Maximum allowed number of visited nodes in shortest path computation. This threshold is applied only for Dijkstra algorithm | `100000` | | allow_resolve_locations | number | Specifies whether the name of a nearest street to the location can be resolved or not. Default value is true | `true` | -| attribution | string | Specifies whether the name of a nearest street to the location can be resolved or not. Default value is true | `"openrouteservice.org, OpenStreetMap contributors"` | +| attribution | string | Attribution added to the response metadata | `"openrouteservice.org, OpenStreetMap contributors"` | --- #### ors.services.snap