Compare commits

...
Sign in to create a new pull request.

8 commits

Author SHA1 Message Date
cf65dc0614 [serdes] [kml] save KML track in the gxt:Track format if it has timestamps
Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
2024-08-27 20:45:47 +04:00
648a8b1a84 [serdes] [gpx] fix GPX file serialization to support timestamps parsing
Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
2024-08-27 20:42:56 +04:00
0c4a0101d7 [serdes] [kml] merge points on validation instead of serialization
It will fix the issue when we cannot get the marged points indexes to skip the same timestamps during the serialization process because timestamps count should be equal to the points in line count (or 0).

Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
2024-08-27 20:42:56 +04:00
dde804e913 [serdes] [gpx] fix GPX serializing to export tracks preserving the timestamps
Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
2024-08-27 20:42:56 +04:00
7f350c56e3 [bookmarks] [gps] save the altitude and timestamps to the TrackData
Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
2024-08-27 20:42:56 +04:00
bd00ed347f [kml] add timestamps vector to the MultiGeometry
Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
2024-08-27 20:42:56 +04:00
2227204c19 [gps] fix CHECK in GetSize
Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
2024-08-27 20:42:56 +04:00
7741ec4f12 [gps] reserve space for the recorded track points and timestamps
Signed-off-by: Kiryl Kaveryn <kirylkaveryn@gmail.com>
2024-08-27 20:42:54 +04:00
14 changed files with 220 additions and 19 deletions

View file

