Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export4.0 #1160

Merged
merged 4 commits into from
May 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/

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.annotations.*;
import org.heigit.ors.api.errors.CommonResponseEntityExceptionHandler;
import org.heigit.ors.api.requests.export.ExportRequest;
import org.heigit.ors.api.requests.common.APIEnums;
import org.heigit.ors.api.responses.export.json.JsonExportResponse;
import org.heigit.ors.export.ExportErrorCodes;
import org.heigit.ors.export.ExportResult;
import org.heigit.ors.exceptions.*;
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.*;

import javax.servlet.http.HttpServletResponse;

@RestController
@Api(value = "Export Service", description = "Export the base graph for different modes of transport", tags = "Export")
@RequestMapping("/v2/export")
@ApiResponses({
@ApiResponse(code = 400, message = "The request is incorrect and therefore can not be processed."),
@ApiResponse(code = 404, message = "An element could not be found. If possible, a more detailed error code is provided."),
@ApiResponse(code = 405, message = "The specified HTTP method is not supported. For more details, refer to the EndPoint documentation."),
@ApiResponse(code = 413, message = "The request is larger than the server is able to process, the data provided in the request exceeds the capacity limit."),
@ApiResponse(code = 500, message = "An unexpected error was encountered and a more detailed error code is provided."),
@ApiResponse(code = 501, message = "Indicates that the server does not support the functionality needed to fulfill the request."),
@ApiResponse(code = 503, message = "The server is currently unavailable due to overload or maintenance.")
})
public class ExportAPI {
static final CommonResponseEntityExceptionHandler errorHandler = new CommonResponseEntityExceptionHandler(ExportErrorCodes.BASE);

// generic catch methods - when extra info is provided in the url, the other methods are accessed.
@GetMapping
@ApiOperation(value = "", hidden = true)
public void getGetMapping() throws MissingParameterException {
throw new MissingParameterException(ExportErrorCodes.MISSING_PARAMETER, "profile");
}

@PostMapping
@ApiOperation(value = "", hidden = true)
public String getPostMapping(@RequestBody ExportRequest request) throws MissingParameterException {
throw new MissingParameterException(ExportErrorCodes.MISSING_PARAMETER, "profile");
}

// Matches any response type that has not been defined
@PostMapping(value="/{profile}/*")
@ApiOperation(value = "", 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}")
@ApiOperation(notes = "Returns a list of points, edges and weights within a given bounding box for a selected profile as JSON", value = "Export Service (POST)", httpMethod = "POST", consumes = "application/json", produces = "application/json")
@ApiResponses(
@ApiResponse(code = 200,
message = "Standard response for successfully processed requests. Returns JSON.", //TODO: add docs
response = JsonExportResponse.class)
)
public JsonExportResponse getDefault(@ApiParam(value = "Specifies the route profile.", required = true, example = "driving-car") @PathVariable APIEnums.Profile profile,
@ApiParam(value = "The request payload", required = true) @RequestBody ExportRequest request) throws StatusCodeException {
return getJsonExport(profile, request);
}

@PostMapping(value = "/{profile}/json", produces = {"application/json;charset=UTF-8"})
@ApiOperation(notes = "Returns a list of points, edges and weights within a given bounding box for a selected profile JSON", value = "Export Service JSON (POST)", httpMethod = "POST", consumes = "application/json", produces = "application/json")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "JSON Response", response = JsonExportResponse.class)
})
public JsonExportResponse getJsonExport(
@ApiParam(value = "Specifies the profile.", required = true, example = "driving-car") @PathVariable APIEnums.Profile profile,
@ApiParam(value = "The request payload", required = true) @RequestBody ExportRequest request) throws StatusCodeException {
request.setProfile(profile);
request.setResponseType(APIEnums.CentralityResponseType.JSON);

ExportResult result = request.generateExportFromRequest();

return new JsonExportResponse(result);
}

@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<Object> handleMissingParams(final MissingServletRequestParameterException e) {
return errorHandler.handleStatusCodeException(new MissingParameterException(ExportErrorCodes.MISSING_PARAMETER, e.getParameterName()));
}


