// Copyright (c) Charles J. Cliffe // SPDX-License-Identifier: GPL-2.0+ #include "BookmarkMgr.h" #include "CubicSDR.h" #include "DataTree.h" #define BOOKMARK_RECENTS_MAX 25 BookmarkEntry::~BookmarkEntry() { delete node; } BookmarkMgr::BookmarkMgr() { rangesSorted = false; } // void BookmarkMgr::saveToFile(const std::string& bookmarkFn, bool backup, bool useFullpath) { std::lock_guard < std::recursive_mutex > lock(busy_lock); DataTree s("cubicsdr_bookmarks"); DataNode *header = s.rootNode()->newChild("header"); header->newChild("version")->element()->set(wxString(CUBICSDR_VERSION).ToStdWstring()); DataNode *branches = s.rootNode()->newChild("branches"); *branches->newChild("active") = wxGetApp().getAppFrame()->getBookmarkView()->getExpandState("active")?1:0; *branches->newChild("range") = wxGetApp().getAppFrame()->getBookmarkView()->getExpandState("range")?1:0; *branches->newChild("bookmark") = wxGetApp().getAppFrame()->getBookmarkView()->getExpandState("bookmark")?1:0; *branches->newChild("recent") = wxGetApp().getAppFrame()->getBookmarkView()->getExpandState("recent")?1:0; DataNode *view_ranges = s.rootNode()->newChild("ranges"); for (const auto& re_i : ranges) { DataNode *range = view_ranges->newChild("range"); *range->newChild("label") = re_i->label; *range->newChild("freq") = re_i->freq; *range->newChild("start") = re_i->startFreq; *range->newChild("end") = re_i->endFreq; } DataNode *modems = s.rootNode()->newChild("modems"); for (auto &bmd_i : bmData) { DataNode *group = modems->newChild("group"); *group->newChild("@name") = bmd_i.first; *group->newChild("@expanded") = (getExpandState(bmd_i.first)?std::string("true"):std::string("false")); for (auto &bm_i : bmd_i.second ) { //if a matching demodulator exists, use its data instead to be be saved, because output_device could have been //modified by the user. So, save that "live" version instead. auto matchingDemod = wxGetApp().getDemodMgr().getLastDemodulatorWith(bm_i->type, bm_i->label, bm_i->frequency, bm_i->bandwidth); if (matchingDemod != nullptr) { wxGetApp().getDemodMgr().saveInstance(group->newChild("modem"), matchingDemod); } else { group->newChildCloneFrom("modem", bm_i->node); } } } DataNode *recent_modems = s.rootNode()->newChild("recent_modems"); for (const auto& demod : wxGetApp().getDemodMgr().getDemodulators()) { wxGetApp().getDemodMgr().saveInstance(recent_modems->newChild("modem"),demod); } for (auto &r_i : this->recents) { recent_modems->newChildCloneFrom("modem", r_i->node); } wxFileName saveFile; wxFileName saveFileBackup; if (useFullpath) { saveFile.Assign(bookmarkFn); saveFileBackup.Assign(bookmarkFn + ".backup"); } else { saveFile.Assign(wxGetApp().getConfig()->getConfigDir(), bookmarkFn); saveFileBackup.Assign(wxGetApp().getConfig()->getConfigDir(), bookmarkFn + ".backup"); } if (saveFile.IsDirWritable()) { // Hopefully leave at least a readable backup in case of failure.. if (backup && saveFile.FileExists() && (!saveFileBackup.FileExists() || saveFileBackup.IsFileWritable())) { wxCopyFile(saveFile.GetFullPath(wxPATH_NATIVE).ToStdString(), saveFileBackup.GetFullPath(wxPATH_NATIVE).ToStdString()); } s.SaveToFileXML(saveFile.GetFullPath(wxPATH_NATIVE).ToStdString()); } } bool BookmarkMgr::loadFromFile(const std::string& bookmarkFn, bool backup, bool useFullpath) { std::lock_guard < std::recursive_mutex > lock(busy_lock); wxFileName loadFile; wxFileName failFile; wxFileName lastLoaded; wxFileName backupFile; if (useFullpath) { loadFile.Assign(bookmarkFn); failFile.Assign(bookmarkFn + ".failedload"); lastLoaded.Assign(bookmarkFn + ".lastloaded"); backupFile.Assign(bookmarkFn + ".backup"); } else { loadFile.Assign(wxGetApp().getConfig()->getConfigDir(), bookmarkFn); failFile.Assign(wxGetApp().getConfig()->getConfigDir(), bookmarkFn + ".failedload"); lastLoaded.Assign(wxGetApp().getConfig()->getConfigDir(), bookmarkFn + ".lastloaded"); backupFile.Assign(wxGetApp().getConfig()->getConfigDir(), bookmarkFn + ".backup"); } DataTree s; bool loadStatusOk = true; // File exists but is not readable if (loadFile.FileExists() && !loadFile.IsFileReadable()) { return false; } // New instance of bookmark savefiles if (backup && !loadFile.FileExists() && !lastLoaded.FileExists() && !backupFile.FileExists()) { loadDefaultRanges(); return true; } // Attempt to load file if (!s.LoadFromFileXML(loadFile.GetFullPath(wxPATH_NATIVE).ToStdString())) { return false; } //Check if it is a bookmark file, read the root node. if (s.rootNode()->getName() != "cubicsdr_bookmarks") { return false; } // Clear any active data bmData.clear(); recents.clear(); ranges.clear(); bmDataSorted.clear(); if (s.rootNode()->hasAnother("branches")) { DataNode *branches = s.rootNode()->getNext("branches"); int bActive = 1, bRange = 0, bBookmark = 1, bRecent = 1; if (branches->hasAnother("active")) branches->getNext("active")->element()->get(bActive); if (branches->hasAnother("range")) branches->getNext("range")->element()->get(bRange); if (branches->hasAnother("bookmark")) branches->getNext("bookmark")->element()->get(bBookmark); if (branches->hasAnother("recent")) branches->getNext("recent")->element()->get(bRecent); wxGetApp().getAppFrame()->getBookmarkView()->setExpandState("active", bActive != 0); wxGetApp().getAppFrame()->getBookmarkView()->setExpandState("range", bRange != 0); wxGetApp().getAppFrame()->getBookmarkView()->setExpandState("bookmark", bBookmark != 0); wxGetApp().getAppFrame()->getBookmarkView()->setExpandState("recent", bRecent != 0); } if (s.rootNode()->hasAnother("ranges")) { DataNode *view_ranges = s.rootNode()->getNext("ranges"); while (view_ranges->hasAnother("range")) { DataNode *range = view_ranges->getNext("range"); BookmarkRangeEntryPtr re(new BookmarkRangeEntry); if (range->hasAnother("label")) range->getNext("label")->element()->get(re->label); if (range->hasAnother("freq")) range->getNext("freq")->element()->get(re->freq); if (range->hasAnother("start")) range->getNext("start")->element()->get(re->startFreq); if (range->hasAnother("end")) range->getNext("end")->element()->get(re->endFreq); addRange(re); } } if (s.rootNode()->hasAnother("modems")) { DataNode *modems = s.rootNode()->getNext("modems"); while (modems->hasAnother("group")) { DataNode *group = modems->getNext("group"); std::string groupExpandState = "true"; std::string groupName = "Unnamed"; if (group->hasAnother("@name")) { groupName = group->getNext("@name")->element()->toString(); } if (group->hasAnother("@expanded")) { groupExpandState = group->getNext("@expanded")->element()->toString(); } setExpandState(groupName, (groupExpandState == "true")); while (group->hasAnother("modem")) { DataNode *modem = group->getNext("modem"); BookmarkEntryPtr be = nodeToBookmark(modem); if (be) { addBookmark(groupName, be); } else { std::cout << "error loading bookmarked modem.." << std::endl; loadStatusOk = false; } } } } if (s.rootNode()->hasAnother("recent_modems")) { DataNode *recent_modems = s.rootNode()->getNext("recent_modems"); while (recent_modems->hasAnother("modem")) { DataNode *modem = recent_modems->getNext("modem"); BookmarkEntryPtr be = nodeToBookmark(modem); if (be) { addRecent(be); } else { std::cout << "error loading recent modem.." << std::endl; loadStatusOk = false; } } } if (backup) { if (loadStatusOk) { // Loaded OK; keep a copy if (loadFile.IsDirWritable()) { if (loadFile.FileExists() && (!lastLoaded.FileExists() || lastLoaded.IsFileWritable())) { wxCopyFile(loadFile.GetFullPath(wxPATH_NATIVE).ToStdString(), lastLoaded.GetFullPath(wxPATH_NATIVE).ToStdString()); } } } else { if (loadFile.IsDirWritable()) { // Load failed; keep a copy of the failed bookmark file for analysis? if (loadFile.FileExists() && (!failFile.FileExists() || failFile.IsFileWritable())) { wxCopyFile(loadFile.GetFullPath(wxPATH_NATIVE).ToStdString(), failFile.GetFullPath(wxPATH_NATIVE).ToStdString()); } } } } return loadStatusOk; } void BookmarkMgr::loadDefaultRanges() { addRange(std::make_shared(L"2200 Meters (135.7-137.8 kHz)", 136750, 135700, 137800)); addRange(std::make_shared(L"630 Meters (472-479 kHz)", 475500, 472000, 479000)); addRange(std::make_shared(L"160 Meters (1.8-2 MHz)", 1900000, 1800000, 2000000)); addRange(std::make_shared(L"80 Meters (3.5-4.0 MHz)", 3750000, 3500000, 4000000)); addRange(std::make_shared(L"60 Meters (5.332-5.405Mhz)", 5368500, 5332000, 5405000)); addRange(std::make_shared(L"40 Meters (7.0-7.3 MHz)", 7150000, 7000000, 7300000)); addRange(std::make_shared(L"30 Meters (10.1-10.15 MHz)", 10125000, 10100000, 10150000)); addRange(std::make_shared(L"20 Meters (14.0-14.35 MHz)", 14175000, 14000000, 14350000)); addRange(std::make_shared(L"17 Meters (17.044-19.092 MHz)", 18068180, 17044180, 19092180)); addRange(std::make_shared(L"15 Meters (21-21.45 MHz)", 21225000, 21000000, 21450000)); addRange(std::make_shared(L"12 Meters (24.89-24.99 MHz)", 24940000, 24890000, 24990000)); addRange(std::make_shared(L"10 Meters (28-29.7 MHz)", 28850000, 28000000, 29700000)); addRange(std::make_shared(L"6 Meters (50-54 MHz)", 52000000, 50000000, 54000000)); addRange(std::make_shared(L"4 Meters (70-70.5 MHz)", 70250000, 70000000, 70500000)); addRange(std::make_shared(L"2 Meters (144-148 MHz)", 146000000, 144000000, 148000000)); addRange(std::make_shared(L"1.25 Meters (219-225 MHz)", 222000000, 219000000, 225000000)); addRange(std::make_shared(L"70 cm (420-450 MHz)", 435000000, 420000000, 450000000)); addRange(std::make_shared(L"33 cm (902-928 MHz)", 915000000, 902000000, 928000000)); addRange(std::make_shared(L"23 cm (1240-1300 MHz)", 1270000000, 1240000000, 1300000000)); addRange(std::make_shared(L"13 cm lower (2300-2310 MHz)", 2305000000, 2300000000, 2310000000)); addRange(std::make_shared(L"13 cm upper (2390-2450 MHz)", 2420000000, 2390000000, 2450000000)); } void BookmarkMgr::resetBookmarks() { std::lock_guard < std::recursive_mutex > lock(busy_lock); // Clear any active data bmData.clear(); recents.clear(); ranges.clear(); bmDataSorted.clear(); loadDefaultRanges(); } bool BookmarkMgr::hasLastLoad(const std::string& bookmarkFn) { wxFileName lastLoaded(wxGetApp().getConfig()->getConfigDir(), bookmarkFn + ".lastloaded"); return lastLoaded.FileExists() && lastLoaded.IsFileReadable(); } bool BookmarkMgr::hasBackup(const std::string& bookmarkFn) { wxFileName backupFile(wxGetApp().getConfig()->getConfigDir(), bookmarkFn + ".backup"); return backupFile.FileExists() && backupFile.IsFileReadable(); } void BookmarkMgr::addBookmark(const std::string& group, const DemodulatorInstancePtr& demod) { std::lock_guard < std::recursive_mutex > lock(busy_lock); //Create a BookmarkEntry from demod data, saving its //characteristics in be->node. BookmarkEntryPtr be = demodToBookmarkEntry(demod); bmData[group].push_back(be); bmDataSorted[group] = false; } void BookmarkMgr::addBookmark(const std::string& group, const BookmarkEntryPtr& be) { std::lock_guard < std::recursive_mutex > lock(busy_lock); bmData[group].push_back(be); bmDataSorted[group] = false; } void BookmarkMgr::removeBookmark(const std::string& group, const BookmarkEntryPtr& be) { std::lock_guard < std::recursive_mutex > lock(busy_lock); if (bmData.find(group) == bmData.end()) { return; } auto i = std::find(bmData[group].begin(), bmData[group].end(), be); if (i != bmData[group].end()) { bmData[group].erase(i); } } void BookmarkMgr::removeBookmark(const BookmarkEntryPtr& be) { std::lock_guard < std::recursive_mutex > lock(busy_lock); for (auto &bmd_i : bmData) { auto i = std::find(bmd_i.second.begin(), bmd_i.second.end(), be); if (i != bmd_i.second.end()) { bmd_i.second.erase(i); } } } void BookmarkMgr::moveBookmark(const BookmarkEntryPtr& be, const std::string& group) { std::lock_guard < std::recursive_mutex > lock(busy_lock); for (auto &bmd_i : bmData) { auto i = std::find(bmd_i.second.begin(), bmd_i.second.end(), be); if (i != bmd_i.second.end()) { if (bmd_i.first == group) { return; } bmData[group].push_back(*i); bmd_i.second.erase(i); bmDataSorted[group] = false; bmDataSorted[bmd_i.first] = false; return; } } } void BookmarkMgr::addGroup(const std::string& group) { std::lock_guard < std::recursive_mutex > lock(busy_lock); if (bmData.find(group) == bmData.end()) { BookmarkList dummy = bmData[group]; } } void BookmarkMgr::removeGroup(const std::string& group) { std::lock_guard < std::recursive_mutex > lock(busy_lock); auto i = bmData.find(group); if (i != bmData.end()) { bmData.erase(group); } } void BookmarkMgr::renameGroup(const std::string& group, const std::string& ngroup) { if (group == ngroup) { return; } std::lock_guard < std::recursive_mutex > lock(busy_lock); auto i = bmData.find(group); auto it = bmData.find(ngroup); if (i != bmData.end() && it != bmData.end()) { for (const auto& ii : bmData[group]) { bmData[ngroup].push_back(ii); } bmData.erase(group); } else if (i != bmData.end()) { bmData[ngroup] = bmData[group]; bmData.erase(group); } } BookmarkList BookmarkMgr::getBookmarks(const std::string& group) { std::lock_guard < std::recursive_mutex > lock(busy_lock); if (bmData.find(group) == bmData.end()) { return BookmarkList(); } if (!bmDataSorted[group]) { std::sort(bmData[group].begin(), bmData[group].end(), [](const BookmarkEntryPtr a, const BookmarkEntryPtr b) -> bool { return a->frequency < b->frequency;}); bmDataSorted[group] = true; } return bmData[group]; } void BookmarkMgr::getGroups(BookmarkNames &arr) { std::lock_guard < std::recursive_mutex > lock(busy_lock); for (auto & i : bmData) { arr.push_back(i.first); } } void BookmarkMgr::getGroups(wxArrayString &arr) { std::lock_guard < std::recursive_mutex > lock(busy_lock); for (auto & i : bmData) { arr.push_back(i.first); } } void BookmarkMgr::setExpandState(const std::string& groupName, bool state) { std::lock_guard < std::recursive_mutex > lock(busy_lock); expandState[groupName] = state; } bool BookmarkMgr::getExpandState(const std::string& groupName) { std::lock_guard < std::recursive_mutex > lock(busy_lock); if (expandState.find(groupName) == expandState.end()) { return true; } return expandState[groupName]; } void BookmarkMgr::updateActiveList() { if (wxGetApp().isShuttingDown()) { return; } BookmarkView *bmv = wxGetApp().getAppFrame()->getBookmarkView(); if (bmv) { bmv->updateActiveList(); } } void BookmarkMgr::updateBookmarks() { BookmarkView *bmv = wxGetApp().getAppFrame()->getBookmarkView(); if (bmv) { bmv->updateBookmarks(); } } void BookmarkMgr::updateBookmarks(const std::string& group) { BookmarkView *bmv = wxGetApp().getAppFrame()->getBookmarkView(); if (bmv) { bmv->updateBookmarks(group); } } void BookmarkMgr::addRecent(const DemodulatorInstancePtr& demod) { std::lock_guard < std::recursive_mutex > lock(busy_lock); recents.push_back(demodToBookmarkEntry(demod)); trimRecents(); } void BookmarkMgr::addRecent(const BookmarkEntryPtr& be) { std::lock_guard < std::recursive_mutex > lock(busy_lock); recents.push_back(be); trimRecents(); } void BookmarkMgr::removeRecent(const BookmarkEntryPtr& be) { std::lock_guard < std::recursive_mutex > lock(busy_lock); auto bm_i = std::find(recents.begin(),recents.end(), be); if (bm_i != recents.end()) { recents.erase(bm_i); } } BookmarkList BookmarkMgr::getRecents() { std::lock_guard < std::recursive_mutex > lock(busy_lock); //return a copy return recents; } void BookmarkMgr::clearRecents() { std::lock_guard < std::recursive_mutex > lock(busy_lock); recents.clear(); } void BookmarkMgr::trimRecents() { if (recents.size() > BOOKMARK_RECENTS_MAX) { recents.erase(recents.begin(), recents.begin()+1); } } void BookmarkMgr::addRange(const BookmarkRangeEntryPtr& re) { std::lock_guard < std::recursive_mutex > lock(busy_lock); ranges.push_back(re); rangesSorted = false; } void BookmarkMgr::removeRange(const BookmarkRangeEntryPtr& re) { std::lock_guard < std::recursive_mutex > lock(busy_lock); auto re_i = std::find(ranges.begin(), ranges.end(), re); if (re_i != ranges.end()) { ranges.erase(re_i); } } BookmarkRangeList BookmarkMgr::getRanges() { std::lock_guard < std::recursive_mutex > lock(busy_lock); if (!rangesSorted) { std::sort(ranges.begin(), ranges.end(), [](const BookmarkRangeEntryPtr a, const BookmarkRangeEntryPtr b) -> bool { return a->freq < b->freq;}); rangesSorted = true; } return ranges; } void BookmarkMgr::clearRanges() { std::lock_guard < std::recursive_mutex > lock(busy_lock); ranges.clear(); } BookmarkEntryPtr BookmarkMgr::demodToBookmarkEntry(const DemodulatorInstancePtr& demod) { BookmarkEntryPtr be(new BookmarkEntry); be->bandwidth = demod->getBandwidth(); be->type = demod->getDemodulatorType(); be->label = demod->getDemodulatorUserLabel(); be->frequency = demod->getFrequency(); //fine to do so here, so long nobody overrides be->node, DataNode will be //deleted at last BookmarkEntryPtr be ref. be->node = new DataNode; wxGetApp().getDemodMgr().saveInstance(be->node, demod); return be; } std::wstring BookmarkMgr::getSafeWstringValue(DataNode* node, const std::string& childNodeName) { std::wstring decodedWString; if (node != nullptr) { DataNode* childNode = node->getNext(childNodeName.c_str()); //1) decode as encoded wstring: try { childNode->element()->get(decodedWString); } catch (const DataTypeMismatchException &) { //2) wstring decode fail, try simple std::string std::string decodedStdString; try { childNode->element()->get(decodedStdString); //use wxString for a clean conversion to a wstring: decodedWString = wxString(decodedStdString).ToStdWstring(); } catch (const DataTypeMismatchException &) { //nothing works, return an empty string. decodedWString = L""; } } } return decodedWString; } BookmarkEntryPtr BookmarkMgr::nodeToBookmark(DataNode *node) { if (!node->hasAnother("frequency") || !node->hasAnother("type") || !node->hasAnother("bandwidth")) { return nullptr; } BookmarkEntryPtr be(new BookmarkEntry()); node->getNext("frequency")->element()->get(be->frequency); node->getNext("type")->element()->get(be->type); node->getNext("bandwidth")->element()->get(be->bandwidth); if (node->hasAnother("user_label")) { be->label = BookmarkMgr::getSafeWstringValue( node, "user_label"); } node->rewindAll(); //fine to do so here, so long nobody overrides be->node, DataNode will be //deleted at last BookmarkEntryPtr be ref. //copy data from *node. be->node = new DataNode("node",*node); return be; } std::wstring BookmarkMgr::getBookmarkEntryDisplayName(const BookmarkEntryPtr& bmEnt) { std::wstring dispName = bmEnt->label; if (dispName.empty()) { std::string freqStr = frequencyToStr(bmEnt->frequency) + " " + bmEnt->type; dispName = wxString(freqStr).ToStdWstring(); } return dispName; } std::wstring BookmarkMgr::getActiveDisplayName(const DemodulatorInstancePtr& demod) { std::wstring activeName = demod->getDemodulatorUserLabel(); if (activeName.empty()) { std::string wstr = frequencyToStr(demod->getFrequency()) + " " + demod->getDemodulatorType(); activeName = wxString(wstr).ToStdWstring(); } return activeName; } void BookmarkMgr::removeActive(const DemodulatorInstancePtr& demod) { if (demod == nullptr) { return; } //Delete demodulator wxGetApp().getDemodMgr().setActiveDemodulator(nullptr, true); wxGetApp().getDemodMgr().setActiveDemodulator(nullptr, false); wxGetApp().removeDemodulator(demod); wxGetApp().getDemodMgr().deleteThread(demod); }