[gpx] Add xml schema to color in gpx export

Signed-off-by: cyber-toad <the.cyber.toad@proton.me>
This commit is contained in:
cyber-toad 2025-01-18 00:14:54 +01:00 committed by Alexander Borsuk
parent 6b6b7d145e
commit 1f0a6760e0
7 changed files with 265 additions and 65 deletions

View file

@ -0,0 +1,28 @@
<?xml version="1.0"?>
<gpx version="1.1" creator="Organic Maps" xmlns="http://www.topografix.com/GPX/1/1"
xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3"
xmlns:gpx_style="http://www.topografix.com/GPX/gpx_style/0/2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 https://www.topografix.com/GPX/1/1/gpx.xsd http://www.topografix.com/GPX/gpx_style/0/2 https://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 https://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd">
<metadata>
<name>new</name>
<desc>Short description</desc>
</metadata>
<trk>
<name>new red</name>
<desc>description 1</desc>
<extensions>
<gpxx:TrackExtension><gpxx:DisplayColor>DarkGray</gpxx:DisplayColor></gpxx:TrackExtension>
<gpx_style:line><gpx_style:color>443344</gpx_style:color></gpx_style:line>
<xsi:gpx><color>#FF443344</color></xsi:gpx>
</extensions>
<trkseg>
<trkpt lat="48.209846" lon="16.376023">
<ele>123</ele>
</trkpt>
<trkpt lat="48.209855" lon="16.376066">
<ele>123</ele>
</trkpt>
</trkseg>
</trk>
</gpx>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<gpx xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.topografix.com/GPX/1/1" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns:gpx_style="http://www.topografix.com/GPX/gpx_style/0/2" version="1.1" creator="https://gpx.studio">
<metadata>
<name>new</name>
<author>
<name>gpx.studio</name>
<link href="https://gpx.studio"></link>
</author>
<desc>Short description</desc>
</metadata>
<trk>
<name>new red</name>
<desc>description 1</desc>
<type>Running</type>
<extensions>
<gpx_style:line>
<color>443344</color>
</gpx_style:line>
</extensions>
<trkseg>
<trkpt lat="48.209846" lon="16.376023">
<ele>123</ele>
</trkpt>
<trkpt lat="48.209855" lon="16.376066">
<ele>123</ele>
</trkpt>
</trkseg>
</trk>
</gpx>

View file

