diff --git a/CHANGELOG.md b/CHANGELOG.md index 32ecf18457..39a3a12dae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ RELEASING: ## [unreleased] ### Added +- TopoJSON graph export ([#1926](https://github.com/GIScience/openrouteservice/pull/1926)) ### Changed diff --git a/ors-api/src/main/java/org/heigit/ors/api/APIEnums.java b/ors-api/src/main/java/org/heigit/ors/api/APIEnums.java index 423793786a..8e6dd99686 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/APIEnums.java +++ b/ors-api/src/main/java/org/heigit/ors/api/APIEnums.java @@ -151,7 +151,8 @@ public String toString() { @Schema(name = "Export response type", description = "Format of the export response.") public enum ExportResponseType { - JSON("json"); + JSON("json"), + TOPOJSON("topojson"); private final String value; diff --git a/ors-api/src/main/java/org/heigit/ors/api/controllers/ExportAPI.java b/ors-api/src/main/java/org/heigit/ors/api/controllers/ExportAPI.java index f9799ac8c3..3be0695b18 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/controllers/ExportAPI.java +++ b/ors-api/src/main/java/org/heigit/ors/api/controllers/ExportAPI.java @@ -30,6 +30,7 @@ import org.heigit.ors.api.errors.CommonResponseEntityExceptionHandler; import org.heigit.ors.api.requests.export.ExportApiRequest; import org.heigit.ors.api.responses.export.json.JsonExportResponse; +import org.heigit.ors.api.responses.export.topojson.TopoJsonExportResponse; import org.heigit.ors.api.services.ExportService; import org.heigit.ors.common.EncoderNameEnum; import org.heigit.ors.exceptions.*; @@ -73,13 +74,6 @@ public String getPostMapping(@RequestBody ExportApiRequest request) throws Missi throw new MissingParameterException(ExportErrorCodes.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, ExportErrorCodes.UNSUPPORTED_EXPORT_FORMAT, "This response format is not supported"); - } - // Functional request methods @PostMapping(value = "/{profile}") @Operation( @@ -104,7 +98,7 @@ public JsonExportResponse getDefault(@Parameter(description = "Specifies the rou @PostMapping(value = "/{profile}/json", produces = {"application/json;charset=UTF-8"}) @Operation( - description = "Returns a list of points, edges and weights within a given bounding box for a selected profile JSON.", + description = "Returns a list of points, edges and weights within a given bounding box for a selected profile as JSON.", summary = "Export Service JSON" ) @ApiResponse( @@ -127,6 +121,38 @@ public JsonExportResponse getJsonExport( return new JsonExportResponse(result); } + @PostMapping(value = "/{profile}/topojson", produces = {"application/json;charset=UTF-8"}) + @Operation( + description = "Returns a list of edges, edge properties, and their topology within a given bounding box for a selected profile.", + summary = "Export Service TopoJSON" + ) + @ApiResponse( + responseCode = "200", + description = "TopoJSON Response.", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = TopoJsonExportResponse.class) + ) + }) + public TopoJsonExportResponse getTopoJsonExport( + @Parameter(description = "Specifies the profile.", required = true, example = "driving-car") @PathVariable String profile, + @Parameter(description = "The request payload", required = true) @RequestBody ExportApiRequest request) throws StatusCodeException { + request.setProfile(getProfileEnum(profile)); + request.setProfileName(profile); + request.setResponseType(APIEnums.ExportResponseType.TOPOJSON); + + ExportResult result = exportService.generateExportFromRequest(request); + + return TopoJsonExportResponse.fromExportResult(result); + } + + // Matches any response type that has not been defined + @PostMapping(value = "/{profile}/{responseType}") + @Operation(hidden = true) + public void getInvalidResponseType(@PathVariable String profile, @PathVariable String responseType) throws StatusCodeException { + throw new StatusCodeException(HttpServletResponse.SC_NOT_ACCEPTABLE, ExportErrorCodes.UNSUPPORTED_EXPORT_FORMAT, "The response format %s is not supported".formatted(responseType)); + } + @ExceptionHandler(MissingServletRequestParameterException.class) public ResponseEntity handleMissingParams(final MissingServletRequestParameterException e) { return errorHandler.handleStatusCodeException(new MissingParameterException(ExportErrorCodes.MISSING_PARAMETER, e.getParameterName())); diff --git a/ors-api/src/main/java/org/heigit/ors/api/requests/common/APIRequest.java b/ors-api/src/main/java/org/heigit/ors/api/requests/common/APIRequest.java index f16e49f9dd..53c07cab34 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/requests/common/APIRequest.java +++ b/ors-api/src/main/java/org/heigit/ors/api/requests/common/APIRequest.java @@ -8,6 +8,7 @@ public class APIRequest { public static final String PARAM_ID = "id"; public static final String PARAM_PROFILE = "profile"; + public static final String PARAM_PROFILE_NAME = "profile_name"; @Schema(name = PARAM_ID, description = "Arbitrary identification string of the request reflected in the meta information.", example = "my_request") @@ -19,7 +20,7 @@ public class APIRequest { @Schema(name = PARAM_PROFILE, hidden = true) protected APIEnums.Profile profile; - + @Schema(name = PARAM_PROFILE_NAME, hidden = true) protected String profileName; public boolean hasId() { diff --git a/ors-api/src/main/java/org/heigit/ors/api/requests/export/ExportApiRequest.java b/ors-api/src/main/java/org/heigit/ors/api/requests/export/ExportApiRequest.java index f5317cad38..9a22282edf 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/requests/export/ExportApiRequest.java +++ b/ors-api/src/main/java/org/heigit/ors/api/requests/export/ExportApiRequest.java @@ -5,8 +5,8 @@ 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.api.APIEnums; +import org.heigit.ors.api.requests.common.APIRequest; import java.util.List; @@ -17,8 +17,8 @@ public class ExportApiRequest extends APIRequest { public static final String PARAM_BBOX = "bbox"; public static final String PARAM_PROFILE = "profile"; public static final String PARAM_FORMAT = "format"; - - public static final String PARAM_DEBUG = "debug"; + public static final String PARAM_GEOMETRY = "geometry"; + public static final String PARAM_ADDITIONAL_INFO = "additional_info"; @Schema(name = PARAM_ID, description = "Arbitrary identification string of the request reflected in the meta information.", example = "export_request") @@ -40,9 +40,13 @@ public class ExportApiRequest extends APIRequest { @JsonProperty(PARAM_FORMAT) private APIEnums.ExportResponseType responseType = APIEnums.ExportResponseType.JSON; - @Schema(name = PARAM_DEBUG, hidden = true) - @JsonProperty(PARAM_DEBUG) - private boolean debug; + @Schema(name = PARAM_GEOMETRY, description = "Whether to return the exact geometry of the graph or a simplified (beeline) representation.", example = "true", defaultValue = "true") + @JsonProperty(PARAM_GEOMETRY) + private boolean geometry = true; + + @Schema(name = PARAM_ADDITIONAL_INFO, hidden = true) + @JsonProperty(PARAM_ADDITIONAL_INFO) + private boolean additionalInfo; @JsonCreator public ExportApiRequest(@JsonProperty(value = PARAM_BBOX, required = true) List> bbox) { @@ -62,8 +66,8 @@ public void setId(String id) { this.hasId = true; } - public boolean debug() { - return debug; + public boolean additionalInfo() { + return additionalInfo; } public List> getBbox() { @@ -86,4 +90,11 @@ public void setResponseType(APIEnums.ExportResponseType responseType) { this.responseType = responseType; } + public APIEnums.ExportResponseType getResponseType() { + return responseType; + } + + public boolean getGeometry() { + return geometry; + } } diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/export/json/JsonExportResponse.java b/ors-api/src/main/java/org/heigit/ors/api/responses/export/json/JsonExportResponse.java index aac5cf6355..880fba5db4 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/responses/export/json/JsonExportResponse.java +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/export/json/JsonExportResponse.java @@ -46,7 +46,7 @@ public JsonExportResponse(ExportResult exportResult) { nodesCount = nodes.stream().count(); edges = new ArrayList<>(); - for (Map.Entry, Double> edgeWeight : exportResult.getEdgeWeigths().entrySet()) { + for (Map.Entry, Double> edgeWeight : exportResult.getEdgeWeights().entrySet()) { edges.add(new JsonEdge(edgeWeight)); } edgesCount = edges.stream().count(); diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/export/topojson/Arc.java b/ors-api/src/main/java/org/heigit/ors/api/responses/export/topojson/Arc.java new file mode 100644 index 0000000000..09f8df93ee --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/export/topojson/Arc.java @@ -0,0 +1,21 @@ +package org.heigit.ors.api.responses.export.topojson; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.List; + +@Builder +@Getter +@Setter +public class Arc implements Serializable { + private List> coordinates; + + @JsonValue + public List> getCoordinates() { + return coordinates; + } +} diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/export/topojson/Geometry.java b/ors-api/src/main/java/org/heigit/ors/api/responses/export/topojson/Geometry.java new file mode 100644 index 0000000000..61cc0f9555 --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/export/topojson/Geometry.java @@ -0,0 +1,23 @@ +package org.heigit.ors.api.responses.export.topojson; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.Builder; +import lombok.Getter; + +import java.io.Serializable; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({"type", "properties", "arcs"}) +@Getter +@Builder +public class Geometry implements Serializable { + @JsonProperty("type") + private String type; + @JsonProperty("properties") + private transient Properties properties; + @JsonProperty("arcs") + private List arcs; +} diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/export/topojson/Network.java b/ors-api/src/main/java/org/heigit/ors/api/responses/export/topojson/Network.java new file mode 100644 index 0000000000..ae0ea659f9 --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/export/topojson/Network.java @@ -0,0 +1,21 @@ +package org.heigit.ors.api.responses.export.topojson; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.Builder; +import lombok.Getter; + +import java.io.Serializable; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({"type", "geometries"}) +@Getter +@Builder +public class Network implements Serializable { + @JsonProperty("type") + private String type; + @JsonProperty("geometries") + private List geometries; +} diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/export/topojson/Objects.java b/ors-api/src/main/java/org/heigit/ors/api/responses/export/topojson/Objects.java new file mode 100644 index 0000000000..e7d79d78e0 --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/export/topojson/Objects.java @@ -0,0 +1,16 @@ +package org.heigit.ors.api.responses.export.topojson; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; + +import java.io.Serializable; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Getter +@Builder +public class Objects implements Serializable { + @JsonProperty("network") + private Network network; +} diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/export/topojson/Properties.java b/ors-api/src/main/java/org/heigit/ors/api/responses/export/topojson/Properties.java new file mode 100644 index 0000000000..8b7fb067da --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/export/topojson/Properties.java @@ -0,0 +1,31 @@ +package org.heigit.ors.api.responses.export.topojson; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; + +import java.io.Serializable; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Getter +@Builder +public class Properties implements Serializable { + @JsonProperty("weight") + private Double weight; + @JsonProperty("osm_id") + private Long osmId; + @JsonProperty("both_directions") + private Boolean bothDirections; + @JsonProperty("speed") + private Double speed; + @JsonProperty("speed_reverse") + private Double speedReverse; + @JsonProperty("ors_ids") + private List orsIds; + @JsonProperty("ors_nodes") + private List orsNodes; + @JsonProperty("distances") + private List distances; +} diff --git a/ors-api/src/main/java/org/heigit/ors/api/responses/export/topojson/TopoJsonExportResponse.java b/ors-api/src/main/java/org/heigit/ors/api/responses/export/topojson/TopoJsonExportResponse.java new file mode 100644 index 0000000000..74bfd25ed9 --- /dev/null +++ b/ors-api/src/main/java/org/heigit/ors/api/responses/export/topojson/TopoJsonExportResponse.java @@ -0,0 +1,143 @@ +package org.heigit.ors.api.responses.export.topojson; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.Builder; +import lombok.Getter; +import org.heigit.ors.common.Pair; +import org.heigit.ors.export.ExportResult; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import static org.heigit.ors.export.ExportResult.TopoGeometry; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({"type", "transform", "objects", "arcs", "bbox"}) +@Getter +@Builder +public class TopoJsonExportResponse implements Serializable { + @JsonProperty(value = "type") + @Builder.Default + private String type = "Topology"; + @JsonProperty("objects") + @Builder.Default + private Objects objects = null; + @JsonProperty("arcs") + @Builder.Default + private List arcs = new LinkedList<>(); + @JsonProperty("bbox") + @Builder.Default + private List bbox = new ArrayList<>(4); + + public static TopoJsonExportResponse fromExportResult(ExportResult exportResult) { + BBox bbox = new BBox(); + LinkedList geometries = new LinkedList<>(); + LinkedList arcsLocal = new LinkedList<>(); + if (exportResult.hasTopoGeometries()) { + buildGeometriesFromTopoGeometries(exportResult, bbox, geometries, arcsLocal); + } else { + buildGeometriesFromEdges(exportResult, bbox, geometries, arcsLocal); + } + return TopoJsonExportResponse.builder() + .type("Topology") + .objects(Objects.builder().network(Network.builder() + .type("GeometryCollection") + .geometries(geometries) + .build()).build()) + .arcs(arcsLocal) + .bbox(bbox.toList()) + .build(); + } + + private static void buildGeometriesFromEdges(ExportResult exportResult, BBox bbox, LinkedList geometries, LinkedList arcsLocal) { + Map, Double> edgeWeights = exportResult.getEdgeWeights(); + Map, LineString> edgeGeometries = exportResult.getEdgeGeometries(); + int arcCount = 0; + for (Map.Entry, Double> edgeWeight : edgeWeights.entrySet()) { + arcsLocal.add(Arc.builder().coordinates(makeCoordinateList(edgeGeometries.get(edgeWeight.getKey()), bbox)).build()); + List arcList = List.of(arcCount); + arcCount++; + + Properties properties = Properties.builder() + .weight(edgeWeight.getValue()) + .build(); + Geometry geometry = Geometry.builder() + .type("LineString") + .properties(properties) + .arcs(arcList) + .build(); + geometries.add(geometry); + } + } + + private static void buildGeometriesFromTopoGeometries(ExportResult exportResult, BBox bbox, List geometries, List arcsLocal) { + Map topoGeometries = exportResult.getTopoGeometries(); + int arcCount = 0; + for (long osmId : topoGeometries.keySet()) { + TopoGeometry topoGeometry = topoGeometries.get(osmId); + Map arcs = topoGeometry.getArcs(); + List orsIdList = arcs.keySet().stream().sorted().toList(); + List arcList = new LinkedList<>(); + List nodeList = new LinkedList<>(); + List distanceList = new LinkedList<>(); + for (int orsId : orsIdList) { + ExportResult.TopoArc currentArc = arcs.get(orsId); + arcsLocal.add(Arc.builder().coordinates(makeCoordinateList(currentArc.geometry(), bbox)).build()); + arcList.add(arcCount); + if (nodeList.isEmpty()) { + nodeList.add(currentArc.from()); + } + nodeList.add(currentArc.to()); + distanceList.add(currentArc.length()); + arcCount++; + } + + Properties properties = Properties.builder() + .osmId(osmId) + .bothDirections(topoGeometry.isBothDirections()) + .speed(topoGeometry.getSpeed()) + .speedReverse(topoGeometry.isBothDirections() ? topoGeometry.getSpeedReverse() : null) + .orsIds(orsIdList) + .orsNodes(nodeList) + .distances(distanceList) + .build(); + Geometry geometry = Geometry.builder() + .type("LineString") + .properties(properties) + .arcs(arcList) + .build(); + geometries.add(geometry); + } + } + + private static List> makeCoordinateList(LineString geometry, BBox bbox) { + List> coordinates = new LinkedList<>(); + for (Coordinate coordinate : geometry.getCoordinates()) { + coordinates.add(List.of(coordinate.x, coordinate.y)); + bbox.update(coordinate.x, coordinate.y); + } + return coordinates; + } + + private static class BBox { + private double[] coords = {Double.MAX_VALUE, Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE}; + + public void update(double x, double y) { + coords[0] = Math.min(coords[0], x); + coords[1] = Math.min(coords[1], y); + coords[2] = Math.max(coords[2], x); + coords[3] = Math.max(coords[3], y); + } + + public List toList() { + return List.of(coords[0], coords[1], coords[2], coords[3]); + } + } +} diff --git a/ors-api/src/main/java/org/heigit/ors/api/services/ExportService.java b/ors-api/src/main/java/org/heigit/ors/api/services/ExportService.java index e5f5edcea3..e47cfd9fd1 100644 --- a/ors-api/src/main/java/org/heigit/ors/api/services/ExportService.java +++ b/ors-api/src/main/java/org/heigit/ors/api/services/ExportService.java @@ -2,6 +2,7 @@ import com.google.common.primitives.Doubles; import com.graphhopper.util.shapes.BBox; +import org.heigit.ors.api.APIEnums; import org.heigit.ors.api.config.ApiEngineProperties; import org.heigit.ors.api.config.EndpointsProperties; import org.heigit.ors.api.requests.export.ExportApiRequest; @@ -58,7 +59,9 @@ private org.heigit.ors.export.ExportRequest convertExportRequest(ExportApiReques } exportRequest.setBoundingBox(convertBBox(exportApiRequest.getBbox())); - exportRequest.setDebug(exportApiRequest.debug()); + exportRequest.setAdditionalEdgeInfo(exportApiRequest.additionalInfo()); + exportRequest.setTopoJson(exportApiRequest.getResponseType().equals(APIEnums.ExportResponseType.TOPOJSON)); + exportRequest.setUseRealGeometry(exportApiRequest.getGeometry()); return exportRequest; } diff --git a/ors-api/src/test/java/org/heigit/ors/api/responses/export/topojson/TopoJsonExportResponseTest.java b/ors-api/src/test/java/org/heigit/ors/api/responses/export/topojson/TopoJsonExportResponseTest.java new file mode 100644 index 0000000000..436f9d7c23 --- /dev/null +++ b/ors-api/src/test/java/org/heigit/ors/api/responses/export/topojson/TopoJsonExportResponseTest.java @@ -0,0 +1,174 @@ +package org.heigit.ors.api.responses.export.topojson; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.heigit.ors.common.Pair; +import org.heigit.ors.export.ExportResult; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class TopoJsonExportResponseTest { + + TopoJsonExportResponse topoJsonExportResponse; + String topologyLayerName = "network"; + private static final GeometryFactory geometryFactory = new GeometryFactory(); + + // setup function + @BeforeEach + void setUp() { + Geometry geometry1 = Geometry.builder() + .type("LineString") + .arcs(List.of(0, 1)) + .properties(Properties.builder().osmId(41106L).build()) + .build(); + Geometry geometry2 = Geometry.builder() + .type("LineString") + .arcs(List.of(2)) + .properties(Properties.builder().osmId(41107L).build()) + .build(); + Network network = Network.builder() + .type("GeometryCollection") + .geometries(List.of(geometry1, geometry2)) + .build(); + Arc arc1 = Arc.builder() + .coordinates(List.of( + List.of(-72.816497, 19.948588), + List.of(-72.816354, 19.948234), + List.of(-72.816335, 19.948205), + List.of(-72.816297, 19.948193), + List.of(-72.816213, 19.948215), + List.of(-72.816098, 19.948245), + List.of(-72.816021, 19.948248) + )) + .build(); + Arc arc2 = Arc.builder() + .coordinates(List.of( + List.of(-72.816021, 19.948248), + List.of(-72.815938, 19.948247), + List.of(-72.815861, 19.948224), + List.of(-72.815746, 19.948186), + List.of(-72.815574, 19.948146) + )) + .build(); + Arc arc3 = Arc.builder() + .coordinates(List.of( + List.of(-72.816021, 19.948248), + List.of(-72.815574, 19.948146) + )) + .build(); + topoJsonExportResponse = TopoJsonExportResponse.builder() + .type("Topology") + .objects(Objects.builder().network(network).build()) + .bbox(List.of(-72.822573, 19.947123, -72.81259, 19.952703)) + .arcs(List.of(arc1, arc2, arc3)) + .build(); + + } + + @Test + void testTopoJsonSerialization() throws JsonProcessingException { + // Serialization with jackson + ObjectMapper objectMapper = new ObjectMapper(); + + String jsonString = objectMapper.writeValueAsString(topoJsonExportResponse); + + // Test the serialization + JsonNode jsonNode = objectMapper.readTree(jsonString); + Assertions.assertEquals(jsonNode.get("type").asText(), topoJsonExportResponse.getType()); + for (int i = 0; i < topoJsonExportResponse.getBbox().size(); i++) { + Assertions.assertEquals(jsonNode.get("bbox").get(i).asDouble(), topoJsonExportResponse.getBbox().get(i)); + } + for (int i = 0; i < topoJsonExportResponse.getArcs().size(); i++) { + for (int j = 0; j < topoJsonExportResponse.getArcs().get(i).getCoordinates().size(); j++) { + for (int k = 0; k < topoJsonExportResponse.getArcs().get(i).getCoordinates().get(j).size(); k++) { + Assertions.assertEquals(jsonNode.get("arcs").get(i).get(j).get(k).asDouble(), topoJsonExportResponse.getArcs().get(i).getCoordinates().get(j).get(k)); + } + } + } + Assertions.assertEquals(1, jsonNode.get("objects").size()); + Assertions.assertEquals(jsonNode.get("objects").get("network").get("type").asText(), topoJsonExportResponse.getObjects().getNetwork().getType()); + for (int j = 0; j < topoJsonExportResponse.getObjects().getNetwork().getGeometries().size(); j++) { + Assertions.assertEquals(jsonNode.get("objects").get("network").get("geometries").get(j).get("type").asText(), topoJsonExportResponse.getObjects().getNetwork().getGeometries().get(j).getType()); + for (int k = 0; k < topoJsonExportResponse.getObjects().getNetwork().getGeometries().get(j).getArcs().size(); k++) { + Assertions.assertEquals(jsonNode.get("objects").get("network").get("geometries").get(j).get("arcs").get(k).asInt(), topoJsonExportResponse.getObjects().getNetwork().getGeometries().get(j).getArcs().get(k)); + } + Assertions.assertEquals(jsonNode.get("objects").get("network").get("geometries").get(j).get("properties").get("osm_id").asText(), topoJsonExportResponse.getObjects().getNetwork().getGeometries().get(j).getProperties().getOsmId().toString()); + } + } + + @Test + void testEmptyTopoJsonObjectSerialization() throws JsonProcessingException { + TopoJsonExportResponse emptyTopoJsonExportResponse = TopoJsonExportResponse.builder().build(); + ObjectMapper objectMapper = new ObjectMapper(); + String jsonString = objectMapper.writeValueAsString(emptyTopoJsonExportResponse); + JsonNode jsonNode = objectMapper.readTree(jsonString); + Assertions.assertEquals("Topology", emptyTopoJsonExportResponse.getType()); + Assertions.assertEquals(0, jsonNode.get("bbox").size()); + Assertions.assertEquals(0, jsonNode.get("arcs").size()); + } + + @Test + void testFromExportResult() { + ExportResult exportResult = new ExportResult(); + exportResult.addLocation(0, new Coordinate(0.0, 0.0)); + exportResult.addLocation(1, new Coordinate(1.0, 1.0)); + exportResult.addLocation(2, new Coordinate(2.0, 2.0)); + exportResult.addLocation(3, new Coordinate(3.0, 3.0)); + exportResult.addLocation(4, new Coordinate(4.0, 4.0)); + + exportResult.addEdge(new Pair<>(0, 1), 1.0); + exportResult.addEdge(new Pair<>(1, 2), 2.0); + exportResult.addEdge(new Pair<>(2, 3), 2.0); + exportResult.addEdgeExtra(new Pair<>(0, 1), new HashMap<>(Map.of("osm_id", 1L, "foo", "baz"))); + exportResult.addEdgeExtra(new Pair<>(1, 2), new HashMap<>(Map.of("osm_id", 1L, "foo", "bar"))); + exportResult.addEdgeExtra(new Pair<>(2, 3), new HashMap<>(Map.of("osm_id", 1L, "foo", "bla"))); + exportResult.addEdgeExtra(new Pair<>(0, 4), new HashMap<>(Map.of("osm_id", 2L, "foo", "bub"))); + + ExportResult.TopoGeometry topoGeometry1 = new ExportResult.TopoGeometry(1.0F, 1.0F); + topoGeometry1.getArcs().put(1, new ExportResult.TopoArc(geometryFactory.createLineString(new Coordinate[]{new Coordinate(0.0, 0.0), new Coordinate(1.0, 1.0)}), 1.0, 0, 1)); + topoGeometry1.getArcs().put(2, new ExportResult.TopoArc(geometryFactory.createLineString(new Coordinate[]{new Coordinate(1.0, 1.0), new Coordinate(2.0, 2.0)}), 2.0, 1, 2)); + topoGeometry1.getArcs().put(3, new ExportResult.TopoArc(geometryFactory.createLineString(new Coordinate[]{new Coordinate(2.0, 2.0), new Coordinate(3.0, 3.0)}), 3.0, 2, 3)); + topoGeometry1.setBothDirections(true); + exportResult.getTopoGeometries().put(1L, topoGeometry1); + ExportResult.TopoGeometry topoGeometry2 = new ExportResult.TopoGeometry(2.0F, 2.0F); + topoGeometry2.getArcs().put(4, new ExportResult.TopoArc(geometryFactory.createLineString(new Coordinate[]{new Coordinate(0.0, 0.0), new Coordinate(4.0, 5.0)}), 4.0, 0, 4)); + exportResult.getTopoGeometries().put(2L, topoGeometry2); + + TopoJsonExportResponse exportResultToTopoJson = TopoJsonExportResponse.fromExportResult(exportResult); + Network network = exportResultToTopoJson.getObjects().getNetwork(); + Assertions.assertEquals("GeometryCollection", network.getType()); + Assertions.assertEquals(2, network.getGeometries().size()); + Geometry geometry1 = network.getGeometries().get(0); + Assertions.assertEquals("LineString", geometry1.getType()); + Assertions.assertEquals(List.of(0, 1, 2), geometry1.getArcs()); + Assertions.assertEquals(1.0, geometry1.getProperties().getSpeed()); + Assertions.assertEquals(1.0, geometry1.getProperties().getSpeedReverse()); + Assertions.assertEquals(1L, geometry1.getProperties().getOsmId()); + Geometry geometry2 = network.getGeometries().get(1); + Assertions.assertEquals("LineString", geometry2.getType()); + Assertions.assertEquals(List.of(3), geometry2.getArcs()); + Assertions.assertEquals(2.0, geometry2.getProperties().getSpeed()); + Assertions.assertNull(geometry2.getProperties().getSpeedReverse()); + Assertions.assertEquals(2L, geometry2.getProperties().getOsmId()); + List arcs = exportResultToTopoJson.getArcs(); + Assertions.assertEquals(4, arcs.size()); + Assertions.assertEquals(List.of(List.of(0.0, 0.0), List.of(1.0, 1.0)), arcs.get(0).getCoordinates()); + Assertions.assertEquals(List.of(List.of(1.0, 1.0), List.of(2.0, 2.0)), arcs.get(1).getCoordinates()); + Assertions.assertEquals(List.of(List.of(2.0, 2.0), List.of(3.0, 3.0)), arcs.get(2).getCoordinates()); + Assertions.assertEquals(List.of(List.of(0.0, 0.0), List.of(4.0, 5.0)), arcs.get(3).getCoordinates()); + List bbox = exportResultToTopoJson.getBbox(); + Assertions.assertEquals(4, bbox.size()); + Assertions.assertEquals(0.0, bbox.get(0)); + Assertions.assertEquals(0.0, bbox.get(1)); + Assertions.assertEquals(4.0, bbox.get(2)); + Assertions.assertEquals(5.0, bbox.get(3)); + } +} \ No newline at end of file diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/export/ParamsTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/export/ParamsTest.java new file mode 100644 index 0000000000..eb0c5368e0 --- /dev/null +++ b/ors-api/src/test/java/org/heigit/ors/apitests/export/ParamsTest.java @@ -0,0 +1,273 @@ +package org.heigit.ors.apitests.export; + +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.heigit.ors.export.ExportErrorCodes; +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 = "export") +@VersionAnnotation(version = "v2") +class ParamsTest extends ServiceTest { + public ParamsTest() { + JSONArray coord1 = new JSONArray(); + coord1.put(0.001); + coord1.put(0.001); + JSONArray coord2 = new JSONArray(); + coord2.put(8.681495); + coord2.put(49.41461); + JSONArray coord3 = new JSONArray(); + coord3.put(8.686507); + coord3.put(49.41943); + + JSONArray bboxMock = new JSONArray(); + bboxMock.put(coord1); + bboxMock.put(coord1); + + JSONArray bboxFaulty = new JSONArray(); + bboxFaulty.put(coord1); + + JSONArray bboxProper = new JSONArray(); + bboxProper.put(coord2) + .put(coord3); + + addParameter("bboxFake", bboxMock); + addParameter("bboxFaulty", bboxFaulty); + addParameter("bboxProper", bboxProper); + + JSONArray coord4 = new JSONArray(); + coord4.put(8.6280012); + coord4.put(49.3669484); + JSONArray coord5 = new JSONArray(); + coord5.put(8.6391592); + coord5.put(49.3732920); + JSONArray bboxCar = new JSONArray(); + bboxCar.put(coord4); + bboxCar.put(coord5); + addParameter("bboxCar", bboxCar); + } + + @Test + void basicPingTest() { + JSONObject body = new JSONObject(); + body.put("bbox", getParameter("bboxFake")); + given() + .headers(jsonContent) + .pathParam("profile", "driving-car") + .body(body.toString()) + .when() + .post(getEndPointPath() + "/{profile}/topojson") + .then() + .statusCode(200); + } + + @Test + void expectUnknownProfile() { + JSONObject body = new JSONObject(); + body.put("bbox", getParameter("bboxFake")); + given() + .headers(jsonContent) + .pathParam("profile", "driving-car-123") + .body(body.toString()) + .when() + .post(getEndPointPath() + "/{profile}/json") + .then() + .assertThat() + .body("error.code", Matchers.is(ExportErrorCodes.INVALID_PARAMETER_VALUE)) + .statusCode(400); + } + + @Test + void expectInvalidResponseFormat() { + JSONObject body = new JSONObject(); + body.put("bbox", getParameter("bboxFake")); + given() + .headers(jsonContent) + .pathParam("profile", "driving-car") + .body(body.toString()) + .when() + .post(getEndPointPath() + "/{profile}/blah") + .then().log().ifValidationFails() + .assertThat() + .body("error.code", Matchers.is(ExportErrorCodes.UNSUPPORTED_EXPORT_FORMAT)) + .body("error.message", Matchers.is("The response format blah is not supported")) + .statusCode(406); + } + + @Test + void expectMissingBbox() { + JSONObject body = new JSONObject(); + + given() + .headers(jsonContent) + .pathParam("profile", "driving-car") + .body(body.toString()) + .when() + .post(getEndPointPath() + "/{profile}/topojson") + .then() + .assertThat() + //not sure mismatched input is correct here, missing parameter seems more intuitive? + .body("error.code", Matchers.is(ExportErrorCodes.MISMATCHED_INPUT)) + .statusCode(400); + } + + @Test + void expectFaultyBbox() { + JSONObject body = new JSONObject(); + body.put("bbox", getParameter("bboxFaulty")); + given() + .headers(jsonContent) + .pathParam("profile", "driving-car") + .body(body.toString()) + .when() + .post(getEndPointPath() + "/{profile}/topojson") + .then() + .assertThat() + .body("error.code", Matchers.is(ExportErrorCodes.INVALID_PARAMETER_VALUE)) + .statusCode(400); + } + + @Test + void expectNodesAndEdges() { + JSONObject body = new JSONObject(); + body.put("bbox", getParameter("bboxProper")); + given() + .headers(jsonContent) + .pathParam("profile", "driving-car") + .body(body.toString()) + .when() + .post(getEndPointPath() + "/{profile}/json") + .then().log().ifValidationFails() + .assertThat() + .body("containsKey('nodes')", is(true)) + .body("containsKey('edges')", is(true)) + .body("containsKey('nodes_count')", is(true)) + .body("containsKey('edges_count')", is(true)) + .body("containsKey('edges_extra')", is(false)) + .statusCode(200); + } + + @Test + void expectTopoJsonFormat() { + JSONObject body = new JSONObject(); + body.put("bbox", getParameter("bboxProper")); + given() + .headers(jsonContent) + .pathParam("profile", "wheelchair") + .body(body.toString()) + .when() + .post(getEndPointPath() + "/{profile}/topojson") + .then().log().ifValidationFails() + .assertThat() + .body("type", is("Topology")) + .body("objects.size()", is(1)) + .body("objects.network.type", is("GeometryCollection")) + .body("bbox.size()", is(4)) + // approximations due to float comparison + .body("bbox[0]", is(8.681523F)) + .body("bbox[1]", is(49.414877F)) + .body("bbox[2]", is(8.686507F)) + .body("bbox[3]", is(49.41943F)) + .body("arcs.size()", is(69)) + .body("objects.network.geometries.size()", is(35)) + .body("objects.network.geometries[0].properties.osm_id", is(4084860)) + .body("objects.network.geometries[0].properties.both_directions", is(true)) + .body("objects.network.geometries[0].properties.distances.size()", is(2)) + .body("objects.network.geometries[0].properties.ors_ids.size()", is(2)) + .body("objects.network.geometries[0].properties.speed", is(5.0F)) + .body("objects.network.geometries[0].properties.speed_reverse", is(5.0F)) + .body("objects.network.geometries[0].properties.ors_nodes.size()", is(3)) + .body("objects.network.geometries[0].arcs.size()", is(2)) // Geometry with more than 1 arc exists + .body("arcs[4].size()", is(3)) // Arc with more than 2 coordinates exists + .statusCode(200); + } + + @Test + void expectTopoJsonWithSimpleGeometry() { + JSONObject body = new JSONObject(); + body.put("bbox", getParameter("bboxProper")); + body.put("geometry", false); + given() + .headers(jsonContent) + .pathParam("profile", "wheelchair") + .body(body.toString()) + .when() + .post(getEndPointPath() + "/{profile}/topojson") + .then().log().ifValidationFails() + .assertThat() + .body("type", is("Topology")) + .body("arcs.size()", is(69)) + .body("objects.network.geometries.size()", is(35)) + .body("arcs[4].size()", is(2)) // Arc 4 now has only 2 coordinates + .statusCode(200); + } + + @Test + void expectTopoJsonFormatOneWay() { + JSONObject body = new JSONObject(); + body.put("bbox", getParameter("bboxCar")); + given() + .headers(jsonContent) + .pathParam("profile", "driving-car") + .body(body.toString()) + .when() + .post(getEndPointPath() + "/{profile}/topojson") + .then().log().ifValidationFails() + .assertThat() + .body("type", is("Topology")) + .body("arcs.size()", is(28)) + .body("objects.network.geometries.size()", is(27)) + .body("objects.network.geometries[0].properties.both_directions", is(false)) + .body("objects.network.geometries[0].properties.containsKey('speed_reverse')", is(false)) + .statusCode(200); + } + + @Test + void expectTopoJsonFallbackNoOsmIdMode() { + JSONObject body = new JSONObject(); + body.put("bbox", getParameter("bboxCar")); + given() + .headers(jsonContent) + .pathParam("profile", "driving-hgv") + .body(body.toString()) + .when() + .post(getEndPointPath() + "/{profile}/topojson") + .then().log().ifValidationFails() + .assertThat() + .body("type", is("Topology")) + .body("arcs.size()", is(30)) + .body("objects.network.geometries.size()", is(30)) + .body("objects.network.geometries[0].properties.containsKey('weight')", is(true)) + .statusCode(200); + } + + @Test + void expectAdditionalInfo() { + JSONObject body = new JSONObject(); + body.put("bbox", getParameter("bboxProper")); + body.put("additional_info", true); + given() + .headers(jsonContent) + .pathParam("profile", "wheelchair") + .body(body.toString()) + .when() + .post(getEndPointPath() + "/{profile}/json") + .then().log().ifValidationFails() + .assertThat() + .body("containsKey('nodes')", is(true)) + .body("containsKey('edges')", is(true)) + .body("containsKey('edges_extra')", is(true)) + .body("edges_extra[0].extra.containsKey('osm_id')", is(true)) + .body("edges_extra[0].extra.containsKey('ors_id')", is(true)) + .body("nodes_count", is(59)) + .body("edges_count", is(128)) + .statusCode(200); + } +} diff --git a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java index 56380dd944..3bdb59014a 100644 --- a/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java +++ b/ors-api/src/test/java/org/heigit/ors/apitests/routing/ResultTest.java @@ -2304,7 +2304,7 @@ void testWheelchairSmoothnessRestriction() { void testWheelchairDebugExport() { JSONObject body = new JSONObject(); body.put("bbox", HelperFunctions.constructCoords("8.662440776824953, 49.41372343556617|8.677289485931398, 49.42018658125273")); - body.put("debug", true); + body.put("additional_info", true); given() .header("Accept", "application/json") .header("Content-Type", "application/json") @@ -2315,7 +2315,6 @@ void testWheelchairDebugExport() { .then().log().ifValidationFails() .assertThat() .statusCode(200); - } @Test diff --git a/ors-api/src/test/resources/application-test.yml b/ors-api/src/test/resources/application-test.yml index 62a606a420..a48032043c 100644 --- a/ors-api/src/test/resources/application-test.yml +++ b/ors-api/src/test/resources/application-test.yml @@ -75,6 +75,7 @@ ors: restrictions: true WaySurfaceType: Tollways: + OsmId: Borders: boundaries: ./src/test/files/borders/borders.geojson ids: ./src/test/files/borders/ids.csv diff --git a/ors-engine/src/main/java/org/heigit/ors/export/ExportRequest.java b/ors-engine/src/main/java/org/heigit/ors/export/ExportRequest.java index 1ac74f7d6f..8f848a1c9c 100644 --- a/ors-engine/src/main/java/org/heigit/ors/export/ExportRequest.java +++ b/ors-engine/src/main/java/org/heigit/ors/export/ExportRequest.java @@ -7,14 +7,13 @@ import com.graphhopper.storage.Graph; import com.graphhopper.storage.NodeAccess; import com.graphhopper.storage.index.LocationIndex; -import com.graphhopper.util.EdgeExplorer; -import com.graphhopper.util.EdgeIterator; -import com.graphhopper.util.EdgeIteratorState; -import com.graphhopper.util.PMap; +import com.graphhopper.util.*; import com.graphhopper.util.shapes.BBox; import org.apache.log4j.Logger; import org.heigit.ors.common.Pair; import org.heigit.ors.common.ServiceRequest; +import org.heigit.ors.export.ExportResult.TopoArc; +import org.heigit.ors.export.ExportResult.TopoGeometry; import org.heigit.ors.routing.RoutingProfile; import org.heigit.ors.routing.RoutingProfileType; import org.heigit.ors.routing.WeightingMethod; @@ -24,19 +23,30 @@ import org.heigit.ors.routing.graphhopper.extensions.storages.WheelchairAttributesGraphStorage; import org.heigit.ors.util.ProfileTools; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; -import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; public class ExportRequest extends ServiceRequest { private static final Logger LOGGER = Logger.getLogger(ExportRequest.class); + private static final GeometryFactory geometryFactory = new GeometryFactory(); private BBox boundingBox; private String profileName; private int profileType = -1; - private boolean debug; + private boolean additionalEdgeInfo; + private boolean topoJson; + private boolean useRealGeometry; + + private static final int NO_TIME = -1; + private OsmIdGraphStorage osmIdGraphStorage; + private WheelchairAttributesGraphStorage wheelchairAttributesGraphStorage; + private Weighting weighting; public String getProfileName() { return profileName; @@ -46,112 +56,131 @@ public void setProfileName(String profileName) { this.profileName = profileName; } - public BBox getBoundingBox() { - return this.boundingBox; - } - public void setBoundingBox(BBox bbox) { this.boundingBox = bbox; } - public int getProfileType() { - return profileType; - } - public void setProfileType(int profileType) { this.profileType = profileType; } - public void setDebug(boolean debug) { - this.debug = debug; + public void setAdditionalEdgeInfo(boolean additionalEdgeInfo) { + this.additionalEdgeInfo = additionalEdgeInfo; + } + + public void setTopoJson(boolean equals) { + this.topoJson = equals; } - public boolean debug() { - return debug; + public void setUseRealGeometry(boolean useRealGeometry) { + this.useRealGeometry = useRealGeometry; } public ExportResult computeExport(RoutingProfile routingProfile) { ExportResult res = new ExportResult(); + // Prepare graph data access GraphHopper gh = routingProfile.getGraphhopper(); - String encoderName = RoutingProfileType.getEncoderName(getProfileType()); + String encoderName = RoutingProfileType.getEncoderName(profileType); Graph graph = gh.getGraphHopperStorage().getBaseGraph(); - + NodeAccess nodeAccess = graph.getNodeAccess(); PMap hintsMap = new PMap(); - int weightingMethod = WeightingMethod.FASTEST; - ProfileTools.setWeightingMethod(hintsMap, weightingMethod, getProfileType(), false); - String localProfileName = ProfileTools.makeProfileName(encoderName, hintsMap.getString("weighting_method", ""), false); - Weighting weighting = gh.createWeighting(gh.getProfile(localProfileName), hintsMap); - - FlagEncoder flagEncoder = gh.getEncodingManager().getEncoder(encoderName); - EdgeExplorer explorer = graph.createEdgeExplorer(AccessFilter.outEdges(flagEncoder.getAccessEnc())); - + ProfileTools.setWeightingMethod(hintsMap, WeightingMethod.FASTEST, profileType, false); + weighting = gh.createWeighting(gh.getProfile(ProfileTools.makeProfileName(encoderName, hintsMap.getString("weighting_method", ""), false)), hintsMap); + osmIdGraphStorage = GraphStorageUtils.getGraphExtension(gh.getGraphHopperStorage(), OsmIdGraphStorage.class); + wheelchairAttributesGraphStorage = GraphStorageUtils.getGraphExtension(gh.getGraphHopperStorage(), WheelchairAttributesGraphStorage.class); // filter graph for nodes in Bounding Box - LocationIndex index = gh.getLocationIndex(); - NodeAccess nodeAccess = graph.getNodeAccess(); - BBox bbox = getBoundingBox(); - - ArrayList nodesInBBox = new ArrayList<>(); - index.query(bbox, edgeId -> { - // According to GHUtility.getEdgeFromEdgeKey, edgeIds are calculated as edgeKey/2. - EdgeIteratorState edge = graph.getEdgeIteratorStateForKey(edgeId * 2); - int baseNode = edge.getBaseNode(); - int adjNode = edge.getAdjNode(); - - if (bbox.contains(nodeAccess.getLat(baseNode), nodeAccess.getLon(baseNode))) { - nodesInBBox.add(baseNode); - } - if (bbox.contains(nodeAccess.getLat(adjNode), nodeAccess.getLon(adjNode))) { - nodesInBBox.add(adjNode); - } - }); - + Set nodesInBBox = nodesInBBox(gh.getLocationIndex(), nodeAccess, graph); LOGGER.debug("Found %d nodes in bbox.".formatted(nodesInBBox.size())); - - if (nodesInBBox.isEmpty()) { - // without nodes, no export can be calculated + if (nodesInBBox.isEmpty()) { // without nodes, no export can be calculated res.setWarning(new ExportWarning(ExportWarning.EMPTY_BBOX)); return res; } - // calculate node coordinates + // iterate over all edges and add them to the result object for (int from : nodesInBBox) { - Coordinate coord = new Coordinate(nodeAccess.getLon(from), nodeAccess.getLat(from)); - res.addLocation(from, coord); - - EdgeIterator iter = explorer.setBaseNode(from); + Coordinate fromCoords = new Coordinate(nodeAccess.getLon(from), nodeAccess.getLat(from)); + res.addLocation(from, fromCoords); + EdgeIterator iter = graph.createEdgeExplorer(AccessFilter.outEdges(gh.getEncodingManager().getEncoder(encoderName).getAccessEnc())).setBaseNode(from); while (iter.next()) { int to = iter.getAdjNode(); if (nodesInBBox.contains(to)) { - double weight = weighting.calcEdgeWeight(iter, false, EdgeIterator.NO_EDGE); - Pair p = new Pair<>(from, to); - res.addEdge(p, weight); - - if (debug()) { - Map extra = new HashMap<>(); - extra.put("edge_id", iter.getEdge()); - WheelchairAttributesGraphStorage storage = GraphStorageUtils.getGraphExtension(gh.getGraphHopperStorage(), WheelchairAttributesGraphStorage.class); - if (storage != null) { - WheelchairAttributes attributes = new WheelchairAttributes(); - byte[] buffer = new byte[WheelchairAttributesGraphStorage.BYTE_COUNT]; - storage.getEdgeValues(iter.getEdge(), attributes, buffer); - if (attributes.hasValues()) { - extra.put("incline", attributes.getIncline()); - extra.put("surface_quality_known", attributes.isSurfaceQualityKnown()); - extra.put("suitable", attributes.isSuitable()); - } - } - OsmIdGraphStorage storage2 = GraphStorageUtils.getGraphExtension(gh.getGraphHopperStorage(), OsmIdGraphStorage.class); - if (storage2 != null) { - extra.put("osm_id", storage2.getEdgeValue(iter.getEdge())); - } - res.addEdgeExtra(p, extra); + LOGGER.debug("Edge %d: from %d to %d".formatted(iter.getEdge(), from, to)); + LineString geo; + if (useRealGeometry) { + geo = iter.fetchWayGeometry(FetchMode.ALL).toLineString(false); + } else { + Coordinate toCoords = new Coordinate(nodeAccess.getLon(to), nodeAccess.getLat(to)); + geo = geometryFactory.createLineString(new Coordinate[]{fromCoords, toCoords}); + } + + if (topoJson && osmIdGraphStorage != null) { + addEdgeToTopoGeometries(res, iter, geo); + } else { + addEdgeToResultObject(res, iter, geo, from, to); } } } } - return res; } + + private Set nodesInBBox(LocationIndex index, NodeAccess nodeAccess, Graph graph) { + Set ret = new HashSet<>(); + index.query(boundingBox, edgeId -> { + // According to GHUtility.getEdgeFromEdgeKey, edgeIds are calculated as edgeKey/2. + EdgeIteratorState edge = graph.getEdgeIteratorStateForKey(edgeId * 2); + int baseNode = edge.getBaseNode(); + int adjNode = edge.getAdjNode(); + if (this.boundingBox.contains(nodeAccess.getLat(baseNode), nodeAccess.getLon(baseNode))) { + ret.add(baseNode); + } + if (this.boundingBox.contains(nodeAccess.getLat(adjNode), nodeAccess.getLon(adjNode))) { + ret.add(adjNode); + } + }); + return ret; + } + + private void addEdgeToTopoGeometries(ExportResult res, EdgeIterator iter, LineString geo) { + boolean reverse = iter.getEdgeKey() % 2 == 1; + TopoGeometry topoGeometry = res.getTopoGeometries().computeIfAbsent(osmIdGraphStorage.getEdgeValue(iter.getEdge()), x -> + new TopoGeometry(weighting.getSpeedCalculator().getSpeed(iter, reverse, NO_TIME), + weighting.getSpeedCalculator().getSpeed(iter, !reverse, NO_TIME)) + ); + topoGeometry.getArcs().compute(iter.getEdge(), (k, v) -> { + if (v != null) { + topoGeometry.setBothDirections(true); + return v; + } else { + return reverse ? new TopoArc(geo.reverse(), iter.getDistance(), iter.getAdjNode(), iter.getBaseNode()) : + new TopoArc(geo, iter.getDistance(), iter.getBaseNode(), iter.getAdjNode()); + } + }); + } + + private void addEdgeToResultObject(ExportResult res, EdgeIterator iter, LineString geo, int from, int to) { + Pair p = new Pair<>(from, to); + res.addEdge(p, weighting.calcEdgeWeight(iter, false, NO_TIME)); + res.getEdgeGeometries().put(p, geo); + if (additionalEdgeInfo) { + Map extra = new HashMap<>(); + if (osmIdGraphStorage != null) { + extra.put("osm_id", osmIdGraphStorage.getEdgeValue(iter.getEdge())); + } + extra.put("ors_id", iter.getEdge()); + if (wheelchairAttributesGraphStorage != null) { + WheelchairAttributes attributes = new WheelchairAttributes(); + byte[] buffer = new byte[WheelchairAttributesGraphStorage.BYTE_COUNT]; + wheelchairAttributesGraphStorage.getEdgeValues(iter.getEdge(), attributes, buffer); + if (attributes.hasValues()) { + extra.put("incline", attributes.getIncline()); + extra.put("surface_quality_known", attributes.isSurfaceQualityKnown()); + extra.put("suitable", attributes.isSuitable()); + } + } + res.addEdgeExtra(p, extra); + } + } } diff --git a/ors-engine/src/main/java/org/heigit/ors/export/ExportResult.java b/ors-engine/src/main/java/org/heigit/ors/export/ExportResult.java index 797a23cbf1..0047b6768a 100644 --- a/ors-engine/src/main/java/org/heigit/ors/export/ExportResult.java +++ b/ors-engine/src/main/java/org/heigit/ors/export/ExportResult.java @@ -1,64 +1,45 @@ package org.heigit.ors.export; +import lombok.Getter; +import lombok.Setter; import org.heigit.ors.common.Pair; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; import java.util.HashMap; import java.util.Map; +@Getter public class ExportResult { - private Map locations; - private Map, Double> edgeWeigths; + private final Map locations; + private final Map, Double> edgeWeights; + private final Map, LineString> edgeGeometries; + private final Map topoGeometries; private Map, Map> edgeExtras; + @Setter private ExportWarning warning; public ExportResult() { this.locations = new HashMap<>(); - this.edgeWeigths = new HashMap<>(); + this.edgeWeights = new HashMap<>(); + this.edgeGeometries = new HashMap<>(); + this.topoGeometries = new HashMap<>(); this.warning = null; } - public Map, Double> getEdgeWeigths() { - return edgeWeigths; - } - - public void setEdgeWeigths(Map, Double> edgeWeigths) { - this.edgeWeigths = edgeWeigths; - } - public void addEdge(Pair edge, Double weight) { - this.edgeWeigths.put(edge, weight); - } - - public Map getLocations() { - return locations; - } - - public void setLocations(Map locations) { - this.locations = locations; + this.edgeWeights.put(edge, weight); } public void addLocation(Integer node, Coordinate coord) { this.locations.put(node, coord); } - public ExportWarning getWarning() { - return warning; - } - - public void setWarning(ExportWarning warning) { - this.warning = warning; - } - public boolean hasWarning() { return this.warning != null; } - public Map, Map> getEdgeExtras() { - return edgeExtras; - } - public void addEdgeExtra(Pair edge, Map extra) { if (edgeExtras == null) { edgeExtras = new HashMap<>(); @@ -69,4 +50,25 @@ public void addEdgeExtra(Pair edge, Map extra) public boolean hasEdgeExtras() { return edgeExtras != null; } + + public boolean hasTopoGeometries() { + return !topoGeometries.isEmpty(); + } + + @Getter + public static class TopoGeometry { + private final double speed; + private final double speedReverse; + private final Map arcs = new HashMap<>(); + @Setter + private boolean bothDirections; + + public TopoGeometry(double speed, double speedReverse) { + this.speed = speed; + this.speedReverse = speedReverse; + } + } + + public record TopoArc(LineString geometry, double length, int from, int to) { + } }