IsochronesService.java

package org.heigit.ors.api.services;

import org.heigit.ors.api.EndpointsProperties;
import org.heigit.ors.api.requests.isochrones.IsochronesRequest;
import org.heigit.ors.api.requests.isochrones.IsochronesRequestEnums;
import org.heigit.ors.api.requests.routing.RouteRequestOptions;
import org.heigit.ors.common.DistanceUnit;
import org.heigit.ors.common.StatusCode;
import org.heigit.ors.common.TravelRangeType;
import org.heigit.ors.common.TravellerInfo;
import org.heigit.ors.exceptions.InternalServerException;
import org.heigit.ors.exceptions.ParameterOutOfRangeException;
import org.heigit.ors.exceptions.ParameterValueException;
import org.heigit.ors.exceptions.StatusCodeException;
import org.heigit.ors.fastisochrones.partitioning.FastIsochroneFactory;
import org.heigit.ors.isochrones.*;
import org.heigit.ors.isochrones.statistics.StatisticsProviderConfiguration;
import org.heigit.ors.api.APIEnums;
import org.heigit.ors.routing.RouteSearchParameters;
import org.heigit.ors.routing.RoutingProfileManager;
import org.heigit.ors.routing.RoutingProfileType;
import org.heigit.ors.util.DistanceUnitUtil;
import org.locationtech.jts.geom.Coordinate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.heigit.ors.api.requests.isochrones.IsochronesRequest.convertAttributes;
import static org.heigit.ors.api.requests.isochrones.IsochronesRequest.convertToIsochronesProfileType;
import static org.heigit.ors.api.requests.isochrones.IsochronesRequestEnums.CalculationMethod.CONCAVE_BALLS;
import static org.heigit.ors.api.requests.isochrones.IsochronesRequestEnums.CalculationMethod.FASTISOCHRONE;
import static org.heigit.ors.common.TravelRangeType.DISTANCE;

@Service
public class IsochronesService extends ApiService {

    @Autowired
    public IsochronesService(EndpointsProperties endpointsProperties) {
        this.endpointsProperties = endpointsProperties;
    }

    public void generateIsochronesFromRequest(IsochronesRequest isochronesRequest) throws Exception {
        isochronesRequest.setIsochroneRequest(convertIsochroneRequest(isochronesRequest));
        // request object is built, now check if ors config allows all settings
        List<TravellerInfo> travellers = isochronesRequest.getIsochroneRequest().getTravellers();

        // TODO REFACTORING where should we put the validation code?
        validateAgainstConfig(isochronesRequest.getIsochroneRequest(), travellers);

        if (!travellers.isEmpty()) {
            isochronesRequest.setIsoMaps(new IsochroneMapCollection());

            for (int i = 0; i < travellers.size(); ++i) {
                IsochroneSearchParameters searchParams = isochronesRequest.getIsochroneRequest().getSearchParameters(i);
                IsochroneMap isochroneMap = RoutingProfileManager.getInstance().buildIsochrone(searchParams);
                isochronesRequest.getIsoMaps().add(isochroneMap);
            }

        }
    }

    Float convertSmoothing(Double smoothingValue) throws ParameterValueException {
        float f = (float) smoothingValue.doubleValue();

        if (smoothingValue < 0 || smoothingValue > 100)
            throw new ParameterValueException(IsochronesErrorCodes.INVALID_PARAMETER_VALUE, IsochronesRequest.PARAM_SMOOTHING, smoothingValue.toString());

        return f;
    }

    String convertLocationType(IsochronesRequestEnums.LocationType locationType) throws ParameterValueException {
        IsochronesRequestEnums.LocationType value;

        switch (locationType) {
            case DESTINATION:
                value = IsochronesRequestEnums.LocationType.DESTINATION;
                break;
            case START:
                value = IsochronesRequestEnums.LocationType.START;
                break;
            default:
                throw new ParameterValueException(IsochronesErrorCodes.INVALID_PARAMETER_VALUE, IsochronesRequest.PARAM_LOCATION_TYPE, locationType.toString());
        }

        return value.toString();
    }

