diff --git a/coding/writer.hpp b/coding/writer.hpp
index b190c044a4..46f22829d5 100644
--- a/coding/writer.hpp
+++ b/coding/writer.hpp
@@ -34,6 +34,12 @@ public:
virtual uint64_t Pos() const = 0;
virtual void Write(void const * p, size_t size) = 0;
+ Writer & operator<<(std::string_view str)
+ {
+ Write(str.data(), str.length());
+ return *this;
+ }
+
// Disable deletion via this interface, because some dtors in derived classes are noexcept(false).
protected:
~Writer() = default;
diff --git a/data/gpx_test_data/export_test.gpx b/data/gpx_test_data/export_test.gpx
new file mode 100644
index 0000000000..36aa0f3e1a
--- /dev/null
+++ b/data/gpx_test_data/export_test.gpx
@@ -0,0 +1,25 @@
+
+
+
+ My route
+ & "]]>
+
+
+ Point 1
+ Point 1
+
+
+ Some random route
+
+ 00FF00
+
+
+
+ 123
+
+
+ 456
+
+
+
+
\ No newline at end of file
diff --git a/data/gpx_test_data/export_test_empty.gpx b/data/gpx_test_data/export_test_empty.gpx
new file mode 100644
index 0000000000..ad945c2945
--- /dev/null
+++ b/data/gpx_test_data/export_test_empty.gpx
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/kml/kml_tests/gpx_tests.cpp b/kml/kml_tests/gpx_tests.cpp
index 2ba28399c6..dd72be162b 100644
--- a/kml/kml_tests/gpx_tests.cpp
+++ b/kml/kml_tests/gpx_tests.cpp
@@ -31,6 +31,29 @@ static kml::FileData loadGpxFromFile(std::string const & file) {
return loadGpxFromString(text);
}
+void importExportCompare(char const * testFile)
+{
+ auto const fileName = GetPlatform().TestsDataPathForFile(testFile);
+ std::string sourceFileText;
+ FileReader(fileName).ReadAsString(sourceFileText);
+ kml::FileData const dataFromFile = loadGpxFromFile(testFile);
+ std::string resultBuffer;
+ MemWriter sink(resultBuffer);
+ kml::gpx::SerializerGpx ser(dataFromFile);
+ ser.Serialize(sink);
+ TEST_EQUAL(resultBuffer, sourceFileText, ());
+}
+
+UNIT_TEST(Gpx_ImportExport_Test)
+{
+ importExportCompare("gpx_test_data/export_test.gpx");
+}
+
+UNIT_TEST(Gpx_ImportExportEmpty_Test)
+{
+ importExportCompare("gpx_test_data/export_test_empty.gpx");
+}
+
UNIT_TEST(Gpx_Test_Point)
{
std::string_view constexpr input = R"(
diff --git a/kml/serdes.cpp b/kml/serdes.cpp
index 3be716984d..543116c821 100644
--- a/kml/serdes.cpp
+++ b/kml/serdes.cpp
@@ -50,13 +50,6 @@ std::string const kExtendedDataFooter =
std::string const kCompilationFooter = "" + kCompilation + ">\n";
-std::string_view constexpr kIndent0 = {};
-std::string_view constexpr kIndent2 = {" "};
-std::string_view constexpr kIndent4 = {" "};
-std::string_view constexpr kIndent6 = {" "};
-std::string_view constexpr kIndent8 = {" "};
-std::string_view constexpr kIndent10 = {" "};
-
std::string GetLocalizableString(LocalizableString const & s, int8_t lang)
{
auto const it = s.find(lang);
@@ -142,39 +135,7 @@ BookmarkIcon GetIcon(std::string const & iconName)
return BookmarkIcon::None;
}
-void SaveStringWithCDATA(KmlWriter::WriterWrapper & writer, std::string s)
-{
- if (s.empty())
- return;
-
- // Expat loads XML 1.0 only. Sometimes users copy and paste bookmark descriptions or even names from the web.
- // Rarely, in these copy-pasted texts, there are invalid XML1.0 symbols.
- // See https://en.wikipedia.org/wiki/Valid_characters_in_XML
- // A robust solution requires parsing invalid XML on loading (then users can restore "bad" XML files), see
- // https://github.com/organicmaps/organicmaps/issues/3837
- // When a robust solution is implemented, this workaround can be removed for better performance/battery.
- //
- // This solution is a simple ASCII-range check that does not check symbols from other unicode ranges
- // (they will require a more complex and slower approach of converting UTF-8 string to unicode first).
- // It should be enough for many cases, according to user reports and wrong characters in their data.
- s.erase(std::remove_if(s.begin(), s.end(), [](unsigned char c)
- {
- if (c >= 0x20 || c == 0x09 || c == 0x0a || c == 0x0d)
- return false;
- return true;
- }), s.end());
-
- if (s.empty())
- return;
-
- // According to kml/xml spec, we need to escape special symbols with CDATA.
- if (s.find_first_of("<&") != std::string::npos)
- writer << "";
- else
- writer << s;
-}
-
-void SaveStyle(KmlWriter::WriterWrapper & writer, std::string const & style,
+void SaveStyle(Writer & writer, std::string const & style,
std::string_view const & indent)
{
if (style.empty())
@@ -189,7 +150,7 @@ void SaveStyle(KmlWriter::WriterWrapper & writer, std::string const & style,
<< indent << kIndent2 << "\n";
}
-void SaveColorToABGR(KmlWriter::WriterWrapper & writer, uint32_t rgba)
+void SaveColorToABGR(Writer & writer, uint32_t rgba)
{
writer << NumToHex(static_cast(rgba & 0xFF))
<< NumToHex(static_cast((rgba >> 8) & 0xFF))
@@ -206,7 +167,7 @@ std::string TimestampToString(Timestamp const & timestamp)
return strTimeStamp;
}
-void SaveLocalizableString(KmlWriter::WriterWrapper & writer, LocalizableString const & str,
+void SaveLocalizableString(Writer & writer, LocalizableString const & str,
std::string const & tagName, std::string_view const & indent)
{
writer << indent << "\n";
@@ -221,7 +182,7 @@ void SaveLocalizableString(KmlWriter::WriterWrapper & writer, LocalizableString
}
template
-void SaveStringsArray(KmlWriter::WriterWrapper & writer,
+void SaveStringsArray(Writer & writer,
std::vector const & stringsArray,
std::string const & tagName, std::string_view const & indent)
{
@@ -245,7 +206,7 @@ void SaveStringsArray(KmlWriter::WriterWrapper & writer,
writer << indent << "\n";
}
-void SaveStringsMap(KmlWriter::WriterWrapper & writer,
+void SaveStringsMap(Writer & writer,
std::map const & stringsMap,
std::string const & tagName, std::string_view const & indent)
{
@@ -262,11 +223,11 @@ void SaveStringsMap(KmlWriter::WriterWrapper & writer,
writer << indent << "\n";
}
-void SaveCategoryData(KmlWriter::WriterWrapper & writer, CategoryData const & categoryData,
+void SaveCategoryData(Writer & writer, CategoryData const & categoryData,
std::string const & extendedServerId,
std::vector const * compilationData);
-void SaveCategoryExtendedData(KmlWriter::WriterWrapper & writer, CategoryData const & categoryData,
+void SaveCategoryExtendedData(Writer & writer, CategoryData const & categoryData,
std::string const & extendedServerId,
std::vector const * compilationData)
{
@@ -355,7 +316,7 @@ void SaveCategoryExtendedData(KmlWriter::WriterWrapper & writer, CategoryData co
writer << kIndent4 << kCompilationFooter;
}
-void SaveCategoryData(KmlWriter::WriterWrapper & writer, CategoryData const & categoryData,
+void SaveCategoryData(Writer & writer, CategoryData const & categoryData,
std::string const & extendedServerId,
std::vector const * compilationData)
{
@@ -382,7 +343,7 @@ void SaveCategoryData(KmlWriter::WriterWrapper & writer, CategoryData const & ca
SaveCategoryExtendedData(writer, categoryData, extendedServerId, compilationData);
}
-void SaveBookmarkExtendedData(KmlWriter::WriterWrapper & writer, BookmarkData const & bookmarkData)
+void SaveBookmarkExtendedData(Writer & writer, BookmarkData const & bookmarkData)
{
writer << kIndent4 << kExtendedDataHeader;
if (!bookmarkData.m_name.empty())
@@ -454,7 +415,7 @@ void SaveBookmarkExtendedData(KmlWriter::WriterWrapper & writer, BookmarkData co
writer << kIndent4 << kExtendedDataFooter;
}
-void SaveBookmarkData(KmlWriter::WriterWrapper & writer, BookmarkData const & bookmarkData)
+void SaveBookmarkData(Writer & writer, BookmarkData const & bookmarkData)
{
writer << kIndent2 << "\n";
writer << kIndent4 << "";
@@ -485,7 +446,7 @@ void SaveBookmarkData(KmlWriter::WriterWrapper & writer, BookmarkData const & bo
writer << kIndent2 << "\n";
}
-void SaveTrackLayer(KmlWriter::WriterWrapper & writer, TrackLayer const & layer,
+void SaveTrackLayer(Writer & writer, TrackLayer const & layer,
std::string_view const & indent)
{
writer << indent << "";
@@ -494,7 +455,7 @@ void SaveTrackLayer(KmlWriter::WriterWrapper & writer, TrackLayer const & layer,
writer << indent << "" << strings::to_string(layer.m_lineWidth) << "\n";
}
-void SaveTrackGeometry(KmlWriter::WriterWrapper & writer, MultiGeometry const & geom)
+void SaveTrackGeometry(Writer & writer, MultiGeometry const & geom)
{
size_t const sz = geom.m_lines.size();
if (sz == 0)
@@ -531,7 +492,7 @@ void SaveTrackGeometry(KmlWriter::WriterWrapper & writer, MultiGeometry const &
writer << kIndent4 << "\n";
}
-void SaveTrackExtendedData(KmlWriter::WriterWrapper & writer, TrackData const & trackData)
+void SaveTrackExtendedData(Writer & writer, TrackData const & trackData)
{
writer << kIndent4 << kExtendedDataHeader;
SaveLocalizableString(writer, trackData.m_name, "name", kIndent6);
@@ -557,7 +518,7 @@ void SaveTrackExtendedData(KmlWriter::WriterWrapper & writer, TrackData const &
writer << kIndent4 << kExtendedDataFooter;
}
-void SaveTrackData(KmlWriter::WriterWrapper & writer, TrackData const & trackData)
+void SaveTrackData(Writer & writer, TrackData const & trackData)
{
writer << kIndent2 << "\n";
writer << kIndent4 << "";
@@ -639,12 +600,6 @@ bool ParsePointWithAltitude(std::string_view s, char const * delim,
}
} // namespace
-KmlWriter::WriterWrapper & KmlWriter::WriterWrapper::operator<<(std::string_view str)
-{
- m_writer.Write(str.data(), str.length());
- return *this;
-}
-
void KmlWriter::Write(FileData const & fileData)
{
m_writer << kKmlHeader;
diff --git a/kml/serdes.hpp b/kml/serdes.hpp
index 99c4670be5..6179bb4cdf 100644
--- a/kml/serdes.hpp
+++ b/kml/serdes.hpp
@@ -20,17 +20,6 @@ class KmlWriter
public:
DECLARE_EXCEPTION(WriteKmlException, RootException);
- class WriterWrapper
- {
- public:
- explicit WriterWrapper(Writer & writer)
- : m_writer(writer)
- {}
- WriterWrapper & operator<<(std::string_view str);
- private:
- Writer & m_writer;
- };
-
explicit KmlWriter(Writer & writer)
: m_writer(writer)
{}
@@ -38,7 +27,7 @@ public:
void Write(FileData const & fileData);
private:
- WriterWrapper m_writer;
+ Writer & m_writer;
};
class SerializerKml
diff --git a/kml/serdes_common.cpp b/kml/serdes_common.cpp
index 4b4bb4b519..b93bf711e8 100644
--- a/kml/serdes_common.cpp
+++ b/kml/serdes_common.cpp
@@ -25,4 +25,35 @@ std::string PointToString(geometry::PointWithAltitude const & pt)
return PointToString(pt.GetPoint());
}
+void SaveStringWithCDATA(Writer & writer, std::string s)
+{
+ if (s.empty())
+ return;
+
+ // Expat loads XML 1.0 only. Sometimes users copy and paste bookmark descriptions or even names from the web.
+ // Rarely, in these copy-pasted texts, there are invalid XML1.0 symbols.
+ // See https://en.wikipedia.org/wiki/Valid_characters_in_XML
+ // A robust solution requires parsing invalid XML on loading (then users can restore "bad" XML files), see
+ // https://github.com/organicmaps/organicmaps/issues/3837
+ // When a robust solution is implemented, this workaround can be removed for better performance/battery.
+ //
+ // This solution is a simple ASCII-range check that does not check symbols from other unicode ranges
+ // (they will require a more complex and slower approach of converting UTF-8 string to unicode first).
+ // It should be enough for many cases, according to user reports and wrong characters in their data.
+ s.erase(std::remove_if(s.begin(), s.end(), [](unsigned char c)
+ {
+ if (c >= 0x20 || c == 0x09 || c == 0x0a || c == 0x0d)
+ return false;
+ return true;
+ }), s.end());
+
+ if (s.empty())
+ return;
+
+ // According to kml/xml spec, we need to escape special symbols with CDATA.
+ if (s.find_first_of("<&") != std::string::npos)
+ writer << "";
+ else
+ writer << s;
+}
} // namespace kml
diff --git a/kml/serdes_common.hpp b/kml/serdes_common.hpp
index f70e6e2a04..19032724fa 100644
--- a/kml/serdes_common.hpp
+++ b/kml/serdes_common.hpp
@@ -1,9 +1,12 @@
#pragma once
#include "coding/string_utf8_multilang.hpp"
+
#include "geometry/point2d.hpp"
#include "geometry/point_with_altitude.hpp"
+#include "type_utils.hpp"
+
namespace kml
{
auto constexpr kDefaultLang = StringUtf8Multilang::kDefaultCode;
@@ -21,4 +24,13 @@ std::string PointToString(m2::PointD const & org);
std::string PointToString(geometry::PointWithAltitude const & pt);
+void SaveStringWithCDATA(Writer & writer, std::string s);
+
+std::string_view constexpr kIndent0 {};
+std::string_view constexpr kIndent2 {" "};
+std::string_view constexpr kIndent4 {" "};
+std::string_view constexpr kIndent6 {" "};
+std::string_view constexpr kIndent8 {" "};
+std::string_view constexpr kIndent10 {" "};
+
} // namespace kml
diff --git a/kml/serdes_gpx.cpp b/kml/serdes_gpx.cpp
index 56e002f2b2..3a7226c817 100644
--- a/kml/serdes_gpx.cpp
+++ b/kml/serdes_gpx.cpp
@@ -14,7 +14,8 @@ namespace kml
{
namespace gpx
{
-
+namespace
+{
std::string_view constexpr kTrk = "trk";
std::string_view constexpr kTrkSeg = "trkseg";
std::string_view constexpr kRte = "rte";
@@ -30,7 +31,15 @@ std::string_view constexpr kDesc = "desc";
std::string_view constexpr kMetadata = "metadata";
std::string_view constexpr kEle = "ele";
std::string_view constexpr kCmt = "cmt";
+
+std::string_view constexpr kGpxHeader =
+ "\n"
+ "\n";
+
+std::string_view constexpr kGpxFooter = "";
+
int constexpr kInvalidColor = 0;
+} // namespace
GpxParser::GpxParser(FileData & data)
: m_data{data}
@@ -358,6 +367,119 @@ std::string GpxParser::BuildDescription() const
return m_description + "\n\n" + m_comment;
}
+namespace
+{
+
+std::optional GetDefaultLanguage(LocalizableString const & lstr)
+{
+ auto const firstLang = lstr.begin();
+ if (firstLang != lstr.end())
+ return {firstLang->second};
+ return {};
+}
+
+std::string CoordToString(double c)
+{
+ std::ostringstream ss;
+ ss.precision(8);
+ ss << c;
+ return ss.str();
+}
+
+void SaveColorToRGB(Writer & writer, uint32_t rgba)
+{
+ writer << 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)
+{
+ writer << "\n";
+ if (auto const name = GetDefaultLanguage(categoryData.m_name))
+ writer << kIndent2 << "" << name.value() << "\n";
+ if (auto const description = GetDefaultLanguage(categoryData.m_description))
+ {
+ writer << kIndent2 << "";
+ SaveStringWithCDATA(writer, description.value());
+ writer << "\n";
+ }
+ writer << "\n";
+}
+
+void SaveBookmarkData(Writer & writer, BookmarkData const & bookmarkData)
+{
+ auto const [lat, lon] = mercator::ToLatLon(bookmarkData.m_point);
+ writer << "\n";
+ if (auto const name = GetDefaultLanguage(bookmarkData.m_name))
+ writer << kIndent2 << "" << name.value() << "\n";
+ if (auto const description = GetDefaultLanguage(bookmarkData.m_description))
+ {
+ writer << kIndent2 << "";
+ SaveStringWithCDATA(writer, description.value());
+ writer << "\n";
+ }
+ writer << "\n";
+}
+
+bool TrackHasAltitudes(TrackData const & trackData)
+{
+ auto const & lines = trackData.m_geometry.m_lines;
+ if (lines.empty() || lines.front().empty())
+ return false;
+ auto const altitude = lines.front().front().GetAltitude();
+ return altitude != geometry::kDefaultAltitudeMeters && altitude != geometry::kInvalidAltitude;
+}
+
+uint32_t TrackColor(TrackData const & trackData)
+{
+ if (trackData.m_layers.empty())
+ return kDefaultTrackColor;
+ return trackData.m_layers.front().m_color.m_rgba;
+}
+
+void SaveTrackData(Writer & writer, TrackData const & trackData)
+{
+ writer << "\n";
+ auto name = GetDefaultLanguage(trackData.m_name);
+ if (name.has_value())
+ writer << kIndent2 << "" << name.value() << "\n";
+ if (auto const color = TrackColor(trackData); color != kDefaultTrackColor)
+ {
+ writer << kIndent2 << "\n" << kIndent4 << "";
+ SaveColorToRGB(writer, color);
+ writer << "\n" << kIndent2 << "\n";
+ }
+ bool const trackHasAltitude = TrackHasAltitudes(trackData);
+ for (auto const & line : trackData.m_geometry.m_lines)
+ {
+ writer << kIndent2 << "\n";
+ for (auto const & point : line)
+ {
+ auto const [lat, lon] = mercator::ToLatLon(point);
+ writer << kIndent4 << "\n";
+ if (trackHasAltitude)
+ writer << kIndent6 << "" << CoordToString(point.GetAltitude()) << "\n";
+ writer << kIndent4 << "\n";
+ }
+ writer << kIndent2 << "\n";
+ }
+ writer << "\n";
+}
+} // namespace
+
+void GpxWriter::Write(FileData const & fileData)
+{
+ m_writer << kGpxHeader;
+ SaveCategoryData(m_writer, fileData.m_categoryData);
+ for (auto const & bookmarkData : fileData.m_bookmarksData)
+ SaveBookmarkData(m_writer, bookmarkData);
+ for (auto const & trackData : fileData.m_tracksData)
+ SaveTrackData(m_writer, trackData);
+ m_writer << kGpxFooter;
+}
+
} // namespace gpx
DeserializerGpx::DeserializerGpx(FileData & fileData)
diff --git a/kml/serdes_gpx.hpp b/kml/serdes_gpx.hpp
index 852e7fb00d..26aebe221a 100644
--- a/kml/serdes_gpx.hpp
+++ b/kml/serdes_gpx.hpp
@@ -4,6 +4,7 @@
#include "coding/parse_xml.hpp"
#include "coding/reader.hpp"
+#include "coding/writer.hpp"
#include "geometry/point_with_altitude.hpp"
@@ -15,6 +16,42 @@ namespace kml
{
namespace gpx
{
+
+class GpxWriter
+{
+public:
+ DECLARE_EXCEPTION(WriteGpxException, RootException);
+
+ explicit GpxWriter(Writer & writer)
+ : m_writer(writer)
+ {}
+
+ void Write(FileData const & fileData);
+
+private:
+ Writer & m_writer;
+};
+
+class SerializerGpx
+{
+public:
+ DECLARE_EXCEPTION(SerializeException, RootException);
+
+ explicit SerializerGpx(FileData const & fileData)
+ : m_fileData(fileData)
+ {}
+
+ template
+ void Serialize(Sink & sink)
+ {
+ GpxWriter gpxWriter(sink);
+ gpxWriter.Write(m_fileData);
+ }
+
+private:
+ FileData const & m_fileData;
+};
+
class GpxParser
{
public:
diff --git a/map/bookmark_helpers.cpp b/map/bookmark_helpers.cpp
index d20a4eea64..43b470a42d 100644
--- a/map/bookmark_helpers.cpp
+++ b/map/bookmark_helpers.cpp
@@ -473,22 +473,41 @@ std::unique_ptr LoadKmlData(Reader const & reader, KmlFileType fi
return data;
}
-bool SaveKmlFile(kml::FileData & kmlData, std::string const & file, KmlFileType fileType)
+bool SaveGpxData(kml::FileData & kmlData, Writer & writer)
{
- bool success;
try
{
- FileWriter writer(file);
- success = SaveKmlData(kmlData, writer, fileType);
+ kml::gpx::SerializerGpx ser(kmlData);
+ ser.Serialize(writer);
+ }
+ catch (Writer::Exception const & e)
+ {
+ LOG(LWARNING, ("GPX writing failure:", e.what()));
+ return false;
}
catch (std::exception const & e)
{
- LOG(LWARNING, ("KML", fileType, "saving failure:", e.what()));
- success = false;
+ LOG(LWARNING, ("GPX serialization failure:", e.what()));
+ return false;
+ }
+ return true;
+}
+
+bool SaveKmlFile(kml::FileData & kmlData, std::string const & file, KmlFileType fileType)
+{
+ FileWriter writer(file);
+ LOG(LINFO, ("Save kml file", file, ", type", fileType));
+ switch (fileType)
+ {
+ case KmlFileType::Text: // fallthrough
+ case KmlFileType::Binary: return SaveKmlData(kmlData, writer, fileType);
+ case KmlFileType::Gpx: return SaveGpxData(kmlData, writer);
+ default:
+ {
+ LOG(LWARNING, ("Unexpected KmlFileType", fileType));
+ return false;
+ }
}
- if (!success)
- LOG(LWARNING, ("Saving bookmarks failed, file", file));
- return success;
}
bool SaveKmlFileSafe(kml::FileData & kmlData, std::string const & file, KmlFileType fileType)