RoutingService.java
package org.heigit.ors.api.services;
import org.heigit.ors.api.APIEnums;
import org.heigit.ors.api.EndpointsProperties;
import org.heigit.ors.api.requests.routing.RouteRequest;
import org.heigit.ors.api.requests.routing.RouteRequestRoundTripOptions;
import org.heigit.ors.common.StatusCode;
import org.heigit.ors.exceptions.*;
import org.heigit.ors.localization.LocalizationManager;
import org.heigit.ors.routing.*;
import org.locationtech.jts.geom.Coordinate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
@Service
public class RoutingService extends ApiService {
@Autowired
public RoutingService(EndpointsProperties endpointsProperties) {
this.endpointsProperties = endpointsProperties;
}
@Override
double getMaximumAvoidPolygonArea() {
return this.endpointsProperties.getRouting().getMaximumAvoidPolygonArea();
}
@Override
double getMaximumAvoidPolygonExtent() {
return this.endpointsProperties.getRouting().getMaximumAvoidPolygonExtent();
}
public RouteResult[] generateRouteFromRequest(RouteRequest request) throws StatusCodeException {
RoutingRequest routingRequest = this.convertRouteRequest(request);
try {
return RoutingProfileManager.getInstance().computeRoute(routingRequest);
} catch (StatusCodeException e) {
throw e;
} catch (Exception e) {
throw new StatusCodeException(StatusCode.INTERNAL_SERVER_ERROR, RoutingErrorCodes.UNKNOWN);
}
}
public RoutingRequest convertRouteRequest(RouteRequest request) throws StatusCodeException {
RoutingRequest routingRequest = new RoutingRequest();
boolean isRoundTrip = request.hasRouteOptions() && request.getRouteOptions().hasRoundTripOptions();
routingRequest.setCoordinates(convertCoordinates(request.getCoordinates(), isRoundTrip));
routingRequest.setGeometryFormat(convertGeometryFormat(request.getResponseType()));
if (request.hasUseElevation())
routingRequest.setIncludeElevation(request.getUseElevation());
if (request.hasContinueStraightAtWaypoints())
routingRequest.setContinueStraight(request.getContinueStraightAtWaypoints());
if (request.hasIncludeGeometry())
routingRequest.setIncludeGeometry(convertIncludeGeometry(request));
if (request.hasIncludeManeuvers())
routingRequest.setIncludeManeuvers(request.getIncludeManeuvers());
if (request.hasIncludeInstructions())
routingRequest.setIncludeInstructions(request.getIncludeInstructionsInResponse());
if (request.hasIncludeRoundaboutExitInfo())
routingRequest.setIncludeRoundaboutExits(request.getIncludeRoundaboutExitInfo());
if (request.hasAttributes())
routingRequest.setAttributes(convertAttributes(request));
if (request.hasExtraInfo()) {
routingRequest.setExtraInfo(convertExtraInfo(request));
for (APIEnums.ExtraInfo extra : request.getExtraInfo()) {
if (extra.compareTo(APIEnums.ExtraInfo.COUNTRY_INFO) == 0) {
routingRequest.setIncludeCountryInfo(true);
}
}
}
if (request.hasLanguage())
routingRequest.setLanguage(convertLanguage(request.getLanguage()));
if (request.hasInstructionsFormat())
routingRequest.setInstructionsFormat(convertInstructionsFormat(request.getInstructionsFormat()));
if (request.hasUnits())
routingRequest.setUnits(convertUnits(request.getUnits()));
if (request.hasSimplifyGeometry()) {
routingRequest.setGeometrySimplify(request.getSimplifyGeometry());
if (request.hasExtraInfo() && request.getSimplifyGeometry()) {
throw new IncompatibleParameterException(RoutingErrorCodes.INCOMPATIBLE_PARAMETERS, RouteRequest.PARAM_SIMPLIFY_GEOMETRY, "true", RouteRequest.PARAM_EXTRA_INFO, "*");
}
}
if (request.hasSkipSegments()) {
routingRequest.setSkipSegments(processSkipSegments(request));
}
if (request.hasId())
routingRequest.setId(request.getId());
if (request.hasMaximumSpeed()) {
routingRequest.setMaximumSpeed(request.getMaximumSpeed());
}
int profileType = -1;
int coordinatesLength = request.getCoordinates().size();
RouteSearchParameters params = new RouteSearchParameters();
if (request.hasExtraInfo()) {
routingRequest.setExtraInfo(convertExtraInfo(request));//todo remove duplicate?
params.setExtraInfo(convertExtraInfo(request));
}
if (request.hasSuppressWarnings())
params.setSuppressWarnings(request.getSuppressWarnings());
try {
profileType = convertRouteProfileType(request.getProfile());
params.setProfileType(profileType);
} catch (Exception e) {
throw new ParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequest.PARAM_PROFILE);
}
APIEnums.RoutePreference preference = request.hasRoutePreference() ? request.getRoutePreference() : APIEnums.RoutePreference.RECOMMENDED;
params.setWeightingMethod(convertWeightingMethod(request, preference));
if (request.hasBearings())
params.setBearings(convertBearings(request.getBearings(), coordinatesLength));
if (request.hasContinueStraightAtWaypoints())
params.setContinueStraight(request.getContinueStraightAtWaypoints());
if (request.hasMaximumSearchRadii())
params.setMaximumRadiuses(convertMaxRadii(request.getMaximumSearchRadii(), coordinatesLength, profileType));
if (request.hasUseContractionHierarchies()) {
params.setFlexibleMode(convertSetFlexibleMode(request.getUseContractionHierarchies()));
params.setOptimized(request.getUseContractionHierarchies());
}
if (request.hasRouteOptions()) {
params = processRouteRequestOptions(request, params);
}
if (request.hasAlternativeRoutes()) {
if (request.getCoordinates().size() > 2) {
throw new IncompatibleParameterException(RoutingErrorCodes.INCOMPATIBLE_PARAMETERS, RouteRequest.PARAM_ALTERNATIVE_ROUTES, "(number of waypoints > 2)");
}
if (request.getAlternativeRoutes().hasTargetCount()) {
params.setAlternativeRoutesCount(request.getAlternativeRoutes().getTargetCount());
int countLimit = endpointsProperties.getRouting().getMaximumAlternativeRoutes();
if (countLimit > 0 && request.getAlternativeRoutes().getTargetCount() > countLimit) {
throw new ParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequest.PARAM_ALTERNATIVE_ROUTES, Integer.toString(request.getAlternativeRoutes().getTargetCount()), "The target alternative routes count has to be equal to or less than " + countLimit);
}
}
if (request.getAlternativeRoutes().hasWeightFactor())
params.setAlternativeRoutesWeightFactor(request.getAlternativeRoutes().getWeightFactor());
if (request.getAlternativeRoutes().hasShareFactor())
params.setAlternativeRoutesShareFactor(request.getAlternativeRoutes().getShareFactor());
}
if (request.hasDeparture() && request.hasArrival())
throw new IncompatibleParameterException(RoutingErrorCodes.INCOMPATIBLE_PARAMETERS, RouteRequest.PARAM_DEPARTURE, RouteRequest.PARAM_ARRIVAL);
else if (request.hasDeparture())
params.setDeparture(request.getDeparture());
else if (request.hasArrival())
params.setArrival(request.getArrival());
if (request.hasMaximumSpeed()) {
params.setMaximumSpeed(request.getMaximumSpeed());
}
// propagate GTFS-parameters to params to convert to ptRequest in RoutingProfile.computeRoute
if (request.hasSchedule()) {
params.setSchedule(request.getSchedule());
}
if (request.hasWalkingTime()) {
params.setWalkingTime(request.getWalkingTime());
}
if (request.hasScheduleRows()) {
params.setScheduleRows(request.getScheduleRows());
}
if (request.hasIgnoreTransfers()) {
params.setIgnoreTransfers(request.isIgnoreTransfers());
}
if (request.hasScheduleDuration()) {
params.setScheduleDuaration(request.getScheduleDuration());
}
params.setConsiderTurnRestrictions(false);
routingRequest.setSearchParameters(params);
return routingRequest;
}
private Coordinate[] convertCoordinates(List<List<Double>> coordinates, boolean allowSingleCoordinate) throws ParameterValueException {
if (!allowSingleCoordinate && coordinates.size() < 2)
throw new ParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequest.PARAM_COORDINATES);
if (allowSingleCoordinate && coordinates.size() > 1)
throw new ParameterValueException(
RoutingErrorCodes.INVALID_PARAMETER_VALUE,
RouteRequest.PARAM_COORDINATES,
"Length = " + coordinates.size(),
"Only one coordinate pair is allowed");
ArrayList<Coordinate> coords = new ArrayList<>();
for (List<Double> coord : coordinates) {
coords.add(convertSingleCoordinate(coord));
}
return coords.toArray(new Coordinate[coords.size()]);
}
private Coordinate convertSingleCoordinate(List<Double> coordinate) throws ParameterValueException {
if (coordinate.size() != 2)
throw new ParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequest.PARAM_COORDINATES);
return new Coordinate(coordinate.get(0), coordinate.get(1));
}
private String convertGeometryFormat(APIEnums.RouteResponseType responseType) throws ParameterValueException {
switch (responseType) {
case GEOJSON:
return "geojson";
case JSON:
return "encodedpolyline";
case GPX:
return "gpx";
default:
throw new ParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequest.PARAM_FORMAT);
}
}
private boolean convertIncludeGeometry(RouteRequest request) throws IncompatibleParameterException {
if (!request.getIncludeGeometry() && request.getResponseType() != APIEnums.RouteResponseType.JSON) {
throw new IncompatibleParameterException(RoutingErrorCodes.INVALID_PARAMETER_VALUE,
RouteRequest.PARAM_GEOMETRY, "false",
RouteRequest.PARAM_FORMAT, APIEnums.RouteResponseType.GEOJSON + "/" + APIEnums.RouteResponseType.GPX);
}
return request.getIncludeGeometry();
}
//TODO method needed, or directly call delegate method?
private static String[] convertAttributes(RouteRequest request) {
return convertAPIEnumListToStrings(request.getAttributes());
}
private static int convertExtraInfo(RouteRequest request) {
String[] extraInfosStrings = convertAPIEnumListToStrings(request.getExtraInfo());
String extraInfoPiped = String.join("|", extraInfosStrings);
return RouteExtraInfoFlag.getFromString(extraInfoPiped);
}
private String convertLanguage(APIEnums.Languages languageIn) throws StatusCodeException {
boolean isLanguageSupported;
String languageString = languageIn.toString();
try {
isLanguageSupported = LocalizationManager.getInstance().isLanguageSupported(languageString);
} catch (Exception e) {
throw new InternalServerException(RoutingErrorCodes.UNKNOWN, "Could not access Localization Manager");
}
if (!isLanguageSupported)
throw new StatusCodeException(StatusCode.BAD_REQUEST, RoutingErrorCodes.INVALID_PARAMETER_VALUE, "Specified language '" + languageIn + "' is not supported.");
return languageString;
}
private RouteInstructionsFormat convertInstructionsFormat(APIEnums.InstructionsFormat formatIn) throws UnknownParameterValueException {
RouteInstructionsFormat instrFormat = RouteInstructionsFormat.fromString(formatIn.toString());
if (instrFormat == RouteInstructionsFormat.UNKNOWN)
throw new UnknownParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequest.PARAM_INSTRUCTIONS_FORMAT, formatIn.toString());
return instrFormat;
}
private List<Integer> processSkipSegments(RouteRequest request) throws ParameterOutOfRangeException, ParameterValueException, EmptyElementException {
for (Integer skipSegment : request.getSkipSegments()) {
if (skipSegment >= request.getCoordinates().size()) {
throw new ParameterOutOfRangeException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequest.PARAM_SKIP_SEGMENTS, skipSegment.toString(), String.valueOf(request.getCoordinates().size() - 1));
}
if (skipSegment <= 0) {
throw new ParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequest.PARAM_SKIP_SEGMENTS, request.getSkipSegments().toString(), "The individual skip_segments values have to be greater than 0.");
}
}
if (request.getSkipSegments().size() > request.getCoordinates().size() - 1) {
throw new ParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequest.PARAM_SKIP_SEGMENTS, request.getSkipSegments().toString(), "The amount of segments to skip shouldn't be more than segments in the coordinates.");
}
if (request.getSkipSegments().isEmpty()) {
throw new EmptyElementException(RoutingErrorCodes.EMPTY_ELEMENT, RouteRequest.PARAM_SKIP_SEGMENTS);
}
return request.getSkipSegments();
}
private int convertWeightingMethod(RouteRequest request, APIEnums.RoutePreference preferenceIn) throws UnknownParameterValueException {
if (request.getProfile().equals(APIEnums.Profile.DRIVING_CAR) && preferenceIn.equals(APIEnums.RoutePreference.RECOMMENDED))
return WeightingMethod.FASTEST;
int weightingMethod = WeightingMethod.getFromString(preferenceIn.toString());
if (weightingMethod == WeightingMethod.UNKNOWN)
throw new UnknownParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequest.PARAM_PREFERENCE, preferenceIn.toString());
return weightingMethod;
}
private WayPointBearing[] convertBearings(Double[][] bearingsIn, int coordinatesLength) throws ParameterValueException {
if (bearingsIn == null || bearingsIn.length == 0)
return new WayPointBearing[0];
if (bearingsIn.length != coordinatesLength && bearingsIn.length != coordinatesLength - 1)
throw new ParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequest.PARAM_BEARINGS, Arrays.toString(bearingsIn), "The number of bearings must be equal to the number of waypoints on the route.");
WayPointBearing[] bearingsList = new WayPointBearing[coordinatesLength];
for (int i = 0; i < bearingsIn.length; i++) {
Double[] singleBearingIn = bearingsIn[i];
if (singleBearingIn.length == 0) {
bearingsList[i] = new WayPointBearing(Double.NaN);
} else if (singleBearingIn.length == 1) {
bearingsList[i] = new WayPointBearing(singleBearingIn[0]);
} else {
bearingsList[i] = new WayPointBearing(singleBearingIn[0], singleBearingIn[1]);
}
}
return bearingsList;
}
private double[] convertMaxRadii(Double[] radiiIn, int coordinatesLength, int profileType) throws ParameterValueException {
if (radiiIn != null) {
if (radiiIn.length == 1) {
double[] maxRadii = new double[coordinatesLength];
Arrays.fill(maxRadii, radiiIn[0]);
return maxRadii;
}
if (radiiIn.length != coordinatesLength)
throw new ParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_VALUE, RouteRequest.PARAM_RADII, Arrays.toString(radiiIn), "The number of specified radiuses must be one or equal to the number of specified waypoints.");
return Stream.of(radiiIn).mapToDouble(Double::doubleValue).toArray();
} else if (profileType == RoutingProfileType.WHEELCHAIR) {
// As there are generally less ways that can be used as pedestrian ways, we need to restrict search
// radii else we end up with starting and ending ways really far from the actual points. This is
// especially a problem for wheelchair users as the restrictions are stricter
double[] maxRadii = new double[coordinatesLength];
Arrays.fill(maxRadii, 50);
return maxRadii;
} else {
return new double[0];
}
}
private boolean convertSetFlexibleMode(boolean useContractionHierarchies) throws ParameterValueException {
if (useContractionHierarchies)
throw new ParameterValueException(RoutingErrorCodes.INVALID_PARAMETER_FORMAT, RouteRequest.PARAM_OPTIMIZED);
return true;
}
private RouteSearchParameters processRouteRequestOptions(RouteRequest request, RouteSearchParameters params) throws StatusCodeException {
params = processRequestOptions(request.getRouteOptions(), params);
if (request.getRouteOptions().hasProfileParams())
params.setProfileParams(convertParameters(request.getRouteOptions(), params.getProfileType()));
if (request.getRouteOptions().hasVehicleType())
params.setVehicleType(convertVehicleType(request.getRouteOptions().getVehicleType(), params.getProfileType()));
if (request.getRouteOptions().hasRoundTripOptions()) {
RouteRequestRoundTripOptions roundTripOptions = request.getRouteOptions().getRoundTripOptions();
if (roundTripOptions.hasLength()) {
params.setRoundTripLength(roundTripOptions.getLength());
} else {
throw new MissingParameterException(RoutingErrorCodes.MISSING_PARAMETER, RouteRequestRoundTripOptions.PARAM_LENGTH);
}
if (roundTripOptions.hasPoints()) {
params.setRoundTripPoints(roundTripOptions.getPoints());
}
if (roundTripOptions.hasSeed()) {
params.setRoundTripSeed(roundTripOptions.getSeed());
}
}
return params;
}
}