@ExceptionHandler({HttpMessageNotReadableException.class, HttpMessageConversionException.class, Exception.class})
public ResponseEntity<Object> handleReadingBodyException(final Exception e) {
final Throwable cause = e.getCause();
if (cause instanceof UnrecognizedPropertyException) {
return errorHandler.handleUnknownParameterException(new UnknownParameterException(ExportErrorCodes.UNKNOWN_PARAMETER, ((UnrecognizedPropertyException) cause).getPropertyName()));
} else if (cause instanceof InvalidFormatException) {
return errorHandler.handleStatusCodeException(new ParameterValueException(ExportErrorCodes.INVALID_PARAMETER_FORMAT, ((InvalidFormatException) cause).getValue().toString()));
} else if (cause instanceof InvalidDefinitionException) {
return errorHandler.handleStatusCodeException(new ParameterValueException(ExportErrorCodes.INVALID_PARAMETER_VALUE, ((InvalidDefinitionException) cause).getPath().get(0).getFieldName()));
} else if (cause instanceof MismatchedInputException) {
return errorHandler.handleStatusCodeException(new ParameterValueException(ExportErrorCodes.MISMATCHED_INPUT, ((MismatchedInputException) cause).getPath().get(0).getFieldName()));
} 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(ExportErrorCodes.MISSING_PARAMETER, "Request body could not be read"));
}
return errorHandler.handleGenericException(e);
}
}

