From 1783c907141b3d34971373c641b4a85c2c7724a3 Mon Sep 17 00:00:00 2001
From: Kiryl Kaveryn <kirylkaveryn@gmail.com>
Date: Mon, 17 Feb 2025 16:35:00 +0400
Subject: [PATCH] [map] move the track stats calc logic from ElevationInfo to
 the TrackStatistics

Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
---
 .../CoreApi/Bookmarks/TrackInfo+Core.h        |   8 +-
 iphone/CoreApi/CoreApi/Bookmarks/TrackInfo.mm |  31 +--
 .../CoreApi/Framework/MWMFrameworkHelper.mm   |   4 +-
 .../Common/PlacePageTrackData.mm              |   4 +-
 map/CMakeLists.txt                            |   2 +
 map/elevation_info.cpp                        |  92 +++++----
 map/elevation_info.hpp                        |  22 +--
 map/framework.cpp                             |   6 +-
 map/framework.hpp                             |   6 +-
 map/gps_track.cpp                             |   8 +-
 map/gps_track.hpp                             |   4 +-
 map/gps_track_collection.cpp                  |  27 +--
 map/gps_track_collection.hpp                  |  18 +-
 map/gps_tracker.cpp                           |   5 +
 map/gps_tracker.hpp                           |   4 +-
 map/map_tests/CMakeLists.txt                  |   1 +
 map/map_tests/elevation_info_tests.cpp        |  89 ++++-----
 map/map_tests/track_statistics_tests.cpp      | 185 ++++++++++++++++++
 map/track.cpp                                 |  46 +----
 map/track.hpp                                 |   3 +
 map/track_statistics.cpp                      | 108 ++++++++++
 map/track_statistics.hpp                      |  29 +++
 xcode/map/map.xcodeproj/project.pbxproj       |  12 ++
 23 files changed, 484 insertions(+), 230 deletions(-)
 create mode 100644 map/map_tests/track_statistics_tests.cpp
 create mode 100644 map/track_statistics.cpp
 create mode 100644 map/track_statistics.hpp

diff --git a/iphone/CoreApi/CoreApi/Bookmarks/TrackInfo+Core.h b/iphone/CoreApi/CoreApi/Bookmarks/TrackInfo+Core.h
index ea22fc34b5..a68ce30cbc 100644
--- a/iphone/CoreApi/CoreApi/Bookmarks/TrackInfo+Core.h
+++ b/iphone/CoreApi/CoreApi/Bookmarks/TrackInfo+Core.h
@@ -1,14 +1,10 @@
 #import "TrackInfo.h"
 
 #include <CoreApi/Framework.h>
-#include "map/gps_track_collection.hpp"
-#include "map/elevation_info.hpp"
+#include "map/track_statistics.hpp"
 
 @interface TrackInfo (Core)
 
-- (instancetype)initWithGpsTrackInfo:(GpsTrackInfo const &)info;
-- (instancetype)initWithDistance:(double)distance duration:(double)duration;
-
-- (void)setElevationInfo:(ElevationInfo const &)elevationInfo;
+- (instancetype)initWithTrackStatistics:(TrackStatistics const &)statistics;
 
 @end
diff --git a/iphone/CoreApi/CoreApi/Bookmarks/TrackInfo.mm b/iphone/CoreApi/CoreApi/Bookmarks/TrackInfo.mm
index 592fd73793..a6fa375979 100644
--- a/iphone/CoreApi/CoreApi/Bookmarks/TrackInfo.mm
+++ b/iphone/CoreApi/CoreApi/Bookmarks/TrackInfo.mm
@@ -3,8 +3,6 @@
 #import "DistanceFormatter.h"
 #import "DurationFormatter.h"
 