@ -1,5 +1,9 @@
<?xml version="1.0"?>
<gpx xmlns="http://www.topografix.com/GPX/1/1" version="1.1">
<gpx version="1.1" creator="Organic Maps" xmlns="http://www.topografix.com/GPX/1/1"
xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3"
xmlns:gpx_style="http://www.topografix.com/GPX/gpx_style/0/2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 https://www.topografix.com/GPX/1/1/gpx.xsd http://www.topografix.com/GPX/gpx_style/0/2 https://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 https://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd">
<metadata>
<name>My route</name>
<desc><![CDATA[You will see this in the document and can use reserved characters like < > & "]]></desc>
@ -15,7 +19,9 @@
<trk>
<name>Some random route</name>
<extensions>
<color>00FF00</color>
<gpxx:TrackExtension><gpxx:DisplayColor>Green</gpxx:DisplayColor></gpxx:TrackExtension>
<gpx_style:line><gpx_style:color>00FF00</gpx_style:color></gpx_style:line>
<xsi:gpx><color>#FF00FF00</color></xsi:gpx>
</extensions>
<trkseg>
<trkpt lat="48.209846" lon="16.376023">

View file

@ -1,5 +1,9 @@
<?xml version="1.0"?>
<gpx xmlns="http://www.topografix.com/GPX/1/1" version="1.1">
<gpx version="1.1" creator="Organic Maps" xmlns="http://www.topografix.com/GPX/1/1"
xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3"
xmlns:gpx_style="http://www.topografix.com/GPX/gpx_style/0/2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 https://www.topografix.com/GPX/1/1/gpx.xsd http://www.topografix.com/GPX/gpx_style/0/2 https://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 https://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd">
<metadata>
</metadata>
</gpx>

View file

@ -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<decltype(resultBuffer)> 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)
</wpt>
)";
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)
</gpx>
)";
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)
</gpx>
)";
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
</wpt>
<wpt lat="1" lon="2"><name>5</name><cmt>qqq</cmt><desc>qqq</desc></wpt>
)";
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<uint32_t>(0x1FF), kml::gpx::GpxParser::ParseColorFromHexString("000001"), ());
TEST_EQUAL(std::optional<uint32_t>(0x100FF), kml::gpx::GpxParser::ParseColorFromHexString("000100"), ());
TEST_EQUAL(std::optional<uint32_t>(0x10000FF), kml::gpx::GpxParser::ParseColorFromHexString("010000"), ());
TEST_EQUAL(std::optional<uint32_t>(0x1FF), kml::gpx::GpxParser::ParseColorFromHexString("#000001"), ());
TEST_EQUAL(std::optional<uint32_t>(0x100FF), kml::gpx::GpxParser::ParseColorFromHexString("#000100"), ());
TEST_EQUAL(std::optional<uint32_t>(0x10000FF), kml::gpx::GpxParser::ParseColorFromHexString("#010000"), ());
TEST_EQUAL(std::optional<uint32_t>(0x1FF), kml::gpx::GpxParser::ParseColorFromHexString("#FF000001"), ());
TEST_EQUAL(std::optional<uint32_t>(0x100FF), kml::gpx::GpxParser::ParseColorFromHexString("#FF000100"), ());
TEST_EQUAL(std::optional<uint32_t>(0x10000FF), kml::gpx::GpxParser::ParseColorFromHexString("#FF010000"), ());
TEST_EQUAL(std::optional<uint32_t>(0x10000AA), kml::gpx::GpxParser::ParseColorFromHexString("#AA010000"), ());
TEST_EQUAL(std::optional<uint32_t>(), 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

View file

@ -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 =
"<?xml version=\"1.0\"?>\n"
"<gpx xmlns=\"http://www.topografix.com/GPX/1/1\" version=\"1.1\">\n";
std::string_view constexpr kGpxHeader = R"(<?xml version="1.0"?>
<gpx version="1.1" creator="Organic Maps" xmlns="http://www.topografix.com/GPX/1/1"
xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3"
xmlns:gpx_style="http://www.topografix.com/GPX/gpx_style/0/2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 https://www.topografix.com/GPX/1/1/gpx.xsd http://www.topografix.com/GPX/gpx_style/0/2 https://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 https://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd">
)";
std::string_view constexpr kGpxFooter = "</gpx>";
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<uint32_t> 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<int, int, int> 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<RGBAToGarmin>({
{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<int>::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<uint8_t>((rgba >> 8) & 0xFF));
}
void SaveColorToARGB(Writer & writer, uint32_t rgba)
{
writer << NumToHex(static_cast<uint8_t>(rgba & 0xFF))
<< NumToHex(static_cast<uint8_t>(rgba >> 24 & 0xFF))
<< NumToHex(static_cast<uint8_t>((rgba >> 16) & 0xFF))
<< NumToHex(static_cast<uint8_t>((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 << "<extensions>\n" << kIndent4 << "<color>";
writer << kIndent2 << "<extensions>\n";
writer << kIndent4 << "<gpxx:TrackExtension><gpxx:DisplayColor>";
writer << MapGarminColor(color);
writer << "</gpxx:DisplayColor></gpxx:TrackExtension>\n";
writer << kIndent4 << "<gpx_style:line><gpx_style:color>";
SaveColorToRGB(writer, color);
writer << "</color>\n" << kIndent2 << "</extensions>\n";
writer << "</gpx_style:color></gpx_style:line>\n";
writer << kIndent4 << "<xsi:gpx><color>#";
SaveColorToARGB(writer, color);
writer << "</color></xsi:gpx>\n";
writer << kIndent2 << "</extensions>\n";
}
bool const trackHasAltitude = TrackHasAltitudes(trackData);
auto const & geom = trackData.m_geometry;

View file

@ -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<uint32_t> 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