Merge pull request #62 from dpaulat/feature/timeline-part-3

Complete Timeline Animation
This commit is contained in:
Dan Paulat 2023-06-20 22:54:23 -05:00 committed by GitHub
commit effe78e1be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 730 additions and 525 deletions

View file

@ -211,6 +211,7 @@ MainWindow::MainWindow(QWidget* parent) :
ui->resourceExplorerDock->toggleViewAction()->setText(
tr("&Resource Explorer"));
ui->actionResourceExplorer->setVisible(false);
ui->resourceExplorerDock->toggleViewAction()->setVisible(false);
ui->menuView->insertAction(ui->actionAlerts,
p->alertDockWidget_->toggleViewAction());
@ -711,7 +712,10 @@ void MainWindowImpl::ConnectAnimationSignals()
&manager::TimelineManager::AnimationStateUpdated,
animationDockWidget_,
&ui::AnimationDockWidget::UpdateAnimationState);
connect(timelineManager_.get(),
&manager::TimelineManager::ViewTypeUpdated,
animationDockWidget_,
&ui::AnimationDockWidget::UpdateViewType);
connect(timelineManager_.get(),
&manager::TimelineManager::LiveStateUpdated,
animationDockWidget_,
@ -790,6 +794,8 @@ void MainWindowImpl::ConnectOtherSignals()
{
map->SetMapLocation(latitude, longitude, true);
}
UpdateRadarSite();
},
Qt::QueuedConnection);
connect(mainWindow_,
@ -807,6 +813,8 @@ void MainWindowImpl::ConnectOtherSignals()
{
map->SelectRadarSite(selectedRadarSite);
}
UpdateRadarSite();
});
connect(updateManager_.get(),
&manager::UpdateManager::UpdateAvailable,

View file

@ -115,6 +115,42 @@
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<widget class="QScrollArea" name="radarToolboxScrollArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContents</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="radarToolboxScrollAreaContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>175</width>
<height>696</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="radarInfoFrame">
<property name="frameShape">
@ -240,6 +276,23 @@
</layout>
</widget>
</item>
<item>
<spacer name="radarToolboxSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QWidget" name="settingsFrame" native="true">
<layout class="QVBoxLayout" name="verticalLayout_4">
@ -258,19 +311,6 @@
</layout>
</widget>
</item>
<item>
<spacer name="radarToolboxSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>

View file

@ -19,7 +19,9 @@
# pragma warning(push, 0)
#endif
#include <boost/asio/post.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/thread_pool.hpp>
#include <boost/container_hash/hash.hpp>
#include <boost/range/irange.hpp>
#include <boost/timer/timer.hpp>
@ -91,7 +93,7 @@ public:
group_ {group},
product_ {product},
refreshEnabled_ {false},
refreshTimer_ {scwx::util::io_context()},
refreshTimer_ {threadPool_},
refreshTimerMutex_ {},
provider_ {nullptr}
{
@ -106,6 +108,8 @@ public:
void Disable();
boost::asio::thread_pool threadPool_ {1u};
const std::string radarId_;
const common::RadarProductGroup group_;
const std::string product_;
@ -179,6 +183,8 @@ public:
RadarProductManager* self_;
boost::asio::thread_pool threadPool_ {4u};
std::shared_ptr<ProviderManager>
GetLevel3ProviderManager(const std::string& product);
@ -199,6 +205,10 @@ public:
void UpdateRecentRecords(RadarProductRecordList& recentList,
std::shared_ptr<types::RadarProductRecord> record);
void LoadNexradFileAsync(CreateNexradFileFunction load,
std::shared_ptr<request::NexradFileRequest> request,
std::mutex& mutex,
std::chrono::system_clock::time_point time);
void LoadProviderData(std::chrono::system_clock::time_point time,
std::shared_ptr<ProviderManager> providerManager,
RadarProductRecordMap& recordMap,
@ -206,6 +216,14 @@ public:
std::mutex& loadDataMutex,
std::shared_ptr<request::NexradFileRequest> request);
void PopulateLevel2ProductTimes(std::chrono::system_clock::time_point time);
void PopulateLevel3ProductTimes(const std::string& product,
std::chrono::system_clock::time_point time);
static void
PopulateProductTimes(std::shared_ptr<ProviderManager> providerManager,
RadarProductRecordMap& productRecordMap,
std::shared_mutex& productRecordMutex,
std::chrono::system_clock::time_point time);
static void
LoadNexradFile(CreateNexradFileFunction load,
@ -515,7 +533,8 @@ void RadarProductManager::EnableRefresh(common::RadarProductGroup group,
p->GetLevel3ProviderManager(product);
// Only enable refresh on available products
scwx::util::async(
boost::asio::post(
p->threadPool_,
[=, this]()
{
providerManager->provider_->RequestAvailableProducts();
@ -606,7 +625,8 @@ void RadarProductManagerImpl::RefreshData(
providerManager->refreshTimer_.cancel();
}
scwx::util::async(
boost::asio::post(
threadPool_,
[=, this]()
{
auto [newObjects, totalObjects] =
@ -766,9 +786,8 @@ void RadarProductManagerImpl::LoadProviderData(
providerManager->name(),
scwx::util::TimeString(time));
RadarProductManagerImpl::LoadNexradFile(
[=, &recordMap, &recordMutex, &loadDataMutex]()
-> std::shared_ptr<wsr88d::NexradFile>
LoadNexradFileAsync(
[=, &recordMap, &recordMutex]() -> std::shared_ptr<wsr88d::NexradFile>
{
std::shared_ptr<types::RadarProductRecord> existingRecord = nullptr;
std::shared_ptr<wsr88d::NexradFile> nexradFile = nullptr;
@ -792,9 +811,18 @@ void RadarProductManagerImpl::LoadProviderData(
if (existingRecord == nullptr)
{
std::string key = providerManager->provider_->FindKey(time);
if (!key.empty())
{
nexradFile = providerManager->provider_->LoadObjectByKey(key);
}
else
{
logger_->warn("Attempting to load object without key: {}",
scwx::util::TimeString(time));
}
}
else
{
nexradFile = existingRecord->nexrad_file();
}
@ -857,11 +885,15 @@ void RadarProductManager::LoadData(
{
logger_->debug("LoadData()");
scwx::util::async(
[=, &is]()
{
RadarProductManagerImpl::LoadNexradFile(
[=, &is]() -> std::shared_ptr<wsr88d::NexradFile>
{ return wsr88d::NexradFileFactory::Create(is); },
request,
fileLoadMutex_);
});
}
void RadarProductManager::LoadFile(
@ -898,11 +930,15 @@ void RadarProductManager::LoadFile(
}
});
scwx::util::async(
[=]()
{
RadarProductManagerImpl::LoadNexradFile(
[=]() -> std::shared_ptr<wsr88d::NexradFile>
{ return wsr88d::NexradFileFactory::Create(filename); },
request,
fileLoadMutex_);
});
}
else if (request != nullptr)
{
@ -911,15 +947,23 @@ void RadarProductManager::LoadFile(
}
}
void RadarProductManagerImpl::LoadNexradFileAsync(
CreateNexradFileFunction load,
std::shared_ptr<request::NexradFileRequest> request,
std::mutex& mutex,
std::chrono::system_clock::time_point time)
{
boost::asio::post(threadPool_,
[=, &mutex]()
{ LoadNexradFile(load, request, mutex, time); });
}
void RadarProductManagerImpl::LoadNexradFile(
CreateNexradFileFunction load,
std::shared_ptr<request::NexradFileRequest> request,
std::mutex& mutex,
std::chrono::system_clock::time_point time)
{
scwx::util::async(
[=, &mutex]()
{
std::unique_lock lock {mutex};
std::shared_ptr<wsr88d::NexradFile> nexradFile = load();
@ -955,11 +999,39 @@ void RadarProductManagerImpl::LoadNexradFile(
request->set_radar_product_record(record);
Q_EMIT request->RequestComplete(request);
}
});
}
void RadarProductManagerImpl::PopulateLevel2ProductTimes(
std::chrono::system_clock::time_point time)
{
PopulateProductTimes(level2ProviderManager_,
level2ProductRecords_,
level2ProductRecordMutex_,
time);
}
void RadarProductManagerImpl::PopulateLevel3ProductTimes(
const std::string& product, std::chrono::system_clock::time_point time)
{
// Get provider manager
auto level3ProviderManager = GetLevel3ProviderManager(product);
// Get product records
std::unique_lock level3ProductRecordLock {level3ProductRecordMutex_};
auto& level3ProductRecords = level3ProductRecordsMap_[product];
level3ProductRecordLock.unlock();
PopulateProductTimes(level3ProviderManager,
level3ProductRecords,
level3ProductRecordMutex_,
time);
}
void RadarProductManagerImpl::PopulateProductTimes(
std::shared_ptr<ProviderManager> providerManager,
RadarProductRecordMap& productRecordMap,
std::shared_mutex& productRecordMutex,
std::chrono::system_clock::time_point time)
{
const auto today = std::chrono::floor<std::chrono::days>(time);
const auto yesterday = today - std::chrono::days {1};
@ -973,7 +1045,7 @@ void RadarProductManagerImpl::PopulateLevel2ProductTimes(
std::for_each(std::execution::par_unseq,
dates.begin(),
dates.end(),
[&, this](const auto& date)
[&](const auto& date)
{
// Don't query for a time point in the future
if (date > std::chrono::system_clock::now())
@ -983,8 +1055,7 @@ void RadarProductManagerImpl::PopulateLevel2ProductTimes(
// Query the provider for volume time points
auto timePoints =
level2ProviderManager_->provider_->GetTimePointsByDate(
date);
providerManager->provider_->GetTimePointsByDate(date);
// Lock the merged volume time list
std::unique_lock volumeTimesLock {volumeTimesMutex};
@ -995,14 +1066,13 @@ void RadarProductManagerImpl::PopulateLevel2ProductTimes(
std::inserter(volumeTimes, volumeTimes.end()));
});
// Lock the level 2 product record map
std::unique_lock lock {level2ProductRecordMutex_};
// Lock the product record map
std::unique_lock lock {productRecordMutex};
// Merge volume times into map
std::transform(
volumeTimes.cbegin(),
std::transform(volumeTimes.cbegin(),
volumeTimes.cend(),
std::inserter(level2ProductRecords_, level2ProductRecords_.begin()),
std::inserter(productRecordMap, productRecordMap.begin()),
[](const std::chrono::system_clock::time_point& time)
{
return std::pair<std::chrono::system_clock::time_point,
@ -1042,7 +1112,7 @@ RadarProductManagerImpl::GetLevel2ProductRecord(
record = recordPtr->second.lock();
}
if (record == nullptr &&
if (recordPtr != nullptr && record == nullptr &&
recordTime != std::chrono::system_clock::time_point {})
{
// Product is expired, reload it
@ -1076,6 +1146,9 @@ RadarProductManagerImpl::GetLevel3ProductRecord(
RadarProductRecordMap::const_pointer recordPtr {nullptr};
std::chrono::system_clock::time_point recordTime {time};
// Ensure Level 3 product records are updated
PopulateLevel3ProductTimes(product, time);
std::unique_lock lock {level3ProductRecordMutex_};
auto it = level3ProductRecordsMap_.find(product);
@ -1099,15 +1172,12 @@ RadarProductManagerImpl::GetLevel3ProductRecord(
if (recordPtr != nullptr)
{
if (time == std::chrono::system_clock::time_point {} ||
time == recordPtr->first)
{
// Don't check for an exact time match for level 3 products
recordTime = recordPtr->first;
record = recordPtr->second.lock();
}
}
if (record == nullptr &&
if (recordPtr != nullptr && record == nullptr &&
recordTime != std::chrono::system_clock::time_point {})
{
// Product is expired, reload it
@ -1305,7 +1375,8 @@ void RadarProductManager::UpdateAvailableProducts()
logger_->debug("UpdateAvailableProducts()");
scwx::util::async(
boost::asio::post(
p->threadPool_,
[this]()
{
auto level3ProviderManager =

View file

@ -1,3 +1,5 @@
#define NOMINMAX
#include <scwx/qt/manager/timeline_manager.hpp>
#include <scwx/qt/manager/radar_product_manager.hpp>
#include <scwx/qt/manager/settings_manager.hpp>
@ -166,6 +168,8 @@ void TimelineManager::SetViewType(types::MapTime viewType)
// If the selected view type is archive, select using the pinned time
p->SelectTimeAsync(p->pinnedTime_);
}
Q_EMIT ViewTypeUpdated(viewType);
}
void TimelineManager::SetLoopTime(std::chrono::minutes loopTime)
@ -390,9 +394,10 @@ void TimelineManager::Impl::UpdateCacheLimit(
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));
// Dynamically update maximum cached volume scans to the lesser of
// either 1.5x the loop length or 5 greater than the loop length
radarProductManager->SetCacheLimit(std::min(
static_cast<std::size_t>(numVolumeScans * 1.5), numVolumeScans + 5u));
}
void TimelineManager::Impl::Play()

View file

@ -359,7 +359,8 @@ void MapWidget::SelectElevation(float elevation)
void MapWidget::SelectRadarProduct(common::RadarProductGroup group,
const std::string& product,
std::int16_t productCode,
std::chrono::system_clock::time_point time)
std::chrono::system_clock::time_point time,
bool update)
{
bool radarProductViewCreated = false;
@ -420,7 +421,7 @@ void MapWidget::SelectRadarProduct(common::RadarProductGroup group,
common::GetLevel3Palette(productCode);
p->InitializeNewRadarProductView(palette);
}
else
else if (update)
{
radarProductView->Update();
}
@ -486,7 +487,9 @@ void MapWidget::SelectRadarSite(std::shared_ptr<config::RadarSite> radarSite,
radarProductView->set_radar_product_manager(p->radarProductManager_);
SelectRadarProduct(radarProductView->GetRadarProductGroup(),
radarProductView->GetRadarProductName(),
0);
0,
radarProductView->selected_time(),
false);
}
AddLayers();
@ -1053,9 +1056,6 @@ 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();

View file

@ -54,12 +54,14 @@ public:
* @param [in] group Radar product group
* @param [in] product Radar product name
* @param [in] productCode Radar product code (optional)
* @paran [in] time Product time. Default is the latest available.
* @param [in] time Product time. Default is the latest available.
* @param [in] update Whether to update the radar product view on selection
*/
void SelectRadarProduct(common::RadarProductGroup group,
const std::string& product,
std::int16_t productCode = 0,
std::chrono::system_clock::time_point time = {});
std::chrono::system_clock::time_point time = {},
bool update = true);
void SelectRadarProduct(std::shared_ptr<types::RadarProductRecord> record);

View file

@ -20,20 +20,34 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
class AnimationDockWidgetImpl
{
public:
explicit AnimationDockWidgetImpl(AnimationDockWidget* self) : self_ {self} {}
explicit AnimationDockWidgetImpl(AnimationDockWidget* self) : self_ {self}
{
static const QString prefix = QObject::tr("Auto Update");
static const QString disabled = QObject::tr("Disabled");
static const QString enabled = QObject::tr("Enabled");
enabledString_ = QString("%1: %2").arg(prefix).arg(enabled);
disabledString_ = QString("%1: %2").arg(prefix).arg(disabled);
}
~AnimationDockWidgetImpl() = default;
const QIcon kPauseIcon_ {":/res/icons/font-awesome-6/pause-solid.svg"};
const QIcon kPlayIcon_ {":/res/icons/font-awesome-6/play-solid.svg"};
QString enabledString_;
QString disabledString_;
AnimationDockWidget* self_;
types::AnimationState animationState_ {types::AnimationState::Pause};
types::MapTime viewType_ {types::MapTime::Live};
bool isLive_ {true};
std::chrono::sys_days selectedDate_ {};
std::chrono::seconds selectedTime_ {};
void ConnectSignals();
void UpdateAutoUpdateLabel();
};
AnimationDockWidget::AnimationDockWidget(QWidget* parent) :
@ -211,6 +225,8 @@ void AnimationDockWidgetImpl::ConnectSignals()
void AnimationDockWidget::UpdateAnimationState(types::AnimationState state)
{
if (p->animationState_ != state)
{
// Update icon to opposite of state
switch (state)
{
@ -222,21 +238,43 @@ void AnimationDockWidget::UpdateAnimationState(types::AnimationState state)
ui->playButton->setIcon(p->kPauseIcon_);
break;
}
p->animationState_ = state;
p->UpdateAutoUpdateLabel();
}
}
void AnimationDockWidget::UpdateLiveState(bool isLive)
{
static const QString prefix = tr("Auto Update");
static const QString disabled = tr("Disabled");
static const QString enabled = tr("Enabled");
if (isLive)
if (p->isLive_ != isLive)
{
ui->autoUpdateLabel->setText(QString("%1: %2").arg(prefix).arg(enabled));
p->isLive_ = isLive;
p->UpdateAutoUpdateLabel();
}
}
void AnimationDockWidget::UpdateViewType(types::MapTime viewType)
{
if (p->viewType_ != viewType)
{
p->viewType_ = viewType;
p->UpdateAutoUpdateLabel();
}
}
void AnimationDockWidgetImpl::UpdateAutoUpdateLabel()
{
// Display "Auto Update: Enabled" if:
// - The map is live, and auto-updating (map widget update)
// - "Live" is selected, and the map is playing (timeline manager update)
if (isLive_ || (viewType_ == types::MapTime::Live &&
animationState_ == types::AnimationState::Play))
{
self_->ui->autoUpdateLabel->setText(enabledString_);
}
else
{
ui->autoUpdateLabel->setText(QString("%1: %2").arg(prefix).arg(disabled));
self_->ui->autoUpdateLabel->setText(disabledString_);
}
}

View file

@ -31,6 +31,7 @@ public:
public slots:
void UpdateAnimationState(types::AnimationState state);
void UpdateLiveState(bool isLive);
void UpdateViewType(types::MapTime viewType);
signals:
void ViewTypeChanged(types::MapTime viewType);

View file

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>200</width>
<height>348</height>
<height>543</height>
</rect>
</property>
<property name="windowTitle">
@ -15,6 +15,42 @@
</property>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>182</width>
<height>506</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="timelineGroupBox">
<property name="title">
@ -309,6 +345,10 @@
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
<resources>
<include location="../../../../scwx-qt.qrc"/>
</resources>

View file

@ -176,28 +176,28 @@ bool WmoHeader::Parse(std::istream& is)
if (wmoTokenList.size() < 3 || wmoTokenList.size() > 4)
{
logger_->debug("Invalid number of WMO tokens");
logger_->warn("Invalid number of WMO tokens");
headerValid = false;
}
else if (wmoTokenList[0].size() != 6)
{
logger_->debug("WMO identifier malformed");
logger_->warn("WMO identifier malformed");
headerValid = false;
}
else if (wmoTokenList[1].size() != 4)
{
logger_->debug("ICAO malformed");
logger_->warn("ICAO malformed");
headerValid = false;
}
else if (wmoTokenList[2].size() != 6)
{
logger_->debug("Date/time malformed");
logger_->warn("Date/time malformed");
headerValid = false;
}
else if (wmoTokenList.size() == 4 && wmoTokenList[3].size() != 3)
{
// BBB indicator is optional
logger_->debug("BBB indicator malformed");
logger_->warn("BBB indicator malformed");
headerValid = false;
}
else
@ -226,7 +226,7 @@ bool WmoHeader::Parse(std::istream& is)
{
if (awipsLine.size() != 6)
{
logger_->debug("AWIPS Identifier Line bad size");
logger_->warn("AWIPS Identifier Line bad size");
headerValid = false;
}
else