mirror of
				https://github.com/ciphervance/supercell-wx.git
				synced 2025-11-03 15:40:06 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			541 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			541 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
#include <scwx/qt/manager/timeline_manager.hpp>
 | 
						|
#include <scwx/qt/manager/radar_product_manager.hpp>
 | 
						|
#include <scwx/util/logger.hpp>
 | 
						|
#include <scwx/util/map.hpp>
 | 
						|
#include <scwx/util/threads.hpp>
 | 
						|
#include <scwx/util/time.hpp>
 | 
						|
 | 
						|
#include <mutex>
 | 
						|
 | 
						|
#include <boost/asio/steady_timer.hpp>
 | 
						|
#include <fmt/chrono.h>
 | 
						|
 | 
						|
namespace scwx
 | 
						|
{
 | 
						|
namespace qt
 | 
						|
{
 | 
						|
namespace manager
 | 
						|
{
 | 
						|
 | 
						|
static const std::string logPrefix_ = "scwx::qt::manager::timeline_manager";
 | 
						|
static const auto        logger_    = scwx::util::Logger::Create(logPrefix_);
 | 
						|
 | 
						|
enum class Direction
 | 
						|
{
 | 
						|
   Back,
 | 
						|
   Next
 | 
						|
};
 | 
						|
 | 
						|
class TimelineManager::Impl
 | 
						|
{
 | 
						|
public:
 | 
						|
   explicit Impl(TimelineManager* self) : self_ {self} {}
 | 
						|
 | 
						|
   ~Impl()
 | 
						|
   {
 | 
						|
      // Lock mutexes before destroying
 | 
						|
      std::unique_lock animationTimerLock {animationTimerMutex_};
 | 
						|
      animationTimer_.cancel();
 | 
						|
 | 
						|
      std::unique_lock selectTimeLock {selectTimeMutex_};
 | 
						|
   }
 | 
						|
 | 
						|
   TimelineManager* self_;
 | 
						|
 | 
						|
   std::pair<std::chrono::system_clock::time_point,
 | 
						|
             std::chrono::system_clock::time_point>
 | 
						|
        GetLoopStartAndEndTimes();
 | 
						|
   void UpdateCacheLimit(
 | 
						|
      std::shared_ptr<manager::RadarProductManager> radarProductManager,
 | 
						|
      const std::set<std::chrono::system_clock::time_point>& volumeTimes);
 | 
						|
 | 
						|
   void Pause();
 | 
						|
   void Play();
 | 
						|
   void SelectTime(std::chrono::system_clock::time_point selectedTime = {});
 | 
						|
   void Step(Direction direction);
 | 
						|
 | 
						|
   std::string                           radarSite_ {"?"};
 | 
						|
   std::string                           previousRadarSite_ {"?"};
 | 
						|
   std::chrono::system_clock::time_point pinnedTime_ {};
 | 
						|
   std::chrono::system_clock::time_point adjustedTime_ {};
 | 
						|
   std::chrono::system_clock::time_point selectedTime_ {};
 | 
						|
   types::MapTime                        viewType_ {types::MapTime::Live};
 | 
						|
   std::chrono::minutes                  loopTime_ {30};
 | 
						|
   double                                loopSpeed_ {5.0};
 | 
						|
 | 
						|
   types::AnimationState     animationState_ {types::AnimationState::Pause};
 | 
						|
   boost::asio::steady_timer animationTimer_ {scwx::util::io_context()};
 | 
						|
   std::mutex                animationTimerMutex_ {};
 | 
						|
 | 
						|
   std::mutex selectTimeMutex_ {};
 | 
						|
};
 | 
						|
 | 
						|
TimelineManager::TimelineManager() : p(std::make_unique<Impl>(this)) {}
 | 
						|
TimelineManager::~TimelineManager() = default;
 | 
						|
 | 
						|
void TimelineManager::SetRadarSite(const std::string& radarSite)
 | 
						|
{
 | 
						|
   if (p->radarSite_ == radarSite)
 | 
						|
   {
 | 
						|
      // No action needed
 | 
						|
      return;
 | 
						|
   }
 | 
						|
 | 
						|
   logger_->debug("SetRadarSite: {}", radarSite);
 | 
						|
 | 
						|
   p->radarSite_ = radarSite;
 | 
						|
 | 
						|
   if (p->viewType_ == types::MapTime::Live)
 | 
						|
   {
 | 
						|
      // If the selected view type is live, select the current products
 | 
						|
      p->SelectTime();
 | 
						|
   }
 | 
						|
   else
 | 
						|
   {
 | 
						|
      // If the selected view type is archive, select using the selected time
 | 
						|
      p->SelectTime(p->selectedTime_);
 | 
						|
   }
 | 
						|
}
 | 
						|
 | 
						|
void TimelineManager::SetDateTime(
 | 
						|
   std::chrono::system_clock::time_point dateTime)
 | 
						|
{
 | 
						|
   logger_->debug("SetDateTime: {}", scwx::util::TimeString(dateTime));
 | 
						|
 | 
						|
   p->pinnedTime_ = dateTime;
 | 
						|
 | 
						|
   if (p->viewType_ == types::MapTime::Archive)
 | 
						|
   {
 | 
						|
      // Only select if the view type is archive
 | 
						|
      p->SelectTime(dateTime);
 | 
						|
   }
 | 
						|
 | 
						|
   // Ignore a date/time selection if the view type is live
 | 
						|
}
 | 
						|
 | 
						|
void TimelineManager::SetViewType(types::MapTime viewType)
 | 
						|
{
 | 
						|
   logger_->debug("SetViewType: {}", types::GetMapTimeName(viewType));
 | 
						|
 | 
						|
   p->viewType_ = viewType;
 | 
						|
 | 
						|
   if (p->viewType_ == types::MapTime::Live)
 | 
						|
   {
 | 
						|
      // If the selected view type is live, select the current products
 | 
						|
      p->SelectTime();
 | 
						|
   }
 | 
						|
   else
 | 
						|
   {
 | 
						|
      // If the selected view type is archive, select using the pinned time
 | 
						|
      p->SelectTime(p->pinnedTime_);
 | 
						|
   }
 | 
						|
}
 | 
						|
 | 
						|
void TimelineManager::SetLoopTime(std::chrono::minutes loopTime)
 | 
						|
{
 | 
						|
   logger_->debug("SetLoopTime: {}", loopTime);
 | 
						|
 | 
						|
   p->loopTime_ = loopTime;
 | 
						|
}
 | 
						|
 | 
						|
void TimelineManager::SetLoopSpeed(double loopSpeed)
 | 
						|
{
 | 
						|
   logger_->debug("SetLoopSpeed: {}", loopSpeed);
 | 
						|
 | 
						|
   if (loopSpeed < 1.0)
 | 
						|
   {
 | 
						|
      loopSpeed = 1.0;
 | 
						|
   }
 | 
						|
 | 
						|
   p->loopSpeed_ = loopSpeed;
 | 
						|
}
 | 
						|
 | 
						|
void TimelineManager::AnimationStepBegin()
 | 
						|
{
 | 
						|
   logger_->debug("AnimationStepBegin");
 | 
						|
 | 
						|
   p->Pause();
 | 
						|
 | 
						|
   if (p->viewType_ == types::MapTime::Live ||
 | 
						|
       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_);
 | 
						|
   }
 | 
						|
   else
 | 
						|
   {
 | 
						|
      // If the selected view type is archive, select using the pinned time
 | 
						|
      p->SelectTime(p->pinnedTime_ - p->loopTime_);
 | 
						|
   }
 | 
						|
}
 | 
						|
 | 
						|
void TimelineManager::AnimationStepBack()
 | 
						|
{
 | 
						|
   logger_->debug("AnimationStepBack");
 | 
						|
 | 
						|
   p->Pause();
 | 
						|
   p->Step(Direction::Back);
 | 
						|
}
 | 
						|
 | 
						|
void TimelineManager::AnimationPlayPause()
 | 
						|
{
 | 
						|
   if (p->animationState_ == types::AnimationState::Pause)
 | 
						|
   {
 | 
						|
      logger_->debug("AnimationPlay");
 | 
						|
      p->Play();
 | 
						|
   }
 | 
						|
   else
 | 
						|
   {
 | 
						|
      logger_->debug("AnimationPause");
 | 
						|
      p->Pause();
 | 
						|
   }
 | 
						|
}
 | 
						|
 | 
						|
void TimelineManager::AnimationStepNext()
 | 
						|
{
 | 
						|
   logger_->debug("AnimationStepNext");
 | 
						|
 | 
						|
   p->Pause();
 | 
						|
   p->Step(Direction::Next);
 | 
						|
}
 | 
						|
 | 
						|
void TimelineManager::AnimationStepEnd()
 | 
						|
{
 | 
						|
   logger_->debug("AnimationStepEnd");
 | 
						|
 | 
						|
   p->Pause();
 | 
						|
 | 
						|
   if (p->viewType_ == types::MapTime::Live)
 | 
						|
   {
 | 
						|
      // If the selected view type is live, select the current products
 | 
						|
      p->SelectTime();
 | 
						|
   }
 | 
						|
   else
 | 
						|
   {
 | 
						|
      // If the selected view type is archive, select using the pinned time
 | 
						|
      p->SelectTime(p->pinnedTime_);
 | 
						|
   }
 | 
						|
}
 | 
						|
 | 
						|
void TimelineManager::Impl::Pause()
 | 
						|
{
 | 
						|
   // Cancel animation
 | 
						|
   std::unique_lock animationTimerLock {animationTimerMutex_};
 | 
						|
   animationTimer_.cancel();
 | 
						|
 | 
						|
   if (animationState_ != types::AnimationState::Pause)
 | 
						|
   {
 | 
						|
      animationState_ = types::AnimationState::Pause;
 | 
						|
      Q_EMIT self_->AnimationStateUpdated(animationState_);
 | 
						|
   }
 | 
						|
}
 | 
						|
 | 
						|
std::pair<std::chrono::system_clock::time_point,
 | 
						|
          std::chrono::system_clock::time_point>
 | 
						|
TimelineManager::Impl::GetLoopStartAndEndTimes()
 | 
						|
{
 | 
						|
   // Determine loop end time
 | 
						|
   std::chrono::system_clock::time_point endTime;
 | 
						|
 | 
						|
   if (viewType_ == types::MapTime::Live ||
 | 
						|
       pinnedTime_ == std::chrono::system_clock::time_point {})
 | 
						|
   {
 | 
						|
      endTime = std::chrono::floor<std::chrono::minutes>(
 | 
						|
         std::chrono::system_clock::now());
 | 
						|
   }
 | 
						|
   else
 | 
						|
   {
 | 
						|
      endTime = pinnedTime_;
 | 
						|
   }
 | 
						|
 | 
						|
   // Determine loop start time and current position in the loop
 | 
						|
   std::chrono::system_clock::time_point startTime = endTime - loopTime_;
 | 
						|
 | 
						|
   return {startTime, endTime};
 | 
						|
}
 | 
						|
 | 
						|
void TimelineManager::Impl::UpdateCacheLimit(
 | 
						|
   std::shared_ptr<manager::RadarProductManager>          radarProductManager,
 | 
						|
   const std::set<std::chrono::system_clock::time_point>& volumeTimes)
 | 
						|
{
 | 
						|
   // Calculate the number of volume scans in the loop
 | 
						|
   auto [startTime, endTime] = GetLoopStartAndEndTimes();
 | 
						|
   auto startIter = util::GetBoundedElementIterator(volumeTimes, startTime);
 | 
						|
   auto endIter   = util::GetBoundedElementIterator(volumeTimes, endTime);
 | 
						|
   std::size_t numVolumeScans = std::distance(startIter, endIter) + 1;
 | 
						|
 | 
						|
   // Dynamically update maximum cached volume scans to 1.5x the loop length
 | 
						|
   radarProductManager->SetCacheLimit(
 | 
						|
      static_cast<std::size_t>(numVolumeScans * 1.5));
 | 
						|
}
 | 
						|
 | 
						|
void TimelineManager::Impl::Play()
 | 
						|
{
 | 
						|
   using namespace std::chrono_literals;
 | 
						|
 | 
						|
   if (animationState_ != types::AnimationState::Play)
 | 
						|
   {
 | 
						|
      animationState_ = types::AnimationState::Play;
 | 
						|
      Q_EMIT self_->AnimationStateUpdated(animationState_);
 | 
						|
   }
 | 
						|
 | 
						|
   {
 | 
						|
      std::unique_lock animationTimerLock {animationTimerMutex_};
 | 
						|
      animationTimer_.cancel();
 | 
						|
   }
 | 
						|
 | 
						|
   scwx::util::async(
 | 
						|
      [this]()
 | 
						|
      {
 | 
						|
         // 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();
 | 
						|
 | 
						|
         // Select the time
 | 
						|
         SelectTime(newTime);
 | 
						|
 | 
						|
         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_));
 | 
						|
         }
 | 
						|
         else
 | 
						|
         {
 | 
						|
            // Pause for 2.5 seconds at the end of the loop
 | 
						|
            interval = std::chrono::milliseconds(2500);
 | 
						|
         }
 | 
						|
 | 
						|
         std::unique_lock animationTimerLock {animationTimerMutex_};
 | 
						|
 | 
						|
         animationTimer_.expires_after(interval);
 | 
						|
         animationTimer_.async_wait(
 | 
						|
            [this](const boost::system::error_code& e)
 | 
						|
            {
 | 
						|
               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());
 | 
						|
               }
 | 
						|
            });
 | 
						|
      });
 | 
						|
}
 | 
						|
 | 
						|