-#include "map/elevation_info.hpp"
-
 @implementation TrackInfo
 
 - (BOOL)hasElevationInfo {
@@ -19,31 +17,16 @@
 
 @implementation TrackInfo (Core)
 
-- (instancetype)initWithGpsTrackInfo:(GpsTrackInfo const &)trackInfo {
+- (instancetype)initWithTrackStatistics:(TrackStatistics const &)statistics {
   if (self = [super init]) {
-    _distance = trackInfo.m_length;
-    _duration = trackInfo.m_duration;
-    _ascent = trackInfo.m_ascent;
-    _descent = trackInfo.m_descent;
-    _maxElevation = trackInfo.m_maxElevation;
-    _minElevation = trackInfo.m_minElevation;
+    _distance = statistics.m_length;
+    _duration = statistics.m_duration;
+    _ascent = statistics.m_ascent;
+    _descent = statistics.m_descent;
+    _maxElevation = statistics.m_maxElevation;
+    _minElevation = statistics.m_minElevation;
   }
   return self;
 }
 
-- (instancetype)initWithDistance:(double)distance duration:(double)duration {
-  if (self = [super init]) {
-    _distance = distance;
-    _duration = duration;
-  }
-  return self;
-}
-
-- (void)setElevationInfo:(ElevationInfo const &)elevationInfo {
-  _ascent = elevationInfo.GetAscent();
-  _descent = elevationInfo.GetDescent();
-  _maxElevation = elevationInfo.GetMaxAltitude();
-  _minElevation = elevationInfo.GetMinAltitude();
-}
-
 @end
diff --git a/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.mm b/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.mm
index f71be86ac1..d00c60df64 100644
--- a/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.mm
+++ b/iphone/CoreApi/CoreApi/Framework/MWMFrameworkHelper.mm
@@ -224,8 +224,8 @@ static Framework::ProductsPopupCloseReason ConvertProductPopupCloseReasonToCore(
     GetFramework().SetTrackRecordingUpdateHandler(nullptr);
     return;
   }
-  GetFramework().SetTrackRecordingUpdateHandler([trackRecordingDidUpdate](GpsTrackInfo const & gpsTrackInfo) {
-    TrackInfo * info = [[TrackInfo alloc] initWithGpsTrackInfo:gpsTrackInfo];
+  GetFramework().SetTrackRecordingUpdateHandler([trackRecordingDidUpdate](TrackStatistics const & statistics) {
+    TrackInfo * info = [[TrackInfo alloc] initWithTrackStatistics:statistics];
     trackRecordingDidUpdate(info);
   });
 }
diff --git a/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageTrackData.mm b/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageTrackData.mm
index c75681e772..ad421d8f16 100644
--- a/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageTrackData.mm
+++ b/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageTrackData.mm
@@ -12,11 +12,9 @@
   self = [super init];
   if (self) {
     _trackId = track.GetData().m_id;
-    _trackInfo = [[TrackInfo alloc] initWithDistance:track.GetLengthMeters()
-                                                     duration:track.GetDurationInSeconds()];
+    _trackInfo = [[TrackInfo alloc] initWithTrackStatistics:track.GetStatistics()];
     auto const & elevationInfo = track.GetElevationInfo();
     if (track.HasAltitudes() && elevationInfo.has_value()) {
-      [_trackInfo setElevationInfo:elevationInfo.value()];
       auto const & bm = GetFramework().GetBookmarkManager();
       _elevationProfileData = [[ElevationProfileData alloc] initWithTrackId:_trackId
                                                               elevationInfo:elevationInfo.value()
diff --git a/map/CMakeLists.txt b/map/CMakeLists.txt
index 2bd9dc7c6a..856e778d9b 100644
--- a/map/CMakeLists.txt
+++ b/map/CMakeLists.txt
@@ -57,6 +57,8 @@ set(SRC
   search_product_info.hpp
   track.cpp
   track.hpp
+  track_statistics.cpp
+  track_statistics.hpp
   track_mark.cpp
   track_mark.hpp
   traffic_manager.cpp
diff --git a/map/elevation_info.cpp b/map/elevation_info.cpp
index d057970b89..481d161db4 100644
--- a/map/elevation_info.cpp
+++ b/map/elevation_info.cpp
@@ -4,54 +4,60 @@
 
 #include "geometry/mercator.hpp"
 
-ElevationInfo::ElevationInfo(kml::MultiGeometry const & geometry)
-{
-  double distance = 0;
-  // Concatenate all segments.
-  for (size_t lineIndex = 0; lineIndex < geometry.m_lines.size(); ++lineIndex)
-  {
-    auto const & line = geometry.m_lines[lineIndex];
-    if (line.empty())
-    {
-      LOG(LWARNING, ("Empty line in elevation info"));
-      continue;
-    }
+using namespace geometry;
+using namespace mercator;
 
-    if (lineIndex == 0)
-    {
-      m_minAltitude = line.front().GetAltitude();
-      m_maxAltitude = m_minAltitude;
-    }
+ElevationInfo::ElevationInfo(std::vector<GeometryLine> const & lines)
+{
+  // Concatenate all segments.
+  for (size_t lineIndex = 0; lineIndex < lines.size(); ++lineIndex)
+  {
+    auto const & line = lines[lineIndex];
+    if (line.empty())
+      continue;
 
     if (lineIndex > 0)
-      m_segmentsDistances.emplace_back(distance);
+      m_segmentsDistances.emplace_back(m_points.back().m_distance);
 
-    for (size_t pointIndex = 0; pointIndex < line.size(); ++pointIndex)
-    {
-      auto const & currentPoint = line[pointIndex];
-      auto const & currentPointAltitude = currentPoint.GetAltitude();
-      if (currentPointAltitude < m_minAltitude)
-        m_minAltitude = currentPointAltitude;
-      if (currentPointAltitude > m_maxAltitude)
-        m_maxAltitude = currentPointAltitude;
-
-      if (pointIndex == 0)
-      {
-        m_points.emplace_back(currentPoint, distance);
-        continue;
-      }
-
-      auto const & previousPoint = line[pointIndex - 1];
-      distance += mercator::DistanceOnEarth(previousPoint.GetPoint(), currentPoint.GetPoint());
-      m_points.emplace_back(currentPoint, distance);
-
-      auto const deltaAltitude = currentPointAltitude - previousPoint.GetAltitude();
-      if (deltaAltitude > 0)
-        m_ascent += deltaAltitude;
-      else
-        m_descent -= deltaAltitude;
-    }
+    AddPoints(line, true /* new segment */);
   }
   /// @todo(KK) Implement difficulty calculation.
   m_difficulty = Difficulty::Unknown;
 }
+
+void ElevationInfo::AddGpsPoints(GpsPoints const & points)
+{
+  GeometryLine line;
+  line.reserve(points.size());
+  for (auto const & point : points)
+    line.emplace_back(FromLatLon(point.m_latitude, point.m_longitude), point.m_altitude);
+  AddPoints(line);
+}
+
+void ElevationInfo::AddPoints(GeometryLine const & line, bool isNewSegment)
+{
+  if (line.empty())
+    return;
+
+  double distance = m_points.empty() ? 0 : m_points.back().m_distance;
+  for (size_t pointIndex = 0; pointIndex < line.size(); ++pointIndex)
+  {
+    auto const & point = line[pointIndex];
+
+    if (m_points.empty())
+    {
+      m_points.emplace_back(point, distance);
+      continue;
+    }
+
+    if (isNewSegment && pointIndex == 0)
+    {
+      m_points.emplace_back(point, distance);
+      continue;
+    }
+
+    auto const & previousPoint = m_points.back().m_point;
+    distance += mercator::DistanceOnEarth(previousPoint.GetPoint(), point.GetPoint());
+    m_points.emplace_back(point, distance);
+  }
+}
diff --git a/map/elevation_info.hpp b/map/elevation_info.hpp
index 9291d8ff28..e80fd2fefb 100644
--- a/map/elevation_info.hpp
+++ b/map/elevation_info.hpp
@@ -5,6 +5,8 @@
 #include "geometry/point_with_altitude.hpp"
 #include "geometry/latlon.hpp"
 
+#include "platform/location.hpp"
+
 #include <cstdint>
 #include <string>
 #include <vector>
@@ -22,6 +24,8 @@ public:
   };
 
   using Points = std::vector<Point>;
+  using GpsPoints = std::vector<location::GpsInfo>;
+  using GeometryLine = kml::MultiGeometry::LineT;
   using SegmentsDistances = std::vector<double>;
 
   enum Difficulty : uint8_t
@@ -33,31 +37,23 @@ public:
   };
 
   ElevationInfo() = default;
-  explicit ElevationInfo(kml::MultiGeometry const & geometry);
+  explicit ElevationInfo(std::vector<GeometryLine> const & lines);
+
+  void AddGpsPoints(GpsPoints const & points);
 
   size_t GetSize() const { return m_points.size(); };
   Points const & GetPoints() const { return m_points; };
-  uint32_t GetAscent() const { return m_ascent; }
-  uint32_t GetDescent() const { return m_descent; }
-  geometry::Altitude GetMinAltitude() const { return m_minAltitude; }
-  geometry::Altitude GetMaxAltitude() const { return m_maxAltitude; }
   uint8_t GetDifficulty() const { return m_difficulty; }
   SegmentsDistances const & GetSegmentsDistances() const { return m_segmentsDistances; };
 
 private:
   // Points with distance from start of the track and altitude.
   Points m_points;
-  // Ascent in meters.
-  uint32_t m_ascent = 0;
-  // Descent in meters.
-  uint32_t m_descent = 0;
-  // Altitude in meters.
-  geometry::Altitude m_minAltitude = 0;
-  // Altitude in meters.
-  geometry::Altitude m_maxAltitude = 0;
   // Some digital difficulty level with value in range [0-kMaxDifficulty]
   // or kInvalidDifficulty when difficulty is not found or incorrect.
   Difficulty m_difficulty = Difficulty::Unknown;
   // Distances to the start of each segment.
   SegmentsDistances m_segmentsDistances;
+
+  void AddPoints(GeometryLine const & line, bool isNewSegment = false);
 };
diff --git a/map/framework.cpp b/map/framework.cpp
index 09ddd25f3e..b20207f3ff 100644
--- a/map/framework.cpp
+++ b/map/framework.cpp
@@ -1736,7 +1736,7 @@ void Framework::SetTrackRecordingUpdateHandler(TrackRecordingUpdateHandler && tr
 {
   m_trackRecordingUpdateHandler = std::move(trackRecordingDidUpdate);
   if (m_trackRecordingUpdateHandler)
-    m_trackRecordingUpdateHandler(GpsTracker::Instance().GetTrackInfo());
+    m_trackRecordingUpdateHandler(GpsTracker::Instance().GetTrackStatistics());
 }
 
 void Framework::StopTrackRecording()
@@ -1768,7 +1768,7 @@ bool Framework::IsTrackRecordingEnabled() const
 
 void Framework::OnUpdateGpsTrackPointsCallback(vector<pair<size_t, location::GpsInfo>> && toAdd,
                                                pair<size_t, size_t> const & toRemove,
-                                               GpsTrackInfo const & trackInfo)
+                                               TrackStatistics const & trackStatistics)
 {
   ASSERT(m_drapeEngine.get() != nullptr, ());
 
@@ -1797,7 +1797,7 @@ void Framework::OnUpdateGpsTrackPointsCallback(vector<pair<size_t, location::Gps
   m_drapeEngine->UpdateGpsTrackPoints(std::move(pointsAdd), std::move(indicesRemove));
 
   if (m_trackRecordingUpdateHandler)
-    m_trackRecordingUpdateHandler(trackInfo);
+    m_trackRecordingUpdateHandler(trackStatistics);
 }
 
 void Framework::MarkMapStyle(MapStyle mapStyle)
diff --git a/map/framework.hpp b/map/framework.hpp
index da68948ae9..16dd6a0217 100644
--- a/map/framework.hpp
+++ b/map/framework.hpp
@@ -15,9 +15,9 @@
 #include "map/search_api.hpp"
 #include "map/search_mark.hpp"
 #include "map/track.hpp"
+#include "map/track_statistics.hpp"
 #include "map/traffic_manager.hpp"
 #include "map/transit/transit_reader.hpp"
-#include "map/gps_track_collection.hpp"
 
 #include "drape_frontend/gui/skin.hpp"
 #include "drape_frontend/drape_api.hpp"
@@ -437,7 +437,7 @@ public:
   void ConnectToGpsTracker();
   void DisconnectFromGpsTracker();
 
-  using TrackRecordingUpdateHandler = platform::SafeCallback<void(GpsTrackInfo const & trackInfo)>;
+  using TrackRecordingUpdateHandler = platform::SafeCallback<void(TrackStatistics const & trackStatistics)>;
   void StartTrackRecording();
   void SetTrackRecordingUpdateHandler(TrackRecordingUpdateHandler && trackRecordingDidUpdate);
   void StopTrackRecording();
@@ -468,7 +468,7 @@ private:
 
   void OnUpdateGpsTrackPointsCallback(std::vector<std::pair<size_t, location::GpsInfo>> && toAdd,
                                       std::pair<size_t, size_t> const & toRemove,
-                                      GpsTrackInfo const & trackInfo);
+                                      TrackStatistics const & trackStatistics);
 
   TrackRecordingUpdateHandler m_trackRecordingUpdateHandler;
 
diff --git a/map/gps_track.cpp b/map/gps_track.cpp
index c808753cda..9392263d69 100644
--- a/map/gps_track.cpp
+++ b/map/gps_track.cpp
@@ -79,9 +79,9 @@ void GpsTrack::AddPoints(vector<location::GpsInfo> const & points)
   ScheduleTask();
 }
 
-GpsTrackInfo GpsTrack::GetTrackInfo() const
+TrackStatistics GpsTrack::GetTrackStatistics() const
 {
-  return m_collection ? m_collection->GetTrackInfo() : GpsTrackInfo();
+  return m_collection ? m_collection->GetTrackStatistics() : TrackStatistics();
 }
 
 void GpsTrack::Clear()
@@ -303,7 +303,7 @@ void GpsTrack::NotifyCallback(pair<size_t, size_t> const & addedIds, pair<size_t
     if (toAdd.empty())
       return; // nothing to send
 
-    m_callback(std::move(toAdd), make_pair(kInvalidId, kInvalidId), m_collection->GetTrackInfo());
+    m_callback(std::move(toAdd), make_pair(kInvalidId, kInvalidId), m_collection->GetTrackStatistics());
   }
   else
   {
@@ -324,6 +324,6 @@ void GpsTrack::NotifyCallback(pair<size_t, size_t> const & addedIds, pair<size_t
     if (toAdd.empty() && evictedIds.first == kInvalidId)
       return; // nothing to send
 
-    m_callback(std::move(toAdd), evictedIds, m_collection->GetTrackInfo());
+    m_callback(std::move(toAdd), evictedIds, m_collection->GetTrackStatistics());
   }
 }
diff --git a/map/gps_track.hpp b/map/gps_track.hpp
index 6ba0ba138f..494ab7c0fa 100644
--- a/map/gps_track.hpp
+++ b/map/gps_track.hpp
@@ -31,7 +31,7 @@ public:
   void AddPoints(std::vector<location::GpsInfo> const & points);
 
   /// Returns track statistics
-  GpsTrackInfo GetTrackInfo() const;
+  TrackStatistics GetTrackStatistics() const;
 
   /// Clears any previous tracking info
   /// @note Callback is called with 'toRemove' points, if some points were removed.
@@ -47,7 +47,7 @@ public:
   using TGpsTrackDiffCallback =
       std::function<void(std::vector<std::pair<size_t, location::GpsInfo>> && toAdd,
                          std::pair<size_t, size_t> const & toRemove,
-                         GpsTrackInfo const & trackInfo)>;
+                         TrackStatistics const & trackStatistics)>;
 
   /// Sets callback on change of gps track.
   /// @param callback - callback callable object
diff --git a/map/gps_track_collection.cpp b/map/gps_track_collection.cpp
index f39c2e964c..a141a30b78 100644
--- a/map/gps_track_collection.cpp
+++ b/map/gps_track_collection.cpp
@@ -2,8 +2,6 @@
 
 #include "base/assert.hpp"
 
-#include "geometry/distance_on_sphere.hpp"
-
 #include <algorithm>
 
 namespace
@@ -35,7 +33,7 @@ size_t const GpsTrackCollection::kInvalidId = std::numeric_limits<size_t>::max()
 
 GpsTrackCollection::GpsTrackCollection()
   : m_lastId(0)
-  , m_trackInfo(GpsTrackInfo())
+  , m_statistics(TrackStatistics())
 {}
 
 std::pair<size_t, size_t> GpsTrackCollection::Add(std::vector<TItem> const & items)
@@ -51,26 +49,7 @@ std::pair<size_t, size_t> GpsTrackCollection::Add(std::vector<TItem> const & ite
     if (!m_items.empty() && m_items.back().m_timestamp > item.m_timestamp)
       continue;
 
-    if (m_items.empty())
-    {
-      m_trackInfo.m_maxElevation = item.m_altitude;
-      m_trackInfo.m_minElevation = item.m_altitude;
-    }
-    else
-    {
-      auto const & lastItem = m_items.back();
-      m_trackInfo.m_length += ms::DistanceOnEarth(lastItem.GetLatLon(), item.GetLatLon());
-      m_trackInfo.m_duration = item.m_timestamp - m_items.front().m_timestamp;
-
-      auto const deltaAltitude = item.m_altitude - lastItem.m_altitude;
-      if (item.m_altitude > lastItem.m_altitude)
-        m_trackInfo.m_ascent += deltaAltitude;
-      if (item.m_altitude < lastItem.m_altitude)
-        m_trackInfo.m_descent -= deltaAltitude;
-
-      m_trackInfo.m_maxElevation = std::max(static_cast<double>(m_trackInfo.m_maxElevation), item.m_altitude);
-      m_trackInfo.m_minElevation = std::min(static_cast<double>(m_trackInfo.m_minElevation), item.m_altitude);
-    }
+    m_statistics.AddGpsInfoPoint(item);
 
     m_items.emplace_back(item);
     ++added;
@@ -106,7 +85,7 @@ std::pair<size_t, size_t> GpsTrackCollection::Clear(bool resetIds)
 
   m_items.clear();
   m_items.shrink_to_fit();
-  m_trackInfo = {};
+  m_statistics = {};
 
   if (resetIds)
     m_lastId = 0;
diff --git a/map/gps_track_collection.hpp b/map/gps_track_collection.hpp
index 140459e729..f1e91dc87f 100644
--- a/map/gps_track_collection.hpp
+++ b/map/gps_track_collection.hpp
@@ -2,21 +2,14 @@
 
 #include "platform/location.hpp"
 
+#include "map/track_statistics.hpp"
+#include "map/elevation_info.hpp"
+
 #include <deque>
 #include <limits>
 #include <utility>
 #include <vector>
 
-struct GpsTrackInfo
-{
-  double m_length;
-  double m_duration;
-  uint32_t m_ascent;
-  uint32_t m_descent;
-  int16_t m_minElevation;
-  int16_t m_maxElevation;
-};
-
 class GpsTrackCollection final
 {
 public:
@@ -46,7 +39,8 @@ public:
   /// Returns number of items in the collection
   size_t GetSize() const;
 
-  GpsTrackInfo GetTrackInfo() const { return m_trackInfo; }
+  /// Returns track statistics.
+  const TrackStatistics GetTrackStatistics() const { return m_statistics; }
 
   /// Enumerates items in the collection.
   /// @param f - callable object, which is called with params - item and item id,
@@ -72,5 +66,5 @@ private:
   std::deque<TItem> m_items;  // asc. sorted by timestamp
 
   size_t m_lastId;
-  GpsTrackInfo m_trackInfo;
+  TrackStatistics m_statistics;
 };
diff --git a/map/gps_tracker.cpp b/map/gps_tracker.cpp
index 2d33dbcf8d..23080815ed 100644
--- a/map/gps_tracker.cpp
+++ b/map/gps_tracker.cpp
@@ -75,6 +75,11 @@ size_t GpsTracker::GetTrackSize() const
   return m_track.GetSize();
 }
 
+TrackStatistics GpsTracker::GetTrackStatistics() const
+{
+  return m_track.GetTrackStatistics();
+}
+
 void GpsTracker::Connect(TGpsTrackDiffCallback const & fn)
 {
   m_track.SetCallback(fn);
diff --git a/map/gps_tracker.hpp b/map/gps_tracker.hpp
index 38ff7915c2..8cce4eab0f 100644
--- a/map/gps_tracker.hpp
+++ b/map/gps_tracker.hpp
@@ -17,12 +17,12 @@ public:
 
   bool IsEmpty() const;
   size_t GetTrackSize() const;
-  GpsTrackInfo GetTrackInfo() const { return m_track.GetTrackInfo(); }
+  TrackStatistics GetTrackStatistics() const;
 
   using TGpsTrackDiffCallback =
       std::function<void(std::vector<std::pair<size_t, location::GpsInfo>> && toAdd,
                          std::pair<size_t, size_t> const & toRemove,
-                         GpsTrackInfo const & trackInfo)>;
+                         TrackStatistics const & trackStatistics)>;
 
   void Connect(TGpsTrackDiffCallback const & fn);
   void Disconnect();
diff --git a/map/map_tests/CMakeLists.txt b/map/map_tests/CMakeLists.txt
index c79a222283..fcfc0b46f3 100644
--- a/map/map_tests/CMakeLists.txt
+++ b/map/map_tests/CMakeLists.txt
@@ -18,6 +18,7 @@ set(SRC
   transliteration_test.cpp
   working_time_tests.cpp
   elevation_info_tests.cpp
+  track_statistics_tests.cpp
 )
 
 omim_add_test(${PROJECT_NAME} ${SRC} REQUIRE_QT REQUIRE_SERVER)
diff --git a/map/map_tests/elevation_info_tests.cpp b/map/map_tests/elevation_info_tests.cpp
index 9188c1065b..fdd138bf6c 100644
--- a/map/map_tests/elevation_info_tests.cpp
+++ b/map/map_tests/elevation_info_tests.cpp
@@ -7,18 +7,24 @@
 
 #include "kml/types.hpp"
 
-namespace geometry
+namespace elevation_info_tests
 {
 using namespace geometry;
+using namespace location;
+
+GpsInfo const BuildGpsInfo(double latitude, double longitude, double altitude)
+{
+  GpsInfo gpsInfo;
+  gpsInfo.m_latitude = latitude;
+  gpsInfo.m_longitude = longitude;
+  gpsInfo.m_altitude = altitude;
+  return gpsInfo;
+}
 
 UNIT_TEST(ElevationInfo_EmptyMultiGeometry)
 {
   ElevationInfo ei;
   TEST_EQUAL(0, ei.GetSize(), ());
-  TEST_EQUAL(0, ei.GetAscent(), ());
-  TEST_EQUAL(0, ei.GetDescent(), ());
-  TEST_EQUAL(ei.GetMinAltitude(), kDefaultAltitudeMeters, ());
-  TEST_EQUAL(ei.GetMaxAltitude(), kDefaultAltitudeMeters, ());
 }
 
 UNIT_TEST(ElevationInfo_FromMultiGeometry)
@@ -32,13 +38,9 @@ UNIT_TEST(ElevationInfo_FromMultiGeometry)
     point2,
     point3
   });
-  ElevationInfo ei(geometry);
+  ElevationInfo ei(geometry.m_lines);
 
   TEST_EQUAL(3, ei.GetSize(), ());
-  TEST_EQUAL(ei.GetMinAltitude(), 50, ());
-  TEST_EQUAL(ei.GetMaxAltitude(), 150, ());
-  TEST_EQUAL(ei.GetAscent(), 50, ()); // Ascent from 100 -> 150
-  TEST_EQUAL(ei.GetDescent(), 100, ()); // Descent from 150 -> 50
 
   double distance = 0;
   TEST_EQUAL(ei.GetPoints()[0].m_distance, distance, ());
@@ -48,23 +50,6 @@ UNIT_TEST(ElevationInfo_FromMultiGeometry)
   TEST_EQUAL(ei.GetPoints()[2].m_distance, distance, ());
 }
 
-UNIT_TEST(ElevationInfo_NoAltitudePoints)
-{
-  kml::MultiGeometry geometry;
-  geometry.AddLine({
-    PointWithAltitude({0.0, 0.0}),
-    PointWithAltitude({1.0, 1.0}),
-    PointWithAltitude({2.0, 2.0})
-  });
-  ElevationInfo ei(geometry);
-
-  TEST_EQUAL(3, ei.GetSize(), ());
-  TEST_EQUAL(ei.GetMinAltitude(), kDefaultAltitudeMeters, ());
-  TEST_EQUAL(ei.GetMaxAltitude(), kDefaultAltitudeMeters, ());
-  TEST_EQUAL(ei.GetAscent(), 0, ());
-  TEST_EQUAL(ei.GetDescent(), 0, ());
-}
-
 UNIT_TEST(ElevationInfo_MultipleLines)
 {
   kml::MultiGeometry geometry;
@@ -82,32 +67,28 @@ UNIT_TEST(ElevationInfo_MultipleLines)
     PointWithAltitude({4.0, 4.0}, 200),
     PointWithAltitude({5.0, 5.0}, 250)
   });
-  ElevationInfo ei(geometry);
+  ElevationInfo ei(geometry.m_lines);
 
   TEST_EQUAL(8, ei.GetSize(), ());
-  TEST_EQUAL(ei.GetMinAltitude(), 50, ());
-  TEST_EQUAL(ei.GetMaxAltitude(), 250, ());
-  TEST_EQUAL(ei.GetAscent(), 125, ()); // Ascent from 100 -> 150, 50 -> 75, 200 -> 250
-  TEST_EQUAL(ei.GetDescent(), 25, ()); // Descent from 150 -> 140, 75 -> 60
 }
 
 UNIT_TEST(ElevationInfo_SegmentDistances)
 {
   kml::MultiGeometry geometry;
   geometry.AddLine({
-    geometry::PointWithAltitude({0.0, 0.0}),
-    geometry::PointWithAltitude({1.0, 0.0})
+    PointWithAltitude({0.0, 0.0}),
+    PointWithAltitude({1.0, 0.0})
   });
   geometry.AddLine({
-    geometry::PointWithAltitude({2.0, 0.0}),
-    geometry::PointWithAltitude({3.0, 0.0})
+    PointWithAltitude({2.0, 0.0}),
+    PointWithAltitude({3.0, 0.0})
   });
   geometry.AddLine({
-    geometry::PointWithAltitude({4.0, 0.0}),
-    geometry::PointWithAltitude({5.0, 0.0})
+    PointWithAltitude({4.0, 0.0}),
+    PointWithAltitude({5.0, 0.0})
   });
 
-  ElevationInfo ei(geometry);
+  ElevationInfo ei(geometry.m_lines);
   auto const & segmentDistances = ei.GetSegmentsDistances();
   auto const points = ei.GetPoints();
 
@@ -116,21 +97,23 @@ UNIT_TEST(ElevationInfo_SegmentDistances)
   TEST_EQUAL(segmentDistances[1], ei.GetPoints()[4].m_distance, ());
 }
 
