diff --git a/qt/place_page_dialog.cpp b/qt/place_page_dialog.cpp index e07ed731873..7e3361c5bb2 100644 --- a/qt/place_page_dialog.cpp +++ b/qt/place_page_dialog.cpp @@ -82,12 +82,14 @@ PlacePageDialog::PlacePageDialog(QWidget * parent, place_page::Info const & info if (info.IsFeature()) { grid->addWidget(new QLabel("Feature ID"), row, 0); - grid->addWidget(new QLabel(QString::fromStdString(DebugPrint(info.GetID()))), row++, 1); + auto labelF = new QLabel(QString::fromStdString(DebugPrint(info.GetID()))); + labelF->setTextInteractionFlags(Qt::TextSelectableByMouse); + grid->addWidget(labelF, row++, 1); grid->addWidget(new QLabel("Raw Types"), row, 0); - QLabel * label = new QLabel(QString::fromStdString(DebugPrint(info.GetTypes()))); - label->setTextInteractionFlags(Qt::TextSelectableByMouse); - grid->addWidget(label, row++, 1); + QLabel * labelT = new QLabel(QString::fromStdString(DebugPrint(info.GetTypes()))); + labelT->setTextInteractionFlags(Qt::TextSelectableByMouse); + grid->addWidget(labelT, row++, 1); } for (auto const prop : info.AvailableProperties()) { diff --git a/search/processor.cpp b/search/processor.cpp index 05cfc72aa9e..43b7737df09 100644 --- a/search/processor.cpp +++ b/search/processor.cpp @@ -59,8 +59,11 @@ #include "base/string_utils.hpp" #include +#include +#include #include #include +#include #include "3party/Alohalytics/src/alohalytics.h" #include "3party/open-location-code/openlocationcode.h" @@ -430,6 +433,212 @@ bool Processor::IsCancelled() const return ret; } +void Processor::SearchByFeatureId() +{ + // String processing is suboptimal in this method so + // we need a guard against very long strings. + size_t const kMaxFeatureIdStringSize = 1000; + if (m_query.size() > kMaxFeatureIdStringSize) + return; + + using Trie = base::MemTrie>; + static Trie countriesTrie; + if (countriesTrie.GetNumNodes() == 1) + { + for (auto const & country : m_infoGetter.GetCountries()) + countriesTrie.Add(country.m_countryId, true); + } + + auto trimLeadingSpaces = [](string & s) { + while (!s.empty() && s.front() == ' ') + s = s.substr(1); + }; + + auto const eatFid = [&trimLeadingSpaces](string & s, uint32_t & fid) -> bool { + trimLeadingSpaces(s); + + if (s.empty()) + return false; + + size_t i = 0; + while (i < s.size() && isdigit(s[i])) + ++i; + + auto const prefix = s.substr(0, i); + if (strings::to_uint32(prefix, fid)) + { + s = s.substr(prefix.size()); + return true; + } + return false; + }; + + auto const eatMwmName = [&trimLeadingSpaces](string & s, storage::CountryId & mwmName) -> bool { + trimLeadingSpaces(s); + + // Greedily eat as much as possible because some country names are prefixes of others. + optional lastPos; + for (size_t i = 0; i < s.size(); ++i) + { + // todo(@m) This must be much faster but MemTrie's iterators do not expose nodes. + if (countriesTrie.HasKey(s.substr(0, i))) + lastPos = i; + } + if (!lastPos) + return false; + + mwmName = s.substr(0, *lastPos); + s = s.substr(*lastPos); + strings::EatPrefix(s, ".mwm"); + return true; + }; + + auto const eatVersion = [&trimLeadingSpaces](string & s, uint32_t & version) -> bool { + trimLeadingSpaces(s); + + if (!s.empty() && s.front() == '0' && (s.size() == 1 || !isdigit(s[1]))) + { + version = 0; + s = s.substr(1); + return true; + } + + size_t const kVersionLength = 6; + if (s.size() >= kVersionLength && all_of(s.begin(), s.begin() + kVersionLength, ::isdigit) && + (s.size() == kVersionLength || !isdigit(s[kVersionLength + 1]))) + { + VERIFY(strings::to_uint32(s.substr(0, kVersionLength), version), ()); + s = s.substr(kVersionLength); + return true; + } + + return false; + }; + + // Create a copy of the query to trim it in-place. + string query(m_query); + strings::Trim(query); + + string const kFidPrefix = "fid"; + bool hasPrefix = false; + + if (strings::EatPrefix(query, kFidPrefix)) + { + hasPrefix = true; + + strings::Trim(query); + if (strings::EatPrefix(query, "=")) + strings::Trim(query); + } + + vector> infos; + m_dataSource.GetMwmsInfo(infos); + + // Case 0. + if (hasPrefix) + { + string s = query; + uint32_t fid; + if (eatFid(s, fid)) + { + // Loop over mwms, sort by distance. + + vector>> results; + vector> guards; + for (auto const & info : infos) + { + auto guard = make_unique(m_dataSource, MwmSet::MwmId(info)); + if (fid >= guard->GetNumFeatures()) + continue; + auto ft = guard->GetFeatureByIndex(fid); + if (!ft) + continue; + auto const center = feature::GetCenter(*ft, FeatureType::WORST_GEOMETRY); + double dist = center.SquaredLength(m_viewport.Center()); + auto pivot = m_viewport.Center(); + if (m_position) + { + auto const distPos = center.SquaredLength(*m_position); + if (dist > distPos) + { + dist = distPos; + pivot = *m_position; + } + } + results.emplace_back(dist, pivot, guard->GetCountryFileName(), move(ft)); + guards.push_back(move(guard)); + } + sort(results.begin(), results.end()); + for (auto const & [dist, pivot, country, ft] : results) + { + auto const center = feature::GetCenter(*ft, FeatureType::WORST_GEOMETRY); + string name; + ft->GetReadableName(name); + m_emitter.AddResultNoChecks( + m_ranker.MakeResult(RankerResult(*ft, center, pivot, name, country), + true /* needAddress */, true /* needHighlighting */)); + m_emitter.Emit(); + } + } + } + + auto const tryEmitting = [this, &infos](storage::CountryId const & mwmName, uint32_t fid) { + for (auto const & info : infos) + { + if (info->GetCountryName() != mwmName) + continue; + + auto guard = make_unique(m_dataSource, MwmSet::MwmId(info)); + if (fid >= guard->GetNumFeatures()) + continue; + auto ft = guard->GetFeatureByIndex(fid); + if (!ft) + continue; + auto const center = feature::GetCenter(*ft, FeatureType::WORST_GEOMETRY); + string name; + ft->GetReadableName(name); + m_emitter.AddResultNoChecks(m_ranker.MakeResult( + RankerResult(*ft, center, m2::PointD() /* pivot */, name, guard->GetCountryFileName()), + true /* needAddress */, true /* needHighlighting */)); + m_emitter.Emit(); + } + }; + + // Case 1. + if (hasPrefix) + { + string s = query; + bool const parenPref = strings::EatPrefix(s, "("); + bool const parenSuff = strings::EatSuffix(s, ")"); + storage::CountryId mwmName; + uint32_t fid; + if (parenPref == parenSuff && eatMwmName(s, mwmName) && strings::EatPrefix(s, ",") && + eatFid(s, fid)) + { + tryEmitting(mwmName, fid); + } + } + + // Case 2. + { + string s = query; + storage::CountryId mwmName; + uint32_t version; + uint32_t fid; + + bool ok = true; + ok = ok && strings::EatPrefix(s, "{ MwmId ["); + ok = ok && eatMwmName(s, mwmName); + ok = ok && strings::EatPrefix(s, ", "); + ok = ok && eatVersion(s, version); + ok = ok && strings::EatPrefix(s, "], "); + ok = ok && eatFid(s, fid); + ok = ok && strings::EatPrefix(s, " }"); + if (ok) + tryEmitting(mwmName, fid); + } +} + Locales Processor::GetCategoryLocales() const { static int8_t const enLocaleCode = CategoriesHolder::MapLocaleToInteger("en"); @@ -511,6 +720,7 @@ void Processor::Search(SearchParams const & params) try { + SearchDebug(); SearchCoordinates(); SearchPlusCode(); SearchPostcode(); @@ -552,6 +762,11 @@ void Processor::Search(SearchParams const & params) SendStatistics(params, viewport, m_emitter.GetResults()); } +void Processor::SearchDebug() +{ + SearchByFeatureId(); +} + void Processor::SearchCoordinates() { buffer_vector results; diff --git a/search/processor.hpp b/search/processor.hpp index 82d5b1c3109..61d88934d6e 100644 --- a/search/processor.hpp +++ b/search/processor.hpp @@ -72,6 +72,8 @@ class Processor : public base::Cancellable void Search(SearchParams const & params); + // Tries to parse a custom debugging command from |m_query|. + void SearchDebug(); // Tries to generate a (lat, lon) result from |m_query|. void SearchCoordinates(); // Tries to parse a plus code from |m_query| and generate a (lat, lon) result. @@ -112,6 +114,16 @@ class Processor : public base::Cancellable bool IsCancelled() const override; protected: + // Show feature by FeatureId. May try to guess as much as possible after the "fid=" prefix but + // at least supports the formats below. + // 0. fid=123 to search for the feature with index 123, results ordered by distance + // from |m_position| or |m_viewport|, whichever is present and closer. + // 1. fid=MwmName,123 or fid=(MwmName,123) to search for the feature with + // index 123 in the Mwm "MwmName" (for example, "Laos" or "Laos.mwm"). + // 2. fid={ MwmId [Laos, 200623], 123 } or just { MwmId [Laos, 200623], 123 } or whatever current + // format of the string returned by FeatureID's DebugPrint is. + void SearchByFeatureId(); + Locales GetCategoryLocales() const; template diff --git a/search/search_integration_tests/processor_test.cpp b/search/search_integration_tests/processor_test.cpp index 7bfc8e39753..dadac918eaf 100644 --- a/search/search_integration_tests/processor_test.cpp +++ b/search/search_integration_tests/processor_test.cpp @@ -972,8 +972,36 @@ UNIT_CLASS_TEST(ProcessorTest, TestCategorialSearch) } } -UNIT_CLASS_TEST(ProcessorTest, TestCoords) +UNIT_CLASS_TEST(ProcessorTest, SearchDebug) { + string const countryName = "Wonderland"; + + TestCity debugville(m2::PointD(0, 0), "Debugville", "en", 100 /* rank */); + + TestCafe cafe(m2::PointD(0.01, 0), "", "ru"); + + TestPOI hotel(m2::PointD(0, 0.01), "", "ru"); + hotel.SetTypes({{"tourism", "hotel"}}); + + auto const testWorldId = BuildWorld([&](TestMwmBuilder & builder) { + builder.Add(debugville); + }); + auto wonderlandId = BuildCountry(countryName, [&](TestMwmBuilder & builder) { + builder.Add(hotel); + builder.Add(cafe); + }); + + auto const ruleCity = ExactMatch(testWorldId, debugville); + auto const ruleCafe = ExactMatch(wonderlandId, cafe); + auto const ruleHotel = ExactMatch(wonderlandId, hotel); + + TEST(ResultsMatch("fid=0", {ruleCity, ruleCafe}), ()); + TEST(ResultsMatch("fid=1 ", {ruleHotel}), ()); +} + +UNIT_CLASS_TEST(ProcessorTest, SearchCoordinates) +{ + // vector> tests = { {"51.681644 39.183481", 51.681644, 39.183481},