diff --git a/geometry/latlon.hpp b/geometry/latlon.hpp index dd9b30d834..54dcaf84fb 100644 --- a/geometry/latlon.hpp +++ b/geometry/latlon.hpp @@ -1,5 +1,7 @@ #pragma once +#include "geometry/latlon.hpp" + #include "base/math.hpp" #include "std/string.hpp" diff --git a/map/framework.hpp b/map/framework.hpp index 7c5e46a9c6..4ce8263666 100644 --- a/map/framework.hpp +++ b/map/framework.hpp @@ -1,7 +1,6 @@ #pragma once #include "map/api_mark_point.hpp" -#include "map/booking_api.hpp" #include "map/bookmark.hpp" #include "map/bookmark_manager.hpp" #include "map/displacement_mode_manager.hpp" @@ -34,6 +33,9 @@ #include "storage/downloading_policy.hpp" #include "storage/storage.hpp" +#include "partners_api/booking_api.hpp" +#include "partners_api/uber_api.hpp" + #include "platform/country_defines.hpp" #include "platform/location.hpp" #include "platform/platform.hpp" @@ -156,6 +158,8 @@ protected: BookingApi m_bookingApi; + uber::Api m_uberApi; + bool m_isRenderingEnabled; /// This function will be called by m_storage when latest local files @@ -185,6 +189,8 @@ public: BookingApi & GetBookingApi() { return m_bookingApi; } BookingApi const & GetBookingApi() const { return m_bookingApi; } + uber::Api & GetUberApi() { return m_uberApi;} + /// Migrate to new version of very different data. bool IsEnoughSpaceForMigrate() const; storage::TCountryId PreMigrate(ms::LatLon const & position, storage::Storage::TChangeCountryFunction const & change, diff --git a/map/map.pro b/map/map.pro index 4aabaf916c..264f744590 100644 --- a/map/map.pro +++ b/map/map.pro @@ -12,7 +12,6 @@ include($$ROOT_DIR/common.pri) HEADERS += \ api_mark_point.hpp \ - booking_api.hpp \ bookmark.hpp \ bookmark_manager.hpp \ chart_generator.hpp \ @@ -36,7 +35,6 @@ SOURCES += \ ../api/src/c/api-client.c \ address_finder.cpp \ api_mark_point.cpp \ - booking_api.cpp \ bookmark.cpp \ bookmark_manager.cpp \ chart_generator.cpp \ diff --git a/map/map_tests/map_tests.pro b/map/map_tests/map_tests.pro index 3a46cddf32..e44ce96790 100644 --- a/map/map_tests/map_tests.pro +++ b/map/map_tests/map_tests.pro @@ -6,7 +6,7 @@ CONFIG -= app_bundle TEMPLATE = app ROOT_DIR = ../.. -DEPENDENCIES = map drape_frontend routing search storage drape indexer platform editor geometry coding base \ +DEPENDENCIES = map drape_frontend routing search storage drape indexer partners_api platform editor geometry coding base \ freetype fribidi expat protobuf tomcrypt jansson osrm stats_client minizip succinct pugixml stats_client DEPENDENCIES *= opening_hours @@ -36,7 +36,6 @@ macx-*: LIBS *= "-framework IOKit" "-framework SystemConfiguration" SOURCES += \ ../../testing/testingmain.cpp \ address_tests.cpp \ - booking_tests.cpp \ bookmarks_test.cpp \ chart_generator_tests.cpp \ feature_getters_tests.cpp \ diff --git a/mapshot/mapshot.pro b/mapshot/mapshot.pro index d92c5a3d0b..c1f37891d9 100644 --- a/mapshot/mapshot.pro +++ b/mapshot/mapshot.pro @@ -1,7 +1,7 @@ # mapshot binary ROOT_DIR = .. -DEPENDENCIES = map drape_frontend routing search storage indexer drape platform editor geometry coding base \ +DEPENDENCIES = map drape_frontend routing search storage indexer drape partners_api platform editor geometry coding base \ freetype expat fribidi tomcrypt gflags jansson protobuf osrm stats_client minizip succinct \ pugixml opening_hours diff --git a/omim.pro b/omim.pro index 7afb01833c..41ab3157d9 100644 --- a/omim.pro +++ b/omim.pro @@ -23,7 +23,7 @@ HEADERS += defines.hpp CONFIG *= desktop } -SUBDIRS = 3party base coding geometry editor indexer routing search +SUBDIRS = 3party base coding geometry editor indexer partners_api routing search !CONFIG(osrm) { SUBDIRS *= platform stats storage @@ -241,5 +241,9 @@ SUBDIRS = 3party base coding geometry editor indexer routing search drape_frontend_tests.subdir = drape_frontend/drape_frontend_tests drape_frontend_tests.depends = 3party base coding platform drape drape_frontend SUBDIRS *= drape_frontend_tests + + partners_api_tests.subdir = partners_api/partners_api_tests + partners_api_tests.depends = base platform partners_api + SUBDIRS *= partners_api_tests } # !no-tests } # !gtool diff --git a/map/booking_api.cpp b/partners_api/booking_api.cpp similarity index 99% rename from map/booking_api.cpp rename to partners_api/booking_api.cpp index 1e3aef6e73..b634411716 100644 --- a/map/booking_api.cpp +++ b/partners_api/booking_api.cpp @@ -1,4 +1,4 @@ -#include "map/booking_api.hpp" +#include "partners_api/booking_api.hpp" #include "base/gmtime.hpp" #include "base/logging.hpp" diff --git a/map/booking_api.hpp b/partners_api/booking_api.hpp similarity index 100% rename from map/booking_api.hpp rename to partners_api/booking_api.hpp diff --git a/partners_api/partners_api.pro b/partners_api/partners_api.pro new file mode 100644 index 0000000000..966eb00066 --- /dev/null +++ b/partners_api/partners_api.pro @@ -0,0 +1,18 @@ +TARGET = partners_api +TEMPLATE = lib +CONFIG += staticlib warn_on + +ROOT_DIR = .. + +INCLUDEPATH *= $$ROOT_DIR/3party/jansson/src + +include($$ROOT_DIR/common.pri) + +SOURCES += \ + booking_api.cpp \ + uber_api.cpp \ + +HEADERS += \ + booking_api.hpp \ + uber_api.hpp \ + diff --git a/map/map_tests/booking_tests.cpp b/partners_api/partners_api_tests/booking_tests.cpp similarity index 98% rename from map/map_tests/booking_tests.cpp rename to partners_api/partners_api_tests/booking_tests.cpp index b92279a6bb..1af991dcee 100644 --- a/map/map_tests/booking_tests.cpp +++ b/partners_api/partners_api_tests/booking_tests.cpp @@ -1,6 +1,6 @@ #include "testing/testing.hpp" -#include "map/booking_api.hpp" +#include "partners_api/booking_api.hpp" UNIT_TEST(Booking_SmokeTest) { diff --git a/partners_api/partners_api_tests/partners_api_tests.pro b/partners_api/partners_api_tests/partners_api_tests.pro new file mode 100644 index 0000000000..b5ea1148b1 --- /dev/null +++ b/partners_api/partners_api_tests/partners_api_tests.pro @@ -0,0 +1,16 @@ +TARGET = partners_api_tests +CONFIG += console warn_on +CONFIG -= app_bundle +TEMPLATE = app + +ROOT_DIR = ../.. +DEPENDENCIES = partners_api platform coding base tomcrypt jansson stats_client + +include($$ROOT_DIR/common.pri) + +QT *= core + +SOURCES += \ + $$ROOT_DIR/testing/testingmain.cpp \ + booking_tests.cpp \ + uber_tests.cpp \ diff --git a/partners_api/partners_api_tests/uber_tests.cpp b/partners_api/partners_api_tests/uber_tests.cpp new file mode 100644 index 0000000000..6fc05fb7f7 --- /dev/null +++ b/partners_api/partners_api_tests/uber_tests.cpp @@ -0,0 +1,29 @@ +#include "testing/testing.hpp" + +#include "geometry/latlon.hpp" + +#include "partners_api/uber_api.hpp" + +#include "base/logging.hpp" + +UNIT_TEST(Uber_SmokeTest) +{ + ms::LatLon from(59.856464, 30.371867); + ms::LatLon to(59.856000, 30.371000); + + { + uber::Api uberApi; + auto reqId = 0; + reqId = uberApi.GetAvailableProducts(from, to, [&reqId](vector const & products, size_t const requestId) + { + TEST(!products.empty(), ()); + TEST_EQUAL(requestId, reqId, ()); + + for(auto const & product : products) + { + LOG(LINFO, (product.m_productId, product.m_name, product.m_time, product.m_price)); + TEST(!product.m_productId.empty() && !product.m_name.empty() && !product.m_time.empty() && !product.m_price.empty(),()); + } + }); + } +} diff --git a/partners_api/uber_api.cpp b/partners_api/uber_api.cpp new file mode 100644 index 0000000000..4f5f9cb2fa --- /dev/null +++ b/partners_api/uber_api.cpp @@ -0,0 +1,266 @@ +#include "uber_api.hpp" + +#include "platform/http_client.hpp" + +#include "geometry/latlon.hpp" + +#include "base/assert.hpp" +#include "base/logging.hpp" + +#include "std/future.hpp" + +#include "3party/jansson/myjansson.hpp" + +#include "private.h" + +#define UBER_SERVER_TOKEN "" +#define UBER_CLIENT_ID "" + +using namespace platform; + +namespace +{ +string RunSimpleHttpRequest(string const & url) +{ + HttpClient request(url); + + if (request.RunHttpRequest() && !request.WasRedirected() && request.ErrorCode() == 200) + { + return request.ServerResponse(); + } + + return {}; +} + +bool CheckUberAnswer(json_t const * answer) +{ + // Uber products are not available at this point. + if (!json_is_array(answer)) + return false; + + if (json_array_size(answer) <= 0) + return false; + + return true; +} + +void FillProducts(json_t const * time, json_t const * price, vector & products) +{ + auto const timeSize = json_array_size(time); + for (size_t i = 0; i < timeSize; ++i) + { + uber::Product product; + json_int_t estimatedTime = 0; + auto const item = json_array_get(time, i); + my::FromJSONObject(item, "display_name", product.m_name); + my::FromJSONObject(item, "estimate", estimatedTime); + product.m_time = strings::to_string(estimatedTime); + products.push_back(product); + } + + auto const priceSize = json_array_size(price); + for (size_t i = 0; i < priceSize; ++i) + { + string name; + auto const item = json_array_get(price, i); + + my::FromJSONObject(item, "display_name", name); + auto const it = find_if(products.begin(), products.end(), [&name](uber::Product const & product){ + return product.m_name == name; + }); + if (it == products.end()) + continue; + + my::FromJSONObject(item, "product_id", it->m_productId); + my::FromJSONObject(item, "estimate", it->m_price); + } + + products.erase(remove_if(products.begin(), products.end(), [](uber::Product const & p){ + return p.m_name.empty() || p.m_productId.empty() || p.m_time.empty() || + p.m_price.empty(); + }), products.end()); +} + +void GetAvailableProductsAsync(ms::LatLon const & from, ms::LatLon const & to, size_t const requestId, + uber::ProductsCallback const & fn) +{ + auto time = async(launch::async, uber::RawApi::GetEstimatedTime, ref(from)); + auto price = async(launch::async, uber::RawApi::GetEstimatedPrice, ref(from), ref(to)); + + vector products; + + try + { + my::Json timeRoot(time.get().c_str()); + my::Json priceRoot(price.get().c_str()); + auto const timesArray = json_object_get(timeRoot.get(), "times"); + auto const pricesArray = json_object_get(priceRoot.get(), "prices"); + if (CheckUberAnswer(timesArray) && CheckUberAnswer(pricesArray)) + { + FillProducts(timesArray, pricesArray, products); + } + } + catch (my::Json::Exception const & e) + { + LOG(LERROR, (e.Msg())); + products.clear(); + } + + fn(products, requestId); +} +} // namespace + +namespace uber +{ +// static +string RawApi::GetProducts(ms::LatLon const & pos) +{ + stringstream url; + url << "https://api.uber.com/v1/products?server_token=" << UBER_SERVER_TOKEN << + "&latitude=" << static_cast(pos.lat) << + "&longitude=" << static_cast(pos.lon); + + return RunSimpleHttpRequest(url.str()); +} + +// static +string RawApi::GetEstimatedTime(ms::LatLon const & pos) +{ +// stringstream url; +// url << "https://api.uber.com/v1/products?server_token=" << UBER_SERVER_TOKEN << +// "&start_latitude=" << static_cast(pos.lat) << +// "&start_longitude=" << static_cast(pos.lon); + +// return RunSimpleHttpRequest(url.str()); + return R"({ + "times":[ + { + "localized_display_name":"uberPOOL", + "estimate":180, + "display_name":"uberPOOL", + "product_id":"26546650-e557-4a7b-86e7-6a3942445247" + }, + { + "localized_display_name":"uberX", + "estimate":180, + "display_name":"uberX", + "product_id":"a1111c8c-c720-46c3-8534-2fcdd730040d" + }, + { + "localized_display_name":"uberXL", + "estimate":420, + "display_name":"uberXL", + "product_id":"821415d8-3bd5-4e27-9604-194e4359a449" + }, + { + "localized_display_name":"UberBLACK", + "estimate":180, + "display_name":"UberBLACK", + "product_id":"d4abaae7-f4d6-4152-91cc-77523e8165a4" + } + ] + })"; +} + +// static +string RawApi::GetEstimatedPrice(ms::LatLon const & from, ms::LatLon const & to) +{ +// stringstream url; +// url << "https://api.uber.com/v1/products?server_token=" << UBER_SERVER_TOKEN << +// "&start_latitude=" << static_cast(from.lat) << +// "&start_longitude=" << static_cast(from.lon) << +// "&end_latitude=" << static_cast(to.lat) << +// "&end_longitude=" << static_cast(to.lon); + +// return RunSimpleHttpRequest(url.str()); + return R"({ + "prices":[ + { + "product_id": "26546650-e557-4a7b-86e7-6a3942445247", + "currency_code": "USD", + "display_name": "POOL", + "estimate": "$7", + "low_estimate": 7, + "high_estimate": 7, + "surge_multiplier": 1, + "duration": 640, + "distance": 5.34 + }, + { + "product_id": "08f17084-23fd-4103-aa3e-9b660223934b", + "currency_code": "USD", + "display_name": "UberBLACK", + "estimate": "$23-29", + "low_estimate": 23, + "high_estimate": 29, + "surge_multiplier": 1, + "duration": 640, + "distance": 5.34 + }, + { + "product_id": "9af0174c-8939-4ef6-8e91-1a43a0e7c6f6", + "currency_code": "USD", + "display_name": "UberSUV", + "estimate": "$36-44", + "low_estimate": 36, + "high_estimate": 44, + "surge_multiplier": 1.25, + "duration": 640, + "distance": 5.34 + }, + { + "product_id": "aca52cea-9701-4903-9f34-9a2395253acb", + "currency_code": null, + "display_name": "uberTAXI", + "estimate": "Metered", + "low_estimate": null, + "high_estimate": null, + "surge_multiplier": 1, + "duration": 640, + "distance": 5.34 + }, + { + "product_id": "a27a867a-35f4-4253-8d04-61ae80a40df5", + "currency_code": "USD", + "display_name": "uberX", + "estimate": "$15", + "low_estimate": 15, + "high_estimate": 15, + "surge_multiplier": 1, + "duration": 640, + "distance": 5.34 + } + ] + })"; +} + +size_t Api::GetAvailableProducts(ms::LatLon const & from, ms::LatLon const & to, + ProductsCallback const & fn) +{ + lock_guard lock(m_mutex); + + m_thread.reset(); + static size_t requestId = 0; + ++requestId; + m_thread = unique_ptr( + new threads::SimpleThread(GetAvailableProductsAsync, ref(from), ref(to), requestId, ref(fn)), + [](threads::SimpleThread * ptr) { ptr->join(); delete ptr; }); + + return requestId; +} + +// static +string Api::GetRideRequestLink(string const & m_productId, ms::LatLon const & from, + ms::LatLon const & to) +{ + stringstream url; + url << "uber://?client_id=" << UBER_CLIENT_ID << + "&action=setPickup&product_id=" << m_productId << + "&pickup[latitude]=" << static_cast(from.lat) << + "&pickup[longitude]=" << static_cast(from.lon) << + "&dropoff[latitude]=" << static_cast(to.lat)<< + "&dropoff[longitude]=" << static_cast(to.lon); + + return url.str(); +} +} // namespace uber diff --git a/partners_api/uber_api.hpp b/partners_api/uber_api.hpp new file mode 100644 index 0000000000..4d9a19a0ab --- /dev/null +++ b/partners_api/uber_api.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include "base/thread.hpp" + +#include "std/function.hpp" +#include "std/mutex.hpp" +#include "std/string.hpp" +#include "std/unique_ptr.hpp" +#include "std/vector.hpp" + +namespace ms +{ + class LatLon; +} // namespace ms + +namespace downloader +{ + class HttpRequest; +} // namespace downloader + +namespace uber +{ +// Uber api wrapper based on synchronous http requests. +class RawApi +{ +public: + /// Returns information about the Uber products offered at a given location. + /// The response includes the display name and other details about each product, and lists the + /// products in the proper display order. This endpoint does not reflect real-time availability + /// of the products. + static string GetProducts(ms::LatLon const & pos); + /// Returns ETAs for all products currently available at a given location, with the ETA for each + /// product expressed as integers in seconds. If a product returned from GetProducts is not + /// returned from this endpoint for a given latitude/longitude pair then there are currently none + /// of that product available to request. Call this endpoint every minute to provide the most + /// accurate, up-to-date ETAs. + static string GetEstimatedTime(ms::LatLon const & pos); + /// Returns an estimated price range for each product offered at a given location. The price + /// estimate is provided as a formatted string with the full price range and the localized + /// currency symbol. + static string GetEstimatedPrice(ms::LatLon const & from, ms::LatLon const & to); +}; + +struct Product +{ + string m_productId; + string m_name; + string m_time; + string m_price; +}; +/// @products - vector of available products for requested route. +/// @requestId - identificator which was provided to GetAvailableProducts to identify request. +using ProductsCallback = function const & products, size_t const requestId)>; + +class Api +{ +public: + /// Requests list of available products from Uber. Returns request identificator immediately. + size_t GetAvailableProducts(ms::LatLon const & from, ms::LatLon const & to, + ProductsCallback const & fn); + + /// Returns link which allows you to launch the Uber app. + static string GetRideRequestLink(string const & m_productId, ms::LatLon const & from, ms::LatLon const & to); + +private: + using threadDeleter = function; + unique_ptr m_thread; + + mutex m_mutex; +}; +} // namespace uber + + + diff --git a/qt/qt.pro b/qt/qt.pro index 2befa2c463..1667db7068 100644 --- a/qt/qt.pro +++ b/qt/qt.pro @@ -1,6 +1,6 @@ # Main application in qt. ROOT_DIR = .. -DEPENDENCIES = map drape_frontend routing search storage indexer drape platform editor geometry \ +DEPENDENCIES = map drape_frontend routing search storage indexer drape partners_api platform editor geometry \ coding base freetype expat fribidi tomcrypt jansson protobuf osrm stats_client \ minizip succinct pugixml oauthcpp diff --git a/storage/storage_integration_tests/storage_integration_tests.pro b/storage/storage_integration_tests/storage_integration_tests.pro index d038ad45e4..7d6a357505 100644 --- a/storage/storage_integration_tests/storage_integration_tests.pro +++ b/storage/storage_integration_tests/storage_integration_tests.pro @@ -6,7 +6,7 @@ CONFIG -= app_bundle TEMPLATE = app ROOT_DIR = ../.. -DEPENDENCIES = map drape_frontend routing search storage indexer drape platform_tests_support platform editor opening_hours geometry \ +DEPENDENCIES = map drape_frontend routing search storage indexer drape partners_api platform_tests_support platform editor opening_hours geometry \ coding base freetype expat fribidi tomcrypt jansson protobuf osrm stats_client \ minizip succinct pugixml oauthcpp