#include "kml/serdes.hpp" #include "indexer/classificator.hpp" #include "geometry/mercator.hpp" #include "coding/hex.hpp" #include "coding/string_utf8_multilang.hpp" #include "base/assert.hpp" #include "base/stl_helpers.hpp" #include "base/string_utils.hpp" #include "base/timer.hpp" #include using namespace std::string_literals; namespace kml { namespace { std::string const kPlacemark = "Placemark"; std::string const kStyle = "Style"; std::string const kDocument = "Document"; std::string const kStyleMap = "StyleMap"; std::string const kStyleUrl = "styleUrl"; std::string const kPair = "Pair"; std::string const kExtendedData = "ExtendedData"; std::string const kKmlHeader = "\n" "\n" "\n"; std::string const kKmlFooter = "\n" "\n"; std::string const kExtendedDataHeader = "\n"; std::string const kExtendedDataFooter = "\n"; auto const kDefaultLang = StringUtf8Multilang::kDefaultCode; auto const kDefaultTrackWidth = 5.0; auto const kDefaultTrackColor = 0x006ec7ff; std::string Indent(size_t count) { static std::string const kIndent = " "; std::ostringstream indent; for (size_t i = 0; i < count; ++i) indent << kIndent; return indent.str(); } static std::string const kIndent2 = Indent(2); static std::string const kIndent4 = Indent(4); static std::string const kIndent6 = Indent(6); static std::string const kIndent8 = Indent(8); static std::string const kIndent10 = Indent(10); std::string PointToString(m2::PointD const & org) { double const lon = MercatorBounds::XToLon(org.x); double const lat = MercatorBounds::YToLat(org.y); std::ostringstream ss; ss.precision(8); ss << lon << "," << lat; return ss.str(); } std::string GetLocalizableString(LocalizableString const & s, int8_t lang) { auto const it = s.find(lang); if (it == s.cend()) return {}; return it->second; } PredefinedColor ExtractPlacemarkPredefinedColor(std::string const & s) { if (s == "#placemark-red") return PredefinedColor::Red; if (s == "#placemark-blue") return PredefinedColor::Blue; if (s == "#placemark-purple") return PredefinedColor::Purple; if (s == "#placemark-yellow") return PredefinedColor::Yellow; if (s == "#placemark-pink") return PredefinedColor::Pink; if (s == "#placemark-brown") return PredefinedColor::Brown; if (s == "#placemark-green") return PredefinedColor::Green; if (s == "#placemark-orange") return PredefinedColor::Orange; // Default color. return PredefinedColor::Red; } std::string GetStyleForPredefinedColor(PredefinedColor color) { switch (color) { case PredefinedColor::Red: return "placemark-red"; case PredefinedColor::Blue: return "placemark-blue"; case PredefinedColor::Purple: return "placemark-purple"; case PredefinedColor::Yellow: return "placemark-yellow"; case PredefinedColor::Pink: return "placemark-pink"; case PredefinedColor::Brown: return "placemark-brown"; case PredefinedColor::Green: return "placemark-green"; case PredefinedColor::Orange: return "placemark-orange"; case PredefinedColor::None: case PredefinedColor::Count: return {}; } UNREACHABLE(); } BookmarkIcon GetIcon(std::string const & iconName) { for (size_t i = 0; i < static_cast(BookmarkIcon::Count); ++i) { auto const icon = static_cast(i); if (iconName == DebugPrint(icon)) return icon; } return BookmarkIcon::None; } 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); } void SaveStringWithCDATA(KmlWriter::WriterWrapper & writer, std::string const & s) { 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) { if (style.empty()) return; writer << kIndent2 << "\n"; } void SaveColorToABGR(KmlWriter::WriterWrapper & writer, uint32_t rgba) { writer << NumToHex(static_cast(rgba & 0xFF)) << NumToHex(static_cast((rgba >> 8) & 0xFF)) << NumToHex(static_cast((rgba >> 16) & 0xFF)) << NumToHex(static_cast((rgba >> 24) & 0xFF)); } std::string TimestampToString(Timestamp const & timestamp) { auto const ts = std::chrono::system_clock::to_time_t(timestamp); std::string const strTimeStamp = base::TimestampToString(ts); if (strTimeStamp.size() != 20) MYTHROW(KmlWriter::WriteKmlException, ("We always generate fixed length UTC-format timestamp.")); return strTimeStamp; } void SaveLocalizableString(KmlWriter::WriterWrapper & writer, LocalizableString const & str, std::string const & tagName, std::string const & offsetStr) { writer << offsetStr << "\n"; for (auto const & s : str) { writer << offsetStr << kIndent2 << ""; SaveStringWithCDATA(writer, s.second); writer << "\n"; } writer << offsetStr << "\n"; } void SaveStringsArray(KmlWriter::WriterWrapper & writer, std::vector const & stringsArray, std::string const & tagName, std::string const & offsetStr) { if (stringsArray.empty()) return; writer << offsetStr << "\n"; for (auto const & s : stringsArray) { writer << offsetStr << kIndent2 << ""; SaveStringWithCDATA(writer, s); writer << "\n"; } writer << offsetStr << "\n"; } void SavePointsArray(KmlWriter::WriterWrapper & writer, std::vector const & pointsArray, std::string const & tagName, std::string const & offsetStr) { if (pointsArray.empty()) return; writer << offsetStr << "\n"; for (auto const & p : pointsArray) { writer << offsetStr << kIndent2 << ""; writer << PointToString(p); writer << "\n"; } writer << offsetStr << "\n"; } void SaveStringsMap(KmlWriter::WriterWrapper & writer, std::map const & stringsMap, std::string const & tagName, std::string const & offsetStr) { if (stringsMap.empty()) return; writer << offsetStr << "\n"; for (auto const & p : stringsMap) { writer << offsetStr << kIndent2 << ""; SaveStringWithCDATA(writer, p.second); writer << "\n"; } writer << offsetStr << "\n"; } void SaveCategoryExtendedData(KmlWriter::WriterWrapper & writer, CategoryData const & categoryData, std::string const & extendedServerId) { writer << kIndent2 << kExtendedDataHeader; if (!extendedServerId.empty()) writer << kIndent4 << "" << extendedServerId << "\n"; SaveLocalizableString(writer, categoryData.m_name, "name", kIndent4); SaveLocalizableString(writer, categoryData.m_annotation, "annotation", kIndent4); SaveLocalizableString(writer, categoryData.m_description, "description", kIndent4); if (!categoryData.m_imageUrl.empty()) writer << kIndent4 << "" << categoryData.m_imageUrl << "\n"; if (!categoryData.m_authorId.empty() || !categoryData.m_authorName.empty()) { writer << kIndent4 << ""; SaveStringWithCDATA(writer, categoryData.m_authorName); writer << "\n"; } if (categoryData.m_lastModified != Timestamp()) { writer << kIndent4 << "" << TimestampToString(categoryData.m_lastModified) << "\n"; } double constexpr kEps = 1e-5; if (fabs(categoryData.m_rating) > kEps) { writer << kIndent4 << "" << strings::to_string(categoryData.m_rating) << "\n"; } if (categoryData.m_reviewsNumber > 0) { writer << kIndent4 << "" << strings::to_string(categoryData.m_reviewsNumber) << "\n"; } writer << kIndent4 << "" << DebugPrint(categoryData.m_accessRules) << "\n"; SaveStringsArray(writer, categoryData.m_tags, "tags", kIndent4); SavePointsArray(writer, categoryData.m_cities, "cities", kIndent4); std::vector languageCodes; languageCodes.reserve(categoryData.m_languageCodes.size()); for (auto const & lang : categoryData.m_languageCodes) { std::string str = StringUtf8Multilang::GetLangByCode(lang); if (!str.empty()) languageCodes.push_back(std::move(str)); } SaveStringsArray(writer, languageCodes, "languageCodes", kIndent4); SaveStringsMap(writer, categoryData.m_properties, "properties", kIndent4); writer << kIndent2 << kExtendedDataFooter; } void SaveCategoryData(KmlWriter::WriterWrapper & writer, CategoryData const & categoryData, std::string const & extendedServerId) { for (uint8_t i = 0; i < base::Underlying(PredefinedColor::Count); ++i) SaveStyle(writer, GetStyleForPredefinedColor(static_cast(i))); // Use CDATA if we have special symbols in the name. writer << kIndent2 << ""; SaveStringWithCDATA(writer, GetLocalizableString(categoryData.m_name, kDefaultLang)); writer << "\n"; if (!categoryData.m_description.empty()) { writer << kIndent2 << ""; SaveStringWithCDATA(writer, GetLocalizableString(categoryData.m_description, kDefaultLang)); writer << "\n"; } writer << kIndent2 << "" << (categoryData.m_visible ? "1" : "0") <<"\n"; SaveCategoryExtendedData(writer, categoryData, extendedServerId); } void SaveBookmarkExtendedData(KmlWriter::WriterWrapper & writer, BookmarkData const & bookmarkData) { if (bookmarkData.m_name.empty() && bookmarkData.m_description.empty() && bookmarkData.m_customName.empty() && bookmarkData.m_viewportScale == 0 && bookmarkData.m_icon == BookmarkIcon::None && bookmarkData.m_featureTypes.empty() && bookmarkData.m_boundTracks.empty()) { return; } writer << kIndent4 << kExtendedDataHeader; SaveLocalizableString(writer, bookmarkData.m_name, "name", kIndent6); SaveLocalizableString(writer, bookmarkData.m_description, "description", kIndent6); if (!bookmarkData.m_featureTypes.empty()) { std::vector types; types.reserve(bookmarkData.m_featureTypes.size()); auto const & c = classif(); if (!c.HasTypesMapping()) MYTHROW(SerializerKml::SerializeException, ("Types mapping is not loaded.")); for (auto const & t : bookmarkData.m_featureTypes) types.push_back(c.GetReadableObjectName(c.GetTypeForIndex(t))); SaveStringsArray(writer, types, "featureTypes", kIndent6); } if (!bookmarkData.m_customName.empty()) SaveLocalizableString(writer, bookmarkData.m_customName, "customName", kIndent6); if (bookmarkData.m_viewportScale != 0) { auto const scale = strings::to_string(static_cast(bookmarkData.m_viewportScale)); writer << kIndent6 << "" << scale << "\n"; } if (bookmarkData.m_icon != BookmarkIcon::None) writer << kIndent6 << "" << DebugPrint(bookmarkData.m_icon) << "\n"; std::vector boundTracks; boundTracks.reserve(bookmarkData.m_boundTracks.size()); for (auto const & t : bookmarkData.m_boundTracks) boundTracks.push_back(strings::to_string(static_cast(t))); SaveStringsArray(writer, boundTracks, "boundTracks", kIndent6); writer << kIndent4 << kExtendedDataFooter; } void SaveBookmarkData(KmlWriter::WriterWrapper & writer, BookmarkData const & bookmarkData) { writer << kIndent2 << "\n"; writer << kIndent4 << ""; std::string const defaultLang = StringUtf8Multilang::GetLangByCode(kDefaultLangCode); SaveStringWithCDATA(writer, GetPreferredBookmarkName(bookmarkData, defaultLang)); writer << "\n"; if (!bookmarkData.m_description.empty()) { writer << kIndent4 << ""; SaveStringWithCDATA(writer, GetLocalizableString(bookmarkData.m_description, kDefaultLang)); writer << "\n"; } if (bookmarkData.m_timestamp != Timestamp()) { writer << kIndent4 << "" << TimestampToString(bookmarkData.m_timestamp) << "\n"; } auto const style = GetStyleForPredefinedColor(bookmarkData.m_color.m_predefinedColor); writer << kIndent4 << "#" << style << "\n" << kIndent4 << "" << PointToString(bookmarkData.m_point) << "\n"; SaveBookmarkExtendedData(writer, bookmarkData); writer << kIndent2 << "\n"; } void SaveTrackLayer(KmlWriter::WriterWrapper & writer, TrackLayer const & layer, std::string const & offsetStr) { writer << offsetStr << ""; SaveColorToABGR(writer, layer.m_color.m_rgba); writer << "\n"; writer << offsetStr << "" << strings::to_string(layer.m_lineWidth) << "\n"; } void SaveTrackExtendedData(KmlWriter::WriterWrapper & writer, TrackData const & trackData) { writer << kIndent4 << kExtendedDataHeader; SaveLocalizableString(writer, trackData.m_name, "name", kIndent6); SaveLocalizableString(writer, trackData.m_description, "description", kIndent6); auto const localId = strings::to_string(static_cast(trackData.m_localId)); writer << kIndent6 << "" << localId << "\n"; writer << kIndent6 << "\n"; for (size_t i = 1 /* since second layer */; i < trackData.m_layers.size(); ++i) { writer << kIndent8 << "\n"; SaveTrackLayer(writer, trackData.m_layers[i], kIndent10); writer << kIndent8 << "\n"; } writer << kIndent6 << "\n"; writer << kIndent4 << kExtendedDataFooter; } void SaveTrackData(KmlWriter::WriterWrapper & writer, TrackData const & trackData) { writer << kIndent2 << "\n"; writer << kIndent4 << ""; SaveStringWithCDATA(writer, GetLocalizableString(trackData.m_name, kDefaultLang)); writer << "\n"; if (!trackData.m_description.empty()) { writer << kIndent4 << ""; SaveStringWithCDATA(writer, GetLocalizableString(trackData.m_description, kDefaultLang)); writer << "\n"; } if (trackData.m_layers.empty()) MYTHROW(KmlWriter::WriteKmlException, ("Layers list is empty.")); auto const & layer = trackData.m_layers.front(); writer << kIndent4 << "\n"; if (trackData.m_timestamp != Timestamp()) { writer << kIndent4 << "" << TimestampToString(trackData.m_timestamp) << "\n"; } writer << kIndent4 << ""; for (size_t i = 0; i < trackData.m_points.size(); ++i) { writer << PointToString(trackData.m_points[i]); if (i + 1 != trackData.m_points.size()) writer << " "; } writer << "\n"; SaveTrackExtendedData(writer, trackData); writer << kIndent2 << "\n"; } } // namespace KmlWriter::WriterWrapper & KmlWriter::WriterWrapper::operator<<(std::string const & str) { m_writer.Write(str.data(), str.length()); return *this; } void KmlWriter::Write(FileData const & fileData) { m_writer << kKmlHeader; // Save category. SaveCategoryData(m_writer, fileData.m_categoryData, fileData.m_serverId); // Save bookmarks. for (auto const & bookmarkData : fileData.m_bookmarksData) SaveBookmarkData(m_writer, bookmarkData); // Saving tracks. for (auto const & trackData : fileData.m_tracksData) SaveTrackData(m_writer, trackData); m_writer << kKmlFooter; } KmlParser::KmlParser(FileData & data) : m_data(data) , m_attrCode(StringUtf8Multilang::kUnsupportedLanguageCode) { ResetPoint(); } void KmlParser::ResetPoint() { m_name.clear(); m_description.clear(); m_org = {}; m_predefinedColor = PredefinedColor::None; m_viewportScale = 0; m_timestamp = {}; m_color = 0; m_styleId.clear(); m_mapStyleId.clear(); m_styleUrlKey.clear(); m_featureTypes.clear(); m_customName.clear(); m_boundTracks.clear(); m_localId = 0; m_trackLayers.clear(); m_trackWidth = kDefaultTrackWidth; m_icon = BookmarkIcon::None; m_points.clear(); m_geometryType = GEOMETRY_TYPE_UNKNOWN; } bool KmlParser::ParsePoint(std::string const & s, char const * delim, m2::PointD & pt) { // Order in string is: lon, lat, z. strings::SimpleTokenizer iter(s, delim); if (!iter) return false; double lon; if (strings::to_double(*iter, lon) && MercatorBounds::ValidLon(lon) && ++iter) { double lat; if (strings::to_double(*iter, lat) && MercatorBounds::ValidLat(lat)) { pt = MercatorBounds::FromLatLon(lat, lon); return true; } } return false; } void KmlParser::SetOrigin(std::string const & s) { m_geometryType = GEOMETRY_TYPE_POINT; m2::PointD pt; if (ParsePoint(s, ", \n\r\t", pt)) m_org = pt; } void KmlParser::ParseLineCoordinates(std::string const & s, char const * blockSeparator, char const * coordSeparator) { m_geometryType = GEOMETRY_TYPE_LINE; strings::SimpleTokenizer tupleIter(s, blockSeparator); while (tupleIter) { m2::PointD pt; if (ParsePoint(*tupleIter, coordSeparator, pt)) { if (m_points.empty() || !pt.EqualDxDy(m_points.back(), 1e-5 /* eps */)) m_points.push_back(std::move(pt)); } ++tupleIter; } } bool KmlParser::MakeValid() { if (GEOMETRY_TYPE_POINT == m_geometryType) { if (MercatorBounds::ValidX(m_org.x) && MercatorBounds::ValidY(m_org.y)) { // Set default name. if (m_name.empty() && m_featureTypes.empty()) m_name[kDefaultLang] = PointToString(m_org); // Set default pin. if (m_predefinedColor == PredefinedColor::None) m_predefinedColor = PredefinedColor::Red; return true; } return false; } else if (GEOMETRY_TYPE_LINE == m_geometryType) { return m_points.size() > 1; } return false; } void KmlParser::ParseColor(std::string const & value) { auto const fromHex = FromHex(value); if (fromHex.size() != 4) return; // Color positions in HEX – aabbggrr. m_color = ToRGBA(fromHex[3], fromHex[2], fromHex[1], fromHex[0]); } bool KmlParser::GetColorForStyle(std::string const & styleUrl, uint32_t & color) const { if (styleUrl.empty()) return false; // Remove leading '#' symbol auto const it = m_styleUrl2Color.find(styleUrl.substr(1)); if (it != m_styleUrl2Color.cend()) { color = it->second; return true; } return false; } double KmlParser::GetTrackWidthForStyle(std::string const & styleUrl) const { if (styleUrl.empty()) return kDefaultTrackWidth; // Remove leading '#' symbol auto const it = m_styleUrl2Width.find(styleUrl.substr(1)); if (it != m_styleUrl2Width.cend()) return it->second; return kDefaultTrackWidth; } bool KmlParser::Push(std::string const & name) { m_tags.push_back(name); return true; } void KmlParser::AddAttr(std::string const & attr, std::string const & value) { std::string attrInLowerCase = attr; strings::AsciiToLower(attrInLowerCase); if (IsValidAttribute(kStyle, value, attrInLowerCase)) m_styleId = value; else if (IsValidAttribute(kStyleMap, value, attrInLowerCase)) m_mapStyleId = value; if (attrInLowerCase == "code") m_attrCode = StringUtf8Multilang::GetLangIndex(value); else if (attrInLowerCase == "id") m_attrId = value; else if (attrInLowerCase == "key") m_attrKey = value; } bool KmlParser::IsValidAttribute(std::string const & type, std::string const & value, std::string const & attrInLowerCase) const { return (GetTagFromEnd(0) == type && !value.empty() && attrInLowerCase == "id"); } std::string const & KmlParser::GetTagFromEnd(size_t n) const { ASSERT_LESS(n, m_tags.size(), ()); return m_tags[m_tags.size() - n - 1]; } void KmlParser::Pop(std::string const & tag) { ASSERT_EQUAL(m_tags.back(), tag, ()); if (tag == kPlacemark) { if (MakeValid()) { if (GEOMETRY_TYPE_POINT == m_geometryType) { BookmarkData data; data.m_name = std::move(m_name); data.m_description = std::move(m_description); data.m_color.m_predefinedColor = m_predefinedColor; data.m_color.m_rgba = m_color; data.m_icon = m_icon; data.m_viewportScale = m_viewportScale; data.m_timestamp = m_timestamp; data.m_point = m_org; data.m_featureTypes = std::move(m_featureTypes); data.m_customName = std::move(m_customName); data.m_boundTracks = std::move(m_boundTracks); // Here we set custom name from 'name' field for KML-files exported from 3rd-party services. if (data.m_name.size() == 1 && data.m_name.begin()->first == kDefaultLangCode && data.m_customName.empty() && data.m_featureTypes.empty()) { data.m_customName = data.m_name; } m_data.m_bookmarksData.push_back(std::move(data)); } else if (GEOMETRY_TYPE_LINE == m_geometryType) { TrackData data; data.m_localId = m_localId; data.m_name = std::move(m_name); data.m_description = std::move(m_description); data.m_layers = std::move(m_trackLayers); data.m_timestamp = m_timestamp; data.m_points = m_points; m_data.m_tracksData.push_back(std::move(data)); } } ResetPoint(); } else if (tag == kStyle) { if (GetTagFromEnd(1) == kDocument) { if (!m_styleId.empty()) { m_styleUrl2Color[m_styleId] = m_color; m_styleUrl2Width[m_styleId] = m_trackWidth; m_color = 0; m_trackWidth = kDefaultTrackWidth; } } } else if ((tag == "LineStyle" && m_tags.size() > 2 && GetTagFromEnd(2) == kPlacemark) || (tag == "mwm:additionalLineStyle" && m_tags.size() > 3 && GetTagFromEnd(3) == kPlacemark)) { // This code assumes that