-UNIT_TEST(ElevationInfo_PositiveAndNegativeAltitudes)
+UNIT_TEST(ElevationInfo_BuildWithGpsPoints)
 {
-  kml::MultiGeometry geometry;
-  geometry.AddLine({
-    PointWithAltitude({0.0, 0.0}, -10),
-    PointWithAltitude({1.0, 1.0}, 20),
-    PointWithAltitude({2.0, 2.0}, -5),
-    PointWithAltitude({3.0, 3.0}, 15)
+  auto ei = ElevationInfo();
+  ei.AddGpsPoints({
+    BuildGpsInfo(0.0, 0.0, 0),
+    BuildGpsInfo(1.0, 1.0, 50),
+    BuildGpsInfo(2.0, 2.0, 100),
   });
-  ElevationInfo ei(geometry);
+  ei.AddGpsPoints({
+    BuildGpsInfo(3.0, 3.0, -50)
+  });
+  ei.AddGpsPoints({
+    BuildGpsInfo(4.0, 4.0, 0)
+  });
+  ei.AddGpsPoints({});
 
-  TEST_EQUAL(4, ei.GetSize(), ());
-  TEST_EQUAL(ei.GetMinAltitude(), -10, ());
-  TEST_EQUAL(ei.GetMaxAltitude(), 20, ());
-  TEST_EQUAL(ei.GetAscent(), 50, ()); // Ascent from -10 -> 20 and -5 -> 15
-  TEST_EQUAL(ei.GetDescent(), 25, ()); // Descent from 20 -> -5
+  TEST_EQUAL(5, ei.GetSize(), ());
+  TEST_EQUAL(ei.GetSegmentsDistances().size(), 0, ());
 }
-} // namespace geometry
+} // namespace elevation_info_testa
diff --git a/map/map_tests/track_statistics_tests.cpp b/map/map_tests/track_statistics_tests.cpp
new file mode 100644
index 0000000000..1e2b3e0666
--- /dev/null
+++ b/map/map_tests/track_statistics_tests.cpp
@@ -0,0 +1,185 @@
+#include "testing/testing.hpp"
+
+#include "map/track_statistics.hpp"
+
+#include "geometry/point_with_altitude.hpp"
+#include "geometry/mercator.hpp"
+
+#include "kml/types.hpp"
+
+namespace track_statistics_tests
+{
+using namespace geometry;
+using namespace location;
+
+GpsInfo const BuildGpsInfo(double latitude, double longitude, double altitude, double timestamp = 0)
+{
+  GpsInfo gpsInfo;
+  gpsInfo.m_latitude = latitude;
+  gpsInfo.m_longitude = longitude;
+  gpsInfo.m_altitude = altitude;
+  gpsInfo.m_timestamp = timestamp;
+  return gpsInfo;
+}
+
+UNIT_TEST(TrackStatistics_EmptyMultiGeometry)
+{
+  TrackStatistics ts;
+  TEST_EQUAL(0, ts.m_ascent, ());
+  TEST_EQUAL(0, ts.m_descent, ());
+  TEST_EQUAL(ts.m_minElevation, kDefaultAltitudeMeters, ());
+  TEST_EQUAL(ts.m_maxElevation, kDefaultAltitudeMeters, ());
+}
+
+UNIT_TEST(TrackStatistics_FromMultiGeometry)
+{
+  kml::MultiGeometry geometry;
+  auto const point1 = PointWithAltitude({0.0, 0.0}, 100);
+  auto const point2 = PointWithAltitude({1.0, 1.0}, 150);
+  auto const point3 = PointWithAltitude({2.0, 2.0}, 50);
+  geometry.AddLine({
+    point1,
+    point2,
+    point3
+  });
+  geometry.AddTimestamps({
+    0,
+    1,
+    2
+  });
+  auto const ts = TrackStatistics(geometry);
+  TEST_EQUAL(ts.m_minElevation, 50, ());
+  TEST_EQUAL(ts.m_maxElevation, 150, ());
+  TEST_EQUAL(ts.m_ascent, 50, ()); // Ascent from 100 -> 150
+  TEST_EQUAL(ts.m_descent, 100, ()); // Descent from 150 -> 50
+  TEST_EQUAL(ts.m_duration, 2, ());
+
+  double distance = 0;
+  distance += mercator::DistanceOnEarth(point1, point2);
+  distance += mercator::DistanceOnEarth(point2, point3);
+  TEST_EQUAL(ts.m_length, distance, ());
+}
+
+UNIT_TEST(TrackStatistics_NoAltitudeAndTimestampPoints)
+{
+  kml::MultiGeometry geometry;
+  geometry.AddLine({
+    PointWithAltitude({0.0, 0.0}),
+    PointWithAltitude({1.0, 1.0}),
+    PointWithAltitude({2.0, 2.0})
+  });
+
+  auto const ts = TrackStatistics(geometry);
+
+  TEST_EQUAL(ts.m_minElevation, kDefaultAltitudeMeters, ());
+  TEST_EQUAL(ts.m_maxElevation, kDefaultAltitudeMeters, ());
+  TEST_EQUAL(ts.m_ascent, 0, ());
+  TEST_EQUAL(ts.m_descent, 0, ());
+  TEST_EQUAL(ts.m_duration, 0, ());
+}
+
+UNIT_TEST(TrackStatistics_MultipleLines)
+{
+  kml::MultiGeometry geometry;
+  geometry.AddLine({
+    PointWithAltitude({0.0, 0.0}, 100),
+    PointWithAltitude({1.0, 1.0}, 150),
+    PointWithAltitude({1.0, 1.0}, 140)
+  });
+  geometry.AddTimestamps({
+    0,
+    1,
+    2
+  });
+  geometry.AddLine({
+    PointWithAltitude({2.0, 2.0}, 50),
+    PointWithAltitude({3.0, 3.0}, 75),
+    PointWithAltitude({3.0, 3.0}, 60)
+  });
+  geometry.AddTimestamps({
+    0,
+    0,
+    0
+  });
+  geometry.AddLine({
+    PointWithAltitude({4.0, 4.0}, 200),
+    PointWithAltitude({5.0, 5.0}, 250)
+  });
+  geometry.AddTimestamps({
+    4,
+    5
+  });
+  auto const ts = TrackStatistics(geometry);
+
+  TEST_EQUAL(ts.m_minElevation, 50, ());
+  TEST_EQUAL(ts.m_maxElevation, 250, ());
+  TEST_EQUAL(ts.m_ascent, 125, ()); // Ascent from 100 -> 150, 50 -> 75, 200 -> 250
+  TEST_EQUAL(ts.m_descent, 25, ()); // Descent from 150 -> 140, 75 -> 60
+  TEST_EQUAL(ts.m_duration, 3, ());
+}
+
+UNIT_TEST(TrackStatistics_WithGpsPoints)
+{
+  const std::vector<std::vector<GpsInfo>> pointsData = {
+    { BuildGpsInfo(0.0, 0.0, 0, 0),
+      BuildGpsInfo(1.0, 1.0, 50, 1),
+      BuildGpsInfo(2.0, 2.0, 100, 2)
+    },
+    {
+      BuildGpsInfo(3.0, 3.0, -50, 5)
+    },
+    {
+      BuildGpsInfo(4.0, 4.0, 0, 10)
+    }
+  };
+  TrackStatistics ts;
+  for (auto const & pointsList : pointsData)
+  {
+    for (auto const & point : pointsList)
+      ts.AddGpsInfoPoint(point);
+  }
+  TEST_EQUAL(ts.m_minElevation, -50, ());
+  TEST_EQUAL(ts.m_maxElevation, 100, ());
+  TEST_EQUAL(ts.m_ascent, 150, ()); // Ascent from 0 -> 50, 50 -> 100, -50 -> 0
+  TEST_EQUAL(ts.m_descent, 150, ()); // Descent from 100 -> -50
+  TEST_EQUAL(ts.m_duration, 10, ());
+}
+
+UNIT_TEST(TrackStatistics_PositiveAndNegativeAltitudes)
+{
+  kml::MultiGeometry geometry;
+  geometry.AddLine({
+    PointWithAltitude({0.0, 0.0}, -10),
+    PointWithAltitude({1.0, 1.0}, 20),
+    PointWithAltitude({2.0, 2.0}, -5),
+    PointWithAltitude({3.0, 3.0}, 15)
+  });
+  auto const ts = TrackStatistics(geometry);
+
+  TEST_EQUAL(ts.m_minElevation, -10, ());
+  TEST_EQUAL(ts.m_maxElevation, 20, ());
+  TEST_EQUAL(ts.m_ascent, 50, ()); // Ascent from -10 -> 20 and -5 -> 15
+  TEST_EQUAL(ts.m_descent, 25, ()); // Descent from 20 -> -5
+}
+
+UNIT_TEST(TrackStatistics_SmallAltitudeDelta)
+{
+  const std::vector<GpsInfo> points = {
+    BuildGpsInfo(0.0, 0.0, 0),
+    BuildGpsInfo(1.0, 1.0, 0.2),
+    BuildGpsInfo(2.0, 2.0, 0.4),
+    BuildGpsInfo(3.0, 3.0, 0.6),
+    BuildGpsInfo(4.0, 4.0, 0.8),
+    BuildGpsInfo(5.0, 5.0, 1.0)
+  };
+
+  TrackStatistics ts;
+  for (auto const & point : points)
+    ts.AddGpsInfoPoint(point);
+
+  TEST_EQUAL(ts.m_minElevation, 0, ());
+  TEST_EQUAL(ts.m_maxElevation, 1.0, ());
+  TEST_EQUAL(ts.m_ascent, 1.0, ());
+  TEST_EQUAL(ts.m_descent, 0, ());
+}
+} // namespace track_statistics_tests
diff --git a/map/track.cpp b/map/track.cpp
index fcdd167e55..ecae285897 100644
--- a/map/track.cpp
+++ b/map/track.cpp
@@ -8,24 +8,6 @@
 
 #include <utility>
 
