From 3d6d08e2a12618ec5abaefc2b81a209ed79c3175 Mon Sep 17 00:00:00 2001 From: Viktor Govako Date: Sat, 9 Aug 2025 14:50:16 -0300 Subject: [PATCH] [kml] Factor out ParseXXXColor functions. Signed-off-by: Viktor Govako --- libs/kml/CMakeLists.txt | 2 + libs/kml/color_parser.cpp | 122 ++++++++++++++++++++++ libs/kml/color_parser.hpp | 21 ++++ libs/kml/kml_tests/CMakeLists.txt | 1 + libs/kml/kml_tests/color_parser_tests.cpp | 19 ++++ libs/kml/serdes.cpp | 1 + libs/kml/serdes_common.hpp | 10 +- libs/kml/serdes_gpx.cpp | 49 +++------ xcode/kml/kml.xcodeproj/project.pbxproj | 6 ++ 9 files changed, 185 insertions(+), 46 deletions(-) create mode 100644 libs/kml/color_parser.cpp create mode 100644 libs/kml/color_parser.hpp create mode 100644 libs/kml/kml_tests/color_parser_tests.cpp diff --git a/libs/kml/CMakeLists.txt b/libs/kml/CMakeLists.txt index 4bfa286a5..39c34ff72 100644 --- a/libs/kml/CMakeLists.txt +++ b/libs/kml/CMakeLists.txt @@ -1,6 +1,8 @@ project(kml) set(SRC + color_parser.cpp + color_parser.hpp header_binary.hpp minzoom_quadtree.hpp serdes_common.cpp diff --git a/libs/kml/color_parser.cpp b/libs/kml/color_parser.cpp new file mode 100644 index 000000000..2e66603ae --- /dev/null +++ b/libs/kml/color_parser.cpp @@ -0,0 +1,122 @@ +#include "color_parser.hpp" + +#include "coding/hex.hpp" + +#include "base/string_utils.hpp" + +namespace kml +{ + +struct RGBColor +{ + uint8_t r, g, b; +}; + +std::optional ParseHexColor(std::string_view c) +{ + if (c.empty()) + return {}; + + if (c.front() == '#') + c.remove_prefix(1); + if (c.size() != 6 && c.size() != 8) + return {}; + + auto const colorBytes = FromHex(c); + switch (colorBytes.size()) + { + case 3: return ToRGBA(colorBytes[0], colorBytes[1], colorBytes[2]); + case 4: return ToRGBA(colorBytes[1], colorBytes[2], colorBytes[3], colorBytes[0]); + default: return {}; + } +} + +// 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) +{ + /// @todo Unify with RGBColor instead of string. + static std::pair arrColors[] = { + {"Black", "000000"}, {"DarkRed", "8b0000"}, {"DarkGreen", "006400"}, {"DarkYellow", "b5b820"}, + {"DarkBlue", "00008b"}, {"DarkMagenta", "8b008b"}, {"DarkCyan", "008b8b"}, {"LightGray", "cccccc"}, + {"DarkGray", "444444"}, {"Red", "ff0000"}, {"Green", "00ff00"}, {"Yellow", "ffff00"}, + {"Blue", "0000ff"}, {"Magenta", "ff00ff"}, {"Cyan", "00ffff"}, {"White", "ffffff"}}; + + for (auto const & e : arrColors) + if (c == e.first) + return ParseHexColor(e.second); + + return {}; +} + +std::optional ParseOSMColor(std::string_view c) +{ + static std::pair arrColors[] = { + {"black", {0, 0, 0}}, + {"white", {255, 255, 255}}, + {"red", {255, 0, 0}}, + {"green", {0, 128, 0}}, + {"blue", {0, 0, 255}}, + {"yellow", {255, 255, 0}}, + {"orange", {255, 165, 0}}, + {"gray", {128, 128, 128}}, + {"grey", {128, 128, 128}}, // British spelling + {"brown", {165, 42, 42}}, + {"pink", {255, 192, 203}}, + {"purple", {128, 0, 128}}, + {"cyan", {0, 255, 255}}, + {"magenta", {255, 0, 255}}, + + {"maroon", {128, 0, 0}}, + {"olive", {128, 128, 0}}, + {"teal", {0, 128, 128}}, + {"navy", {0, 0, 128}}, + {"silver", {192, 192, 192}}, + {"lime", {0, 255, 0}}, + {"aqua", {0, 255, 255}}, // cyan + {"fuchsia", {255, 0, 255}}, // magenta + + // From top taginfo for "colour" and CSS standart values. + {"darkgreen", {0, 100, 0}}, + {"beige", {245, 245, 220}}, + {"dimgray", {105, 105, 105}}, + {"lightgrey", {211, 211, 211}}, // British spelling + {"lightgray", {211, 211, 211}}, + {"tan", {210, 180, 140}}, + {"gold", {255, 215, 0}}, + + {"red;white", {255, 127, 127}}, + {"red and white", {255, 127, 127}}, + {"red-white", {255, 127, 127}}, + }; + + if (!c.empty()) + { + if (c[0] == '#') + { + using strings::to_uint; + if (c.size() == 7) // #rrggbb + { + uint8_t r, g, b; + if (to_uint(c.substr(1, 2), r, 16) && to_uint(c.substr(3, 2), g, 16) && to_uint(c.substr(5, 2), b, 16)) + return ToRGBA(r, g, b); + } + else if (c.size() == 4) // #rgb shorthand + { + uint8_t r, g, b; + if (to_uint(c.substr(1, 1), r, 16) && to_uint(c.substr(2, 1), g, 16) && to_uint(c.substr(3, 1), b, 16)) + return ToRGBA(uint8_t(r * 17), uint8_t(g * 17), uint8_t(b * 17)); + } + } + else + { + for (auto const & e : arrColors) + if (c == e.first) + return ToRGBA(e.second.r, e.second.g, e.second.b); + } + } + + return {}; +} + +} // namespace kml diff --git a/libs/kml/color_parser.hpp b/libs/kml/color_parser.hpp new file mode 100644 index 000000000..f4bb86633 --- /dev/null +++ b/libs/kml/color_parser.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include + +namespace kml +{ + +template +constexpr uint32_t ToRGBA(Channel red, Channel green, Channel blue, Channel alpha = Channel(255)) +{ + return static_cast(red) << 24 | static_cast(green) << 16 | static_cast(blue) << 8 | + static_cast(alpha); +} + +std::optional ParseHexColor(std::string_view c); +std::optional ParseGarminColor(std::string_view c); +std::optional ParseOSMColor(std::string_view c); + +} // namespace kml diff --git a/libs/kml/kml_tests/CMakeLists.txt b/libs/kml/kml_tests/CMakeLists.txt index c74eb3436..04593f1d5 100644 --- a/libs/kml/kml_tests/CMakeLists.txt +++ b/libs/kml/kml_tests/CMakeLists.txt @@ -1,6 +1,7 @@ project(kml_tests) set(SRC + color_parser_tests.cpp gpx_tests.cpp minzoom_quadtree_tests.cpp serdes_tests.cpp diff --git a/libs/kml/kml_tests/color_parser_tests.cpp b/libs/kml/kml_tests/color_parser_tests.cpp new file mode 100644 index 000000000..90eb96320 --- /dev/null +++ b/libs/kml/kml_tests/color_parser_tests.cpp @@ -0,0 +1,19 @@ +#include "testing/testing.hpp" + +#include "kml/color_parser.hpp" + +UNIT_TEST(ColorParser_Smoke) +{ + auto const magenta = kml::ParseGarminColor("Magenta"); + TEST(magenta, ()); + TEST_EQUAL(magenta, kml::ParseOSMColor("magenta"), ()); + TEST_EQUAL(magenta, kml::ParseHexColor("ff00ff"), ()); + TEST_EQUAL(magenta, kml::ParseHexColor("#ff00ff"), ()); + TEST_EQUAL(magenta, kml::ParseOSMColor("#f0f"), ()); + + TEST(!kml::ParseGarminColor("xxyyzz"), ()); + TEST(!kml::ParseOSMColor("#xxyyzz"), ()); + + // Current implementation gives assert with default 0 channel value. I didn't change this. + // TEST(!kml::ParseHexColor("#xxyyzz"), ()); +} diff --git a/libs/kml/serdes.cpp b/libs/kml/serdes.cpp index ae40ce442..b1007a6a2 100644 --- a/libs/kml/serdes.cpp +++ b/libs/kml/serdes.cpp @@ -1,4 +1,5 @@ #include "kml/serdes.hpp" +#include "kml/color_parser.hpp" #include "indexer/classificator.hpp" diff --git a/libs/kml/serdes_common.hpp b/libs/kml/serdes_common.hpp index 8c05cfa94..8b40547f9 100644 --- a/libs/kml/serdes_common.hpp +++ b/libs/kml/serdes_common.hpp @@ -1,4 +1,5 @@ #pragma once +#include "kml/type_utils.hpp" #include "coding/string_utf8_multilang.hpp" #include "coding/writer.hpp" @@ -6,21 +7,12 @@ #include "geometry/point2d.hpp" #include "geometry/point_with_altitude.hpp" -#include "type_utils.hpp" - namespace kml { auto constexpr kDefaultLang = StringUtf8Multilang::kDefaultCode; auto constexpr kDefaultTrackWidth = 5.0; auto constexpr kDefaultTrackColor = 0x006ec7ff; -template -uint32_t ToRGBA(Channel red, Channel green, Channel blue, Channel alpha) -{ - return static_cast(red) << 24 | static_cast(green) << 16 | static_cast(blue) << 8 | - static_cast(alpha); -} - std::string PointToString(m2::PointD const & org, char const separator); std::string PointToLineString(geometry::PointWithAltitude const & pt); diff --git a/libs/kml/serdes_gpx.cpp b/libs/kml/serdes_gpx.cpp index cb6e2e0ff..705a15d29 100644 --- a/libs/kml/serdes_gpx.cpp +++ b/libs/kml/serdes_gpx.cpp @@ -1,4 +1,5 @@ #include "kml/serdes_gpx.hpp" +#include "kml/color_parser.hpp" #include "kml/serdes_common.hpp" #include "coding/hex.hpp" @@ -134,31 +135,16 @@ std::string const & GpxParser::GetTagFromEnd(size_t n) const std::optional GpxParser::ParseColorFromHexString(std::string_view colorStr) { - if (colorStr.empty()) - { + auto const res = ParseHexColor(colorStr); + if (!res) LOG(LWARNING, ("Invalid color value", colorStr)); - return {}; - } - 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 {}; - } + return res; } void GpxParser::ParseColor(std::string_view colorStr) { - if (auto const parsed = ParseColorFromHexString(colorStr); parsed) - m_color = parsed.value(); + if (auto const parsed = ParseColorFromHexString(colorStr)) + m_color = *parsed; } // https://osmand.net/docs/technical/osmand-file-formats/osmand-gpx/. Supported colors: @@ -168,6 +154,7 @@ void GpxParser::ParseOsmandColor(std::string const & value) auto const color = ParseColorFromHexString(value); if (!color) return; + if (m_tags.size() > 2 && GetTagFromEnd(2) == gpx::kGpx) { m_globalColor = *color; @@ -176,31 +163,19 @@ void GpxParser::ParseOsmandColor(std::string const & value) layer.m_color.m_rgba = m_globalColor; } else - { m_color = *color; - } } -// Garmin extensions spec: https://www8.garmin.com/xmlschemas/GpxExtensionsv3.xsd -// Color mapping: https://help.locusmap.eu/topic/extend-garmin-gpx-compatibilty void GpxParser::ParseGarminColor(std::string const & v) { - static std::unordered_map const kGarminToHex = { - {"Black", "000000"}, {"DarkRed", "8b0000"}, {"DarkGreen", "006400"}, {"DarkYellow", "b5b820"}, - {"DarkBlue", "00008b"}, {"DarkMagenta", "8b008b"}, {"DarkCyan", "008b8b"}, {"LightGray", "cccccc"}, - {"DarkGray", "444444"}, {"Red", "ff0000"}, {"Green", "00ff00"}, {"Yellow", "ffff00"}, - {"Blue", "0000ff"}, {"Magenta", "ff00ff"}, {"Cyan", "00ffff"}, {"White", "ffffff"}, - {"Transparent", "ff0000"}}; - auto const it = kGarminToHex.find(v); - if (it != kGarminToHex.end()) - { - return ParseColor(it->second); - } - else + auto const res = kml::ParseGarminColor(v); + if (!res) { LOG(LWARNING, ("Unsupported color value", v)); - return ParseColor("ff0000"); // default to red + m_color = ToRGBA(255, 0, 0); // default to red } + else + m_color = *res; } void GpxParser::CheckAndCorrectTimestamps() diff --git a/xcode/kml/kml.xcodeproj/project.pbxproj b/xcode/kml/kml.xcodeproj/project.pbxproj index d56d1c68a..9f97bb2b2 100644 --- a/xcode/kml/kml.xcodeproj/project.pbxproj +++ b/xcode/kml/kml.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 45E456142058509200D9F45E /* testingmain.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 45E456122058508C00D9F45E /* testingmain.cpp */; }; 464344F3294F952700984CB7 /* gpx_tests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 464344F2294F952700984CB7 /* gpx_tests.cpp */; }; 46AA9E60294549B000ECED73 /* serdes_gpx.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 464BD0FB294546B20011955A /* serdes_gpx.cpp */; }; + ACBAA59E2E47C53800769B1B /* color_parser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = ACBAA59D2E47C53800769B1B /* color_parser.cpp */; }; ACDD8A7B2A73684F000F2C43 /* serdes_common.cpp in Sources */ = {isa = PBXBuildFile; fileRef = ACDD8A782A736045000F2C43 /* serdes_common.cpp */; }; E2AA225E25275C6B002589E2 /* minzoom_quadtree_tests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E2AA225C25275C6B002589E2 /* minzoom_quadtree_tests.cpp */; }; E2DC9C9125264E3E0098174E /* types.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E2DC9C9025264E3E0098174E /* types.cpp */; }; @@ -60,6 +61,8 @@ 464344F2294F952700984CB7 /* gpx_tests.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = gpx_tests.cpp; sourceTree = ""; }; 464BD0FB294546B20011955A /* serdes_gpx.cpp */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.cpp.cpp; path = serdes_gpx.cpp; sourceTree = ""; }; 464BD0FC294546B20011955A /* serdes_gpx.hpp */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.cpp.h; path = serdes_gpx.hpp; sourceTree = ""; }; + ACBAA59C2E47C53800769B1B /* color_parser.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = color_parser.hpp; sourceTree = ""; }; + ACBAA59D2E47C53800769B1B /* color_parser.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = color_parser.cpp; sourceTree = ""; }; ACDD8A772A736045000F2C43 /* serdes_common.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = serdes_common.hpp; sourceTree = ""; }; ACDD8A782A736045000F2C43 /* serdes_common.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = serdes_common.cpp; sourceTree = ""; }; E2AA225925275C1D002589E2 /* minzoom_quadtree.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = minzoom_quadtree.hpp; sourceTree = ""; }; @@ -136,6 +139,8 @@ 45E4557F205849A600D9F45E /* kml */ = { isa = PBXGroup; children = ( + ACBAA59D2E47C53800769B1B /* color_parser.cpp */, + ACBAA59C2E47C53800769B1B /* color_parser.hpp */, 45E4558D20584AB900D9F45E /* header_binary.hpp */, E2AA225925275C1D002589E2 /* minzoom_quadtree.hpp */, 45E4558E20584AB900D9F45E /* serdes.cpp */, @@ -284,6 +289,7 @@ 464344F3294F952700984CB7 /* gpx_tests.cpp in Sources */, 4568C86420BD455700E2192B /* type_utils.cpp in Sources */, 46AA9E60294549B000ECED73 /* serdes_gpx.cpp in Sources */, + ACBAA59E2E47C53800769B1B /* color_parser.cpp in Sources */, 45E4559520584ABA00D9F45E /* serdes.cpp in Sources */, E2DC9C9125264E3E0098174E /* types.cpp in Sources */, ACDD8A7B2A73684F000F2C43 /* serdes_common.cpp in Sources */,