void TimelineManager::Impl::SelectTime(
 | 
						|
   std::chrono::system_clock::time_point selectedTime)
 | 
						|
{
 | 
						|
   if (selectedTime_ == selectedTime && radarSite_ == previousRadarSite_)
 | 
						|
   {
 | 
						|
      // Nothing to do
 | 
						|
      return;
 | 
						|
   }
 | 
						|
   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;
 | 
						|
 | 
						|
      logger_->debug("Time updated: Live");
 | 
						|
 | 
						|
      Q_EMIT self_->LiveStateUpdated(true);
 | 
						|
      Q_EMIT self_->VolumeTimeUpdated(selectedTime);
 | 
						|
      Q_EMIT self_->SelectedTimeUpdated(selectedTime);
 | 
						|
 | 
						|
      return;
 | 
						|
   }
 | 
						|
 | 
						|
   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_)
 | 
						|
            {
 | 
						|
               // If the time was found, select it
 | 
						|
               adjustedTime_ = *elementPtr;
 | 
						|
 | 
						|
               logger_->debug("Volume time updated: {}",
 | 
						|
                              scwx::util::TimeString(adjustedTime_));
 | 
						|
 | 
						|
               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_;
 | 
						|
      });
 | 
						|
}
 | 
						|
 | 
						|
