diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 07945d18..4d258327 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -672,6 +672,10 @@ void MainWindowImpl::ConnectAnimationSignals() &ui::AnimationDockWidget::LoopSpeedChanged, timelineManager_.get(), &manager::TimelineManager::SetLoopSpeed); + connect(animationDockWidget_, + &ui::AnimationDockWidget::LoopDelayChanged, + timelineManager_.get(), + &manager::TimelineManager::SetLoopDelay); connect(animationDockWidget_, &ui::AnimationDockWidget::AnimationStepBeginSelected, timelineManager_.get(), diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 1d3d1126..e9bb8463 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -210,7 +210,8 @@ public: static void LoadNexradFile(CreateNexradFileFunction load, std::shared_ptr request, - std::mutex& mutex); + std::mutex& mutex, + std::chrono::system_clock::time_point time = {}); const std::string radarId_; bool initialized_; @@ -801,7 +802,8 @@ void RadarProductManagerImpl::LoadProviderData( return nexradFile; }, request, - loadDataMutex); + loadDataMutex, + time); } void RadarProductManager::LoadLevel2Data( @@ -912,7 +914,8 @@ void RadarProductManager::LoadFile( void RadarProductManagerImpl::LoadNexradFile( CreateNexradFileFunction load, std::shared_ptr request, - std::mutex& mutex) + std::mutex& mutex, + std::chrono::system_clock::time_point time) { scwx::util::async( [=, &mutex]() @@ -929,6 +932,15 @@ void RadarProductManagerImpl::LoadNexradFile( { record = types::RadarProductRecord::Create(nexradFile); + // If the time is already determined, override the time in the file. + // Sometimes, level 2 data has been seen to be a few seconds off + // between filename and file data. Overriding this can help prevent + // issues with locating and storing the correct records. + if (time != std::chrono::system_clock::time_point {}) + { + record->set_time(time); + } + std::shared_ptr manager = RadarProductManager::Instance(record->radar_id()); @@ -1192,15 +1204,17 @@ void RadarProductManagerImpl::UpdateRecentRecords( std::shared_ptr record) { const std::size_t recentListMaxSize {cacheLimit_}; + bool iteratorErased = false; auto it = std::find(recentList.cbegin(), recentList.cend(), record); if (it != recentList.cbegin() && it != recentList.cend()) { // If the record exists beyond the front of the list, remove it recentList.erase(it); + iteratorErased = true; } - if (recentList.size() == 0 || it != recentList.cbegin()) + if (iteratorErased || recentList.size() == 0 || it != recentList.cbegin()) { // Add the record to the front of the list, unless it's already there recentList.push_front(record); diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp index 606a58ca..7bcb9b64 100644 --- a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp @@ -59,8 +59,11 @@ public: void Pause(); void Play(); - void SelectTime(std::chrono::system_clock::time_point selectedTime = {}); - void Step(Direction direction); + void + SelectTimeAsync(std::chrono::system_clock::time_point selectedTime = {}); + std::pair + SelectTime(std::chrono::system_clock::time_point selectedTime = {}); + void StepAsync(Direction direction); std::size_t mapCount_ {0}; std::string radarSite_ {"?"}; @@ -71,6 +74,7 @@ public: types::MapTime viewType_ {types::MapTime::Live}; std::chrono::minutes loopTime_ {30}; double loopSpeed_ {5.0}; + std::chrono::milliseconds loopDelay_ {2500}; bool radarSweepMonitorActive_ {false}; std::mutex radarSweepMonitorMutex_ {}; @@ -113,7 +117,7 @@ void TimelineManager::SetRadarSite(const std::string& radarSite) else { // If the selected view type is archive, select using the selected time - p->SelectTime(p->selectedTime_); + p->SelectTimeAsync(p->selectedTime_); } } @@ -127,7 +131,7 @@ void TimelineManager::SetDateTime( if (p->viewType_ == types::MapTime::Archive) { // Only select if the view type is archive - p->SelectTime(dateTime); + p->SelectTimeAsync(dateTime); } // Ignore a date/time selection if the view type is live @@ -147,7 +151,7 @@ void TimelineManager::SetViewType(types::MapTime viewType) else { // If the selected view type is archive, select using the pinned time - p->SelectTime(p->pinnedTime_); + p->SelectTimeAsync(p->pinnedTime_); } } @@ -170,6 +174,13 @@ void TimelineManager::SetLoopSpeed(double loopSpeed) p->loopSpeed_ = loopSpeed; } +void TimelineManager::SetLoopDelay(std::chrono::milliseconds loopDelay) +{ + logger_->debug("SetLoopDelay: {}", loopDelay); + + p->loopDelay_ = loopDelay; +} + void TimelineManager::AnimationStepBegin() { logger_->debug("AnimationStepBegin"); @@ -180,12 +191,12 @@ void TimelineManager::AnimationStepBegin() p->pinnedTime_ == std::chrono::system_clock::time_point {}) { // If the selected view type is live, select the current products - p->SelectTime(std::chrono::system_clock::now() - p->loopTime_); + p->SelectTimeAsync(std::chrono::system_clock::now() - p->loopTime_); } else { // If the selected view type is archive, select using the pinned time - p->SelectTime(p->pinnedTime_ - p->loopTime_); + p->SelectTimeAsync(p->pinnedTime_ - p->loopTime_); } } @@ -194,7 +205,7 @@ void TimelineManager::AnimationStepBack() logger_->debug("AnimationStepBack"); p->Pause(); - p->Step(Direction::Back); + p->StepAsync(Direction::Back); } void TimelineManager::AnimationPlayPause() @@ -216,7 +227,7 @@ void TimelineManager::AnimationStepNext() logger_->debug("AnimationStepNext"); p->Pause(); - p->Step(Direction::Next); + p->StepAsync(Direction::Next); } void TimelineManager::AnimationStepEnd() @@ -233,7 +244,7 @@ void TimelineManager::AnimationStepEnd() else { // If the selected view type is archive, select using the pinned time - p->SelectTime(p->pinnedTime_); + p->SelectTimeAsync(p->pinnedTime_); } } @@ -407,30 +418,50 @@ void TimelineManager::Impl::Play() 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::milliseconds(std::lroundl(1000.0 / loopSpeed_)); + interval = std::chrono::duration_cast( + std::chrono::milliseconds(std::lroundl(1000.0 / loopSpeed_)) - + elapsedTime); } else { - // Pause for 2.5 seconds at the end of the loop - interval = std::chrono::milliseconds(2500); + // Pause at the end of the loop + interval = std::chrono::duration_cast( + loopDelay_ - elapsedTime); } - 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) { @@ -453,118 +484,94 @@ void TimelineManager::Impl::Play() }); } -void TimelineManager::Impl::SelectTime( +void TimelineManager::Impl::SelectTimeAsync( std::chrono::system_clock::time_point selectedTime) { + scwx::util::async([=, this]() { SelectTime(selectedTime); }); +} + +std::pair TimelineManager::Impl::SelectTime( + std::chrono::system_clock::time_point selectedTime) +{ + bool volumeTimeUpdated = false; + bool selectedTimeUpdated = false; + if (selectedTime_ == selectedTime && radarSite_ == previousRadarSite_) { // Nothing to do - return; + return {volumeTimeUpdated, selectedTimeUpdated}; } else if (selectedTime == std::chrono::system_clock::time_point {}) { - scwx::util::async( - [=, this]() - { - // Take a lock for time selection - std::unique_lock lock {selectTimeMutex_}; + // If a default time point is given, reset to a live view + selectedTime_ = selectedTime; + adjustedTime_ = selectedTime; + previousRadarSite_ = radarSite_; - // If a default time point is given, reset to a live view - selectedTime_ = selectedTime; - adjustedTime_ = selectedTime; + logger_->debug("Time updated: Live"); - logger_->debug("Time updated: Live"); + Q_EMIT self_->LiveStateUpdated(true); + Q_EMIT self_->VolumeTimeUpdated(selectedTime); + Q_EMIT self_->SelectedTimeUpdated(selectedTime); - std::unique_lock radarSweepMonitorLock {radarSweepMonitorMutex_}; + volumeTimeUpdated = true; + selectedTimeUpdated = true; - // 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; + return {volumeTimeUpdated, selectedTimeUpdated}; } - scwx::util::async( - [=, this]() + // Take a lock for time selection + std::unique_lock lock {selectTimeMutex_}; + + // Request active volume times + auto radarProductManager = + manager::RadarProductManager::Instance(radarSite_); + auto volumeTimes = radarProductManager->GetActiveVolumeTimes(selectedTime); + + // Dynamically update maximum cached volume scans + UpdateCacheLimit(radarProductManager, volumeTimes); + + // Find the best match bounded time + auto elementPtr = util::GetBoundedElementPointer(volumeTimes, selectedTime); + + // The timeline is no longer live + Q_EMIT self_->LiveStateUpdated(false); + + if (elementPtr != nullptr) + { + // If the adjusted time changed, or if a new radar site has been selected + if (adjustedTime_ != *elementPtr || radarSite_ != previousRadarSite_) { - // Take a lock for time selection - std::unique_lock lock {selectTimeMutex_}; + // If the time was found, select it + adjustedTime_ = *elementPtr; - // Request active volume times - auto radarProductManager = - manager::RadarProductManager::Instance(radarSite_); - auto volumeTimes = - radarProductManager->GetActiveVolumeTimes(selectedTime); + logger_->debug("Volume time updated: {}", + scwx::util::TimeString(adjustedTime_)); - // Dynamically update maximum cached volume scans - UpdateCacheLimit(radarProductManager, volumeTimes); + volumeTimeUpdated = true; + Q_EMIT self_->VolumeTimeUpdated(adjustedTime_); + } + } + else + { + // No volume time was found + logger_->info("No volume scan found for {}", + scwx::util::TimeString(selectedTime)); + } - // Find the best match bounded time - auto elementPtr = - util::GetBoundedElementPointer(volumeTimes, selectedTime); + logger_->trace("Selected time updated: {}", + scwx::util::TimeString(selectedTime)); - // The timeline is no longer live - Q_EMIT self_->LiveStateUpdated(false); + selectedTime_ = selectedTime; + selectedTimeUpdated = true; + Q_EMIT self_->SelectedTimeUpdated(selectedTime); - bool volumeTimeUpdated = false; + previousRadarSite_ = radarSite_; - 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 || - radarSite_ != previousRadarSite_) - { - // If the time was found, select it - adjustedTime_ = *elementPtr; - - logger_->debug("Volume time updated: {}", - scwx::util::TimeString(adjustedTime_)); - - volumeTimeUpdated = true; - Q_EMIT self_->VolumeTimeUpdated(adjustedTime_); - } - } - else - { - // No volume time was found - logger_->info("No volume scan found for {}", - scwx::util::TimeString(selectedTime)); - } - - logger_->trace("Selected time updated: {}", - scwx::util::TimeString(selectedTime)); - - selectedTime_ = selectedTime; - Q_EMIT self_->SelectedTimeUpdated(selectedTime); - - previousRadarSite_ = radarSite_; - - if (volumeTimeUpdated) - { - // Wait for radar sweeps to update - RadarSweepMonitorWait(radarSweepMonitorLock); - } - else - { - RadarSweepMonitorDisable(); - } - }); + return {volumeTimeUpdated, selectedTimeUpdated}; } -void TimelineManager::Impl::Step(Direction direction) +void TimelineManager::Impl::StepAsync(Direction direction) { scwx::util::async( [=, this]() diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp index 0751fed1..1a4154ac 100644 --- a/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp @@ -34,6 +34,7 @@ public slots: void SetLoopTime(std::chrono::minutes loopTime); void SetLoopSpeed(double loopSpeed); + void SetLoopDelay(std::chrono::milliseconds loopDelay); void AnimationStepBegin(); void AnimationStepBack(); diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 6d0146c0..05ce34f4 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1053,6 +1053,9 @@ void MapWidgetImpl::SetRadarSite(const std::string& radarSite) // Set new RadarProductManager radarProductManager_ = manager::RadarProductManager::Instance(radarSite); + // Re-enable auto-update + autoUpdateEnabled_ = true; + // Connect signals to new RadarProductManager RadarProductManagerConnect(); diff --git a/scwx-qt/source/scwx/qt/types/radar_product_record.cpp b/scwx-qt/source/scwx/qt/types/radar_product_record.cpp index ce402342..6a1953e3 100644 --- a/scwx-qt/source/scwx/qt/types/radar_product_record.cpp +++ b/scwx-qt/source/scwx/qt/types/radar_product_record.cpp @@ -133,6 +133,11 @@ std::chrono::system_clock::time_point RadarProductRecord::time() const return p->time_; } +void RadarProductRecord::set_time(std::chrono::system_clock::time_point time) +{ + p->time_ = time; +} + std::shared_ptr RadarProductRecord::Create(std::shared_ptr nexradFile) { diff --git a/scwx-qt/source/scwx/qt/types/radar_product_record.hpp b/scwx-qt/source/scwx/qt/types/radar_product_record.hpp index ab1775d0..dd27ccbc 100644 --- a/scwx-qt/source/scwx/qt/types/radar_product_record.hpp +++ b/scwx-qt/source/scwx/qt/types/radar_product_record.hpp @@ -22,7 +22,7 @@ public: explicit RadarProductRecord(std::shared_ptr nexradFile); ~RadarProductRecord(); - RadarProductRecord(const RadarProductRecord&) = delete; + RadarProductRecord(const RadarProductRecord&) = delete; RadarProductRecord& operator=(const RadarProductRecord&) = delete; RadarProductRecord(RadarProductRecord&&) noexcept; @@ -38,6 +38,8 @@ public: std::string site_id() const; std::chrono::system_clock::time_point time() const; + void set_time(std::chrono::system_clock::time_point time); + static std::shared_ptr Create(std::shared_ptr nexradFile); diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp index 4714b2e3..5bfc5a89 100644 --- a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp @@ -88,6 +88,7 @@ AnimationDockWidget::AnimationDockWidget(QWidget* parent) : // Set loop defaults ui->loopTimeSpinBox->setValue(30); ui->loopSpeedSpinBox->setValue(5.0); + ui->loopDelaySpinBox->setValue(2.5); // Connect widget signals p->ConnectSignals(); @@ -161,6 +162,15 @@ void AnimationDockWidgetImpl::ConnectSignals() &QDoubleSpinBox::valueChanged, self_, [this](double d) { Q_EMIT self_->LoopSpeedChanged(d); }); + QObject::connect( + self_->ui->loopDelaySpinBox, + &QDoubleSpinBox::valueChanged, + self_, + [this](double d) + { + Q_EMIT self_->LoopDelayChanged(std::chrono::milliseconds( + static_cast(d * 1000.0))); + }); // Animation controls QObject::connect(self_->ui->beginButton, diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.hpp b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.hpp index 705ca02f..ea42a541 100644 --- a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.hpp +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.hpp @@ -38,6 +38,7 @@ signals: void LoopTimeChanged(std::chrono::minutes loopTime); void LoopSpeedChanged(double loopSpeed); + void LoopDelayChanged(std::chrono::milliseconds loopDelay); void AnimationStepBeginSelected(); void AnimationStepBackSelected(); diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui index 4e3502ca..8a3ab3b8 100644 --- a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui @@ -7,7 +7,7 @@ 0 0 200 - 337 + 348 @@ -130,13 +130,6 @@ 0 - - - - Loop Time - - - @@ -156,6 +149,13 @@ + + + + Loop Time + + + @@ -179,6 +179,32 @@ + + + + Loop Delay + + + + + + + sec + + + 1 + + + 15.000000000000000 + + + 0.100000000000000 + + + 2.500000000000000 + + + 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 06c912bc..f55c2fb2 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -725,7 +725,8 @@ void Level2ProductViewImpl::ComputeCoordinates( const std::uint16_t numRadials = static_cast(radarData->size()); const std::uint16_t numRangeBins = - momentData0->number_of_data_moment_gates(); + std::max(momentData0->number_of_data_moment_gates() + 1u, + common::MAX_DATA_MOMENT_GATES); auto radials = boost::irange(0u, numRadials); auto gates = boost::irange(0u, numRangeBins); diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index 87d4a37f..72368076 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -422,6 +422,12 @@ void Ar2vFileImpl::IndexFile() std::shared_ptr radial0 = (*elevationCut.second)[0]; + if (radial0 == nullptr) + { + logger_->warn("Empty radial data"); + continue; + } + for (rda::DataBlockType dataBlockType : rda::MomentDataBlockTypeIterator()) {