diff --git a/data/test_data/gpx/color_map_dst.gpx b/data/test_data/gpx/color_map_dst.gpx
new file mode 100644
index 0000000000..90096b2dfd
--- /dev/null
+++ b/data/test_data/gpx/color_map_dst.gpx
@@ -0,0 +1,28 @@
+
+
+
+ new
+ Short description
+
+
+ new red
+ description 1
+
+ DarkGray
+ 443344
+ #FF443344
+
+
+
+ 123
+
+
+ 123
+
+
+
+
\ No newline at end of file
diff --git a/data/test_data/gpx/color_map_src.gpx b/data/test_data/gpx/color_map_src.gpx
new file mode 100644
index 0000000000..7528cf1207
--- /dev/null
+++ b/data/test_data/gpx/color_map_src.gpx
@@ -0,0 +1,29 @@
+
+
+
+ new
+
+ gpx.studio
+
+
+ Short description
+
+
+ new red
+ description 1
+ Running
+
+
+ 443344
+
+
+
+
+ 123
+
+
+ 123
+
+
+
+
\ No newline at end of file
diff --git a/data/test_data/gpx/export_test.gpx b/data/test_data/gpx/export_test.gpx
index 549da4bf67..03f31b35e4 100644
--- a/data/test_data/gpx/export_test.gpx
+++ b/data/test_data/gpx/export_test.gpx
@@ -1,5 +1,9 @@
-
+
My route
& "]]>
@@ -15,7 +19,9 @@
Some random route
- 00FF00
+ Green
+ 00FF00
+ #FF00FF00
diff --git a/data/test_data/gpx/export_test_empty.gpx b/data/test_data/gpx/export_test_empty.gpx
index ad945c2945..76c67ba06d 100644
--- a/data/test_data/gpx/export_test_empty.gpx
+++ b/data/test_data/gpx/export_test_empty.gpx
@@ -1,5 +1,9 @@
-
+
\ No newline at end of file
diff --git a/kml/kml_tests/gpx_tests.cpp b/kml/kml_tests/gpx_tests.cpp
index 37c2274441..a1d90a26cd 100644
--- a/kml/kml_tests/gpx_tests.cpp
+++ b/kml/kml_tests/gpx_tests.cpp
@@ -11,7 +11,8 @@
namespace gpx_tests
{
-static kml::FileData loadGpxFromString(std::string_view content) {
+static kml::FileData LoadGpxFromString(std::string_view content)
+{
TEST_NO_THROW(
{
kml::FileData dataFromText;
@@ -20,34 +21,59 @@ static kml::FileData loadGpxFromString(std::string_view content) {
}, ());
}
-static kml::FileData loadGpxFromFile(std::string const & file) {
+static kml::FileData LoadGpxFromFile(std::string const & file)
+{
auto const fileName = GetPlatform().TestsDataPathForFile(file);
std::string text;
FileReader(fileName).ReadAsString(text);
- return loadGpxFromString(text);
+ return LoadGpxFromString(text);
}
-void importExportCompare(char const * testFile)
+static std::string ReadFile(char const * testFile)
{
auto const fileName = GetPlatform().TestsDataPathForFile(testFile);
std::string sourceFileText;
FileReader(fileName).ReadAsString(sourceFileText);
- kml::FileData const dataFromFile = loadGpxFromFile(testFile);
+ return sourceFileText;
+}
+
+static std::string ReadFileAndSerialize(char const * testFile)
+{
+ kml::FileData const dataFromFile = LoadGpxFromFile(testFile);
std::string resultBuffer;
MemWriter sink(resultBuffer);
kml::gpx::SerializerGpx ser(dataFromFile);
ser.Serialize(sink);
+ return resultBuffer;
+}
+
+void ImportExportCompare(char const * testFile)
+{
+ std::string const sourceFileText = ReadFile(testFile);
+ std::string const resultBuffer = ReadFileAndSerialize(testFile);
TEST_EQUAL(resultBuffer, sourceFileText, ());
}
+void ImportExportCompare(char const * sourceFile, char const * destinationFile)
+{
+ std::string const resultBuffer = ReadFileAndSerialize(sourceFile);
+ std::string const destinationFileText = ReadFile(destinationFile);
+ TEST_EQUAL(resultBuffer, destinationFileText, ());
+}
+
UNIT_TEST(Gpx_ImportExport_Test)
{
- importExportCompare("test_data/gpx/export_test.gpx");
+ ImportExportCompare("test_data/gpx/export_test.gpx");
}
UNIT_TEST(Gpx_ImportExportEmpty_Test)
{
- importExportCompare("test_data/gpx/export_test_empty.gpx");
+ ImportExportCompare("test_data/gpx/export_test_empty.gpx");
+}
+
+UNIT_TEST(Gpx_ColorMapExport_Test)
+{
+ ImportExportCompare("test_data/gpx/color_map_src.gpx", "test_data/gpx/color_map_dst.gpx");
}
UNIT_TEST(Gpx_Test_Point_With_Valid_Timestamp)
@@ -68,7 +94,7 @@ UNIT_TEST(Gpx_Test_Point_With_Valid_Timestamp)
bookmarkData.m_color = {kml::PredefinedColor::Red, 0};
data.m_bookmarksData.emplace_back(std::move(bookmarkData));
- kml::FileData const dataFromText = loadGpxFromString(input);
+ kml::FileData const dataFromText = LoadGpxFromString(input);
TEST_EQUAL(dataFromText, data, ());
}
@@ -83,14 +109,14 @@ UNIT_TEST(Gpx_Test_Point_With_Invalid_Timestamp)
)";
- kml::FileData const dataFromText = loadGpxFromString(input);
+ kml::FileData const dataFromText = LoadGpxFromString(input);
TEST_EQUAL(dataFromText.m_bookmarksData.size(), 1, ());
}
UNIT_TEST(Gpx_Test_Track_Without_Timestamps)
{
auto const fileName = "test_data/gpx/track_without_timestamps.gpx";
- kml::FileData const dataFromText = loadGpxFromFile(fileName);
+ kml::FileData const dataFromText = LoadGpxFromFile(fileName);
auto const & lines = dataFromText.m_tracksData[0].m_geometry.m_lines;
TEST_EQUAL(lines.size(), 2, ());
{
@@ -124,7 +150,7 @@ UNIT_TEST(Gpx_Test_Track_Without_Timestamps)
UNIT_TEST(Gpx_Test_Track_With_Timestamps)
{
auto const fileName = "test_data/gpx/track_with_timestamps.gpx";
- kml::FileData const dataFromText = loadGpxFromFile(fileName);
+ kml::FileData const dataFromText = LoadGpxFromFile(fileName);
auto const & geometry = dataFromText.m_tracksData[0].m_geometry;
TEST_EQUAL(geometry.m_lines.size(), 2, ());
TEST_EQUAL(geometry.m_timestamps.size(), 2, ());
@@ -168,7 +194,7 @@ UNIT_TEST(Gpx_Altitude_Issues)
)";
- kml::FileData const dataFromText = loadGpxFromString(input);
+ kml::FileData const dataFromText = LoadGpxFromString(input);
auto const & line = dataFromText.m_tracksData[0].m_geometry.m_lines[0];
TEST_EQUAL(line.size(), 6, ());
TEST_EQUAL(line[0], geometry::PointWithAltitude(mercator::FromLatLon(1, 1), geometry::kInvalidAltitude), ());
@@ -200,7 +226,7 @@ UNIT_TEST(Gpx_Timestamp_Issues)
)";
- kml::FileData const dataFromText = loadGpxFromString(input);
+ kml::FileData const dataFromText = LoadGpxFromString(input);
auto const & times = dataFromText.m_tracksData[0].m_geometry.m_timestamps[0];
TEST_EQUAL(times.size(), 8, ());
TEST_EQUAL(times[0], base::StringToTimestamp("2024-05-04T19:00:00Z"), ());
@@ -215,21 +241,21 @@ UNIT_TEST(Gpx_Timestamp_Issues)
UNIT_TEST(GoMap)
{
- kml::FileData const dataFromFile = loadGpxFromFile("test_data/gpx/go_map.gpx");
+ kml::FileData const dataFromFile = LoadGpxFromFile("test_data/gpx/go_map.gpx");
auto const & line = dataFromFile.m_tracksData[0].m_geometry.m_lines[0];
TEST_EQUAL(line.size(), 101, ());
}
UNIT_TEST(GpxStudio)
{
- kml::FileData const dataFromFile = loadGpxFromFile("test_data/gpx/gpx_studio.gpx");
+ kml::FileData const dataFromFile = LoadGpxFromFile("test_data/gpx/gpx_studio.gpx");
auto const & line = dataFromFile.m_tracksData[0].m_geometry.m_lines[0];
TEST_EQUAL(line.size(), 328, ());
}
UNIT_TEST(OsmTrack)
{
- kml::FileData const dataFromFile = loadGpxFromFile("test_data/gpx/osm_track.gpx");
+ kml::FileData const dataFromFile = LoadGpxFromFile("test_data/gpx/osm_track.gpx");
auto const & line = dataFromFile.m_tracksData[0].m_geometry.m_lines[0];
auto const & timestamps = dataFromFile.m_tracksData[0].m_geometry.m_timestamps[0];
TEST_EQUAL(line.size(), 182, ());
@@ -238,14 +264,14 @@ UNIT_TEST(OsmTrack)
UNIT_TEST(TowerCollector)
{
- kml::FileData const dataFromFile = loadGpxFromFile("test_data/gpx/tower_collector.gpx");
+ kml::FileData const dataFromFile = LoadGpxFromFile("test_data/gpx/tower_collector.gpx");
auto line = dataFromFile.m_tracksData[0].m_geometry.m_lines[0];
TEST_EQUAL(line.size(), 35, ());
}
UNIT_TEST(PointsOnly)
{
- kml::FileData const dataFromFile = loadGpxFromFile("test_data/gpx/points.gpx");
+ kml::FileData const dataFromFile = LoadGpxFromFile("test_data/gpx/points.gpx");
auto bookmarks = dataFromFile.m_bookmarksData;
TEST_EQUAL(bookmarks.size(), 3, ());
TEST_EQUAL("Point 1", bookmarks[0].m_name[kml::kDefaultLang], ());
@@ -254,7 +280,7 @@ UNIT_TEST(PointsOnly)
UNIT_TEST(Route)
{
- kml::FileData dataFromFile = loadGpxFromFile("test_data/gpx/route.gpx");
+ kml::FileData dataFromFile = LoadGpxFromFile("test_data/gpx/route.gpx");
auto line = dataFromFile.m_tracksData[0].m_geometry.m_lines[0];
TEST_EQUAL(line.size(), 2, ());
TEST_EQUAL(dataFromFile.m_categoryData.m_name[kml::kDefaultLang], "Some random route", ());
@@ -264,7 +290,7 @@ UNIT_TEST(Route)
UNIT_TEST(Color)
{
- kml::FileData const dataFromFile = loadGpxFromFile("test_data/gpx/color.gpx");
+ kml::FileData const dataFromFile = LoadGpxFromFile("test_data/gpx/color.gpx");
uint32_t const red = 0xFF0000FF;
uint32_t const blue = 0x0000FFFF;
uint32_t const black = 0x000000FF;
@@ -276,7 +302,7 @@ UNIT_TEST(Color)
UNIT_TEST(MultiTrackNames)
{
- kml::FileData dataFromFile = loadGpxFromFile("test_data/gpx/color.gpx");
+ kml::FileData dataFromFile = LoadGpxFromFile("test_data/gpx/color.gpx");
TEST_EQUAL("new", dataFromFile.m_categoryData.m_name[kml::kDefaultLang], ());
TEST_EQUAL("Short description", dataFromFile.m_categoryData.m_description[kml::kDefaultLang], ());
TEST_EQUAL("new red", dataFromFile.m_tracksData[0].m_name[kml::kDefaultLang], ());
@@ -287,14 +313,14 @@ UNIT_TEST(MultiTrackNames)
UNIT_TEST(Empty)
{
- kml::FileData dataFromFile = loadGpxFromFile("test_data/gpx/empty.gpx");
+ kml::FileData dataFromFile = LoadGpxFromFile("test_data/gpx/empty.gpx");
TEST_EQUAL("new", dataFromFile.m_categoryData.m_name[kml::kDefaultLang], ());
TEST_EQUAL(0, dataFromFile.m_tracksData.size(), ());
}
UNIT_TEST(OsmandColor1)
{
- kml::FileData const dataFromFile = loadGpxFromFile("test_data/gpx/osmand1.gpx");
+ kml::FileData const dataFromFile = LoadGpxFromFile("test_data/gpx/osmand1.gpx");
uint32_t constexpr expected = 0xFF7800FF;
TEST_EQUAL(dataFromFile.m_tracksData.size(), 4, ());
TEST_EQUAL(expected, dataFromFile.m_tracksData[0].m_layers[0].m_color.m_rgba, ());
@@ -305,7 +331,7 @@ UNIT_TEST(OsmandColor1)
UNIT_TEST(OsmandColor2)
{
- kml::FileData const dataFromFile = loadGpxFromFile("test_data/gpx/osmand2.gpx");
+ kml::FileData const dataFromFile = LoadGpxFromFile("test_data/gpx/osmand2.gpx");
uint32_t const expected1 = 0x00FF00FF;
uint32_t const expected2 = 0x1010A0FF;
TEST_EQUAL(expected1, dataFromFile.m_bookmarksData[0].m_color.m_rgba, ());
@@ -330,7 +356,7 @@ d5
5qqqqqq
)";
- kml::FileData const dataFromText = loadGpxFromString(input);
+ kml::FileData const dataFromText = LoadGpxFromString(input);
TEST_EQUAL("d1", dataFromText.m_bookmarksData[0].m_description.at(kml::kDefaultLang), ());
TEST_EQUAL("d2\n\nc2", dataFromText.m_bookmarksData[1].m_description.at(kml::kDefaultLang), ());
TEST_EQUAL("c3", dataFromText.m_bookmarksData[2].m_description.at(kml::kDefaultLang), ());
@@ -340,9 +366,35 @@ d5
UNIT_TEST(OpentracksColor)
{
- kml::FileData dataFromFile = loadGpxFromFile("test_data/gpx/opentracks_color.gpx");
+ kml::FileData dataFromFile = LoadGpxFromFile("test_data/gpx/opentracks_color.gpx");
uint32_t const expected = 0xC0C0C0FF;
TEST_EQUAL(expected, dataFromFile.m_tracksData[0].m_layers[0].m_color.m_rgba, ());
}
+UNIT_TEST(ParseFromString)
+{
+ // String hex sequence #AARRGGBB, uint32 sequence RGBA
+ TEST_EQUAL(std::optional(0x1FF), kml::gpx::GpxParser::ParseColorFromHexString("000001"), ());
+ TEST_EQUAL(std::optional(0x100FF), kml::gpx::GpxParser::ParseColorFromHexString("000100"), ());
+ TEST_EQUAL(std::optional(0x10000FF), kml::gpx::GpxParser::ParseColorFromHexString("010000"), ());
+ TEST_EQUAL(std::optional(0x1FF), kml::gpx::GpxParser::ParseColorFromHexString("#000001"), ());
+ TEST_EQUAL(std::optional(0x100FF), kml::gpx::GpxParser::ParseColorFromHexString("#000100"), ());
+ TEST_EQUAL(std::optional(0x10000FF), kml::gpx::GpxParser::ParseColorFromHexString("#010000"), ());
+ TEST_EQUAL(std::optional(0x1FF), kml::gpx::GpxParser::ParseColorFromHexString("#FF000001"), ());
+ TEST_EQUAL(std::optional(0x100FF), kml::gpx::GpxParser::ParseColorFromHexString("#FF000100"), ());
+ TEST_EQUAL(std::optional(0x10000FF), kml::gpx::GpxParser::ParseColorFromHexString("#FF010000"), ());
+ TEST_EQUAL(std::optional(0x10000AA), kml::gpx::GpxParser::ParseColorFromHexString("#AA010000"), ());
+ TEST_EQUAL(std::optional(), kml::gpx::GpxParser::ParseColorFromHexString("DarkRed"), ());
+}
+
+UNIT_TEST(MapGarminColor)
+{
+ TEST_EQUAL("DarkCyan", kml::gpx::MapGarminColor(0x008b8bff), ());
+ TEST_EQUAL("White", kml::gpx::MapGarminColor(0xffffffff), ());
+ TEST_EQUAL("DarkYellow", kml::gpx::MapGarminColor(0xb4b820ff), ());
+ TEST_EQUAL("DarkYellow", kml::gpx::MapGarminColor(0xb6b820ff), ());
+ TEST_EQUAL("DarkYellow", kml::gpx::MapGarminColor(0xb5b721ff), ());
+}
+
+
} // namespace gpx_tests
diff --git a/kml/serdes_gpx.cpp b/kml/serdes_gpx.cpp
index 36366caa16..1a95c31030 100644
--- a/kml/serdes_gpx.cpp
+++ b/kml/serdes_gpx.cpp
@@ -33,10 +33,13 @@ std::string_view constexpr kEle = "ele";
std::string_view constexpr kCmt = "cmt";
std::string_view constexpr kTime = "time";
-std::string_view constexpr kGpxHeader =
- "\n"
- "\n";
-
+std::string_view constexpr kGpxHeader = R"(
+
+)";
std::string_view constexpr kGpxFooter = "";
int constexpr kInvalidColor = 0;
@@ -131,52 +134,53 @@ std::string const & GpxParser::GetTagFromEnd(size_t n) const
return m_tags[m_tags.size() - n - 1];
}
-void GpxParser::ParseColor(std::string const & value)
+std::optional GpxParser::ParseColorFromHexString(std::string_view colorStr)
{
- auto const colorBytes = FromHex(value);
- if (colorBytes.size() != 3)
+ if (colorStr.empty())
{
- LOG(LWARNING, ("Invalid color value", value));
- return;
+ LOG(LWARNING, ("Invalid color value", colorStr));
+ return {};
}
- m_color = kml::ToRGBA(colorBytes[0], colorBytes[1], colorBytes[2], (char)255);
+ if (colorStr.front() == '#')
+ colorStr.remove_prefix(1);
+ if (colorStr.size() != 6 && colorStr.size() != 8)
+ {
+ LOG(LWARNING, ("Invalid color value", colorStr));
+ return {};
+ }
+ auto const colorBytes = FromHex(colorStr);
+ switch (colorBytes.size())
+ {
+ case 3: return kml::ToRGBA(colorBytes[0], colorBytes[1], colorBytes[2], (char)255);
+ case 4: return kml::ToRGBA(colorBytes[1], colorBytes[2], colorBytes[3], colorBytes[0]);
+ default:
+ LOG(LWARNING, ("Invalid color value", colorStr));
+ return {};
+ }
+}
+
+void GpxParser::ParseColor(std::string_view colorStr)
+{
+ if (const auto parsed = ParseColorFromHexString(colorStr); parsed)
+ m_color = parsed.value();
}
// https://osmand.net/docs/technical/osmand-file-formats/osmand-gpx/. Supported colors: #AARRGGBB/#RRGGBB/AARRGGBB/RRGGBB
void GpxParser::ParseOsmandColor(std::string const & value)
{
- if (value.empty())
- {
- LOG(LWARNING, ("Empty color value"));
+ auto const color = ParseColorFromHexString(value);
+ if (!color)
return;
- }
- std::string_view colorStr = value;
- if (colorStr.at(0) == '#')
- colorStr = colorStr.substr(1, colorStr.size() - 1);
- auto const colorBytes = FromHex(colorStr);
- uint32_t color;
- switch (colorBytes.size())
- {
- case 3:
- color = kml::ToRGBA(colorBytes[0], colorBytes[1], colorBytes[2], (char)255);
- break;
- case 4:
- color = kml::ToRGBA(colorBytes[1], colorBytes[2], colorBytes[3], colorBytes[0]);
- break;
- default:
- LOG(LWARNING, ("Invalid color value", value));
- return;
- }
if (m_tags.size() > 2 && GetTagFromEnd(2) == gpx::kGpx)
{
- m_globalColor = color;
+ m_globalColor = *color;
for (auto & track : m_data.m_tracksData)
for (auto & layer : track.m_layers)
layer.m_color.m_rgba = m_globalColor;
}
else
{
- m_color = color;
+ m_color = *color;
}
}
@@ -465,6 +469,64 @@ std::string GpxParser::BuildDescription() const
return m_description + "\n\n" + m_comment;
}
+std::tuple ExtractRGB(uint32_t color)
+{
+ return {(color >> 24) & 0xFF, (color >> 16) & 0xFF, (color >> 8) & 0xFF};
+}
+
+int ColorDistance(uint32_t color1, uint32_t color2)
+{
+ auto const [r1, g1, b1] = ExtractRGB(color1);
+ auto const [r2, g2, b2] = ExtractRGB(color2);
+ return (r1 - r2) * (r1 - r2) + (g1 - g2) * (g1 - g2) + (b1 - b2) * (b1 - b2);
+}
+
+struct RGBAToGarmin
+{
+ uint32_t rgba;
+ std::string_view color;
+};
+
+auto constexpr kRGBAToGarmin = std::to_array({
+ {0x000000ff, "Black"},
+ {0x8b0000ff, "DarkRed"},
+ {0x006400ff, "DarkGreen"},
+ {0xb5b820ff, "DarkYellow"},
+ {0x00008bff, "DarkBlue"},
+ {0x8b008bff, "DarkMagenta"},
+ {0x008b8bff, "DarkCyan"},
+ {0xccccccff, "LightGray"},
+ {0x444444ff, "DarkGray"},
+ {0xff0000ff, "Red"},
+ {0x00ff00ff, "Green"},
+ {0xffff00ff, "Yellow"},
+ {0x0000ffff, "Blue"},
+ {0xff00ffff, "Magenta"},
+ {0x00ffffff, "Cyan"},
+ {0xffffffff, "White"}
+});
+
+
+std::string_view MapGarminColor(uint32_t rgba)
+{
+ std::string_view closestColor = kRGBAToGarmin[0].color;
+ auto minDistance = std::numeric_limits::max();
+ for (const auto & [rgbaGarmin, color] : kRGBAToGarmin)
+ {
+ auto const distance = ColorDistance(rgba, rgbaGarmin);
+
+ if (distance == 0)
+ return color; // Exact match.
+
+ if (distance < minDistance)
+ {
+ minDistance = distance;
+ closestColor = color;
+ }
+ }
+ return closestColor;
+}
+
namespace
{
@@ -483,6 +545,13 @@ void SaveColorToRGB(Writer & writer, uint32_t rgba)
<< NumToHex(static_cast((rgba >> 8) & 0xFF));
}
+void SaveColorToARGB(Writer & writer, uint32_t rgba)
+{
+ writer << NumToHex(static_cast(rgba & 0xFF))
+ << NumToHex(static_cast(rgba >> 24 & 0xFF))
+ << NumToHex(static_cast((rgba >> 16) & 0xFF))
+ << NumToHex(static_cast((rgba >> 8) & 0xFF));
+}
void SaveCategoryData(Writer & writer, CategoryData const & categoryData)
{
@@ -555,9 +624,17 @@ void SaveTrackData(Writer & writer, TrackData const & trackData)
}
if (auto const color = TrackColor(trackData); color != kDefaultTrackColor)
{
- writer << kIndent2 << "\n" << kIndent4 << "";
+ writer << kIndent2 << "\n";
+ writer << kIndent4 << "";
+ writer << MapGarminColor(color);
+ writer << "\n";
+ writer << kIndent4 << "";
SaveColorToRGB(writer, color);
- writer << "\n" << kIndent2 << "\n";
+ writer << "\n";
+ writer << kIndent4 << "#";
+ SaveColorToARGB(writer, color);
+ writer << "\n";
+ writer << kIndent2 << "\n";
}
bool const trackHasAltitude = TrackHasAltitudes(trackData);
auto const & geom = trackData.m_geometry;
diff --git a/kml/serdes_gpx.hpp b/kml/serdes_gpx.hpp
index d592c1a3bf..593d169b17 100644
--- a/kml/serdes_gpx.hpp
+++ b/kml/serdes_gpx.hpp
@@ -61,6 +61,7 @@ public:
std::string const & GetTagFromEnd(size_t n) const;
void Pop(std::string_view tag);
void CharData(std::string & value);
+ static std::optional ParseColorFromHexString(std::string_view colorStr);
private:
enum GeometryType
@@ -72,7 +73,7 @@ private:
void ResetPoint();
bool MakeValid();
- void ParseColor(std::string const & value);
+ void ParseColor(std::string_view colorStr);
void ParseGarminColor(std::string const & value);
void ParseOsmandColor(std::string const & value);
bool IsValidCoordinatesPosition() const;
@@ -108,6 +109,9 @@ private:
void ParseTimestamp(std::string const & value);
std::string BuildDescription() const;
};
+
+std::string_view MapGarminColor(uint32_t rgba);
+
} // namespace gpx
class DeserializerGpx