From ea8d54e7162ef1b754cad13ea01a77b93af7f695 Mon Sep 17 00:00:00 2001 From: Arsentiy Milchakov Date: Tue, 28 Aug 2018 13:24:15 +0300 Subject: [PATCH] [eye] The eye is added. --- CMakeLists.txt | 1 + base/atomic_shared_ptr.hpp | 11 ++- coding/serdes_json.hpp | 120 +++++++++++++++++++++++++------ eye/CMakeLists.txt | 19 +++++ eye/eye.cpp | 115 ++++++++++++++++++++++++++++++ eye/eye.hpp | 42 +++++++++++ eye/eye_info.hpp | 141 +++++++++++++++++++++++++++++++++++++ eye/eye_serdes.cpp | 56 +++++++++++++++ eye/eye_serdes.hpp | 20 ++++++ eye/eye_storage.cpp | 56 +++++++++++++++ eye/eye_storage.hpp | 14 ++++ 11 files changed, 572 insertions(+), 23 deletions(-) create mode 100644 eye/CMakeLists.txt create mode 100644 eye/eye.cpp create mode 100644 eye/eye.hpp create mode 100644 eye/eye_info.hpp create mode 100644 eye/eye_serdes.cpp create mode 100644 eye/eye_serdes.hpp create mode 100644 eye/eye_storage.cpp create mode 100644 eye/eye_storage.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 351d31f4dc..81a66e89ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -271,6 +271,7 @@ add_subdirectory(coding) add_subdirectory(drape) add_subdirectory(drape_frontend) add_subdirectory(editor) +add_subdirectory(eye) add_subdirectory(generator/mwm_diff) add_subdirectory(geocoder) add_subdirectory(geometry) diff --git a/base/atomic_shared_ptr.hpp b/base/atomic_shared_ptr.hpp index 8a346c6401..a4e5b51fb9 100644 --- a/base/atomic_shared_ptr.hpp +++ b/base/atomic_shared_ptr.hpp @@ -11,12 +11,17 @@ template class AtomicSharedPtr final { public: + using ContentType = T const; + using ValueType = std::shared_ptr; + AtomicSharedPtr() = default; - void Set(std::shared_ptr value) noexcept { atomic_store(&m_wrapped, value); } - std::shared_ptr Get() const noexcept { return atomic_load(&m_wrapped); } + void Set(ValueType value) noexcept { atomic_store(&m_wrapped, value); } + ValueType Get() const noexcept { return atomic_load(&m_wrapped); } private: - std::shared_ptr m_wrapped = std::make_shared(); + ValueType m_wrapped = std::make_shared(); + + DISALLOW_COPY(AtomicSharedPtr); }; } // namespace base diff --git a/coding/serdes_json.hpp b/coding/serdes_json.hpp index 6739d74155..85041b79b0 100644 --- a/coding/serdes_json.hpp +++ b/coding/serdes_json.hpp @@ -6,14 +6,47 @@ #include "3party/jansson/myjansson.hpp" +#include #include #include #include #include +#include #include namespace coding { +namespace traits +{ +namespace impl +{ +template +auto is_iterable_checker(int) -> decltype( + std::begin(std::declval()), + std::end(std::declval()), + ++std::declval())) &>(), + std::true_type {}); + +template +std::false_type is_iterable_checker(...); +} // namespace impl + +template +using is_iterable = decltype(impl::is_iterable_checker(0)); +} // namespace traits + +template +using EnableIfIterable = std::enable_if_t::value>; + +template +using EnableIfNotIterable = std::enable_if_t::value>; + +template +using EnableIfEnum = std::enable_if_t::value>; + +template +using EnableIfNotEnum = std::enable_if_t::value>; + template class SerializerJson { @@ -52,30 +85,21 @@ public: void operator()(double const d, char const * name = nullptr) { ToJsonObjectOrValue(d, name); } void operator()(std::string const & s, char const * name = nullptr) { ToJsonObjectOrValue(s, name); } - template - void operator()(std::vector const & vs, char const * name = nullptr) - { - NewScopeWith(my::NewJSONArray(), name, [this, &vs] { - for (auto const & v : vs) - (*this)(v); - }); - } - - template - void operator()(std::unordered_set const & dest, char const * name = nullptr) - { - NewScopeWith(my::NewJSONArray(), name, [this, &dest] { - for (auto const & v : dest) - (*this)(v); - }); - } - - template + template * = nullptr, EnableIfNotEnum * = nullptr> void operator()(R const & r, char const * name = nullptr) { NewScopeWith(my::NewJSONObject(), name, [this, &r] { r.Visit(*this); }); } + template * = nullptr> + void operator()(T const & src, char const * name = nullptr) + { + NewScopeWith(my::NewJSONArray(), name, [this, &src] { + for (auto const & v : src) + (*this)(v); + }); + } + template void operator()(std::unique_ptr const & r, char const * name = nullptr) { @@ -94,6 +118,17 @@ public: }); } + void operator()(std::chrono::system_clock::time_point const & t, char const * name = nullptr) + { + (*this)(static_cast(t.time_since_epoch().count()), name); + } + + template * = nullptr> + void operator()(T const & t, char const * name = nullptr) + { + (*this)(static_cast>(t), name); + } + protected: template void NewScopeWith(my::JSONPtr json_object, char const * name, Fn && fn) @@ -205,7 +240,33 @@ public: RestoreContext(outerContext); } - template + template + void operator()(std::array & dst, char const * name = nullptr) + { + json_t * outerContext = SaveContext(name); + + if (!json_is_array(m_json)) + MYTHROW(my::Json::Exception, + ("The field", name, "must contain a json array.", json_dumps(m_json, 0))); + + if (N != json_array_size(m_json)) + { + MYTHROW(my::Json::Exception, ("The field", name, "must contain a json array of size", N, + "but size is", json_array_size(m_json))); + } + + for (size_t index = 0; index < N; ++index) + { + json_t * context = SaveContext(); + m_json = json_array_get(context, index); + (*this)(dst[index]); + RestoreContext(context); + } + + RestoreContext(outerContext); + } + + template * = nullptr> void operator()(R & r, char const * name = nullptr) { json_t * context = SaveContext(name); @@ -233,6 +294,25 @@ public: RestoreContext(context); } + void operator()(std::chrono::system_clock::time_point & dst, char const * name = nullptr) + { + uint64_t t = 0; + FromJSONObject(m_json, name, t); + + std::chrono::system_clock::time_point::duration d(t); + + dst = std::chrono::system_clock::time_point(d); + } + + template * = nullptr> + void operator()(T & t, char const * name = nullptr) + { + using UnderlyingType = std::underlying_type_t; + UnderlyingType res; + FromJSONObject(m_json, name, res); + t = static_cast(res); + } + protected: json_t * SaveContext(char const * name = nullptr) { diff --git a/eye/CMakeLists.txt b/eye/CMakeLists.txt new file mode 100644 index 0000000000..bd3101f95e --- /dev/null +++ b/eye/CMakeLists.txt @@ -0,0 +1,19 @@ +project(eye) + +include_directories(${OMIM_ROOT}/3party/jansson/src) + +set( + SRC + eye.cpp + eye.hpp + eye_info.hpp + eye_serdes.cpp + eye_serdes.hpp + eye_storage.cpp + eye_storage.hpp +) + +omim_add_library(${PROJECT_NAME} ${SRC}) + +omim_add_test_subdirectory(eye_tests) +omim_add_test_subdirectory(eye_tests_support) diff --git a/eye/eye.cpp b/eye/eye.cpp new file mode 100644 index 0000000000..b5d6cd1e10 --- /dev/null +++ b/eye/eye.cpp @@ -0,0 +1,115 @@ +#include "eye/eye.hpp" +#include "eye/eye_serdes.hpp" +#include "eye/eye_storage.hpp" + +#include "platform/platform.hpp" + +#include "coding/file_writer.hpp" + +#include "base/logging.hpp" + +#include + +using namespace eye; + +namespace +{ +void Load(Info & info) +{ + std::vector fileData; + Storage::Load(Storage::GetEyeFilePath(), fileData); + + if (fileData.empty()) + { + info = {}; + return; + } + + try + { + Serdes::Deserialize(fileData, info); + } + catch (Serdes::UnknownVersion const & ex) + { + LOG(LERROR, ("Unknown eye file version, eye will be disabled. Exception:", ex.Msg())); + } +} + +bool Save(Info const & info) +{ + std::vector fileData; + Serdes::Serialize(info, fileData); + return Storage::Save(Storage::GetEyeFilePath(), fileData); +} +} // namespace + +namespace eye +{ +Eye::Eye() +{ + Info info; + Load(info); + m_info.Set(std::make_shared(info)); +} + +// static +Eye & Eye::Instance() +{ + static Eye instance; + return instance; +} + +Eye::InfoType Eye::GetInfo() const +{ + return m_info.Get(); +} + +void Eye::Save(InfoType const & info) +{ + if (::Save(*info)) + m_info.Set(info); +} + +void Eye::AppendTip(Tips::Type type, Tips::Event event) +{ + auto const info = m_info.Get(); + auto editableInfo = make_shared(*info); + auto & editableTips = editableInfo->m_tips; + + auto & shownTips = editableTips.m_shownTips; + + ++(editableTips.m_totalShownTipsCount); + editableTips.m_lastShown = Clock::now(); + + auto it = std::find_if(shownTips.begin(), shownTips.end(), [type](Tips::Info const & tipsInfo) + { + return tipsInfo.m_type == type; + }); + + if (it != shownTips.cend()) + { + it->m_eventCounters.Increment(event); + it->m_lastShown = editableTips.m_lastShown; + } + else + { + Tips::Info tipInfo; + tipInfo.m_type = type; + tipInfo.m_eventCounters.Increment(event); + tipInfo.m_lastShown = editableTips.m_lastShown; + shownTips.emplace_back(std::move(tipInfo)); + } + + Save(editableInfo); +} + +// Eye::Event methods ------------------------------------------------------------------------------ +// static +void Eye::Event::TipShown(Tips::Type type, Tips::Event event) +{ + Platform().RunTask(Platform::Thread::File, [type, event] + { + Instance().AppendTip(type, event); + }); +} +} // namespace eye diff --git a/eye/eye.hpp b/eye/eye.hpp new file mode 100644 index 0000000000..c9330ba7e9 --- /dev/null +++ b/eye/eye.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include "eye/eye_info.hpp" + +#include "base/atomic_shared_ptr.hpp" +#include "base/macros.hpp" + +namespace eye +{ +// Note This class IS thread-safe. +// All write operations are asynchronous and work on Platform::Thread::File thread. +// Read operations are synchronous and return shared pointer with constant copy of internal +// container. +class Eye +{ +public: + friend class EyeForTesting; + using InfoType = base::AtomicSharedPtr::ValueType; + + class Event + { + public: + static void TipShown(Tips::Type type, Tips::Event event); + }; + + static Eye & Instance(); + + InfoType GetInfo() const; + +private: + Eye(); + + void Save(InfoType const & info); + + // Event processing: + void AppendTip(Tips::Type type, Tips::Event event); + + base::AtomicSharedPtr m_info; + + DISALLOW_COPY_AND_MOVE(Eye); +}; +} // namespace eye diff --git a/eye/eye_info.hpp b/eye/eye_info.hpp new file mode 100644 index 0000000000..fd51afd163 --- /dev/null +++ b/eye/eye_info.hpp @@ -0,0 +1,141 @@ +#pragma once + +#include "storage/index.hpp" + +#include "geometry/point2d.hpp" + +#include "base/visitor.hpp" + +#include +#include +#include +#include + +namespace eye +{ +namespace traits +{ +namespace impl +{ +template +auto is_enum_with_count_checker(int) -> decltype( + T::Count, + std::is_enum {}); + +template +std::false_type is_enum_with_count_checker(...); +} // namespace impl + +template +using is_enum_with_count = decltype(impl::is_enum_with_count_checker(0)); +} // namespace traits + +template +using EnableIfIsEnumWithCount = std::enable_if_t::value>; + +template +class Counters; + +template +class Counters> +{ +public: + void Increment(T const key) + { + ++m_counters[static_cast(key)]; + } + + R Get(T const key) const + { + return m_counters[static_cast(key)]; + } + + DECLARE_VISITOR_AND_DEBUG_PRINT(Counters, visitor(m_counters, "counters")) + +private: + std::array(T::Count)> m_counters = {}; +}; + +enum class Version : int8_t +{ + Unknown = -1, + V0 = 0, + Latest = V0 +}; + +using Clock = std::chrono::system_clock; +using Time = Clock::time_point; + +struct Tips +{ + // The order is important. + // New types must be added before Type::Count item. + enum class Type : uint8_t + { + BookmarksCatalog, + BookingHotels, + DiscoverButton, + MapsLayers, + + Count + }; + + enum class Event : uint8_t + { + ActionClicked, + GotitClicked, + + Count + }; + + struct Info + { + DECLARE_VISITOR(visitor(m_type, "type"), visitor(m_eventCounters, "event_counters"), + visitor(m_lastShown, "last_shown")) + + Type m_type; + Counters m_eventCounters; + Time m_lastShown; + }; + + DECLARE_VISITOR(visitor(m_shownTips, "last_shown_tips"), + visitor(m_totalShownTipsCount, "total_shown_tips_count"), + visitor(m_lastShown, "last_shown")) + + std::vector m_shownTips; + uint32_t m_totalShownTipsCount = 0; + Time m_lastShown; +}; + +struct InfoV0 +{ + static Version GetVersion() { return Version::V0; } + DECLARE_VISITOR(visitor(m_tips, "tips")) + + Tips m_tips; +}; + +using Info = InfoV0; + +inline std::string DebugPrint(Tips::Type const & type) +{ + switch (type) + { + case Tips::Type::BookmarksCatalog: return "BookmarksCatalog"; + case Tips::Type::BookingHotels: return "BookingHotels"; + case Tips::Type::DiscoverButton: return "DiscoverButton"; + case Tips::Type::MapsLayers: return "MapsLayers"; + case Tips::Type::Count: return "Count"; + } +} + +inline std::string DebugPrint(Tips::Event const & type) +{ + switch (type) + { + case Tips::Event::ActionClicked: return "ActionClicked"; + case Tips::Event::GotitClicked: return "GotitClicked"; + case Tips::Event::Count: return "Count"; + } +} +} // namespace eye diff --git a/eye/eye_serdes.cpp b/eye/eye_serdes.cpp new file mode 100644 index 0000000000..262031e86d --- /dev/null +++ b/eye/eye_serdes.cpp @@ -0,0 +1,56 @@ +#include "eye/eye_serdes.hpp" + +#include "coding/reader.hpp" +#include "coding/serdes_json.hpp" +#include "coding/write_to_sink.hpp" +#include "coding/writer.hpp" + +#include "base/logging.hpp" + +#include +#include + +namespace eye +{ +// static +void Serdes::Serialize(Info const & info, std::vector & result) +{ + result.clear(); + using Sink = MemWriter>; + Sink writer(result); + + WriteToSink(writer, static_cast(Info::GetVersion())); + + coding::SerializerJson ser(writer); + ser(info); +} + +// static +void Serdes::Deserialize(std::vector const & bytes, Info & result) +{ + MemReader reader(bytes.data(), bytes.size()); + NonOwningReaderSource source(reader); + + using VersionType = std::underlying_type_t; + VersionType version = static_cast(Version::Unknown); + ReadPrimitiveFromSource(source, version); + if (version == static_cast(Version::V0)) + { + try + { + // TODO: Use temporary object InfoV0 and implement method to convert it to Info, + // TODO: when InfoV1 will be implemented. + coding::DeserializerJson des(source); + des(result); + } + catch (my::Json::Exception & ex) + { + LOG(LERROR, ("Cannot deserialize eye file. Exception:", ex.Msg(), "Version:", version, + "File content:", bytes)); + } + return; + } + + MYTHROW(UnknownVersion, ("Unknown data version:", static_cast(version))); +} +} // namespace eye diff --git a/eye/eye_serdes.hpp b/eye/eye_serdes.hpp new file mode 100644 index 0000000000..b37d9c6f40 --- /dev/null +++ b/eye/eye_serdes.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "eye/eye_info.hpp" + +#include "base/exception.hpp" + +#include +#include + +namespace eye +{ +class Serdes +{ +public: + DECLARE_EXCEPTION(UnknownVersion, RootException); + + static void Serialize(Info const & info, std::vector & result); + static void Deserialize(std::vector const & bytes, Info & result); +}; +} // namespace eye diff --git a/eye/eye_storage.cpp b/eye/eye_storage.cpp new file mode 100644 index 0000000000..31e44fff02 --- /dev/null +++ b/eye/eye_storage.cpp @@ -0,0 +1,56 @@ +#include "eye/eye_storage.hpp" + +#include "platform/platform.hpp" + +#include "coding/file_name_utils.hpp" +#include "coding/file_reader.hpp" +#include "coding/file_writer.hpp" +#include "coding/internal/file_data.hpp" + +#include "base/logging.hpp" + +namespace eye +{ +// static +std::string Storage::GetEyeFilePath() +{ + return GetPlatform().WritablePathForFile("eye"); +} + +// static +bool Storage::Save(std::string const & filePath, std::vector const & src) +{ + return my::WriteToTempAndRenameToFile(filePath, [&src](string const & fileName) + { + try + { + FileWriter writer(fileName); + writer.Write(src.data(), src.size()); + } + catch (FileWriter::Exception const &) + { + return false; + } + + return true; + }); +} + +// static +void Storage::Load(std::string const & filePath, std::vector & dst) +{ + try + { + FileReader reader(filePath); + + dst.clear(); + dst.resize(reader.Size()); + + reader.Read(0, dst.data(), dst.size()); + } + catch (FileReader::Exception const &) + { + dst.clear(); + } +} +} // namespace eye diff --git a/eye/eye_storage.hpp b/eye/eye_storage.hpp new file mode 100644 index 0000000000..540b4eb7bd --- /dev/null +++ b/eye/eye_storage.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace eye +{ +class Storage +{ +public: + static std::string GetEyeFilePath(); + static bool Save(std::string const & filePath, std::vector const & src); + static void Load(std::string const & filePath, std::vector & dst); +}; +} // namespace eye