@ -13,6 +13,8 @@
#include "base/string_utils.hpp"
#include "base/timer.hpp"
#include <chrono>
namespace kml
{
namespace
@ -455,7 +457,7 @@ void SaveTrackLayer(Writer & writer, TrackLayer const & layer,
writer << indent << "<width>" << strings::to_string(layer.m_lineWidth) << "</width>\n";
}
void SaveTrackGeometry(Writer & writer, MultiGeometry const & geom)
void SaveTrackGeometryAsLineString(Writer & writer, MultiGeometry const & geom)
{
size_t const sz = geom.m_lines.size();
if (sz == 0)
@ -478,18 +480,66 @@ void SaveTrackGeometry(Writer & writer, MultiGeometry const & geom)
ASSERT(false, ());
continue;
}
writer << linesIndent << "<LineString><coordinates>";
writer << PointToString(e[0]);
for (size_t i = 1; i < e.size(); ++i)
writer << " " << PointToString(e[i]);
writer << "</coordinates></LineString>\n";
}
if (sz > 1)
writer << kIndent4 << "</MultiGeometry>\n";
}
void SaveTrackGeometryAsGxTrack(Writer & writer, MultiGeometry const & geom)
{
size_t const sz = geom.m_lines.size();
if (sz == 0)
{
ASSERT(false, ());
return;
}
auto linesIndent = kIndent4;
if (sz > 1)
{
linesIndent = kIndent8;
writer << kIndent4 << "<gx:MultiTrack>\n";
/// @TODO(KK): add the <altitudeMode>absolute</altitudeMode> if needed
}
for (size_t lineIndex = 0; lineIndex < geom.m_lines.size(); ++lineIndex)
{
auto const & line = geom.m_lines[lineIndex];
if (line.empty())
{
ASSERT(false, ());
continue;
}
writer << linesIndent << "<gx:Track>\n";
/// @TODO(KK): add the <altitudeMode>absolute</altitudeMode> if needed
if (geom.HasTimestampsFor(lineIndex))
{
/// @TODO(KK): Is it the proper way to handle timestamps for tracks sizes mismatch? Maybe it will be better to skip the timestamps section and throws an error if smth happens?
CHECK_EQUAL(geom.m_lines.size(), geom.m_timestamps.size(), ("Timestamps size mismatch"));
auto const & timestampsForLine = geom.m_timestamps[lineIndex];
for (size_t pointIndex = 0; pointIndex < line.size(); ++pointIndex)
{
CHECK_EQUAL(line.size(), timestampsForLine.size(), ("Timestamps size mismatch for track:", lineIndex));
auto const timeStr = base::SecondsSinceEpochToString(timestampsForLine[pointIndex]);
writer << linesIndent << kIndent4 << "<when>" << timeStr << "</when>\n";
}
}
for (const auto & point : line)
writer << linesIndent << kIndent4 << "<gx:coord>" << PointToGxString(point) << "</gx:coord>\n";
writer << linesIndent << "</gx:Track>\n";
}
if (sz > 1)
writer << kIndent4 << "</MultiGeometry>\n";
writer << kIndent4 << "</gx:MultiTrack>\n";
}
void SaveTrackExtendedData(Writer & writer, TrackData const & trackData)
@ -546,7 +596,10 @@ void SaveTrackData(Writer & writer, TrackData const & trackData)
<< "</when></TimeStamp>\n";
}
SaveTrackGeometry(writer, trackData.m_geometry);
if (trackData.m_geometry.HasTimestamps())
SaveTrackGeometryAsGxTrack(writer, trackData.m_geometry);
else
SaveTrackGeometryAsLineString(writer, trackData.m_geometry);
SaveTrackExtendedData(writer, trackData);
@ -674,15 +727,18 @@ void KmlParser::ParseAndAddPoints(MultiGeometry::LineT & line, std::string_view
geometry::PointWithAltitude point;
if (ParsePointWithAltitude(v, coordSeparator, point))
{
// We don't expect vertical surfaces, so do not compare heights here.
// Will get a lot of duplicating points otherwise after import some user KMLs.
// https://github.com/organicmaps/organicmaps/issues/3895
if (line.empty() || !AlmostEqualAbs(line.back().GetPoint(), point.GetPoint(), kMwmPointAccuracy))
line.emplace_back(point);
line.emplace_back(point);
}
});
}
void KmlParser::ParseAndAddTimestamps(MultiGeometry::TimeT & timestamps, std::string_view s,
char const * blockSeparator)
{
auto const ts = base::StringToTimestamp(std::string{s});
timestamps.emplace_back(ts);
}
void KmlParser::ParseLineString(std::string const & s)
{
// If m_org is not empty, then it's still a Bookmark but with track data
@ -773,6 +829,7 @@ bool KmlParser::Push(std::string movedTag)
{
m_geometryType = GEOMETRY_TYPE_LINE;
m_geometry.m_lines.emplace_back();
m_geometry.m_timestamps.emplace_back();
}
return true;
}
@ -977,7 +1034,12 @@ void KmlParser::CharData(std::string & value)
{
if (!IsTrack(prevTag))
return false;
if (currTag == "when")
{
auto & timestamps = m_geometry.m_timestamps;
ASSERT(!timestamps.empty(), ());
ParseAndAddTimestamps(timestamps.back(), value, "\n\r\t");
}
if (currTag == "coord" || currTag == "gx:coord")
{
auto & lines = m_geometry.m_lines;

View file

@ -83,6 +83,8 @@ private:
void SetOrigin(std::string const & s);
static void ParseAndAddPoints(MultiGeometry::LineT & line, std::string_view s,
char const * blockSeparator, char const * coordSeparator);
void ParseAndAddTimestamps(MultiGeometry::TimeT & timestamps, std::string_view s,
char const * blockSeparator);
void ParseLineString(std::string const & s);
bool MakeValid();

View file

@ -25,6 +25,13 @@ std::string PointToString(geometry::PointWithAltitude const & pt)
return PointToString(pt.GetPoint());
}
std::string PointToGxString(geometry::PointWithAltitude const & pt)
{
std::string str = PointToString(pt);
std::replace(str.begin(), str.end(), ',', ' ');
return str;
}
void SaveStringWithCDATA(Writer & writer, std::string s)
{
if (s.empty())

View file

@ -23,6 +23,7 @@ uint32_t ToRGBA(Channel red, Channel green, Channel blue, Channel alpha)
std::string PointToString(m2::PointD const & org);
std::string PointToString(geometry::PointWithAltitude const & pt);
std::string PointToGxString(geometry::PointWithAltitude const & pt);
void SaveStringWithCDATA(Writer & writer, std::string s);

View file

@ -31,6 +31,7 @@ std::string_view constexpr kDesc = "desc";
std::string_view constexpr kMetadata = "metadata";
std::string_view constexpr kEle = "ele";
std::string_view constexpr kCmt = "cmt";
std::string_view constexpr kTime = "time";
std::string_view constexpr kGpxHeader =
"<?xml version=\"1.0\"?>\n"
@ -63,6 +64,7 @@ void GpxParser::ResetPoint()
m_lat = 0.;
m_lon = 0.;
m_altitude = geometry::kInvalidAltitude;
m_timestamp = base::INVALID_TIME_STAMP;
}
bool GpxParser::MakeValid()
@ -221,12 +223,18 @@ void GpxParser::Pop(std::string_view tag)
{
m2::PointD const p = mercator::FromLatLon(m_lat, m_lon);
if (m_line.empty() || !AlmostEqualAbs(m_line.back().GetPoint(), p, kMwmPointAccuracy))
{
m_line.emplace_back(p, m_altitude);
if (m_timestamp != base::INVALID_TIME_STAMP)
m_timestamps.emplace_back(m_timestamp);
}
m_altitude = geometry::kInvalidAltitude;
m_timestamp = base::INVALID_TIME_STAMP;
}
else if (tag == gpx::kTrkSeg || tag == gpx::kRte)
{
m_geometry.m_lines.push_back(std::move(m_line));
m_geometry.m_timestamps.push_back(std::move(m_timestamps));
}
else if (tag == gpx::kWpt)
{
@ -310,6 +318,8 @@ void GpxParser::CharData(std::string & value)
ParseAltitude(value);
else if (currTag == gpx::kCmt)
m_comment = value;
else if (currTag == gpx::kTime)
ParseTimestamp(value);
}
}
@ -358,6 +368,13 @@ void GpxParser::ParseAltitude(std::string const & value)
m_altitude = geometry::kInvalidAltitude;
}
void GpxParser::ParseTimestamp(std::string const & value)
{
if (base::StringToTimestamp(value) == base::INVALID_TIME_STAMP)
LOG(LWARNING, ("Invalid timestamp value", value));
m_timestamp = base::StringToTimestamp(value);
}
std::string GpxParser::BuildDescription() const
{
if (m_description.empty())
@ -457,18 +474,32 @@ void SaveTrackData(Writer & writer, TrackData const & trackData)
writer << "</color>\n" << kIndent2 << "</extensions>\n";
}
bool const trackHasAltitude = TrackHasAltitudes(trackData);
for (auto const & line : trackData.m_geometry.m_lines)
auto const & geom = trackData.m_geometry;
for (size_t lineIndex = 0; lineIndex < geom.m_lines.size(); ++lineIndex)
{
auto const & line = geom.m_lines[lineIndex];
writer << kIndent2 << "<trkseg>\n";
for (auto const & point : line)
for (size_t pointIndex = 0; pointIndex < line.size(); ++pointIndex)
{
auto const & point = line[pointIndex];
auto const [lat, lon] = mercator::ToLatLon(point);
writer << kIndent4 << "<trkpt lat=\"" << CoordToString(lat) << "\" lon=\"" << CoordToString(lon) << "\">\n";
if (trackHasAltitude)
writer << kIndent6 << "<ele>" << CoordToString(point.GetAltitude()) << "</ele>\n";
if (geom.HasTimestampsFor(lineIndex))
{
CHECK_EQUAL(line.size(), geom.m_timestamps[lineIndex].size(), ("Timestamps size mismatch for track:", lineIndex));
auto const timestamp = geom.m_timestamps[lineIndex][pointIndex];
auto const timestampStr = base::SecondsSinceEpochToString(timestamp);
writer << kIndent6 << "<time>" << timestampStr << "</time>\n";
}
writer << kIndent4 << "</trkpt>\n";
++pointIndex;
}
writer << kIndent2 << "</trkseg>\n";
++lineIndex;
}
writer << "</trk>\n";
}