    TravelRangeType convertRangeType(IsochronesRequestEnums.RangeType rangeType) throws ParameterValueException {
        TravelRangeType travelRangeType;

        switch (rangeType) {
            case DISTANCE:
                travelRangeType = TravelRangeType.DISTANCE;
                break;
            case TIME:
                travelRangeType = TravelRangeType.TIME;
                break;
            default:
                throw new ParameterValueException(IsochronesErrorCodes.INVALID_PARAMETER_VALUE, IsochronesRequest.PARAM_RANGE_TYPE, rangeType.toString());
        }

        return travelRangeType;

    }

    String convertAreaUnit(APIEnums.Units unitsIn) throws ParameterValueException {

        DistanceUnit convertedAreaUnit;
        try {
            convertedAreaUnit = DistanceUnitUtil.getFromString(unitsIn.toString(), DistanceUnit.UNKNOWN);
            if (convertedAreaUnit == DistanceUnit.UNKNOWN)
                throw new ParameterValueException(IsochronesErrorCodes.INVALID_PARAMETER_VALUE, IsochronesRequest.PARAM_AREA_UNITS, unitsIn.toString());

            return DistanceUnitUtil.toString(convertedAreaUnit);

        } catch (Exception e) {
            throw new ParameterValueException(IsochronesErrorCodes.INVALID_PARAMETER_VALUE, IsochronesRequest.PARAM_AREA_UNITS, unitsIn.toString());
        }
    }

    String convertRangeUnit(APIEnums.Units unitsIn) throws ParameterValueException {

        DistanceUnit units;
        try {
            units = DistanceUnitUtil.getFromString(unitsIn.toString(), DistanceUnit.UNKNOWN);
            if (units == DistanceUnit.UNKNOWN)
                throw new ParameterValueException(IsochronesErrorCodes.INVALID_PARAMETER_VALUE, IsochronesRequest.PARAM_RANGE_UNITS, unitsIn.toString());
        } catch (Exception e) {
            throw new ParameterValueException(IsochronesErrorCodes.INVALID_PARAMETER_VALUE, IsochronesRequest.PARAM_RANGE_UNITS, unitsIn.toString());
        }
        return DistanceUnitUtil.toString(units);

    }

    Coordinate convertSingleCoordinate(Double[] coordinate) throws ParameterValueException {
        Coordinate realCoordinate;
        if (coordinate.length != 2) {
            throw new ParameterValueException(IsochronesErrorCodes.INVALID_PARAMETER_VALUE, IsochronesRequest.PARAM_LOCATIONS);
        }
        try {
            realCoordinate = new Coordinate(coordinate[0], coordinate[1]);
        } catch (Exception e) {
            throw new ParameterValueException(IsochronesErrorCodes.INVALID_PARAMETER_VALUE, IsochronesRequest.PARAM_LOCATIONS);
        }
        return realCoordinate;
    }