-namespace
-{
-double GetLengthInMeters(kml::MultiGeometry::LineT const & points, size_t pointIndex)
-{
-  CHECK_LESS(pointIndex, points.size(), (pointIndex, points.size()));
-
-  double length = 0.0;
-  for (size_t i = 1; i <= pointIndex; ++i)
-  {
-    auto const & pt1 = points[i - 1].GetPoint();
-    auto const & pt2 = points[i].GetPoint();
-    auto const segmentLength = mercator::DistanceOnEarth(pt1, pt2);
-    length += segmentLength;
-  }
-  return length;
-}
-}  // namespace
-
 Track::Track(kml::TrackData && data)
   : Base(data.m_id == kml::kInvalidTrackId ? UserMarkIdStorage::Instance().GetNextTrackId() : data.m_id)
   , m_data(std::move(data))
@@ -120,13 +102,7 @@ m2::RectD Track::GetLimitRect() const
 
 double Track::GetLengthMeters() const
 {
-  if (m_interactionData)
-    return m_interactionData->m_lengths.back().back();
-
-  double len = 0;
-  for (auto const & line : m_data.m_geometry.m_lines)
-    len += GetLengthInMeters(line, line.size() - 1);
-  return len;
+  return GetStatistics().m_length;
 }
 
 double Track::GetLengthMetersImpl(size_t lineIndex, size_t ptIndex) const
