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..83652a90c1 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) { @@ -210,10 +208,10 @@ private ValidatableResponse doRequestAndExceptSuccess(JSONObject body, String pr .pathParam("profile", profile) .body(body.toString()) .when() - .log().ifValidationFails() + .log().all() .post(url) .then() - .log().ifValidationFails() + .log().all() .statusCode(200); // Check if the response contains the expected keys @@ -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()),