mirror of
				https://github.com/ciphervance/supercell-wx.git
				synced 2025-10-31 02:50:04 +00:00 
			
		
		
		
	Catch exceptions in background threads
This commit is contained in:
		
							parent
							
								
									863cdd0384
								
							
						
					
					
						commit
						e1d61fccfa
					
				
					 13 changed files with 866 additions and 665 deletions
				
			
		|  | @ -42,7 +42,17 @@ public: | |||
|          [this](const types::TextEventKey& key, size_t messageIndex) | ||||
|          { | ||||
|             boost::asio::post(threadPool_, | ||||
|                               [=, this]() { HandleAlert(key, messageIndex); }); | ||||
|                               [=, this]() | ||||
|                               { | ||||
|                                  try | ||||
|                                  { | ||||
|                                     HandleAlert(key, messageIndex); | ||||
|                                  } | ||||
|                                  catch (const std::exception& ex) | ||||
|                                  { | ||||
|                                     logger_->error(ex.what()); | ||||
|                                  } | ||||
|                               }); | ||||
|          }); | ||||
|    } | ||||
| 
 | ||||
|  |  | |||
|  | @ -25,6 +25,8 @@ public: | |||
| 
 | ||||
|    ~Impl() { threadPool_.join(); } | ||||
| 
 | ||||
|    void DownloadSync(const std::shared_ptr<request::DownloadRequest>& request); | ||||
| 
 | ||||
|    boost::asio::thread_pool threadPool_ {1u}; | ||||
| 
 | ||||