@@ -236,25 +212,23 @@ kml::MultiGeometry::LineT Track::GetGeometry() const
   return geometry;
 }
 
+TrackStatistics Track::GetStatistics() const
+{
+  if (!m_trackStatistics.has_value())
+    m_trackStatistics = TrackStatistics(m_data.m_geometry);
+  return m_trackStatistics.value();
+}
+
 std::optional<ElevationInfo> Track::GetElevationInfo() const
 {
   if (!HasAltitudes())
     return std::nullopt;
   if (!m_elevationInfo)
-    m_elevationInfo = ElevationInfo(GetData().m_geometry);
+    m_elevationInfo = ElevationInfo(GetData().m_geometry.m_lines);
   return m_elevationInfo;
 }
 
 double Track::GetDurationInSeconds() const
 {
-  double duration = 0.0;
-  if (!m_data.m_geometry.HasTimestamps())
-    return duration;
-  for (size_t i = 0; i < m_data.m_geometry.m_timestamps.size(); ++i)
-  {
-    ASSERT(m_data.m_geometry.HasTimestampsFor(i), ());
-    auto const & timestamps = m_data.m_geometry.m_timestamps[i];
-    duration += timestamps.back() - timestamps.front();
-  }
-  return duration;
+  return GetStatistics().m_duration;
 }
