From 89136340f98c4c1045cc80b73721dde0d91e64e4 Mon Sep 17 00:00:00 2001 From: Siarhei Fedartsou Date: Sat, 27 Aug 2022 14:00:36 +0200 Subject: [PATCH] Add Flatbuffers support to NodeJS bindings --- docs/http.md | 2 +- docs/nodejs/api.md | 22 +++++---- include/nodejs/node_osrm_support.hpp | 71 ++++++++++++++++++++++++---- src/nodejs/node_osrm.cpp | 47 +++++++++++++----- test/nodejs/match.js | 18 +++++++ test/nodejs/nearest.js | 18 +++++++ test/nodejs/route.js | 37 +++++++++++++++ test/nodejs/table.js | 18 +++++++ test/nodejs/trip.js | 11 +++++ 9 files changed, 212 insertions(+), 32 deletions(-) diff --git a/docs/http.md b/docs/http.md index acdc1cd4f..2b1eb721c 100644 --- a/docs/http.md +++ b/docs/http.md @@ -956,7 +956,7 @@ Object used to describe waypoint on a route. 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 30358e4e2..e37c7c318 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 `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.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.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). - `callback` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** **Examples** @@ -98,7 +99,8 @@ Note: `coordinates` in the general options only supports a single `{longitude},{ 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.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)** + - `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). + - `callback` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** **Examples** @@ -140,8 +142,9 @@ Optionally returns distance table. - `options.fallback_coordinate` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** Either `input` (default) or `snapped`. If using a `fallback_speed`, use either the user-supplied coordinate (`input`), or the snapped coordinate (`snapped`) for calculating the as-the-crow-flies distance between two points. - `options.scale_factor` **[Number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** Multiply the table duration values in the table by this number for more controlled input into a route optimization solver. - `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.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.annotations` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)?** Return the requested table or tables in response. Can be `['duration']` (return the duration matrix, default), `[distance']` (return the distance matrix), or `['duration', distance']` (return both the duration matrix and the distance matrix). -- `callback` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** + - `callback` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** **Examples** @@ -225,7 +228,8 @@ if they can not be matched successfully. - `options.tidy` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** Allows the input track modification to obtain better matching quality for noisy tracks. (optional, default `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.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)** + - `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). + - `callback` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** **Examples** @@ -294,7 +298,8 @@ Right now, the following combinations are possible: - `options.destination` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Return route ends at `any` or `last` coordinate. (optional, default `any`) - `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.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)** + - `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). + - `callback` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** **Examples** @@ -332,12 +337,13 @@ 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 + 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. + 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** diff --git a/include/nodejs/node_osrm_support.hpp b/include/nodejs/node_osrm_support.hpp index 25f96661d..15fe42fc7 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,18 @@ 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()); + + BOOST_ASSERT(fb->code()); + + if (result_status == osrm::Status::Error) + { + throw std::logic_error(fbs_result->code()->message()->c_str()); + } +} inline engine_config_ptr argumentsToEngineConfig(const Nan::FunctionCallbackInfo &args) { @@ -725,6 +738,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,8 +928,9 @@ 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()) { @@ -895,7 +939,6 @@ argumentsToPluginParameters(const Nan::FunctionCallbackInfo &args) 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,19 +957,27 @@ 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 {false}; } - else if (format_str == "json_buffer") + else if (format_str == "buffer" || format_str == "json_buffer") { 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 {}; } } - + // output to buffer by default for Flatbuffers + if (output_format == osrm::engine::api::BaseParameters::OutputFormatType::FLATBUFFERS) + { + return {true}; + } return {}; } diff --git a/src/nodejs/node_osrm.cpp b/src/nodejs/node_osrm.cpp index 5c8c486a5..1142d9e9c 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) diff --git a/test/nodejs/match.js b/test/nodejs/match.js index 3161060e9..611a47f14 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 1fce37af0..02879910b 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 7d27639ea..34c1310c0 100644 --- a/test/nodejs/route.js +++ b/test/nodejs/route.js @@ -5,6 +5,43 @@ 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 in 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: routes Monaco', function(assert) { diff --git a/test/nodejs/table.js b/test/nodejs/table.js index d2c50d83f..b73d74b7f 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 683032b3c..4b777fe15 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);