View file

@ -96,12 +96,15 @@ private:
double m_lat;
double m_lon;
geometry::Altitude m_altitude;
time_t m_timestamp;
MultiGeometry::LineT m_line;
MultiGeometry::TimeT m_timestamps;
std::string m_customName;
void ParseName(std::string const & value, std::string const & prevTag);
void ParseDescription(std::string const & value, std::string const & prevTag);
void ParseAltitude(std::string const & value);
void ParseTimestamp(std::string const & value);
std::string BuildDescription() const;
};
} // namespace gpx

View file

@ -53,6 +53,7 @@ MarkId constexpr kInvalidMarkId = std::numeric_limits<MarkId>::max();
MarkId constexpr kDebugMarkId = kInvalidMarkId - 1;
TrackId constexpr kInvalidTrackId = std::numeric_limits<TrackId>::max();
CompilationId constexpr kInvalidCompilationId = std::numeric_limits<CompilationId>::max();
double constexpr kInvalidTimestamp = 0.0;
inline uint64_t ToSecondsSinceEpoch(Timestamp const & time)
{

View file

@ -344,27 +344,54 @@ struct TrackLayer
struct MultiGeometry
{
using LineT = std::vector<geometry::PointWithAltitude>;
std::vector<LineT> m_lines;
using TimeT = std::vector<double>;
void Clear() { m_lines.clear(); }
std::vector<LineT> m_lines;
std::vector<TimeT> m_timestamps;
void Clear() {
m_lines.clear();
m_timestamps.clear();
}
/// @TODO(KK): should timestamps be validated and how?
bool IsValid() const { return !m_lines.empty(); }
bool operator==(MultiGeometry const & rhs) const
{
return IsEqual(m_lines, rhs.m_lines);
return IsEqual(m_lines, rhs.m_lines) && m_timestamps == rhs.m_timestamps;
}
friend std::string DebugPrint(MultiGeometry const & geom)
{
/// @TODO(KK): Add timestamps.
return ::DebugPrint(geom.m_lines);
}
void FromPoints(std::vector<m2::PointD> const & points);
void Assign(std::initializer_list<geometry::PointWithAltitude> lst)
{
/// @TODO(KK): Assign is used only in tests
m_lines.emplace_back();
m_lines.back().assign(lst);
}
bool HasTimestamps() const
{
if (m_timestamps.empty())
return false;
for (auto const & timestamp : m_timestamps)
{
if (!timestamp.empty())
return true;
}
return false;
}
bool HasTimestampsFor(size_t lineIndex) const
{
return !(m_timestamps.empty() || lineIndex >= m_timestamps.size() || m_timestamps[lineIndex].empty());
}
};
struct TrackData

View file

@ -199,10 +199,57 @@ void ValidateKmlData(std::unique_ptr<kml::FileData> & data)
if (!data)
return;
// Filter points with the duplicated coordinates.
for (auto & t : data->m_tracksData)
{
if (t.m_layers.empty())
t.m_layers.emplace_back(kml::KmlParser::GetDefaultTrackLayer());
kml::MultiGeometry validGeometry;
auto const & geometry = t.m_geometry;
for (size_t lineIndex = 0; lineIndex < geometry.m_lines.size(); ++lineIndex)
{
auto const & line = geometry.m_lines[lineIndex];
auto const & timestamps = geometry.m_timestamps[lineIndex];
if (line.empty())
{
LOG(LWARNING, ("Empty line in track:", t.m_name[kml::kDefaultLang]));
continue;
}
kml::MultiGeometry::LineT validLine;
kml::MultiGeometry::TimeT validTimestamps;
auto const hasTimestamps = geometry.HasTimestampsFor(lineIndex);
if (hasTimestamps && timestamps.size() != line.size())
MYTHROW(kml::DeserializerKml::DeserializeException, ("Timestamps count doesn't match points count."));
for (size_t pointIndex = 0; pointIndex < line.size(); ++pointIndex)
{
auto const & prevPoint = validLine.back();
auto const & currPoint = line[pointIndex];
// We don't expect vertical surfaces, so do not compare heights here.
// Will get a lot of duplicating points otherwise after import some user KMLs.
// https://github.com/organicmaps/organicmaps/issues/3895
if (validLine.empty() || !AlmostEqualAbs(prevPoint.GetPoint(), currPoint.GetPoint(), kMwmPointAccuracy))
{
validLine.push_back(currPoint);
if (hasTimestamps)
validTimestamps.push_back(timestamps[pointIndex]);
}
}
if (!validLine.empty())
{
validGeometry.m_lines.push_back(validLine);
validGeometry.m_timestamps.push_back(validTimestamps);
}
}
t.m_geometry = std::move(validGeometry);
}
}