diff --git a/map/track.hpp b/map/track.hpp
index da030c2fdc..53f8da6f1d 100644
--- a/map/track.hpp
+++ b/map/track.hpp
@@ -3,6 +3,7 @@
 #include "kml/types.hpp"
 
 #include "map/elevation_info.hpp"
+#include "map/track_statistics.hpp"
 
 #include "drape_frontend/user_marks_provider.hpp"
 
@@ -31,6 +32,7 @@ public:
   m2::RectD GetLimitRect() const;
   double GetLengthMeters() const;
   double GetDurationInSeconds() const;
+  TrackStatistics GetStatistics() const;
   std::optional<ElevationInfo> GetElevationInfo() const;
   std::pair<m2::PointD, double> GetCenterPoint() const;
 
@@ -80,6 +82,7 @@ private:
 
   kml::TrackData m_data;
   kml::MarkGroupId m_groupID = kml::kInvalidMarkGroupId;
+  mutable std::optional<TrackStatistics> m_trackStatistics;
   mutable std::optional<ElevationInfo> m_elevationInfo;
 
   struct InteractionData
diff --git a/map/track_statistics.cpp b/map/track_statistics.cpp
new file mode 100644
index 0000000000..ca0791a8fd
--- /dev/null
+++ b/map/track_statistics.cpp
@@ -0,0 +1,108 @@
+#include "map/track_statistics.hpp"
+
+#include "geometry/mercator.hpp"
+#include "base/logging.hpp"
+
+using namespace geometry;
+
+double constexpr kInvalidTimestamp = std::numeric_limits<double>::min();
+PointWithAltitude const kInvalidPoint = {m2::PointD::Zero(), kInvalidAltitude};
+
+TrackStatistics::TrackStatistics()
+  : m_length(0),
+    m_duration(0),
+    m_ascent(0),
+    m_descent(0),
+    m_minElevation(kDefaultAltitudeMeters),
+    m_maxElevation(kDefaultAltitudeMeters),
+    m_previousPoint(kInvalidPoint),
+    m_previousTimestamp(kInvalidTimestamp)
+{}
+
+TrackStatistics::TrackStatistics(kml::MultiGeometry const & geometry)
+  : TrackStatistics()
+{
+  for (auto const & line : geometry.m_lines)
+    AddPoints(line, true);
+  if (geometry.HasTimestamps())
+  {
+    for (size_t i = 0; i < geometry.m_timestamps.size(); ++i)
+    {
+      ASSERT(geometry.HasTimestampsFor(i), ());
+      AddTimestamps(geometry.m_timestamps[i], true);
+    }
+  }
+}
+
+void TrackStatistics::AddGpsInfoPoint(location::GpsInfo const & point)
+{
+  auto const pointWithAltitude = geometry::PointWithAltitude(mercator::FromLatLon(point.m_latitude, point.m_longitude), point.m_altitude);
+
+  AddPoints({pointWithAltitude}, false);
+  AddTimestamps({point.m_timestamp}, false);
+}
+
+void TrackStatistics::AddPoints(kml::MultiGeometry::LineT const & line, bool isNewSegment)
+{
+  if (line.empty())
+    return;
+
+  size_t startIndex = 0;
+  if (HasNoPoints() || isNewSegment)
+  {
+    InitializeNewSegment(line[0]);
+    startIndex = 1;
+  }
+
+  ProcessPoints(line, startIndex);
+}
+
+void TrackStatistics::InitializeNewSegment(PointWithAltitude const & firstPoint)
+{
+  auto const altitude = firstPoint.GetAltitude();
+
+  bool const hasNoPoints = HasNoPoints();
+  m_minElevation = hasNoPoints ? altitude : std::min(m_minElevation, altitude);
+  m_maxElevation = hasNoPoints ? altitude : std::max(m_maxElevation, altitude);
+
+  m_previousPoint = firstPoint;
+}
+
+void TrackStatistics::ProcessPoints(kml::MultiGeometry::LineT const & points, size_t startIndex)
+{
+  for (size_t i = startIndex; i < points.size(); ++i)
+  {
+    auto const & point = points[i];
+    auto const pointAltitude = point.GetAltitude();
+
+    m_minElevation = std::min(m_minElevation, pointAltitude);
+    m_maxElevation = std::max(m_maxElevation, pointAltitude);
+
+    auto const deltaAltitude = pointAltitude - m_previousPoint.GetAltitude();
+    if (deltaAltitude > 0)
+      m_ascent += deltaAltitude;
+    else
+      m_descent -= deltaAltitude;
+    m_length += mercator::DistanceOnEarth(m_previousPoint.GetPoint(), point.GetPoint());
+
+    m_previousPoint = point;
+  }
+}
+
+void TrackStatistics::AddTimestamps(kml::MultiGeometry::TimeT const & timestamps, bool isNewSegment)
+{
+  if (timestamps.empty())
+    return;
+
+  if (m_previousTimestamp == kInvalidTimestamp)
+    m_previousTimestamp = timestamps.front();
+
+  auto const baseTimestamp = isNewSegment ? timestamps.front() : m_previousTimestamp;
+  m_duration += timestamps.back() - baseTimestamp;
+  m_previousTimestamp = timestamps.back();
+}
+
+bool TrackStatistics::HasNoPoints() const
+{
+  return m_previousPoint == kInvalidPoint;
+}
diff --git a/map/track_statistics.hpp b/map/track_statistics.hpp
new file mode 100644
index 0000000000..bd512f4d58
--- /dev/null
+++ b/map/track_statistics.hpp
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "platform/location.hpp"
+
+#include "kml/types.hpp"
+
+struct TrackStatistics
+{
+  TrackStatistics();
+  explicit TrackStatistics(kml::MultiGeometry const & geometry);
+
+  double m_length;
+  double m_duration;
+  double m_ascent;
+  double m_descent;
+  int16_t m_minElevation;
+  int16_t m_maxElevation;
+
+  void AddGpsInfoPoint(location::GpsInfo const & point);
+private:
+  void AddPoints(kml::MultiGeometry::LineT const & line, bool isNewSegment);
+  void AddTimestamps(kml::MultiGeometry::TimeT const & timestamps, bool isNewSegment);
+  void InitializeNewSegment(geometry::PointWithAltitude const & firstPoint);
+  void ProcessPoints(kml::MultiGeometry::LineT const & points, size_t startIndex);
+  bool HasNoPoints() const;
+
+  geometry::PointWithAltitude m_previousPoint;
+  double m_previousTimestamp;
+};
diff --git a/xcode/map/map.xcodeproj/project.pbxproj b/xcode/map/map.xcodeproj/project.pbxproj
index f5810fc704..ef97e0d864 100644
--- a/xcode/map/map.xcodeproj/project.pbxproj
+++ b/xcode/map/map.xcodeproj/project.pbxproj
@@ -92,6 +92,9 @@
 		BBFC7E3A202D29C000531BE7 /* user_mark_layer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = BBFC7E38202D29BF00531BE7 /* user_mark_layer.cpp */; };
 		BBFC7E3B202D29C000531BE7 /* user_mark_layer.hpp in Headers */ = {isa = PBXBuildFile; fileRef = BBFC7E39202D29BF00531BE7 /* user_mark_layer.hpp */; };
 		ED49D74C2CEF3D69004AF27E /* elevation_info_tests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = ED49D74B2CEF3CE3004AF27E /* elevation_info_tests.cpp */; };
