diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cd59ea493f..3dc24cbaa54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Optimizations: - CHANGED: Map matching is now almost twice as fast. [#5060](https://github.com/Project-OSRM/osrm-backend/pull/5060) - CHANGED: Use Grisu2 for serializing floating point numbers. [#5188](https://github.com/Project-OSRM/osrm-backend/pull/5188) + - ADDED: Node bindings can return pre-rendered JSON buffer. [#5189](https://github.com/Project-OSRM/osrm-backend/pull/5189) - Bugfixes: - FIXED: collapsing of ExitRoundabout instructions [#5114](https://github.com/Project-OSRM/osrm-backend/issues/5114) - FIXED: negative distances in table plugin annotation [#5106](https://github.com/Project-OSRM/osrm-backend/issues/5106) diff --git a/docs/nodejs/api.md b/docs/nodejs/api.md index 01ef653cb43..e729962f013 100644 --- a/docs/nodejs/api.md +++ b/docs/nodejs/api.md @@ -297,6 +297,29 @@ Returns **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refer 2) `waypoint_index`: index of the point in the trip. **`trips`**: an array of [`Route`](#route) objects that assemble the trace. +## Plugin behaviour + +All plugins support a second additional object that is available to configure some NodeJS specific behaviours. + +- `plugin_config` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** Object literal containing parameters for the trip query. + - `plugin_config.format` **[String](https://developer.mozilla.org/en-US/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. + +**Examples** + +```javascript +var osrm = new OSRM('network.osrm'); +var options = { + coordinates: [ + [13.36761474609375, 52.51663871100423], + [13.374481201171875, 52.506191342034576] + ] +}; +osrm.route(options, { format: "json_buffer" }, function(err, response) { + if (err) throw err; + console.log(response.toString("utf-8")); +}); +``` + ## Responses Responses diff --git a/include/nodejs/node_osrm_support.hpp b/include/nodejs/node_osrm_support.hpp index 6ffea408b66..4a93eb7a74c 100644 --- a/include/nodejs/node_osrm_support.hpp +++ b/include/nodejs/node_osrm_support.hpp @@ -2,6 +2,7 @@ #define OSRM_BINDINGS_NODE_SUPPORT_HPP #include "nodejs/json_v8_renderer.hpp" +#include "util/json_renderer.hpp" #include "osrm/approach.hpp" #include "osrm/bearing.hpp" @@ -24,6 +25,7 @@ #include #include #include +#include #include #include @@ -42,6 +44,13 @@ using match_parameters_ptr = std::unique_ptr; using nearest_parameters_ptr = std::unique_ptr; using table_parameters_ptr = std::unique_ptr; +struct PluginParameters +{ + bool renderJSONToBuffer = false; +}; + +using ObjectOrString = typename mapbox::util::variant; + template inline v8::Local render(const ResultT &result); template <> v8::Local inline render(const std::string &result) @@ -49,11 +58,21 @@ template <> v8::Local inline render(const std::string &result) return Nan::CopyBuffer(result.data(), result.size()).ToLocalChecked(); } -template <> v8::Local inline render(const osrm::json::Object &result) +template <> v8::Local inline render(const ObjectOrString &result) { - v8::Local value; - renderToV8(value, result); - return value; + if (result.is()) + { + // Convert osrm::json object tree into matching v8 object tree + v8::Local value; + renderToV8(value, result.get()); + return value; + } + else + { + // Return the string object as a node Buffer + return Nan::CopyBuffer(result.get().data(), result.get().size()) + .ToLocalChecked(); + } } inline void ParseResult(const osrm::Status &result_status, osrm::json::Object &result) @@ -814,6 +833,50 @@ inline bool parseCommonParameters(const v8::Local &obj, ParamType &p return true; } +inline PluginParameters +argumentsToPluginParameters(const Nan::FunctionCallbackInfo &args) +{ + if (args.Length() < 3 || !args[1]->IsObject()) + { + return {}; + } + v8::Local obj = Nan::To(args[1]).ToLocalChecked(); + if (obj->Has(Nan::New("format").ToLocalChecked())) + { + + v8::Local format = obj->Get(Nan::New("format").ToLocalChecked()); + if (format.IsEmpty()) + { + return {}; + } + + if (!format->IsString()) + { + Nan::ThrowError("format must be a string: \"object\" or \"json_buffer\""); + return {}; + } + + const Nan::Utf8String format_utf8str(format); + std::string format_str{*format_utf8str, *format_utf8str + format_utf8str.length()}; + + if (format_str == "object") + { + return {false}; + } + else if (format_str == "json_buffer") + { + return {true}; + } + else + { + Nan::ThrowError("format must be a string: \"object\" or \"json_buffer\""); + return {}; + } + } + + return {}; +} + inline route_parameters_ptr argumentsToRouteParameter(const Nan::FunctionCallbackInfo &args, bool requires_multiple_coordinates) @@ -1357,6 +1420,6 @@ argumentsToMatchParameter(const Nan::FunctionCallbackInfo &args, return params; } -} // ns node_osrm +} // namespace node_osrm #endif diff --git a/src/nodejs/node_osrm.cpp b/src/nodejs/node_osrm.cpp index aa7e03851f6..14d970c775a 100644 --- a/src/nodejs/node_osrm.cpp +++ b/src/nodejs/node_osrm.cpp @@ -9,12 +9,15 @@ #include "osrm/trip_parameters.hpp" #include +#include #include #include #include "nodejs/node_osrm.hpp" #include "nodejs/node_osrm_support.hpp" +#include "util/json_renderer.hpp" + namespace node_osrm { @@ -122,6 +125,8 @@ inline void async(const Nan::FunctionCallbackInfo &info, if (!params) return; + auto pluginParams = argumentsToPluginParameters(info); + BOOST_ASSERT(params->IsValid()); if (!info[info.Length() - 1]->IsFunction()) @@ -137,9 +142,89 @@ inline void async(const Nan::FunctionCallbackInfo &info, Worker(std::shared_ptr osrm_, ParamPtr params_, ServiceMemFn service, - Nan::Callback *callback) + Nan::Callback *callback, + PluginParameters pluginParams_) : Base(callback), osrm{std::move(osrm_)}, service{std::move(service)}, - params{std::move(params_)} + params{std::move(params_)}, pluginParams{std::move(pluginParams_)} + { + } + + void Execute() override try + { + osrm::json::Object r; + const auto status = ((*osrm).*(service))(*params, r); + ParseResult(status, r); + if (pluginParams.renderJSONToBuffer) + { + std::ostringstream buf; + osrm::util::json::render(buf, r); + result = buf.str(); + } + else + { + result = r; + } + } + catch (const std::exception &e) + { + SetErrorMessage(e.what()); + } + + void HandleOKCallback() override + { + Nan::HandleScope scope; + + const constexpr auto argc = 2u; + v8::Local argv[argc] = {Nan::Null(), render(result)}; + + callback->Call(argc, argv); + } + + // Keeps the OSRM object alive even after shutdown until we're done with callback + std::shared_ptr osrm; + ServiceMemFn service; + const ParamPtr params; + const PluginParameters pluginParams; + + ObjectOrString result; + }; + + auto *callback = new Nan::Callback{info[info.Length() - 1].As()}; + Nan::AsyncQueueWorker( + new Worker{self->this_, std::move(params), service, callback, std::move(pluginParams)}); +} + +template +inline void asyncForTiles(const Nan::FunctionCallbackInfo &info, + ParameterParser argsToParams, + ServiceMemFn service, + bool requires_multiple_coordinates) +{ + auto params = argsToParams(info, requires_multiple_coordinates); + if (!params) + return; + + auto pluginParams = argumentsToPluginParameters(info); + + BOOST_ASSERT(params->IsValid()); + + if (!info[info.Length() - 1]->IsFunction()) + return Nan::ThrowTypeError("last argument must be a callback function"); + + auto *const self = Nan::ObjectWrap::Unwrap(info.Holder()); + using ParamPtr = decltype(params); + + struct Worker final : Nan::AsyncWorker + { + using Base = Nan::AsyncWorker; + + Worker(std::shared_ptr osrm_, + ParamPtr params_, + ServiceMemFn service, + Nan::Callback *callback, + PluginParameters pluginParams_) + : Base(callback), osrm{std::move(osrm_)}, service{std::move(service)}, + params{std::move(params_)}, pluginParams{std::move(pluginParams_)} { } @@ -167,18 +252,14 @@ inline void async(const Nan::FunctionCallbackInfo &info, std::shared_ptr osrm; ServiceMemFn service; const ParamPtr params; + const PluginParameters pluginParams; - // All services return json::Object .. except for Tile! - using ObjectOrString = - typename std::conditional::value, - std::string, - osrm::json::Object>::type; - - ObjectOrString result; + std::string result; }; auto *callback = new Nan::Callback{info[info.Length() - 1].As()}; - Nan::AsyncQueueWorker(new Worker{self->this_, std::move(params), service, callback}); + Nan::AsyncQueueWorker( + new Worker{self->this_, std::move(params), service, callback, std::move(pluginParams)}); } // clang-format off @@ -341,7 +422,7 @@ NAN_METHOD(Engine::table) // // clang-format on NAN_METHOD(Engine::tile) { - async(info, &argumentsToTileParameters, &osrm::OSRM::Tile, {/*unused*/}); + asyncForTiles(info, &argumentsToTileParameters, &osrm::OSRM::Tile, {/*unused*/}); } // clang-format off diff --git a/test/nodejs/match.js b/test/nodejs/match.js index 10b5a72eb9d..da4d7853b0e 100644 --- a/test/nodejs/match.js +++ b/test/nodejs/match.js @@ -25,6 +25,28 @@ test('match: match in Monaco', function(assert) { }); }); +test('match: match in Monaco returning a buffer', function(assert) { + assert.plan(6); + var osrm = new OSRM(data_path); + var options = { + coordinates: three_test_coordinates, + timestamps: [1424684612, 1424684616, 1424684620] + }; + osrm.match(options, { format: 'json_buffer' }, function(err, response) { + assert.ifError(err); + assert.ok(response instanceof Buffer); + response = JSON.parse(response); + assert.equal(response.matchings.length, 1); + assert.ok(response.matchings.every(function(m) { + return !!m.distance && !!m.duration && Array.isArray(m.legs) && !!m.geometry && m.confidence > 0; + })) + assert.equal(response.tracepoints.length, 3); + assert.ok(response.tracepoints.every(function(t) { + return !!t.hint && !isNaN(t.matchings_index) && !isNaN(t.waypoint_index) && !!t.name; + })); + }); +}); + test('match: match in Monaco without timestamps', function(assert) { assert.plan(3); var osrm = new OSRM(data_path); @@ -225,6 +247,16 @@ test('match: throws on invalid tidy param', function(assert) { /tidy must be of type Boolean/); }); +test('match: throws on invalid config param', function(assert) { + assert.plan(1); + var osrm = new OSRM({path: mld_data_path, algorithm: 'MLD'}); + var options = { + coordinates: three_test_coordinates, + }; + assert.throws(function() { osrm.match(options, { format: 'invalid' }, function(err, response) {}) }, + /format must be a string:/); +}); + test('match: match in Monaco without motorways', function(assert) { assert.plan(3); var osrm = new OSRM({path: mld_data_path, algorithm: 'MLD'}); diff --git a/test/nodejs/nearest.js b/test/nodejs/nearest.js index 4a038dbe535..1fce37af054 100644 --- a/test/nodejs/nearest.js +++ b/test/nodejs/nearest.js @@ -19,6 +19,21 @@ test('nearest', function(assert) { }); }); +test('nearest', function(assert) { + assert.plan(5); + var osrm = new OSRM(data_path); + osrm.nearest({ + coordinates: [three_test_coordinates[0]] + }, { format: 'json_buffer' }, function(err, result) { + assert.ifError(err); + assert.ok(result instanceof Buffer); + result = JSON.parse(result); + assert.equal(result.waypoints.length, 1); + assert.equal(result.waypoints[0].location.length, 2); + assert.ok(result.waypoints[0].hasOwnProperty('name')); + }); +}); + test('nearest: can ask for multiple nearest pts', function(assert) { assert.plan(2); var osrm = new OSRM(data_path); @@ -32,7 +47,7 @@ test('nearest: can ask for multiple nearest pts', function(assert) { }); test('nearest: throws on invalid args', function(assert) { - assert.plan(6); + assert.plan(7); var osrm = new OSRM(data_path); var options = {}; assert.throws(function() { osrm.nearest(options); }, @@ -52,6 +67,10 @@ test('nearest: throws on invalid args', function(assert) { options.number = 0; assert.throws(function() { osrm.nearest(options, function(err, res) {}); }, /Number must be an integer greater than or equal to 1/); + + options.number = 1; + assert.throws(function() { osrm.nearest(options, { format: 'invalid' }, function(err, res) {}); }, + /format must be a string:/); }); test('nearest: nearest in Monaco without motorways', function(assert) { diff --git a/test/nodejs/route.js b/test/nodejs/route.js index ae28f3762d5..011a098a30c 100644 --- a/test/nodejs/route.js +++ b/test/nodejs/route.js @@ -43,8 +43,22 @@ test('route: routes Monaco on CoreCH', function(assert) { }); }); +test('route: routes Monaco and returns a JSON buffer', function(assert) { + assert.plan(6); + var osrm = new OSRM({path: monaco_corech_path, algorithm: 'CoreCH'}); + osrm.route({coordinates: [[13.43864,52.51993],[13.415852,52.513191]]}, { format: 'json_buffer'}, function(err, result) { + assert.ifError(err); + assert.ok(result instanceof Buffer); + const route = JSON.parse(result); + assert.ok(route.waypoints); + assert.ok(route.routes); + assert.ok(route.routes.length); + assert.ok(route.routes[0].geometry); + }); +}); + test('route: throws with too few or invalid args', function(assert) { - assert.plan(3); + assert.plan(4); var osrm = new OSRM(monaco_path); assert.throws(function() { osrm.route({coordinates: two_test_coordinates}) }, /Two arguments required/); @@ -52,6 +66,8 @@ test('route: throws with too few or invalid args', function(assert) { /First arg must be an object/); assert.throws(function() { osrm.route({coordinates: two_test_coordinates}, true)}, /last argument must be a callback function/); + assert.throws(function() { osrm.route({coordinates: two_test_coordinates}, { format: 'invalid' }, function(err, route) {})}, + /format must be a string:/); }); test('route: provides no alternatives by default, but when requested it may (not guaranteed)', function(assert) { diff --git a/test/nodejs/table.js b/test/nodejs/table.js index 220added1b0..e763eea866e 100644 --- a/test/nodejs/table.js +++ b/test/nodejs/table.js @@ -48,6 +48,20 @@ test('table: test annotations paramater combination', function(assert) { }); }); +test('table: returns buffer', function(assert) { + assert.plan(3); + var osrm = new OSRM(data_path); + var options = { + coordinates: [three_test_coordinates[0], three_test_coordinates[1]], + }; + osrm.table(options, { format: 'json_buffer' }, function(err, table) { + assert.ifError(err); + assert.ok(table instanceof Buffer); + table = JSON.parse(table); + assert.ok(table['durations'], 'distances table result should exist'); + }); +}); + var tables = ['distances', 'durations']; tables.forEach(function(annotation) { @@ -116,7 +130,7 @@ tables.forEach(function(annotation) { }); test('table: ' + annotation + ' throws on invalid arguments', function(assert) { - assert.plan(14); + assert.plan(15); var osrm = new OSRM(data_path); var options = {annotations: [annotation.slice(0,-1)]}; assert.throws(function() { osrm.table(options); }, @@ -135,6 +149,9 @@ tables.forEach(function(annotation) { /Coordinates must be an array of \(lon\/lat\) pairs/); options.coordinates = two_test_coordinates; + assert.throws(function() { osrm.table(options, { format: 'invalid' }, function(err, response) {}) }, + /format must be a string:/); + options.sources = true; assert.throws(function() { osrm.table(options, function(err, response) {}) }, /Sources must be an array of indices \(or undefined\)/); diff --git a/test/nodejs/trip.js b/test/nodejs/trip.js index ff2294712db..f26d97b83ca 100644 --- a/test/nodejs/trip.js +++ b/test/nodejs/trip.js @@ -17,6 +17,19 @@ test('trip: trip in Monaco', function(assert) { }); }); +test('trip: trip in Monaco as a buffer', function(assert) { + assert.plan(3); + var osrm = new OSRM(data_path); + osrm.trip({coordinates: two_test_coordinates}, { format: 'json_buffer' }, function(err, trip) { + assert.ifError(err); + assert.ok(trip instanceof Buffer); + trip = JSON.parse(trip); + for (t = 0; t < trip.trips.length; t++) { + assert.ok(trip.trips[t].geometry); + } + }); +}); + test('trip: trip with many locations in Monaco', function(assert) { assert.plan(2); @@ -33,12 +46,14 @@ test('trip: trip with many locations in Monaco', function(assert) { }); test('trip: throws with too few or invalid args', function(assert) { - assert.plan(2); + assert.plan(3); var osrm = new OSRM(data_path); assert.throws(function() { osrm.trip({coordinates: two_test_coordinates}) }, /Two arguments required/); assert.throws(function() { osrm.trip(null, function(err, trip) {}) }, /First arg must be an object/); + assert.throws(function() { osrm.trip({coordinates: two_test_coordinates}, { format: 'invalid' }, function(err, trip) {}) }, + /format must be a string:/); }); test('trip: throws with bad params', function(assert) {