    IsochroneRequest convertIsochroneRequest(IsochronesRequest isochronesRequest) throws Exception {
        IsochroneRequest convertedIsochroneRequest = new IsochroneRequest();
        EndpointsProperties.EndpointIsochronesProperties isochroneProperties = endpointsProperties.getIsochrones();
        convertedIsochroneRequest.setMaximumLocations(isochroneProperties.getMaximumLocations());
        convertedIsochroneRequest.setAllowComputeArea(isochroneProperties.isAllowComputeArea());
        convertedIsochroneRequest.setMaximumIntervals(isochroneProperties.getMaximumIntervals());
        convertedIsochroneRequest.setMaximumRangeDistanceDefault(isochroneProperties.getMaximumRangeDistanceDefault());
        convertedIsochroneRequest.setProfileMaxRangeDistances(isochroneProperties.getProfileMaxRangeDistances());
        convertedIsochroneRequest.setMaximumRangeDistanceDefaultFastisochrones(isochroneProperties.getFastisochrones().getMaximumRangeDistanceDefault());
        convertedIsochroneRequest.setProfileMaxRangeDistancesFastisochrones(isochroneProperties.getFastisochrones().getProfileMaxRangeDistances());
        convertedIsochroneRequest.setMaximumRangeTimeDefault(isochroneProperties.getMaximumRangeTimeDefault());
        convertedIsochroneRequest.setProfileMaxRangeTimes(isochroneProperties.getProfileMaxRangeTimes());
        convertedIsochroneRequest.setMaximumRangeTimeDefaultFastisochrones(isochroneProperties.getFastisochrones().getMaximumRangeTimeDefault());
        convertedIsochroneRequest.setProfileMaxRangeTimesFastisochrones(isochroneProperties.getFastisochrones().getProfileMaxRangeTimes());
        convertedIsochroneRequest.setStatsProviders(constructStatisticsProvidersConfiguration(isochroneProperties.getStatisticsProviders()));

        for (int i = 0; i < isochronesRequest.getLocations().length; i++) {
            Double[] location = isochronesRequest.getLocations()[i];
            TravellerInfo travellerInfo = this.constructTravellerInfo(isochronesRequest, location);
            travellerInfo.setId(Integer.toString(i));
            try {
                convertedIsochroneRequest.addTraveller(travellerInfo);
            } catch (Exception ex) {
                throw new InternalServerException(IsochronesErrorCodes.UNKNOWN, IsochronesRequest.PARAM_INTERVAL);
            }
        }
        if (isochronesRequest.hasId())
            convertedIsochroneRequest.setId(isochronesRequest.getId());
        if (isochronesRequest.hasRangeUnits())
            convertedIsochroneRequest.setUnits(convertRangeUnit(isochronesRequest.getRangeUnit()));
        if (isochronesRequest.hasAreaUnits())
            convertedIsochroneRequest.setAreaUnits(convertAreaUnit(isochronesRequest.getAreaUnit()));
        if (isochronesRequest.hasAttributes())
            convertedIsochroneRequest.setAttributes(convertAttributes(isochronesRequest.getAttributes()));
        if (isochronesRequest.hasSmoothing())
            convertedIsochroneRequest.setSmoothingFactor(convertSmoothing(isochronesRequest.getSmoothing()));
        if (isochronesRequest.hasIntersections())
            convertedIsochroneRequest.setIncludeIntersections(isochronesRequest.getIntersections());
        if (isochronesRequest.hasOptions())
            convertedIsochroneRequest.setCalcMethod(convertCalcMethod(CONCAVE_BALLS));
        else
            convertedIsochroneRequest.setCalcMethod(convertCalcMethod(FASTISOCHRONE));
        return convertedIsochroneRequest;

    }

    Map<String, StatisticsProviderConfiguration> constructStatisticsProvidersConfiguration(Map<String, EndpointsProperties.EndpointIsochronesProperties.StatisticsProviderProperties> statsProperties) {
        Map<String, StatisticsProviderConfiguration> statsProviders = new HashMap<>();

        if (statsProperties != null) {
            int id = 0;
            for (EndpointsProperties.EndpointIsochronesProperties.StatisticsProviderProperties providerProperties : statsProperties.values()) {
                Map<String, String> propertyMapping = providerProperties.getPropertyMapping();
                Map<String, String> propMapping = new HashMap<>();
                for (Map.Entry<String, String> propEntry : propertyMapping.entrySet())
                    propMapping.put(propEntry.getValue(), propEntry.getKey());
                if (propMapping.size() > 0) {
                    StatisticsProviderConfiguration provConfig = new StatisticsProviderConfiguration(id++, providerProperties.getProviderName(), providerProperties.getProviderParameters(), propMapping, providerProperties.getAttribution());
                    for (Map.Entry<String, String> property : propMapping.entrySet())
                        statsProviders.put(property.getKey().toLowerCase(), provConfig);
                }
            }
        }

        return statsProviders;
    }