void TimelineManager::Impl::Step(Direction direction)
 | 
						|
{
 | 
						|
   scwx::util::async(
 | 
						|
      [=, this]()
 | 
						|
      {
 | 
						|
         // 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())
 | 
						|
            {
 | 
						|
               // 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_);
 | 
						|
            }
 | 
						|
         }
 | 
						|
      });
 | 
						|
}
 | 
						|
 | 
						|
std::shared_ptr<TimelineManager> TimelineManager::Instance()
 | 
						|
{
 | 
						|
   static std::weak_ptr<TimelineManager> timelineManagerReference_ {};
 | 
						|
   static std::mutex                     instanceMutex_ {};
 | 
						|
 | 
						|
   std::unique_lock lock(instanceMutex_);
 | 
						|
 | 
						|
   std::shared_ptr<TimelineManager> timelineManager =
 | 
						|
      timelineManagerReference_.lock();
 | 
						|
 | 
						|
   if (timelineManager == nullptr)
 | 
						|
   {
 | 
						|
      timelineManager           = std::make_shared<TimelineManager>();
 | 
						|
      timelineManagerReference_ = timelineManager;
 | 
						|
   }
 | 
						|
 | 
						|
   return timelineManager;
 | 
						|
}
 | 
						|
 | 
						|
} // namespace manager
 | 
						|
} // namespace qt
 | 
						|
} // namespace scwx
 |