diff --git a/changelog.md b/changelog.md index f8a0897..6d890e1 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,10 @@ # CHANGELOG +## 2.2.5 + +FEAT: +- The native OSRM API v1 was added but some parameters and error responses are still missing + ## 2.2.4 FIXED: - The pg module can emit error event and they were not catched and so it caused some crashs of Road2 diff --git a/docker/config/service.json b/docker/config/service.json index b3b4c68..1dec91f 100644 --- a/docker/config/service.json +++ b/docker/config/service.json @@ -58,6 +58,10 @@ { "name" : "simple", "version" : "1.0.0" + }, + { + "name" : "osrm", + "version" : "1.0.0" } ] } diff --git a/docker/distributions/debian/Dockerfile b/docker/distributions/debian/Dockerfile index eb8dbc8..07b6fea 100644 --- a/docker/distributions/debian/Dockerfile +++ b/docker/distributions/debian/Dockerfile @@ -28,7 +28,7 @@ RUN apt-get update && \ libsqlite3-mod-spatialite libzmq3-dev libczmq-dev ### Installation prime-server -COPY --from=build /usr/local/lib/libprime_server.so.0.7.0 /usr/lib/libprime_server.so.0.0.0 +COPY --from=build /usr/local/lib/libprime_server.so.0.7.1 /usr/lib/libprime_server.so.0.0.0 COPY --from=build /usr/local/lib/libprime_server.so.0 /usr/lib/libprime_server.so.0 COPY --from=build /usr/local/lib/libprime_server.so /usr/lib/libprime_server.so diff --git a/documentation/apis/osrm/1.0.0/api.json b/documentation/apis/osrm/1.0.0/api.json new file mode 100644 index 0000000..3ccd607 --- /dev/null +++ b/documentation/apis/osrm/1.0.0/api.json @@ -0,0 +1,262 @@ +{ + "openapi": "3.0.0", + "info": { + "description": "Description of the OSRM API available via Road2. Only route operation in its v1 is available.", + "version": "1.0.0", + "title": "OSRM API inside Road2", + "contact": { + "email": "contact.geoservices@ign.fr" + } + }, + "servers": [ + { + "url": "http://localhost:8080/osrm/1.0.0/", + "description": "Local server" + } + ], + "tags": [ + { + "name": "Discover", + "description": "Discover the resources available on this instance." + }, + { + "name": "Route", + "description": "Ask for one or multiple routes." + } + ], + "paths": { + "/resources": { + "get": { + "tags": [ + "Discover" + ], + "summary": "Request used to discover the resources available on this instance.", + "description": "", + "operationId": "resources", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/resources" + } + } + } + }, + "404": { + "description": "Not found" + } + } + } + }, + "/{resourceId}/{profileId}/{optimizationId}/route/v1/_/{coordinates}": { + "get": { + "tags": [ + "Route" + ], + "summary": "Compute a route", + "description": "This API route is the same of [the OSRM v1 route](http://project-osrm.org/docs/v5.5.1/api/#route-service). But the profile parameter is separated in three parameters : resourceId, profileId and optimizationId. It allows us to have the concepts of Road2 and a better compatibility with the native OSRM API.", + "operationId": "route", + "parameters": [ + { + "name": "resourceId", + "in": "path", + "description": "The resource used for the compute. The list of resources is available on /resources. A resource is a concept of Road2, it is an agregation of multiple graphs. For instance, an unique topology with multiple costs.", + "required": true, + "schema": { + "type": "string" + }, + "example": "bdtopo-osrm" + }, + { + "name": "coordinates", + "in": "path", + "description": "String of format {longitude},{latitude};{longitude},{latitude}[;{longitude},{latitude} ...] or polyline({polyline}).", + "required": true, + "schema": { + "type": "string" + }, + "example": "2.337306,48.849319" + }, + { + "name": "profileId", + "in": "path", + "description": "The profile used for the compute. The list of profiles per resource is available on /resources.", + "required": true, + "schema": { + "type": "string" + }, + "example": "car" + }, + { + "name": "optimizationId", + "in": "path", + "description": "The optimization used for the compute. The list of optimizations per resource is available on /resources.", + "required": true, + "schema": { + "type": "string" + }, + "example": "fastest" + }, + { + "name": "alternatives", + "in": "query", + "description": "Search for alternative routes and return as well. Please note that even if an alternative route is requested, a result cannot be guaranteed.", + "required": false, + "schema": { + "type": "string" + }, + "example": "false" + }, + { + "name": "steps", + "in": "query", + "description": "Return route steps for each route leg.", + "required": false, + "schema": { + "type": "string" + }, + "example": "false" + }, + { + "name": "annotations", + "in": "query", + "description": "Returns additional metadata for each coordinate along the route geometry.", + "required": false, + "schema": { + "type": "string" + }, + "example": "false" + }, + { + "name": "geometries", + "in": "query", + "description": "Returned route geometry format (influences overview and per step). Values can be : polyline (default), polyline6 , geojson.", + "required": false, + "schema": { + "type": "string" + }, + "example": "polyline" + }, + { + "name": "overview", + "in": "query", + "description": "Add overview geometry either full, simplified according to highest zoom level it could be display on, or not at all. Values can be : simplified (default), full , false.", + "required": false, + "schema": { + "type": "string" + }, + "example": "simplified" + }, + { + "name": "continue_straight", + "in": "query", + "description": "Forces the route to keep going straight at waypoints constraining uturns there even if it would be faster. Default value depends on the profile. Values can be : default (default), true , false. ", + "required": false, + "schema": { + "type": "string" + }, + "example": "default" + } + ], + "responses": { + "200": { + "description": "Successful operation. For all the details, see [the OSRM documentation](http://project-osrm.org/docs/v5.5.1/api/#route-service).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/osrmValidResponse" + } + } + } + }, + "400": { + "description": "Error. For all the details, see [the OSRM documentation](http://project-osrm.org/docs/v5.5.1/api/#route-service).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/osrmErrorResponse" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "osrmErrorResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "osrmValidResponse": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description" : "if the request was successful Ok otherwise see the service dependent and general status codes on the [the OSRM documentation](http://project-osrm.org/docs/v5.5.1/api/#route-service)." + }, + "routes": { + "type": "array", + "items": { + "type": "object", + "description": "For all the details, see the OSRM documentation of a [route object](http://project-osrm.org/docs/v5.5.1/api/#route-object)." + } + }, + "waypoints": { + "type": "array", + "items": { + "type": "object", + "description": "For all the details, see the OSRM documentation of a [waypoint object](http://project-osrm.org/docs/v5.5.1/api/#waypoint-object)." + } + } + } + }, + "resources": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "description": { + "type": "string" + }, + "profiles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "optimizations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/documentation/apis/valhalla/1.0.0/api.json b/documentation/apis/valhalla/1.0.0/api.json new file mode 100644 index 0000000..f908a17 --- /dev/null +++ b/documentation/apis/valhalla/1.0.0/api.json @@ -0,0 +1,178 @@ +{ + "openapi": "3.0.0", + "info": { + "description": "Description of the Valhalla API available via Road2. Only the isochrone API is available for now.", + "version": "1.0.0", + "title": "Valhalla API inside Road2", + "contact": { + "email": "contact.geoservices@ign.fr" + } + }, + "servers": [ + { + "url": "http://localhost:8080/valhalla/1.0.0/", + "description": "Local server" + } + ], + "tags": [ + { + "name": "Discover", + "description": "Discover the resources available on this instance." + }, + { + "name": "Isolines", + "description": "Ask for one or multiple isolines." + } + ], + "paths": { + "/resources": { + "get": { + "tags": [ + "Discover" + ], + "summary": "Request used to discover the resources available on this instance.", + "description": "", + "operationId": "resources", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/resources" + } + } + } + }, + "404": { + "description": "Not found" + } + } + } + }, + "/resource/{resourceId}/{profileId}/{optimizationId}/isochrone": { + "get": { + "tags": [ + "Isolines" + ], + "summary": "Compute a route", + "description": "This API route is the same of [the valhalla isochrone](https://valhalla.github.io/valhalla/api/isochrone/api-reference/). But there are three additional parameters : resourceId, profileId and optimizationId. It allows us to have the concepts of Road2 and a better compatibility with the native Valhalla API.", + "operationId": "isochrone", + "parameters": [ + { + "name": "resourceId", + "in": "path", + "description": "The resource used for the compute. The list of resources is available on /resources. A resource is a concept of Road2, it is an agregation of multiple graphs. For instance, an unique topology with multiple costs.", + "required": true, + "schema": { + "type": "string" + }, + "example": "bdtopo-osrm" + }, + { + "name": "profileId", + "in": "path", + "description": "The profile used for the compute. The list of profiles per resource is available on /resources.", + "required": true, + "schema": { + "type": "string" + }, + "example": "car" + }, + { + "name": "optimizationId", + "in": "path", + "description": "The optimization used for the compute. The list of optimizations per resource is available on /resources.", + "required": true, + "schema": { + "type": "string" + }, + "example": "fastest" + }, + { + "name": "json", + "in": "query", + "description": "That is a JSON which contains all the parameters for the computation. The parameter costing is always cost and the real value is given with resourceId/profileId/optimizationId. For all the details, see [the valhalla docmentation](https://valhalla.github.io/valhalla/api/isochrone/api-reference/)", + "required": false, + "schema": { + "type": "string" + }, + "example": "{\"locations\":[{\"lat\":40.744014,\"lon\":-73.990508}],\"costing\":\"cost\",\"contours\":[{\"time\":15.0,\"color\":\"ff0000\"}]}&id=Walk_From_Office" + } + ], + "responses": { + "200": { + "description": "Successful operation. The service returns a [GeoJSON](http://geojson.org/). For more details, see [the valhalla documentation](https://valhalla.github.io/valhalla/api/isochrone/api-reference/).", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "4XX": { + "description": "Error. For more details, see [the valhalla documentation](https://valhalla.github.io/valhalla/api/turn-by-turn/api-reference/#http-status-codes-and-conditions).", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "5XX": { + "description": "Error. For more details, see [the valhalla documentation](https://valhalla.github.io/valhalla/api/turn-by-turn/api-reference/#http-status-codes-and-conditions).", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "resources": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "description": { + "type": "string" + }, + "profiles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "optimizations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + } \ No newline at end of file diff --git a/package.json b/package.json index a6fb385..a2dcb62 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "road2", - "version": "2.2.4", + "version": "2.2.5", "description": "Calcul d'itinéraire", "author": "RDEV - IGN", "main": "src/js/road2.js", diff --git a/src/js/apis/admin/1.0.0/update.js b/src/js/apis/admin/1.0.0/update.js index 78bebfe..6b6a339 100644 --- a/src/js/apis/admin/1.0.0/update.js +++ b/src/js/apis/admin/1.0.0/update.js @@ -2,7 +2,7 @@ const log4js = require('log4js'); -var LOGGER = log4js.getLogger("INIT"); +var LOGGER = log4js.getLogger("UPDATE"); module.exports = { diff --git a/src/js/apis/osrm/1.0.0/controller/controller.js b/src/js/apis/osrm/1.0.0/controller/controller.js new file mode 100644 index 0000000..b1dc53c --- /dev/null +++ b/src/js/apis/osrm/1.0.0/controller/controller.js @@ -0,0 +1,457 @@ +'use strict'; + +const log4js = require('log4js'); + +const polyline = require('@mapbox/polyline'); + +const copyManager = require('../../../../utils/copyManager'); +const Distance = require('../../../../geography/distance'); +const Duration = require('../../../../time/duration'); +const errorManager = require('../../../../utils/errorManager'); +const Point = require('../../../../geometry/point'); +const RouteRequest = require('../../../../requests/routeRequest'); + +let LOGGER = log4js.getLogger("CONTROLLER"); + +module.exports = { + + /** + * + * @function + * @name checkRouteParameters + * @description Check parameters for a request on /route + * @param {object} parameters - request parameters + * @param {object} service - Service class' instance + * @param {string} method - request method + * @return {object} RouteRequest - RouteRequest class' instance + * + */ + + checkRouteParameters: function(parameters, service, method) { + + let resource; + let start = {}; + let end = {}; + let profile; + let optimization; + let coordinatesSequence; + let askedProjection; + + LOGGER.debug("checkRouteParameters()"); + + // Resource + if (!parameters.resource) { + throw errorManager.createError(" Parameter 'resourceId' not found ", 400); + } else { + + LOGGER.debug("user resource:"); + LOGGER.debug(parameters.resource); + + // Check resource availability, and compatibility between its type and the request + if (!service.verifyResourceExistenceById(parameters.resource)) { + throw errorManager.createError(" Parameter 'resourceId' is invalid: it does not exist on this service ", 400); + } else { + + resource = service.getResourceById(parameters.resource); + // Check if this operation is allowed for this resource + if (!resource.verifyAvailabilityOperation("route")){ + throw errorManager.createError(" Operation 'route' is not permitted on this resource ", 400); + } else { + LOGGER.debug("operation route valide on this resource"); + } + + } + } + + // Get route operation to check some things + let routeOperation = resource.getOperationById("route"); + askedProjection = routeOperation.getParameterById("projection").defaultValueContent; + LOGGER.debug("default crs: " + askedProjection); + + + // Profile and Optimization + // --- + + if (!parameters.profile) { + throw errorManager.createError(" Parameter 'profileId' not found", 400); + } else { + LOGGER.debug("user profile:"); + LOGGER.debug(parameters.profile); + // Parameter's validity check + let validity = routeOperation.getParameterById("profile").check(parameters.profile); + if (validity.code !== "ok") { + throw errorManager.createError(" Parameter 'profileId' is invalid: " + validity.message, 400); + } else { + profile = parameters.profile; + LOGGER.debug("user profile valide"); + } + } + + if (!parameters.optimization) { + throw errorManager.createError(" Parameter 'optimizationId' not found", 400); + } else { + LOGGER.debug("user optimization:"); + LOGGER.debug(parameters.optimization); + // Parameter's validity check + let validity = routeOperation.getParameterById("optimization").check(parameters.optimization); + if (validity.code !== "ok") { + throw errorManager.createError(" Parameter 'optimizationId' is invalid: " + validity.message, 400); + } else { + optimization = parameters.optimization; + LOGGER.debug("user optimization is valid"); + } + } + + // OSRM API specific parameters + // coordinates (2 possible formats) + if (!parameters.coordinates) { + throw errorManager.createError(" Parameter 'coordinates' not found", 400); + } else { + LOGGER.debug("raw coordinates:"); + LOGGER.debug(parameters.coordinates); + + let rawStringPattern = /^-?\d+(\.\d+)?,-?\d+(\.\d+)?(;-?\d+(\.\d+)?,-?\d+(\.\d+)?)+$/; + let polylinePattern = /^polyline\(\S+\)$/; + + if (rawStringPattern.test(parameters.coordinates)) { + LOGGER.debug("coordinates are expressed in list format"); + coordinatesSequence = []; + const coordinatesMatchList = parameters.coordinates.matchAll(/(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)/g); + LOGGER.debug("coordinates matches list:"); + LOGGER.debug(coordinatesMatchList); + for (const matchItem of coordinatesMatchList) { + LOGGER.debug("coordinates match:"); + LOGGER.debug(matchItem); + coordinatesSequence.push([parseFloat(matchItem[1]), parseFloat(matchItem[2])]); + } + } else if (polylinePattern.test(parameters.coordinates)) { + LOGGER.debug("coordinates are expressed in polyline format"); + coordinatesSequence = polyline.decode(parameters.coordinates); + } else { + throw errorManager.createError(" Parameter 'coordinates' is invalid: does not match allowed formats", 400); + } + LOGGER.debug("coordinates sequence:"); + LOGGER.debug(coordinatesSequence); + + if (coordinatesSequence.length >= 2) { + // Route start point + parameters.start = coordinatesSequence[0].join(","); + LOGGER.debug("user start:"); + LOGGER.debug(parameters.start); + let validity = routeOperation.getParameterById("start").check(parameters.start, askedProjection); + if (validity.code !== "ok") { + throw errorManager.createError(" Parameter 'start' is invalid: " + validity.message, 400); + } else { + LOGGER.debug("user start valide") + start = new Point(coordinatesSequence[0][0], coordinatesSequence[0][1], askedProjection); + LOGGER.debug("user start in road2' object:"); + LOGGER.debug(start); + } + validity = null; + + // Route end point + parameters.end = coordinatesSequence[coordinatesSequence.length-1].join(","); + LOGGER.debug("user end:"); + LOGGER.debug(parameters.end); + validity = routeOperation.getParameterById("end").check(parameters.end, askedProjection); + if (validity.code !== "ok") { + throw errorManager.createError(" Parameter 'end' is invalid: " + validity.message, 400); + } else { + LOGGER.debug("user end valide") + end = new Point(coordinatesSequence[coordinatesSequence.length-1][0], coordinatesSequence[coordinatesSequence.length-1][1], askedProjection); + LOGGER.debug("user end in road2' object:"); + LOGGER.debug(end); + } + } else { + throw errorManager.createError(" Parameter 'coordinates' is invalid: it must contain at least two points"); + } + } + + // Instanciate routeRequest with mandatory parameters + let routeRequest = new RouteRequest(parameters.resource, start, end, profile, optimization); + + LOGGER.debug(routeRequest); + + // Check profile's validity and compatibility with the chosen optimization + if (!resource.checkSourceAvailibilityFromRequest(routeRequest)) { + throw errorManager.createError(" Parameters 'profile' and 'optimization' are not compatible ", 400); + } else { + LOGGER.debug("profile et optimization compatibles"); + } + + // Intermediate points + if (coordinatesSequence.length > 2) { + + LOGGER.debug("user intermediates:"); + LOGGER.debug(coordinatesSequence.slice(1, coordinatesSequence.length-1)); + + let finalIntermediates = ""; + for (let i = 1; i < (coordinatesSequence.length - 2); i++) { + finalIntermediates = finalIntermediates.concat(coordinatesSequence[i].join(","), "|"); + } + finalIntermediates = finalIntermediates.concat(coordinatesSequence[coordinatesSequence.length - 2].join(",")); + + // Check coordinates validity + let validity = routeOperation.getParameterById("intermediates").check(finalIntermediates, askedProjection); + if (validity.code !== "ok") { + throw errorManager.createError(" Parameter 'coordinates' is invalid: " + validity.message, 400); + } else { + + LOGGER.debug("valid intermediates"); + + if (!routeOperation.getParameterById("intermediates").convertIntoTable(finalIntermediates, routeRequest.intermediates, askedProjection)) { + throw errorManager.createError(" Parameter 'intermediates' is invalid. Wrong format or out of the bbox. ", 400); + } else { + LOGGER.debug("intermediates in a table:"); + LOGGER.debug(routeRequest.intermediates); + } + } + } + + // steps (OSRM) / getSteps (Road2) + if (parameters.steps) { + LOGGER.debug("user getSteps:"); + LOGGER.debug(parameters.steps); + + // Check coordinates validity + let validity = routeOperation.getParameterById("getSteps").check(parameters.steps); + if (validity.code !== "ok") { + throw errorManager.createError(" Parameter 'steps' is invalid: " + validity.message, 400); + } else { + + routeRequest.computeSteps = routeOperation.getParameterById("getSteps").specificConvertion(parameters.steps) + if (routeRequest.computeSteps === null) { + throw errorManager.createError(" Parameter 'steps' is invalid ", 400); + } else { + LOGGER.debug("converted getSteps: " + routeRequest.computeSteps); + } + } + } else { + + // Default value could be taken from configuration + // routeRequest.computeSteps = routeOperation.getParameterById("getSteps").defaultValueContent; + // but we take the default of OSRM HTTP API : false + // (see http://project-osrm.org/docs/v5.5.1/api/#route-service) + routeRequest.computeSteps = false; + LOGGER.debug("general default getSteps: " + routeRequest.computeSteps); + + } + + // geometries (OSRM) / geometryFormat (Road2) + if (parameters.geometries) { + + LOGGER.debug("user geometryFormat:"); + LOGGER.debug(parameters.geometries); + + // Check coordinates validity + let validity = routeOperation.getParameterById("geometryFormat").check(parameters.geometries); + if (validity.code !== "ok") { + throw errorManager.createError(" Parameter 'geometries' is invalid: " + validity.message, 400); + } else { + LOGGER.debug("geometryFormat valide"); + } + + routeRequest.geometryFormat = parameters.geometries; + + } else { + + // Default value could be taken from configuration + // routeRequest.geometryFormat = routeOperation.getParameterById("geometryFormat").defaultValueContent; + // but we take the default of OSRM HTTP API : polyline + // (see http://project-osrm.org/docs/v5.5.1/api/#route-service) + routeRequest.geometryFormat = "polyline"; + LOGGER.debug("default geometryFormat used: " + routeRequest.geometryFormat); + + } + + /* The following OSRM optional parameters are ignored for now, as they don't seem to match any routeRequest property: + - alternatives + - annotations + - bearings + - continue_straight (can maybe be converted to a "constraints" routeRequest property) + - format + - hints + - overview (always "full" in road2) + - radiuses + */ + // optional routeRequest parameters with no OSRM equivalent are ignored or fixed + + routeRequest.bbox = false + routeRequest.distanceUnit = "meter" + routeRequest.timeUnit = "second" + + return routeRequest; + + }, + + /** + * + * @function + * @name writeRouteResponse + * @description Rewrite engine's response to respond to a /route request + * @param {object} RouteRequest - RouteRequest class instance + * @param {object} RouteResponse - RouteResponse class instance + * @param {object} service - Service class instance + * @return {object} userResponse - Response body to serialize for the user + * + */ + + writeRouteResponse: function(routeRequest, routeResponse, service) { + + LOGGER.debug("writeRouteResponse()"); + + // Initialize userResponse + let userResponse = { + "code": routeResponse.engineExtras.code + }; + LOGGER.debug("Engine response code :"); + LOGGER.debug(userResponse.code); + + // Waypoints + let waypointArray = copyManager.deepCopy(routeResponse.engineExtras.waypoints); + let startingPoint = routeResponse.routes[0].portions[0].start; + waypointArray[0].location = [startingPoint.x, startingPoint.y]; + for (let i = 1; i < waypointArray.length; i++) { + let point = routeResponse.routes[0].portions[i-1].end; + waypointArray[i].location = [point.x, point.y]; + } + userResponse.waypoints = waypointArray; + LOGGER.debug("Waypoints :"); + LOGGER.debug(userResponse.waypoints); + + // Routes + let routeArray = new Array(); + for (let routeIdx = 0; routeIdx < routeResponse.routes.length; routeIdx++) { + let simpleRoute = routeResponse.routes[routeIdx]; // from road2 standard response + let extraRoute = routeResponse.engineExtras.routes[routeIdx]; // from engine specific extrasz + // both sources will be fused to craft a response compliant with OSRM's official API definition + + // Geometry + routeArray[routeIdx] = {}; + routeArray[routeIdx].geometry = simpleRoute.geometry.getGeometryWithFormat(routeRequest.geometryFormat); + LOGGER.debug("Route " + routeIdx + "'s geometry: " + userResponse.geometry); + + // Distance + if (!simpleRoute.distance.convert(routeRequest.distanceUnit)) { + throw errorManager.createError(" Error during conversion of route distance in response. ", 400); + } else { + routeArray[routeIdx].distance = simpleRoute.distance.value; + } + + // Duration + if (!simpleRoute.duration.convert(routeRequest.timeUnit)) { + throw errorManager.createError(" Error during conversion of route duration in response. ", 400); + } else { + routeArray[routeIdx].duration = simpleRoute.duration.value; + } + + // Legs (Road2's "portions") + let legArray = new Array(); + for (let legIdx = 0; legIdx < simpleRoute.portions.length; legIdx++) { + LOGGER.debug("Computing leg " + legIdx + " of route " + routeIdx); + let portion = simpleRoute.portions[legIdx]; // from road2 standard response + let leg = extraRoute.legs[legIdx]; // from engine specific extras + legArray[legIdx] = {}; + + // Distance + if (!portion.distance.convert(routeRequest.distanceUnit)) { + LOGGER.debug("error during distance conversion: distance " + portion.distance); + throw errorManager.createError(" Error during convertion of portion distance in response. ", 400); + } else { + legArray[legIdx].distance = portion.distance.value; + } + + // Duration + if (!portion.duration.convert(routeRequest.timeUnit)) { + LOGGER.debug("error during duration conversion: duration " + portion.duration); + throw errorManager.createError(" Error during convertion of portion duration in response. ", 400); + } else { + legArray[legIdx].duration = portion.duration.value; + } + + // Steps (optional) + let stepArray = new Array(); + if (routeRequest.computeSteps && portion.steps.length !== 0) { + + for (let stepIdx = 0; stepIdx < portion.steps.length; stepIdx++) { + + let simpleStep = portion.steps[stepIdx]; // from road2 standard response + let extraStep = leg.steps[stepIdx]; // from engine specific extras + + let tmpName = ""; + try { + tmpName = JSON.stringify(simpleStep.attributes.name); + } catch(error) { + LOGGER.debug(error); + } + + stepArray[stepIdx] = { + "geometry": simpleStep.geometry.getGeometryWithFormat(routeRequest.geometryFormat), + "intersections": copyManager.deepCopy(extraStep.intersections), + "maneuver": { + "type": simpleStep.instruction.type + }, + "mode": extraStep.mode, + "name": tmpName + }; + + // Distance + if (!simpleStep.distance.convert(routeRequest.distanceUnit)) { + LOGGER.debug("error during distance conversion: distance " + simpleStep.distance); + throw errorManager.createError(" Error during convertion of portion distance in response. ", 400); + } else { + stepArray[stepIdx].distance = simpleStep.distance.value; + } + + // Duration + if (!simpleStep.duration.convert(routeRequest.timeUnit)) { + LOGGER.debug("error during duration conversion: duration " + simpleStep.duration); + throw errorManager.createError(" Error during convertion of portion duration in response. ", 400); + } else { + stepArray[stepIdx].duration = simpleStep.duration.value; + } + + if (simpleStep.instruction.modifier) { + stepArray[stepIdx].maneuver.modifier = simpleStep.instruction.modifier; + } + if (simpleStep.instruction.exit) { + stepArray[stepIdx].maneuver.exit = simpleStep.instruction.exit; + } + if (extraStep.maneuver) { + // Test with hasOwnProperty because it can be 0 inside this property + if (extraStep.maneuver.hasOwnProperty("bearing_before")) { + stepArray[stepIdx].maneuver.bearing_before = extraStep.maneuver.bearing_before; + } + if (extraStep.maneuver.hasOwnProperty("bearing_after")) { + stepArray[stepIdx].maneuver.bearing_after = extraStep.maneuver.bearing_after; + } + if (extraStep.maneuver.hasOwnProperty("location")) { + stepArray[stepIdx].maneuver.location = [extraStep.maneuver.location[0],extraStep.maneuver.location[1]]; + } + } + + } + + legArray[legIdx].steps = stepArray; + legArray[legIdx].summary = leg.summary; + + } else { + + // We add an empty array (see http://project-osrm.org/docs/v5.5.1/api/#route-object) + legArray[legIdx].steps = new Array(); + legArray[legIdx].summary = ""; + LOGGER.debug("no steps asked by user"); + + } + } + + routeArray[routeIdx].legs = legArray; + } + + // Finalze userResponse + userResponse.routes = routeArray; + + return userResponse; + } + +} diff --git a/src/js/apis/osrm/1.0.0/index.js b/src/js/apis/osrm/1.0.0/index.js new file mode 100644 index 0000000..6147084 --- /dev/null +++ b/src/js/apis/osrm/1.0.0/index.js @@ -0,0 +1,172 @@ +'use strict'; + + +const path = require('path'); +const express = require('express'); +const log4js = require('log4js'); +const controller = require('./controller/controller'); +const errorManager = require('../../../utils/errorManager'); +const swaggerUi = require('swagger-ui-express'); + +let LOGGER = log4js.getLogger("OSRM"); +let router = express.Router(); + +// API entrypoint +router.all("/", function(req, res) { + LOGGER.debug("request on /osrm/1.0.0/"); + res.send("Road2 via OSRM API 1.0.0"); +}); + + +// swagger-ui +let apiJsonPath = path.join(__dirname, '..', '..', '..','..','..', 'documentation','apis','osrm', '1.0.0', 'api.json'); +LOGGER.info("using file '"+ apiJsonPath + "' to initialize swagger-ui for OSRM API version 1.0.0"); +let swaggerDocument = require(apiJsonPath); +router.use('/openapi', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); + +// GetCapabilities +router.all("/resources", function(req, res, next) { + + LOGGER.debug("request on /osrm/1.0.0/resources?"); + return next(errorManager.createError("Not Implemented", 501)); + +}); + +// Route: routing request +router.route("/:resource/:profile/:optimization/route/v1/_/:coordinates") + + .get(async function(req, res, next) { + + LOGGER.debug("GET request on /osrm/1.0.0/:resource"); + LOGGER.debug(req.originalUrl); + + // get service instance + let service = req.app.get("service"); + + // check if operation is permitted on this service instance + if (!service.verifyAvailabilityOperation("route")) { + return next(errorManager.createError(" Operation not permitted on this service ", 400)); + } + + // get request parameters, both from path and query + let path_parameters = req.params + let query_parameters = req.query; + let parameters = {} + for (const key in query_parameters) { + parameters[key] = query_parameters[key] + } + for (const key in path_parameters) { + parameters[key] = path_parameters[key] + } + LOGGER.debug(parameters); + + try { + + // Check request parameters + const routeRequest = controller.checkRouteParameters(parameters, service, "GET"); + LOGGER.debug(routeRequest); + // Send to service and get response object + const routeResponse = await service.computeRequest(routeRequest); + LOGGER.debug(routeResponse); + // Format response + const userResponse = controller.writeRouteResponse(routeRequest, routeResponse, service); + LOGGER.debug(userResponse); + + res.set('content-type', 'application/json'); + res.status(200).json(userResponse); + + } catch (error) { + return next(error); + } + }); + + +// Error management +// This part must be placed after normal routes definitions +// --- +router.use(logError); +router.use(sendError); +// This must be the last item to send an HTTP 404 error if evry route calls next. +router.use(notFoundError); +// --- + +/** +* +* @function +* @name logError +* @description Callback to log error +* +*/ + +function logError(err, req, res, next) { + + let message = { + request: req.originalUrl, + query: req.query, + body: req.body, + error: { + errorType: err.code, + message: err.message, + stack: err.stack + } + }; + + if (err.status) { + LOGGER.debug(message); + } else { + LOGGER.error(message); + } + + next(err); +} + +/** +* +* @function +* @name sendError +* @description Callback to send error to client +* +*/ + +function sendError(err, req, res, next) { + // Behaviour should differ between production and development environments + if (process.env.NODE_ENV === "production") { + if (err.status) { + // if error has a status, this error should be sent to the client + res.status(err.status); + res.json({code: err.code, message: err.message}); + } else { + // if error has no status, this error's details should not be sent to the client + res.status(500); + res.json({code: "internal", message: "Internal Server Error"}); + } + } else if ((process.env.NODE_ENV === "debug")) { + res.status(err.status || 500); + res.json({code: err.code, + message: err.message, + stack: err.stack, + // useful for SQL errors + more: err + }); + } else { + // in the development environment, every error should be sent to clients + res.status(err.status || 500); + res.json({code: err.code, message: err.message}); + } + +} + +/** +* +* @function +* @name sendError +* @description Callback to send HTTP "Not Found" error to client +* +*/ + +function notFoundError(req, res) { + res.status(404); + res.send({ error: "Not found" }); +} + +module.exports = router; diff --git a/src/js/apis/osrm/1.0.0/init.js b/src/js/apis/osrm/1.0.0/init.js new file mode 100644 index 0000000..7173a1f --- /dev/null +++ b/src/js/apis/osrm/1.0.0/init.js @@ -0,0 +1,24 @@ +'use strict'; + +const log4js = require('log4js'); + +let LOGGER = log4js.getLogger("INIT"); + +module.exports = { + + /** + * + * @function + * @name run + * @description Fonction lancée avant la mise en service du serveur. + * @param {object} app - App ExpressJS + * @param {string} uid - uid de l'api. Il permet de stocker des objets dans app. + * @return {boolean} True si tout s'est bien passé et False sinon + * + */ + + run: function(app, uid) { + return true; + } + +} diff --git a/src/js/apis/osrm/1.0.0/update.js b/src/js/apis/osrm/1.0.0/update.js new file mode 100644 index 0000000..8a977fe --- /dev/null +++ b/src/js/apis/osrm/1.0.0/update.js @@ -0,0 +1,26 @@ +'use strict'; + +const log4js = require('log4js'); + +let LOGGER = log4js.getLogger("UPDATE"); + +module.exports = { + + /** + * + * @function + * @name run + * @description Fonction lancée lors d'une MAJ sur le serveur. + * @param {object} app - App ExpressJS + * @param {string} uid - uid de l'api. Il permet de stocker des objets dans app. + * @return {boolean} True si tout s'est bien passé et False sinon + * + */ + + run: function(app, uid) { + + return true; + + } + +} diff --git a/src/js/apis/simple/1.0.0/update.js b/src/js/apis/simple/1.0.0/update.js index 141acd3..eb9866f 100644 --- a/src/js/apis/simple/1.0.0/update.js +++ b/src/js/apis/simple/1.0.0/update.js @@ -2,15 +2,15 @@ const log4js = require('log4js'); -var LOGGER = log4js.getLogger("INIT"); +var LOGGER = log4js.getLogger("UPDATE"); module.exports = { /** * * @function - * @name createGetCapabilities - * @description Fonction utilisée pour créer le GetCapabilities + * @name updateGetCapabilities + * @description Fonction utilisée pour mettre à jour le GetCapabilities * @param {object} app - App ExpressJS * @return {boolean} True si tout s'est bien passé et False sinon * diff --git a/src/js/responses/routeResponse.js b/src/js/responses/routeResponse.js index 5bcea10..53b878e 100644 --- a/src/js/responses/routeResponse.js +++ b/src/js/responses/routeResponse.js @@ -42,13 +42,16 @@ module.exports = class routeResponse extends Response { // profile this._profile = profile; - // optmization + // optimization this._optimization = optimization; // Itinéraires //Tableau contenant l'ensemble des itinéraires calculés par le moteur this._routes = new Array(); + // Informations spécifiques à un moteur et son API native + this._engineExtras = {}; + } /** @@ -189,4 +192,27 @@ module.exports = class routeResponse extends Response { this._routes = st; } + /** + * + * @function + * @name get engineExtras + * @description Récupérer les propriétés non génériques, spécifiques au moteur et à son API + * + */ + get engineExtras () { + return this._engineExtras; + } + + /** + * + * @function + * @name set engineExtras + * @description Attribuer les propriétés non génériques, spécifiques au moteur et à son API + * @param {object} ee - Dictionnaire de propriétés spécifiques au moteur et à son API + * + */ + set engineExtras (ee) { + this._engineExtras = ee; + } + } diff --git a/src/js/sources/osrmSource.js b/src/js/sources/osrmSource.js index b163f56..b5a507c 100644 --- a/src/js/sources/osrmSource.js +++ b/src/js/sources/osrmSource.js @@ -10,6 +10,7 @@ const Point = require('../geometry/point'); const Step = require('../responses/step'); const Distance = require('../geography/distance'); const Duration = require('../time/duration'); +const copyManager = require('../utils/copyManager') const errorManager = require('../utils/errorManager'); const log4js = require('log4js'); @@ -372,6 +373,11 @@ module.exports = class osrmSource extends Source { let profile; let optimization; let routes = new Array(); + let engineExtras = { + "code": "", + "routes": new Array(), + "waypoints": new Array() + }; // Récupération des paramètres de la requête que l'on veut transmettre dans la réponse // --- @@ -385,6 +391,12 @@ module.exports = class osrmSource extends Source { optimization = routeRequest.optimization; // --- + // Récupération des paramètres de la réponse OSRM pour une éventuelle réponse avec son API native + // Si on est ici, pour le moment, c'est que c'est Ok, sinon une erreur aura déjà été renvoyée + engineExtras.code = "Ok"; + + // --- + // Lecture de la réponse OSRM // --- @@ -428,6 +440,16 @@ module.exports = class osrmSource extends Source { LOGGER.debug("osrm response has 1 or more routes"); } + for (let waypointIdx = 0; waypointIdx < osrmResponse.waypoints.length; waypointIdx++) { + engineExtras.waypoints[waypointIdx] = { + "hint": osrmResponse.waypoints[waypointIdx].hint, + "distance": osrmResponse.waypoints[waypointIdx].distance, + "name": osrmResponse.waypoints[waypointIdx].name + }; + } + LOGGER.debug("OSRM engineExtras waypoints (before adding to routeResponse:"); + LOGGER.debug(engineExtras.waypoints); + // routes // Il peut y avoir plusieurs itinéraires for (let i = 0; i < osrmResponse.routes.length; i++) { @@ -436,6 +458,8 @@ module.exports = class osrmSource extends Source { let portions = new Array(); let currentOsrmRoute = osrmResponse.routes[i]; + engineExtras.routes[i] = {}; + let nativeLegs = new Array(); // On commence par créer l'itinéraire avec les attributs obligatoires routes[i] = new Route( new Line(currentOsrmRoute.geometry, "geojson", super.projection) ); @@ -474,13 +498,15 @@ module.exports = class osrmSource extends Source { let legEnd = new Point(osrmResponse.waypoints[j+1].location[0], osrmResponse.waypoints[j+1].location[1], super.projection); if (!legEnd.transform(askedProjection)) { - throw errorManager.createError(" Error during reprojection of leg end in OSRM response. "); + throw errorManager.createError(" Error during reprojection of leg end in OSRM response. "); } else { LOGGER.debug("portion end in asked projection:"); LOGGER.debug(legEnd); } + portions[j] = new Portion(legStart, legEnd); + nativeLegs[j] = {}; // On récupère la distance et la durée portions[j].distance = new Distance(currentOsrmRouteLeg.distance,"meter"); @@ -488,6 +514,7 @@ module.exports = class osrmSource extends Source { // Steps let steps = new Array(); + let nativeSteps = new Array(); // On va associer les étapes à la portion concernée for (let k=0; k < currentOsrmRouteLeg.steps.length; k++) { @@ -524,17 +551,58 @@ module.exports = class osrmSource extends Source { if (currentOsrmRouteStep.maneuver.exit) { steps[k].instruction.exit = currentOsrmRouteStep.maneuver.exit; } + + // Add OSRM extra in the routeResponse + nativeSteps[k] = {}; + nativeSteps[k].mode = currentOsrmRouteStep.mode; + + // Add intersections extra + let nativeIntersections = new Array(); + for (let intersectionIndex = 0; intersectionIndex < currentOsrmRouteStep.intersections.length; intersectionIndex++) { + let currentIntersection = currentOsrmRouteStep.intersections[intersectionIndex]; + nativeIntersections[intersectionIndex] = copyManager.deepCopy(currentIntersection); + let location = new Point(currentIntersection.location[0], currentIntersection.location[1], super.projection); + if (!location.transform(askedProjection)) { + throw errorManager.createError(" Error during reprojection of intersection in OSRM response. "); + } + nativeIntersections[intersectionIndex].location = [location.x, location.y]; + } + nativeSteps[k].intersections = nativeIntersections; + + // Add maneuver extra + let nativeManeuver = {}; + // Test with hasOwnProperty because it can be 0 inside this property + if (currentOsrmRouteStep.maneuver.hasOwnProperty("bearing_before")) { + nativeManeuver.bearing_before = currentOsrmRouteStep.maneuver.bearing_before; + } + if (currentOsrmRouteStep.maneuver.hasOwnProperty("bearing_after")) { + nativeManeuver.bearing_after = currentOsrmRouteStep.maneuver.bearing_after; + } + if (currentOsrmRouteStep.maneuver.hasOwnProperty("location")) { + let location = new Point(currentOsrmRouteStep.maneuver.location[0], currentOsrmRouteStep.maneuver.location[1], super.projection); + if (!location.transform(askedProjection)) { + throw errorManager.createError(" Error during reprojection of step location in OSRM response. "); + } + nativeManeuver.location = [location.x, location.y]; + } + + nativeSteps[k].maneuver = nativeManeuver; + } portions[j].steps = steps; + nativeLegs[j].steps = nativeSteps; + nativeLegs[j].summary = currentOsrmRouteLeg.summary; } routes[i].portions = portions; + engineExtras.routes[i].legs = nativeLegs; } routeResponse.routes = routes; + routeResponse.engineExtras = engineExtras; return routeResponse; diff --git a/test/functional/request/cucumber/configurations/local-service.json b/test/functional/request/cucumber/configurations/local-service.json index f40e7ae..ccbaa75 100644 --- a/test/functional/request/cucumber/configurations/local-service.json +++ b/test/functional/request/cucumber/configurations/local-service.json @@ -10,6 +10,11 @@ "nearest": "/simple/1.0.0/nearest", "getcapabilities": "/simple/1.0.0/getcapabilities" } + }, + "osrm": { + "1.0.0": { + "///route/v1/_/": "/osrm/1.0.0////route/v1/_/" + } } }, "defaultParameters": [ diff --git a/test/functional/request/cucumber/features/req-osrm-1.0.0-common.feature b/test/functional/request/cucumber/features/req-osrm-1.0.0-common.feature new file mode 100644 index 0000000..5a6e9da --- /dev/null +++ b/test/functional/request/cucumber/features/req-osrm-1.0.0-common.feature @@ -0,0 +1,78 @@ +Feature: Road2-osrm-1.0.0-common + Tests fonctionnels de Road2 via l'API osrm/1.0.0 + + Background: + Given I have loaded all my test configuration in "../../configurations/local-service.json" + + Scenario: Route principale de l'API osrm/1.0.0 + Given an "GET" request on "/osrm/1.0.0" + When I send the request + Then the server should send a response with status 200 + And the response should contain "Road2 via OSRM API 1.0.0" + + Scenario: GetCapabilities de l'API osrm/1.0.0 + Given an "GET" request on "/osrm/1.0.0/resources" + When I send the request + Then the server should send a response with status 501 + And the response should contain "Not Implemented" + + Scenario: Requête sur une route inexistante + Given an "GET" request on "/osrm/1.0.0/test" + When I send the request + Then the server should send a response with status 404 + And the response should contain "Not found" + + Scenario: [route] Route sur l'API osrm 1.0.0 avec mauvaise resource + Given an "GET" request on operation "///route/v1/_/" in api "osrm" "1.0.0" + And with path parameters: + | key | value | + | resource | test | + | profile | car | + | optimization | fastest | + | coordinates | 2.333865,48.881989;2.344851,48.872393 | + When I send the request + Then the server should send a response with status 400 + And the response should have an header "content-type" with value "application/json" + And the response should contain an attribute "message" with value "Parameter 'resourceId' is invalid: it does not exist on this service" + + Scenario: [route] Route sur l'API osrm 1.0.0 avec mauvais profile + Given an "GET" request on operation "///route/v1/_/" in api "osrm" "1.0.0" + And with path parameters: + | key | value | + | resource | bduni-idf-osrm | + | profile | test | + | optimization | fastest | + | coordinates | 2.333865,48.881989;2.344851,48.872393 | + When I send the request + Then the server should send a response with status 400 + And the response should have an header "content-type" with value "application/json" + And the response should contain an attribute "message" with value "Parameter 'profileId' is invalid" + + Scenario: [route] Route sur l'API osrm 1.0.0 avec mauvaise optimisation + Given an "GET" request on operation "///route/v1/_/" in api "osrm" "1.0.0" + And with path parameters: + | key | value | + | resource | bduni-idf-osrm | + | profile | car | + | optimization | test | + | coordinates | 2.333865,48.881989;2.344851,48.872393 | + When I send the request + Then the server should send a response with status 400 + And the response should have an header "content-type" with value "application/json" + And the response should contain an attribute "message" with value "Parameter 'optimizationId' is invalid" + + Scenario: [route] Route sur l'API osrm 1.0.0 avec mauvaises coordonnées + Given an "GET" request on operation "///route/v1/_/" in api "osrm" "1.0.0" + And with path parameters: + | key | value | + | resource | bduni-idf-osrm | + | profile | car | + | optimization | fastest | + | coordinates | 2.333865,48.881989;2.344851, | + When I send the request + Then the server should send a response with status 400 + And the response should have an header "content-type" with value "application/json" + And the response should contain an attribute "message" with value "Parameter 'coordinates' is invalid" + + + diff --git a/test/functional/request/cucumber/features/req-osrm-1.0.0-osrm.feature b/test/functional/request/cucumber/features/req-osrm-1.0.0-osrm.feature new file mode 100644 index 0000000..f853ab2 --- /dev/null +++ b/test/functional/request/cucumber/features/req-osrm-1.0.0-osrm.feature @@ -0,0 +1,172 @@ +Feature: Road2-osrm-1.0.0-osrm + Tests fonctionnels de Road2 via l'API osrm/1.0.0 avec le moteur OSRM + + Background: + Given I have loaded all my test configuration in "../../configurations/local-service.json" + + Scenario: Route sur l'API osrm 1.0.0 + Given an "GET" request on operation "///route/v1/_/" in api "osrm" "1.0.0" + And with path parameters: + | key | value | + | resource | bduni-idf-osrm | + | profile | car | + | optimization | fastest | + | coordinates | 2.333865,48.881989;2.344851,48.872393 | + When I send the request + Then the server should send a response with status 200 + And the response should have an header "content-type" with value "application/json" + And the response should contain an attribute "code" with value "Ok" + And the response should contain an attribute "waypoints" + And the response should contain an attribute "routes" + And the response should contain an attribute "routes.[0].geometry" + And the response should contain an attribute "routes.[0].distance" + And the response should contain an attribute "routes.[0].duration" + And the response should contain an attribute "routes.[0].legs" + And the response should contain an attribute "routes.[0].legs.[0].distance" + And the response should contain an attribute "routes.[0].legs.[0].duration" + And the response should contain an attribute "routes.[0].legs.[0].steps" + And the response should contain an attribute "routes.[0].legs.[0].summary" + And the response should contain an attribute "waypoints.[0].name" + And the response should contain an attribute "waypoints.[0].distance" + And the response should contain an attribute "waypoints.[0].location" + And the response should contain an attribute "waypoints.[0].hint" + + Scenario: [osrm/1.0.0] Route avec point intermédiaire + Given an "GET" request on operation "///route/v1/_/" in api "osrm" "1.0.0" + And with path parameters: + | key | value | + | resource | bduni-idf-osrm | + | profile | car | + | optimization | fastest | + | coordinates | 2.431111978054117,48.843103213614626;2.332134095987717,48.871144172016784;2.382134095987717,48.851144172016784 | + When I send the request + Then the server should send a response with status 200 + And the response should have an header "content-type" with value "application/json" + And the response should contain an attribute "code" with value "Ok" + And the response should contain an attribute "waypoints.[2].name" + And the response should contain an attribute "routes.[0].legs.[1].summary" + + Scenario: [osrm/1.0.0] Route avec steps=false (default) + Given an "GET" request on operation "///route/v1/_/" in api "osrm" "1.0.0" + And with path parameters: + | key | value | + | resource | bduni-idf-osrm | + | profile | car | + | optimization | fastest | + | coordinates | 2.431111978054117,48.843103213614626;2.332134095987717,48.871144172016784;2.382134095987717,48.851144172016784 | + When I send the request + Then the server should send a response with status 200 + And the response should have an header "content-type" with value "application/json" + And the response should contain an attribute "code" with value "Ok" + And the response should not contain an attribute "routes.[0].legs.[0].steps.[0]" + + Scenario: [osrm/1.0.0] Route avec steps=false explicitement + Given an "GET" request on operation "///route/v1/_/" in api "osrm" "1.0.0" + And with path parameters: + | key | value | + | resource | bduni-idf-osrm | + | profile | car | + | optimization | fastest | + | coordinates | 2.431111978054117,48.843103213614626;2.332134095987717,48.871144172016784;2.382134095987717,48.851144172016784 | + And with query parameters: + | key | value | + | steps | false | + When I send the request + Then the server should send a response with status 200 + And the response should have an header "content-type" with value "application/json" + And the response should contain an attribute "code" with value "Ok" + And the response should not contain an attribute "routes.[0].legs.[0].steps.[0]" + + Scenario: [osrm/1.0.0] Route avec steps=true + Given an "GET" request on operation "///route/v1/_/" in api "osrm" "1.0.0" + And with path parameters: + | key | value | + | resource | bduni-idf-osrm | + | profile | car | + | optimization | fastest | + | coordinates | 2.431111978054117,48.843103213614626;2.332134095987717,48.871144172016784;2.382134095987717,48.851144172016784 | + And with query parameters: + | key | value | + | steps | true | + When I send the request + Then the server should send a response with status 200 + And the response should have an header "content-type" with value "application/json" + And the response should contain an attribute "code" with value "Ok" + And the response should contain an attribute "routes.[0].legs.[0].steps.[0].name" + And the response should contain an attribute "routes.[0].legs.[0].steps.[0].distance" + And the response should contain an attribute "routes.[0].legs.[0].steps.[0].duration" + And the response should contain an attribute "routes.[0].legs.[0].steps.[0].mode" + And the response should contain an attribute "routes.[0].legs.[0].steps.[0].geometry" + And the response should contain an attribute "routes.[0].legs.[0].steps.[0].maneuver" + And the response should contain an attribute "routes.[0].legs.[0].steps.[0].intersections" + + Scenario: [osrm/1.0.0] Route avec geometries=polyline (default) + Given an "GET" request on operation "///route/v1/_/" in api "osrm" "1.0.0" + And with path parameters: + | key | value | + | resource | bduni-idf-osrm | + | profile | car | + | optimization | fastest | + | coordinates | 2.431111978054117,48.843103213614626;2.332134095987717,48.871144172016784;2.382134095987717,48.851144172016784 | + When I send the request + Then the server should send a response with status 200 + And the response should have an header "content-type" with value "application/json" + And the response should contain an attribute "code" with value "Ok" + And the response should contain a string attribute "routes.[0].geometry" + And the response should contain a string attribute "waypoints.[0].hint" + + Scenario: [osrm/1.0.0] Route avec geometries=polyline explicitement + Given an "GET" request on operation "///route/v1/_/" in api "osrm" "1.0.0" + And with path parameters: + | key | value | + | resource | bduni-idf-osrm | + | profile | car | + | optimization | fastest | + | coordinates | 2.431111978054117,48.843103213614626;2.332134095987717,48.871144172016784;2.382134095987717,48.851144172016784 | + And with query parameters: + | key | value | + | geometries | polyline | + When I send the request + Then the server should send a response with status 200 + And the response should have an header "content-type" with value "application/json" + And the response should contain an attribute "code" with value "Ok" + And the response should contain a string attribute "routes.[0].geometry" + And the response should contain a string attribute "waypoints.[0].hint" + + Scenario: [osrm/1.0.0] Route avec geometries=geojson + Given an "GET" request on operation "///route/v1/_/" in api "osrm" "1.0.0" + And with path parameters: + | key | value | + | resource | bduni-idf-osrm | + | profile | car | + | optimization | fastest | + | coordinates | 2.431111978054117,48.843103213614626;2.332134095987717,48.871144172016784 | + And with query parameters: + | key | value | + | geometries | geojson | + When I send the request + Then the server should send a response with status 200 + And the response should have an header "content-type" with value "application/json" + And the response should contain an attribute "code" with value "Ok" + And the response should contain a string attribute "routes.[0].geometry.type" + And the response should contain a string attribute "waypoints.[0].hint" + + Scenario: [osrm/1.0.0] Route avec geometries=geojson et steps=true + Given an "GET" request on operation "///route/v1/_/" in api "osrm" "1.0.0" + And with path parameters: + | key | value | + | resource | bduni-idf-osrm | + | profile | car | + | optimization | fastest | + | coordinates | 2.431111978054117,48.843103213614626;2.332134095987717,48.871144172016784 | + And with query parameters: + | key | value | + | geometries | geojson | + | steps | true | + When I send the request + Then the server should send a response with status 200 + And the response should have an header "content-type" with value "application/json" + And the response should contain an attribute "code" with value "Ok" + And the response should contain a string attribute "routes.[0].geometry.type" + And the response should contain a string attribute "routes.[0].legs.[0].steps.[0].geometry.type" + And the response should contain a string attribute "waypoints.[0].hint" \ No newline at end of file