Merge pull request #52 from dpaulat/feature/timeline-animation

Initial Timeline Animation Implementation
This commit is contained in:
Dan Paulat 2023-05-30 23:36:43 -05:00 committed by GitHub
commit 287903f180
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1970 additions and 113 deletions

View file

@ -127,12 +127,41 @@ jobs:
ninja supercell-wx wxtest
cmake --install . --component supercell-wx
- name: Upload Artifacts
- name: Collect Artifacts
if: matrix.os == 'ubuntu-22.04'
shell: bash
run: |
pushd supercell-wx/
cd lib/
ln -s libssl.so.3 libssl.so
cd ..
mkdir -p plugins/sqldrivers/
cd plugins/sqldrivers/
cp "${RUNNER_WORKSPACE}/Qt/${{ matrix.qt_version }}/${{ matrix.qt_arch }}/plugins/sqldrivers/libqsqlite.so" .
popd
tar -czf supercell-wx-${{ matrix.artifact_suffix }}.tar.gz supercell-wx/
- name: Upload Artifacts (Windows)
if: matrix.os == 'windows-2022'
uses: actions/upload-artifact@v3
with:
name: supercell-wx-${{ matrix.artifact_suffix }}
path: ${{ github.workspace }}/supercell-wx/
- name: Upload Debug Artifacts (Windows)
if: matrix.os == 'windows-2022'
uses: actions/upload-artifact@v3
with:
name: supercell-wx-debug-${{ matrix.artifact_suffix }}
path: ${{ github.workspace }}/build/bin/*.pdb
- name: Upload Artifacts (Linux)
if: matrix.os == 'ubuntu-22.04'
uses: actions/upload-artifact@v3
with:
name: supercell-wx-${{ matrix.artifact_suffix }}
path: ${{ github.workspace }}/supercell-wx-${{ matrix.artifact_suffix }}.tar.gz
- name: Test Supercell Wx
working-directory: ${{ github.workspace }}/build
run: ctest -C ${{ matrix.build_type }} --exclude-regex mbgl-test-runner

View file

@ -17,7 +17,15 @@ set_property(DIRECTORY
PROPERTY CMAKE_CONFIGURE_DEPENDS
conanfile.txt)
conan_cmake_autodetect(settings)
# Don't use RelWithDebInfo Conan packages
if (${CMAKE_BUILD_TYPE} STREQUAL "RelWithDebInfo")
set(conan_build_type "Release")
else()
set(conan_build_type ${CMAKE_BUILD_TYPE})
endif()
conan_cmake_autodetect(settings
BUILD_TYPE ${conan_build_type})
conan_cmake_install(PATH_OR_REFERENCE ${PROJECT_SOURCE_DIR}
BUILD missing

View file

@ -10,6 +10,15 @@ find_package(ZLIB)
target_include_directories(mbgl-core PRIVATE ${ZLIB_INCLUDE_DIRS})
target_link_libraries(mbgl-core INTERFACE ${ZLIB_LIBRARIES})
if (MSVC)
# Produce PDB file for debug
target_compile_options(mbgl-core PRIVATE "$<$<CONFIG:Release>:/Zi>")
target_compile_options(qmaplibregl PRIVATE "$<$<CONFIG:Release>:/Zi>")
target_link_options(qmaplibregl PRIVATE "$<$<CONFIG:Release>:/DEBUG>")
target_link_options(qmaplibregl PRIVATE "$<$<CONFIG:Release>:/OPT:REF>")
target_link_options(qmaplibregl PRIVATE "$<$<CONFIG:Release>:/OPT:ICF>")
endif()
set(MBGL_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/mapbox-gl-native/include
${CMAKE_CURRENT_SOURCE_DIR}/mapbox-gl-native/platform/qt/include PARENT_SCOPE)

View file

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="pause" class="svg-inline--fa fa-pause" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path fill="currentColor" d="M272 63.1l-32 0c-26.51 0-48 21.49-48 47.1v288c0 26.51 21.49 48 48 48L272 448c26.51 0 48-21.49 48-48v-288C320 85.49 298.5 63.1 272 63.1zM80 63.1l-32 0c-26.51 0-48 21.49-48 48v288C0 426.5 21.49 448 48 448l32 0c26.51 0 48-21.49 48-48v-288C128 85.49 106.5 63.1 80 63.1z"></path></svg>

After

Width:  |  Height:  |  Size: 487 B

View file

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="play" class="svg-inline--fa fa-play" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M176 480C148.6 480 128 457.6 128 432v-352c0-25.38 20.4-47.98 48.01-47.98c8.686 0 17.35 2.352 25.02 7.031l288 176C503.3 223.8 512 239.3 512 256s-8.703 32.23-22.97 40.95l-288 176C193.4 477.6 184.7 480 176 480z"></path></svg>

After

Width:  |  Height:  |  Size: 427 B

View file

@ -65,12 +65,14 @@ set(HDR_MANAGER source/scwx/qt/manager/radar_product_manager.hpp
source/scwx/qt/manager/resource_manager.hpp
source/scwx/qt/manager/settings_manager.hpp
source/scwx/qt/manager/text_event_manager.hpp
source/scwx/qt/manager/timeline_manager.hpp
source/scwx/qt/manager/update_manager.hpp)
set(SRC_MANAGER source/scwx/qt/manager/radar_product_manager.cpp
source/scwx/qt/manager/radar_product_manager_notifier.cpp
source/scwx/qt/manager/resource_manager.cpp
source/scwx/qt/manager/settings_manager.cpp
source/scwx/qt/manager/text_event_manager.cpp
source/scwx/qt/manager/timeline_manager.cpp
source/scwx/qt/manager/update_manager.cpp)
set(HDR_MAP source/scwx/qt/map/alert_layer.hpp
source/scwx/qt/map/color_table_layer.hpp
@ -132,16 +134,19 @@ set(SRC_SETTINGS source/scwx/qt/settings/general_settings.cpp
set(HDR_TYPES source/scwx/qt/types/alert_types.hpp
source/scwx/qt/types/font_types.hpp
source/scwx/qt/types/github_types.hpp
source/scwx/qt/types/map_types.hpp
source/scwx/qt/types/qt_types.hpp
source/scwx/qt/types/radar_product_record.hpp
source/scwx/qt/types/text_event_key.hpp)
set(SRC_TYPES source/scwx/qt/types/alert_types.cpp
source/scwx/qt/types/github_types.cpp
source/scwx/qt/types/map_types.cpp
source/scwx/qt/types/radar_product_record.cpp
source/scwx/qt/types/text_event_key.cpp)
set(HDR_UI source/scwx/qt/ui/about_dialog.hpp
source/scwx/qt/ui/alert_dialog.hpp
source/scwx/qt/ui/alert_dock_widget.hpp
source/scwx/qt/ui/animation_dock_widget.hpp
source/scwx/qt/ui/flow_layout.hpp
source/scwx/qt/ui/imgui_debug_dialog.hpp
source/scwx/qt/ui/imgui_debug_widget.hpp
@ -154,6 +159,7 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp
set(SRC_UI source/scwx/qt/ui/about_dialog.cpp
source/scwx/qt/ui/alert_dialog.cpp
source/scwx/qt/ui/alert_dock_widget.cpp
source/scwx/qt/ui/animation_dock_widget.cpp
source/scwx/qt/ui/flow_layout.cpp
source/scwx/qt/ui/imgui_debug_dialog.cpp
source/scwx/qt/ui/imgui_debug_widget.cpp
@ -166,6 +172,7 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp
set(UI_UI source/scwx/qt/ui/about_dialog.ui
source/scwx/qt/ui/alert_dialog.ui
source/scwx/qt/ui/alert_dock_widget.ui
source/scwx/qt/ui/animation_dock_widget.ui
source/scwx/qt/ui/imgui_debug_dialog.ui
source/scwx/qt/ui/radar_site_dialog.ui
source/scwx/qt/ui/settings_dialog.ui
@ -179,7 +186,8 @@ set(HDR_UTIL source/scwx/qt/util/color.hpp
source/scwx/qt/util/streams.hpp
source/scwx/qt/util/texture_atlas.hpp
source/scwx/qt/util/q_file_buffer.hpp
source/scwx/qt/util/q_file_input_stream.hpp)
source/scwx/qt/util/q_file_input_stream.hpp
source/scwx/qt/util/time.hpp)
set(SRC_UTIL source/scwx/qt/util/color.cpp
source/scwx/qt/util/file.cpp
source/scwx/qt/util/font.cpp
@ -188,7 +196,8 @@ set(SRC_UTIL source/scwx/qt/util/color.cpp
source/scwx/qt/util/json.cpp
source/scwx/qt/util/texture_atlas.cpp
source/scwx/qt/util/q_file_buffer.cpp
source/scwx/qt/util/q_file_input_stream.cpp)
source/scwx/qt/util/q_file_input_stream.cpp
source/scwx/qt/util/time.cpp)
set(HDR_VIEW source/scwx/qt/view/level2_product_view.hpp
source/scwx/qt/view/level3_product_view.hpp
source/scwx/qt/view/level3_radial_view.hpp
@ -379,6 +388,15 @@ target_compile_options(supercell-wx PRIVATE
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall -Wextra -Wpedantic -Werror>
)
if (MSVC)
# Produce PDB file for debug
target_compile_options(scwx-qt PRIVATE "$<$<CONFIG:Release>:/Zi>")
target_compile_options(supercell-wx PRIVATE "$<$<CONFIG:Release>:/Zi>")
target_link_options(supercell-wx PRIVATE "$<$<CONFIG:Release>:/DEBUG>")
target_link_options(supercell-wx PRIVATE "$<$<CONFIG:Release>:/OPT:REF>")
target_link_options(supercell-wx PRIVATE "$<$<CONFIG:Release>:/OPT:ICF>")
endif()
target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::OpenGLWidgets
Boost::json

View file

@ -24,6 +24,8 @@
<file>res/icons/font-awesome-6/gears-solid.svg</file>
<file>res/icons/font-awesome-6/github.svg</file>
<file>res/icons/font-awesome-6/palette-solid.svg</file>
<file>res/icons/font-awesome-6/pause-solid.svg</file>
<file>res/icons/font-awesome-6/play-solid.svg</file>
<file>res/icons/font-awesome-6/rotate-left-solid.svg</file>
<file>res/icons/font-awesome-6/sliders-solid.svg</file>
<file>res/icons/font-awesome-6/square-minus-regular.svg</file>

View file

@ -8,6 +8,7 @@
#include <scwx/qt/manager/radar_product_manager.hpp>
#include <scwx/qt/manager/settings_manager.hpp>
#include <scwx/qt/manager/text_event_manager.hpp>
#include <scwx/qt/manager/timeline_manager.hpp>
#include <scwx/qt/manager/update_manager.hpp>
#include <scwx/qt/map/map_provider.hpp>
#include <scwx/qt/map/map_widget.hpp>
@ -15,6 +16,7 @@
#include <scwx/qt/ui/alert_dock_widget.hpp>
#include <scwx/qt/ui/flow_layout.hpp>
#include <scwx/qt/ui/about_dialog.hpp>
#include <scwx/qt/ui/animation_dock_widget.hpp>
#include <scwx/qt/ui/imgui_debug_dialog.hpp>
#include <scwx/qt/ui/level2_products_widget.hpp>
#include <scwx/qt/ui/level2_settings_widget.hpp>
@ -62,6 +64,7 @@ public:
level2SettingsWidget_ {nullptr},
level3ProductsWidget_ {nullptr},
alertDockWidget_ {nullptr},
animationDockWidget_ {nullptr},
aboutDialog_ {nullptr},
imGuiDebugDialog_ {nullptr},
radarSiteDialog_ {nullptr},
@ -69,6 +72,7 @@ public:
updateDialog_ {nullptr},
radarProductModel_ {nullptr},
textEventManager_ {manager::TextEventManager::Instance()},
timelineManager_ {manager::TimelineManager::Instance()},
updateManager_ {manager::UpdateManager::Instance()},
maps_ {},
elevationCuts_ {},
@ -109,6 +113,7 @@ public:
void AsyncSetup();
void ConfigureMapLayout();
void ConnectAnimationSignals();
void ConnectMapSignals();
void ConnectOtherSignals();
void HandleFocusChange(QWidget* focused);
@ -139,6 +144,7 @@ public:
ui::Level3ProductsWidget* level3ProductsWidget_;
ui::AlertDockWidget* alertDockWidget_;
ui::AnimationDockWidget* animationDockWidget_;
ui::AboutDialog* aboutDialog_;
ui::ImGuiDebugDialog* imGuiDebugDialog_;
ui::RadarSiteDialog* radarSiteDialog_;
@ -147,6 +153,7 @@ public:
std::unique_ptr<model::RadarProductModel> radarProductModel_;
std::shared_ptr<manager::TextEventManager> textEventManager_;
std::shared_ptr<manager::TimelineManager> timelineManager_;
std::shared_ptr<manager::UpdateManager> updateManager_;
std::vector<map::MapWidget*> maps_;
@ -182,12 +189,23 @@ MainWindow::MainWindow(QWidget* parent) :
p->alertDockWidget_->setVisible(false);
addDockWidget(Qt::BottomDockWidgetArea, p->alertDockWidget_);
// Animation Dock Widget
p->animationDockWidget_ = new ui::AnimationDockWidget(this);
p->animationDockWidget_->setVisible(true);
addDockWidget(Qt::LeftDockWidgetArea, p->animationDockWidget_);
// Configure Menu
ui->menuView->insertAction(ui->actionRadarToolbox,
ui->radarToolboxDock->toggleViewAction());
ui->radarToolboxDock->toggleViewAction()->setText(tr("Radar &Toolbox"));
ui->actionRadarToolbox->setVisible(false);
ui->menuView->insertAction(ui->actionAnimationToolbox,
p->animationDockWidget_->toggleViewAction());
p->animationDockWidget_->toggleViewAction()->setText(
tr("A&nimation Toolbox"));
ui->actionAnimationToolbox->setVisible(false);
ui->menuView->insertAction(ui->actionResourceExplorer,
ui->resourceExplorerDock->toggleViewAction());
ui->resourceExplorerDock->toggleViewAction()->setText(
@ -257,6 +275,7 @@ MainWindow::MainWindow(QWidget* parent) :
p->PopulateMapStyles();
p->ConnectMapSignals();
p->ConnectAnimationSignals();
p->ConnectOtherSignals();
p->HandleFocusChange(p->activeMap_);
p->AsyncSetup();
@ -273,7 +292,7 @@ void MainWindow::showEvent(QShowEvent* event)
{
QMainWindow::showEvent(event);
resizeDocks({ui->radarToolboxDock}, {150}, Qt::Horizontal);
resizeDocks({ui->radarToolboxDock}, {188}, Qt::Horizontal);
}
void MainWindow::on_actionOpenNexrad_triggered()
@ -634,6 +653,75 @@ void MainWindowImpl::ConnectMapSignals()
}
}
void MainWindowImpl::ConnectAnimationSignals()
{
connect(animationDockWidget_,
&ui::AnimationDockWidget::DateTimeChanged,
timelineManager_.get(),
&manager::TimelineManager::SetDateTime);
connect(animationDockWidget_,
&ui::AnimationDockWidget::ViewTypeChanged,
timelineManager_.get(),
&manager::TimelineManager::SetViewType);
connect(animationDockWidget_,
&ui::AnimationDockWidget::LoopTimeChanged,
timelineManager_.get(),
&manager::TimelineManager::SetLoopTime);
connect(animationDockWidget_,
&ui::AnimationDockWidget::LoopSpeedChanged,
timelineManager_.get(),
&manager::TimelineManager::SetLoopSpeed);
connect(animationDockWidget_,
&ui::AnimationDockWidget::AnimationStepBeginSelected,
timelineManager_.get(),
&manager::TimelineManager::AnimationStepBegin);
connect(animationDockWidget_,
&ui::AnimationDockWidget::AnimationStepBackSelected,
timelineManager_.get(),
&manager::TimelineManager::AnimationStepBack);
connect(animationDockWidget_,
&ui::AnimationDockWidget::AnimationPlaySelected,
timelineManager_.get(),
&manager::TimelineManager::AnimationPlayPause);
connect(animationDockWidget_,
&ui::AnimationDockWidget::AnimationStepNextSelected,
timelineManager_.get(),
&manager::TimelineManager::AnimationStepNext);
connect(animationDockWidget_,
&ui::AnimationDockWidget::AnimationStepEndSelected,
timelineManager_.get(),
&manager::TimelineManager::AnimationStepEnd);
connect(timelineManager_.get(),
&manager::TimelineManager::VolumeTimeUpdated,
[this](std::chrono::system_clock::time_point dateTime)
{
for (auto map : maps_)
{
map->SelectTime(dateTime);
}
});
connect(timelineManager_.get(),
&manager::TimelineManager::AnimationStateUpdated,
animationDockWidget_,
&ui::AnimationDockWidget::UpdateAnimationState);
connect(timelineManager_.get(),
&manager::TimelineManager::LiveStateUpdated,
animationDockWidget_,
&ui::AnimationDockWidget::UpdateLiveState);
connect(timelineManager_.get(),
&manager::TimelineManager::LiveStateUpdated,
[this](bool isLive)
{
for (auto map : maps_)
{
map->SetAutoUpdate(isLive);
}
});
}
void MainWindowImpl::ConnectOtherSignals()
{
connect(qApp,
@ -761,7 +849,8 @@ void MainWindowImpl::SelectRadarProduct(map::MapWidget* mapWidget,
UpdateRadarProductSettings();
}
mapWidget->SelectRadarProduct(group, productName, productCode);
mapWidget->SelectRadarProduct(
group, productName, productCode, mapWidget->GetSelectedTime());
}
void MainWindowImpl::SetActiveMap(map::MapWidget* mapWidget)
@ -841,11 +930,15 @@ void MainWindowImpl::UpdateRadarSite()
mainWindow_->ui->radarSiteValueLabel->setText(radarSite->id().c_str());
mainWindow_->ui->radarLocationLabel->setText(
radarSite->location_name().c_str());
timelineManager_->SetRadarSite(radarSite->id());
}
else
{
mainWindow_->ui->radarSiteValueLabel->setVisible(false);
mainWindow_->ui->radarLocationLabel->setVisible(false);
timelineManager_->SetRadarSite("?");
}
}

View file

@ -76,6 +76,7 @@
<string>&amp;View</string>
</property>
<addaction name="actionRadarToolbox"/>
<addaction name="actionAnimationToolbox"/>
<addaction name="actionResourceExplorer"/>
<addaction name="actionAlerts"/>
</widget>
@ -359,7 +360,7 @@
</action>
<action name="actionRadarToolbox">
<property name="text">
<string>Radar Toolbox</string>
<string>Radar &amp;Toolbox</string>
</property>
</action>
<action name="actionResourceExplorer">
@ -429,6 +430,11 @@
<string>&amp;Check for Updates</string>
</property>
</action>
<action name="actionAnimationToolbox">
<property name="text">
<string>A&amp;nimation Toolbox</string>
</property>
</action>
</widget>
<resources>
<include location="../../../../scwx-qt.qrc"/>

View file

@ -13,6 +13,7 @@
#include <execution>
#include <mutex>
#include <shared_mutex>
#include <unordered_set>
#if defined(_MSC_VER)
# pragma warning(push, 0)
@ -204,6 +205,7 @@ public:
std::shared_mutex& recordMutex,
std::mutex& loadDataMutex,
std::shared_ptr<request::NexradFileRequest> request);
void PopulateLevel2ProductTimes(std::chrono::system_clock::time_point time);
static void
LoadNexradFile(CreateNexradFileFunction load,
@ -215,6 +217,7 @@ public:
bool level3ProductsInitialized_;
std::shared_ptr<config::RadarSite> radarSite_;
std::size_t cacheLimit_ {6u};
std::vector<float> coordinates0_5Degree_;
std::vector<float> coordinates1Degree_;
@ -245,7 +248,7 @@ public:
std::shared_ptr<ProviderManager>,
boost::hash<boost::uuids::uuid>>
refreshMap_ {};
std::mutex refreshMapMutex_ {};
std::shared_mutex refreshMapMutex_ {};
};
RadarProductManager::RadarProductManager(const std::string& radarId) :
@ -676,6 +679,79 @@ void RadarProductManagerImpl::RefreshData(
});
}
std::set<std::chrono::system_clock::time_point>
RadarProductManager::GetActiveVolumeTimes(
std::chrono::system_clock::time_point time)
{
std::unordered_set<std::shared_ptr<provider::NexradDataProvider>>
providers {};
std::set<std::chrono::system_clock::time_point> volumeTimes {};
std::mutex volumeTimesMutex {};
// Return a default set of volume times if the default time point is given
if (time == std::chrono::system_clock::time_point {})
{
return volumeTimes;
}
// Lock the refresh map
std::shared_lock refreshLock {p->refreshMapMutex_};
// For each entry in the refresh map (refresh is enabled)
for (auto& refreshEntry : p->refreshMap_)
{
// Add the provider for the current entry
providers.insert(refreshEntry.second->provider_);
}
// Unlock the refresh map
refreshLock.unlock();
const auto today = std::chrono::floor<std::chrono::days>(time);
const auto yesterday = today - std::chrono::days {1};
const auto tomorrow = today + std::chrono::days {1};
const auto dates = {yesterday, today, tomorrow};
// For each provider (in parallel)
std::for_each(
std::execution::par_unseq,
providers.begin(),
providers.end(),
[&](const std::shared_ptr<provider::NexradDataProvider>& provider)
{
// For yesterday, today and tomorrow (in parallel)
std::for_each(std::execution::par_unseq,
dates.begin(),
dates.end(),
[&](const auto& date)
{
// Don't query for a time point in the future
if (date > std::chrono::system_clock::now())
{
return;
}
// Query the provider for volume time points
auto timePoints = provider->GetTimePointsByDate(date);
// TODO: Note, this will miss volume times present in
// Level 2 products with a second scan
// Lock the merged volume time list
std::unique_lock volumeTimesLock {volumeTimesMutex};
// Copy time points to the merged list
std::copy(
timePoints.begin(),
timePoints.end(),
std::inserter(volumeTimes, volumeTimes.end()));
});
});
// Return merged volume times list
return volumeTimes;
}
void RadarProductManagerImpl::LoadProviderData(
std::chrono::system_clock::time_point time,
std::shared_ptr<ProviderManager> providerManager,
@ -869,6 +945,59 @@ void RadarProductManagerImpl::LoadNexradFile(
});
}
void RadarProductManagerImpl::PopulateLevel2ProductTimes(
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};
const auto tomorrow = today + std::chrono::days {1};
const auto dates = {yesterday, today, tomorrow};
std::set<std::chrono::system_clock::time_point> volumeTimes {};
std::mutex volumeTimesMutex {};
// For yesterday, today and tomorrow (in parallel)
std::for_each(std::execution::par_unseq,
dates.begin(),
dates.end(),
[&, this](const auto& date)
{
// Don't query for a time point in the future
if (date > std::chrono::system_clock::now())
{
return;
}
// Query the provider for volume time points
auto timePoints =
level2ProviderManager_->provider_->GetTimePointsByDate(
date);
// Lock the merged volume time list
std::unique_lock volumeTimesLock {volumeTimesMutex};
// Copy time points to the merged list
std::copy(timePoints.begin(),
timePoints.end(),
std::inserter(volumeTimes, volumeTimes.end()));
});
// Lock the level 2 product record map
std::unique_lock lock {level2ProductRecordMutex_};
// Merge volume times into map
std::transform(
volumeTimes.cbegin(),
volumeTimes.cend(),
std::inserter(level2ProductRecords_, level2ProductRecords_.begin()),
[](const std::chrono::system_clock::time_point& time)
{
return std::pair<std::chrono::system_clock::time_point,
std::weak_ptr<types::RadarProductRecord>>(
time, std::weak_ptr<types::RadarProductRecord> {});
});
}
std::tuple<std::shared_ptr<types::RadarProductRecord>,
std::chrono::system_clock::time_point>
RadarProductManagerImpl::GetLevel2ProductRecord(
@ -878,6 +1007,9 @@ RadarProductManagerImpl::GetLevel2ProductRecord(
RadarProductRecordMap::const_pointer recordPtr {nullptr};
std::chrono::system_clock::time_point recordTime {time};
// Ensure Level 2 product records are updated
PopulateLevel2ProductTimes(time);
if (!level2ProductRecords_.empty() &&
time == std::chrono::system_clock::time_point {})
{
@ -892,13 +1024,10 @@ RadarProductManagerImpl::GetLevel2ProductRecord(
if (recordPtr != nullptr)
{
if (time == std::chrono::system_clock::time_point {} ||
time == recordPtr->first)
{
// Don't check for an exact time match for level 2 products
recordTime = recordPtr->first;
record = recordPtr->second.lock();
}
}
if (record == nullptr &&
recordTime != std::chrono::system_clock::time_point {})
@ -938,7 +1067,7 @@ RadarProductManagerImpl::GetLevel3ProductRecord(
auto it = level3ProductRecordsMap_.find(product);
if (it != level3ProductRecordsMap_.cend())
if (it != level3ProductRecordsMap_.cend() && !it->second.empty())
{
if (time == std::chrono::system_clock::time_point {})
{
@ -1061,7 +1190,7 @@ void RadarProductManagerImpl::UpdateRecentRecords(
RadarProductRecordList& recentList,
std::shared_ptr<types::RadarProductRecord> record)
{
static constexpr std::size_t kRecentListMaxSize_ {2u};
const std::size_t recentListMaxSize {cacheLimit_};
auto it = std::find(recentList.cbegin(), recentList.cend(), record);
if (it != recentList.cbegin() && it != recentList.cend())
@ -1076,7 +1205,7 @@ void RadarProductManagerImpl::UpdateRecentRecords(
recentList.push_front(record);
}
while (recentList.size() > kRecentListMaxSize_)
while (recentList.size() > recentListMaxSize)
{
// Remove from the end of the list while it's too big
recentList.pop_back();
@ -1141,6 +1270,11 @@ std::vector<std::string> RadarProductManager::GetLevel3Products()
return level3ProviderManager->provider_->GetAvailableProducts();
}
void RadarProductManager::SetCacheLimit(size_t cacheLimit)
{
p->cacheLimit_ = cacheLimit;
}
void RadarProductManager::UpdateAvailableProducts()
{
std::lock_guard<std::mutex> guard(p->level3ProductsInitializeMutex_);

View file

@ -9,6 +9,7 @@
#include <scwx/wsr88d/level3_file.hpp>
#include <memory>
#include <set>
#include <unordered_map>
#include <vector>
@ -63,6 +64,17 @@ public:
bool enabled,
boost::uuids::uuid uuid = boost::uuids::nil_uuid());
/**
* @brief Gets a merged list of the volume times for products with refresh
* enabled. The volume times will be for the previous, current and next day.
*
* @param [in] time Current date to provide to volume time query
*
* @return Merged list of active volume times
*/
std::set<std::chrono::system_clock::time_point>
GetActiveVolumeTimes(std::chrono::system_clock::time_point time);
/**
* @brief Get level 2 radar data for a data block type, elevation, and time.
*
@ -114,6 +126,14 @@ public:
common::Level3ProductCategoryMap GetAvailableLevel3Categories();
std::vector<std::string> GetLevel3Products();
/**
* @brief Set the maximum number of products of each type that may be cached.
*
* @param [in] cacheLimit The maximum number of products of each type
*/
void SetCacheLimit(std::size_t cacheLimit);
void UpdateAvailableProducts();
signals:

View file

@ -0,0 +1,541 @@
#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;
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;
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");
emit self_->LiveStateUpdated(true);
emit self_->VolumeTimeUpdated(selectedTime);
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
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_));
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;
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_));
emit self_->LiveStateUpdated(false);
emit self_->VolumeTimeUpdated(adjustedTime_);
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_));
emit self_->LiveStateUpdated(false);
emit self_->VolumeTimeUpdated(adjustedTime_);
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

View file

@ -0,0 +1,57 @@
#pragma once
#include <scwx/qt/types/map_types.hpp>
#include <chrono>
#include <memory>
#include <QObject>
namespace scwx
{
namespace qt
{
namespace manager
{
class TimelineManager : public QObject
{
Q_OBJECT
public:
explicit TimelineManager();
~TimelineManager();
static std::shared_ptr<TimelineManager> Instance();
public slots:
void SetRadarSite(const std::string& radarSite);
void SetDateTime(std::chrono::system_clock::time_point dateTime);
void SetViewType(types::MapTime viewType);
void SetLoopTime(std::chrono::minutes loopTime);
void SetLoopSpeed(double loopSpeed);
void AnimationStepBegin();
void AnimationStepBack();
void AnimationPlayPause();
void AnimationStepNext();
void AnimationStepEnd();
signals:
void SelectedTimeUpdated(std::chrono::system_clock::time_point dateTime);
void VolumeTimeUpdated(std::chrono::system_clock::time_point dateTime);
void AnimationStateUpdated(types::AnimationState state);
void LiveStateUpdated(bool isLive);
void ViewTypeUpdated(types::MapTime viewType);
private:
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace manager
} // namespace qt
} // namespace scwx

View file

@ -62,8 +62,8 @@ public:
overlayLayer_ {nullptr},
colorTableLayer_ {nullptr},
autoRefreshEnabled_ {true},
autoUpdateEnabled_ {true},
selectedLevel2Product_ {common::Level2Product::Unknown},
selectedTime_ {},
lastPos_(),
currentStyleIndex_ {0},
currentStyle_ {nullptr},
@ -147,9 +147,9 @@ public:
std::shared_ptr<ColorTableLayer> colorTableLayer_;
bool autoRefreshEnabled_;
bool autoUpdateEnabled_;
common::Level2Product selectedLevel2Product_;
std::chrono::system_clock::time_point selectedTime_;
QPointF lastPos_;
std::size_t currentStyleIndex_;
@ -316,6 +316,21 @@ std::shared_ptr<config::RadarSite> MapWidget::GetRadarSite() const
return radarSite;
}
std::chrono::system_clock::time_point MapWidget::GetSelectedTime() const
{
auto radarProductView = p->context_->radar_product_view();
std::chrono::system_clock::time_point time;
// If there is an active radar product view
if (radarProductView != nullptr)
{
// Select the time associated with the active radar product
time = radarProductView->GetSelectedTime();
}
return time;
}
std::uint16_t MapWidget::GetVcp() const
{
auto radarProductView = p->context_->radar_product_view();
@ -434,9 +449,8 @@ void MapWidget::SelectRadarProduct(
scwx::util::TimeString(time));
p->SetRadarSite(radarId);
p->selectedTime_ = time;
SelectRadarProduct(group, product, productCode);
SelectRadarProduct(group, product, productCode, time);
}
void MapWidget::SelectRadarSite(const std::string& id, bool updateCoordinates)
@ -478,6 +492,23 @@ void MapWidget::SelectRadarSite(std::shared_ptr<config::RadarSite> radarSite,
AddLayers();
// TODO: Disable refresh from old site
emit RadarSiteUpdated(radarSite);
}
}
void MapWidget::SelectTime(std::chrono::system_clock::time_point time)
{
auto radarProductView = p->context_->radar_product_view();
// If there is an active radar product view
if (radarProductView != nullptr)
{
// Select the time associated with the active radar product
radarProductView->SelectTime(time);
// Trigger an update of the radar product view
radarProductView->Update();
}
}
@ -506,6 +537,11 @@ void MapWidget::SetAutoRefresh(bool enabled)
}
}
void MapWidget::SetAutoUpdate(bool enabled)
{
p->autoUpdateEnabled_ = enabled;
}
void MapWidget::SetMapLocation(double latitude,
double longitude,
bool updateRadarSite)
@ -865,6 +901,8 @@ void MapWidgetImpl::RadarProductManagerConnect()
std::make_shared<request::NexradFileRequest>();
// File request callback
if (autoUpdateEnabled_)
{
connect(
request.get(),
&request::NexradFileRequest::RequestComplete,
@ -879,6 +917,7 @@ void MapWidgetImpl::RadarProductManagerConnect()
widget_->SelectRadarProduct(record);
}
});
}
// Load file
scwx::util::async(

View file

@ -42,6 +42,7 @@ public:
common::RadarProductGroup GetRadarProductGroup() const;
std::string GetRadarProductName() const;
std::shared_ptr<config::RadarSite> GetRadarSite() const;
std::chrono::system_clock::time_point GetSelectedTime() const;
std::uint16_t GetVcp() const;
void SelectElevation(float elevation);
@ -80,8 +81,16 @@ public:
void SelectRadarSite(std::shared_ptr<config::RadarSite> radarSite,
bool updateCoordinates = true);
/**
* @brief Selects the time associated with the active radar product.
*
* @param [in] time Product time
*/
void SelectTime(std::chrono::system_clock::time_point time);
void SetActive(bool isActive);
void SetAutoRefresh(bool enabled);
void SetAutoUpdate(bool enabled);
/**
* @brief Sets the current map location.
@ -132,6 +141,7 @@ signals:
double bearing,
double pitch);
void MapStyleChanged(const std::string& styleName);
void RadarSiteUpdated(std::shared_ptr<config::RadarSite> radarSite);
void RadarSweepUpdated();
};

View file

@ -421,8 +421,7 @@ AlertModelImpl::GetStartTime(const types::TextEventKey& key)
if (messageList.size() > 0)
{
auto& firstMessage = messageList.front();
auto firstSegment = firstMessage->segment(0);
return firstSegment->header_->vtecString_[0].pVtec_.event_begin();
return firstMessage->segment_event_begin(0);
}
else
{

View file

@ -0,0 +1,22 @@
#include <scwx/qt/types/map_types.hpp>
#include <unordered_map>
namespace scwx
{
namespace qt
{
namespace types
{
static const std::unordered_map<MapTime, std::string> mapTimeName_ {
{MapTime::Live, "Live"}, {MapTime::Archive, "Archive"}};
std::string GetMapTimeName(MapTime mapTime)
{
return mapTimeName_.at(mapTime);
}
} // namespace types
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,28 @@
#pragma once
#include <string>
namespace scwx
{
namespace qt
{
namespace types
{
enum class AnimationState
{
Play,
Pause
};
enum class MapTime
{
Live,
Archive
};
std::string GetMapTimeName(MapTime mapTime);
} // namespace types
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,219 @@
#include "animation_dock_widget.hpp"
#include "ui_animation_dock_widget.h"
#include <scwx/qt/util/time.hpp>
#include <scwx/util/logger.hpp>
#include <QTimer>
namespace scwx
{
namespace qt
{
namespace ui
{
static const std::string logPrefix_ = "scwx::qt::ui::animation_dock_widget";
static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
class AnimationDockWidgetImpl
{
public:
explicit AnimationDockWidgetImpl(AnimationDockWidget* self) : self_ {self} {}
~AnimationDockWidgetImpl() = default;
const QIcon kPauseIcon_ {":/res/icons/font-awesome-6/pause-solid.svg"};
const QIcon kPlayIcon_ {":/res/icons/font-awesome-6/play-solid.svg"};
AnimationDockWidget* self_;
types::AnimationState animationState_ {types::AnimationState::Pause};
std::chrono::sys_days selectedDate_ {};
std::chrono::seconds selectedTime_ {};
void ConnectSignals();
};
AnimationDockWidget::AnimationDockWidget(QWidget* parent) :
QDockWidget(parent),
p {std::make_unique<AnimationDockWidgetImpl>(this)},
ui(new Ui::AnimationDockWidget)
{
ui->setupUi(this);
// Set date/time edit enabled/disabled
ui->dateEdit->setEnabled(ui->archiveViewRadioButton->isChecked());
ui->timeEdit->setEnabled(ui->archiveViewRadioButton->isChecked());
// Update date/time edit enabled/disabled based on Archive View radio button
connect(ui->archiveViewRadioButton,
&QRadioButton::toggled,
this,
[this](bool checked)
{
ui->dateEdit->setEnabled(checked);
ui->timeEdit->setEnabled(checked);
});
// Set current date/time
QDateTime currentDateTime = QDateTime::currentDateTimeUtc();
QDate currentDate = currentDateTime.date();
QTime currentTime = currentDateTime.time();
ui->dateEdit->setDate(currentDate);
ui->timeEdit->setTime(currentTime);
ui->dateEdit->setMaximumDate(currentDateTime.date());
p->selectedDate_ = util::SysDays(currentDate);
p->selectedTime_ =
std::chrono::seconds(currentTime.msecsSinceStartOfDay() / 1000);
// Update maximum date on a timer
QTimer* maxDateTimer = new QTimer(this);
connect(maxDateTimer,
&QTimer::timeout,
this,
[this]()
{
// Update maximum date to today
QDate currentDate = QDateTime::currentDateTimeUtc().date();
if (ui->dateEdit->maximumDate() != currentDate)
{
ui->dateEdit->setMaximumDate(currentDate);
}
});
// Evaluate every 15 seconds, every second is unnecessary
maxDateTimer->start(15000);
// Set loop defaults
ui->loopTimeSpinBox->setValue(30);
ui->loopSpeedSpinBox->setValue(5.0);
// Connect widget signals
p->ConnectSignals();
}
AnimationDockWidget::~AnimationDockWidget()
{
delete ui;
}
void AnimationDockWidgetImpl::ConnectSignals()
{
// View type
QObject::connect(self_->ui->liveViewRadioButton,
&QRadioButton::toggled,
self_,
[this](bool checked)
{
if (checked)
{
emit self_->ViewTypeChanged(types::MapTime::Live);
}
});
QObject::connect(self_->ui->archiveViewRadioButton,
&QRadioButton::toggled,
self_,
[this](bool checked)
{
if (checked)
{
emit self_->ViewTypeChanged(types::MapTime::Archive);
}
});
// Date/time controls
QObject::connect( //
self_->ui->dateEdit,
&QDateTimeEdit::dateChanged,
self_,
[this](QDate date)
{
if (date.isValid())
{
selectedDate_ = util::SysDays(date);
emit self_->DateTimeChanged(selectedDate_ + selectedTime_);
}
});
QObject::connect(
self_->ui->timeEdit,
&QDateTimeEdit::timeChanged,
self_,
[this](QTime time)
{
if (time.isValid())
{
selectedTime_ =
std::chrono::seconds(time.msecsSinceStartOfDay() / 1000);
emit self_->DateTimeChanged(selectedDate_ + selectedTime_);
}
});
// Loop controls
QObject::connect(self_->ui->loopTimeSpinBox,
&QSpinBox::valueChanged,
self_,
[this](int i)
{ emit self_->LoopTimeChanged(std::chrono::minutes(i)); });
QObject::connect(self_->ui->loopSpeedSpinBox,
&QDoubleSpinBox::valueChanged,
self_,
[this](double d) { emit self_->LoopSpeedChanged(d); });
// Animation controls
QObject::connect(self_->ui->beginButton,
&QAbstractButton::clicked,
self_,
[this]() { emit self_->AnimationStepBeginSelected(); });
QObject::connect(self_->ui->stepBackButton,
&QAbstractButton::clicked,
self_,
[this]() { emit self_->AnimationStepBackSelected(); });
QObject::connect(self_->ui->playButton,
&QAbstractButton::clicked,
self_,
[this]() { emit self_->AnimationPlaySelected(); });
QObject::connect(self_->ui->stepNextButton,
&QAbstractButton::clicked,
self_,
[this]() { emit self_->AnimationStepNextSelected(); });
QObject::connect(self_->ui->endButton,
&QAbstractButton::clicked,
self_,
[this]() { emit self_->AnimationStepEndSelected(); });
}
void AnimationDockWidget::UpdateAnimationState(types::AnimationState state)
{
// Update icon to opposite of state
switch (state)
{
case types::AnimationState::Pause:
ui->playButton->setIcon(p->kPlayIcon_);
break;
case types::AnimationState::Play:
ui->playButton->setIcon(p->kPauseIcon_);
break;
}
}
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)
{
ui->autoUpdateLabel->setText(QString("%1: %2").arg(prefix).arg(enabled));
}
else
{
ui->autoUpdateLabel->setText(QString("%1: %2").arg(prefix).arg(disabled));
}
}
} // namespace ui
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,56 @@
#pragma once
#include <scwx/qt/types/map_types.hpp>
#include <chrono>
#include <QDockWidget>
namespace Ui
{
class AnimationDockWidget;
}
namespace scwx
{
namespace qt
{
namespace ui
{
class AnimationDockWidgetImpl;
class AnimationDockWidget : public QDockWidget
{
Q_OBJECT
public:
explicit AnimationDockWidget(QWidget* parent = nullptr);
~AnimationDockWidget();
public slots:
void UpdateAnimationState(types::AnimationState state);
void UpdateLiveState(bool isLive);
signals:
void ViewTypeChanged(types::MapTime viewType);
void DateTimeChanged(std::chrono::system_clock::time_point dateTime);
void LoopTimeChanged(std::chrono::minutes loopTime);
void LoopSpeedChanged(double loopSpeed);
void AnimationStepBeginSelected();
void AnimationStepBackSelected();
void AnimationPlaySelected();
void AnimationStepNextSelected();
void AnimationStepEndSelected();
private:
friend class AnimationDockWidgetImpl;
std::unique_ptr<AnimationDockWidgetImpl> p;
Ui::AnimationDockWidget* ui;
};
} // namespace ui
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,290 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AnimationDockWidget</class>
<widget class="QDockWidget" name="AnimationDockWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>200</width>
<height>337</height>
</rect>
</property>
<property name="windowTitle">
<string>Animation Toolbox</string>
</property>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="timelineGroupBox">
<property name="title">
<string>Timeline</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QLabel" name="autoUpdateLabel">
<property name="text">
<string>Auto Update: Enabled</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="liveViewRadioButton">
<property name="text">
<string>Live View</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="archiveViewRadioButton">
<property name="text">
<string>Archive View</string>
</property>
</widget>
</item>
<item>
<widget class="QDateEdit" name="dateEdit">
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectToNearestValue</enum>
</property>
<property name="minimumDateTime">
<datetime>
<hour>0</hour>
<minute>0</minute>
<second>0</second>
<year>1991</year>
<month>6</month>
<day>1</day>
</datetime>
</property>
<property name="displayFormat">
<string>yyyy-MM-dd</string>
</property>
<property name="calendarPopup">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<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="QTimeEdit" name="timeEdit">
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectToNearestValue</enum>
</property>
<property name="displayFormat">
<string>HH:mm</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>UTC</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_3">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QGridLayout" name="gridLayout">
<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 row="0" column="0">
<widget class="QLabel" name="loopTimeLabel">
<property name="text">
<string>Loop Time</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="loopTimeSpinBox">
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectToNearestValue</enum>
</property>
<property name="suffix">
<string> min</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>1440</number>
</property>
<property name="value">
<number>30</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="loopSpeedLabel">
<property name="text">
<string>Loop Speed</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="loopSpeedSpinBox">
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectToNearestValue</enum>
</property>
<property name="suffix">
<string>x</string>
</property>
<property name="minimum">
<double>1.000000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_2">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>1</number>
</property>
<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="QToolButton" name="beginButton">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../scwx-qt.qrc">
<normaloff>:/res/icons/font-awesome-6/backward-step-solid.svg</normaloff>:/res/icons/font-awesome-6/backward-step-solid.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="stepBackButton">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../scwx-qt.qrc">
<normaloff>:/res/icons/font-awesome-6/angle-left-solid.svg</normaloff>:/res/icons/font-awesome-6/angle-left-solid.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="playButton">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../scwx-qt.qrc">
<normaloff>:/res/icons/font-awesome-6/play-solid.svg</normaloff>:/res/icons/font-awesome-6/play-solid.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="stepNextButton">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../scwx-qt.qrc">
<normaloff>:/res/icons/font-awesome-6/angle-right-solid.svg</normaloff>:/res/icons/font-awesome-6/angle-right-solid.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="endButton">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../scwx-qt.qrc">
<normaloff>:/res/icons/font-awesome-6/forward-step-solid.svg</normaloff>:/res/icons/font-awesome-6/forward-step-solid.svg</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
<resources>
<include location="../../../../scwx-qt.qrc"/>
</resources>
<connections/>
</ui>

View file

@ -0,0 +1,24 @@
#include <scwx/qt/util/time.hpp>
namespace scwx
{
namespace qt
{
namespace util
{
std::chrono::sys_days SysDays(const QDate& date)
{
using namespace std::chrono;
using sys_days = time_point<system_clock, days>;
constexpr auto julianEpoch = sys_days {-4713y / November / 24d};
constexpr auto unixEpoch = sys_days {1970y / January / 1d};
constexpr auto offset = std::chrono::days(julianEpoch - unixEpoch);
return std::chrono::sys_days(std::chrono::days(date.toJulianDay()) +
julianEpoch);
}
} // namespace util
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,25 @@
#pragma once
#include <chrono>
#include <QDateTime>
namespace scwx
{
namespace qt
{
namespace util
{
/**
* @brief Convert QDate to std::chrono::sys_days.
*
* @param [in] date Date to convert
*
* @return Days
*/
std::chrono::sys_days SysDays(const QDate& date);
} // namespace util
} // namespace qt
} // namespace scwx