+		ED85D1CC2D5F4B5B00D8075D /* track_statistics.hpp in Headers */ = {isa = PBXBuildFile; fileRef = ED85D1CB2D5F4B5B00D8075D /* track_statistics.hpp */; };
+		ED85D1CE2D5F4B7200D8075D /* track_statistics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = ED85D1CD2D5F4B6600D8075D /* track_statistics.cpp */; };
+		ED85D1D02D5F508700D8075D /* track_statistics_tests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = ED85D1CF2D5F508700D8075D /* track_statistics_tests.cpp */; };
 		F6B283031C1B03320081957A /* gps_track_collection.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F6B282FB1C1B03320081957A /* gps_track_collection.cpp */; };
 		F6B283041C1B03320081957A /* gps_track_collection.hpp in Headers */ = {isa = PBXBuildFile; fileRef = F6B282FC1C1B03320081957A /* gps_track_collection.hpp */; };
 		F6B283051C1B03320081957A /* gps_track_filter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F6B282FD1C1B03320081957A /* gps_track_filter.cpp */; };
@@ -240,6 +243,9 @@
 		BBFC7E38202D29BF00531BE7 /* user_mark_layer.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = user_mark_layer.cpp; sourceTree = "<group>"; };
 		BBFC7E39202D29BF00531BE7 /* user_mark_layer.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = user_mark_layer.hpp; sourceTree = "<group>"; };
 		ED49D74B2CEF3CE3004AF27E /* elevation_info_tests.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = elevation_info_tests.cpp; sourceTree = "<group>"; };
+		ED85D1CB2D5F4B5B00D8075D /* track_statistics.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = track_statistics.hpp; sourceTree = "<group>"; };
+		ED85D1CD2D5F4B6600D8075D /* track_statistics.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = track_statistics.cpp; sourceTree = "<group>"; };
+		ED85D1CF2D5F508700D8075D /* track_statistics_tests.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = track_statistics_tests.cpp; sourceTree = "<group>"; };
 		F6B282FB1C1B03320081957A /* gps_track_collection.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = gps_track_collection.cpp; sourceTree = "<group>"; };
 		F6B282FC1C1B03320081957A /* gps_track_collection.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = gps_track_collection.hpp; sourceTree = "<group>"; };
 		F6B282FD1C1B03320081957A /* gps_track_filter.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = gps_track_filter.cpp; sourceTree = "<group>"; };
@@ -355,6 +361,7 @@
 				BB421D6A1E8C0026005BFA4D /* transliteration_test.cpp */,
 				674A2A351B27011A001A525C /* working_time_tests.cpp */,
 				ED49D74B2CEF3CE3004AF27E /* elevation_info_tests.cpp */,
