bool Editor::Save(string const & fullFilePath) const { // TODO(AlexZ): Improve synchronization in Editor code. static mutex saveMutex; lock_guard<mutex> lock(saveMutex); if (m_features.empty()) { my::DeleteFileX(GetEditorFilePath()); return true; } xml_document doc; xml_node root = doc.append_child(kXmlRootNode); // Use format_version for possible future format changes. root.append_attribute("format_version") = 1; for (auto const & mwm : m_features) { xml_node mwmNode = root.append_child(kXmlMwmNode); mwmNode.append_attribute("name") = mwm.first.GetInfo()->GetCountryName().c_str(); mwmNode.append_attribute("version") = static_cast<long long>(mwm.first.GetInfo()->GetVersion()); xml_node deleted = mwmNode.append_child(kDeleteSection); xml_node modified = mwmNode.append_child(kModifySection); xml_node created = mwmNode.append_child(kCreateSection); for (auto const & index : mwm.second) { FeatureTypeInfo const & fti = index.second; // TODO: Do we really need to serialize deleted features in full details? Looks like mwm ID and meta fields are enough. XMLFeature xf = fti.m_feature.ToXML(true /*type serializing helps during migration*/); xf.SetMWMFeatureIndex(index.first); if (!fti.m_street.empty()) xf.SetTagValue(kAddrStreetTag, fti.m_street); ASSERT_NOT_EQUAL(0, fti.m_modificationTimestamp, ()); xf.SetModificationTime(fti.m_modificationTimestamp); if (fti.m_uploadAttemptTimestamp != my::INVALID_TIME_STAMP) { xf.SetUploadTime(fti.m_uploadAttemptTimestamp); ASSERT(!fti.m_uploadStatus.empty(), ("Upload status updates with upload timestamp.")); xf.SetUploadStatus(fti.m_uploadStatus); if (!fti.m_uploadError.empty()) xf.SetUploadError(fti.m_uploadError); } switch (fti.m_status) { case FeatureStatus::Deleted: VERIFY(xf.AttachToParentNode(deleted), ()); break; case FeatureStatus::Modified: VERIFY(xf.AttachToParentNode(modified), ()); break; case FeatureStatus::Created: VERIFY(xf.AttachToParentNode(created), ()); break; case FeatureStatus::Untouched: CHECK(false, ("Not edited features shouldn't be here.")); } } } return my::WriteToTempAndRenameToFile( fullFilePath, [&doc](string const & fileName) { return doc.save_file(fileName.data(), " "); }); }
void Editor::UploadChanges(string const & key, string const & secret, TChangesetTags tags, TFinishUploadCallback callBack) { if (m_notes->NotUploadedNotesCount()) UploadNotes(key, secret); if (!HaveMapEditsToUpload()) { LOG(LDEBUG, ("There are no local edits to upload.")); return; } alohalytics::LogEvent("Editor_DataSync_started"); // TODO(AlexZ): features access should be synchronized. auto const upload = [this](string key, string secret, TChangesetTags tags, TFinishUploadCallback callBack) { // This lambda was designed to start after app goes into background. But for cases when user is immediately // coming back to the app we work with a copy, because 'for' loops below can take a significant amount of time. auto features = m_features; int uploadedFeaturesCount = 0, errorsCount = 0; ChangesetWrapper changeset({key, secret}, tags); for (auto & id : features) { for (auto & index : id.second) { FeatureTypeInfo & fti = index.second; // Do not process already uploaded features or those failed permanently. if (!NeedsUpload(fti.m_uploadStatus)) continue; string ourDebugFeatureString; try { switch (fti.m_status) { case FeatureStatus::Untouched: CHECK(false, ("It's impossible.")); continue; case FeatureStatus::Obsolete: continue; // Obsolete features will be deleted by OSMers. case FeatureStatus::Created: { XMLFeature feature = fti.m_feature.ToXML(true); if (!fti.m_street.empty()) feature.SetTagValue(kAddrStreetTag, fti.m_street); ourDebugFeatureString = DebugPrint(feature); ASSERT_EQUAL(feature.GetType(), XMLFeature::Type::Node, ("Linear and area features creation is not supported yet.")); try { XMLFeature osmFeature = changeset.GetMatchingNodeFeatureFromOSM(fti.m_feature.GetCenter()); // If we are here, it means that object already exists at the given point. // To avoid nodes duplication, merge and apply changes to it instead of creating an new one. XMLFeature const osmFeatureCopy = osmFeature; osmFeature.ApplyPatch(feature); // Check to avoid uploading duplicates into OSM. if (osmFeature == osmFeatureCopy) { LOG(LWARNING, ("Local changes are equal to OSM, feature has not been uploaded.", osmFeatureCopy)); // Don't delete this local change right now for user to see it in profile. // It will be automatically deleted by migration code on the next maps update. } else { LOG(LDEBUG, ("Create case: uploading patched feature", osmFeature)); changeset.Modify(osmFeature); } } catch (ChangesetWrapper::OsmObjectWasDeletedException const &) { // Object was never created by anyone else - it's safe to create it. changeset.Create(feature); } catch (ChangesetWrapper::EmptyFeatureException const &) { // There is another node nearby, but it should be safe to create a new one. changeset.Create(feature); } catch (...) { // Pass network or other errors to outside exception handler. throw; } } break; case FeatureStatus::Modified: { // Do not serialize feature's type to avoid breaking OSM data. // TODO: Implement correct types matching when we support modifying existing feature types. XMLFeature feature = fti.m_feature.ToXML(false); if (!fti.m_street.empty()) feature.SetTagValue(kAddrStreetTag, fti.m_street); ourDebugFeatureString = DebugPrint(feature); auto const originalFeaturePtr = GetOriginalFeature(fti.m_feature.GetID()); if (!originalFeaturePtr) { LOG(LERROR, ("A feature with id", fti.m_feature.GetID(), "cannot be loaded.")); alohalytics::LogEvent("Editor_MissingFeature_Error"); RemoveFeatureFromStorageIfExists(fti.m_feature.GetID()); continue; } XMLFeature osmFeature = GetMatchingFeatureFromOSM( changeset, *originalFeaturePtr); XMLFeature const osmFeatureCopy = osmFeature; osmFeature.ApplyPatch(feature); // Check to avoid uploading duplicates into OSM. if (osmFeature == osmFeatureCopy) { LOG(LWARNING, ("Local changes are equal to OSM, feature has not been uploaded.", osmFeatureCopy)); // Don't delete this local change right now for user to see it in profile. // It will be automatically deleted by migration code on the next maps update. } else { LOG(LDEBUG, ("Uploading patched feature", osmFeature)); changeset.Modify(osmFeature); } } break; case FeatureStatus::Deleted: auto const originalFeaturePtr = GetOriginalFeature(fti.m_feature.GetID()); if (!originalFeaturePtr) { LOG(LERROR, ("A feature with id", fti.m_feature.GetID(), "cannot be loaded.")); alohalytics::LogEvent("Editor_MissingFeature_Error"); RemoveFeatureFromStorageIfExists(fti.m_feature.GetID()); continue; } changeset.Delete(GetMatchingFeatureFromOSM( changeset, *originalFeaturePtr)); break; } fti.m_uploadStatus = kUploaded; fti.m_uploadError.clear(); ++uploadedFeaturesCount; } catch (ChangesetWrapper::OsmObjectWasDeletedException const & ex) { fti.m_uploadStatus = kDeletedFromOSMServer; fti.m_uploadError = ex.what(); ++errorsCount; LOG(LWARNING, (ex.what())); } catch (ChangesetWrapper::RelationFeatureAreNotSupportedException const & ex) { fti.m_uploadStatus = kRelationsAreNotSupported; fti.m_uploadError = ex.what(); ++errorsCount; LOG(LWARNING, (ex.what())); } catch (ChangesetWrapper::EmptyFeatureException const & ex) { fti.m_uploadStatus = kWrongMatch; fti.m_uploadError = ex.what(); ++errorsCount; LOG(LWARNING, (ex.what())); } catch (RootException const & ex) { fti.m_uploadStatus = kNeedsRetry; fti.m_uploadError = ex.what(); ++errorsCount; LOG(LWARNING, (ex.what())); } // TODO(AlexZ): Use timestamp from the server. fti.m_uploadAttemptTimestamp = time(nullptr); if (fti.m_uploadStatus != kUploaded) { ms::LatLon const ll = MercatorBounds::ToLatLon(feature::GetCenter(fti.m_feature)); alohalytics::LogEvent("Editor_DataSync_error", {{"type", fti.m_uploadStatus}, {"details", fti.m_uploadError}, {"our", ourDebugFeatureString}, {"mwm", fti.m_feature.GetID().GetMwmName()}, {"mwm_version", strings::to_string(fti.m_feature.GetID().GetMwmVersion())} }, alohalytics::Location::FromLatLon(ll.lat, ll.lon)); } // Call Save every time we modify each feature's information. SaveUploadedInformation(fti); } } alohalytics::LogEvent("Editor_DataSync_finished", {{"errors", strings::to_string(errorsCount)}, {"uploaded", strings::to_string(uploadedFeaturesCount)}, {"changeset", strings::to_string(changeset.GetChangesetId())} }); if (callBack) { UploadResult result = UploadResult::NothingToUpload; if (uploadedFeaturesCount) result = UploadResult::Success; else if (errorsCount) result = UploadResult::Error; callBack(result); } }; // Do not run more than one upload thread at a time. static auto future = async(launch::async, upload, key, secret, tags, callBack); auto const status = future.wait_for(milliseconds(0)); if (status == future_status::ready) future = async(launch::async, upload, key, secret, tags, callBack); }