diff --git a/search/CMakeLists.txt b/search/CMakeLists.txt index 4a258e8e2c..5c3cac49a5 100644 --- a/search/CMakeLists.txt +++ b/search/CMakeLists.txt @@ -24,6 +24,8 @@ set( engine.cpp engine.hpp everywhere_search_params.hpp + feature_loader.cpp + feature_loader.hpp feature_offset_match.hpp features_filter.cpp features_filter.hpp diff --git a/search/feature_loader.cpp b/search/feature_loader.cpp new file mode 100644 index 0000000000..32f939701e --- /dev/null +++ b/search/feature_loader.cpp @@ -0,0 +1,26 @@ +#include "search/feature_loader.hpp" + +#include "indexer/feature_decl.hpp" + +#include "base/stl_add.hpp" + +namespace search +{ +FeatureLoader::FeatureLoader(Index const & index) : m_index(index) {} + +bool FeatureLoader::Load(FeatureID const & id, FeatureType & ft) +{ + ASSERT(m_checker.CalledOnOriginalThread(), ()); + + auto const & mwmId = id.m_mwmId; + if (!m_guard || m_guard->GetId() != mwmId) + m_guard = my::make_unique(m_index, mwmId); + return m_guard->GetFeatureByIndex(id.m_index, ft); +} + +void FeatureLoader::Reset() +{ + ASSERT(m_checker.CalledOnOriginalThread(), ()); + m_guard.reset(); +} +} // namespace search diff --git a/search/feature_loader.hpp b/search/feature_loader.hpp new file mode 100644 index 0000000000..132c4c00df --- /dev/null +++ b/search/feature_loader.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "indexer/index.hpp" +#include "indexer/scales.hpp" + +#include "base/assert.hpp" +#include "base/macros.hpp" +#include "base/thread_checker.hpp" + +#include +#include + +class FeatureType; +struct FeatureID; + +namespace search +{ +class FeatureLoader +{ +public: + explicit FeatureLoader(Index const & index); + + WARN_UNUSED_RESULT bool Load(FeatureID const & id, FeatureType & ft); + + void Reset(); + + template + void ForEachInRect(m2::RectD const & rect, ToDo && toDo) + { + ASSERT(m_checker.CalledOnOriginalThread(), ()); + m_index.ForEachInRect(std::forward(toDo), rect, scales::GetUpperScale()); + } + +private: + Index const & m_index; + std::unique_ptr m_guard; + + ThreadChecker m_checker; +}; +} // namespace search diff --git a/search/house_detector.cpp b/search/house_detector.cpp index ddf83f9eee..768ee8077a 100644 --- a/search/house_detector.cpp +++ b/search/house_detector.cpp @@ -223,26 +223,6 @@ bool House::GetNearbyMatch(ParsedNumber const & number) const return m_number.IsIntersect(number, HN_NEARBY_DISTANCE); } -FeatureLoader::FeatureLoader(Index const & index) : m_index(index) {} - -void FeatureLoader::CreateLoader(MwmSet::MwmId const & mwmId) -{ - if (!m_guard || mwmId != m_guard->GetId()) - m_guard = make_unique(m_index, mwmId); -} - -bool FeatureLoader::Load(FeatureID const & id, FeatureType & f) -{ - CreateLoader(id.m_mwmId); - return m_guard->GetFeatureByIndex(id.m_index, f); -} - -template -void FeatureLoader::ForEachInRect(m2::RectD const & rect, ToDo toDo) -{ - m_index.ForEachInRect(toDo, rect, scales::GetUpperScale()); -} - m2::RectD Street::GetLimitRect(double offsetMeters) const { m2::RectD rect; @@ -481,7 +461,7 @@ int HouseDetector::LoadStreets(vector const & ids) } } - m_loader.Free(); + m_loader.Reset(); return count; } diff --git a/search/house_detector.hpp b/search/house_detector.hpp index d23a4b8264..5c4ece56d1 100644 --- a/search/house_detector.hpp +++ b/search/house_detector.hpp @@ -1,4 +1,6 @@ #pragma once + +#include "search/feature_loader.hpp" #include "search/projection_on_street.hpp" #include "indexer/feature_decl.hpp" @@ -12,26 +14,10 @@ #include "std/string.hpp" #include "std/queue.hpp" +class Index; namespace search { - -class FeatureLoader -{ - Index const & m_index; - unique_ptr m_guard; - - void CreateLoader(MwmSet::MwmId const & mwmId); - -public: - FeatureLoader(Index const & index); - - WARN_UNUSED_RESULT bool Load(FeatureID const & id, FeatureType & f); - inline void Free() { m_guard.reset(); } - - template void ForEachInRect(m2::RectD const & rect, ToDo toDo); -}; - struct ParsedNumber { string m_fullN; diff --git a/search/search.pro b/search/search.pro index 2c11aa354f..f5a98532fe 100644 --- a/search/search.pro +++ b/search/search.pro @@ -23,6 +23,7 @@ HEADERS += \ emitter.hpp \ engine.hpp \ everywhere_search_params.hpp \ + feature_loader.hpp \ feature_offset_match.hpp \ features_filter.hpp \ features_layer.hpp \ @@ -90,6 +91,7 @@ SOURCES += \ dummy_rank_table.cpp \ editor_delegate.cpp \ engine.cpp \ + feature_loader.cpp \ features_filter.cpp \ features_layer.cpp \ features_layer_matcher.cpp \ diff --git a/search/search_quality/assessment_tool/CMakeLists.txt b/search/search_quality/assessment_tool/CMakeLists.txt index dee27cb93c..31b3e3129d 100644 --- a/search/search_quality/assessment_tool/CMakeLists.txt +++ b/search/search_quality/assessment_tool/CMakeLists.txt @@ -19,7 +19,6 @@ set( languages_list.hpp main_model.cpp main_model.hpp - main_model.hpp main_view.cpp main_view.hpp model.hpp diff --git a/search/search_quality/assessment_tool/context.cpp b/search/search_quality/assessment_tool/context.cpp index 835cbbaf1f..d7cac1138d 100644 --- a/search/search_quality/assessment_tool/context.cpp +++ b/search/search_quality/assessment_tool/context.cpp @@ -1,5 +1,12 @@ #include "search/search_quality/assessment_tool/context.hpp" +#include "search/feature_loader.hpp" +#include "search/search_quality/matcher.hpp" + +#include "base/assert.hpp" + +using namespace std; + // Context ----------------------------------------------------------------------------------------- void Context::Clear() { @@ -8,6 +15,79 @@ void Context::Clear() m_initialized = false; } +search::Sample Context::MakeSample(search::FeatureLoader & loader) const +{ + search::Sample sample = m_sample; + + if (!m_initialized) + return sample; + + auto const & relevances = m_edits.GetRelevances(); + + auto & results = sample.m_results; + results.clear(); + + CHECK_EQUAL(m_goldenMatching.size(), m_sample.m_results.size(), ()); + CHECK_EQUAL(m_actualMatching.size(), relevances.size(), ()); + CHECK_EQUAL(m_actualMatching.size(), m_results.GetCount(), ()); + + // Iterates over original (loaded from the file with search samples) + // results first. + for (size_t i = 0; i < m_sample.m_results.size(); ++i) + { + auto const j = m_goldenMatching[i]; + + // Some results weren't matched, so they weren't displayed to the + // assessor. But we want to keep them. + if (j == search::Matcher::kInvalidId) + { + results.push_back(m_sample.m_results[i]); + continue; + } + + // No need to keep irrelevant results. + if (relevances[j] == search::Sample::Result::Relevance::Irrelevant) + continue; + + auto result = m_sample.m_results[i]; + result.m_relevance = relevances[j]; + results.push_back(result); + } + + // Iterates over results retrieved during assessment. + for (size_t i = 0; i < m_results.GetCount(); ++i) + { + auto const j = m_actualMatching[i]; + if (j != search::Matcher::kInvalidId) + { + // This result was processed by the loop above. + continue; + } + + // No need to keep irrelevant results. + if (relevances[i] == search::Sample::Result::Relevance::Irrelevant) + continue; + + auto const & result = m_results.GetResult(i); + // No need in non-feature results. + if (result.GetResultType() != search::Result::RESULT_FEATURE) + continue; + + FeatureType ft; + CHECK(loader.Load(result.GetFeatureID(), ft), ()); + results.push_back(search::Sample::Result::Build(ft, relevances[i])); + } + + return sample; +} + +void Context::ApplyEdits() +{ + if (!m_initialized) + return; + m_edits.ResetRelevances(m_edits.GetRelevances()); +} + // ContextList ------------------------------------------------------------------------------------- ContextList::ContextList(OnUpdate onUpdate): m_onUpdate(onUpdate) {} @@ -21,14 +101,28 @@ void ContextList::Resize(size_t size) m_hasChanges.resize(size); for (size_t i = oldSize; i < size; ++i) { - m_contexts.emplace_back([this, i]() { + m_contexts.emplace_back([this, i](Edits::Update const & update) { if (!m_hasChanges[i] && m_contexts[i].HasChanges()) ++m_numChanges; if (m_hasChanges[i] && !m_contexts[i].HasChanges()) --m_numChanges; m_hasChanges[i] = m_contexts[i].HasChanges(); if (m_onUpdate) - m_onUpdate(i); + m_onUpdate(i, update); }); } } + +vector ContextList::MakeSamples(search::FeatureLoader & loader) const +{ + vector samples; + for (auto const & context : m_contexts) + samples.push_back(context.MakeSample(loader)); + return samples; +} + +void ContextList::ApplyEdits() +{ + for (auto & context : m_contexts) + context.ApplyEdits(); +} diff --git a/search/search_quality/assessment_tool/context.hpp b/search/search_quality/assessment_tool/context.hpp index 3ea199cbe1..cfbcf744cf 100644 --- a/search/search_quality/assessment_tool/context.hpp +++ b/search/search_quality/assessment_tool/context.hpp @@ -11,6 +11,11 @@ #include #include +namespace search +{ +class FeatureLoader; +} + struct Context { explicit Context(Edits::OnUpdate onUpdate) : m_edits(onUpdate) {} @@ -18,9 +23,19 @@ struct Context bool HasChanges() const { return m_initialized && m_edits.HasChanges(); } void Clear(); + // Makes sample in accordance with uncommited edits. + search::Sample MakeSample(search::FeatureLoader & loader) const; + + // Commits all edits. + void ApplyEdits(); + search::Sample m_sample; search::Results m_results; Edits m_edits; + + std::vector m_goldenMatching; + std::vector m_actualMatching; + bool m_initialized = false; }; @@ -48,7 +63,7 @@ public: ContextList const * m_contexts = nullptr; }; - using OnUpdate = std::function; + using OnUpdate = std::function; explicit ContextList(OnUpdate onUpdate); @@ -60,6 +75,12 @@ public: bool HasChanges() const { return m_numChanges != 0; } + // Generates search samples in accordance with uncommited edits. + std::vector MakeSamples(search::FeatureLoader & loader) const; + + // Commits all edits. + void ApplyEdits(); + private: std::vector m_contexts; std::vector m_hasChanges; diff --git a/search/search_quality/assessment_tool/edits.cpp b/search/search_quality/assessment_tool/edits.cpp index a8302ad9f1..522d858efc 100644 --- a/search/search_quality/assessment_tool/edits.cpp +++ b/search/search_quality/assessment_tool/edits.cpp @@ -20,10 +20,15 @@ Edits::Relevance Edits::RelevanceEditor::Get() const return relevances[m_index]; } +bool Edits::RelevanceEditor::HasChanges() const +{ + return m_parent.HasChanges(m_index); +} + // Edits ------------------------------------------------------------------------------------------- void Edits::ResetRelevances(std::vector const & relevances) { - WithObserver([this, &relevances]() { + WithObserver(Update::AllRelevancesUpdate(), [this, &relevances]() { m_origRelevances = relevances; m_currRelevances = relevances; m_numEdits = 0; @@ -32,7 +37,7 @@ void Edits::ResetRelevances(std::vector const & relevances) bool Edits::SetRelevance(size_t index, Relevance relevance) { - return WithObserver([this, index, relevance]() { + return WithObserver(Update::SingleRelevanceUpdate(index), [this, index, relevance]() { CHECK_LESS(index, m_currRelevances.size(), ()); CHECK_EQUAL(m_currRelevances.size(), m_origRelevances.size(), ()); @@ -53,7 +58,7 @@ bool Edits::SetRelevance(size_t index, Relevance relevance) void Edits::Clear() { - WithObserver([this]() { + WithObserver(Update::AllRelevancesUpdate(), [this]() { m_origRelevances.clear(); m_currRelevances.clear(); m_numEdits = 0; @@ -61,3 +66,9 @@ void Edits::Clear() } bool Edits::HasChanges() const { return m_numEdits != 0; } + +bool Edits::HasChanges(size_t index) const +{ + CHECK_LESS(index, m_currRelevances.size(), ()); + return m_currRelevances[index] != m_origRelevances[index]; +} diff --git a/search/search_quality/assessment_tool/edits.hpp b/search/search_quality/assessment_tool/edits.hpp index d81df4d387..1c04b92dda 100644 --- a/search/search_quality/assessment_tool/edits.hpp +++ b/search/search_quality/assessment_tool/edits.hpp @@ -6,13 +6,38 @@ #include #include +#include #include #include class Edits { public: - using OnUpdate = std::function; + struct Update + { + static auto constexpr kInvalidIndex = std::numeric_limits::max(); + + enum class Type + { + SingleRelevance, + AllRelevances + }; + + static Update AllRelevancesUpdate() { return Update{}; } + + static Update SingleRelevanceUpdate(size_t index) + { + Update result; + result.m_index = index; + result.m_type = Type::SingleRelevance; + return result; + } + + size_t m_index = kInvalidIndex; + Type m_type = Type::AllRelevances; + }; + + using OnUpdate = std::function; using Relevance = search::Sample::Result::Relevance; class RelevanceEditor @@ -24,6 +49,7 @@ public: // differs from the original one. bool Set(Relevance relevance); Relevance Get() const; + bool HasChanges() const; private: Edits & m_parent; @@ -38,18 +64,19 @@ public: // |relevance| differs from the original one. bool SetRelevance(size_t index, Relevance relevance); - std::vector const & GetRelevances() { return m_currRelevances; } + std::vector const & GetRelevances() const { return m_currRelevances; } void Clear(); bool HasChanges() const; + bool HasChanges(size_t index) const; private: template - typename std::result_of::type WithObserver(Fn && fn) + typename std::result_of::type WithObserver(Update const & update, Fn && fn) { - MY_SCOPE_GUARD(cleanup, [this]() { + MY_SCOPE_GUARD(cleanup, [&]() { if (m_onUpdate) - m_onUpdate(); + m_onUpdate(update); }); return fn(); } diff --git a/search/search_quality/assessment_tool/main_model.cpp b/search/search_quality/assessment_tool/main_model.cpp index 3b46364959..64655241ff 100644 --- a/search/search_quality/assessment_tool/main_model.cpp +++ b/search/search_quality/assessment_tool/main_model.cpp @@ -1,5 +1,6 @@ #include "search/search_quality/assessment_tool/main_model.hpp" +#include "search/feature_loader.hpp" #include "search/search_quality/assessment_tool/view.hpp" #include "search/search_quality/matcher.hpp" @@ -18,9 +19,11 @@ #include #include +#include #include using Relevance = search::Sample::Result::Relevance; +using namespace std; // MainModel::SampleContext ------------------------------------------------------------------------ @@ -28,24 +31,27 @@ using Relevance = search::Sample::Result::Relevance; MainModel::MainModel(Framework & framework) : m_framework(framework) , m_index(m_framework.GetIndex()) - , m_contexts([this](size_t index) { OnUpdate(index); }) + , m_contexts([this](size_t index, Edits::Update const & update) { OnUpdate(index, update); }) { } -void MainModel::Open(std::string const & path) +void MainModel::Open(string const & path) { CHECK(m_view, ()); - std::ifstream ifs(path); - if (!ifs) + string contents; + { - m_view->ShowError("Can't open file: " + path); - return; + ifstream ifs(path); + if (!ifs) + { + m_view->ShowError("Can't open file: " + path); + return; + } + contents.assign(istreambuf_iterator(ifs), istreambuf_iterator()); } - std::string const contents((std::istreambuf_iterator(ifs)), - (std::istreambuf_iterator())); - std::vector samples; + vector samples; if (!search::Sample::DeserializeFromJSON(contents, samples)) { m_view->ShowError("Can't parse samples: " + path); @@ -61,10 +67,41 @@ void MainModel::Open(std::string const & path) context.Clear(); context.m_sample = samples[i]; } + m_path = path; m_view->SetSamples(ContextList::SamplesSlice(m_contexts)); } +void MainModel::Save() +{ + CHECK(HasChanges(), ()); + SaveAs(m_path); +} + +void MainModel::SaveAs(string const & path) +{ + CHECK(HasChanges(), ()); + CHECK(!path.empty(), ()); + + search::FeatureLoader loader(m_index); + + string contents; + search::Sample::SerializeToJSON(m_contexts.MakeSamples(loader), contents); + + { + ofstream ofs(path); + if (!ofs) + { + m_view->ShowError("Can't open file: " + path); + return; + } + copy(contents.begin(), contents.end(), ostreambuf_iterator(ofs)); + } + + m_contexts.ApplyEdits(); + m_path = path; +} + void MainModel::OnSampleSelected(int index) { CHECK(m_threadChecker.CalledOnOriginalThread(), ()); @@ -83,7 +120,8 @@ void MainModel::OnSampleSelected(int index) if (context.m_initialized) { - OnResults(timestamp, index, context.m_results, context.m_edits.GetRelevances()); + OnResults(timestamp, index, context.m_results, context.m_edits.GetRelevances(), + context.m_goldenMatching, context.m_actualMatching); return; } @@ -98,19 +136,17 @@ void MainModel::OnSampleSelected(int index) params.SetPosition(latLon.lat, latLon.lon); params.m_onResults = [this, index, sample, timestamp](search::Results const & results) { - std::vector relevances; + vector relevances; + vector goldenMatching; + vector actualMatching; if (results.IsEndedNormal()) { - search::Matcher matcher(m_index); - - std::vector const actual(results.begin(), results.end()); - std::vector goldenMatching; - { - std::vector actualMatching; - matcher.Match(sample.m_results, actual, goldenMatching, actualMatching); - } + search::FeatureLoader loader(m_index); + search::Matcher matcher(loader); + vector const actual(results.begin(), results.end()); + matcher.Match(sample.m_results, actual, goldenMatching, actualMatching); relevances.assign(actual.size(), Relevance::Irrelevant); for (size_t i = 0; i < goldenMatching.size(); ++i) { @@ -119,25 +155,27 @@ void MainModel::OnSampleSelected(int index) } } - GetPlatform().RunOnGuiThread([this, timestamp, index, results, relevances]() { - OnResults(timestamp, index, results, relevances); - }); + GetPlatform().RunOnGuiThread(bind(&MainModel::OnResults, this, timestamp, index, results, + relevances, goldenMatching, actualMatching)); }; m_queryHandle = engine.Search(params, sample.m_viewport); } } -void MainModel::OnUpdate(size_t index) +bool MainModel::HasChanges() { return m_contexts.HasChanges(); } + +void MainModel::OnUpdate(size_t index, Edits::Update const & update) { CHECK_LESS(index, m_contexts.Size(), ()); auto & context = m_contexts[index]; - m_view->OnSampleChanged(index, context.HasChanges()); + m_view->OnSampleChanged(index, update, context.HasChanges()); m_view->OnSamplesChanged(m_contexts.HasChanges()); } void MainModel::OnResults(uint64_t timestamp, size_t index, search::Results const & results, - std::vector const & relevances) + vector const & relevances, vector goldenMatching, + vector actualMatching) { CHECK(m_threadChecker.CalledOnOriginalThread(), ()); @@ -156,9 +194,11 @@ void MainModel::OnResults(uint64_t timestamp, size_t index, search::Results cons { context.m_results = results; context.m_edits.ResetRelevances(relevances); + context.m_goldenMatching = goldenMatching; + context.m_actualMatching = actualMatching; context.m_initialized = true; } - m_view->OnSampleChanged(index, context.HasChanges()); + m_view->OnSampleChanged(index, Edits::Update::AllRelevancesUpdate(), context.HasChanges()); m_view->EnableSampleEditing(index, context.m_edits); } diff --git a/search/search_quality/assessment_tool/main_model.hpp b/search/search_quality/assessment_tool/main_model.hpp index 27d7944395..39f05f4272 100644 --- a/search/search_quality/assessment_tool/main_model.hpp +++ b/search/search_quality/assessment_tool/main_model.hpp @@ -27,20 +27,29 @@ public: // Model overrides: void Open(std::string const & path) override; + void Save() override; + void SaveAs(std::string const & path) override; + void OnSampleSelected(int index) override; + bool HasChanges() override; private: - void OnUpdate(size_t index); + void OnUpdate(size_t index, Edits::Update const & update); void OnResults(uint64_t timestamp, size_t index, search::Results const & results, - std::vector const & relevances); + std::vector const & relevances, + std::vector goldenMatching, std::vector actualMatching); void ResetSearch(); Framework & m_framework; Index const & m_index; + ContextList m_contexts; + // Path to the last file search samples were loaded from or saved to. + std::string m_path; + std::weak_ptr m_queryHandle; uint64_t m_queryTimestamp = 0; size_t m_numShownResults = 0; diff --git a/search/search_quality/assessment_tool/main_view.cpp b/search/search_quality/assessment_tool/main_view.cpp index 1470928199..7e2c75824d 100644 --- a/search/search_quality/assessment_tool/main_view.cpp +++ b/search/search_quality/assessment_tool/main_view.cpp @@ -25,6 +25,11 @@ using Relevance = search::Sample::Result::Relevance; +namespace +{ +char const kJSON[] = "JSON files (*.json)"; +} // namespace + MainView::MainView(Framework & framework) : m_framework(framework) { QDesktopWidget const * desktop = QApplication::desktop(); @@ -48,6 +53,7 @@ MainView::~MainView() void MainView::SetSamples(ContextList::SamplesSlice const & samples) { m_samplesView->SetSamples(samples); + m_sampleView->Clear(); } void MainView::ShowSample(size_t index, search::Sample const & sample, bool hasEdits) @@ -57,7 +63,7 @@ void MainView::ShowSample(size_t index, search::Sample const & sample, bool hasE m_sampleView->SetContents(sample); m_sampleView->show(); - OnSampleChanged(index, hasEdits); + OnSampleChanged(index, Edits::Update::AllRelevancesUpdate(), hasEdits); } void MainView::ShowResults(search::Results::Iter begin, search::Results::Iter end) @@ -65,15 +71,16 @@ void MainView::ShowResults(search::Results::Iter begin, search::Results::Iter en m_sampleView->ShowResults(begin, end); } -void MainView::OnSampleChanged(size_t index, bool hasEdits) +void MainView::OnSampleChanged(size_t index, Edits::Update const & update, bool hasEdits) { - if (m_samplesView->IsSelected(index)) - { - if (hasEdits) - m_sampleDock->setWindowTitle(tr("Sample *")); - else - m_sampleDock->setWindowTitle(tr("Sample")); - } + m_samplesView->OnUpdate(index); + if (!m_samplesView->IsSelected(index)) + return; + if (hasEdits) + m_sampleDock->setWindowTitle(tr("Sample *")); + else + m_sampleDock->setWindowTitle(tr("Sample")); + m_sampleView->Update(update); } void MainView::OnSamplesChanged(bool hasEdits) @@ -82,6 +89,8 @@ void MainView::OnSamplesChanged(bool hasEdits) m_samplesDock->setWindowTitle("Samples *"); else m_samplesDock->setWindowTitle("Samples"); + m_save->setEnabled(hasEdits); + m_saveAs->setEnabled(hasEdits); } void MainView::EnableSampleEditing(size_t index, Edits & edits) @@ -113,13 +122,31 @@ void MainView::InitMenuBar() auto * fileMenu = bar->addMenu(tr("&File")); { - auto * open = new QAction(tr("&Open queries..."), this /* parent */); + auto * open = new QAction(tr("&Open samples..."), this /* parent */); open->setShortcuts(QKeySequence::Open); - open->setStatusTip(tr("Open the file with queries for assessment")); + open->setStatusTip(tr("Open the file with samples for assessment")); connect(open, &QAction::triggered, this, &MainView::Open); fileMenu->addAction(open); } + { + m_save = new QAction(tr("Save samples"), this /* parent */); + m_save->setShortcuts(QKeySequence::Save); + m_save->setStatusTip(tr("Save the file with assessed samples")); + m_save->setEnabled(false); + connect(m_save, &QAction::triggered, this, &MainView::Save); + fileMenu->addAction(m_save); + } + + { + m_saveAs = new QAction(tr("Save samples as..."), this /* parent */); + m_saveAs->setShortcuts(QKeySequence::SaveAs); + m_saveAs->setStatusTip(tr("Save the file with assessed samples")); + m_saveAs->setEnabled(false); + connect(m_saveAs, &QAction::triggered, this, &MainView::SaveAs); + fileMenu->addAction(m_saveAs); + } + fileMenu->addSeparator(); { @@ -185,8 +212,8 @@ void MainView::Open() { CHECK(m_model, ()); - auto const file = QFileDialog::getOpenFileName(this, tr("Open samples..."), QString(), - tr("JSON files (*.json)")) + auto const file = QFileDialog::getOpenFileName(this /* parent */, tr("Open samples..."), + QString() /* dir */, tr(kJSON)) .toStdString(); if (file.empty()) return; @@ -194,6 +221,17 @@ void MainView::Open() m_model->Open(file); } +void MainView::Save() { m_model->Save(); } + +void MainView::SaveAs() +{ + auto const file = QFileDialog::getSaveFileName(this /* parent */, tr("Save samples as..."), + QString() /* dir */, tr(kJSON)) + .toStdString(); + if (!file.empty()) + m_model->SaveAs(file); +} + QDockWidget * MainView::CreateDock(std::string const & title, QWidget & widget) { auto * dock = new QDockWidget(ToQString(title), this /* parent */, Qt::Widget); diff --git a/search/search_quality/assessment_tool/main_view.hpp b/search/search_quality/assessment_tool/main_view.hpp index a85d16a65d..f98c2baf7e 100644 --- a/search/search_quality/assessment_tool/main_view.hpp +++ b/search/search_quality/assessment_tool/main_view.hpp @@ -31,7 +31,7 @@ public: void ShowSample(size_t index, search::Sample const & sample, bool hasEdits) override; void ShowResults(search::Results::Iter begin, search::Results::Iter end) override; - void OnSampleChanged(size_t index, bool hasEdits) override; + void OnSampleChanged(size_t index, Edits::Update const & update, bool hasEdits) override; void EnableSampleEditing(size_t index, Edits & edits) override; void OnSamplesChanged(bool hasEdits) override; @@ -46,6 +46,8 @@ private: void InitMenuBar(); void Open(); + void Save(); + void SaveAs(); QDockWidget * CreateDock(std::string const & title, QWidget & widget); @@ -56,4 +58,7 @@ private: SampleView * m_sampleView = nullptr; QDockWidget * m_sampleDock = nullptr; + + QAction * m_save = nullptr; + QAction * m_saveAs = nullptr; }; diff --git a/search/search_quality/assessment_tool/model.hpp b/search/search_quality/assessment_tool/model.hpp index f36f09663e..4ee2c68efd 100644 --- a/search/search_quality/assessment_tool/model.hpp +++ b/search/search_quality/assessment_tool/model.hpp @@ -12,7 +12,11 @@ public: void SetView(View & view) { m_view = &view; } virtual void Open(std::string const & path) = 0; + virtual void Save() = 0; + virtual void SaveAs(std::string const & path) = 0; + virtual void OnSampleSelected(int index) = 0; + virtual bool HasChanges() = 0; protected: View * m_view = nullptr; diff --git a/search/search_quality/assessment_tool/result_view.cpp b/search/search_quality/assessment_tool/result_view.cpp index faeb8e3da1..d672aa6743 100644 --- a/search/search_quality/assessment_tool/result_view.cpp +++ b/search/search_quality/assessment_tool/result_view.cpp @@ -61,6 +61,14 @@ void ResultView::EnableEditing(Edits::RelevanceEditor && editor) setEnabled(true); } +void ResultView::Update() +{ + if (m_editor && m_editor->HasChanges()) + setStyleSheet("#result {background: rgba(255, 255, 200, 50%)}"); + else + setStyleSheet(""); +} + void ResultView::Init() { auto * layout = new QVBoxLayout(this /* parent */); @@ -124,9 +132,5 @@ void ResultView::OnRelevanceChanged() else if (m_vital->isChecked()) relevance = Relevance::Vital; - bool changed = m_editor->Set(relevance); - if (changed) - setStyleSheet("#result {background: rgba(255, 255, 200, 50%)}"); - else - setStyleSheet(""); + m_editor->Set(relevance); } diff --git a/search/search_quality/assessment_tool/result_view.hpp b/search/search_quality/assessment_tool/result_view.hpp index 1201928b9b..4df28b80ed 100644 --- a/search/search_quality/assessment_tool/result_view.hpp +++ b/search/search_quality/assessment_tool/result_view.hpp @@ -24,6 +24,8 @@ public: void EnableEditing(Edits::RelevanceEditor && editor); + void Update(); + private: void Init(); void SetContents(search::Result const & result); diff --git a/search/search_quality/assessment_tool/results_view.cpp b/search/search_quality/assessment_tool/results_view.cpp index 64c6edbc64..e535ca5d81 100644 --- a/search/search_quality/assessment_tool/results_view.cpp +++ b/search/search_quality/assessment_tool/results_view.cpp @@ -32,6 +32,20 @@ ResultView const & ResultsView::Get(size_t i) const return *m_results[i]; } +void ResultsView::Update(Edits::Update const & update) +{ + switch (update.m_type) + { + case Edits::Update::Type::SingleRelevance: + m_results[update.m_index]->Update(); + break; + case Edits::Update::Type::AllRelevances: + for (auto * result : m_results) + result->Update(); + break; + } +} + void ResultsView::Clear() { m_results.clear(); diff --git a/search/search_quality/assessment_tool/results_view.hpp b/search/search_quality/assessment_tool/results_view.hpp index 633d1e11da..14ef24ebc0 100644 --- a/search/search_quality/assessment_tool/results_view.hpp +++ b/search/search_quality/assessment_tool/results_view.hpp @@ -1,5 +1,7 @@ #pragma once +#include "search/search_quality/assessment_tool/edits.hpp" + #include #include @@ -22,6 +24,7 @@ public: ResultView & Get(size_t i); ResultView const & Get(size_t i) const; + void Update(Edits::Update const & update); size_t Size() const { return m_results.size(); } diff --git a/search/search_quality/assessment_tool/sample_view.cpp b/search/search_quality/assessment_tool/sample_view.cpp index f390302c98..d48a87a338 100644 --- a/search/search_quality/assessment_tool/sample_view.cpp +++ b/search/search_quality/assessment_tool/sample_view.cpp @@ -59,6 +59,8 @@ SampleView::SampleView(QWidget * parent) : QWidget(parent) } setLayout(mainLayout); + + Clear(); } void SampleView::SetContents(search::Sample const & sample) @@ -86,3 +88,13 @@ void SampleView::EnableEditing(Edits & edits) for (size_t i = 0; i < numRelevances; ++i) m_results->Get(i).EnableEditing(Edits::RelevanceEditor(*m_edits, i)); } + +void SampleView::Update(Edits::Update const & update) { m_results->Update(update); } + +void SampleView::Clear() +{ + m_query->setText(QString()); + m_langs->Select("default"); + m_results->Clear(); + m_edits = nullptr; +} diff --git a/search/search_quality/assessment_tool/sample_view.hpp b/search/search_quality/assessment_tool/sample_view.hpp index 65cf06fea1..3f67bebb85 100644 --- a/search/search_quality/assessment_tool/sample_view.hpp +++ b/search/search_quality/assessment_tool/sample_view.hpp @@ -24,6 +24,9 @@ public: void EnableEditing(Edits & edits); + void Update(Edits::Update const & update); + void Clear(); + private: QLineEdit * m_query = nullptr; LanguagesList * m_langs = nullptr; diff --git a/search/search_quality/assessment_tool/samples_view.cpp b/search/search_quality/assessment_tool/samples_view.cpp index e6a8284f30..f35a9b8d3d 100644 --- a/search/search_quality/assessment_tool/samples_view.cpp +++ b/search/search_quality/assessment_tool/samples_view.cpp @@ -3,7 +3,6 @@ #include "search/search_quality/assessment_tool/helpers.hpp" #include "base/assert.hpp" -#include "base/checked_cast.hpp" #include #include @@ -47,5 +46,5 @@ SamplesView::SamplesView(QWidget * parent) : QTableView(parent) bool SamplesView::IsSelected(size_t index) const { - return selectionModel()->isRowSelected(index, QModelIndex()); + return selectionModel()->isRowSelected(base::checked_cast(index), QModelIndex()); } diff --git a/search/search_quality/assessment_tool/samples_view.hpp b/search/search_quality/assessment_tool/samples_view.hpp index 007b595eb1..502c2fb2db 100644 --- a/search/search_quality/assessment_tool/samples_view.hpp +++ b/search/search_quality/assessment_tool/samples_view.hpp @@ -2,6 +2,8 @@ #include "search/search_quality/assessment_tool/context.hpp" +#include "base/checked_cast.hpp" + #include #include @@ -15,6 +17,7 @@ public: void SetSamples(ContextList::SamplesSlice const & samples) { m_model->SetSamples(samples); } bool IsSelected(size_t index) const; + void OnUpdate(size_t index) { m_model->OnUpdate(index); } private: class Model : public QStandardItemModel @@ -27,7 +30,15 @@ private: removeRows(0, rowCount()); m_samples = samples; if (m_samples.IsValid()) - insertRows(0, m_samples.Size()); + insertRows(0, base::checked_cast(m_samples.Size())); + } + + void OnUpdate(size_t index) + { + auto const ix = createIndex(base::checked_cast(index) /* row */, 0 /* column */); + + // Need to refresh view when some item is updated. + emit dataChanged(ix, ix); } // QStandardItemModel overrides: diff --git a/search/search_quality/assessment_tool/view.hpp b/search/search_quality/assessment_tool/view.hpp index 736553f8f5..a8797c3a59 100644 --- a/search/search_quality/assessment_tool/view.hpp +++ b/search/search_quality/assessment_tool/view.hpp @@ -2,6 +2,7 @@ #include "search/result.hpp" #include "search/search_quality/assessment_tool/context.hpp" +#include "search/search_quality/assessment_tool/edits.hpp" #include #include @@ -21,7 +22,7 @@ public: virtual void ShowSample(size_t index, search::Sample const & sample, bool hasEdits) = 0; virtual void ShowResults(search::Results::Iter begin, search::Results::Iter end) = 0; - virtual void OnSampleChanged(size_t index, bool hasEdits) = 0; + virtual void OnSampleChanged(size_t index, Edits::Update const & update, bool hasEdits) = 0; virtual void EnableSampleEditing(size_t index, Edits & edits) = 0; virtual void OnSamplesChanged(bool hasEdits) = 0; diff --git a/search/search_quality/features_collector_tool/features_collector_tool.cpp b/search/search_quality/features_collector_tool/features_collector_tool.cpp index aeaf39063f..598f11c913 100644 --- a/search/search_quality/features_collector_tool/features_collector_tool.cpp +++ b/search/search_quality/features_collector_tool/features_collector_tool.cpp @@ -1,3 +1,4 @@ +#include "search/feature_loader.hpp" #include "search/ranking_info.hpp" #include "search/result.hpp" #include "search/search_quality/helpers.hpp" @@ -162,7 +163,8 @@ int main(int argc, char * argv[]) } vector stats(samples.size()); - Matcher matcher(engine); + FeatureLoader loader(engine); + Matcher matcher(loader); cout << "SampleId,"; RankingInfo::PrintCSVHeader(cout); diff --git a/search/search_quality/matcher.cpp b/search/search_quality/matcher.cpp index d0c831f4c3..12988ae066 100644 --- a/search/search_quality/matcher.cpp +++ b/search/search_quality/matcher.cpp @@ -1,8 +1,9 @@ #include "search/search_quality/matcher.hpp" +#include "search/feature_loader.hpp" + #include "indexer/feature.hpp" #include "indexer/feature_algo.hpp" -#include "indexer/feature_decl.hpp" #include "base/string_utils.hpp" @@ -15,7 +16,7 @@ namespace search // static size_t constexpr Matcher::kInvalidId; -Matcher::Matcher(Index const & index) : m_index(index) {} +Matcher::Matcher(FeatureLoader & loader) : m_loader(loader) {} void Matcher::Match(std::vector const & golden, std::vector const & actual, std::vector & goldenMatching, std::vector & actualMatching) @@ -49,14 +50,6 @@ void Matcher::Match(std::vector const & golden, std::vectorGetId() != mwmId) - m_guard = my::make_unique(m_index, mwmId); - return m_guard->GetFeatureByIndex(id.m_index, ft); -} - bool Matcher::Matches(Sample::Result const & golden, search::Result const & actual) { static double constexpr kToleranceMeters = 50; @@ -65,7 +58,7 @@ bool Matcher::Matches(Sample::Result const & golden, search::Result const & actu return false; FeatureType ft; - if (!GetFeature(actual.GetFeatureID(), ft)) + if (!m_loader.Load(actual.GetFeatureID(), ft)) return false; auto const houseNumber = ft.GetHouseNumber(); diff --git a/search/search_quality/matcher.hpp b/search/search_quality/matcher.hpp index 90de3a4904..73362486c5 100644 --- a/search/search_quality/matcher.hpp +++ b/search/search_quality/matcher.hpp @@ -3,36 +3,27 @@ #include "search/result.hpp" #include "search/search_quality/sample.hpp" -#include "indexer/index.hpp" - -#include "base/macros.hpp" - #include #include -#include #include -class FeatureType; -struct FeatureID; - namespace search { +class FeatureLoader; + class Matcher { public: static size_t constexpr kInvalidId = std::numeric_limits::max(); - Matcher(Index const & index); + explicit Matcher(FeatureLoader & loader); void Match(std::vector const & golden, std::vector const & actual, std::vector & goldenMatching, std::vector & actualMatching); private: - WARN_UNUSED_RESULT bool GetFeature(FeatureID const & id, FeatureType & ft); - bool Matches(Sample::Result const & golden, Result const & actual); - Index const & m_index; - std::unique_ptr m_guard; + FeatureLoader & m_loader; }; } // namespace search diff --git a/search/search_quality/sample.cpp b/search/search_quality/sample.cpp index c433933d21..15b1d78730 100644 --- a/search/search_quality/sample.cpp +++ b/search/search_quality/sample.cpp @@ -2,6 +2,10 @@ #include "search/search_quality/helpers.hpp" +#include "indexer/feature.hpp" +#include "indexer/feature_algo.hpp" +#include "indexer/feature_data.hpp" + #include "base/logging.hpp" #include "base/string_utils.hpp" @@ -51,6 +55,22 @@ struct FreeDeletor namespace search { +// static +Sample::Result Sample::Result::Build(FeatureType & ft, Relevance relevance) +{ + Sample::Result r; + r.m_pos = feature::GetCenter(ft); + { + string name; + ft.GetReadableName(name); + r.m_name = strings::MakeUniString(name); + } + r.m_houseNumber = ft.GetHouseNumber(); + r.m_types = feature::TypesHolder(ft).ToObjectNames(); + r.m_relevance = relevance; + return r; +} + bool Sample::Result::operator<(Sample::Result const & rhs) const { if (m_pos != rhs.m_pos) diff --git a/search/search_quality/sample.hpp b/search/search_quality/sample.hpp index 779d6376c0..e82f34f9f6 100644 --- a/search/search_quality/sample.hpp +++ b/search/search_quality/sample.hpp @@ -10,6 +10,8 @@ #include #include +class FeatureType; + namespace search { struct Sample @@ -23,6 +25,8 @@ struct Sample Vital }; + static Result Build(FeatureType & ft, Relevance relevance); + bool operator<(Result const & rhs) const; bool operator==(Result const & rhs) const;