@ExceptionHandler(StatusCodeException.class)
public ResponseEntity<Object> handleException(final StatusCodeException e) {
return errorHandler.handleStatusCodeException(e);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package org.heigit.ors.api.requests.export;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.primitives.Doubles;
import com.graphhopper.util.shapes.BBox;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import org.heigit.ors.api.requests.common.APIEnums;
import org.heigit.ors.api.requests.common.APIRequest;
import org.heigit.ors.common.StatusCode;
import org.heigit.ors.exceptions.ParameterValueException;
import org.heigit.ors.exceptions.StatusCodeException;
import org.heigit.ors.export.ExportErrorCodes;
import org.heigit.ors.export.ExportResult;
import org.heigit.ors.routing.RoutingProfileManager;

import java.util.List;

@ApiModel(value = "Centrality Service", description = "The JSON body request sent to the centrality service which defines options and parameters regarding the centrality measure to calculate.")
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public class ExportRequest extends APIRequest {
public static final String PARAM_ID = "id";
public static final String PARAM_BBOX = "bbox";
public static final String PARAM_PROFILE = "profile";
public static final String PARAM_FORMAT = "format";

@ApiModelProperty(name = PARAM_ID, value = "Arbitrary identification string of the request reflected in the meta information.",
example = "centrality_request")
@JsonProperty(PARAM_ID)
private String id;
@JsonIgnore
private boolean hasId = false;

@ApiModelProperty(name = PARAM_PROFILE, hidden = true)
private APIEnums.Profile profile;

@ApiModelProperty(name = PARAM_BBOX, value = "The bounding box to use for the request as an array of `longitude/latitude` pairs",
example = "[8.681495,49.41461,8.686507,49.41943]",
required = true)
@JsonProperty(PARAM_BBOX)
private List<List<Double>> bbox; //apparently, this has to be a non-primitive type…

@ApiModelProperty(name = PARAM_FORMAT, hidden = true)
@JsonProperty(PARAM_FORMAT)
private APIEnums.CentralityResponseType responseType = APIEnums.CentralityResponseType.JSON;

@JsonCreator
public ExportRequest(@JsonProperty(value = PARAM_BBOX, required = true) List<List<Double>> bbox) {
this.bbox = bbox;
}

public boolean hasId() {
return hasId;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
this.hasId = true;
}

public List<List<Double>> getBbox () {
return bbox;
}

public void setBbox(List<List<Double>> bbox ) {
this.bbox = bbox;
}

public APIEnums.Profile getProfile() {
return profile;
}

public void setProfile(APIEnums.Profile profile) {
this.profile = profile;
}

public void setResponseType(APIEnums.CentralityResponseType responseType) {
this.responseType = responseType;
}

public ExportResult generateExportFromRequest() throws StatusCodeException {
org.heigit.ors.export.ExportRequest exportRequest = this.convertExportRequest();

try {
return RoutingProfileManager.getInstance().computeExport(exportRequest);
} catch (StatusCodeException e) {
throw e;
} catch (Exception e) {
throw new StatusCodeException(StatusCode.INTERNAL_SERVER_ERROR, ExportErrorCodes.UNKNOWN);
}
}

private org.heigit.ors.export.ExportRequest convertExportRequest() throws StatusCodeException {
org.heigit.ors.export.ExportRequest exportRequest = new org.heigit.ors.export.ExportRequest();

if (this.hasId())
exportRequest.setId(this.getId());

int profileType = -1;

try {
profileType = convertRouteProfileType(this.getProfile());
exportRequest.setProfileType(profileType);
} catch (Exception e) {
throw new ParameterValueException(ExportErrorCodes.INVALID_PARAMETER_VALUE, ExportRequest.PARAM_PROFILE);
}

exportRequest.setBoundingBox(convertBBox(this.getBbox()));

return exportRequest;
}

BBox convertBBox(List<List<Double>> coordinates) throws ParameterValueException {
if (coordinates.size() != 2) {
throw new ParameterValueException(ExportErrorCodes.INVALID_PARAMETER_VALUE, ExportRequest.PARAM_BBOX);
}

double[] coords = {};

for (List<Double> coord : coordinates) {
coords = Doubles.concat(coords, convertSingleCoordinate(coord));
}

return new BBox(coords);
}

private double[] convertSingleCoordinate(List<Double> coordinate) throws ParameterValueException {
if (coordinate.size() != 2) {
throw new ParameterValueException(ExportErrorCodes.INVALID_PARAMETER_VALUE, ExportRequest.PARAM_BBOX);
}

return new double[]{coordinate.get(0), coordinate.get(1)};
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.heigit.ors.api.responses.export;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.heigit.ors.api.responses.common.boundingbox.BoundingBox;
import org.heigit.ors.export.ExportResult;

//TODO: should this include ExportResponseInfo, as does RouteResponse?
public class ExportResponse {
@JsonIgnore
protected BoundingBox bbox;

@JsonIgnore
protected ExportResult exportResults;

public ExportResponse() {};

// In RouteResponse, this method was used to get metadata from RouteRequest.
public ExportResponse(ExportResult result) {
this.exportResults = result;
}

public BoundingBox getBbox() {
return bbox;
}

public ExportResult getExportResults() {
return exportResults;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.heigit.ors.api.responses.export.json;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import org.heigit.ors.common.Pair;

import java.util.Map;

public class JsonEdge {
@ApiModelProperty(value = "Id of the start point of the edge", example = "1")
@JsonProperty(value = "fromId")
@JsonFormat(shape = JsonFormat.Shape.NUMBER_INT)
protected Integer fromId;

@ApiModelProperty(value = "Id of the end point of the edge", example = "2")
@JsonProperty(value = "toId")
@JsonFormat(shape = JsonFormat.Shape.NUMBER_INT)
protected Integer toId;

@ApiModelProperty(value = "Weight of the corresponding edge in the given bounding box",
example = "123.45")
@JsonProperty(value = "weight")
@JsonFormat(shape = JsonFormat.Shape.NUMBER_FLOAT)
protected Double weight;

JsonEdge(Map.Entry<Pair<Integer, Integer>, Double> weightedEdge) {
this.fromId = weightedEdge.getKey().first;
this.toId = weightedEdge.getKey().second;
this.weight = weightedEdge.getValue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.heigit.ors.api.responses.export.json;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.vividsolutions.jts.geom.Coordinate;
import io.swagger.annotations.ApiModel;
import org.heigit.ors.api.responses.export.ExportResponse;
import org.heigit.ors.api.responses.routing.json.JSONWarning;
import org.heigit.ors.export.ExportResult;
import org.heigit.ors.common.Pair;
import org.heigit.ors.export.ExportWarning;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@ApiModel(description = "The Export Response contains nodes and edge weights from the requested BBox")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class JsonExportResponse extends ExportResponse {

@JsonProperty("nodes")
public List<JsonNode> nodes;

@JsonProperty("edges")
public List<JsonEdge> edges;

@JsonProperty("warning")
public JSONWarning warning;

public JsonExportResponse(ExportResult exportResult) {
super(exportResult);

this.nodes = new ArrayList<>();
for (Map.Entry<Integer, Coordinate> location : exportResult.getLocations().entrySet()) {
this.nodes.add(new JsonNode(location));
}

this.edges = new ArrayList<>();
for (Map.Entry<Pair<Integer, Integer>, Double> edgeWeight : exportResult.getEdgeWeigths().entrySet()) {
this.edges.add(new JsonEdge(edgeWeight));
}


if (exportResult.hasWarning()) {
ExportWarning warning = exportResult.getWarning();
this.warning = new JSONWarning(warning.getWarningCode(), warning.getWarningMessage());
}
}
}
Loading