diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 3e126487..07945d18 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -560,6 +560,7 @@ void MainWindowImpl::ConfigureMapLayout() vs->setHandleWidth(1); maps_.resize(mapCount); + timelineManager_->SetMapCount(mapCount); auto MoveSplitter = [=, this](int /*pos*/, int /*index*/) { @@ -720,6 +721,23 @@ void MainWindowImpl::ConnectAnimationSignals() map->SetAutoUpdate(isLive); } }); + + for (std::size_t i = 0; i < maps_.size(); i++) + { + connect(maps_[i], + &map::MapWidget::RadarSweepUpdated, + timelineManager_.get(), + [=, this]() { timelineManager_->ReceiveRadarSweepUpdated(i); }); + connect(maps_[i], + &map::MapWidget::RadarSweepNotUpdated, + timelineManager_.get(), + [=, this](types::NoUpdateReason reason) + { timelineManager_->ReceiveRadarSweepNotUpdated(i, reason); }); + connect(maps_[i], + &map::MapWidget::WidgetPainted, + timelineManager_.get(), + [=, this]() { timelineManager_->ReceiveMapWidgetPainted(i); }); + } } void MainWindowImpl::ConnectOtherSignals() diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp index 77225c6f..606a58ca 100644 --- a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -26,6 +27,9 @@ enum class Direction Next }; +// Wait up to 5 seconds for radar sweeps to update +static constexpr std::chrono::seconds kRadarSweepMonitorTimeout_ {5}; + class TimelineManager::Impl { public: @@ -49,11 +53,16 @@ public: std::shared_ptr radarProductManager, const std::set& volumeTimes); + void RadarSweepMonitorDisable(); + void RadarSweepMonitorReset(); + void RadarSweepMonitorWait(std::unique_lock& lock); + void Pause(); void Play(); void SelectTime(std::chrono::system_clock::time_point selectedTime = {}); void Step(Direction direction); + std::size_t mapCount_ {0}; std::string radarSite_ {"?"}; std::string previousRadarSite_ {"?"}; std::chrono::system_clock::time_point pinnedTime_ {}; @@ -63,6 +72,12 @@ public: std::chrono::minutes loopTime_ {30}; double loopSpeed_ {5.0}; + bool radarSweepMonitorActive_ {false}; + std::mutex radarSweepMonitorMutex_ {}; + std::condition_variable radarSweepMonitorCondition_ {}; + std::set radarSweepsUpdated_ {}; + std::set radarSweepsComplete_ {}; + types::AnimationState animationState_ {types::AnimationState::Pause}; boost::asio::steady_timer animationTimer_ {scwx::util::io_context()}; std::mutex animationTimerMutex_ {}; @@ -73,6 +88,11 @@ public: TimelineManager::TimelineManager() : p(std::make_unique(this)) {} TimelineManager::~TimelineManager() = default; +void TimelineManager::SetMapCount(std::size_t mapCount) +{ + p->mapCount_ = mapCount; +} + void TimelineManager::SetRadarSite(const std::string& radarSite) { if (p->radarSite_ == radarSite) @@ -217,6 +237,87 @@ void TimelineManager::AnimationStepEnd() } } +void TimelineManager::Impl::RadarSweepMonitorDisable() +{ + radarSweepMonitorActive_ = false; +} + +void TimelineManager::Impl::RadarSweepMonitorReset() +{ + radarSweepsUpdated_.clear(); + radarSweepsComplete_.clear(); + + radarSweepMonitorActive_ = true; +} + +void TimelineManager::Impl::RadarSweepMonitorWait( + std::unique_lock& lock) +{ + radarSweepMonitorCondition_.wait_for(lock, kRadarSweepMonitorTimeout_); + radarSweepMonitorActive_ = false; +} + +void TimelineManager::ReceiveRadarSweepUpdated(std::size_t mapIndex) +{ + if (!p->radarSweepMonitorActive_) + { + return; + } + + std::unique_lock lock {p->radarSweepMonitorMutex_}; + + // Radar sweep is updated, but still needs painted + p->radarSweepsUpdated_.insert(mapIndex); +} + +void TimelineManager::ReceiveRadarSweepNotUpdated( + std::size_t mapIndex, types::NoUpdateReason /* reason */) +{ + if (!p->radarSweepMonitorActive_) + { + return; + } + + std::unique_lock lock {p->radarSweepMonitorMutex_}; + + // Radar sweep is complete, no painting will occur + p->radarSweepsComplete_.insert(mapIndex); + + // If all sweeps have completed rendering + if (p->radarSweepsComplete_.size() == p->mapCount_) + { + // Notify monitors + p->radarSweepMonitorActive_ = false; + p->radarSweepMonitorCondition_.notify_all(); + } +} + +void TimelineManager::ReceiveMapWidgetPainted(std::size_t mapIndex) +{ + if (!p->radarSweepMonitorActive_) + { + return; + } + + std::unique_lock lock {p->radarSweepMonitorMutex_}; + + // If the radar sweep has been updated + if (p->radarSweepsUpdated_.contains(mapIndex)) + { + // Mark the radar sweep complete + p->radarSweepsUpdated_.erase(mapIndex); + p->radarSweepsComplete_.insert(mapIndex); + + // If all sweeps have completed rendering + if (p->radarSweepsComplete_.size() == p->mapCount_) + { + // Notify monitors + p->radarSweepMonitorActive_ = false; + p->radarSweepMonitorCondition_.notify_all(); + } + } +} + void TimelineManager::Impl::Pause() { // Cancel animation @@ -306,12 +407,7 @@ void TimelineManager::Impl::Play() newTime = currentTime + 1min; } - // Unlock prior to selecting time - lock.unlock(); - - // Select the time - SelectTime(newTime); - + // Calculate the interval until the next update, prior to selecting std::chrono::milliseconds interval; if (newTime != endTime) { @@ -325,9 +421,16 @@ void TimelineManager::Impl::Play() interval = std::chrono::milliseconds(2500); } + animationTimer_.expires_after(interval); + + // Unlock prior to selecting time + lock.unlock(); + + // Select the time + SelectTime(newTime); + std::unique_lock animationTimerLock {animationTimerMutex_}; - animationTimer_.expires_after(interval); animationTimer_.async_wait( [this](const boost::system::error_code& e) { @@ -360,15 +463,30 @@ void TimelineManager::Impl::SelectTime( } else if (selectedTime == std::chrono::system_clock::time_point {}) { - // If a default time point is given, reset to a live view - selectedTime_ = selectedTime; - adjustedTime_ = selectedTime; + scwx::util::async( + [=, this]() + { + // Take a lock for time selection + std::unique_lock lock {selectTimeMutex_}; - logger_->debug("Time updated: Live"); + // If a default time point is given, reset to a live view + selectedTime_ = selectedTime; + adjustedTime_ = selectedTime; - Q_EMIT self_->LiveStateUpdated(true); - Q_EMIT self_->VolumeTimeUpdated(selectedTime); - Q_EMIT self_->SelectedTimeUpdated(selectedTime); + logger_->debug("Time updated: Live"); + + std::unique_lock radarSweepMonitorLock {radarSweepMonitorMutex_}; + + // Reset radar sweep monitor in preparation for update + RadarSweepMonitorReset(); + + Q_EMIT self_->LiveStateUpdated(true); + Q_EMIT self_->VolumeTimeUpdated(selectedTime); + Q_EMIT self_->SelectedTimeUpdated(selectedTime); + + // Wait for radar sweeps to update + RadarSweepMonitorWait(radarSweepMonitorLock); + }); return; } @@ -395,9 +513,15 @@ void TimelineManager::Impl::SelectTime( // The timeline is no longer live Q_EMIT self_->LiveStateUpdated(false); + bool volumeTimeUpdated = false; + + std::unique_lock radarSweepMonitorLock {radarSweepMonitorMutex_}; + + // Reset radar sweep monitor in preparation for update + RadarSweepMonitorReset(); + if (elementPtr != nullptr) { - // If the adjusted time changed, or if a new radar site has been // selected if (adjustedTime_ != *elementPtr || @@ -409,6 +533,7 @@ void TimelineManager::Impl::SelectTime( logger_->debug("Volume time updated: {}", scwx::util::TimeString(adjustedTime_)); + volumeTimeUpdated = true; Q_EMIT self_->VolumeTimeUpdated(adjustedTime_); } } @@ -426,6 +551,16 @@ void TimelineManager::Impl::SelectTime( Q_EMIT self_->SelectedTimeUpdated(selectedTime); previousRadarSite_ = radarSite_; + + if (volumeTimeUpdated) + { + // Wait for radar sweeps to update + RadarSweepMonitorWait(radarSweepMonitorLock); + } + else + { + RadarSweepMonitorDisable(); + } }); } diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp index a29278b9..0751fed1 100644 --- a/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp @@ -24,6 +24,8 @@ public: static std::shared_ptr Instance(); + void SetMapCount(std::size_t mapCount); + public slots: void SetRadarSite(const std::string& radarSite); @@ -39,6 +41,11 @@ public slots: void AnimationStepNext(); void AnimationStepEnd(); + void ReceiveRadarSweepUpdated(std::size_t mapIndex); + void ReceiveRadarSweepNotUpdated(std::size_t mapIndex, + types::NoUpdateReason reason); + void ReceiveMapWidgetPainted(std::size_t mapIndex); + signals: void SelectedTimeUpdated(std::chrono::system_clock::time_point dateTime); void VolumeTimeUpdated(std::chrono::system_clock::time_point dateTime); diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index a91b9c7d..6d0146c0 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -859,6 +859,9 @@ void MapWidget::paintGL() // Render ImGui Frame ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + + // Paint complete + Q_EMIT WidgetPainted(); } void MapWidget::mapChanged(QMapLibreGL::Map::MapChange mapChange) @@ -1010,6 +1013,10 @@ void MapWidgetImpl::RadarProductViewConnect() Q_EMIT widget_->RadarSweepUpdated(); }, Qt::QueuedConnection); + connect(radarProductView.get(), + &view::RadarProductView::SweepNotComputed, + widget_, + &MapWidget::RadarSweepNotUpdated); } } @@ -1027,6 +1034,10 @@ void MapWidgetImpl::RadarProductViewDisconnect() &view::RadarProductView::SweepComputed, this, nullptr); + disconnect(radarProductView.get(), + &view::RadarProductView::SweepNotComputed, + widget_, + nullptr); } } diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp index 51cfb230..eb28dfc6 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.hpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -143,6 +144,8 @@ signals: void MapStyleChanged(const std::string& styleName); void RadarSiteUpdated(std::shared_ptr radarSite); void RadarSweepUpdated(); + void RadarSweepNotUpdated(types::NoUpdateReason reason); + void WidgetPainted(); }; } // namespace map diff --git a/scwx-qt/source/scwx/qt/types/map_types.hpp b/scwx-qt/source/scwx/qt/types/map_types.hpp index d2c804a2..5853a188 100644 --- a/scwx-qt/source/scwx/qt/types/map_types.hpp +++ b/scwx-qt/source/scwx/qt/types/map_types.hpp @@ -21,6 +21,14 @@ enum class MapTime Archive }; +enum class NoUpdateReason +{ + NoChange, + NotLoaded, + InvalidProduct, + InvalidData +}; + std::string GetMapTimeName(MapTime mapTime); } // namespace types diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 9e341ddd..06c912bc 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -402,6 +402,7 @@ void Level2ProductView::ComputeSweep() if (p->dataBlockType_ == wsr88d::rda::DataBlockType::Unknown) { + Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidProduct); return; } @@ -423,8 +424,14 @@ void Level2ProductView::ComputeSweep() SelectTime(foundTime); } - if (radarData == nullptr || radarData == p->elevationScan_) + if (radarData == nullptr) { + Q_EMIT SweepNotComputed(types::NoUpdateReason::NotLoaded); + return; + } + if (radarData == p->elevationScan_) + { + Q_EMIT SweepNotComputed(types::NoUpdateReason::NoChange); return; } @@ -443,6 +450,7 @@ void Level2ProductView::ComputeSweep() { logger_->warn("No moment data for {}", common::GetLevel2Name(p->product_)); + Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidData); return; } diff --git a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp index ad87c7c4..9ec04d0b 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp @@ -133,6 +133,7 @@ void Level3RadialView::ComputeSweep() if (message == nullptr) { logger_->debug("Level 3 data not found"); + Q_EMIT SweepNotComputed(types::NoUpdateReason::NotLoaded); return; } @@ -142,11 +143,13 @@ void Level3RadialView::ComputeSweep() if (gpm == nullptr) { logger_->warn("Graphic Product Message not found"); + Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidData); return; } else if (gpm == graphic_product_message()) { // Skip if this is the message we previously processed + Q_EMIT SweepNotComputed(types::NoUpdateReason::NoChange); return; } set_graphic_product_message(gpm); @@ -160,6 +163,7 @@ void Level3RadialView::ComputeSweep() if (descriptionBlock == nullptr || symbologyBlock == nullptr) { logger_->warn("Missing blocks"); + Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidData); return; } @@ -168,6 +172,7 @@ void Level3RadialView::ComputeSweep() if (numberOfLayers < 1) { logger_->warn("No layers present in symbology block"); + Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidData); return; } @@ -219,6 +224,7 @@ void Level3RadialView::ComputeSweep() else { logger_->debug("No radial data found"); + Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidData); return; } @@ -227,6 +233,7 @@ void Level3RadialView::ComputeSweep() if (radials < 1 || radials > 720) { logger_->warn("Unsupported number of radials: {}", radials); + Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidData); return; } @@ -254,6 +261,7 @@ void Level3RadialView::ComputeSweep() if (gates < 1) { logger_->warn("No range bins in radial data"); + Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidData); return; } diff --git a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp index e3347689..01bc3d54 100644 --- a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp @@ -116,6 +116,7 @@ void Level3RasterView::ComputeSweep() if (message == nullptr) { logger_->debug("Level 3 data not found"); + Q_EMIT SweepNotComputed(types::NoUpdateReason::NotLoaded); return; } @@ -125,11 +126,13 @@ void Level3RasterView::ComputeSweep() if (gpm == nullptr) { logger_->warn("Graphic Product Message not found"); + Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidData); return; } else if (gpm == graphic_product_message()) { // Skip if this is the message we previously processed + Q_EMIT SweepNotComputed(types::NoUpdateReason::NoChange); return; } set_graphic_product_message(gpm); @@ -143,6 +146,7 @@ void Level3RasterView::ComputeSweep() if (descriptionBlock == nullptr || symbologyBlock == nullptr) { logger_->warn("Missing blocks"); + Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidData); return; } @@ -151,6 +155,7 @@ void Level3RasterView::ComputeSweep() if (numberOfLayers < 1) { logger_->warn("No layers present in symbology block"); + Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidData); return; } @@ -182,6 +187,7 @@ void Level3RasterView::ComputeSweep() if (rasterData == nullptr) { logger_->debug("No raster data found"); + Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidData); return; } @@ -196,6 +202,7 @@ void Level3RasterView::ComputeSweep() if (maxColumns == 0) { logger_->debug("No raster bins found"); + Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidData); return; } diff --git a/scwx-qt/source/scwx/qt/view/radar_product_view.hpp b/scwx-qt/source/scwx/qt/view/radar_product_view.hpp index 16d23a80..dd92db58 100644 --- a/scwx-qt/source/scwx/qt/view/radar_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/radar_product_view.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -75,6 +76,7 @@ protected slots: signals: void ColorTableUpdated(); void SweepComputed(); + void SweepNotComputed(types::NoUpdateReason reason); private: std::unique_ptr p;