View file

@ -108,7 +108,11 @@ Level2ProductView::Level2ProductView(
{
ConnectRadarProductManager();
}
Level2ProductView::~Level2ProductView() = default;
Level2ProductView::~Level2ProductView()
{
std::unique_lock sweepLock {sweep_mutex()};
}
void Level2ProductView::ConnectRadarProductManager()
{
@ -119,7 +123,8 @@ void Level2ProductView::ConnectRadarProductManager()
{
if (record->radar_product_group() ==
common::RadarProductGroup::Level2 &&
record->time() == selected_time())
std::chrono::floor<std::chrono::seconds>(record->time()) ==
selected_time())
{
// If the data associated with the currently selected time is
// reloaded, update the view
@ -290,11 +295,6 @@ void Level2ProductViewImpl::SetProduct(common::Level2Product product)
}
}
void Level2ProductView::Update()
{
util::async([this]() { ComputeSweep(); });
}
void Level2ProductView::UpdateColorTable()
{
if (p->momentDataBlock0_ == nullptr || //

View file

@ -39,7 +39,6 @@ public:
void LoadColorTable(std::shared_ptr<common::ColorTable> colorTable) override;
void SelectElevation(float elevation) override;
void SelectProduct(const std::string& productName) override;
void Update() override;
common::RadarProductGroup GetRadarProductGroup() const override;
std::string GetRadarProductName() const override;

View file

@ -1,7 +1,6 @@
#include <scwx/qt/view/level3_product_view.hpp>
#include <scwx/common/constants.hpp>
#include <scwx/util/logger.hpp>
#include <scwx/util/threads.hpp>
#include <scwx/util/time.hpp>
#include <scwx/wsr88d/rpg/digital_radial_data_array_packet.hpp>
#include <scwx/wsr88d/rpg/graphic_product_message.hpp>
@ -161,11 +160,6 @@ void Level3ProductView::LoadColorTable(
UpdateColorTable();
}
void Level3ProductView::Update()
{
util::async([this]() { ComputeSweep(); });
}
void Level3ProductView::UpdateColorTable()
{
logger_->debug("UpdateColorTable()");

View file

@ -32,7 +32,6 @@ public:
std::uint16_t color_table_max() const override;
void LoadColorTable(std::shared_ptr<common::ColorTable> colorTable) override;
void Update() override;
common::RadarProductGroup GetRadarProductGroup() const override;
std::string GetRadarProductName() const override;

View file

@ -50,7 +50,11 @@ Level3RadialView::Level3RadialView(
p(std::make_unique<Level3RadialViewImpl>())
{
}
Level3RadialView::~Level3RadialView() = default;
Level3RadialView::~Level3RadialView()
{
std::unique_lock sweepLock {sweep_mutex()};
}
float Level3RadialView::range() const
{
@ -150,8 +154,8 @@ void Level3RadialView::ComputeSweep()
return;
}
// A message with radial data should either have a Digital Radial Data Array
// Packet, or a Radial Data Array Packet (TODO)
// A message with radial data should either have a Digital Radial Data
// Array Packet, or a Radial Data Array Packet (TODO)
std::shared_ptr<wsr88d::rpg::DigitalRadialDataArrayPacket>
digitalDataPacket = nullptr;
std::shared_ptr<wsr88d::rpg::RadialDataPacket> radialDataPacket = nullptr;

View file

@ -50,7 +50,11 @@ Level3RasterView::Level3RasterView(
p(std::make_unique<Level3RasterViewImpl>())
{
}
Level3RasterView::~Level3RasterView() = default;
Level3RasterView::~Level3RasterView()
{
std::unique_lock sweepLock {sweep_mutex()};
}
float Level3RasterView::range() const
{

View file

@ -2,6 +2,7 @@
#include <scwx/common/constants.hpp>
#include <scwx/util/logger.hpp>
#include <boost/asio.hpp>
#include <boost/range/irange.hpp>
#include <boost/timer/timer.hpp>
@ -36,6 +37,8 @@ public:
}
~RadarProductViewImpl() = default;
boost::asio::thread_pool threadPool_ {1};
bool initialized_;
std::mutex sweepMutex_;
@ -118,6 +121,11 @@ void RadarProductView::SelectTime(std::chrono::system_clock::time_point time)
p->selectedTime_ = time;
}
void RadarProductView::Update()
{
boost::asio::post(p->threadPool_, [this]() { ComputeSweep(); });
}
bool RadarProductView::IsInitialized() const
{
return p->initialized_;
@ -138,6 +146,11 @@ RadarProductView::GetCfpMomentData() const
return std::tie(data, dataSize, componentSize);
}
std::chrono::system_clock::time_point RadarProductView::GetSelectedTime() const
{
return p->selectedTime_;
}
void RadarProductView::ComputeSweep()
{
logger_->debug("ComputeSweep()");

View file

@ -51,7 +51,7 @@ public:
virtual void SelectElevation(float elevation);
virtual void SelectProduct(const std::string& productName) = 0;
void SelectTime(std::chrono::system_clock::time_point time);
virtual void Update() = 0;
void Update();
bool IsInitialized() const;
@ -62,6 +62,7 @@ public:
GetMomentData() const = 0;
virtual std::tuple<const void*, std::size_t, std::size_t>
GetCfpMomentData() const;
std::chrono::system_clock::time_point GetSelectedTime() const;
protected:
virtual void ConnectRadarProductManager() = 0;

View file

@ -66,6 +66,26 @@ TEST(AwsLevel3DataProvider, GetAvailableProducts)
EXPECT_GT(products.size(), 0);
}
TEST(AwsLevel3DataProvider, GetTimePointsByDate)
{
using namespace std::chrono;
using sys_days = time_point<system_clock, days>;
const auto date = sys_days {2021y / May / 27d};
const auto tomorrow = date + days {1};
AwsLevel3DataProvider provider("KLSX", "N0Q");
auto timePoints = provider.GetTimePointsByDate(date);
EXPECT_GT(timePoints.size(), 0);
for (auto timePoint : timePoints)
{
EXPECT_GE(timePoint, date);
EXPECT_LT(timePoint, tomorrow);
}
}
TEST(AwsLevel3DataProvider, TimePointValid)
{
using namespace std::chrono;

View file

@ -91,11 +91,14 @@ public:
std::shared_ptr<WmoHeader> wmo_header() const;
std::vector<std::string> mnd_header() const;
std::vector<std::string> overview_block() const;
size_t segment_count() const;
std::size_t segment_count() const;
std::vector<std::shared_ptr<const Segment>> segments() const;
std::shared_ptr<const Segment> segment(size_t s) const;
std::shared_ptr<const Segment> segment(std::size_t s) const;
size_t data_size() const;
std::chrono::system_clock::time_point
segment_event_begin(std::size_t s) const;
std::size_t data_size() const;
bool Parse(std::istream& is) override;

View file

@ -26,17 +26,20 @@ public:
AwsNexradDataProvider(AwsNexradDataProvider&&) noexcept;
AwsNexradDataProvider& operator=(AwsNexradDataProvider&&) noexcept;
size_t cache_size() const;
size_t cache_size() const override;
std::chrono::system_clock::time_point last_modified() const;
std::chrono::seconds update_period() const;
std::chrono::system_clock::time_point last_modified() const override;
std::chrono::seconds update_period() const override;
std::string FindKey(std::chrono::system_clock::time_point time);
std::string FindLatestKey();
std::pair<size_t, size_t>
ListObjects(std::chrono::system_clock::time_point date);
std::shared_ptr<wsr88d::NexradFile> LoadObjectByKey(const std::string& key);
std::pair<size_t, size_t> Refresh();
std::string FindKey(std::chrono::system_clock::time_point time) override;
std::string FindLatestKey() override;
std::vector<std::chrono::system_clock::time_point>
GetTimePointsByDate(std::chrono::system_clock::time_point date) override;
std::tuple<bool, size_t, size_t>
ListObjects(std::chrono::system_clock::time_point date) override;
std::shared_ptr<wsr88d::NexradFile>
LoadObjectByKey(const std::string& key) override;
std::pair<size_t, size_t> Refresh() override;
protected:
std::shared_ptr<Aws::S3::S3Client> client();

View file

@ -64,10 +64,11 @@ public:
*
* @param date Date for which to list objects
*
* @return - New objects found for the given date
* @return - Whether query was successful
* - New objects found for the given date
* - Total objects found for the given date
*/
virtual std::pair<size_t, size_t>
virtual std::tuple<bool, size_t, size_t>
ListObjects(std::chrono::system_clock::time_point date) = 0;
/**
@ -101,6 +102,17 @@ public:
virtual std::chrono::system_clock::time_point
GetTimePointByKey(const std::string& key) const = 0;
/**
* Gets NEXRAD data time points for the date supplied. Lists and adds them
* to the cache if required.
*
* @param date Date for which to get NEXRAD data time points
*
* @return NEXRAD data time points
*/
virtual std::vector<std::chrono::system_clock::time_point>
GetTimePointsByDate(std::chrono::system_clock::time_point date) = 0;
/**
* Requests available NEXRAD products for the current radar site, and adds
* the list to the cache.

View file

@ -8,35 +8,50 @@ namespace scwx
namespace util
{
template<class Key, class T, class ReturnType = std::map<Key, T>::const_pointer>
ReturnType GetBoundedElementPointer(std::map<Key, T>& map, const Key& key)
template<class Container>
Container::const_iterator
GetBoundedElementIterator(Container& container,
const typename Container::key_type& key)
{
ReturnType elementPtr {nullptr};
// Find the first element greater than the key requested
auto it = map.upper_bound(key);
typename Container::const_iterator it = container.upper_bound(key);
// An element with a key greater was found
if (it != map.cend())
if (it != container.cend())
{
// Are there elements prior to this element?
if (it != map.cbegin())
if (it != container.cbegin())
{
// Get the element immediately preceding, this the element we are
// looking for
elementPtr = &(*(--it));
--it;
}
else
{
// The current element is a good substitute
elementPtr = &(*it);
}
}
else if (map.size() > 0)
else if (container.size() > 0)
{
// An element with a key greater was not found. If it exists, it must be
// the last element.
elementPtr = &(*map.rbegin());
// the last element. Decrement the end iterator.
--it;
}
return it;
}
template<class Container, class ReturnType = Container::const_pointer>
ReturnType GetBoundedElementPointer(Container& container,
const typename Container::key_type& key)
{
ReturnType elementPtr {nullptr};
auto it = GetBoundedElementIterator(container, key);
if (it != container.cend())
{
elementPtr = &(*(it));
}
return elementPtr;
@ -47,8 +62,9 @@ ReturnType GetBoundedElement(std::map<Key, T>& map, const Key& key)
{
ReturnType element;
typename std::map<Key, T>::pointer elementPtr =
GetBoundedElementPointer<Key, T, typename std::map<Key, T>::pointer>(map,
typename std::map<Key, T>::const_pointer elementPtr =
GetBoundedElementPointer<std::map<Key, T>,
typename std::map<Key, T>::const_pointer>(map,
key);
if (elementPtr != nullptr)
{

View file

@ -1,5 +1,6 @@
#include <scwx/awips/text_product_message.hpp>
#include <scwx/common/characters.hpp>
#include <scwx/util/logger.hpp>
#include <scwx/util/streams.hpp>
#include <istream>
@ -15,6 +16,7 @@ namespace awips
{
static const std::string logPrefix_ = "scwx::awips::text_product_message";
static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
// Issuance date/time takes one of the following forms:
// * <hhmm>_xM_<tz>_day_mon_<dd>_year
@ -101,6 +103,91 @@ std::shared_ptr<const Segment> TextProductMessage::segment(size_t s) const
return p->segments_[s];
}
std::chrono::system_clock::time_point
TextProductMessage::segment_event_begin(std::size_t s) const
{
std::chrono::system_clock::time_point eventBegin {};
auto& header = segment(s)->header_;
if (header.has_value() && !header->vtecString_.empty())
{
// Determine event begin from P-VTEC string
eventBegin = header->vtecString_[0].pVtec_.event_begin();
// If event begin is 000000T0000Z
if (eventBegin == std::chrono::system_clock::time_point {})
{
using namespace std::chrono;
// Determine event end from P-VTEC string
system_clock::time_point eventEnd =
header->vtecString_[0].pVtec_.event_end();
auto endDays = floor<days>(eventEnd);
year_month_day endDate {endDays};
// Determine WMO date/time
std::string wmoDateTime = wmo_header()->date_time();
bool wmoDateTimeValid = false;
unsigned int dayOfMonth = 0;
unsigned long beginHour = 0;
unsigned long beginMinute = 0;
try
{
// WMO date time is in the format DDHHMM
dayOfMonth =
static_cast<unsigned int>(std::stoul(wmoDateTime.substr(0, 2)));
beginHour = std::stoul(wmoDateTime.substr(2, 2));
beginMinute = std::stoul(wmoDateTime.substr(4, 2));
wmoDateTimeValid = true;
}
catch (const std::exception&)
{
logger_->warn("Malformed WMO date/time: {}", wmoDateTime);
}
if (wmoDateTimeValid)
{
// Combine end date year and month with WMO date time
eventBegin =
sys_days {endDate.year() / endDate.month() / day {dayOfMonth}} +
hours {beginHour} + minutes {beginMinute};
// If the begin date is after the end date, assume the start time
// was the previous month (give a 1 day grace period for expiring
// events in the past)
if (eventBegin > eventEnd + 24h)
{
// If the current end month is January
if (endDate.month() == January)
{
// The begin month must be December of last year
eventBegin =
sys_days {
year {static_cast<int>((endDate.year() - 1y).count())} /
December / day {dayOfMonth}} +
hours {beginHour} + minutes {beginMinute};
}
else
{
// Back up one month
eventBegin =
sys_days {endDate.year() /
month {static_cast<unsigned int>(
(endDate.month() - month {1}).count())} /
day {dayOfMonth}} +
hours {beginHour} + minutes {beginMinute};
}
}
}
}
}
return eventBegin;
}
size_t TextProductMessage::data_size() const
{
return 0;

View file

@ -52,6 +52,11 @@ AwsLevel2DataProvider::operator=(AwsLevel2DataProvider&&) noexcept = default;
std::string
AwsLevel2DataProvider::GetPrefix(std::chrono::system_clock::time_point date)
{
if (date < std::chrono::system_clock::time_point {})
{
date = std::chrono::system_clock::time_point {};
}
return fmt::format("{0:%Y/%m/%d}/{1}/", fmt::gmtime(date), p->radarSite_);
}

View file

@ -78,6 +78,11 @@ AwsLevel3DataProvider::operator=(AwsLevel3DataProvider&&) noexcept = default;
std::string
AwsLevel3DataProvider::GetPrefix(std::chrono::system_clock::time_point date)
{
if (date < std::chrono::system_clock::time_point {})
{
date = std::chrono::system_clock::time_point {};
}
return fmt::format(
"{0}_{1}_{2:%Y_%m_%d}_", p->siteId_, p->product_, fmt::gmtime(date));
}

View file

@ -9,6 +9,7 @@
#include <aws/s3/model/GetObjectRequest.h>
#include <aws/s3/model/ListObjectsV2Request.h>
#include <fmt/chrono.h>
namespace scwx
{
@ -19,8 +20,9 @@ static const std::string logPrefix_ =
"scwx::provider::aws_nexrad_data_provider";
static const auto logger_ = util::Logger::Create(logPrefix_);
// Keep at least today, yesterday, and one more date
static const size_t kMinDatesBeforePruning_ = 4;
// Keep at least today, yesterday, and three more dates (archived volume scan
// list size)
static const size_t kMinDatesBeforePruning_ = 6;
static const size_t kMaxObjects_ = 2500;
class AwsNexradDataProvider::Impl
@ -156,7 +158,61 @@ std::string AwsNexradDataProvider::FindLatestKey()
return key;
}
std::pair<size_t, size_t>
std::vector<std::chrono::system_clock::time_point>
AwsNexradDataProvider::GetTimePointsByDate(
std::chrono::system_clock::time_point date)
{
const auto day = std::chrono::floor<std::chrono::days>(date);
std::vector<std::chrono::system_clock::time_point> timePoints {};
logger_->trace("GetTimePointsByDate: {}", util::TimeString(date));
std::shared_lock lock(p->objectsMutex_);
// Is the date present in the date list?
auto currentDateIterator =
std::find(p->objectDates_.cbegin(), p->objectDates_.cend(), day);
if (currentDateIterator == p->objectDates_.cend())
{
// Temporarily unlock mutex
lock.unlock();
// List objects, since the date is not present in the date list
auto [success, newObjects, totalObjects] = ListObjects(date);
if (success)
{
p->UpdateObjectDates(date);
}
// Re-lock mutex
lock.lock();
}
// Determine objects to retrieve
auto objectsBegin = p->objects_.lower_bound(day);
auto objectsEnd = p->objects_.lower_bound(day + std::chrono::days {1});
// Copy time points to destination vector
std::transform(objectsBegin,
objectsEnd,
std::back_inserter(timePoints),
[](const auto& object) { return object.first; });
// Unlock mutex, finished
lock.unlock();
// If we haven't updated the most recently queried dates yet, because the
// date was already cached, update
if (currentDateIterator != p->objectDates_.cend())
{
p->UpdateObjectDates(date);
}
return timePoints;
}
std::tuple<bool, size_t, size_t>
AwsNexradDataProvider::ListObjects(std::chrono::system_clock::time_point date)
{
const std::string prefix {GetPrefix(date)};
@ -222,7 +278,7 @@ AwsNexradDataProvider::ListObjects(std::chrono::system_clock::time_point date)
outcome.GetError().GetMessage());
}
return std::make_pair(newObjects, totalObjects);
return {outcome.IsSuccess(), newObjects, totalObjects};
}
std::shared_ptr<wsr88d::NexradFile>
@ -269,7 +325,7 @@ std::pair<size_t, size_t> AwsNexradDataProvider::Refresh()
// yesterday, to ensure we haven't missed any objects near midnight
if (p->refreshDate_ < today)
{
auto [newObjects, totalObjects] = ListObjects(yesterday);
auto [success, newObjects, totalObjects] = ListObjects(yesterday);
allNewObjects = newObjects;
allTotalObjects = totalObjects;
if (totalObjects > 0)
@ -278,7 +334,7 @@ std::pair<size_t, size_t> AwsNexradDataProvider::Refresh()
}
}
auto [newObjects, totalObjects] = ListObjects(today);
auto [success, newObjects, totalObjects] = ListObjects(today);
allNewObjects += newObjects;
allTotalObjects += totalObjects;
if (totalObjects > 0)

View file

@ -233,6 +233,11 @@ target_compile_options(wxdata PRIVATE
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall -Wextra -Wpedantic -Werror>
)
if (MSVC)
# Produce PDB file for debug
target_compile_options(wxdata PRIVATE "$<$<CONFIG:Release>:/Zi>")
endif()
target_link_libraries(wxdata PUBLIC aws-cpp-sdk-core
aws-cpp-sdk-s3
cpr::cpr