diff --git a/CHANGELOG.md b/CHANGELOG.md index 37628bc3521..e650d38abe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased - Changes from 5.26.0 - API: + - ADDED: Add Flatbuffers support to NodeJS bindings. [#6338](https://github.com/Project-OSRM/osrm-backend/pull/6338) - CHANGED: Add `data_version` field to responses of all services. [#5387](https://github.com/Project-OSRM/osrm-backend/pull/5387) - FIXED: Use Boost.Beast to parse HTTP request. [#6294](https://github.com/Project-OSRM/osrm-backend/pull/6294) - FIXED: Fix inefficient osrm-routed connection handling [#6113](https://github.com/Project-OSRM/osrm-backend/pull/6113) diff --git a/docs/http.md b/docs/http.md index abad2a9067f..07acc968afa 100644 --- a/docs/http.md +++ b/docs/http.md @@ -956,7 +956,7 @@ The object is used to describe the waypoint on a route. The default response format is `json`, but OSRM supports binary [`flatbuffers`](https://google.github.io/flatbuffers/) format, which is much faster in serialization/deserialization, comparing to `json`. -The format itself is described in message descriptors, located at `include/engine/api/flatbuffers directory`. Those descriptors could +The format itself is described in message descriptors, located at `include/engine/api/flatbuffers` directory. Those descriptors could be compiled to provide protocol parsers in Go/Javascript/Typescript/Java/Dart/C#/Python/Lobster/Lua/Rust/PHP/Kotlin. Precompiled protocol parser for C++ is supplied with OSRM. diff --git a/docs/nodejs/api.md b/docs/nodejs/api.md index 2237cf6c575..39d0a9786a1 100644 --- a/docs/nodejs/api.md +++ b/docs/nodejs/api.md @@ -64,6 +64,7 @@ Returns the fastest route between two or more coordinates while visiting the way - `options.approaches` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)?** Keep waypoints on curb side. Can be `null` (unrestricted, default) or `curb`. `null`/`true`/`false` - `options.waypoints` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)?** Indices to coordinates to treat as waypoints. If not supplied, all coordinates are waypoints. Must include first and last coordinate index. + - `options.format` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** Which output format to use, either `json`, or [`flatbuffers`](https://github.com/Project-OSRM/osrm-backend/tree/master/include/engine/api/flatbuffers). - `options.snapping` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** Which edges can be snapped to, either `default`, or `any`. `default` only snaps to edges marked by the profile as `is_startpoint`, `any` will allow snapping to any edge in the routing graph. - `options.skip_waypoints` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Removes waypoints from the response. Waypoints are still calculated, but not serialized. Could be useful in case you are interested in some other part of response and do not want to transfer waste data. (optional, default `false`) - `callback` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** @@ -99,6 +100,7 @@ Note: `coordinates` in the general options only supports a single `{longitude},{ - `options.number` **[Number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** Number of nearest segments that should be returned. Must be an integer greater than or equal to `1`. (optional, default `1`) - `options.approaches` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)?** Keep waypoints on curb side. Can be `null` (unrestricted, default) or `curb`. + - `options.format` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** Which output format to use, either `json`, or [`flatbuffers`](https://github.com/Project-OSRM/osrm-backend/tree/master/include/engine/api/flatbuffers). - `options.snapping` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** Which edges can be snapped to, either `default`, or `any`. `default` only snaps to edges marked by the profile as `is_startpoint`, `any` will allow snapping to any edge in the routing graph. - `callback` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** @@ -334,12 +336,15 @@ specific behaviours. - `plugin_config` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)?** Object literal containing parameters for the trip query. - `plugin_config.format` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** The format of the result object to various API calls. - Valid options are `object` (default), which returns a - standard Javascript object, as described above, and `json_buffer`, which will return a NodeJS - **[Buffer](https://nodejs.org/api/buffer.html)** object, containing a JSON string. The latter has - the advantage that it can be immediately serialized to disk/sent over the network, and the - generation of the string is performed outside the main NodeJS event loop. This option is ignored - by the `tile` plugin. + Valid options are `object` (default if `options.format` is + `json`), which returns a standard Javascript object, as described above, and `buffer`(default if + `options.format` is `flatbuffers`), which will return a NodeJS + **[Buffer](https://nodejs.org/api/buffer.html)** object, containing a JSON string or Flatbuffers + object. The latter has the advantage that it can be immediately serialized to disk/sent over the + network, and the generation of the string is performed outside the main NodeJS event loop. This + option is ignored by the `tile` plugin. Also note that `options.format` set to `flatbuffers` + cannot be used with `plugin_config.format` set to `object`. `json_buffer` is deprecated alias for + `buffer`. **Examples** @@ -351,7 +356,7 @@ var options = { [13.374481201171875, 52.506191342034576] ] }; -osrm.route(options, { format: "json_buffer" }, function(err, response) { +osrm.route(options, { format: "buffer" }, function(err, response) { if (err) throw err; console.log(response.toString("utf-8")); }); diff --git a/include/nodejs/node_osrm_support.hpp b/include/nodejs/node_osrm_support.hpp index 25f96661dfa..f1e288226e1 100644 --- a/include/nodejs/node_osrm_support.hpp +++ b/include/nodejs/node_osrm_support.hpp @@ -2,8 +2,7 @@ #define OSRM_BINDINGS_NODE_SUPPORT_HPP #include "nodejs/json_v8_renderer.hpp" -#include "util/json_renderer.hpp" - +#include "engine/api/flatbuffers/fbresult_generated.h" #include "osrm/approach.hpp" #include "osrm/bearing.hpp" #include "osrm/coordinate.hpp" @@ -18,6 +17,7 @@ #include "osrm/table_parameters.hpp" #include "osrm/tile_parameters.hpp" #include "osrm/trip_parameters.hpp" +#include "util/json_renderer.hpp" #include #include @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -46,7 +47,7 @@ using table_parameters_ptr = std::unique_ptr; struct PluginParameters { - bool renderJSONToBuffer = false; + bool renderToBuffer = false; }; using ObjectOrString = typename mapbox::util::variant; @@ -96,6 +97,17 @@ inline void ParseResult(const osrm::Status &result_status, osrm::json::Object &r } inline void ParseResult(const osrm::Status & /*result_status*/, const std::string & /*unused*/) {} +inline void ParseResult(const osrm::Status &result_status, + const flatbuffers::FlatBufferBuilder &fbs_builder) +{ + auto fbs_result = osrm::engine::api::fbresult::GetFBResult(fbs_builder.GetBufferPointer()); + + if (result_status == osrm::Status::Error) + { + BOOST_ASSERT(fbs_result->code()); + throw std::logic_error(fbs_result->code()->message()->c_str()); + } +} inline engine_config_ptr argumentsToEngineConfig(const Nan::FunctionCallbackInfo &args) { @@ -725,6 +737,36 @@ inline bool argumentsToParameter(const Nan::FunctionCallbackInfo &arg } } + if (Nan::Has(obj, Nan::New("format").ToLocalChecked()).FromJust()) + { + v8::Local format = + Nan::Get(obj, Nan::New("format").ToLocalChecked()).ToLocalChecked(); + if (format.IsEmpty()) + { + return false; + } + + if (!format->IsString()) + { + Nan::ThrowError("format must be a string: \"json\" or \"flatbuffers\""); + return false; + } + + std::string format_str = *Nan::Utf8String(format); + if (format_str == "json") + { + params->format = osrm::engine::api::BaseParameters::OutputFormatType::JSON; + } + else if (format_str == "flatbuffers") + { + params->format = osrm::engine::api::BaseParameters::OutputFormatType::FLATBUFFERS; + } + else + { + Nan::ThrowError("format must be a string: \"json\" or \"flatbuffers\""); + return false; + } + } return true; } @@ -885,17 +927,18 @@ inline bool parseCommonParameters(const v8::Local &obj, ParamType &p return true; } -inline PluginParameters -argumentsToPluginParameters(const Nan::FunctionCallbackInfo &args) +inline PluginParameters argumentsToPluginParameters( + const Nan::FunctionCallbackInfo &args, + const boost::optional &output_format = {}) { if (args.Length() < 3 || !args[1]->IsObject()) { - return {}; + // output to buffer by default for Flatbuffers + return {output_format == osrm::engine::api::BaseParameters::OutputFormatType::FLATBUFFERS}; } v8::Local obj = Nan::To(args[1]).ToLocalChecked(); if (Nan::Has(obj, Nan::New("format").ToLocalChecked()).FromJust()) { - v8::Local format = Nan::Get(obj, Nan::New("format").ToLocalChecked()).ToLocalChecked(); if (format.IsEmpty()) @@ -905,7 +948,7 @@ argumentsToPluginParameters(const Nan::FunctionCallbackInfo &args) if (!format->IsString()) { - Nan::ThrowError("format must be a string: \"object\" or \"json_buffer\""); + Nan::ThrowError("format must be a string: \"object\" or \"buffer\""); return {}; } @@ -914,20 +957,35 @@ argumentsToPluginParameters(const Nan::FunctionCallbackInfo &args) if (format_str == "object") { + if (output_format == osrm::engine::api::BaseParameters::OutputFormatType::FLATBUFFERS) + { + Nan::ThrowError("Flatbuffers result can only output to buffer."); + return {true}; + } return {false}; } + else if (format_str == "buffer") + { + return {true}; + } else if (format_str == "json_buffer") { + if (output_format && + output_format != osrm::engine::api::BaseParameters::OutputFormatType::JSON) + { + Nan::ThrowError("Deprecated `json_buffer` can only be used with JSON format"); + } return {true}; } else { - Nan::ThrowError("format must be a string: \"object\" or \"json_buffer\""); + Nan::ThrowError("format must be a string: \"object\" or \"buffer\""); return {}; } } - return {}; + // output to buffer by default for Flatbuffers + return {output_format == osrm::engine::api::BaseParameters::OutputFormatType::FLATBUFFERS}; } inline route_parameters_ptr diff --git a/src/nodejs/node_osrm.cpp b/src/nodejs/node_osrm.cpp index ba82947d353..37944d60ca9 100644 --- a/src/nodejs/node_osrm.cpp +++ b/src/nodejs/node_osrm.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -128,8 +129,7 @@ inline void async(const Nan::FunctionCallbackInfo &info, auto params = argsToParams(info, requires_multiple_coordinates); if (!params) return; - - auto pluginParams = argumentsToPluginParameters(info); + auto pluginParams = argumentsToPluginParameters(info, params->format); BOOST_ASSERT(params->IsValid()); @@ -156,20 +156,41 @@ inline void async(const Nan::FunctionCallbackInfo &info, void Execute() override try { - osrm::engine::api::ResultT r; - r = osrm::util::json::Object(); - const auto status = ((*osrm).*(service))(*params, r); - auto json_result = r.get(); - ParseResult(status, json_result); - if (pluginParams.renderJSONToBuffer) + switch ( + params->format.value_or(osrm::engine::api::BaseParameters::OutputFormatType::JSON)) { - std::ostringstream buf; - osrm::util::json::render(buf, json_result); - result = buf.str(); + case osrm::engine::api::BaseParameters::OutputFormatType::JSON: + { + osrm::engine::api::ResultT r; + r = osrm::util::json::Object(); + const auto status = ((*osrm).*(service))(*params, r); + auto &json_result = r.get(); + ParseResult(status, json_result); + if (pluginParams.renderToBuffer) + { + std::ostringstream buf; + osrm::util::json::render(buf, json_result); + result = buf.str(); + } + else + { + result = json_result; + } } - else + break; + case osrm::engine::api::BaseParameters::OutputFormatType::FLATBUFFERS: { - result = json_result; + osrm::engine::api::ResultT r = flatbuffers::FlatBufferBuilder(); + const auto status = ((*osrm).*(service))(*params, r); + const auto &fbs_result = r.get(); + ParseResult(status, fbs_result); + BOOST_ASSERT(pluginParams.renderToBuffer); + std::string result_str( + reinterpret_cast(fbs_result.GetBufferPointer()), + fbs_result.GetSize()); + result = std::move(result_str); + } + break; } } catch (const std::exception &e) @@ -299,6 +320,7 @@ inline void asyncForTiles(const Nan::FunctionCallbackInfo &info, * @param {Array} [options.approaches] Keep waypoints on curb side. Can be `null` (unrestricted, default) or `curb`. * `null`/`true`/`false` * @param {Array} [options.waypoints] Indices to coordinates to treat as waypoints. If not supplied, all coordinates are waypoints. Must include first and last coordinate index. + * @param {String} [options.format] Which output format to use, either `json`, or [`flatbuffers`](https://github.com/Project-OSRM/osrm-backend/tree/master/include/engine/api/flatbuffers). * @param {String} [options.snapping] Which edges can be snapped to, either `default`, or `any`. `default` only snaps to edges marked by the profile as `is_startpoint`, `any` will allow snapping to any edge in the routing graph. * @param {Boolean} [options.skip_waypoints=false] Removes waypoints from the response. Waypoints are still calculated, but not serialized. Could be useful in case you are interested in some other part of response and do not want to transfer waste data. * @param {Function} callback @@ -340,6 +362,7 @@ NAN_METHOD(Engine::route) // * @param {Number} [options.number=1] Number of nearest segments that should be returned. * Must be an integer greater than or equal to `1`. * @param {Array} [options.approaches] Keep waypoints on curb side. Can be `null` (unrestricted, default) or `curb`. + * @param {String} [options.format] Which output format to use, either `json`, or [`flatbuffers`](https://github.com/Project-OSRM/osrm-backend/tree/master/include/engine/api/flatbuffers). * @param {String} [options.snapping] Which edges can be snapped to, either `default`, or `any`. `default` only snaps to edges marked by the profile as `is_startpoint`, `any` will allow snapping to any edge in the routing graph. * @param {Function} callback * @@ -606,12 +629,15 @@ NAN_METHOD(Engine::trip) // * @name Configuration * @param {Object} [plugin_config] - Object literal containing parameters for the trip query. * @param {String} [plugin_config.format] The format of the result object to various API calls. - * Valid options are `object` (default), which returns a - * standard Javascript object, as described above, and `json_buffer`, which will return a NodeJS - * **[Buffer](https://nodejs.org/api/buffer.html)** object, containing a JSON string. The latter has - * the advantage that it can be immediately serialized to disk/sent over the network, and the - * generation of the string is performed outside the main NodeJS event loop. This option is ignored - * by the `tile` plugin. + * Valid options are `object` (default if `options.format` is + * `json`), which returns a standard Javascript object, as described above, and `buffer`(default if + * `options.format` is `flatbuffers`), which will return a NodeJS + * **[Buffer](https://nodejs.org/api/buffer.html)** object, containing a JSON string or Flatbuffers + * object. The latter has the advantage that it can be immediately serialized to disk/sent over the + * network, and the generation of the string is performed outside the main NodeJS event loop. This + * option is ignored by the `tile` plugin. Also note that `options.format` set to `flatbuffers` + * cannot be used with `plugin_config.format` set to `object`. `json_buffer` is deprecated alias for + * `buffer`. * * @example * var osrm = new OSRM('network.osrm'); @@ -621,7 +647,7 @@ NAN_METHOD(Engine::trip) // * [13.374481201171875, 52.506191342034576] * ] * }; - * osrm.route(options, { format: "json_buffer" }, function(err, response) { + * osrm.route(options, { format: "buffer" }, function(err, response) { * if (err) throw err; * console.log(response.toString("utf-8")); * }); diff --git a/test/nodejs/match.js b/test/nodejs/match.js index 3161060e954..611a47f14b5 100644 --- a/test/nodejs/match.js +++ b/test/nodejs/match.js @@ -4,6 +4,24 @@ var data_path = require('./constants').data_path; var mld_data_path = require('./constants').mld_data_path; var three_test_coordinates = require('./constants').three_test_coordinates; var two_test_coordinates = require('./constants').two_test_coordinates; +const flatbuffers = require('../../features/support/flatbuffers').flatbuffers; +const FBResult = require('../../features/support/fbresult_generated').osrm.engine.api.fbresult.FBResult; + + +test('match: match in Monaco with flatbuffers format', function(assert) { + assert.plan(2); + var osrm = new OSRM(data_path); + var options = { + coordinates: three_test_coordinates, + timestamps: [1424684612, 1424684616, 1424684620], + format: 'flatbuffers' + }; + osrm.match(options, function(err, response) { + assert.ifError(err); + const fb = FBResult.getRootAsFBResult(new flatbuffers.ByteBuffer(response)); + assert.equal(fb.routesLength(), 1); + }); +}); test('match: match in Monaco', function(assert) { assert.plan(5); diff --git a/test/nodejs/nearest.js b/test/nodejs/nearest.js index 1fce37af054..02879910ba3 100644 --- a/test/nodejs/nearest.js +++ b/test/nodejs/nearest.js @@ -4,8 +4,26 @@ var data_path = require('./constants').data_path; var mld_data_path = require('./constants').mld_data_path; var three_test_coordinates = require('./constants').three_test_coordinates; var two_test_coordinates = require('./constants').two_test_coordinates; +const flatbuffers = require('../../features/support/flatbuffers').flatbuffers; +const FBResult = require('../../features/support/fbresult_generated').osrm.engine.api.fbresult.FBResult; +test('nearest with flatbuffers format', function(assert) { + assert.plan(5); + var osrm = new OSRM(data_path); + osrm.nearest({ + coordinates: [three_test_coordinates[0]], + format: 'flatbuffers' + }, function(err, result) { + assert.ifError(err); + assert.ok(result instanceof Buffer); + const fb = FBResult.getRootAsFBResult(new flatbuffers.ByteBuffer(result)); + assert.equals(fb.waypointsLength(), 1); + assert.ok(fb.waypoints(0).location()); + assert.ok(fb.waypoints(0).name()); + }); +}); + test('nearest', function(assert) { assert.plan(4); var osrm = new OSRM(data_path); diff --git a/test/nodejs/route.js b/test/nodejs/route.js index 7d27639eafe..0e4033843b7 100644 --- a/test/nodejs/route.js +++ b/test/nodejs/route.js @@ -5,6 +5,51 @@ var monaco_mld_path = require('./constants').mld_data_path; var monaco_corech_path = require('./constants').corech_data_path; var three_test_coordinates = require('./constants').three_test_coordinates; var two_test_coordinates = require('./constants').two_test_coordinates; +const flatbuffers = require('../../features/support/flatbuffers').flatbuffers; +const FBResult = require('../../features/support/fbresult_generated').osrm.engine.api.fbresult.FBResult; + +test('route: routes Monaco and can return result in flatbuffers', function(assert) { + assert.plan(5); + var osrm = new OSRM(monaco_path); + osrm.route({coordinates: two_test_coordinates, format: 'flatbuffers'}, function(err, result) { + assert.ifError(err); + assert.ok(result instanceof Buffer); + const fb = FBResult.getRootAsFBResult(new flatbuffers.ByteBuffer(result)); + assert.equals(fb.waypointsLength(), 2); + assert.equals(fb.routesLength(), 1); + assert.ok(fb.routes(0).polyline); + }); +}); + +test('route: routes Monaco and can return result in flatbuffers if output format is passed explicitly', function(assert) { + assert.plan(5); + var osrm = new OSRM(monaco_path); + osrm.route({coordinates: two_test_coordinates, format: 'flatbuffers'}, {output: 'buffer'}, function(err, result) { + assert.ifError(err); + assert.ok(result instanceof Buffer); + var buf = new flatbuffers.ByteBuffer(result); + const fb = FBResult.getRootAsFBResult(buf); + assert.equals(fb.waypointsLength(), 2); + assert.equals(fb.routesLength(), 1); + assert.ok(fb.routes(0).polyline); + }); +}); + +test('route: throws error if required output is object in flatbuffers format', function(assert) { + assert.plan(1); + var osrm = new OSRM(monaco_path); + assert.throws(function() { + osrm.route({coordinates: two_test_coordinates, format: 'flatbuffers'}, {format: 'object'}, function(err, result) {}); + }); +}); + +test('route: throws error if required output is json_buffer in flatbuffers format', function(assert) { + assert.plan(1); + var osrm = new OSRM(monaco_path); + assert.throws(function() { + osrm.route({coordinates: two_test_coordinates, format: 'flatbuffers'}, {format: 'json_buffer'}, function(err, result) {}); + }); +}); test('route: routes Monaco', function(assert) { diff --git a/test/nodejs/table.js b/test/nodejs/table.js index d2c50d83f2a..b73d74b7f0e 100644 --- a/test/nodejs/table.js +++ b/test/nodejs/table.js @@ -4,6 +4,24 @@ var data_path = require('./constants').data_path; var mld_data_path = require('./constants').mld_data_path; var three_test_coordinates = require('./constants').three_test_coordinates; var two_test_coordinates = require('./constants').two_test_coordinates; +const flatbuffers = require('../../features/support/flatbuffers').flatbuffers; +const FBResult = require('../../features/support/fbresult_generated').osrm.engine.api.fbresult.FBResult; + +test('table: flatbuffer format', function(assert) { + assert.plan(3); + var osrm = new OSRM(data_path); + var options = { + coordinates: [three_test_coordinates[0], three_test_coordinates[1]], + format: 'flatbuffers' + }; + osrm.table(options, function(err, table) { + assert.ifError(err); + assert.ok(table instanceof Buffer); + const fb = FBResult.getRootAsFBResult(new flatbuffers.ByteBuffer(table)); + assert.ok(fb.table()); + + }); +}); test('table: test annotations paramater combination', function(assert) { assert.plan(12); diff --git a/test/nodejs/trip.js b/test/nodejs/trip.js index 683032b3c22..4b777fe1514 100644 --- a/test/nodejs/trip.js +++ b/test/nodejs/trip.js @@ -4,7 +4,18 @@ var data_path = require('./constants').data_path; var mld_data_path = require('./constants').mld_data_path; var three_test_coordinates = require('./constants').three_test_coordinates; var two_test_coordinates = require('./constants').two_test_coordinates; +const flatbuffers = require('../../features/support/flatbuffers').flatbuffers; +const FBResult = require('../../features/support/fbresult_generated').osrm.engine.api.fbresult.FBResult; +test('trip: trip in Monaco with flatbuffers format', function(assert) { + assert.plan(2); + var osrm = new OSRM(data_path); + osrm.trip({coordinates: two_test_coordinates, format: 'flatbuffers'}, function(err, trip) { + assert.ifError(err); + const fb = FBResult.getRootAsFBResult(new flatbuffers.ByteBuffer(trip)); + assert.equal(fb.routesLength(), 1); + }); +}); test('trip: trip in Monaco', function(assert) { assert.plan(2);