|    DownloadManager* self_; | ||||
|  | @ -36,224 +38,229 @@ DownloadManager::~DownloadManager() = default; | |||
| void DownloadManager::Download( | ||||
|    const std::shared_ptr<request::DownloadRequest>& request) | ||||
| { | ||||
|    boost::asio::post( | ||||
|       p->threadPool_, | ||||
|       [=]() | ||||
|    boost::asio::post(p->threadPool_, | ||||
|                      [=]() | ||||
|                      { | ||||
|                         try | ||||
|                         { | ||||
|                            p->DownloadSync(request); | ||||
|                         } | ||||
|                         catch (const std::exception& ex) | ||||
|                         { | ||||
|                            logger_->error(ex.what()); | ||||
|                         } | ||||
|                      }); | ||||
| } | ||||
| 
 | ||||
| void DownloadManager::Impl::DownloadSync( | ||||
|    const std::shared_ptr<request::DownloadRequest>& request) | ||||
| { | ||||
|    // Prepare destination file
 | ||||
|    const std::filesystem::path& destinationPath = request->destination_path(); | ||||
| 
 | ||||
|    if (!destinationPath.has_parent_path()) | ||||
|    { | ||||
|       logger_->error("Destination has no parent path: \"{}\""); | ||||
| 
 | ||||
|       Q_EMIT request->RequestComplete( | ||||
|          request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|       return; | ||||
|    } | ||||
| 
 | ||||
|    const std::filesystem::path parentPath = destinationPath.parent_path(); | ||||
| 
 | ||||
|    // Create directory if it doesn't exist
 | ||||
|    if (!std::filesystem::exists(parentPath)) | ||||
|    { | ||||
|       if (!std::filesystem::create_directories(parentPath)) | ||||
|       { | ||||
|          // Prepare destination file
 | ||||
|          const std::filesystem::path& destinationPath = | ||||
|             request->destination_path(); | ||||
|          logger_->error("Unable to create download directory: \"{}\"", | ||||
|                         parentPath.string()); | ||||
| 
 | ||||
|          if (!destinationPath.has_parent_path()) | ||||
|          { | ||||
|             logger_->error("Destination has no parent path: \"{}\""); | ||||
| 
 | ||||
|             Q_EMIT request->RequestComplete( | ||||
|                request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|             return; | ||||
|          } | ||||
| 
 | ||||
|          const std::filesystem::path parentPath = destinationPath.parent_path(); | ||||
| 
 | ||||
|          // Create directory if it doesn't exist
 | ||||
|          if (!std::filesystem::exists(parentPath)) | ||||
|          { | ||||
|             if (!std::filesystem::create_directories(parentPath)) | ||||
|             { | ||||
|                logger_->error("Unable to create download directory: \"{}\"", | ||||
|                               parentPath.string()); | ||||
| 
 | ||||
|                Q_EMIT request->RequestComplete( | ||||
|                   request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|                return; | ||||
|             } | ||||
|          } | ||||
| 
 | ||||
|          // Remove file if it exists
 | ||||
|          if (std::filesystem::exists(destinationPath)) | ||||
|          { | ||||
|             std::error_code error; | ||||
|             if (!std::filesystem::remove(destinationPath, error)) | ||||
|             { | ||||
|                logger_->error( | ||||
|                   "Unable to remove existing destination file ({}): \"{}\"", | ||||
|                   error.message(), | ||||
|                   destinationPath.string()); | ||||
| 
 | ||||
|                Q_EMIT request->RequestComplete( | ||||
|                   request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|                return; | ||||
|             } | ||||
|          } | ||||
| 
 | ||||
|          // Open file for writing
 | ||||
|          std::ofstream ofs {destinationPath, | ||||
|                             std::ios_base::out | std::ios_base::binary | | ||||
|                                std::ios_base::trunc}; | ||||
|          if (!ofs.is_open() || !ofs.good()) | ||||
|          { | ||||
|             logger_->error( | ||||
|                "Unable to open destination file for writing: \"{}\"", | ||||
|                destinationPath.string()); | ||||
| 
 | ||||
|             Q_EMIT request->RequestComplete( | ||||
|                request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|             return; | ||||
|          } | ||||
| 
 | ||||
|          std::chrono::system_clock::time_point lastUpdated {}; | ||||
|          cpr::cpr_off_t                        lastDownloadNow {}; | ||||
|          cpr::cpr_off_t                        lastDownloadTotal {}; | ||||
| 
 | ||||
|          // Download file
 | ||||
|          cpr::Response response = | ||||
|             cpr::Get(cpr::Url {request->url()}, | ||||
|                      cpr::ProgressCallback( | ||||
|                         [&](cpr::cpr_off_t downloadTotal, | ||||
|                             cpr::cpr_off_t downloadNow, | ||||
|                             cpr::cpr_off_t /* uploadTotal */, | ||||
|                             cpr::cpr_off_t /* uploadNow */, | ||||
|                             std::intptr_t /* userdata */) | ||||
|                         { | ||||
|                            using namespace std::chrono_literals; | ||||
| 
 | ||||
|                            std::chrono::system_clock::time_point now = | ||||
|                               std::chrono::system_clock::now(); | ||||
| 
 | ||||
|                            // Only emit an update every 100ms
 | ||||
|                            if ((now > lastUpdated + 100ms || | ||||
|                                 downloadNow == downloadTotal) && | ||||
|                                (downloadNow != lastDownloadNow || | ||||
|                                 downloadTotal != lastDownloadTotal)) | ||||
|                            { | ||||
|                               logger_->trace("Downloaded: {} / {}", | ||||
|                                              downloadNow, | ||||
|                                              downloadTotal); | ||||
| 
 | ||||
|                               Q_EMIT request->ProgressUpdated(downloadNow, | ||||
|                                                               downloadTotal); | ||||
| 
 | ||||
|                               lastUpdated       = now; | ||||
|                               lastDownloadNow   = downloadNow; | ||||
|                               lastDownloadTotal = downloadTotal; | ||||
|                            } | ||||
| 
 | ||||
|                            return !request->IsCanceled(); | ||||
|                         }), | ||||
|                      cpr::WriteCallback( | ||||
|                         [&](std::string data, std::intptr_t /* userdata */) | ||||
|                         { | ||||
|                            // Write file
 | ||||
|                            ofs << data; | ||||
|                            return !request->IsCanceled(); | ||||
|                         })); | ||||
| 
 | ||||
|          bool ofsGood = ofs.good(); | ||||
|          ofs.close(); | ||||
| 
 | ||||
|          // Handle error response
 | ||||
|          if (response.error.code != cpr::ErrorCode::OK || | ||||
|              request->IsCanceled() || !ofsGood) | ||||
|          { | ||||
|             request::DownloadRequest::CompleteReason reason = | ||||
|                request::DownloadRequest::CompleteReason::IOError; | ||||
| 
 | ||||
|             if (request->IsCanceled()) | ||||
|             { | ||||
|                logger_->info("Download request cancelled: {}", request->url()); | ||||
| 
 | ||||
|                reason = request::DownloadRequest::CompleteReason::Canceled; | ||||
|             } | ||||
|             else if (response.error.code != cpr::ErrorCode::OK) | ||||
|             { | ||||
|                logger_->error("Error downloading file ({}): {}", | ||||
|                               response.error.message, | ||||
|                               request->url()); | ||||
| 
 | ||||
|                reason = request::DownloadRequest::CompleteReason::RemoteError; | ||||
|             } | ||||
|             else if (!ofsGood) | ||||
|             { | ||||
|                logger_->error("File I/O error: {}", destinationPath.string()); | ||||
| 
 | ||||
|                reason = request::DownloadRequest::CompleteReason::IOError; | ||||
|             } | ||||
| 
 | ||||
|             std::error_code error; | ||||
|             if (!std::filesystem::remove(destinationPath, error)) | ||||
|             { | ||||
|                logger_->error("Unable to remove destination file: {}, {}", | ||||
|                               destinationPath.string(), | ||||
|                               error.message()); | ||||
|             } | ||||
| 
 | ||||
|             Q_EMIT request->RequestComplete(reason); | ||||
| 
 | ||||
|             return; | ||||
|          } | ||||
| 
 | ||||
|          // Handle response
 | ||||
|          const auto contentMd5 = response.header.find("content-md5"); | ||||
|          if (contentMd5 != response.header.cend() && | ||||
|              !contentMd5->second.empty()) | ||||
|          { | ||||
|             // Open file for reading
 | ||||
|             std::ifstream is {destinationPath, | ||||
|                               std::ios_base::in | std::ios_base::binary}; | ||||
|             if (!is.is_open() || !is.good()) | ||||
|             { | ||||
|                logger_->error("Unable to open destination file for reading: {}", | ||||
|                               destinationPath.string()); | ||||
| 
 | ||||
|                Q_EMIT request->RequestComplete( | ||||
|                   request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|                return; | ||||
|             } | ||||
| 
 | ||||
|             // Compute MD5
 | ||||
|             std::vector<std::uint8_t> digest {}; | ||||
|             if (!util::ComputeDigest(EVP_md5(), is, digest)) | ||||
|             { | ||||
|                logger_->error("Failed to compute MD5: {}", | ||||
|                               destinationPath.string()); | ||||
| 
 | ||||
|                Q_EMIT request->RequestComplete( | ||||
|                   request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|                return; | ||||
|             } | ||||
| 
 | ||||
|             // Compare calculated MD5 with digest in response header
 | ||||
|             QByteArray expectedDigestArray = | ||||
|                QByteArray::fromBase64(contentMd5->second.c_str()); | ||||
|             std::vector<std::uint8_t> expectedDigest( | ||||
|                expectedDigestArray.cbegin(), expectedDigestArray.cend()); | ||||
| 
 | ||||
|             if (digest != expectedDigest) | ||||
|             { | ||||
|                QByteArray calculatedDigest( | ||||
|                   reinterpret_cast<char*>(digest.data()), digest.size()); | ||||
| 
 | ||||
|                logger_->error("Digest mismatch: {} != {}", | ||||
|                               calculatedDigest.toBase64().toStdString(), | ||||
|                               contentMd5->second); | ||||
| 
 | ||||
|                Q_EMIT request->RequestComplete( | ||||
|                   request::DownloadRequest::CompleteReason::DigestError); | ||||
| 
 | ||||
|                return; | ||||
|             } | ||||
|          } | ||||
| 
 | ||||
|          logger_->info("Download complete: {}", request->url()); | ||||
|          Q_EMIT request->RequestComplete( | ||||
|             request::DownloadRequest::CompleteReason::OK); | ||||
|       }); | ||||
|             request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|          return; | ||||
|       } | ||||
|    } | ||||
| 
 | ||||
|    // Remove file if it exists
 | ||||
|    if (std::filesystem::exists(destinationPath)) | ||||
|    { | ||||
|       std::error_code error; | ||||
|       if (!std::filesystem::remove(destinationPath, error)) | ||||
|       { | ||||
|          logger_->error( | ||||
|             "Unable to remove existing destination file ({}): \"{}\"", | ||||
|             error.message(), | ||||
|             destinationPath.string()); | ||||
| 
 | ||||
|          Q_EMIT request->RequestComplete( | ||||
|             request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|          return; | ||||
|       } | ||||
|    } | ||||
| 
 | ||||
|    // Open file for writing
 | ||||
|    std::ofstream ofs {destinationPath, | ||||
|                       std::ios_base::out | std::ios_base::binary | | ||||
|                          std::ios_base::trunc}; | ||||
|    if (!ofs.is_open() || !ofs.good()) | ||||
|    { | ||||
|       logger_->error("Unable to open destination file for writing: \"{}\"", | ||||
|                      destinationPath.string()); | ||||
| 
 | ||||
|       Q_EMIT request->RequestComplete( | ||||
|          request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|       return; | ||||
|    } | ||||
| 
 | ||||
|    std::chrono::system_clock::time_point lastUpdated {}; | ||||
|    cpr::cpr_off_t                        lastDownloadNow {}; | ||||
|    cpr::cpr_off_t                        lastDownloadTotal {}; | ||||
| 
 | ||||
|    // Download file
 | ||||
|    cpr::Response response = cpr::Get( | ||||
|       cpr::Url {request->url()}, | ||||
|       cpr::ProgressCallback( | ||||
|          [&](cpr::cpr_off_t downloadTotal, | ||||
|              cpr::cpr_off_t downloadNow, | ||||
|              cpr::cpr_off_t /* uploadTotal */, | ||||
|              cpr::cpr_off_t /* uploadNow */, | ||||
|              std::intptr_t /* userdata */) | ||||
|          { | ||||
|             using namespace std::chrono_literals; | ||||
| 
 | ||||
|             std::chrono::system_clock::time_point now = | ||||
|                std::chrono::system_clock::now(); | ||||
| 
 | ||||
|             // Only emit an update every 100ms
 | ||||
|             if ((now > lastUpdated + 100ms || downloadNow == downloadTotal) && | ||||
|                 (downloadNow != lastDownloadNow || | ||||
|                  downloadTotal != lastDownloadTotal)) | ||||
|             { | ||||
|                logger_->trace( | ||||
|                   "Downloaded: {} / {}", downloadNow, downloadTotal); | ||||
| 
 | ||||
|                Q_EMIT request->ProgressUpdated(downloadNow, downloadTotal); | ||||
| 
 | ||||
|                lastUpdated       = now; | ||||
|                lastDownloadNow   = downloadNow; | ||||
|                lastDownloadTotal = downloadTotal; | ||||
|             } | ||||
| 
 | ||||
|             return !request->IsCanceled(); | ||||
|          }), | ||||
|       cpr::WriteCallback( | ||||
|          [&](std::string data, std::intptr_t /* userdata */) | ||||
|          { | ||||
|             // Write file
 | ||||
|             ofs << data; | ||||
|             return !request->IsCanceled(); | ||||
|          })); | ||||
| 
 | ||||
|    bool ofsGood = ofs.good(); | ||||
|    ofs.close(); | ||||
| 
 | ||||
|    // Handle error response
 | ||||
|    if (response.error.code != cpr::ErrorCode::OK || request->IsCanceled() || | ||||
|        !ofsGood) | ||||
|    { | ||||
|       request::DownloadRequest::CompleteReason reason = | ||||
|          request::DownloadRequest::CompleteReason::IOError; | ||||
| 
 | ||||
|       if (request->IsCanceled()) | ||||
|       { | ||||
|          logger_->info("Download request cancelled: {}", request->url()); | ||||
| 
 | ||||
|          reason = request::DownloadRequest::CompleteReason::Canceled; | ||||
|       } | ||||
|       else if (response.error.code != cpr::ErrorCode::OK) | ||||
|       { | ||||
|          logger_->error("Error downloading file ({}): {}", | ||||
|                         response.error.message, | ||||
|                         request->url()); | ||||
| 
 | ||||
|          reason = request::DownloadRequest::CompleteReason::RemoteError; | ||||
|       } | ||||
|       else if (!ofsGood) | ||||
|       { | ||||
|          logger_->error("File I/O error: {}", destinationPath.string()); | ||||
| 
 | ||||
|          reason = request::DownloadRequest::CompleteReason::IOError; | ||||
|       } | ||||
| 
 | ||||
|       std::error_code error; | ||||
|       if (!std::filesystem::remove(destinationPath, error)) | ||||
|       { | ||||
|          logger_->error("Unable to remove destination file: {}, {}", | ||||
|                         destinationPath.string(), | ||||
|                         error.message()); | ||||
|       } | ||||
| 
 | ||||
|       Q_EMIT request->RequestComplete(reason); | ||||
| 
 | ||||
|       return; | ||||
|    } | ||||
| 
 | ||||
|    // Handle response
 | ||||
|    const auto contentMd5 = response.header.find("content-md5"); | ||||
|    if (contentMd5 != response.header.cend() && !contentMd5->second.empty()) | ||||
|    { | ||||
|       // Open file for reading
 | ||||
|       std::ifstream is {destinationPath, | ||||
|                         std::ios_base::in | std::ios_base::binary}; | ||||
|       if (!is.is_open() || !is.good()) | ||||
|       { | ||||
|          logger_->error("Unable to open destination file for reading: {}", | ||||
|                         destinationPath.string()); | ||||
| 
 | ||||
|          Q_EMIT request->RequestComplete( | ||||
|             request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|          return; | ||||
|       } | ||||
| 
 | ||||
|       // Compute MD5
 | ||||
|       std::vector<std::uint8_t> digest {}; | ||||
|       if (!util::ComputeDigest(EVP_md5(), is, digest)) | ||||
|       { | ||||
|          logger_->error("Failed to compute MD5: {}", destinationPath.string()); | ||||
| 
 | ||||
|          Q_EMIT request->RequestComplete( | ||||
|             request::DownloadRequest::CompleteReason::IOError); | ||||
| 
 | ||||
|          return; | ||||
|       } | ||||
| 
 | ||||
|       // Compare calculated MD5 with digest in response header
 | ||||
|       QByteArray expectedDigestArray = | ||||
|          QByteArray::fromBase64(contentMd5->second.c_str()); | ||||
|       std::vector<std::uint8_t> expectedDigest(expectedDigestArray.cbegin(), | ||||
|                                                expectedDigestArray.cend()); | ||||
| 
 | ||||
|       if (digest != expectedDigest) | ||||
|       { | ||||
|          QByteArray calculatedDigest(reinterpret_cast<char*>(digest.data()), | ||||
|                                      digest.size()); | ||||
| 
 | ||||
|          logger_->error("Digest mismatch: {} != {}", | ||||
|                         calculatedDigest.toBase64().toStdString(), | ||||
|                         contentMd5->second); | ||||
| 
 | ||||
|          Q_EMIT request->RequestComplete( | ||||
|             request::DownloadRequest::CompleteReason::DigestError); | ||||
| 
 | ||||
|          return; | ||||
|       } | ||||
|    } | ||||
| 
 | ||||
|    logger_->info("Download complete: {}", request->url()); | ||||
|    Q_EMIT request->RequestComplete( | ||||
|       request::DownloadRequest::CompleteReason::OK); | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<DownloadManager> DownloadManager::Instance() | ||||
|  |  | |||
|  | @ -157,12 +157,19 @@ PlacefileManager::PlacefileManager() : p(std::make_unique<Impl>(this)) | |||
|    boost::asio::post(p->threadPool_, | ||||
|                      [this]() | ||||
|                      { | ||||
|                         p->InitializePlacefileSettings(); | ||||
|                         try | ||||
|                         { | ||||
|                            p->InitializePlacefileSettings(); | ||||
| 
 | ||||
|                         // Read placefile settings on startup
 | ||||
|                         main::Application::WaitForInitialization(); | ||||
|                         p->ReadPlacefileSettings(); | ||||
|                         Q_EMIT PlacefilesInitialized(); | ||||
|                            // Read placefile settings on startup
 | ||||
|                            main::Application::WaitForInitialization(); | ||||
|                            p->ReadPlacefileSettings(); | ||||
|                            Q_EMIT PlacefilesInitialized(); | ||||
|                         } | ||||
|                         catch (const std::exception& ex) | ||||
|                         { | ||||
|                            logger_->error(ex.what()); | ||||
|                         } | ||||
|                      }); | ||||
| } | ||||
| 
 | ||||
|  | @ -678,7 +685,7 @@ void PlacefileManager::Impl::PlacefileRecord::ScheduleRefresh() | |||
|          } | ||||
|          else | ||||
|          { | ||||
|             Update(); | ||||
|             UpdateAsync(); | ||||
|          } | ||||
|       }); | ||||
| } | ||||
|  | @ -691,7 +698,18 @@ void PlacefileManager::Impl::PlacefileRecord::CancelRefresh() | |||
| 
 | ||||
| void PlacefileManager::Impl::PlacefileRecord::UpdateAsync() | ||||
| { | ||||
|    boost::asio::post(threadPool_, [this]() { Update(); }); | ||||
|    boost::asio::post(threadPool_, | ||||
|                      [this]() | ||||
|                      { | ||||
|                         try | ||||
|                         { | ||||
|                            Update(); | ||||
|                         } | ||||
|                         catch (const std::exception& ex) | ||||
|                         { | ||||
|                            logger_->error(ex.what()); | ||||
|                         } | ||||
|                      }); | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<PlacefileManager> PlacefileManager::Instance() | ||||
|  |  | |||
|  | @ -196,6 +196,7 @@ public: | |||
|                       std::shared_ptr<ProviderManager> providerManager, | ||||
|                       bool                             enabled); | ||||
|    void RefreshData(std::shared_ptr<ProviderManager> providerManager); | ||||
|    void RefreshDataSync(std::shared_ptr<ProviderManager> providerManager); | ||||
| 
 | ||||
|    std::tuple<std::shared_ptr<types::RadarProductRecord>, | ||||
|               std::chrono::system_clock::time_point> | ||||
|  | @ -225,6 +226,8 @@ public: | |||
|    void PopulateLevel3ProductTimes(const std::string& product, | ||||
|                                    std::chrono::system_clock::time_point time); | ||||
| 
 | ||||
|    void UpdateAvailableProductsSync(); | ||||
| 
 | ||||
|    static void | ||||
|    PopulateProductTimes(std::shared_ptr<ProviderManager> providerManager, | ||||
|                         RadarProductRecordMap&           productRecordMap, | ||||
|  | @ -576,16 +579,23 @@ void RadarProductManager::EnableRefresh(common::RadarProductGroup group, | |||
|          p->threadPool_, | ||||
|          [=, this]() | ||||
|          { | ||||
|             providerManager->provider_->RequestAvailableProducts(); | ||||
|             auto availableProducts = | ||||
|                providerManager->provider_->GetAvailableProducts(); | ||||
| 
 | ||||
|             if (std::find(std::execution::par_unseq, | ||||
|                           availableProducts.cbegin(), | ||||
|                           availableProducts.cend(), | ||||
|                           product) != availableProducts.cend()) | ||||
|             try | ||||
|             { | ||||
|                p->EnableRefresh(uuid, providerManager, enabled); | ||||
|                providerManager->provider_->RequestAvailableProducts(); | ||||
|                auto availableProducts = | ||||
|                   providerManager->provider_->GetAvailableProducts(); | ||||
| 
 | ||||
|                if (std::find(std::execution::par_unseq, | ||||
|                              availableProducts.cbegin(), | ||||
|                              availableProducts.cend(), | ||||
|                              product) != availableProducts.cend()) | ||||
|                { | ||||
|                   p->EnableRefresh(uuid, providerManager, enabled); | ||||
|                } | ||||
|             } | ||||
|             catch (const std::exception& ex) | ||||
|             { | ||||
|                logger_->error(ex.what()); | ||||
|             } | ||||
|          }); | ||||
|    } | ||||
|  | @ -664,95 +674,102 @@ void RadarProductManagerImpl::RefreshData( | |||
|       providerManager->refreshTimer_.cancel(); | ||||
|    } | ||||
| 
 | ||||
|    boost::asio::post( | ||||
|       threadPool_, | ||||
|       [=, this]() | ||||
|    boost::asio::post(threadPool_, | ||||
|                      [=, this]() | ||||
|                      { | ||||
|                         try | ||||
|                         { | ||||
|                            RefreshDataSync(providerManager); | ||||
|                         } | ||||
|                         catch (const std::exception& ex) | ||||
|                         { | ||||
|                            logger_->error(ex.what()); | ||||
|                         } | ||||
|                      }); | ||||
| } | ||||
| 
 | ||||
| void RadarProductManagerImpl::RefreshDataSync( | ||||
|    std::shared_ptr<ProviderManager> providerManager) | ||||
| { | ||||
|    using namespace std::chrono_literals; | ||||
| 
 | ||||
|    auto [newObjects, totalObjects] = providerManager->provider_->Refresh(); | ||||
| 
 | ||||
|    std::chrono::milliseconds interval = kFastRetryInterval_; | ||||
| 
 | ||||
|    if (totalObjects > 0) | ||||
|    { | ||||
|       std::string key = providerManager->provider_->FindLatestKey(); | ||||
|       auto latestTime = providerManager->provider_->GetTimePointByKey(key); | ||||
| 
 | ||||
|       auto updatePeriod      = providerManager->provider_->update_period(); | ||||
|       auto lastModified      = providerManager->provider_->last_modified(); | ||||
|       auto sinceLastModified = std::chrono::system_clock::now() - lastModified; | ||||
| 
 | ||||
|       // For the default interval, assume products are updated at a
 | ||||
|       // constant rate. Expect the next product at a time based on the
 | ||||
|       // previous two.
 | ||||
|       interval = std::chrono::duration_cast<std::chrono::milliseconds>( | ||||
|          updatePeriod - sinceLastModified); | ||||
| 
 | ||||
|       if (updatePeriod > 0s && sinceLastModified > updatePeriod * 5) | ||||
|       { | ||||
|          using namespace std::chrono_literals; | ||||
|          // If it has been at least 5 update periods since the file has
 | ||||
|          // been last modified, slow the retry period
 | ||||
|          interval = kSlowRetryInterval_; | ||||
|       } | ||||
|       else if (interval < std::chrono::milliseconds {kFastRetryInterval_}) | ||||
|       { | ||||
|          // The interval should be no quicker than the fast retry interval
 | ||||
|          interval = kFastRetryInterval_; | ||||
|       } | ||||
| 
 | ||||
|          auto [newObjects, totalObjects] = | ||||
|             providerManager->provider_->Refresh(); | ||||
|       if (newObjects > 0) | ||||
|       { | ||||
|          Q_EMIT providerManager->NewDataAvailable( | ||||
|             providerManager->group_, providerManager->product_, latestTime); | ||||
|       } | ||||
|    } | ||||
|    else if (providerManager->refreshEnabled_) | ||||
|    { | ||||
|       logger_->info("[{}] No data found", providerManager->name()); | ||||
| 
 | ||||
|          std::chrono::milliseconds interval = kFastRetryInterval_; | ||||
|       // If no data is found, retry at the slow retry interval
 | ||||
|       interval = kSlowRetryInterval_; | ||||
|    } | ||||
| 
 | ||||
|          if (totalObjects > 0) | ||||
|          { | ||||
|             std::string key = providerManager->provider_->FindLatestKey(); | ||||
|             auto        latestTime = | ||||
|                providerManager->provider_->GetTimePointByKey(key); | ||||
|    if (providerManager->refreshEnabled_) | ||||
|    { | ||||
|       std::unique_lock lock(providerManager->refreshTimerMutex_); | ||||
| 
 | ||||
|             auto updatePeriod = providerManager->provider_->update_period(); | ||||
|             auto lastModified = providerManager->provider_->last_modified(); | ||||
|             auto sinceLastModified = | ||||
|                std::chrono::system_clock::now() - lastModified; | ||||
|       logger_->debug( | ||||
|          "[{}] Scheduled refresh in {:%M:%S}", | ||||
|          providerManager->name(), | ||||
|          std::chrono::duration_cast<std::chrono::seconds>(interval)); | ||||
| 
 | ||||
|             // For the default interval, assume products are updated at a
 | ||||
|             // constant rate. Expect the next product at a time based on the
 | ||||
|             // previous two.
 | ||||
|             interval = std::chrono::duration_cast<std::chrono::milliseconds>( | ||||
|                updatePeriod - sinceLastModified); | ||||
| 
 | ||||
|             if (updatePeriod > 0s && sinceLastModified > updatePeriod * 5) | ||||
|       { | ||||
|          providerManager->refreshTimer_.expires_after(interval); | ||||
|          providerManager->refreshTimer_.async_wait( | ||||
|             [=, this](const boost::system::error_code& e) | ||||
|             { | ||||
|                // If it has been at least 5 update periods since the file has
 | ||||
|                // been last modified, slow the retry period
 | ||||
|                interval = kSlowRetryInterval_; | ||||
|             } | ||||
|             else if (interval < std::chrono::milliseconds {kFastRetryInterval_}) | ||||
|             { | ||||
|                // The interval should be no quicker than the fast retry interval
 | ||||
|                interval = kFastRetryInterval_; | ||||
|             } | ||||
| 
 | ||||
|             if (newObjects > 0) | ||||
|             { | ||||
|                Q_EMIT providerManager->NewDataAvailable( | ||||
|                   providerManager->group_, | ||||
|                   providerManager->product_, | ||||
|                   latestTime); | ||||
|             } | ||||
|          } | ||||
|          else if (providerManager->refreshEnabled_) | ||||
|          { | ||||
|             logger_->info("[{}] No data found", providerManager->name()); | ||||
| 
 | ||||
|             // If no data is found, retry at the slow retry interval
 | ||||
|             interval = kSlowRetryInterval_; | ||||
|          } | ||||
| 
 | ||||
|          if (providerManager->refreshEnabled_) | ||||
|          { | ||||
|             std::unique_lock lock(providerManager->refreshTimerMutex_); | ||||
| 
 | ||||
|             logger_->debug( | ||||
|                "[{}] Scheduled refresh in {:%M:%S}", | ||||
|                providerManager->name(), | ||||
|                std::chrono::duration_cast<std::chrono::seconds>(interval)); | ||||
| 
 | ||||
|             { | ||||
|                providerManager->refreshTimer_.expires_after(interval); | ||||
|                providerManager->refreshTimer_.async_wait( | ||||
|                   [=, this](const boost::system::error_code& e) | ||||
|                   { | ||||
|                      if (e == boost::system::errc::success) | ||||
|                      { | ||||
|                         RefreshData(providerManager); | ||||
|                      } | ||||
|                      else if (e == boost::asio::error::operation_aborted) | ||||
|                      { | ||||
|                         logger_->debug("[{}] Data refresh timer cancelled", | ||||
|                                        providerManager->name()); | ||||
|                      } | ||||
|                      else | ||||
|                      { | ||||
|                         logger_->warn("[{}] Data refresh timer error: {}", | ||||
|                                       providerManager->name(), | ||||
|                                       e.message()); | ||||
|                      } | ||||
|                   }); | ||||
|             } | ||||
|          } | ||||
|       }); | ||||
|                if (e == boost::system::errc::success) | ||||
|                { | ||||
|                   RefreshData(providerManager); | ||||
|                } | ||||
|                else if (e == boost::asio::error::operation_aborted) | ||||
|                { | ||||
|                   logger_->debug("[{}] Data refresh timer cancelled", | ||||
|                                  providerManager->name()); | ||||
|                } | ||||
|                else | ||||
|                { | ||||
|                   logger_->warn("[{}] Data refresh timer error: {}", | ||||
|                                 providerManager->name(), | ||||
|                                 e.message()); | ||||
|                } | ||||
|             }); | ||||
|       } | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| std::set<std::chrono::system_clock::time_point> | ||||
|  | @ -1009,7 +1026,16 @@ void RadarProductManagerImpl::LoadNexradFileAsync( | |||
| { | ||||
|    boost::asio::post(threadPool_, | ||||
|                      [=, &mutex]() | ||||
|                      { LoadNexradFile(load, request, mutex, time); }); | ||||
|                      { | ||||
|                         try | ||||
|                         { | ||||
|                            LoadNexradFile(load, request, mutex, time); | ||||
|                         } | ||||
|                         catch (const std::exception& ex) | ||||
|                         { | ||||
|                            logger_->error(ex.what()); | ||||
|                         } | ||||
|                      }); | ||||
| } | ||||
| 
 | ||||
| void RadarProductManagerImpl::LoadNexradFile( | ||||
|  | @ -1441,64 +1467,73 @@ void RadarProductManager::UpdateAvailableProducts() | |||
| 
 | ||||
|    logger_->debug("UpdateAvailableProducts()"); | ||||
| 
 | ||||
|    boost::asio::post( | ||||
|       p->threadPool_, | ||||
|       [this]() | ||||
|    boost::asio::post(p->threadPool_, | ||||
|                      [this]() | ||||
|                      { | ||||
|                         try | ||||
|                         { | ||||
|                            p->UpdateAvailableProductsSync(); | ||||
|                         } | ||||
|                         catch (const std::exception& ex) | ||||
|                         { | ||||
|                            logger_->error(ex.what()); | ||||
|                         } | ||||
|                      }); | ||||
| } | ||||
| 
 | ||||
| void RadarProductManagerImpl::UpdateAvailableProductsSync() | ||||
| { | ||||
|    auto level3ProviderManager = | ||||
|       GetLevel3ProviderManager(kDefaultLevel3Product_); | ||||
|    level3ProviderManager->provider_->RequestAvailableProducts(); | ||||
|    auto updatedAwipsIdList = | ||||
|       level3ProviderManager->provider_->GetAvailableProducts(); | ||||
| 
 | ||||
|    std::unique_lock lock {availableCategoryMutex_}; | ||||
| 
 | ||||
|    for (common::Level3ProductCategory category : | ||||
|         common::Level3ProductCategoryIterator()) | ||||
|    { | ||||
|       const auto& products = common::GetLevel3ProductsByCategory(category); | ||||
| 
 | ||||
|       std::unordered_map<std::string, std::vector<std::string>> | ||||
|          availableProducts; | ||||
| 
 | ||||
|       for (const auto& product : products) | ||||
|       { | ||||
|          auto level3ProviderManager = | ||||
|             p->GetLevel3ProviderManager(kDefaultLevel3Product_); | ||||
|          level3ProviderManager->provider_->RequestAvailableProducts(); | ||||
|          auto updatedAwipsIdList = | ||||
|             level3ProviderManager->provider_->GetAvailableProducts(); | ||||
|          const auto& awipsIds = common::GetLevel3AwipsIdsByProduct(product); | ||||
| 
 | ||||
|          std::unique_lock lock {p->availableCategoryMutex_}; | ||||
|          std::vector<std::string> availableAwipsIds; | ||||
| 
 | ||||
|          for (common::Level3ProductCategory category : | ||||
|               common::Level3ProductCategoryIterator()) | ||||
|          for (const auto& awipsId : awipsIds) | ||||
|          { | ||||
|             const auto& products = | ||||
|                common::GetLevel3ProductsByCategory(category); | ||||
| 
 | ||||
|             std::unordered_map<std::string, std::vector<std::string>> | ||||
|                availableProducts; | ||||
| 
 | ||||
|             for (const auto& product : products) | ||||
|             if (std::find(updatedAwipsIdList.cbegin(), | ||||
|                           updatedAwipsIdList.cend(), | ||||
|                           awipsId) != updatedAwipsIdList.cend()) | ||||
|             { | ||||
|                const auto& awipsIds = | ||||
|                   common::GetLevel3AwipsIdsByProduct(product); | ||||
| 
 | ||||
|                std::vector<std::string> availableAwipsIds; | ||||
| 
 | ||||
|                for (const auto& awipsId : awipsIds) | ||||
|                { | ||||
|                   if (std::find(updatedAwipsIdList.cbegin(), | ||||
|                                 updatedAwipsIdList.cend(), | ||||
|                                 awipsId) != updatedAwipsIdList.cend()) | ||||
|                   { | ||||
|                      availableAwipsIds.push_back(awipsId); | ||||
|                   } | ||||
|                } | ||||
| 
 | ||||
|                if (!availableAwipsIds.empty()) | ||||
|                { | ||||
|                   availableProducts.insert_or_assign( | ||||
|                      product, std::move(availableAwipsIds)); | ||||
|                } | ||||
|             } | ||||
| 
 | ||||
|             if (!availableProducts.empty()) | ||||
|             { | ||||
|                p->availableCategoryMap_.insert_or_assign( | ||||
|                   category, std::move(availableProducts)); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                p->availableCategoryMap_.erase(category); | ||||
|                availableAwipsIds.push_back(awipsId); | ||||
|             } | ||||
|          } | ||||
| 
 | ||||
|          Q_EMIT Level3ProductsChanged(); | ||||
|       }); | ||||
|          if (!availableAwipsIds.empty()) | ||||
|          { | ||||
|             availableProducts.insert_or_assign(product, | ||||
|                                                std::move(availableAwipsIds)); | ||||
|          } | ||||
|       } | ||||
| 
 | ||||
|       if (!availableProducts.empty()) | ||||
|       { | ||||
|          availableCategoryMap_.insert_or_assign(category, | ||||
|                                                 std::move(availableProducts)); | ||||
|       } | ||||
|       else | ||||
|       { | ||||
|          availableCategoryMap_.erase(category); | ||||
|       } | ||||
|    } | ||||
| 
 | ||||
|    Q_EMIT self_->Level3ProductsChanged(); | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<RadarProductManager> | ||||
|  |  | |||
|  | @ -50,9 +50,16 @@ public: | |||
|       boost::asio::post(threadPool_, | ||||
|                         [this]() | ||||
|                         { | ||||
|                            main::Application::WaitForInitialization(); | ||||
|                            logger_->debug("Start Refresh"); | ||||
|                            Refresh(); | ||||
|                            try | ||||
|                            { | ||||
|                               main::Application::WaitForInitialization(); | ||||
|                               logger_->debug("Start Refresh"); | ||||
|                               Refresh(); | ||||
|                            } | ||||
|                            catch (const std::exception& ex) | ||||
|                            { | ||||
|                               logger_->error(ex.what()); | ||||
|                            } | ||||
|                         }); | ||||
|    } | ||||
| 
 | ||||
|  | @ -70,6 +77,7 @@ public: | |||
|    } | ||||
| 
 | ||||
|    void HandleMessage(std::shared_ptr<awips::TextProductMessage> message); | ||||
|    void RefreshAsync(); | ||||
|    void Refresh(); | ||||
| 
 | ||||
|    boost::asio::thread_pool threadPool_ {1u}; | ||||
|  | @ -131,20 +139,27 @@ void TextEventManager::LoadFile(const std::string& filename) | |||
|    boost::asio::post(p->threadPool_, | ||||
|                      [=, this]() | ||||
|                      { | ||||
|                         awips::TextProductFile file; | ||||
| 
 | ||||
|                         // Load file
 | ||||
|                         bool fileLoaded = file.LoadFile(filename); | ||||
|                         if (!fileLoaded) | ||||
|                         try | ||||
|                         { | ||||
|                            return; | ||||
|                            awips::TextProductFile file; | ||||
| 
 | ||||
|                            // Load file
 | ||||
|                            bool fileLoaded = file.LoadFile(filename); | ||||
|                            if (!fileLoaded) | ||||
|                            { | ||||
|                               return; | ||||
|                            } | ||||
| 
 | ||||
|                            // Process messages
 | ||||
|                            auto messages = file.messages(); | ||||
|                            for (auto& message : messages) | ||||
|                            { | ||||
|                               p->HandleMessage(message); | ||||
|                            } | ||||
|                         } | ||||
| 
 | ||||
|                         // Process messages
 | ||||
|                         auto messages = file.messages(); | ||||
|                         for (auto& message : messages) | ||||
|                         catch (const std::exception& ex) | ||||
|                         { | ||||
|                            p->HandleMessage(message); | ||||
|                            logger_->error(ex.what()); | ||||
|                         } | ||||
|                      }); | ||||
| } | ||||
|  | @ -212,6 +227,22 @@ void TextEventManager::Impl::HandleMessage( | |||
|    } | ||||
| } | ||||
| 
 | ||||