+				ED85D1CF2D5F508700D8075D /* track_statistics_tests.cpp */,
 			);
 			name = map_tests;
 			path = ../../map/map_tests;
@@ -444,6 +451,8 @@
 				BB1C0194241BF73C0067FD5C /* track_mark.hpp */,
 				6753462C1A4054E800A0A8C3 /* track.cpp */,
 				6753462D1A4054E800A0A8C3 /* track.hpp */,
+				ED85D1CD2D5F4B6600D8075D /* track_statistics.cpp */,
+				ED85D1CB2D5F4B5B00D8075D /* track_statistics.hpp */,
 				347B60741DD9926D0050FA24 /* traffic_manager.cpp */,
 				347B60751DD9926D0050FA24 /* traffic_manager.hpp */,
 				BB4E5F201FCC663700A77250 /* transit */,
@@ -513,6 +522,7 @@
 				675346651A4054E800A0A8C3 /* framework.hpp in Headers */,
 				BBA014B120754997007402E4 /* user_mark_id_storage.hpp in Headers */,
 				674A2A381B2715FB001A525C /* osm_opening_hours.hpp in Headers */,
+				ED85D1CC2D5F4B5B00D8075D /* track_statistics.hpp in Headers */,
 				3DEE1ADF21EE03B400054A91 /* power_manager.hpp in Headers */,
 				F6D2CE7F1EDEB7F500636DFD /* routing_manager.hpp in Headers */,
 				670E39411C46C5C700E9C0A6 /* gps_tracker.hpp in Headers */,
@@ -653,6 +663,7 @@
 				679624B21D1017DB00AE4E3C /* mwm_set_test.cpp in Sources */,
 				679624AD1D1017DB00AE4E3C /* address_tests.cpp in Sources */,
 				67F183791BD5045700AB1840 /* kmz_unarchive_test.cpp in Sources */,
+				ED85D1D02D5F508700D8075D /* track_statistics_tests.cpp in Sources */,
 				FAA8387326BB3C09002E54C6 /* check_mwms.cpp in Sources */,
 				679624AE1D1017DB00AE4E3C /* feature_getters_tests.cpp in Sources */,
 				67F1837A1BD5045700AB1840 /* mwm_url_tests.cpp in Sources */,
@@ -700,6 +711,7 @@
 				F6B283071C1B03320081957A /* gps_track_storage.cpp in Sources */,
 				670E39401C46C5C700E9C0A6 /* gps_tracker.cpp in Sources */,
 				BBA014B220754997007402E4 /* user_mark_id_storage.cpp in Sources */,
+				ED85D1CE2D5F4B7200D8075D /* track_statistics.cpp in Sources */,
 				6753464A1A4054E800A0A8C3 /* bookmark.cpp in Sources */,
 				45580ABE1E2CBD5E00CD535D /* benchmark_tools.cpp in Sources */,
 				3DA5723220C195ED007BDE27 /* everywhere_search_callback.cpp in Sources */,