diff --git a/map/displacement_mode_manager.cpp b/map/displacement_mode_manager.cpp new file mode 100644 index 0000000000..46ce5d5d79 --- /dev/null +++ b/map/displacement_mode_manager.cpp @@ -0,0 +1,29 @@ +#include "map/displacement_mode_manager.hpp" + +DisplacementModeManager::DisplacementModeManager(TCallback && callback) + : m_callback(move(callback)), m_mask(0) +{ +} + +void DisplacementModeManager::Set(Slot slot, bool show) +{ + lock_guard lock(m_mu); + + uint32_t const bit = static_cast(1) << slot; + uint32_t const mask = m_mask; + if (show) + m_mask = mask | bit; + else + m_mask = mask & ~bit; + + // We are only intersected in states "mask is zero" and "mask is not + // zero". Therefore, do nothing if the new state is the same as the + // previous state. + if ((mask == 0) == (m_mask == 0)) + return; + + if (m_mask != 0) + m_callback(true /* show */); + else + m_callback(false /* show */); +} diff --git a/map/displacement_mode_manager.hpp b/map/displacement_mode_manager.hpp new file mode 100644 index 0000000000..a86f7846a6 --- /dev/null +++ b/map/displacement_mode_manager.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "std/cstdint.hpp" +#include "std/function.hpp" +#include "std/mutex.hpp" + +// This class should be used as a single point to control whether +// hotels displacement mode should be activated or not. +// +// *NOTE* as the class is thread-safe, the callback should be +// very-very lightweight and it should be guaranteed that callback or +// its callees do not call DisplacementModeManager API. Otherwise, +// deadlock or exception may happen. +class DisplacementModeManager +{ +public: + using TCallback = function; + + enum Slot + { + SLOT_INTERACTIVE_SEARCH, + SLOT_MAP_SELECTION, + SLOT_DEBUG, + SLOT_COUNT + }; + + DisplacementModeManager(TCallback && callback); + + void Set(Slot slot, bool show); + +private: + TCallback m_callback; + + mutex m_mu; + uint32_t m_mask; + + static_assert(SLOT_COUNT <= sizeof(m_mask) * CHAR_BIT, "Number of slots is too large"); +}; diff --git a/map/framework.cpp b/map/framework.cpp index b5f9ed046b..35b720d862 100644 --- a/map/framework.cpp +++ b/map/framework.cpp @@ -13,6 +13,7 @@ #include "search/engine.hpp" #include "search/geometry_utils.hpp" +#include "search/interactive_search_callback.hpp" #include "search/intermediate_result.hpp" #include "search/processor_factory.hpp" #include "search/result.hpp" @@ -303,6 +304,10 @@ Framework::Framework() , m_storage(platform::migrate::NeedMigrate() ? COUNTRIES_OBSOLETE_FILE : COUNTRIES_FILE) , m_bmManager(*this) , m_isRenderingEnabled(true) + , m_displacementModeManager([this](bool show) { + int const mode = show ? dp::displacement::kHotelMode : dp::displacement::kDefaultMode; + CallDrapeFunction(bind(&df::DrapeEngine::SetDisplacementMode, _1, mode)); + }) , m_lastReportedCountry(kInvalidCountryId) { m_startBackgroundTime = my::Timer::LocalTime(); @@ -914,9 +919,9 @@ void Framework::PrepareToShutdown() DestroyDrapeEngine(); } -void Framework::SetDisplacementMode(int mode) +void Framework::SetDisplacementMode(DisplacementModeManager::Slot slot, bool show) { - CallDrapeFunction(bind(&df::DrapeEngine::SetDisplacementMode, _1, mode)); + m_displacementModeManager.Set(slot, show); } void Framework::SaveViewport() @@ -1066,15 +1071,13 @@ void Framework::InvalidateRect(m2::RectD const & rect) void Framework::StartInteractiveSearch(search::SearchParams const & params) { - using namespace search; - auto const originalOnResults = params.m_onResults; - m_lastInteractiveSearchParams = params; - m_lastInteractiveSearchParams.SetForceSearch(false); - m_lastInteractiveSearchParams.SetMode(search::Mode::Viewport); - m_lastInteractiveSearchParams.SetSuggestsEnabled(false); - m_lastInteractiveSearchParams.m_onResults = [this, originalOnResults](Results const & results) { + auto setMode = [this]() { + SetDisplacementMode(DisplacementModeManager::SLOT_INTERACTIVE_SEARCH, true /* show */); + }; + + auto onResults = [this, originalOnResults](search::Results const & results) { if (!results.IsEndMarker()) { GetPlatform().RunOnGuiThread([this, results]() { @@ -1086,6 +1089,14 @@ void Framework::StartInteractiveSearch(search::SearchParams const & params) if (originalOnResults) originalOnResults(results); }; + + m_lastInteractiveSearchParams = params; + m_lastInteractiveSearchParams.SetForceSearch(false); + m_lastInteractiveSearchParams.SetMode(search::Mode::Viewport); + m_lastInteractiveSearchParams.SetSuggestsEnabled(false); + m_lastInteractiveSearchParams.m_onResults = + search::InteractiveSearchCallback(move(setMode), move(onResults)); + UpdateUserViewportChanged(); } @@ -1474,6 +1485,7 @@ void Framework::CancelInteractiveSearch() if (IsInteractiveSearchActive()) { m_lastInteractiveSearchParams.Clear(); + SetDisplacementMode(DisplacementModeManager::SLOT_INTERACTIVE_SEARCH, false /* show */); CancelQuery(m_lastProcessorHandle); } } @@ -1938,8 +1950,7 @@ void Framework::ActivateMapSelection(bool needAnimation, df::SelectionShape::ESe CallDrapeFunction(bind(&df::DrapeEngine::SelectObject, _1, selectionType, info.GetMercator(), info.GetID(), needAnimation)); - SetDisplacementMode(info.IsHotel() ? dp::displacement::kHotelMode - : dp::displacement::kDefaultMode); + SetDisplacementMode(DisplacementModeManager::SLOT_MAP_SELECTION, info.IsHotel() /* show */); if (m_activateMapSelectionFn) m_activateMapSelectionFn(info); @@ -1958,7 +1969,7 @@ void Framework::DeactivateMapSelection(bool notifyUI) if (somethingWasAlreadySelected) CallDrapeFunction(bind(&df::DrapeEngine::DeselectObject, _1)); - SetDisplacementMode(dp::displacement::kDefaultMode); + SetDisplacementMode(DisplacementModeManager::SLOT_MAP_SELECTION, false /* show */); } void Framework::UpdatePlacePageInfoForCurrentSelection() diff --git a/map/framework.hpp b/map/framework.hpp index 5f3940c480..1c807b6e10 100644 --- a/map/framework.hpp +++ b/map/framework.hpp @@ -4,9 +4,10 @@ #include "map/booking_api.hpp" #include "map/bookmark.hpp" #include "map/bookmark_manager.hpp" -#include "map/place_page_info.hpp" +#include "map/displacement_mode_manager.hpp" #include "map/feature_vec_model.hpp" #include "map/mwm_url.hpp" +#include "map/place_page_info.hpp" #include "map/track.hpp" #include "drape_frontend/gui/skin.hpp" @@ -381,12 +382,14 @@ public: void PrepareToShutdown(); - void SetDisplacementMode(int mode); + void SetDisplacementMode(DisplacementModeManager::Slot slot, bool show); private: void InitCountryInfoGetter(); void InitSearchEngine(); + DisplacementModeManager m_displacementModeManager; + // Last search query params for the interactive search. search::SearchParams m_lastInteractiveSearchParams; diff --git a/map/map.pro b/map/map.pro index bb16d45fbc..582f3744d1 100644 --- a/map/map.pro +++ b/map/map.pro @@ -15,6 +15,7 @@ HEADERS += \ booking_api.hpp \ bookmark.hpp \ bookmark_manager.hpp \ + displacement_mode_manager.hpp \ feature_vec_model.hpp \ framework.hpp \ ge0_parser.hpp \ @@ -37,13 +38,14 @@ SOURCES += \ booking_api.cpp \ bookmark.cpp \ bookmark_manager.cpp \ + displacement_mode_manager.cpp \ feature_vec_model.cpp \ framework.cpp \ ge0_parser.cpp \ geourl_process.cpp \ gps_track.cpp \ - gps_track_filter.cpp \ gps_track_collection.cpp \ + gps_track_filter.cpp \ gps_track_storage.cpp \ gps_tracker.cpp \ mwm_url.cpp \ diff --git a/qt/search_panel.cpp b/qt/search_panel.cpp index 85c6476b65..65095e57b1 100644 --- a/qt/search_panel.cpp +++ b/qt/search_panel.cpp @@ -249,12 +249,18 @@ bool SearchPanel::TryDisplacementModeCmd(QString const & str) if (!isDefaultDisplacementMode && !isHotelDisplacementMode) return false; - + if (isDefaultDisplacementMode) - m_pDrawWidget->GetFramework().SetDisplacementMode(dp::displacement::kDefaultMode); + { + m_pDrawWidget->GetFramework().SetDisplacementMode(DisplacementModeManager::SLOT_DEBUG, + false /* show */); + } else if (isHotelDisplacementMode) - m_pDrawWidget->GetFramework().SetDisplacementMode(dp::displacement::kHotelMode); - + { + m_pDrawWidget->GetFramework().SetDisplacementMode(DisplacementModeManager::SLOT_DEBUG, + true /* show */); + } + return true; } diff --git a/search/hotels_classifier.cpp b/search/hotels_classifier.cpp new file mode 100644 index 0000000000..4a21d3cb67 --- /dev/null +++ b/search/hotels_classifier.cpp @@ -0,0 +1,28 @@ +#include "search/hotels_classifier.hpp" + +#include "search/result.hpp" + +namespace search +{ +void HotelsClassifier::AddBatch(Results const & results) +{ + if (results.IsEndMarker()) + return; + + for (auto const & result : results) + { + ++m_numResults; + if (result.m_metadata.m_isHotel) + ++m_numHotels; + } +} + +bool HotelsClassifier::IsHotelQuery() const +{ + // Threshold used to activate hotels mode. Probably is too strict, + // but we don't have statistics now. + double const kThreshold = 0.95; + + return m_numResults == 0 ? false : m_numHotels >= kThreshold * m_numResults; +} +} // namespace search diff --git a/search/hotels_classifier.hpp b/search/hotels_classifier.hpp new file mode 100644 index 0000000000..1619a5c746 --- /dev/null +++ b/search/hotels_classifier.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "std/cstdint.hpp" + +namespace search +{ +class Results; + +// A binary classifier that can be used in conjunction with search +// engine to decide whether the majority of results are hotels or not. +// +// *NOTE* This class is NOT thread safe. +class HotelsClassifier +{ +public: + void AddBatch(Results const & results); + + bool IsHotelQuery() const; + +private: + uint64_t m_numHotels = 0; + uint64_t m_numResults = 0; +}; +} // namespace search diff --git a/search/interactive_search_callback.cpp b/search/interactive_search_callback.cpp new file mode 100644 index 0000000000..d7981bab4d --- /dev/null +++ b/search/interactive_search_callback.cpp @@ -0,0 +1,25 @@ +#include "search/interactive_search_callback.hpp" + +#include "search/result.hpp" + +namespace search +{ +InteractiveSearchCallback::InteractiveSearchCallback(TSetDisplacementMode && setMode, + TOnResults && onResults) + : m_setMode(move(setMode)), m_onResults(move(onResults)), m_hotelsModeSet(false) +{ +} + +void InteractiveSearchCallback::operator()(search::Results const & results) +{ + m_hotelsClassif.AddBatch(results); + + if (!m_hotelsModeSet && m_hotelsClassif.IsHotelQuery()) + { + m_setMode(); + m_hotelsModeSet = true; + } + + m_onResults(results); +} +} // namespace search diff --git a/search/interactive_search_callback.hpp b/search/interactive_search_callback.hpp new file mode 100644 index 0000000000..8d42f537a0 --- /dev/null +++ b/search/interactive_search_callback.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "search/hotels_classifier.hpp" +#include "search/params.hpp" + +#include "std/function.hpp" + +namespace search +{ +class Results; + +// An on-results-callback that should be used for interactive search. +// +// *NOTE* the class is NOT thread safe. +class InteractiveSearchCallback +{ +public: + using TSetDisplacementMode = function; + using TOnResults = search::TOnResults; + + InteractiveSearchCallback(TSetDisplacementMode && setMode, TOnResults && onResults); + + void operator()(search::Results const & results); + +private: + TSetDisplacementMode m_setMode; + TOnResults m_onResults; + + search::HotelsClassifier m_hotelsClassif; + bool m_hotelsModeSet; +}; +} // namespace search diff --git a/search/intermediate_result.cpp b/search/intermediate_result.cpp index 0a983e036c..c0652c078c 100644 --- a/search/intermediate_result.cpp +++ b/search/intermediate_result.cpp @@ -29,7 +29,8 @@ double const DIST_SAME_STREET = 5000.0; char const * const kEmptyRatingSymbol = "-"; char const * const kPricingSymbol = "$"; -void ProcessMetadata(FeatureType const & ft, Result::Metadata & meta) +void ProcessMetadata(FeatureType const & ft, feature::TypesHolder const & types, + Result::Metadata & meta) { if (meta.m_isInitialized) return; @@ -56,6 +57,7 @@ void ProcessMetadata(FeatureType const & ft, Result::Metadata & meta) bool const isSponsoredHotel = ftypes::IsBookingChecker::Instance()(ft); meta.m_isSponsoredHotel = isSponsoredHotel; + meta.m_isHotel = ftypes::IsHotelChecker::Instance()(ft); if (isSponsoredHotel) { @@ -114,7 +116,7 @@ PreResult2::PreResult2(FeatureType const & f, PreResult1 const * p, m2::PointD c m_region.SetParams(fileName, center); m_distance = PointDistance(center, pivot); - ProcessMetadata(f, m_metadata); + ProcessMetadata(f, m_types, m_metadata); } PreResult2::PreResult2(double lat, double lon) diff --git a/search/result.hpp b/search/result.hpp index b3497bf0c5..e541c9b1f4 100644 --- a/search/result.hpp +++ b/search/result.hpp @@ -31,13 +31,17 @@ public: /// Metadata for search results. Considered valid if m_resultType == RESULT_FEATURE. struct Metadata { - string m_cuisine; // Valid only if not empty. Used for restaurants. - int m_stars = 0; // Valid only if not 0. Used for hotels. - bool m_isSponsoredHotel = false; // Used for hotels. - osm::YesNoUnknown m_isOpenNow = osm::Unknown; // Valid for any result. + string m_cuisine; // Valid only if not empty. Used for restaurants. + + // Following fields are used for hotels only. string m_hotelApproximatePricing; string m_hotelRating; - /// True if the struct is already assigned or need to be calculated otherwise. + int m_stars = 0; + bool m_isSponsoredHotel = false; + bool m_isHotel = false; + + osm::YesNoUnknown m_isOpenNow = osm::Unknown; // Valid for any result. + bool m_isInitialized = false; }; @@ -171,6 +175,9 @@ public: inline IterT Begin() const { return m_vec.begin(); } inline IterT End() const { return m_vec.end(); } + inline IterT begin() const { return m_vec.begin(); } + inline IterT end() const { return m_vec.end(); } + inline size_t GetCount() const { return m_vec.size(); } size_t GetSuggestsCount() const; diff --git a/search/search.pro b/search/search.pro index b3612baf6e..3f50c1dadf 100644 --- a/search/search.pro +++ b/search/search.pro @@ -25,9 +25,11 @@ HEADERS += \ geocoder_context.hpp \ geometry_cache.hpp \ geometry_utils.hpp \ + hotels_classifier.hpp \ house_detector.hpp \ house_numbers_matcher.hpp \ house_to_street_table.hpp \ + interactive_search_callback.hpp \ intermediate_result.hpp \ intersection_result.hpp \ interval_set.hpp \ @@ -82,9 +84,11 @@ SOURCES += \ geocoder_context.cpp \ geometry_cache.cpp \ geometry_utils.cpp \ + hotels_classifier.cpp \ house_detector.cpp \ house_numbers_matcher.cpp \ house_to_street_table.cpp \ + interactive_search_callback.cpp \ intermediate_result.cpp \ intersection_result.cpp \ keyword_lang_matcher.cpp \ diff --git a/search/search_integration_tests/interactive_search_test.cpp b/search/search_integration_tests/interactive_search_test.cpp new file mode 100644 index 0000000000..d7b28ac138 --- /dev/null +++ b/search/search_integration_tests/interactive_search_test.cpp @@ -0,0 +1,110 @@ +#include "testing/testing.hpp" + +#include "generator/generator_tests_support/test_feature.hpp" + +#include "search/interactive_search_callback.hpp" +#include "search/mode.hpp" +#include "search/search_integration_tests/helpers.hpp" +#include "search/search_tests_support/test_results_matching.hpp" +#include "search/search_tests_support/test_search_request.hpp" + +#include "base/macros.hpp" + +using namespace generator::tests_support; +using namespace search::tests_support; + +using TRules = vector>; + +namespace search +{ +namespace +{ +class TestCafe : public TestPOI +{ +public: + TestCafe(m2::PointD const & center) : TestPOI(center, "cafe", "en") + { + SetTypes({{"amenity", "cafe"}}); + } +}; + +class TestHotel : public TestPOI +{ +public: + TestHotel(m2::PointD const & center) : TestPOI(center, "hotel", "en") + { + SetTypes({{"tourism", "hotel"}}); + } +}; + +class InteractiveSearchRequest : public TestSearchRequest +{ +public: + InteractiveSearchRequest(TestSearchEngine & engine, string const & query, + m2::RectD const & viewport, bool & mode) + : TestSearchRequest( + engine, query, "en" /* locale */, Mode::Viewport, viewport, + bind(&InteractiveSearchRequest::OnStarted, this), + InteractiveSearchCallback([&mode]() { mode = true; }, + bind(&InteractiveSearchRequest::OnResults, this, _1))) + { + } +}; + +class InteractiveSearchTest : public SearchTest +{ +}; + +double const kDX[] = {-0.01, 0, 0, 0.01}; +double const kDY[] = {0, -0.01, 0.01, 0}; + +static_assert(ARRAY_SIZE(kDX) == ARRAY_SIZE(kDY), "Wrong deltas lists"); + +UNIT_CLASS_TEST(InteractiveSearchTest, Smoke) +{ + m2::PointD const cafesPivot(-1, -1); + m2::PointD const hotelsPivot(1, 1); + + vector cafes; + for (size_t i = 0; i < ARRAY_SIZE(kDX); ++i) + cafes.emplace_back(m2::Shift(cafesPivot, kDX[i], kDY[i])); + + vector hotels; + for (size_t i = 0; i < ARRAY_SIZE(kDX); ++i) + hotels.emplace_back(m2::Shift(hotelsPivot, kDX[i], kDY[i])); + + auto const id = BuildCountry("Wonderland", [&](TestMwmBuilder & builder) { + for (auto const & cafe : cafes) + builder.Add(cafe); + for (auto const & hotel : hotels) + builder.Add(hotel); + }); + + { + bool mode = false; + InteractiveSearchRequest request( + m_engine, "cafe", m2::RectD(m2::PointD(-1.5, -1.5), m2::PointD(-0.5, -0.5)), mode); + request.Wait(); + + TRules const rules = {ExactMatch(id, cafes[0]), ExactMatch(id, cafes[1]), + ExactMatch(id, cafes[2]), ExactMatch(id, cafes[3])}; + + TEST(!mode, ()); + TEST(MatchResults(m_engine, rules, request.Results()), ()); + } + + { + bool mode = false; + InteractiveSearchRequest request(m_engine, "hotel", + m2::RectD(m2::PointD(0.5, 0.5), m2::PointD(1.5, 1.5)), mode); + request.Wait(); + + TRules const rules = {ExactMatch(id, hotels[0]), ExactMatch(id, hotels[1]), + ExactMatch(id, hotels[2]), ExactMatch(id, hotels[3])}; + + TEST(mode, ()); + TEST(MatchResults(m_engine, rules, request.Results()), ()); + } +} +} // namespace +} // namespace search diff --git a/search/search_integration_tests/search_integration_tests.pro b/search/search_integration_tests/search_integration_tests.pro index c764ff9498..b05b0a5645 100644 --- a/search/search_integration_tests/search_integration_tests.pro +++ b/search/search_integration_tests/search_integration_tests.pro @@ -21,6 +21,7 @@ SOURCES += \ ../../testing/testingmain.cpp \ generate_tests.cpp \ helpers.cpp \ + interactive_search_test.cpp \ pre_ranker_test.cpp \ processor_test.cpp \ smoke_test.cpp \ diff --git a/search/search_tests_support/test_search_request.cpp b/search/search_tests_support/test_search_request.cpp index 58bb686c77..1c7f50d3ac 100644 --- a/search/search_tests_support/test_search_request.cpp +++ b/search/search_tests_support/test_search_request.cpp @@ -29,6 +29,19 @@ TestSearchRequest::TestSearchRequest(TestSearchEngine & engine, SearchParams par engine.Search(params, viewport); } +TestSearchRequest::TestSearchRequest(TestSearchEngine & engine, string const & query, + string const & locale, Mode mode, m2::RectD const & viewport, + TOnStarted onStarted, TOnResults onResults) +{ + SearchParams params; + params.m_query = query; + params.m_inputLocale = locale; + params.SetMode(mode); + params.m_onStarted = move(onStarted); + params.m_onResults = move(onResults); + engine.Search(params, viewport); +} + void TestSearchRequest::Wait() { unique_lock lock(m_mu); diff --git a/search/search_tests_support/test_search_request.hpp b/search/search_tests_support/test_search_request.hpp index 35c1031ebb..4b86d5a3e2 100644 --- a/search/search_tests_support/test_search_request.hpp +++ b/search/search_tests_support/test_search_request.hpp @@ -34,8 +34,13 @@ public: steady_clock::duration ResponseTime() const; vector const & Results() const; -private: +protected: + TestSearchRequest(TestSearchEngine & engine, string const & query, string const & locale, + Mode mode, m2::RectD const & viewport, TOnStarted onStarted, + TOnResults onResults); + void SetUpCallbacks(SearchParams & params); + void OnStarted(); void OnResults(search::Results const & results);