| void TextEventManager::Impl::RefreshAsync() | ||||
| { | ||||
|    boost::asio::post(threadPool_, | ||||
|                      [this]() | ||||
|                      { | ||||
|                         try | ||||
|                         { | ||||
|                            Refresh(); | ||||
|                         } | ||||
|                         catch (const std::exception& ex) | ||||
|                         { | ||||
|                            logger_->error(ex.what()); | ||||
|                         } | ||||
|                      }); | ||||
| } | ||||
| 
 | ||||
| void TextEventManager::Impl::Refresh() | ||||
| { | ||||
|    logger_->trace("Refresh"); | ||||
|  | @ -257,7 +288,7 @@ void TextEventManager::Impl::Refresh() | |||
|          } | ||||
|          else | ||||
|          { | ||||
|             Refresh(); | ||||
|             RefreshAsync(); | ||||
|          } | ||||
|       }); | ||||
| } | ||||
|  |  | |||
|  | @ -69,11 +69,13 @@ public: | |||
| 
 | ||||
|    void Pause(); | ||||
|    void Play(); | ||||
|    void PlaySync(); | ||||
|    void | ||||
|    SelectTimeAsync(std::chrono::system_clock::time_point selectedTime = {}); | ||||
|    std::pair<bool, bool> | ||||
|         SelectTime(std::chrono::system_clock::time_point selectedTime = {}); | ||||
|    void StepAsync(Direction direction); | ||||
|    void Step(Direction direction); | ||||
| 
 | ||||