    TravellerInfo constructTravellerInfo(IsochronesRequest isochronesRequest, Double[] coordinate) throws Exception {
        TravellerInfo travellerInfo = new TravellerInfo();

        RouteSearchParameters routeSearchParameters = constructRouteSearchParameters(isochronesRequest);
        travellerInfo.setRouteSearchParameters(routeSearchParameters);
        if (isochronesRequest.hasRangeType())
            travellerInfo.setRangeType(convertRangeType(isochronesRequest.getRangeType()));
        if (isochronesRequest.hasLocationType())
            travellerInfo.setLocationType(convertLocationType(isochronesRequest.getLocationType()));
        travellerInfo.setLocation(convertSingleCoordinate(coordinate));
        travellerInfo.getRanges();
        //range + interval
        if (isochronesRequest.getRange() == null) {
            throw new ParameterValueException(IsochronesErrorCodes.MISSING_PARAMETER, IsochronesRequest.PARAM_RANGE);
        }
        List<Double> rangeValues = isochronesRequest.getRange();
        Double intervalValue = isochronesRequest.getInterval();
        setRangeAndIntervals(travellerInfo, rangeValues, intervalValue);
        return travellerInfo;
    }

    RouteSearchParameters constructRouteSearchParameters(IsochronesRequest isochronesRequest) throws Exception {
        RouteSearchParameters routeSearchParameters = new RouteSearchParameters();
        int profileType;
        try {
            profileType = convertToIsochronesProfileType(isochronesRequest.getProfile());
        } catch (Exception e) {
            throw new ParameterValueException(IsochronesErrorCodes.INVALID_PARAMETER_VALUE, IsochronesRequest.PARAM_PROFILE);
        }

        if (profileType == RoutingProfileType.UNKNOWN)
            throw new ParameterValueException(IsochronesErrorCodes.INVALID_PARAMETER_VALUE, IsochronesRequest.PARAM_PROFILE);
        routeSearchParameters.setProfileType(profileType);

        if (isochronesRequest.hasOptions()) {
            routeSearchParameters = processIsochronesRequestOptions(isochronesRequest, routeSearchParameters);
        }
        if (isochronesRequest.hasTime()) {
            routeSearchParameters.setDeparture(isochronesRequest.getTime());
            routeSearchParameters.setArrival(isochronesRequest.getTime());
        }
        routeSearchParameters.setConsiderTurnRestrictions(false);
        return routeSearchParameters;
    }

    RouteSearchParameters processIsochronesRequestOptions(IsochronesRequest isochronesRequest, RouteSearchParameters parameters) throws StatusCodeException {
        RouteRequestOptions options = isochronesRequest.getIsochronesOptions();
        parameters = processRequestOptions(options, parameters);
        if (options.hasProfileParams())
            parameters.setProfileParams(convertParameters(options, parameters.getProfileType()));
        return parameters;
    }

    void validateAgainstConfig(IsochroneRequest isochroneRequest, List<TravellerInfo> travellers) throws StatusCodeException {
        if (!isochroneRequest.isAllowComputeArea() && isochroneRequest.hasAttribute("area"))
            throw new StatusCodeException(StatusCode.BAD_REQUEST, IsochronesErrorCodes.FEATURE_NOT_SUPPORTED, "Area computation is not enabled.");

        if (travellers.size() > isochroneRequest.getMaximumLocations())
            throw new ParameterOutOfRangeException(IsochronesErrorCodes.PARAMETER_VALUE_EXCEEDS_MAXIMUM, IsochronesRequest.PARAM_LOCATIONS, Integer.toString(travellers.size()), Integer.toString(isochroneRequest.getMaximumLocations()));

        for (TravellerInfo traveller : travellers) {
            int maxAllowedRange = getMaximumRange(traveller, isochroneRequest);
            double maxRange = traveller.getMaximumRange();
            if (maxRange > maxAllowedRange)
                throw new ParameterOutOfRangeException(IsochronesErrorCodes.PARAMETER_VALUE_EXCEEDS_MAXIMUM, IsochronesRequest.PARAM_RANGE, Double.toString(maxRange), Integer.toString(maxAllowedRange));

            int maxIntervals = isochroneRequest.getMaximumIntervals();
            if (maxIntervals > 0 && maxIntervals < traveller.getRanges().length) {
                throw new ParameterOutOfRangeException(IsochronesErrorCodes.PARAMETER_VALUE_EXCEEDS_MINIMUM, IsochronesRequest.PARAM_INTERVAL, "Resulting number of " + traveller.getRanges().length + " isochrones exceeds maximum value of " + maxIntervals + ".");
            }
        }

    }

