Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mapsme/omim.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rwxr-xr-xdrape_frontend/frontend_renderer.cpp5
-rw-r--r--drape_frontend/my_position_controller.cpp7
-rw-r--r--drape_frontend/my_position_controller.hpp2
-rw-r--r--drape_frontend/overlays_tracker.cpp10
-rw-r--r--drape_frontend/overlays_tracker.hpp21
-rw-r--r--local_ads/CMakeLists.txt8
-rw-r--r--local_ads/event.cpp60
-rw-r--r--local_ads/event.hpp39
-rw-r--r--local_ads/file_helpers.cpp (renamed from local_ads/local_ads_helpers.cpp)14
-rw-r--r--local_ads/file_helpers.hpp (renamed from local_ads/local_ads_helpers.hpp)16
-rw-r--r--local_ads/local_ads.pro8
-rw-r--r--local_ads/local_ads_tests/CMakeLists.txt4
-rw-r--r--local_ads/local_ads_tests/file_helpers_tests.cpp (renamed from local_ads/local_ads_tests/local_ads_helpers_tests.cpp)2
-rw-r--r--local_ads/local_ads_tests/local_ads_tests.pro5
-rw-r--r--local_ads/local_ads_tests/statistics_tests.cpp164
-rw-r--r--local_ads/statistics.cpp532
-rw-r--r--local_ads/statistics.hpp85
-rw-r--r--map/framework.cpp26
-rw-r--r--map/local_ads_manager.cpp6
-rw-r--r--map/local_ads_manager.hpp8
-rw-r--r--qt/qt_common/map_widget.cpp3
-rw-r--r--qt/qt_common/qtoglcontextfactory.cpp7
-rw-r--r--qt/qt_common/qtoglcontextfactory.hpp3
-rw-r--r--xcode/local_ads/local_ads.xcodeproj/project.pbxproj48
25 files changed, 1035 insertions, 49 deletions
diff --git a/.gitignore b/.gitignore
index bc975bd19f..d1dc476002 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@ data/*.mwm.routing
data/*.mwmmeta
data/[!W]*.mwm
data/local_ads*.dat
+data/local_ads_*/*.*
# Compiled Python
*.pyc
diff --git a/drape_frontend/frontend_renderer.cpp b/drape_frontend/frontend_renderer.cpp
index 671fc90fed..b64618011e 100755
--- a/drape_frontend/frontend_renderer.cpp
+++ b/drape_frontend/frontend_renderer.cpp
@@ -1080,7 +1080,10 @@ void FrontendRenderer::EndUpdateOverlayTree()
m_overlayTree->EndOverlayPlacing();
// Track overlays.
- if (m_overlaysTracker->StartTracking(m_currentZoomLevel))
+ if (m_overlaysTracker->StartTracking(m_currentZoomLevel,
+ m_myPositionController->IsModeHasPosition(),
+ m_myPositionController->GetDrawablePosition(),
+ m_myPositionController->GetHorizontalAccuracy()))
{
for (auto const & handle : m_overlayTree->GetHandlesCache())
{
diff --git a/drape_frontend/my_position_controller.cpp b/drape_frontend/my_position_controller.cpp
index 235ca12792..7467723967 100644
--- a/drape_frontend/my_position_controller.cpp
+++ b/drape_frontend/my_position_controller.cpp
@@ -132,6 +132,7 @@ MyPositionController::MyPositionController(Params && params)
, m_needBlockAnimation(false)
, m_wasRotationInScaling(false)
, m_errorRadius(0.0)
+ , m_horizontalAccuracy(0.0)
, m_position(m2::PointD::Zero())
, m_drawDirection(0.0)
, m_oldPosition(m2::PointD::Zero())
@@ -209,6 +210,11 @@ double MyPositionController::GetErrorRadius() const
return m_errorRadius;
}
+double MyPositionController::GetHorizontalAccuracy() const
+{
+ return m_horizontalAccuracy;
+}
+
bool MyPositionController::IsModeChangeViewport() const
{
return m_mode == location::Follow || m_mode == location::FollowAndRotate;
@@ -376,6 +382,7 @@ void MyPositionController::OnLocationUpdate(location::GpsInfo const & info, bool
// there is significant difference between the real location and the estimated one.
m_position = MercatorBounds::FromLatLon(info.m_latitude, info.m_longitude);
m_errorRadius = rect.SizeX() * 0.5;
+ m_horizontalAccuracy = info.m_horizontalAccuracy;
if (info.m_speed > 0.0)
{
diff --git a/drape_frontend/my_position_controller.hpp b/drape_frontend/my_position_controller.hpp
index 68a79ea889..b21e21b480 100644
--- a/drape_frontend/my_position_controller.hpp
+++ b/drape_frontend/my_position_controller.hpp
@@ -76,6 +76,7 @@ public:
m2::PointD const & Position() const;
double GetErrorRadius() const;
+ double GetHorizontalAccuracy() const;
bool IsModeHasPosition() const;
@@ -167,6 +168,7 @@ private:
ref_ptr<Listener> m_listener;
double m_errorRadius; // error radius in mercator
+ double m_horizontalAccuracy;
m2::PointD m_position; // position in mercator
double m_drawDirection;
m2::PointD m_oldPosition; // position in mercator
diff --git a/drape_frontend/overlays_tracker.cpp b/drape_frontend/overlays_tracker.cpp
index 33621044d9..e5ae098335 100644
--- a/drape_frontend/overlays_tracker.cpp
+++ b/drape_frontend/overlays_tracker.cpp
@@ -9,7 +9,8 @@ void OverlaysTracker::SetTrackedOverlaysFeatures(std::vector<FeatureID> && ids)
m_data.insert(std::make_pair(fid, OverlayInfo()));
}
-bool OverlaysTracker::StartTracking(int zoomLevel)
+bool OverlaysTracker::StartTracking(int zoomLevel, bool hasMyPosition,
+ m2::PointD const & myPosition, double gpsAccuracy)
{
if (zoomLevel < kMinZoomLevel)
{
@@ -19,6 +20,9 @@ bool OverlaysTracker::StartTracking(int zoomLevel)
}
m_zoomLevel = zoomLevel;
+ m_hasMyPosition = hasMyPosition;
+ m_myPosition = m_hasMyPosition ? myPosition : m2::PointD();
+ m_gpsAccuracy = m_hasMyPosition ? gpsAccuracy : 0.0;
for (auto & p : m_data)
p.second.m_tracked = false;
@@ -37,7 +41,9 @@ void OverlaysTracker::Track(FeatureID const & fid)
if (it->second.m_status == OverlayStatus::Invisible)
{
it->second.m_status = OverlayStatus::Visible;
- m_events.emplace_back(it->first, m_zoomLevel, std::chrono::system_clock::now());
+ m_events.emplace_back(it->first, static_cast<uint8_t>(m_zoomLevel),
+ std::chrono::steady_clock::now(), m_hasMyPosition,
+ m_myPosition, m_gpsAccuracy);
}
}
diff --git a/drape_frontend/overlays_tracker.hpp b/drape_frontend/overlays_tracker.hpp
index 8bf2508b9d..f8adad72e8 100644
--- a/drape_frontend/overlays_tracker.hpp
+++ b/drape_frontend/overlays_tracker.hpp
@@ -13,13 +13,20 @@ namespace df
struct OverlayShowEvent
{
FeatureID m_feature;
- int m_zoomLevel;
- std::chrono::system_clock::time_point m_timestamp;
- OverlayShowEvent(FeatureID const & feature, int zoomLevel,
- std::chrono::system_clock::time_point const & timestamp)
+ uint8_t m_zoomLevel;
+ std::chrono::steady_clock::time_point m_timestamp;
+ bool m_hasMyPosition;
+ m2::PointD m_myPosition;
+ double m_gpsAccuracy;
+ OverlayShowEvent(FeatureID const & feature, uint8_t zoomLevel,
+ std::chrono::steady_clock::time_point const & timestamp,
+ bool hasMyPosition, m2::PointD const & myPosition, double gpsAccuracy)
: m_feature(feature)
, m_zoomLevel(zoomLevel)
, m_timestamp(timestamp)
+ , m_hasMyPosition(hasMyPosition)
+ , m_myPosition(myPosition)
+ , m_gpsAccuracy(gpsAccuracy)
{}
};
@@ -34,7 +41,8 @@ public:
void SetTrackedOverlaysFeatures(std::vector<FeatureID> && ids);
- bool StartTracking(int zoomLevel);
+ bool StartTracking(int zoomLevel, bool hasMyPosition,
+ m2::PointD const & myPosition, double gpsAccuracy);
void Track(FeatureID const & fid);
void FinishTracking();
@@ -60,6 +68,9 @@ private:
std::map<FeatureID, OverlayInfo> m_data;
std::list<OverlayShowEvent> m_events;
int m_zoomLevel = -1;
+ bool m_hasMyPosition = false;
+ m2::PointD m_myPosition = m2::PointD::Zero();
+ double m_gpsAccuracy = 0.0;
};
} // namespace df
diff --git a/local_ads/CMakeLists.txt b/local_ads/CMakeLists.txt
index 1d2e65354e..21abef6020 100644
--- a/local_ads/CMakeLists.txt
+++ b/local_ads/CMakeLists.txt
@@ -5,8 +5,12 @@ set(
campaign.hpp
campaign_serialization.cpp
campaign_serialization.hpp
- local_ads_helpers.cpp
- local_ads_helpers.hpp
+ event.cpp
+ event.hpp
+ file_helpers.cpp
+ file_helpers.hpp
+ statistics.cpp
+ statistics.hpp
)
add_library(${PROJECT_NAME} ${SRC})
diff --git a/local_ads/event.cpp b/local_ads/event.cpp
new file mode 100644
index 0000000000..42683672d7
--- /dev/null
+++ b/local_ads/event.cpp
@@ -0,0 +1,60 @@
+#include "local_ads/event.hpp"
+
+#include "base/math.hpp"
+
+#include <sstream>
+
+namespace local_ads
+{
+Event::Event(EventType type, int64_t mwmVersion, std::string const & countryId, uint32_t featureId,
+ uint8_t zoomLevel, Timestamp const & timestamp, double latitude, double longitude,
+ uint16_t accuracyInMeters)
+ : m_type(type)
+ , m_mwmVersion(mwmVersion)
+ , m_countryId(countryId)
+ , m_featureId(featureId)
+ , m_zoomLevel(zoomLevel)
+ , m_timestamp(timestamp)
+ , m_latitude(latitude)
+ , m_longitude(longitude)
+ , m_accuracyInMeters(accuracyInMeters)
+{
+}
+
+bool Event::operator<(Event const & event) const
+{
+ if (m_mwmVersion != event.m_mwmVersion)
+ return m_mwmVersion < event.m_mwmVersion;
+
+ if (m_countryId != event.m_countryId)
+ return m_countryId < event.m_countryId;
+
+ return m_timestamp < event.m_timestamp;
+}
+
+bool Event::operator==(Event const & event) const
+{
+ double const kEps = 1e-5;
+ using namespace std::chrono;
+ return m_type == event.m_type && m_mwmVersion == event.m_mwmVersion &&
+ m_countryId == event.m_countryId && m_featureId == event.m_featureId &&
+ m_zoomLevel == event.m_zoomLevel &&
+ my::AlmostEqualAbs(m_latitude, event.m_latitude, kEps) &&
+ my::AlmostEqualAbs(m_longitude, event.m_longitude, kEps) &&
+ m_accuracyInMeters == event.m_accuracyInMeters &&
+ duration_cast<seconds>(m_timestamp - event.m_timestamp).count() == 0;
+}
+
+std::string DebugPrint(Event const & event)
+{
+ using namespace std::chrono;
+ std::ostringstream s;
+ s << "[Type:" << static_cast<uint32_t>(event.m_type) << "; Country: " << event.m_countryId
+ << "; Version: " << event.m_mwmVersion << "; FID: " << event.m_featureId
+ << "; Zoom: " << static_cast<uint32_t>(event.m_zoomLevel)
+ << "; Ts: " << duration_cast<std::chrono::seconds>(event.m_timestamp.time_since_epoch()).count()
+ << "; LatLon: " << event.m_latitude << ", " << event.m_longitude
+ << "; Accuracy: " << event.m_accuracyInMeters << "]";
+ return s.str();
+}
+} // namespace local_ads
diff --git a/local_ads/event.hpp b/local_ads/event.hpp
new file mode 100644
index 0000000000..982bb41c6b
--- /dev/null
+++ b/local_ads/event.hpp
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <chrono>
+#include <string>
+
+namespace local_ads
+{
+using Timestamp = std::chrono::steady_clock::time_point;
+
+enum class EventType
+{
+ ShowPoint = 0,
+ OpenInfo,
+ ClickedPhone,
+ ClickedWebsite
+};
+
+struct Event
+{
+ EventType m_type;
+ int64_t m_mwmVersion;
+ std::string m_countryId;
+ uint32_t m_featureId;
+ uint8_t m_zoomLevel;
+ Timestamp m_timestamp;
+ double m_latitude;
+ double m_longitude;
+ uint16_t m_accuracyInMeters;
+
+ Event(EventType type, int64_t mwmVersion, std::string const & countryId, uint32_t featureId,
+ uint8_t zoomLevel, Timestamp const & timestamp, double latitude, double longitude,
+ uint16_t accuracyInMeters);
+
+ bool operator<(Event const & event) const;
+ bool operator==(Event const & event) const;
+};
+
+std::string DebugPrint(Event const & event);
+} // namespace local_ads
diff --git a/local_ads/local_ads_helpers.cpp b/local_ads/file_helpers.cpp
index 0ff6060985..1bfdb92069 100644
--- a/local_ads/local_ads_helpers.cpp
+++ b/local_ads/file_helpers.cpp
@@ -1,4 +1,4 @@
-#include "local_ads/local_ads_helpers.hpp"
+#include "local_ads/file_helpers.hpp"
#include "coding/multilang_utf8_string.hpp"
#include "coding/reader.hpp"
@@ -14,10 +14,10 @@ void WriteCountryName(FileWriter & writer, std::string const & countryName)
utils::WriteString(writer, countryName);
}
-void WriteDuration(FileWriter & writer, int64_t duration)
+void WriteZigZag(FileWriter & writer, int64_t duration)
{
- uint64_t const encodedDuration = bits::ZigZagEncode(duration);
- WriteToSink(writer, encodedDuration);
+ uint64_t const encoded = bits::ZigZagEncode(duration);
+ WriteToSink(writer, encoded);
}
void WriteRawData(FileWriter & writer, std::vector<uint8_t> const & rawData)
@@ -34,10 +34,10 @@ std::string ReadCountryName(ReaderSource<FileReader> & src)
return countryName;
}
-int64_t ReadDuration(ReaderSource<FileReader> & src)
+int64_t ReadZigZag(ReaderSource<FileReader> & src)
{
- uint64_t const duration = ReadPrimitiveFromSource<uint64_t>(src);
- return bits::ZigZagDecode(duration);
+ uint64_t const value = ReadPrimitiveFromSource<uint64_t>(src);
+ return bits::ZigZagDecode(value);
}
std::vector<uint8_t> ReadRawData(ReaderSource<FileReader> & src)
diff --git a/local_ads/local_ads_helpers.hpp b/local_ads/file_helpers.hpp
index df385b96a3..9078e9e36d 100644
--- a/local_ads/local_ads_helpers.hpp
+++ b/local_ads/file_helpers.hpp
@@ -1,5 +1,7 @@
#pragma once
+#include "local_ads/event.hpp"
+
#include "coding/file_reader.hpp"
#include "coding/file_writer.hpp"
@@ -11,26 +13,26 @@ namespace local_ads
{
void WriteCountryName(FileWriter & writer, std::string const & countryName);
-void WriteDuration(FileWriter & writer, int64_t duration);
+void WriteZigZag(FileWriter & writer, int64_t duration);
template <typename Duration>
-void WriteTimestamp(FileWriter & writer, std::chrono::steady_clock::time_point ts)
+void WriteTimestamp(FileWriter & writer, Timestamp ts)
{
int64_t const d = std::chrono::duration_cast<Duration>(ts.time_since_epoch()).count();
- WriteDuration(writer, d);
+ WriteZigZag(writer, d);
}
void WriteRawData(FileWriter & writer, std::vector<uint8_t> const & rawData);
std::string ReadCountryName(ReaderSource<FileReader> & src);
-int64_t ReadDuration(ReaderSource<FileReader> & src);
+int64_t ReadZigZag(ReaderSource<FileReader> & src);
template <typename Duration>
-std::chrono::steady_clock::time_point ReadTimestamp(ReaderSource<FileReader> & src)
+Timestamp ReadTimestamp(ReaderSource<FileReader> & src)
{
- int64_t const d = ReadDuration(src);
- return std::chrono::steady_clock::time_point(Duration(d));
+ int64_t const d = ReadZigZag(src);
+ return Timestamp(Duration(d));
}
std::vector<uint8_t> ReadRawData(ReaderSource<FileReader> & src);
diff --git a/local_ads/local_ads.pro b/local_ads/local_ads.pro
index dc7f22d840..2f3a9e6c00 100644
--- a/local_ads/local_ads.pro
+++ b/local_ads/local_ads.pro
@@ -8,10 +8,14 @@ include($$ROOT_DIR/common.pri)
SOURCES += \
campaign_serialization.cpp \
- local_ads_helpers.cpp \
+ event.cpp \
+ file_helpers.cpp \
+ statistics.cpp \
HEADERS += \
campaign.hpp \
campaign_serialization.hpp \
- local_ads_helpers.hpp \
+ event.hpp \
+ file_helpers.hpp \
+ statistics.hpp \
diff --git a/local_ads/local_ads_tests/CMakeLists.txt b/local_ads/local_ads_tests/CMakeLists.txt
index 62d0e29b95..2e540837e7 100644
--- a/local_ads/local_ads_tests/CMakeLists.txt
+++ b/local_ads/local_ads_tests/CMakeLists.txt
@@ -3,7 +3,8 @@ project(local_ads_tests)
set(
SRC
campaign_serialization_test.cpp
- local_ads_helpers_tests.cpp
+ file_helpers_tests.cpp
+ statistics_tests.cpp
)
omim_add_test(${PROJECT_NAME} ${SRC})
@@ -12,6 +13,7 @@ omim_link_libraries(
${PROJECT_NAME}
local_ads
coding
+ geometry
platform
platform_tests_support
base
diff --git a/local_ads/local_ads_tests/local_ads_helpers_tests.cpp b/local_ads/local_ads_tests/file_helpers_tests.cpp
index 300e55b939..0b48ad4239 100644
--- a/local_ads/local_ads_tests/local_ads_helpers_tests.cpp
+++ b/local_ads/local_ads_tests/file_helpers_tests.cpp
@@ -1,6 +1,6 @@
#include "testing/testing.hpp"
-#include "local_ads/local_ads_helpers.hpp"
+#include "local_ads/file_helpers.hpp"
#include "coding/file_name_utils.hpp"
diff --git a/local_ads/local_ads_tests/local_ads_tests.pro b/local_ads/local_ads_tests/local_ads_tests.pro
index d3b8831ea7..20a8e5f018 100644
--- a/local_ads/local_ads_tests/local_ads_tests.pro
+++ b/local_ads/local_ads_tests/local_ads_tests.pro
@@ -4,7 +4,7 @@ CONFIG -= app_bundle
TEMPLATE = app
ROOT_DIR = ../..
-DEPENDENCIES = local_ads platform_tests_support platform coding base stats_client
+DEPENDENCIES = local_ads platform_tests_support platform coding geometry base stats_client
include($$ROOT_DIR/common.pri)
@@ -19,4 +19,5 @@ HEADERS += \
SOURCES += \
$$ROOT_DIR/testing/testingmain.cpp \
campaign_serialization_test.cpp \
- local_ads_helpers_tests.cpp \
+ file_helpers_tests.cpp \
+ statistics_tests.cpp \
diff --git a/local_ads/local_ads_tests/statistics_tests.cpp b/local_ads/local_ads_tests/statistics_tests.cpp
new file mode 100644
index 0000000000..f79fa0b0dc
--- /dev/null
+++ b/local_ads/local_ads_tests/statistics_tests.cpp
@@ -0,0 +1,164 @@
+#include "testing/testing.hpp"
+
+#include "local_ads/statistics.hpp"
+
+#include "coding/file_name_utils.hpp"
+
+namespace
+{
+class StatisticsGuard
+{
+public:
+ StatisticsGuard(local_ads::Statistics & statistics) : m_statistics(statistics) {}
+ ~StatisticsGuard()
+ {
+ m_statistics.Teardown();
+ m_statistics.CleanupAfterTesting();
+ }
+
+private:
+ local_ads::Statistics & m_statistics;
+};
+} // namespace
+
+using namespace std::chrono;
+using ET = local_ads::EventType;
+using TS = local_ads::Timestamp;
+
+UNIT_TEST(LocalAdsStatistics_Read_Write_Simple)
+{
+ local_ads::Statistics statistics;
+ StatisticsGuard guard(statistics);
+
+ std::list<local_ads::Event> events;
+ // type, mwmVersion, countryId, featureId, zoomLevel, timestamp, latitude, longitude, accuracyInMeters
+ events.emplace_back(ET::ShowPoint, 123456, "Moscow", 111, 15, TS(minutes(5)), 30.0, 64.0, 10);
+ events.emplace_back(ET::ShowPoint, 123456, "Moscow", 222, 13, TS(minutes(10)), 20.0, 14.0, 20);
+ events.emplace_back(ET::OpenInfo, 123456, "Moscow", 111, 17, TS(minutes(15)), 53.0, 54.0, 10000);
+ std::string unusedFileName;
+ auto unprocessedEvents = statistics.WriteEventsForTesting(events, unusedFileName);
+ TEST_EQUAL(unprocessedEvents.size(), 0, ());
+
+ TEST_EQUAL(statistics.ReadEventsForTesting("Moscow_123456.dat"), events, ());
+}
+
+UNIT_TEST(LocalAdsStatistics_Write_With_Unprocessed)
+{
+ local_ads::Statistics statistics;
+ StatisticsGuard guard(statistics);
+
+ std::list<local_ads::Event> events;
+ events.emplace_back(ET::ShowPoint, 123456, "Moscow", 111, 15, TS(minutes(5)), 0.0, 0.0, 10);
+ events.emplace_back(ET::ShowPoint, 123456, "Moscow", 222, 13, TS(minutes(10)), 20.0, 14.0, 20);
+ events.emplace_back(ET::OpenInfo, 123456, "Moscow", 111, 17, TS(minutes(15)), 15.0, 14.0, 20);
+ std::string fileNameToRebuild;
+ auto unprocessedEvents = statistics.WriteEventsForTesting(events, fileNameToRebuild);
+ TEST_EQUAL(unprocessedEvents.size(), 0, ());
+ TEST(fileNameToRebuild.empty(), ());
+
+ std::list<local_ads::Event> events2;
+ events2.emplace_back(ET::ShowPoint, 123456, "Moscow", 333, 15, TS(minutes(1)), 1.0, 89.0, 20);
+ events2.emplace_back(ET::ShowPoint, 123456, "Moscow", 444, 15, TS(minutes(20)), 30.0, 13.0, 15);
+ auto unprocessedEvents2 = statistics.WriteEventsForTesting(events2, fileNameToRebuild);
+
+ std::list<local_ads::Event> expectedUnprocessedEvents = events2;
+ expectedUnprocessedEvents.sort();
+
+ my::GetNameFromFullPath(fileNameToRebuild);
+ TEST_EQUAL(fileNameToRebuild, "Moscow_123456.dat", ());
+ TEST_EQUAL(expectedUnprocessedEvents, unprocessedEvents2, ());
+}
+
+UNIT_TEST(LocalAdsStatistics_Process_With_Rebuild)
+{
+ local_ads::Statistics statistics;
+ StatisticsGuard guard(statistics);
+
+ std::list<local_ads::Event> events;
+ events.emplace_back(ET::ShowPoint, 123456, "Moscow", 111, 15, TS(minutes(5)), 50.0, 14.0, 20);
+ events.emplace_back(ET::ShowPoint, 123456, "Moscow", 222, 13, TS(minutes(10)), 69.0, 67.0, 100);
+ events.emplace_back(ET::OpenInfo, 123456, "Moscow", 111, 17, TS(minutes(15)), 45.0, 80.0, 34);
+ std::string unused;
+ statistics.WriteEventsForTesting(events, unused);
+
+ TEST_EQUAL(statistics.ReadEventsForTesting("Moscow_123456.dat"), events, ());
+
+ std::list<local_ads::Event> events2;
+ events2.emplace_back(ET::ShowPoint, 123456, "Moscow", 333, 15, TS(minutes(1)), 20.0, 14.0, 12);
+ events2.emplace_back(ET::ShowPoint, 123456, "Moscow", 444, 15, TS(minutes(20)), 30.0, 56.0, 3535);
+
+ statistics.ProcessEventsForTesting(events2);
+
+ std::list<local_ads::Event> expectedResult = events;
+ expectedResult.insert(expectedResult.end(), events2.begin(), events2.end());
+ expectedResult.sort();
+
+ TEST_EQUAL(statistics.ReadEventsForTesting("Moscow_123456.dat"), expectedResult, ());
+}
+
+UNIT_TEST(LocalAdsStatistics_Process_With_Clipping)
+{
+ local_ads::Statistics statistics;
+ StatisticsGuard guard(statistics);
+
+ std::list<local_ads::Event> events;
+ events.emplace_back(ET::ShowPoint, 123456, "Moscow", 111, 15, TS(minutes(5)), 20.0, 14.0, 12);
+ events.emplace_back(ET::ShowPoint, 123456, "Moscow", 222, 13, TS(minutes(10)), 69.0, 67.0, 100);
+ events.emplace_back(ET::OpenInfo, 123456, "Moscow", 111, 17, TS(minutes(25 * 60 + 15)), 20.0,
+ 14.0, 20);
+ std::string unused;
+ statistics.WriteEventsForTesting(events, unused);
+
+ TEST_EQUAL(statistics.ReadEventsForTesting("Moscow_123456.dat"), events, ());
+
+ std::list<local_ads::Event> events2;
+ events2.emplace_back(ET::ShowPoint, 123456, "Moscow", 333, 15, TS(minutes(24 * 183 * 60 + 50)),
+ 20.0, 14.0, 20);
+
+ statistics.ProcessEventsForTesting(events2);
+
+ std::list<local_ads::Event> expectedResult;
+ expectedResult.push_back(local_ads::Event(events.back()));
+ expectedResult.insert(expectedResult.end(), events2.begin(), events2.end());
+ expectedResult.sort();
+
+ TEST_EQUAL(statistics.ReadEventsForTesting("Moscow_123456.dat"), expectedResult, ());
+}
+
+UNIT_TEST(LocalAdsStatistics_Process_Complex)
+{
+ local_ads::Statistics statistics;
+ StatisticsGuard guard(statistics);
+
+ std::list<local_ads::Event> events;
+ events.emplace_back(ET::ShowPoint, 123456, "Moscow", 111, 15, TS(minutes(5)), 20.0, 14.0, 20);
+ events.emplace_back(ET::ShowPoint, 123456, "Minsk", 222, 13, TS(minutes(10)), 30.0, 14.0, 20);
+ events.emplace_back(ET::OpenInfo, 123456, "Minsk", 111, 17, TS(minutes(25)), 40.0, 14.0, 20);
+ events.emplace_back(ET::OpenInfo, 123456, "Minsk", 111, 17, TS(minutes(25 * 60 + 15)), 20.0, 14.0,
+ 20);
+ std::string unused;
+ statistics.WriteEventsForTesting(events, unused);
+
+ std::list<local_ads::Event> expectedResult1;
+ expectedResult1.push_back(local_ads::Event(events.front()));
+ TEST_EQUAL(statistics.ReadEventsForTesting("Moscow_123456.dat"), expectedResult1, ());
+
+ std::list<local_ads::Event> expectedResult2 = events;
+ expectedResult2.erase(expectedResult2.begin());
+ TEST_EQUAL(statistics.ReadEventsForTesting("Minsk_123456.dat"), expectedResult2, ());
+
+ std::list<local_ads::Event> events2;
+ events2.emplace_back(ET::ShowPoint, 123456, "Moscow", 333, 15, TS(minutes(100)), 20.0, 14.0, 20);
+ events2.emplace_back(ET::ShowPoint, 123456, "Minsk", 333, 15, TS(minutes(24 * 183 * 60 + 50)),
+ 20.0, 14.0, 20);
+
+ statistics.ProcessEventsForTesting(events2);
+
+ expectedResult1.push_back(local_ads::Event(events2.front()));
+ TEST_EQUAL(statistics.ReadEventsForTesting("Moscow_123456.dat"), expectedResult1, ());
+
+ expectedResult2.clear();
+ expectedResult2.push_back(local_ads::Event(events.back()));
+ expectedResult2.push_back(local_ads::Event(events2.back()));
+ TEST_EQUAL(statistics.ReadEventsForTesting("Minsk_123456.dat"), expectedResult2, ());
+}
diff --git a/local_ads/statistics.cpp b/local_ads/statistics.cpp
new file mode 100644
index 0000000000..d1514eacf1
--- /dev/null
+++ b/local_ads/statistics.cpp
@@ -0,0 +1,532 @@
+#include "local_ads/statistics.hpp"
+#include "local_ads/file_helpers.hpp"
+
+#include "platform/http_client.hpp"
+#include "platform/platform.hpp"
+
+#include "coding/file_name_utils.hpp"
+#include "coding/file_writer.hpp"
+#include "coding/point_to_integer.hpp"
+#include "coding/url_encode.hpp"
+#include "coding/write_to_sink.hpp"
+
+#include "geometry/mercator.hpp"
+
+#include "base/assert.hpp"
+#include "base/logging.hpp"
+#include "base/string_utils.hpp"
+
+#include <functional>
+#include <sstream>
+
+namespace
+{
+std::string const kStatisticsFolderName = "local_ads_stats";
+std::string const kStatisticsExt = ".dat";
+
+uint64_t constexpr kMaxFilesSizeInBytes = 10 * 1024 * 1024;
+float const kEventsDisposingRate = 0.2f;
+
+auto constexpr kSendingTimeout = std::chrono::hours(1);
+int64_t constexpr kEventMaxLifetimeInSeconds = 24 * 183 * 3600; // About half of year.
+auto constexpr kDeletionPeriod = std::chrono::hours(24);
+
+// TODO: set correct address
+std::string const kStatisticsServer = "";
+
+void WriteMetadata(FileWriter & writer, std::string const & countryId, int64_t mwmVersion,
+ local_ads::Timestamp const & ts)
+{
+ local_ads::WriteCountryName(writer, countryId);
+ local_ads::WriteZigZag(writer, mwmVersion);
+ local_ads::WriteTimestamp<std::chrono::seconds>(writer, ts);
+}
+
+void ReadMetadata(ReaderSource<FileReader> & src, std::string & countryId, int64_t & mwmVersion,
+ local_ads::Timestamp & ts)
+{
+ countryId = local_ads::ReadCountryName(src);
+ mwmVersion = local_ads::ReadZigZag(src);
+ ts = local_ads::ReadTimestamp<std::chrono::seconds>(src);
+}
+
+void WritePackedData(FileWriter & writer, local_ads::Statistics::PackedData && packedData)
+{
+ WriteToSink(writer, packedData.m_eventType);
+ WriteToSink(writer, packedData.m_zoomLevel);
+ WriteToSink(writer, packedData.m_featureIndex);
+ WriteToSink(writer, packedData.m_seconds);
+ local_ads::WriteZigZag(writer, packedData.m_mercator);
+ WriteToSink(writer, packedData.m_accuracy);
+}
+
+template <typename ToDo>
+void ReadPackedData(ReaderSource<FileReader> & src, ToDo && toDo)
+{
+ using PackedData = local_ads::Statistics::PackedData;
+
+ std::string countryId;
+ int64_t mwmVersion;
+ local_ads::Timestamp baseTimestamp;
+ ReadMetadata(src, countryId, mwmVersion, baseTimestamp);
+ while (src.Size() > 0)
+ {
+ PackedData data;
+ data.m_eventType = ReadPrimitiveFromSource<uint8_t>(src);
+ data.m_zoomLevel = ReadPrimitiveFromSource<uint8_t>(src);
+ data.m_featureIndex = ReadPrimitiveFromSource<uint32_t>(src);
+ data.m_seconds = ReadPrimitiveFromSource<uint32_t>(src);
+ data.m_mercator = local_ads::ReadZigZag(src);
+ data.m_accuracy = ReadPrimitiveFromSource<uint16_t>(src);
+ toDo(std::move(data), countryId, mwmVersion, baseTimestamp);
+ }
+}
+
+template <typename ToDo>
+void FilterEvents(std::list<local_ads::Event> const & events, std::string const & countryId,
+ int64_t mwmVersion, ToDo && toDo)
+{
+ for (auto const & event : events)
+ {
+ if (event.m_countryId != countryId || event.m_mwmVersion != mwmVersion)
+ continue;
+ toDo(event);
+ }
+}
+
+local_ads::Timestamp GetMinTimestamp(std::list<local_ads::Event> const & events,
+ std::string const & countryId, int64_t mwmVersion)
+{
+ local_ads::Timestamp minTimestamp = local_ads::Timestamp::max();
+ FilterEvents(events, countryId, mwmVersion, [&minTimestamp](local_ads::Event const & event)
+ {
+ if (event.m_timestamp < minTimestamp)
+ minTimestamp = event.m_timestamp;
+ });
+ return minTimestamp;
+}
+
+local_ads::Timestamp GetMaxTimestamp(std::list<local_ads::Event> const & events,
+ std::string const & countryId, int64_t mwmVersion)
+{
+ local_ads::Timestamp maxTimestamp = local_ads::Timestamp::min();
+ FilterEvents(events, countryId, mwmVersion, [&maxTimestamp](local_ads::Event const & event)
+ {
+ if (event.m_timestamp > maxTimestamp)
+ maxTimestamp = event.m_timestamp;
+ });
+ return maxTimestamp;
+}
+
+std::string GetPath(std::string const & fileName)
+{
+ return my::JoinFoldersToPath({GetPlatform().WritableDir(), kStatisticsFolderName}, fileName);
+}
+
+std::string GetPath(local_ads::Event const & event)
+{
+ return GetPath(event.m_countryId + "_" + strings::to_string(event.m_mwmVersion) + kStatisticsExt);
+}
+
+std::string StatisticsFolder()
+{
+ return GetPath("");
+}
+
+void CreateDirIfNotExist()
+{
+ std::string const statsFolder = StatisticsFolder();
+ if (!GetPlatform().IsFileExistsByFullPath(statsFolder))
+ GetPlatform().MkDir(statsFolder);
+}
+
+std::string MakeRemoteURL(std::string const & userId, std::string const & name, int64_t version)
+{
+ if (kStatisticsServer.empty())
+ return {};
+
+ std::ostringstream ss;
+ ss << kStatisticsServer << "/";
+ ss << UrlEncode(userId) << "/";
+ ss << version << "/";
+ ss << UrlEncode(name);
+ return ss.str();
+}
+} // namespace
+
+namespace local_ads
+{
+Statistics::~Statistics()
+{
+ std::lock_guard<std::mutex> lock(m_mutex);
+ ASSERT(!m_isRunning, ());
+}
+
+void Statistics::Startup()
+{
+ {
+ std::lock_guard<std::mutex> lock(m_mutex);
+ if (m_isRunning)
+ return;
+ m_isRunning = true;
+ }
+ m_thread = threads::SimpleThread(&Statistics::ThreadRoutine, this);
+}
+
+void Statistics::Teardown()
+{
+ {
+ std::lock_guard<std::mutex> lock(m_mutex);
+ if (!m_isRunning)
+ return;
+ m_isRunning = false;
+ }
+ m_condition.notify_one();
+ m_thread.join();
+}
+
+bool Statistics::RequestEvents(std::list<Event> & events, bool & needToSend)
+{
+ std::unique_lock<std::mutex> lock(m_mutex);
+
+ bool const isTimeout = !m_condition.wait_for(lock, kSendingTimeout, [this]
+ {
+ return !m_isRunning || !m_events.empty();
+ });
+
+ if (!m_isRunning)
+ return false;
+
+ using namespace std::chrono;
+ needToSend = isTimeout || (steady_clock::now() > (m_lastSending + kSendingTimeout));
+
+ events = std::move(m_events);
+ m_events.clear();
+ return true;
+}
+
+void Statistics::RegisterEvent(Event && event)
+{
+ std::lock_guard<std::mutex> lock(m_mutex);
+ if (!m_isRunning)
+ return;
+ m_events.push_back(std::move(event));
+ m_condition.notify_one();
+}
+
+void Statistics::RegisterEvents(std::list<Event> && events)
+{
+ std::lock_guard<std::mutex> lock(m_mutex);
+ if (!m_isRunning)
+ return;
+ m_events.splice(m_events.end(), std::move(events));
+ m_condition.notify_one();
+}
+
+void Statistics::ThreadRoutine()
+{
+ std::list<Event> events;
+ bool needToSend = false;
+ while (RequestEvents(events, needToSend))
+ {
+ ProcessEvents(events);
+ events.clear();
+
+ // Send statistics to server.
+ if (needToSend)
+ SendToServer();
+ }
+}
+
+std::list<Event> Statistics::WriteEvents(std::list<Event> & events, std::string & fileNameToRebuild)
+{
+ try
+ {
+ CreateDirIfNotExist();
+ if (m_metadataCache.empty())
+ IndexMetadata();
+
+ std::unique_ptr<FileWriter> writer;
+
+ events.sort();
+
+ auto eventIt = events.begin();
+ for (; eventIt != events.end(); ++eventIt)
+ {
+ Event const & event = *eventIt;
+ MetadataKey const key = std::make_pair(event.m_countryId, event.m_mwmVersion);
+ auto it = m_metadataCache.find(key);
+
+ // Get metadata.
+ Metadata metadata;
+ bool needWriteMetadata = false;
+ if (it == m_metadataCache.end())
+ {
+ metadata.m_timestamp = GetMinTimestamp(events, event.m_countryId, event.m_mwmVersion);
+ metadata.m_fileName = GetPath(event);
+ m_metadataCache[key] = metadata;
+ needWriteMetadata = true;
+ }
+ else
+ {
+ metadata = it->second;
+ }
+
+ if (writer == nullptr || writer->GetName() != metadata.m_fileName)
+ writer = my::make_unique<FileWriter>(metadata.m_fileName, FileWriter::OP_APPEND);
+
+ if (needWriteMetadata)
+ WriteMetadata(*writer, event.m_countryId, event.m_mwmVersion, metadata.m_timestamp);
+
+ // Check if timestamp is out of date. In this case we have to rebuild events package.
+ using namespace std::chrono;
+ int64_t const s = duration_cast<seconds>(event.m_timestamp - metadata.m_timestamp).count();
+ if (s < 0 || s > kEventMaxLifetimeInSeconds)
+ {
+ fileNameToRebuild = writer->GetName();
+
+ // Return unprocessed events.
+ std::list<Event> unprocessedEvents;
+ unprocessedEvents.splice(unprocessedEvents.end(), events, eventIt, events.end());
+ return unprocessedEvents;
+ }
+
+ PackedData data;
+ data.m_featureIndex = event.m_featureId;
+ data.m_seconds = static_cast<uint32_t>(s);
+ data.m_zoomLevel = event.m_zoomLevel;
+ data.m_eventType = static_cast<uint8_t>(event.m_type);
+ auto const mercatorPt = MercatorBounds::FromLatLon(event.m_latitude, event.m_longitude);
+ data.m_mercator = PointToInt64(mercatorPt, POINT_COORD_BITS);
+ data.m_accuracy = event.m_accuracyInMeters;
+ WritePackedData(*writer, std::move(data));
+ }
+ }
+ catch (RootException const & ex)
+ {
+ LOG(LWARNING, (ex.Msg()));
+ }
+ return std::list<Event>();
+}
+
+std::list<Event> Statistics::ReadEvents(std::string const & fileName) const
+{
+ std::list<Event> result;
+ if (!GetPlatform().IsFileExistsByFullPath(fileName))
+ return result;
+
+ try
+ {
+ FileReader reader(fileName);
+ ReaderSource<FileReader> src(reader);
+ ReadPackedData(src, [&result](PackedData && data, std::string const & countryId,
+ int64_t mwmVersion, Timestamp const & baseTimestamp) {
+ auto const mercatorPt = Int64ToPoint(data.m_mercator, POINT_COORD_BITS);
+ result.emplace_back(static_cast<EventType>(data.m_eventType), mwmVersion, countryId,
+ data.m_featureIndex, data.m_zoomLevel,
+ baseTimestamp + std::chrono::seconds(data.m_seconds),
+ MercatorBounds::YToLat(mercatorPt.y),
+ MercatorBounds::XToLon(mercatorPt.x), data.m_accuracy);
+ });
+ }
+ catch (Reader::Exception const & ex)
+ {
+ LOG(LWARNING, ("Error reading file:", fileName, ex.Msg()));
+ }
+ return result;
+}
+
+void Statistics::ProcessEvents(std::list<Event> & events)
+{
+ bool needRebuild;
+ do
+ {
+ std::string fileNameToRebuild;
+ auto unprocessedEvents = WriteEvents(events, fileNameToRebuild);
+ needRebuild = !unprocessedEvents.empty();
+ if (!needRebuild)
+ break;
+
+ // The first event in the list is cause of writing interruption.
+ Event event = unprocessedEvents.front();
+
+ // Read events and merge with unprocessed ones.
+ std::list<Event> newEvents = ReadEvents(fileNameToRebuild);
+ newEvents.splice(newEvents.end(), std::move(unprocessedEvents));
+ newEvents.sort();
+
+ // Clip obsolete events.
+ auto constexpr kLifetime = std::chrono::seconds(kEventMaxLifetimeInSeconds);
+ auto const maxTimestamp = GetMaxTimestamp(newEvents, event.m_countryId, event.m_mwmVersion);
+ auto newMinTimestamp = maxTimestamp - kLifetime + kDeletionPeriod;
+ for (auto eventIt = newEvents.begin(); eventIt != newEvents.end();)
+ {
+ if (eventIt->m_countryId == event.m_countryId &&
+ eventIt->m_mwmVersion == event.m_mwmVersion && eventIt->m_timestamp < newMinTimestamp)
+ {
+ eventIt = newEvents.erase(eventIt);
+ }
+ else
+ {
+ ++eventIt;
+ }
+ }
+
+ // Update run-time cache and delete rebuilding file.
+ m_metadataCache.erase(MetadataKey(event.m_countryId, event.m_mwmVersion));
+ FileWriter::DeleteFileX(fileNameToRebuild);
+ std::swap(events, newEvents);
+ } while (needRebuild);
+}
+
+void Statistics::SendToServer()
+{
+ for (auto it = m_metadataCache.begin(); it != m_metadataCache.end();)
+ {
+ std::string const url = MakeRemoteURL(m_userId, it->first.first, it->first.second);
+ if (url.empty())
+ return;
+
+ std::vector<uint8_t> bytes = SerializeForServer(ReadEvents(it->second.m_fileName));
+ if (bytes.empty())
+ {
+ ++it;
+ continue;
+ }
+
+ platform::HttpClient request(url);
+ request.SetBodyData(std::string(bytes.begin(), bytes.end()), "application/octet-stream");
+ if (request.RunHttpRequest() && request.ErrorCode() == 200)
+ {
+ FileWriter::DeleteFileX(it->second.m_fileName);
+ it = m_metadataCache.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+ m_lastSending = std::chrono::steady_clock::now();
+}
+
+std::vector<uint8_t> Statistics::SerializeForServer(std::list<Event> const & events) const
+{
+ if (events.empty())
+ return {};
+
+ // TODO: implement serialization
+ return std::vector<uint8_t>{1, 2, 3, 4, 5};
+}
+
+std::list<Event> Statistics::WriteEventsForTesting(std::list<Event> const & events,
+ std::string & fileNameToRebuild)
+{
+ std::list<Event> mutableEvents = events;
+ return WriteEvents(mutableEvents, fileNameToRebuild);
+}
+
+void Statistics::IndexMetadata()
+{
+ std::vector<std::string> files;
+ GetPlatform().GetFilesByExt(StatisticsFolder(), kStatisticsExt, files);
+ for (auto const & filename : files)
+ ExtractMetadata(GetPath(filename));
+ BalanceMemory();
+}
+
+void Statistics::ExtractMetadata(std::string const & fileName)
+{
+ ASSERT(GetPlatform().IsFileExistsByFullPath(fileName), ());
+ try
+ {
+ std::string countryId;
+ int64_t mwmVersion;
+ Timestamp baseTimestamp;
+ {
+ FileReader reader(fileName);
+ ReaderSource<FileReader> src(reader);
+ ReadMetadata(src, countryId, mwmVersion, baseTimestamp);
+ }
+ MetadataKey const key = std::make_pair(countryId, mwmVersion);
+ auto it = m_metadataCache.find(key);
+ if (it != m_metadataCache.end())
+ {
+ // The only statistics file for countryId + mwmVersion must exist.
+ if (it->second.m_timestamp < baseTimestamp)
+ FileWriter::DeleteFileX(it->second.m_fileName);
+ else
+ FileWriter::DeleteFileX(fileName);
+ }
+ m_metadataCache[key] = Metadata(fileName, baseTimestamp);
+ }
+ catch (Reader::Exception const & ex)
+ {
+ LOG(LWARNING, ("Error reading file:", fileName, ex.Msg()));
+ }
+}
+
+void Statistics::BalanceMemory()
+{
+ std::map<MetadataKey, uint64_t> sizeInBytes;
+ uint64_t totalSize = 0;
+ for (auto const & metadata : m_metadataCache)
+ {
+ FileReader reader(metadata.second.m_fileName);
+ sizeInBytes[metadata.first] = reader.Size();
+ totalSize += reader.Size();
+ }
+
+ if (totalSize < kMaxFilesSizeInBytes)
+ return;
+
+ auto constexpr kPackedDataSize =
+ sizeof(PackedData::m_featureIndex) + sizeof(PackedData::m_seconds) +
+ sizeof(PackedData::m_accuracy) + sizeof(PackedData::m_mercator) +
+ sizeof(PackedData::m_zoomLevel) + sizeof(PackedData::m_eventType);
+ for (auto const & metadata : sizeInBytes)
+ {
+ auto const disposingSize = static_cast<uint64_t>(metadata.second * kEventsDisposingRate);
+ auto const disposingCount = disposingSize / kPackedDataSize;
+
+ std::string fileName = m_metadataCache[metadata.first].m_fileName;
+ std::list<Event> events = ReadEvents(fileName);
+ m_metadataCache.erase(metadata.first);
+ FileWriter::DeleteFileX(fileName);
+ if (events.size() <= disposingCount)
+ continue;
+
+ events.sort();
+ auto it = events.begin();
+ std::advance(it, static_cast<size_t>(disposingCount));
+ events.erase(events.begin(), it);
+
+ std::string fileNameToRebuild;
+ WriteEvents(events, fileNameToRebuild);
+ ASSERT(fileNameToRebuild.empty(), ());
+ }
+}
+
+void Statistics::SetUserId(std::string const & userId)
+{
+ std::lock_guard<std::mutex> lock(m_mutex);
+ m_userId = userId;
+}
+
+std::list<Event> Statistics::ReadEventsForTesting(std::string const & fileName)
+{
+ return ReadEvents(GetPath(fileName));
+}
+
+void Statistics::ProcessEventsForTesting(std::list<Event> const & events)
+{
+ std::list<Event> mutableEvents = events;
+ ProcessEvents(mutableEvents);
+}
+
+void Statistics::CleanupAfterTesting()
+{
+ std::string const statsFolder = StatisticsFolder();
+ if (GetPlatform().IsFileExistsByFullPath(statsFolder))
+ GetPlatform().RmDirRecursively(statsFolder);
+}
+} // namespace local_ads
diff --git a/local_ads/statistics.hpp b/local_ads/statistics.hpp
new file mode 100644
index 0000000000..27f1a5eb52
--- /dev/null
+++ b/local_ads/statistics.hpp
@@ -0,0 +1,85 @@
+#pragma once
+
+#include "local_ads/event.hpp"
+
+#include "base/thread.hpp"
+
+#include <chrono>
+#include <list>
+#include <map>
+#include <mutex>
+#include <string>
+#include <vector>
+
+namespace local_ads
+{
+class Statistics final
+{
+public:
+ struct PackedData
+ {
+ int64_t m_mercator = 0;
+ uint32_t m_featureIndex = 0;
+ uint32_t m_seconds = 0;
+ uint16_t m_accuracy = 0;
+ uint8_t m_eventType = 0;
+ uint8_t m_zoomLevel = 0;
+ };
+
+ Statistics() = default;
+ ~Statistics();
+
+ void Startup();
+ void Teardown();
+
+ void SetUserId(std::string const & userId);
+
+ void RegisterEvent(Event && event);
+ void RegisterEvents(std::list<Event> && events);
+
+ std::list<Event> WriteEventsForTesting(std::list<Event> const & events,
+ std::string & fileNameToRebuild);
+ std::list<Event> ReadEventsForTesting(std::string const & fileName);
+ void ProcessEventsForTesting(std::list<Event> const & events);
+ void CleanupAfterTesting();
+
+private:
+ void ThreadRoutine();
+ bool RequestEvents(std::list<Event> & events, bool & needToSend);
+
+ void IndexMetadata();
+ void ExtractMetadata(std::string const & fileName);
+ void BalanceMemory();
+
+ std::list<Event> WriteEvents(std::list<Event> & events, std::string & fileNameToRebuild);
+ std::list<Event> ReadEvents(std::string const & fileName) const;
+ void ProcessEvents(std::list<Event> & events);
+
+ void SendToServer();
+ std::vector<uint8_t> SerializeForServer(std::list<Event> const & events) const;
+
+ using MetadataKey = std::pair<std::string, int64_t>;
+ struct Metadata
+ {
+ std::string m_fileName;
+ Timestamp m_timestamp;
+
+ Metadata() = default;
+ Metadata(std::string const & fileName, Timestamp const & timestamp)
+ : m_fileName(fileName), m_timestamp(timestamp)
+ {
+ }
+ };
+ std::map<MetadataKey, Metadata> m_metadataCache;
+ Timestamp m_lastSending;
+
+ std::string m_userId;
+
+ bool m_isRunning = false;
+ std::list<Event> m_events;
+
+ std::condition_variable m_condition;
+ std::mutex m_mutex;
+ threads::SimpleThread m_thread;
+};
+} // namespace local_ads
diff --git a/map/framework.cpp b/map/framework.cpp
index 1218330628..0af8cbc9ff 100644
--- a/map/framework.cpp
+++ b/map/framework.cpp
@@ -437,7 +437,10 @@ Framework::Framework(FrameworkParams const & params)
, m_lastReportedCountry(kInvalidCountryId)
{
if (!params.m_disableLocalAds)
+ {
m_localAdsManager.Startup();
+ m_localAdsManager.GetStatistics().SetUserId(GetPlatform().UniqueClientId());
+ }
m_startBackgroundTime = my::Timer::LocalTime();
@@ -1783,11 +1786,26 @@ void Framework::CreateDrapeEngine(ref_ptr<dp::OGLContextFactory> contextFactory,
});
};
- auto overlaysShowStatsFn = [](std::list<df::OverlayShowEvent> && events)
+ auto overlaysShowStatsFn = [this](std::list<df::OverlayShowEvent> && events)
{
- // TODO: implement sending events. This callback is called on a render thread,
- // so placing here not lightweight code is strictly prohibited! The best option is
- // redirection events to another thread.
+ if (events.empty())
+ return;
+
+ std::list<local_ads::Event> statEvents;
+ for (auto const & event : events)
+ {
+ auto const & mwmInfo = event.m_feature.m_mwmId.GetInfo();
+ if (!mwmInfo)
+ continue;
+
+ statEvents.emplace_back(local_ads::EventType::ShowPoint,
+ mwmInfo->GetVersion(), mwmInfo->GetCountryName(),
+ event.m_feature.m_index, event.m_zoomLevel, event.m_timestamp,
+ MercatorBounds::YToLat(event.m_myPosition.y),
+ MercatorBounds::XToLon(event.m_myPosition.x),
+ static_cast<uint16_t>(event.m_gpsAccuracy));
+ }
+ m_localAdsManager.GetStatistics().RegisterEvents(std::move(statEvents));
};
auto isCountryLoadedByNameFn = bind(&Framework::IsCountryLoadedByName, this, _1);
diff --git a/map/local_ads_manager.cpp b/map/local_ads_manager.cpp
index 150bddfdde..8b0bae5c0c 100644
--- a/map/local_ads_manager.cpp
+++ b/map/local_ads_manager.cpp
@@ -1,7 +1,7 @@
#include "map/local_ads_manager.hpp"
#include "local_ads/campaign_serialization.hpp"
-#include "local_ads/local_ads_helpers.hpp"
+#include "local_ads/file_helpers.hpp"
#include "drape_frontend/drape_engine.hpp"
#include "drape_frontend/visual_params.hpp"
@@ -70,6 +70,8 @@ void LocalAdsManager::Startup()
m_isRunning = true;
}
m_thread = threads::SimpleThread(&LocalAdsManager::ThreadRoutine, this);
+
+ m_statistics.Startup();
}
void LocalAdsManager::Teardown()
@@ -82,6 +84,8 @@ void LocalAdsManager::Teardown()
}
m_condition.notify_one();
m_thread.join();
+
+ m_statistics.Teardown();
}
void LocalAdsManager::SetDrapeEngine(ref_ptr<df::DrapeEngine> engine)
diff --git a/map/local_ads_manager.hpp b/map/local_ads_manager.hpp
index 5978646481..75fbfbbfda 100644
--- a/map/local_ads_manager.hpp
+++ b/map/local_ads_manager.hpp
@@ -1,5 +1,7 @@
#pragma once
+#include "local_ads/statistics.hpp"
+
#include "drape_frontend/custom_symbol.hpp"
#include "drape/pointers.hpp"
@@ -28,7 +30,7 @@ class LocalAdsManager final
public:
using GetMwmsByRectFn = function<std::vector<MwmSet::MwmId>(m2::RectD const &)>;
using GetMwmIdByName = function<MwmSet::MwmId(std::string const &)>;
- using Timestamp = std::chrono::steady_clock::time_point;
+ using Timestamp = local_ads::Timestamp;
LocalAdsManager(GetMwmsByRectFn const & getMwmsByRectFn, GetMwmIdByName const & getMwmIdByName);
LocalAdsManager(LocalAdsManager && /* localAdsManager */) = default;
@@ -44,6 +46,8 @@ public:
void Invalidate();
+ local_ads::Statistics & GetStatistics() { return m_statistics; }
+ local_ads::Statistics const & GetStatistics() const { return m_statistics; }
private:
enum class RequestType
{
@@ -86,4 +90,6 @@ private:
std::vector<Request> m_requestedCampaigns;
std::mutex m_mutex;
threads::SimpleThread m_thread;
+
+ local_ads::Statistics m_statistics;
};
diff --git a/qt/qt_common/map_widget.cpp b/qt/qt_common/map_widget.cpp
index 13f45cecf2..a92c38b80a 100644
--- a/qt/qt_common/map_widget.cpp
+++ b/qt/qt_common/map_widget.cpp
@@ -56,7 +56,10 @@ MapWidget::MapWidget(Framework & framework, QWidget * parent)
MapWidget::~MapWidget()
{
m_framework.EnterBackground();
+ m_framework.SetRenderingDisabled(true);
+ m_contextFactory->PrepareToShutdown();
m_framework.DestroyDrapeEngine();
+ m_contextFactory.reset();
}
void MapWidget::BindHotkeys(QWidget & parent)
diff --git a/qt/qt_common/qtoglcontextfactory.cpp b/qt/qt_common/qtoglcontextfactory.cpp
index e3e1fd6c51..cf16bb3850 100644
--- a/qt/qt_common/qtoglcontextfactory.cpp
+++ b/qt/qt_common/qtoglcontextfactory.cpp
@@ -22,9 +22,14 @@ QtOGLContextFactory::~QtOGLContextFactory()
m_uploadSurface->destroy();
}
+void QtOGLContextFactory::PrepareToShutdown()
+{
+ m_preparedToShutdown = true;
+}
+
bool QtOGLContextFactory::LockFrame()
{
- if (!m_drawContext)
+ if (m_preparedToShutdown || !m_drawContext)
return false;
m_drawContext->lockFrame();
diff --git a/qt/qt_common/qtoglcontextfactory.hpp b/qt/qt_common/qtoglcontextfactory.hpp
index 918be36b81..21c133d229 100644
--- a/qt/qt_common/qtoglcontextfactory.hpp
+++ b/qt/qt_common/qtoglcontextfactory.hpp
@@ -18,6 +18,8 @@ public:
QtOGLContextFactory(QOpenGLContext * rootContext);
~QtOGLContextFactory() override;
+ void PrepareToShutdown();
+
bool LockFrame();
GLuint GetTextureHandle() const;
QRectF const & GetTexRect() const;
@@ -37,6 +39,7 @@ private:
std::unique_ptr<QOffscreenSurface> m_drawSurface;
std::unique_ptr<QtUploadOGLContext> m_uploadContext;
std::unique_ptr<QOffscreenSurface> m_uploadSurface;
+ bool m_preparedToShutdown = false;
};
} // namespace common
} // namespace qt
diff --git a/xcode/local_ads/local_ads.xcodeproj/project.pbxproj b/xcode/local_ads/local_ads.xcodeproj/project.pbxproj
index 965c61216e..00c49cb0f7 100644
--- a/xcode/local_ads/local_ads.xcodeproj/project.pbxproj
+++ b/xcode/local_ads/local_ads.xcodeproj/project.pbxproj
@@ -7,9 +7,15 @@
objects = {
/* Begin PBXBuildFile section */
- 45812AAF1E977D2200D7D3B3 /* local_ads_helpers.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 45812AAD1E977D2200D7D3B3 /* local_ads_helpers.cpp */; };
- 45812AB01E977D2200D7D3B3 /* local_ads_helpers.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 45812AAE1E977D2200D7D3B3 /* local_ads_helpers.hpp */; };
- 45812AB31E977D4500D7D3B3 /* local_ads_helpers_tests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 45812AB11E977D4000D7D3B3 /* local_ads_helpers_tests.cpp */; };
+ 455C5DA51E97EBAC00DBFE48 /* file_helpers_tests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 455C5DA11E97EBA200DBFE48 /* file_helpers_tests.cpp */; };
+ 455C5DA61E97EBAF00DBFE48 /* statistics_tests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 455C5DA21E97EBA200DBFE48 /* statistics_tests.cpp */; };
+ 455C5DAD1E97EBC300DBFE48 /* event.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 455C5DA71E97EBC300DBFE48 /* event.cpp */; };
+ 455C5DAE1E97EBC300DBFE48 /* event.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 455C5DA81E97EBC300DBFE48 /* event.hpp */; };
+ 455C5DAF1E97EBC300DBFE48 /* file_helpers.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 455C5DA91E97EBC300DBFE48 /* file_helpers.cpp */; };
+ 455C5DB01E97EBC300DBFE48 /* file_helpers.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 455C5DAA1E97EBC300DBFE48 /* file_helpers.hpp */; };
+ 455C5DB11E97EBC300DBFE48 /* statistics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 455C5DAB1E97EBC300DBFE48 /* statistics.cpp */; };
+ 455C5DB21E97EBC300DBFE48 /* statistics.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 455C5DAC1E97EBC300DBFE48 /* statistics.hpp */; };
+ 4580DAC61E9D2C3D00E8BCDE /* libgeometry.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4580DAC51E9D2C3D00E8BCDE /* libgeometry.a */; };
45812AB51E9781D500D7D3B3 /* libplatform_tests_support.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 45812AB41E9781D500D7D3B3 /* libplatform_tests_support.a */; };
45FFD6571E965E0600DB854E /* campaign_serialization.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 45FFD6541E965E0600DB854E /* campaign_serialization.cpp */; };
45FFD6581E965E0600DB854E /* campaign_serialization.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 45FFD6551E965E0600DB854E /* campaign_serialization.hpp */; };
@@ -25,9 +31,15 @@
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
- 45812AAD1E977D2200D7D3B3 /* local_ads_helpers.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = local_ads_helpers.cpp; sourceTree = "<group>"; };
- 45812AAE1E977D2200D7D3B3 /* local_ads_helpers.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = local_ads_helpers.hpp; sourceTree = "<group>"; };
- 45812AB11E977D4000D7D3B3 /* local_ads_helpers_tests.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = local_ads_helpers_tests.cpp; sourceTree = "<group>"; };
+ 455C5DA11E97EBA200DBFE48 /* file_helpers_tests.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = file_helpers_tests.cpp; sourceTree = "<group>"; };
+ 455C5DA21E97EBA200DBFE48 /* statistics_tests.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = statistics_tests.cpp; sourceTree = "<group>"; };
+ 455C5DA71E97EBC300DBFE48 /* event.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = event.cpp; sourceTree = "<group>"; };
+ 455C5DA81E97EBC300DBFE48 /* event.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = event.hpp; sourceTree = "<group>"; };
+ 455C5DA91E97EBC300DBFE48 /* file_helpers.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = file_helpers.cpp; sourceTree = "<group>"; };
+ 455C5DAA1E97EBC300DBFE48 /* file_helpers.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = file_helpers.hpp; sourceTree = "<group>"; };
+ 455C5DAB1E97EBC300DBFE48 /* statistics.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = statistics.cpp; sourceTree = "<group>"; };
+ 455C5DAC1E97EBC300DBFE48 /* statistics.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = statistics.hpp; sourceTree = "<group>"; };
+ 4580DAC51E9D2C3D00E8BCDE /* libgeometry.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgeometry.a; path = "../../../omim-build/xcode/Debug-iphonesimulator/libgeometry.a"; sourceTree = "<group>"; };
45812AB41E9781D500D7D3B3 /* libplatform_tests_support.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libplatform_tests_support.a; path = "../../../omim-build/xcode/Debug-iphonesimulator/libplatform_tests_support.a"; sourceTree = "<group>"; };
45FFD6461E965DBB00DB854E /* liblocal_ads.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = liblocal_ads.a; sourceTree = BUILT_PRODUCTS_DIR; };
45FFD6541E965E0600DB854E /* campaign_serialization.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = campaign_serialization.cpp; sourceTree = "<group>"; };
@@ -57,6 +69,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 4580DAC61E9D2C3D00E8BCDE /* libgeometry.a in Frameworks */,
45FFD68A1E96639400DB854E /* libz.tbd in Frameworks */,
45FFD6881E96637C00DB854E /* libalohalitics.a in Frameworks */,
45FFD6861E96636A00DB854E /* libcoding.a in Frameworks */,
@@ -96,9 +109,13 @@
children = (
45FFD6541E965E0600DB854E /* campaign_serialization.cpp */,
45FFD6551E965E0600DB854E /* campaign_serialization.hpp */,
- 45812AAD1E977D2200D7D3B3 /* local_ads_helpers.cpp */,
- 45812AAE1E977D2200D7D3B3 /* local_ads_helpers.hpp */,
45FFD6561E965E0600DB854E /* campaign.hpp */,
+ 455C5DA71E97EBC300DBFE48 /* event.cpp */,
+ 455C5DA81E97EBC300DBFE48 /* event.hpp */,
+ 455C5DA91E97EBC300DBFE48 /* file_helpers.cpp */,
+ 455C5DAA1E97EBC300DBFE48 /* file_helpers.hpp */,
+ 455C5DAB1E97EBC300DBFE48 /* statistics.cpp */,
+ 455C5DAC1E97EBC300DBFE48 /* statistics.hpp */,
);
name = local_ads;
path = ../../local_ads;
@@ -109,7 +126,8 @@
children = (
45FFD67D1E96633300DB854E /* testingmain.cpp */,
45FFD6791E965F3C00DB854E /* campaign_serialization_test.cpp */,
- 45812AB11E977D4000D7D3B3 /* local_ads_helpers_tests.cpp */,
+ 455C5DA11E97EBA200DBFE48 /* file_helpers_tests.cpp */,
+ 455C5DA21E97EBA200DBFE48 /* statistics_tests.cpp */,
);
name = local_ads_tests;
path = ../../local_ads/local_ads_tests;
@@ -118,6 +136,7 @@
45FFD6801E96634B00DB854E /* Frameworks */ = {
isa = PBXGroup;
children = (
+ 4580DAC51E9D2C3D00E8BCDE /* libgeometry.a */,
45812AB41E9781D500D7D3B3 /* libplatform_tests_support.a */,
45FFD6891E96639400DB854E /* libz.tbd */,
45FFD6871E96637C00DB854E /* libalohalitics.a */,
@@ -135,9 +154,11 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
+ 455C5DB01E97EBC300DBFE48 /* file_helpers.hpp in Headers */,
45FFD6591E965E0600DB854E /* campaign.hpp in Headers */,
+ 455C5DAE1E97EBC300DBFE48 /* event.hpp in Headers */,
45FFD6581E965E0600DB854E /* campaign_serialization.hpp in Headers */,
- 45812AB01E977D2200D7D3B3 /* local_ads_helpers.hpp in Headers */,
+ 455C5DB21E97EBC300DBFE48 /* statistics.hpp in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -231,8 +252,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 45812AAF1E977D2200D7D3B3 /* local_ads_helpers.cpp in Sources */,
+ 455C5DAD1E97EBC300DBFE48 /* event.cpp in Sources */,
45FFD6571E965E0600DB854E /* campaign_serialization.cpp in Sources */,
+ 455C5DB11E97EBC300DBFE48 /* statistics.cpp in Sources */,
+ 455C5DAF1E97EBC300DBFE48 /* file_helpers.cpp in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -240,9 +263,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 45812AB31E977D4500D7D3B3 /* local_ads_helpers_tests.cpp in Sources */,
45FFD67C1E96630B00DB854E /* campaign_serialization_test.cpp in Sources */,
45FFD67F1E96634100DB854E /* testingmain.cpp in Sources */,
+ 455C5DA61E97EBAF00DBFE48 /* statistics_tests.cpp in Sources */,
+ 455C5DA51E97EBAC00DBFE48 /* file_helpers_tests.cpp in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};