|    boost::asio::thread_pool playThreadPool_ {1}; | ||||
|    boost::asio::thread_pool selectThreadPool_ {1}; | ||||
|  | @ -405,8 +407,6 @@ void TimelineManager::Impl::UpdateCacheLimit( | |||
| 
 | ||||
| void TimelineManager::Impl::Play() | ||||
| { | ||||
|    using namespace std::chrono_literals; | ||||
| 
 | ||||
|    if (animationState_ != types::AnimationState::Play) | ||||
|    { | ||||
|       animationState_ = types::AnimationState::Play; | ||||
|  | @ -418,92 +418,105 @@ void TimelineManager::Impl::Play() | |||
|       animationTimer_.cancel(); | ||||
|    } | ||||
| 
 | ||||
|    boost::asio::post( | ||||
|       playThreadPool_, | ||||
|       [this]() | ||||
|    boost::asio::post(playThreadPool_, | ||||
|                      [this]() | ||||
|                      { | ||||
|                         try | ||||
|                         { | ||||
|                            PlaySync(); | ||||
|                         } | ||||
|                         catch (const std::exception& ex) | ||||
|                         { | ||||
|                            logger_->error(ex.what()); | ||||
|                         } | ||||
|                      }); | ||||
| } | ||||
| 
 | ||||
| void TimelineManager::Impl::PlaySync() | ||||
| { | ||||
|    using namespace std::chrono_literals; | ||||
| 
 | ||||
|    // Take a lock for time selection
 | ||||
|    std::unique_lock lock {selectTimeMutex_}; | ||||
| 
 | ||||
|    auto [startTime, endTime] = GetLoopStartAndEndTimes(); | ||||
|    std::chrono::system_clock::time_point currentTime = selectedTime_; | ||||
|    std::chrono::system_clock::time_point newTime; | ||||
| 
 | ||||
|    if (currentTime < startTime || currentTime >= endTime) | ||||
|    { | ||||
|       // If the currently selected time is out of the loop, select the
 | ||||
|       // start time
 | ||||
|       newTime = startTime; | ||||
|    } | ||||
|    else | ||||
|    { | ||||
|       // If the currently selected time is in the loop, increment
 | ||||
|       newTime = currentTime + 1min; | ||||
|    } | ||||
| 
 | ||||
|    // Unlock prior to selecting time
 | ||||
|    lock.unlock(); | ||||
| 
 | ||||
|    // Lock radar sweep monitor
 | ||||
|    std::unique_lock radarSweepMonitorLock {radarSweepMonitorMutex_}; | ||||
| 
 | ||||
|    // Reset radar sweep monitor in preparation for update
 | ||||
|    RadarSweepMonitorReset(); | ||||
| 
 | ||||
|    // Select the time
 | ||||
|    auto selectTimeStart = std::chrono::steady_clock::now(); | ||||
|    auto [volumeTimeUpdated, selectedTimeUpdated] = SelectTime(newTime); | ||||
|    auto selectTimeEnd = std::chrono::steady_clock::now(); | ||||
|    auto elapsedTime   = selectTimeEnd - selectTimeStart; | ||||
| 
 | ||||
|    if (volumeTimeUpdated) | ||||
|    { | ||||
|       // Wait for radar sweeps to update
 | ||||
|       RadarSweepMonitorWait(radarSweepMonitorLock); | ||||
|    } | ||||
|    else | ||||
|    { | ||||
|       // Disable radar sweep monitor
 | ||||
|       RadarSweepMonitorDisable(); | ||||
|    } | ||||
| 
 | ||||
|    // Calculate the interval until the next update, prior to selecting
 | ||||
|    std::chrono::milliseconds interval; | ||||
|    if (newTime != endTime) | ||||
|    { | ||||
|       // Determine repeat interval (speed of 1.0 is 1 minute per second)
 | ||||
|       interval = std::chrono::duration_cast<std::chrono::milliseconds>( | ||||
|          std::chrono::milliseconds(std::lroundl(1000.0 / loopSpeed_)) - | ||||
|          elapsedTime); | ||||
|    } | ||||
|    else | ||||
|    { | ||||
|       // Pause at the end of the loop
 | ||||
|       interval = std::chrono::duration_cast<std::chrono::milliseconds>( | ||||
|          loopDelay_ - elapsedTime); | ||||
|    } | ||||
| 
 | ||||
|    std::unique_lock animationTimerLock {animationTimerMutex_}; | ||||
|    animationTimer_.expires_after(interval); | ||||
|    animationTimer_.async_wait( | ||||
|       [this](const boost::system::error_code& e) | ||||
|       { | ||||
|          // Take a lock for time selection
 | ||||
|          std::unique_lock lock {selectTimeMutex_}; | ||||
| 
 | ||||
|          auto [startTime, endTime] = GetLoopStartAndEndTimes(); | ||||
|          std::chrono::system_clock::time_point currentTime = selectedTime_; | ||||
|          std::chrono::system_clock::time_point newTime; | ||||
| 
 | ||||
|          if (currentTime < startTime || currentTime >= endTime) | ||||
|          if (e == boost::system::errc::success) | ||||
|          { | ||||
|             // If the currently selected time is out of the loop, select the
 | ||||
|             // start time
 | ||||
|             newTime = startTime; | ||||
|          } | ||||
|          else | ||||
|          { | ||||
|             // If the currently selected time is in the loop, increment
 | ||||
|             newTime = currentTime + 1min; | ||||
|          } | ||||
| 
 | ||||
|          // Unlock prior to selecting time
 | ||||
|          lock.unlock(); | ||||
| 
 | ||||
|          // Lock radar sweep monitor
 | ||||
|          std::unique_lock radarSweepMonitorLock {radarSweepMonitorMutex_}; | ||||
| 
 | ||||
|          // Reset radar sweep monitor in preparation for update
 | ||||
|          RadarSweepMonitorReset(); | ||||
| 
 | ||||
|          // Select the time
 | ||||
|          auto selectTimeStart = std::chrono::steady_clock::now(); | ||||
|          auto [volumeTimeUpdated, selectedTimeUpdated] = SelectTime(newTime); | ||||
|          auto selectTimeEnd = std::chrono::steady_clock::now(); | ||||
|          auto elapsedTime   = selectTimeEnd - selectTimeStart; | ||||
| 
 | ||||
|          if (volumeTimeUpdated) | ||||
|          { | ||||
|             // Wait for radar sweeps to update
 | ||||
|             RadarSweepMonitorWait(radarSweepMonitorLock); | ||||
|          } | ||||
|          else | ||||
|          { | ||||
|             // Disable radar sweep monitor
 | ||||
|             RadarSweepMonitorDisable(); | ||||
|          } | ||||
| 
 | ||||
|          // Calculate the interval until the next update, prior to selecting
 | ||||
|          std::chrono::milliseconds interval; | ||||
|          if (newTime != endTime) | ||||
|          { | ||||
|             // Determine repeat interval (speed of 1.0 is 1 minute per second)
 | ||||
|             interval = std::chrono::duration_cast<std::chrono::milliseconds>( | ||||
|                std::chrono::milliseconds(std::lroundl(1000.0 / loopSpeed_)) - | ||||
|                elapsedTime); | ||||
|          } | ||||
|          else | ||||
|          { | ||||
|             // Pause at the end of the loop
 | ||||
|             interval = std::chrono::duration_cast<std::chrono::milliseconds>( | ||||
|                loopDelay_ - elapsedTime); | ||||
|          } | ||||
| 
 | ||||
|          std::unique_lock animationTimerLock {animationTimerMutex_}; | ||||
|          animationTimer_.expires_after(interval); | ||||
|          animationTimer_.async_wait( | ||||
|             [this](const boost::system::error_code& e) | ||||
|             if (animationState_ == types::AnimationState::Play) | ||||
|             { | ||||
|                if (e == boost::system::errc::success) | ||||
|                { | ||||
|                   if (animationState_ == types::AnimationState::Play) | ||||
|                   { | ||||
|                      Play(); | ||||
|                   } | ||||
|                } | ||||
|                else if (e == boost::asio::error::operation_aborted) | ||||
|                { | ||||
|                   logger_->debug("Play timer cancelled"); | ||||
|                } | ||||
|                else | ||||
|                { | ||||
|                   logger_->warn("Play timer error: {}", e.message()); | ||||
|                } | ||||
|             }); | ||||
|                Play(); | ||||
|             } | ||||
|          } | ||||
|          else if (e == boost::asio::error::operation_aborted) | ||||
|          { | ||||
|             logger_->debug("Play timer cancelled"); | ||||
|          } | ||||
|          else | ||||
|          { | ||||
|             logger_->warn("Play timer error: {}", e.message()); | ||||
|          } | ||||
|       }); | ||||
| } | ||||
| 
 | ||||
