#include "editor/editor_notes.hpp" #include "platform/platform.hpp" #include "coding/internal/file_data.hpp" #include "geometry/mercator.hpp" #include "base/assert.hpp" #include "base/logging.hpp" #include "base/string_utils.hpp" #include "base/timer.hpp" #include #include #include "3party/pugixml/src/pugixml.hpp" namespace { bool LoadFromXml(pugi::xml_document const & xml, std::list & notes, uint32_t & uploadedNotesCount) { uint64_t notesCount; auto const root = xml.child("notes"); if (!strings::to_uint64(root.attribute("uploadedNotesCount").value(), notesCount)) { LOG(LERROR, ("Can't read uploadedNotesCount from file.")); uploadedNotesCount = 0; } else { uploadedNotesCount = static_cast(notesCount); } for (auto const & xNode : root.select_nodes("note")) { ms::LatLon latLon; auto const node = xNode.node(); auto const lat = node.attribute("lat"); if (!lat || !strings::to_double(lat.value(), latLon.m_lat)) continue; auto const lon = node.attribute("lon"); if (!lon || !strings::to_double(lon.value(), latLon.m_lon)) continue; auto const text = node.attribute("text"); if (!text) continue; notes.emplace_back(latLon, text.value()); } return true; } void SaveToXml(std::list const & notes, pugi::xml_document & xml, uint32_t const uploadedNotesCount) { auto constexpr kDigitsAfterComma = 7; auto root = xml.append_child("notes"); root.append_attribute("uploadedNotesCount") = uploadedNotesCount; for (auto const & note : notes) { auto node = root.append_child("note"); node.append_attribute("lat") = strings::to_string_dac(note.m_point.m_lat, kDigitsAfterComma).data(); node.append_attribute("lon") = strings::to_string_dac(note.m_point.m_lon, kDigitsAfterComma).data(); node.append_attribute("text") = note.m_note.data(); } } /// Not thread-safe, use only for initialization. bool Load(std::string const & fileName, std::list & notes, uint32_t & uploadedNotesCount) { std::string content; try { auto const reader = GetPlatform().GetReader(fileName); reader->ReadAsString(content); } catch (FileAbsentException const &) { // It's normal if no notes file is present. return true; } catch (Reader::Exception const & e) { LOG(LERROR, ("Can't process file.", fileName, e.Msg())); return false; } pugi::xml_document xml; if (!xml.load_buffer(content.data(), content.size())) { LOG(LERROR, ("Can't load notes, XML is ill-formed.", content)); return false; } notes.clear(); if (!LoadFromXml(xml, notes, uploadedNotesCount)) { LOG(LERROR, ("Can't load notes, file is ill-formed.", content)); return false; } return true; } /// Not thread-safe, use synchronization. bool Save(std::string const & fileName, std::list const & notes, uint32_t const uploadedNotesCount) { pugi::xml_document xml; SaveToXml(notes, xml, uploadedNotesCount); return base::WriteToTempAndRenameToFile(fileName, [&xml](std::string const & fileName) { return xml.save_file(fileName.data(), " "); }); } } // namespace namespace editor { std::shared_ptr Notes::MakeNotes(std::string const & fileName, bool const fullPath) { return std::shared_ptr( new Notes(fullPath ? fileName : GetPlatform().WritablePathForFile(fileName))); } Notes::Notes(std::string const & fileName) : m_fileName(fileName) { Load(m_fileName, m_notes, m_uploadedNotesCount); } void Notes::CreateNote(ms::LatLon const & latLon, std::string const & text) { if (text.empty()) { LOG(LWARNING, ("Attempt to create empty note")); return; } if (!mercator::ValidLat(latLon.m_lat) || !mercator::ValidLon(latLon.m_lon)) { LOG(LWARNING, ("A note attached to a wrong latLon", latLon)); return; } std::lock_guard g(m_mu); auto const it = std::find_if(m_notes.begin(), m_notes.end(), [&latLon, &text](Note const & note) { return latLon.EqualDxDy(note.m_point, kTolerance) && text == note.m_note; }); // No need to add the same note. It works in case when saved note are not uploaded yet. if (it != m_notes.end()) return; m_notes.emplace_back(latLon, text); Save(m_fileName, m_notes, m_uploadedNotesCount); } void Notes::Upload(osm::OsmOAuth const & auth) { // Capture self to keep it from destruction until this thread is done. auto const self = shared_from_this(); auto const doUpload = [self, auth]() { std::unique_lock ulock(self->m_mu); // Size of m_notes is decreased only in this method. auto & notes = self->m_notes; size_t size = notes.size(); osm::ServerApi06 api(auth); while (size > 0) { try { ulock.unlock(); auto const id = api.CreateNote(notes.front().m_point, notes.front().m_note); ulock.lock(); LOG(LINFO, ("A note uploaded with id", id)); } catch (osm::ServerApi06::ServerApi06Exception const & e) { LOG(LERROR, ("Can't upload note.", e.Msg())); // We believe that next iterations will suffer from the same error. return; } notes.pop_front(); --size; ++self->m_uploadedNotesCount; Save(self->m_fileName, self->m_notes, self->m_uploadedNotesCount); } }; static auto future = std::async(std::launch::async, doUpload); auto const status = future.wait_for(std::chrono::milliseconds(0)); if (status == std::future_status::ready) future = std::async(std::launch::async, doUpload); } std::list Notes::GetNotes() const { std::lock_guard g(m_mu); return m_notes; } size_t Notes::NotUploadedNotesCount() const { std::lock_guard g(m_mu); return m_notes.size(); } size_t Notes::UploadedNotesCount() const { std::lock_guard g(m_mu); return m_uploadedNotesCount; } } // namespace editor