From fd342c2a1781a30c28395894bbac2cad0874877b Mon Sep 17 00:00:00 2001 From: cyber-toad <128428520+cyber-toad@users.noreply.github.com> Date: Tue, 2 Sep 2025 12:30:43 +0200 Subject: [PATCH] [gpx] Save bookmark color to gpx export (#11238) * [gpx] Save bookmark color to gpx export * [gpx] Code-review fixes * [gpx] Use m_rgba to store initial color, reset on predefined color change * [gpx] Move MapPredefinedColor to color parser * [gpx] Adjust naming Signed-off-by: cyber-toad --- data/test_data/gpx/export_test.gpx | 9 ++ .../gpx/point_with_predefined_color_1.gpx | 13 +++ .../gpx/point_with_predefined_color_2.gpx | 16 ++++ libs/kml/color_parser.cpp | 92 +++++++++++++++++++ libs/kml/color_parser.hpp | 5 + libs/kml/kml_tests/gpx_tests.cpp | 34 +++++-- libs/kml/serdes_gpx.cpp | 82 +++++------------ libs/kml/serdes_gpx.hpp | 2 - libs/map/bookmark.cpp | 2 + 9 files changed, 186 insertions(+), 69 deletions(-) create mode 100644 data/test_data/gpx/point_with_predefined_color_1.gpx create mode 100644 data/test_data/gpx/point_with_predefined_color_2.gpx diff --git a/data/test_data/gpx/export_test.gpx b/data/test_data/gpx/export_test.gpx index 87e3a5305..d757f19bf 100644 --- a/data/test_data/gpx/export_test.gpx +++ b/data/test_data/gpx/export_test.gpx @@ -15,6 +15,15 @@ <&"]]> <&"]]> + + #FF00FF00 + + + + Point with color + + #FFFFC800 + Some random route diff --git a/data/test_data/gpx/point_with_predefined_color_1.gpx b/data/test_data/gpx/point_with_predefined_color_1.gpx new file mode 100644 index 000000000..32b0ad0f9 --- /dev/null +++ b/data/test_data/gpx/point_with_predefined_color_1.gpx @@ -0,0 +1,13 @@ + + + + + + Point 1 + Point 1 + + \ No newline at end of file diff --git a/data/test_data/gpx/point_with_predefined_color_2.gpx b/data/test_data/gpx/point_with_predefined_color_2.gpx new file mode 100644 index 000000000..7cc6e8648 --- /dev/null +++ b/data/test_data/gpx/point_with_predefined_color_2.gpx @@ -0,0 +1,16 @@ + + + + + + Point 1 + Point 1 + + #FF0066CC + + + \ No newline at end of file diff --git a/libs/kml/color_parser.cpp b/libs/kml/color_parser.cpp index 2e66603ae..247457e4d 100644 --- a/libs/kml/color_parser.cpp +++ b/libs/kml/color_parser.cpp @@ -1,6 +1,7 @@ #include "color_parser.hpp" #include "coding/hex.hpp" +#include "types.hpp" #include "base/string_utils.hpp" @@ -31,6 +32,97 @@ std::optional ParseHexColor(std::string_view c) } } +std::tuple ExtractRGB(uint32_t rgbaColor) +{ + return {(rgbaColor >> 24) & 0xFF, (rgbaColor >> 16) & 0xFF, (rgbaColor >> 8) & 0xFF}; +} + +static int ColorDistance(uint32_t rgbaColor1, uint32_t rgbaColor2) +{ + auto const [r1, g1, b1] = ExtractRGB(rgbaColor1); + auto const [r2, g2, b2] = ExtractRGB(rgbaColor2); + 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 (auto const & [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; +} + +struct RGBAToPredefined +{ + uint32_t rgba; + PredefinedColor predefinedColor; +}; + +static std::array buildRGBAToPredefined() +{ + auto res = std::array(); + for (size_t i = 0; i < kOrderedPredefinedColors.size(); ++i) + res[i] = {ColorFromPredefinedColor(kOrderedPredefinedColors[i]).GetRGBA(), kOrderedPredefinedColors[i]}; + return res; +} + +auto const kRGBAToPredefined = buildRGBAToPredefined(); + +PredefinedColor MapPredefinedColor(uint32_t rgba) +{ + auto closestColor = kRGBAToPredefined[0].predefinedColor; + auto minDistance = std::numeric_limits::max(); + for (auto const & [rgbaGarmin, color] : kRGBAToPredefined) + { + auto const distance = ColorDistance(rgba, rgbaGarmin); + + if (distance == 0) + return color; // Exact match. + + if (distance < minDistance) + { + minDistance = distance; + closestColor = color; + } + } + return closestColor; +} + // Garmin extensions spec: https://www8.garmin.com/xmlschemas/GpxExtensionsv3.xsd // Color mapping: https://help.locusmap.eu/topic/extend-garmin-gpx-compatibilty std::optional ParseGarminColor(std::string_view c) diff --git a/libs/kml/color_parser.hpp b/libs/kml/color_parser.hpp index f4bb86633..6fb8cb8e4 100644 --- a/libs/kml/color_parser.hpp +++ b/libs/kml/color_parser.hpp @@ -4,6 +4,8 @@ #include #include +#include "types.hpp" + namespace kml { @@ -18,4 +20,7 @@ std::optional ParseHexColor(std::string_view c); std::optional ParseGarminColor(std::string_view c); std::optional ParseOSMColor(std::string_view c); +PredefinedColor MapPredefinedColor(uint32_t rgba); +std::string_view MapGarminColor(uint32_t rgba); + } // namespace kml diff --git a/libs/kml/kml_tests/gpx_tests.cpp b/libs/kml/kml_tests/gpx_tests.cpp index 1d77396fe..7f803623f 100644 --- a/libs/kml/kml_tests/gpx_tests.cpp +++ b/libs/kml/kml_tests/gpx_tests.cpp @@ -1,5 +1,6 @@ #include "testing/testing.hpp" +#include "kml/color_parser.hpp" #include "kml/serdes_common.hpp" #include "kml/serdes_gpx.hpp" @@ -38,9 +39,8 @@ static std::string ReadFile(char const * testFile) return sourceFileText; } -static std::string ReadFileAndSerialize(char const * testFile) +static std::string Serialize(kml::FileData const & dataFromFile) { - kml::FileData const dataFromFile = LoadGpxFromFile(testFile); std::string resultBuffer; MemWriter sink(resultBuffer); kml::gpx::SerializerGpx ser(dataFromFile); @@ -48,6 +48,12 @@ static std::string ReadFileAndSerialize(char const * testFile) return resultBuffer; } +static std::string ReadFileAndSerialize(char const * testFile) +{ + kml::FileData const dataFromFile = LoadGpxFromFile(testFile); + return Serialize(dataFromFile); +} + void ImportExportCompare(char const * testFile) { std::string const sourceFileText = ReadFile(testFile); @@ -323,6 +329,20 @@ UNIT_TEST(Empty) TEST_EQUAL(0, dataFromFile.m_tracksData.size(), ()); } +UNIT_TEST(ImportExportWptColor) +{ + ImportExportCompare("test_data/gpx/point_with_predefined_color_2.gpx"); +} + +UNIT_TEST(PointWithPredefinedColor) +{ + kml::FileData dataFromFile = LoadGpxFromFile("test_data/gpx/point_with_predefined_color_1.gpx"); + dataFromFile.m_bookmarksData[0].m_color.m_predefinedColor = kml::PredefinedColor::Blue; + auto const actual = Serialize(dataFromFile); + auto const expected = ReadFile("test_data/gpx/point_with_predefined_color_2.gpx"); + TEST_EQUAL(expected, actual, ()); +} + UNIT_TEST(OsmandColor1) { kml::FileData const dataFromFile = LoadGpxFromFile("test_data/gpx/osmand1.gpx"); @@ -394,11 +414,11 @@ UNIT_TEST(ParseFromString) 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), ()); + TEST_EQUAL("DarkCyan", kml::MapGarminColor(0x008b8bff), ()); + TEST_EQUAL("White", kml::MapGarminColor(0xffffffff), ()); + TEST_EQUAL("DarkYellow", kml::MapGarminColor(0xb4b820ff), ()); + TEST_EQUAL("DarkYellow", kml::MapGarminColor(0xb6b820ff), ()); + TEST_EQUAL("DarkYellow", kml::MapGarminColor(0xb5b721ff), ()); } } // namespace gpx_tests diff --git a/libs/kml/serdes_gpx.cpp b/libs/kml/serdes_gpx.cpp index 705a15d29..fe9c2dbf2 100644 --- a/libs/kml/serdes_gpx.cpp +++ b/libs/kml/serdes_gpx.cpp @@ -80,11 +80,10 @@ bool GpxParser::MakeValid() // Set default name. if (m_name.empty()) m_name = kml::PointToLineString(m_org); - - // Set default pin. - if (m_predefinedColor == PredefinedColor::None) + if (m_color != kInvalidColor) + m_predefinedColor = MapPredefinedColor(m_color); + else m_predefinedColor = PredefinedColor::Red; - return true; } return false; @@ -428,61 +427,6 @@ 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 (auto const & [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 { @@ -524,6 +468,16 @@ void SaveCategoryData(Writer & writer, CategoryData const & categoryData) writer << "\n"; } +uint32_t BookmarkColor(BookmarkData const & bookmarkData) +{ + auto const & [predefinedColor, rgba] = bookmarkData.m_color; + if (rgba != kInvalidColor) + return rgba; + if (predefinedColor != PredefinedColor::None && predefinedColor != PredefinedColor::Red) + return ColorFromPredefinedColor(predefinedColor).GetRGBA(); + return kInvalidColor; +} + void SaveBookmarkData(Writer & writer, BookmarkData const & bookmarkData) { auto const [lat, lon] = mercator::ToLatLon(bookmarkData.m_point); @@ -544,6 +498,14 @@ void SaveBookmarkData(Writer & writer, BookmarkData const & bookmarkData) SaveStringWithCDATA(writer, *description); writer << "\n"; } + if (auto const color = BookmarkColor(bookmarkData); color != kInvalidColor) + { + writer << kIndent2 << "\n"; + writer << kIndent4 << "#"; + SaveColorToARGB(writer, color); + writer << "\n"; + writer << kIndent2 << "\n"; + } writer << "\n"; } @@ -583,7 +545,7 @@ void SaveTrackData(Writer & writer, TrackData const & trackData) { writer << kIndent2 << "\n"; writer << kIndent4 << ""; - writer << MapGarminColor(color); + writer << kml::MapGarminColor(color); writer << "\n"; writer << kIndent4 << ""; SaveColorToRGB(writer, color); diff --git a/libs/kml/serdes_gpx.hpp b/libs/kml/serdes_gpx.hpp index d5c42a7dc..06022ad24 100644 --- a/libs/kml/serdes_gpx.hpp +++ b/libs/kml/serdes_gpx.hpp @@ -106,8 +106,6 @@ private: std::string BuildDescription() const; }; -std::string_view MapGarminColor(uint32_t rgba); - } // namespace gpx class DeserializerGpx diff --git a/libs/map/bookmark.cpp b/libs/map/bookmark.cpp index 20c8f6d01..858485fb5 100644 --- a/libs/map/bookmark.cpp +++ b/libs/map/bookmark.cpp @@ -58,6 +58,7 @@ std::string GetBookmarkIconType(kml::BookmarkIcon const & icon) std::string const kCustomImageProperty = "CustomImage"; std::string const kHasElevationProfileProperty = "has_elevation_profile"; +int constexpr kInvalidColor = 0; } // namespace Bookmark::Bookmark(m2::PointD const & ptOrg) : Base(ptOrg, UserMark::BOOKMARK), m_groupId(kml::kInvalidMarkGroupId) @@ -185,6 +186,7 @@ void Bookmark::SetColor(kml::PredefinedColor color) { SetDirty(); m_data.m_color.m_predefinedColor = color; + m_data.m_color.m_rgba = kInvalidColor; } std::string Bookmark::GetPreferredName() const