|  | @ -511,7 +524,17 @@ void TimelineManager::Impl::SelectTimeAsync( | |||
|    std::chrono::system_clock::time_point selectedTime) | ||||
| { | ||||
|    boost::asio::post(selectThreadPool_, | ||||
|                      [=, this]() { SelectTime(selectedTime); }); | ||||
|                      [=, this]() | ||||
|                      { | ||||
|                         try | ||||
|                         { | ||||
|                            SelectTime(selectedTime); | ||||
|                         } | ||||
|                         catch (const std::exception& ex) | ||||
|                         { | ||||
|                            logger_->error(ex.what()); | ||||
|                         } | ||||
|                      }); | ||||
| } | ||||
| 
 | ||||
| std::pair<bool, bool> TimelineManager::Impl::SelectTime( | ||||
|  | @ -597,91 +620,100 @@ std::pair<bool, bool> TimelineManager::Impl::SelectTime( | |||
| 
 | ||||
| void TimelineManager::Impl::StepAsync(Direction direction) | ||||
| { | ||||
|    boost::asio::post( | ||||
|       selectThreadPool_, | ||||
|       [=, this]() | ||||
|    boost::asio::post(selectThreadPool_, | ||||
|                      [=, this]() | ||||
|                      { | ||||
|                         try | ||||
|                         { | ||||
|                            Step(direction); | ||||
|                         } | ||||
|                         catch (const std::exception& ex) | ||||
|                         { | ||||
|                            logger_->error(ex.what()); | ||||
|                         } | ||||
|                      }); | ||||
| } | ||||
| 
 | ||||
| void TimelineManager::Impl::Step(Direction direction) | ||||
| { | ||||
|    // Take a lock for time selection
 | ||||
|    std::unique_lock lock {selectTimeMutex_}; | ||||
| 
 | ||||
|    // Determine time to get active volume times
 | ||||
|    std::chrono::system_clock::time_point queryTime = adjustedTime_; | ||||
|    if (queryTime == std::chrono::system_clock::time_point {}) | ||||
|    { | ||||
|       queryTime = std::chrono::system_clock::now(); | ||||
|    } | ||||
| 
 | ||||
|    // Request active volume times
 | ||||
|    auto radarProductManager = | ||||
|       manager::RadarProductManager::Instance(radarSite_); | ||||
|    auto volumeTimes = radarProductManager->GetActiveVolumeTimes(queryTime); | ||||
| 
 | ||||
|    if (volumeTimes.empty()) | ||||
|    { | ||||
|       logger_->debug("No products to step through"); | ||||
|       return; | ||||
|    } | ||||
| 
 | ||||
|    // Dynamically update maximum cached volume scans
 | ||||
|    UpdateCacheLimit(radarProductManager, volumeTimes); | ||||
| 
 | ||||
|    std::set<std::chrono::system_clock::time_point>::const_iterator it; | ||||
| 
 | ||||
|    if (adjustedTime_ == std::chrono::system_clock::time_point {}) | ||||
|    { | ||||
|       // If the adjusted time is live, get the last element in the set
 | ||||
|       it = std::prev(volumeTimes.cend()); | ||||
|    } | ||||
|    else | ||||
|    { | ||||
|       // Get the current element in the set
 | ||||
|       it = scwx::util::GetBoundedElementIterator(volumeTimes, adjustedTime_); | ||||
|    } | ||||
| 
 | ||||
|    if (it == volumeTimes.cend()) | ||||
|    { | ||||
|       // Should not get here, but protect against an error
 | ||||
|       logger_->error("No suitable volume time found"); | ||||
|       return; | ||||
|    } | ||||
| 
 | ||||
|    if (direction == Direction::Back) | ||||
|    { | ||||
|       // Only if we aren't at the beginning of the volume times set
 | ||||
|       if (it != volumeTimes.cbegin()) | ||||
|       { | ||||
|          // Take a lock for time selection
 | ||||
|          std::unique_lock lock {selectTimeMutex_}; | ||||
|          // Select the previous time
 | ||||
|          adjustedTime_ = *(--it); | ||||
|          selectedTime_ = adjustedTime_; | ||||
| 
 | ||||
|          // Determine time to get active volume times
 | ||||
|          std::chrono::system_clock::time_point queryTime = adjustedTime_; | ||||
|          if (queryTime == std::chrono::system_clock::time_point {}) | ||||
|          { | ||||
|             queryTime = std::chrono::system_clock::now(); | ||||
|          } | ||||
|          logger_->debug("Volume time updated: {}", | ||||
|                         scwx::util::TimeString(adjustedTime_)); | ||||
| 
 | ||||
|          // Request active volume times
 | ||||
|          auto radarProductManager = | ||||
|             manager::RadarProductManager::Instance(radarSite_); | ||||
|          auto volumeTimes = | ||||
|             radarProductManager->GetActiveVolumeTimes(queryTime); | ||||
|          Q_EMIT self_->LiveStateUpdated(false); | ||||
|          Q_EMIT self_->VolumeTimeUpdated(adjustedTime_); | ||||
|          Q_EMIT self_->SelectedTimeUpdated(adjustedTime_); | ||||
|       } | ||||
|    } | ||||
|    else | ||||
|    { | ||||
|       // Only if we aren't at the end of the volume times set
 | ||||
|       if (it != std::prev(volumeTimes.cend())) | ||||
|       { | ||||
|          // Select the next time
 | ||||
|          adjustedTime_ = *(++it); | ||||
|          selectedTime_ = adjustedTime_; | ||||
| 
 | ||||
|          if (volumeTimes.empty()) | ||||
|          { | ||||
|             logger_->debug("No products to step through"); | ||||
|             return; | ||||
|          } | ||||
|          logger_->debug("Volume time updated: {}", | ||||
|                         scwx::util::TimeString(adjustedTime_)); | ||||
| 
 | ||||
|          // Dynamically update maximum cached volume scans
 | ||||
|          UpdateCacheLimit(radarProductManager, volumeTimes); | ||||
| 
 | ||||
|          std::set<std::chrono::system_clock::time_point>::const_iterator it; | ||||
| 
 | ||||
|          if (adjustedTime_ == std::chrono::system_clock::time_point {}) | ||||
|          { | ||||
|             // If the adjusted time is live, get the last element in the set
 | ||||
|             it = std::prev(volumeTimes.cend()); | ||||
|          } | ||||
|          else | ||||
|          { | ||||
|             // Get the current element in the set
 | ||||
|             it = scwx::util::GetBoundedElementIterator(volumeTimes, | ||||
|                                                        adjustedTime_); | ||||
|          } | ||||
| 
 | ||||
|          if (it == volumeTimes.cend()) | ||||
|          { | ||||
|             // Should not get here, but protect against an error
 | ||||
|             logger_->error("No suitable volume time found"); | ||||
|             return; | ||||
|          } | ||||
| 
 | ||||
|          if (direction == Direction::Back) | ||||
|          { | ||||
|             // Only if we aren't at the beginning of the volume times set
 | ||||
|             if (it != volumeTimes.cbegin()) | ||||
|             { | ||||
|                // Select the previous time
 | ||||
|                adjustedTime_ = *(--it); | ||||
|                selectedTime_ = adjustedTime_; | ||||
| 
 | ||||
|                logger_->debug("Volume time updated: {}", | ||||
|                               scwx::util::TimeString(adjustedTime_)); | ||||
| 
 | ||||
|                Q_EMIT self_->LiveStateUpdated(false); | ||||
|                Q_EMIT self_->VolumeTimeUpdated(adjustedTime_); | ||||
|                Q_EMIT self_->SelectedTimeUpdated(adjustedTime_); | ||||
|             } | ||||
|          } | ||||
|          else | ||||
|          { | ||||
|             // Only if we aren't at the end of the volume times set
 | ||||
|             if (it != std::prev(volumeTimes.cend())) | ||||
|             { | ||||
|                // Select the next time
 | ||||
|                adjustedTime_ = *(++it); | ||||
|                selectedTime_ = adjustedTime_; | ||||
| 
 | ||||
|                logger_->debug("Volume time updated: {}", | ||||
|                               scwx::util::TimeString(adjustedTime_)); | ||||
| 
 | ||||
|                Q_EMIT self_->LiveStateUpdated(false); | ||||
|                Q_EMIT self_->VolumeTimeUpdated(adjustedTime_); | ||||
|                Q_EMIT self_->SelectedTimeUpdated(adjustedTime_); | ||||
|             } | ||||
|          } | ||||
|       }); | ||||
|          Q_EMIT self_->LiveStateUpdated(false); | ||||
|          Q_EMIT self_->VolumeTimeUpdated(adjustedTime_); | ||||
|          Q_EMIT self_->SelectedTimeUpdated(adjustedTime_); | ||||
|       } | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<TimelineManager> TimelineManager::Instance() | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Dan Paulat
						Dan Paulat