View file

@ -1148,10 +1148,16 @@ void BookmarkManager::SaveTrackRecording(std::string trackName)
auto const & tracker = GpsTracker::Instance();
CHECK(!tracker.IsEmpty(), ("Track recording should be not be empty"));
auto const trackSize = tracker.GetTrackSize();
kml::MultiGeometry::LineT line;
tracker.ForEachTrackPoint([&line](location::GpsInfo const & pt, size_t id)->bool
kml::MultiGeometry::TimeT timestamps;
line.reserve(trackSize);
timestamps.reserve(trackSize);
tracker.ForEachTrackPoint([&line, &timestamps](location::GpsInfo const & pt, size_t id)->bool
{
line.emplace_back(mercator::FromLatLon(pt.m_latitude, pt.m_longitude));
line.emplace_back(mercator::FromLatLon(pt.m_latitude, pt.m_longitude), pt.m_altitude);
timestamps.emplace_back(pt.m_timestamp);
return true;
});
@ -1162,6 +1168,7 @@ void BookmarkManager::SaveTrackRecording(std::string trackName)
kml::MultiGeometry geometry;
geometry.m_lines.push_back(std::move(line));
geometry.m_timestamps.push_back(std::move(timestamps));
trackData.m_geometry = std::move(geometry);
kml::ColorData colorData;

View file

@ -94,6 +94,12 @@ void GpsTrack::Clear()
ScheduleTask();
}
size_t GpsTrack::GetSize() const
{
CHECK(m_collection != nullptr, ());
return m_collection->GetSize();
}
bool GpsTrack::IsEmpty() const
{
if (!m_collection)

View file

@ -39,6 +39,7 @@ public:
void Clear();
bool IsEmpty() const;
size_t GetSize() const;
/// Sets tracking duration in hours.
/// @note Callback is called with 'toRemove' points, if some points were removed.

View file

@ -99,6 +99,11 @@ bool GpsTracker::IsEmpty() const
return m_track.IsEmpty();
}
size_t GpsTracker::GetTrackSize() const
{
return m_track.GetSize();
}
void GpsTracker::Connect(TGpsTrackDiffCallback const & fn)
{
m_track.SetCallback(fn);

View file

@ -18,6 +18,7 @@ public:
std::chrono::hours GetDuration() const;
bool IsEmpty() const;
size_t GetTrackSize() const;
void SetDuration(std::chrono::hours duration);