[kml] Factor out ParseXXXColor functions.

Signed-off-by: Viktor Govako <viktor.govako@gmail.com>
This commit is contained in:
Viktor Govako
2025-08-09 14:50:16 -03:00
committed by Konstantin Pastbin
parent 5e8eeb1f20
commit 3d6d08e2a1
9 changed files with 185 additions and 46 deletions

View File

@@ -1,6 +1,8 @@
project(kml) project(kml)
set(SRC set(SRC
color_parser.cpp
color_parser.hpp
header_binary.hpp header_binary.hpp
minzoom_quadtree.hpp minzoom_quadtree.hpp
serdes_common.cpp serdes_common.cpp

122
libs/kml/color_parser.cpp Normal file
View File

@@ -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<uint32_t> 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<uint32_t> ParseGarminColor(std::string_view c)
{
/// @todo Unify with RGBColor instead of string.
static std::pair<std::string_view, std::string_view> 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<uint32_t> ParseOSMColor(std::string_view c)
{
static std::pair<std::string_view, RGBColor> 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

21
libs/kml/color_parser.hpp Normal file
View File

@@ -0,0 +1,21 @@
#pragma once
#include <cstdint>
#include <optional>
#include <string_view>
namespace kml
{
template <typename Channel>
constexpr uint32_t ToRGBA(Channel red, Channel green, Channel blue, Channel alpha = Channel(255))
{
return static_cast<uint8_t>(red) << 24 | static_cast<uint8_t>(green) << 16 | static_cast<uint8_t>(blue) << 8 |
static_cast<uint8_t>(alpha);
}
std::optional<uint32_t> ParseHexColor(std::string_view c);
std::optional<uint32_t> ParseGarminColor(std::string_view c);
std::optional<uint32_t> ParseOSMColor(std::string_view c);
} // namespace kml

View File

@@ -1,6 +1,7 @@
project(kml_tests) project(kml_tests)
set(SRC set(SRC
color_parser_tests.cpp
gpx_tests.cpp gpx_tests.cpp
minzoom_quadtree_tests.cpp minzoom_quadtree_tests.cpp
serdes_tests.cpp serdes_tests.cpp

View File

@@ -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"), ());
}

View File

@@ -1,4 +1,5 @@
#include "kml/serdes.hpp" #include "kml/serdes.hpp"
#include "kml/color_parser.hpp"
#include "indexer/classificator.hpp" #include "indexer/classificator.hpp"

View File

@@ -1,4 +1,5 @@
#pragma once #pragma once
#include "kml/type_utils.hpp"
#include "coding/string_utf8_multilang.hpp" #include "coding/string_utf8_multilang.hpp"
#include "coding/writer.hpp" #include "coding/writer.hpp"
@@ -6,21 +7,12 @@
#include "geometry/point2d.hpp" #include "geometry/point2d.hpp"
#include "geometry/point_with_altitude.hpp" #include "geometry/point_with_altitude.hpp"
#include "type_utils.hpp"
namespace kml namespace kml
{ {
auto constexpr kDefaultLang = StringUtf8Multilang::kDefaultCode; auto constexpr kDefaultLang = StringUtf8Multilang::kDefaultCode;
auto constexpr kDefaultTrackWidth = 5.0; auto constexpr kDefaultTrackWidth = 5.0;
auto constexpr kDefaultTrackColor = 0x006ec7ff; auto constexpr kDefaultTrackColor = 0x006ec7ff;
template <typename Channel>
uint32_t ToRGBA(Channel red, Channel green, Channel blue, Channel alpha)
{
return static_cast<uint8_t>(red) << 24 | static_cast<uint8_t>(green) << 16 | static_cast<uint8_t>(blue) << 8 |
static_cast<uint8_t>(alpha);
}
std::string PointToString(m2::PointD const & org, char const separator); std::string PointToString(m2::PointD const & org, char const separator);
std::string PointToLineString(geometry::PointWithAltitude const & pt); std::string PointToLineString(geometry::PointWithAltitude const & pt);

View File

@@ -1,4 +1,5 @@
#include "kml/serdes_gpx.hpp" #include "kml/serdes_gpx.hpp"
#include "kml/color_parser.hpp"
#include "kml/serdes_common.hpp" #include "kml/serdes_common.hpp"
#include "coding/hex.hpp" #include "coding/hex.hpp"
@@ -134,31 +135,16 @@ std::string const & GpxParser::GetTagFromEnd(size_t n) const
std::optional<uint32_t> GpxParser::ParseColorFromHexString(std::string_view colorStr) std::optional<uint32_t> GpxParser::ParseColorFromHexString(std::string_view colorStr)
{ {
if (colorStr.empty()) auto const res = ParseHexColor(colorStr);
{ if (!res)
LOG(LWARNING, ("Invalid color value", colorStr)); LOG(LWARNING, ("Invalid color value", colorStr));
return {}; return res;
}
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) void GpxParser::ParseColor(std::string_view colorStr)
{ {
if (auto const parsed = ParseColorFromHexString(colorStr); parsed) if (auto const parsed = ParseColorFromHexString(colorStr))
m_color = parsed.value(); m_color = *parsed;
} }
// https://osmand.net/docs/technical/osmand-file-formats/osmand-gpx/. Supported colors: // 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); auto const color = ParseColorFromHexString(value);
if (!color) if (!color)
return; return;
if (m_tags.size() > 2 && GetTagFromEnd(2) == gpx::kGpx) if (m_tags.size() > 2 && GetTagFromEnd(2) == gpx::kGpx)
{ {
m_globalColor = *color; m_globalColor = *color;
@@ -176,31 +163,19 @@ void GpxParser::ParseOsmandColor(std::string const & value)
layer.m_color.m_rgba = m_globalColor; layer.m_color.m_rgba = m_globalColor;
} }
else else
{
m_color = *color; 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) void GpxParser::ParseGarminColor(std::string const & v)
{ {
static std::unordered_map<std::string, std::string> const kGarminToHex = { auto const res = kml::ParseGarminColor(v);
{"Black", "000000"}, {"DarkRed", "8b0000"}, {"DarkGreen", "006400"}, {"DarkYellow", "b5b820"}, if (!res)
{"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
{ {
LOG(LWARNING, ("Unsupported color value", v)); 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() void GpxParser::CheckAndCorrectTimestamps()

View File

@@ -15,6 +15,7 @@
45E456142058509200D9F45E /* testingmain.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 45E456122058508C00D9F45E /* testingmain.cpp */; }; 45E456142058509200D9F45E /* testingmain.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 45E456122058508C00D9F45E /* testingmain.cpp */; };
464344F3294F952700984CB7 /* gpx_tests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 464344F2294F952700984CB7 /* gpx_tests.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 */; }; 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 */; }; 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 */; }; 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 */; }; 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 = "<group>"; }; 464344F2294F952700984CB7 /* gpx_tests.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = gpx_tests.cpp; sourceTree = "<group>"; };
464BD0FB294546B20011955A /* serdes_gpx.cpp */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.cpp.cpp; path = serdes_gpx.cpp; sourceTree = "<group>"; }; 464BD0FB294546B20011955A /* serdes_gpx.cpp */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.cpp.cpp; path = serdes_gpx.cpp; sourceTree = "<group>"; };
464BD0FC294546B20011955A /* serdes_gpx.hpp */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.cpp.h; path = serdes_gpx.hpp; sourceTree = "<group>"; }; 464BD0FC294546B20011955A /* serdes_gpx.hpp */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.cpp.h; path = serdes_gpx.hpp; sourceTree = "<group>"; };
ACBAA59C2E47C53800769B1B /* color_parser.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = color_parser.hpp; sourceTree = "<group>"; };
ACBAA59D2E47C53800769B1B /* color_parser.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = color_parser.cpp; sourceTree = "<group>"; };
ACDD8A772A736045000F2C43 /* serdes_common.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = serdes_common.hpp; sourceTree = "<group>"; }; ACDD8A772A736045000F2C43 /* serdes_common.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = serdes_common.hpp; sourceTree = "<group>"; };
ACDD8A782A736045000F2C43 /* serdes_common.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = serdes_common.cpp; sourceTree = "<group>"; }; ACDD8A782A736045000F2C43 /* serdes_common.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = serdes_common.cpp; sourceTree = "<group>"; };
E2AA225925275C1D002589E2 /* minzoom_quadtree.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = minzoom_quadtree.hpp; sourceTree = "<group>"; }; E2AA225925275C1D002589E2 /* minzoom_quadtree.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = minzoom_quadtree.hpp; sourceTree = "<group>"; };
@@ -136,6 +139,8 @@
45E4557F205849A600D9F45E /* kml */ = { 45E4557F205849A600D9F45E /* kml */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
ACBAA59D2E47C53800769B1B /* color_parser.cpp */,
ACBAA59C2E47C53800769B1B /* color_parser.hpp */,
45E4558D20584AB900D9F45E /* header_binary.hpp */, 45E4558D20584AB900D9F45E /* header_binary.hpp */,
E2AA225925275C1D002589E2 /* minzoom_quadtree.hpp */, E2AA225925275C1D002589E2 /* minzoom_quadtree.hpp */,
45E4558E20584AB900D9F45E /* serdes.cpp */, 45E4558E20584AB900D9F45E /* serdes.cpp */,
@@ -284,6 +289,7 @@
464344F3294F952700984CB7 /* gpx_tests.cpp in Sources */, 464344F3294F952700984CB7 /* gpx_tests.cpp in Sources */,
4568C86420BD455700E2192B /* type_utils.cpp in Sources */, 4568C86420BD455700E2192B /* type_utils.cpp in Sources */,
46AA9E60294549B000ECED73 /* serdes_gpx.cpp in Sources */, 46AA9E60294549B000ECED73 /* serdes_gpx.cpp in Sources */,
ACBAA59E2E47C53800769B1B /* color_parser.cpp in Sources */,
45E4559520584ABA00D9F45E /* serdes.cpp in Sources */, 45E4559520584ABA00D9F45E /* serdes.cpp in Sources */,
E2DC9C9125264E3E0098174E /* types.cpp in Sources */, E2DC9C9125264E3E0098174E /* types.cpp in Sources */,
ACDD8A7B2A73684F000F2C43 /* serdes_common.cpp in Sources */, ACDD8A7B2A73684F000F2C43 /* serdes_common.cpp in Sources */,