    private static int getMaximumRange(TravellerInfo traveller, IsochroneRequest isochroneRequest) {
        int profileType = traveller.getRouteSearchParameters().getProfileType();
        TravelRangeType range = traveller.getRangeType();
        String calcMethod = isochroneRequest.getCalcMethod();
        Integer res;

        RoutingProfileManager rpm = RoutingProfileManager.getInstance();
        FastIsochroneFactory fastIsochroneFactory = rpm.getProfiles().getRouteProfile(profileType).getGraphhopper().getFastIsochroneFactory();
        if (fastIsochroneFactory.isEnabled() && calcMethod.equalsIgnoreCase("fastisochrone"))
            return getMaximumRangeFastIsochrone(traveller, isochroneRequest);

        if (range == DISTANCE) {
            res = isochroneRequest.getProfileMaxRangeDistances().get(profileType);
            if (res == null)
                res = isochroneRequest.getMaximumRangeDistanceDefault();
        } else {
            res = isochroneRequest.getProfileMaxRangeTimes().get(profileType);
            if (res == null)
                res = isochroneRequest.getMaximumRangeTimeDefault();
        }

        return res;
    }

    private static int getMaximumRangeFastIsochrone(TravellerInfo traveller, IsochroneRequest isochroneRequest) {
        int profileType = traveller.getRouteSearchParameters().getProfileType();
        TravelRangeType range = traveller.getRangeType();
        Integer res;

        if (range == DISTANCE) {
            res = isochroneRequest.getProfileMaxRangeDistancesFastisochrones().get(profileType);
            if (res == null)
                res = isochroneRequest.getMaximumRangeDistanceDefaultFastisochrones();
        } else {
            res = isochroneRequest.getProfileMaxRangeTimesFastisochrones().get(profileType);
            if (res == null)
                res = isochroneRequest.getMaximumRangeTimeDefaultFastisochrones();
        }

        return res;
    }

    void setRangeAndIntervals(TravellerInfo travellerInfo, List<Double> rangeValues, Double intervalValue) throws ParameterValueException, ParameterOutOfRangeException {
        double rangeValue = -1;
        if (rangeValues.size() == 1) {
            try {
                rangeValue = rangeValues.get(0);
                travellerInfo.setRanges(new double[]{rangeValue});
            } catch (NumberFormatException ex) {
                throw new ParameterValueException(IsochronesErrorCodes.INVALID_PARAMETER_VALUE, "range");
            }
        } else {
            double[] ranges = new double[rangeValues.size()];
            double maxRange = Double.MIN_VALUE;
            for (int i = 0; i < ranges.length; i++) {
                double dv = rangeValues.get(i);
                if (dv > maxRange)
                    maxRange = dv;
                ranges[i] = dv;
            }
            Arrays.sort(ranges);
            travellerInfo.setRanges(ranges);
        }
        // interval, only use if one range is defined

        if (rangeValues.size() == 1 && rangeValue != -1 && intervalValue != null) {
            if (intervalValue > rangeValue) {
                throw new ParameterOutOfRangeException(IsochronesErrorCodes.PARAMETER_VALUE_EXCEEDS_MAXIMUM, IsochronesRequest.PARAM_INTERVAL, Double.toString(intervalValue), Double.toString(rangeValue));
            }
            travellerInfo.setRanges(rangeValue, intervalValue);
        }
    }

    String convertCalcMethod(IsochronesRequestEnums.CalculationMethod bareCalcMethod) throws ParameterValueException {
        try {
            switch (bareCalcMethod) {
                case CONCAVE_BALLS:
                    return "concaveballs";
                case GRID:
                    return "grid";
                case FASTISOCHRONE:
                    return "fastisochrone";
                default:
                    return "none";
            }
        } catch (Exception ex) {
            throw new ParameterValueException(IsochronesErrorCodes.INVALID_PARAMETER_VALUE, "calc_method");
        }
    }

}