diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca2a7f36..3429ce50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 678da305..62bdd8a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/external/mapbox-gl-native.cmake b/external/mapbox-gl-native.cmake index c48900c1..819dad6c 100644 --- a/external/mapbox-gl-native.cmake +++ b/external/mapbox-gl-native.cmake @@ -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 "$<$:/Zi>") + target_compile_options(qmaplibregl PRIVATE "$<$:/Zi>") + target_link_options(qmaplibregl PRIVATE "$<$:/DEBUG>") + target_link_options(qmaplibregl PRIVATE "$<$:/OPT:REF>") + target_link_options(qmaplibregl PRIVATE "$<$:/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) diff --git a/scwx-qt/res/icons/font-awesome-6/pause-solid.svg b/scwx-qt/res/icons/font-awesome-6/pause-solid.svg new file mode 100644 index 00000000..57ee1e2e --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/pause-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scwx-qt/res/icons/font-awesome-6/play-solid.svg b/scwx-qt/res/icons/font-awesome-6/play-solid.svg new file mode 100644 index 00000000..1298e049 --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/play-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index becb5901..5558b857 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -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 $<$>:-Wall -Wextra -Wpedantic -Werror> ) +if (MSVC) + # Produce PDB file for debug + target_compile_options(scwx-qt PRIVATE "$<$:/Zi>") + target_compile_options(supercell-wx PRIVATE "$<$:/Zi>") + target_link_options(supercell-wx PRIVATE "$<$:/DEBUG>") + target_link_options(supercell-wx PRIVATE "$<$:/OPT:REF>") + target_link_options(supercell-wx PRIVATE "$<$:/OPT:ICF>") +endif() + target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::OpenGLWidgets Boost::json diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc index c708db9e..36618bee 100644 --- a/scwx-qt/scwx-qt.qrc +++ b/scwx-qt/scwx-qt.qrc @@ -24,6 +24,8 @@ res/icons/font-awesome-6/gears-solid.svg res/icons/font-awesome-6/github.svg res/icons/font-awesome-6/palette-solid.svg + res/icons/font-awesome-6/pause-solid.svg + res/icons/font-awesome-6/play-solid.svg res/icons/font-awesome-6/rotate-left-solid.svg res/icons/font-awesome-6/sliders-solid.svg res/icons/font-awesome-6/square-minus-regular.svg diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 452aeeb8..8b14dc85 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -15,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -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); @@ -138,15 +143,17 @@ public: ui::Level3ProductsWidget* level3ProductsWidget_; - ui::AlertDockWidget* alertDockWidget_; - ui::AboutDialog* aboutDialog_; - ui::ImGuiDebugDialog* imGuiDebugDialog_; - ui::RadarSiteDialog* radarSiteDialog_; - ui::SettingsDialog* settingsDialog_; - ui::UpdateDialog* updateDialog_; + ui::AlertDockWidget* alertDockWidget_; + ui::AnimationDockWidget* animationDockWidget_; + ui::AboutDialog* aboutDialog_; + ui::ImGuiDebugDialog* imGuiDebugDialog_; + ui::RadarSiteDialog* radarSiteDialog_; + ui::SettingsDialog* settingsDialog_; + ui::UpdateDialog* updateDialog_; std::unique_ptr radarProductModel_; std::shared_ptr textEventManager_; + std::shared_ptr timelineManager_; std::shared_ptr updateManager_; std::vector 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("?"); } } diff --git a/scwx-qt/source/scwx/qt/main/main_window.ui b/scwx-qt/source/scwx/qt/main/main_window.ui index 0d5c5489..33cf502f 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.ui +++ b/scwx-qt/source/scwx/qt/main/main_window.ui @@ -76,6 +76,7 @@ &View + @@ -359,7 +360,7 @@ - Radar Toolbox + Radar &Toolbox @@ -429,6 +430,11 @@ &Check for Updates + + + A&nimation Toolbox + + diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index d062e67a..24ba870d 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #if defined(_MSC_VER) # pragma warning(push, 0) @@ -204,6 +205,7 @@ public: std::shared_mutex& recordMutex, std::mutex& loadDataMutex, std::shared_ptr 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 radarSite_; + std::size_t cacheLimit_ {6u}; std::vector coordinates0_5Degree_; std::vector coordinates1Degree_; @@ -244,8 +247,8 @@ public: std::unordered_map, boost::hash> - refreshMap_ {}; - std::mutex refreshMapMutex_ {}; + refreshMap_ {}; + std::shared_mutex refreshMapMutex_ {}; }; RadarProductManager::RadarProductManager(const std::string& radarId) : @@ -676,6 +679,79 @@ void RadarProductManagerImpl::RefreshData( }); } +std::set +RadarProductManager::GetActiveVolumeTimes( + std::chrono::system_clock::time_point time) +{ + std::unordered_set> + providers {}; + std::set 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(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) + { + // 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, @@ -869,6 +945,59 @@ void RadarProductManagerImpl::LoadNexradFile( }); } +void RadarProductManagerImpl::PopulateLevel2ProductTimes( + std::chrono::system_clock::time_point time) +{ + const auto today = std::chrono::floor(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 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>( + time, std::weak_ptr {}); + }); +} + std::tuple, 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,12 +1024,9 @@ RadarProductManagerImpl::GetLevel2ProductRecord( if (recordPtr != nullptr) { - if (time == std::chrono::system_clock::time_point {} || - time == recordPtr->first) - { - recordTime = recordPtr->first; - record = recordPtr->second.lock(); - } + // Don't check for an exact time match for level 2 products + recordTime = recordPtr->first; + record = recordPtr->second.lock(); } if (record == nullptr && @@ -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 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 RadarProductManager::GetLevel3Products() return level3ProviderManager->provider_->GetAvailableProducts(); } +void RadarProductManager::SetCacheLimit(size_t cacheLimit) +{ + p->cacheLimit_ = cacheLimit; +} + void RadarProductManager::UpdateAvailableProducts() { std::lock_guard guard(p->level3ProductsInitializeMutex_); diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp index 54a57499..ed8ba97b 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -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 + GetActiveVolumeTimes(std::chrono::system_clock::time_point time); + /** * @brief Get level 2 radar data for a data block type, elevation, and time. * @@ -114,7 +126,15 @@ public: common::Level3ProductCategoryMap GetAvailableLevel3Categories(); std::vector GetLevel3Products(); - void UpdateAvailableProducts(); + + /** + * @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: void DataReloaded(std::shared_ptr record); diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp new file mode 100644 index 00000000..fd2413cd --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp @@ -0,0 +1,541 @@ +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +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 + GetLoopStartAndEndTimes(); + void UpdateCacheLimit( + std::shared_ptr radarProductManager, + const std::set& 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(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 +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::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 radarProductManager, + const std::set& 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(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::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::Instance() +{ + static std::weak_ptr timelineManagerReference_ {}; + static std::mutex instanceMutex_ {}; + + std::unique_lock lock(instanceMutex_); + + std::shared_ptr timelineManager = + timelineManagerReference_.lock(); + + if (timelineManager == nullptr) + { + timelineManager = std::make_shared(); + timelineManagerReference_ = timelineManager; + } + + return timelineManager; +} + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp new file mode 100644 index 00000000..a29278b9 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include + +#include +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +class TimelineManager : public QObject +{ + Q_OBJECT + +public: + explicit TimelineManager(); + ~TimelineManager(); + + static std::shared_ptr 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 p; +}; + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index b63fa565..c3aeebc7 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -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_; bool autoRefreshEnabled_; + bool autoUpdateEnabled_; - common::Level2Product selectedLevel2Product_; - std::chrono::system_clock::time_point selectedTime_; + common::Level2Product selectedLevel2Product_; QPointF lastPos_; std::size_t currentStyleIndex_; @@ -316,6 +316,21 @@ std::shared_ptr 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 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,20 +901,23 @@ void MapWidgetImpl::RadarProductManagerConnect() std::make_shared(); // File request callback - connect( - request.get(), - &request::NexradFileRequest::RequestComplete, - this, - [this](std::shared_ptr request) - { - // Select loaded record - auto record = request->radar_product_record(); - - if (record != nullptr) + if (autoUpdateEnabled_) + { + connect( + request.get(), + &request::NexradFileRequest::RequestComplete, + this, + [this](std::shared_ptr request) { - widget_->SelectRadarProduct(record); - } - }); + // Select loaded record + auto record = request->radar_product_record(); + + if (record != nullptr) + { + widget_->SelectRadarProduct(record); + } + }); + } // Load file scwx::util::async( diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp index fbb5746e..51cfb230 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.hpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp @@ -34,15 +34,16 @@ public: explicit MapWidget(const QMapLibreGL::Settings&); ~MapWidget(); - common::Level3ProductCategoryMap GetAvailableLevel3Categories(); - float GetElevation() const; - std::vector GetElevationCuts() const; - std::vector GetLevel3Products(); - std::string GetMapStyle() const; - common::RadarProductGroup GetRadarProductGroup() const; - std::string GetRadarProductName() const; - std::shared_ptr GetRadarSite() const; - std::uint16_t GetVcp() const; + common::Level3ProductCategoryMap GetAvailableLevel3Categories(); + float GetElevation() const; + std::vector GetElevationCuts() const; + std::vector GetLevel3Products(); + std::string GetMapStyle() const; + common::RadarProductGroup GetRadarProductGroup() const; + std::string GetRadarProductName() const; + std::shared_ptr 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 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 radarSite); void RadarSweepUpdated(); }; diff --git a/scwx-qt/source/scwx/qt/model/alert_model.cpp b/scwx-qt/source/scwx/qt/model/alert_model.cpp index 8ea4ccc1..793d67b4 100644 --- a/scwx-qt/source/scwx/qt/model/alert_model.cpp +++ b/scwx-qt/source/scwx/qt/model/alert_model.cpp @@ -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 { diff --git a/scwx-qt/source/scwx/qt/types/map_types.cpp b/scwx-qt/source/scwx/qt/types/map_types.cpp new file mode 100644 index 00000000..d3ea6bc0 --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/map_types.cpp @@ -0,0 +1,22 @@ +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace types +{ + +static const std::unordered_map mapTimeName_ { + {MapTime::Live, "Live"}, {MapTime::Archive, "Archive"}}; + +std::string GetMapTimeName(MapTime mapTime) +{ + return mapTimeName_.at(mapTime); +} + +} // namespace types +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/map_types.hpp b/scwx-qt/source/scwx/qt/types/map_types.hpp new file mode 100644 index 00000000..d2c804a2 --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/map_types.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +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 diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp new file mode 100644 index 00000000..f503c8bf --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp @@ -0,0 +1,219 @@ +#include "animation_dock_widget.hpp" +#include "ui_animation_dock_widget.h" + +#include +#include + +#include + +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(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 diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.hpp b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.hpp new file mode 100644 index 00000000..705ca02f --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include + +#include + +#include + +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 p; + Ui::AnimationDockWidget* ui; +}; + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui new file mode 100644 index 00000000..4e3502ca --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui @@ -0,0 +1,290 @@ + + + AnimationDockWidget + + + + 0 + 0 + 200 + 337 + + + + Animation Toolbox + + + + + + + Timeline + + + + + + Auto Update: Enabled + + + + + + + Live View + + + true + + + + + + + Archive View + + + + + + + QAbstractSpinBox::CorrectToNearestValue + + + + 0 + 0 + 0 + 1991 + 6 + 1 + + + + yyyy-MM-dd + + + true + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractSpinBox::CorrectToNearestValue + + + HH:mm + + + + + + + UTC + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Loop Time + + + + + + + QAbstractSpinBox::CorrectToNearestValue + + + min + + + 1 + + + 1440 + + + 30 + + + + + + + Loop Speed + + + + + + + QAbstractSpinBox::CorrectToNearestValue + + + x + + + 1.000000000000000 + + + 1.000000000000000 + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 1 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + ... + + + + :/res/icons/font-awesome-6/backward-step-solid.svg:/res/icons/font-awesome-6/backward-step-solid.svg + + + + + + + ... + + + + :/res/icons/font-awesome-6/angle-left-solid.svg:/res/icons/font-awesome-6/angle-left-solid.svg + + + + + + + ... + + + + :/res/icons/font-awesome-6/play-solid.svg:/res/icons/font-awesome-6/play-solid.svg + + + + + + + ... + + + + :/res/icons/font-awesome-6/angle-right-solid.svg:/res/icons/font-awesome-6/angle-right-solid.svg + + + + + + + ... + + + + :/res/icons/font-awesome-6/forward-step-solid.svg:/res/icons/font-awesome-6/forward-step-solid.svg + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + diff --git a/scwx-qt/source/scwx/qt/util/time.cpp b/scwx-qt/source/scwx/qt/util/time.cpp new file mode 100644 index 00000000..f34c6ea5 --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/time.cpp @@ -0,0 +1,24 @@ +#include + +namespace scwx +{ +namespace qt +{ +namespace util +{ + +std::chrono::sys_days SysDays(const QDate& date) +{ + using namespace std::chrono; + using sys_days = time_point; + 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 diff --git a/scwx-qt/source/scwx/qt/util/time.hpp b/scwx-qt/source/scwx/qt/util/time.hpp new file mode 100644 index 00000000..8f3e7a53 --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/time.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include + +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 diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 01271814..1cbd2e95 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -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(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 || // diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.hpp b/scwx-qt/source/scwx/qt/view/level2_product_view.hpp index a00dd0da..507e705d 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.hpp @@ -39,7 +39,6 @@ public: void LoadColorTable(std::shared_ptr 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; diff --git a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp index c0a45ea2..5924ce35 100644 --- a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include #include @@ -161,11 +160,6 @@ void Level3ProductView::LoadColorTable( UpdateColorTable(); } -void Level3ProductView::Update() -{ - util::async([this]() { ComputeSweep(); }); -} - void Level3ProductView::UpdateColorTable() { logger_->debug("UpdateColorTable()"); diff --git a/scwx-qt/source/scwx/qt/view/level3_product_view.hpp b/scwx-qt/source/scwx/qt/view/level3_product_view.hpp index c9d299eb..0ea023db 100644 --- a/scwx-qt/source/scwx/qt/view/level3_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/level3_product_view.hpp @@ -32,7 +32,6 @@ public: std::uint16_t color_table_max() const override; void LoadColorTable(std::shared_ptr colorTable) override; - void Update() override; common::RadarProductGroup GetRadarProductGroup() const override; std::string GetRadarProductName() const override; diff --git a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp index 4652c0a2..16e674d3 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp @@ -50,7 +50,11 @@ Level3RadialView::Level3RadialView( p(std::make_unique()) { } -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 digitalDataPacket = nullptr; std::shared_ptr radialDataPacket = nullptr; diff --git a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp index 1c150c70..f5d36185 100644 --- a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp @@ -50,7 +50,11 @@ Level3RasterView::Level3RasterView( p(std::make_unique()) { } -Level3RasterView::~Level3RasterView() = default; + +Level3RasterView::~Level3RasterView() +{ + std::unique_lock sweepLock {sweep_mutex()}; +} float Level3RasterView::range() const { diff --git a/scwx-qt/source/scwx/qt/view/radar_product_view.cpp b/scwx-qt/source/scwx/qt/view/radar_product_view.cpp index 99cd78c6..2f4c6f23 100644 --- a/scwx-qt/source/scwx/qt/view/radar_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/radar_product_view.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -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()"); diff --git a/scwx-qt/source/scwx/qt/view/radar_product_view.hpp b/scwx-qt/source/scwx/qt/view/radar_product_view.hpp index 04c9131d..16d23a80 100644 --- a/scwx-qt/source/scwx/qt/view/radar_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/radar_product_view.hpp @@ -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; @@ -61,7 +61,8 @@ public: virtual std::tuple GetMomentData() const = 0; virtual std::tuple - GetCfpMomentData() const; + GetCfpMomentData() const; + std::chrono::system_clock::time_point GetSelectedTime() const; protected: virtual void ConnectRadarProductManager() = 0; diff --git a/test/source/scwx/provider/aws_level3_data_provider.test.cpp b/test/source/scwx/provider/aws_level3_data_provider.test.cpp index 4e88e4e6..b076d6bb 100644 --- a/test/source/scwx/provider/aws_level3_data_provider.test.cpp +++ b/test/source/scwx/provider/aws_level3_data_provider.test.cpp @@ -66,6 +66,26 @@ TEST(AwsLevel3DataProvider, GetAvailableProducts) EXPECT_GT(products.size(), 0); } +TEST(AwsLevel3DataProvider, GetTimePointsByDate) +{ + using namespace std::chrono; + using sys_days = time_point; + + 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; diff --git a/wxdata/include/scwx/awips/text_product_message.hpp b/wxdata/include/scwx/awips/text_product_message.hpp index ec90f82d..80ba4a16 100644 --- a/wxdata/include/scwx/awips/text_product_message.hpp +++ b/wxdata/include/scwx/awips/text_product_message.hpp @@ -91,11 +91,14 @@ public: std::shared_ptr wmo_header() const; std::vector mnd_header() const; std::vector overview_block() const; - size_t segment_count() const; + std::size_t segment_count() const; std::vector> segments() const; - std::shared_ptr segment(size_t s) const; + std::shared_ptr 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; diff --git a/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp b/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp index 999a4952..436ad0ba 100644 --- a/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp +++ b/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp @@ -20,23 +20,26 @@ public: const std::string& region); virtual ~AwsNexradDataProvider(); - AwsNexradDataProvider(const AwsNexradDataProvider&) = delete; + AwsNexradDataProvider(const AwsNexradDataProvider&) = delete; AwsNexradDataProvider& operator=(const AwsNexradDataProvider&) = delete; 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 - ListObjects(std::chrono::system_clock::time_point date); - std::shared_ptr LoadObjectByKey(const std::string& key); - std::pair Refresh(); + std::string FindKey(std::chrono::system_clock::time_point time) override; + std::string FindLatestKey() override; + std::vector + GetTimePointsByDate(std::chrono::system_clock::time_point date) override; + std::tuple + ListObjects(std::chrono::system_clock::time_point date) override; + std::shared_ptr + LoadObjectByKey(const std::string& key) override; + std::pair Refresh() override; protected: std::shared_ptr client(); diff --git a/wxdata/include/scwx/provider/nexrad_data_provider.hpp b/wxdata/include/scwx/provider/nexrad_data_provider.hpp index 5041f9aa..14a75815 100644 --- a/wxdata/include/scwx/provider/nexrad_data_provider.hpp +++ b/wxdata/include/scwx/provider/nexrad_data_provider.hpp @@ -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 + virtual std::tuple 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 + 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. diff --git a/wxdata/include/scwx/util/map.hpp b/wxdata/include/scwx/util/map.hpp index 3392b5d7..f35e2abf 100644 --- a/wxdata/include/scwx/util/map.hpp +++ b/wxdata/include/scwx/util/map.hpp @@ -8,35 +8,50 @@ namespace scwx namespace util { -template::const_pointer> -ReturnType GetBoundedElementPointer(std::map& map, const Key& key) +template +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 +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,9 +62,10 @@ ReturnType GetBoundedElement(std::map& map, const Key& key) { ReturnType element; - typename std::map::pointer elementPtr = - GetBoundedElementPointer::pointer>(map, - key); + typename std::map::const_pointer elementPtr = + GetBoundedElementPointer, + typename std::map::const_pointer>(map, + key); if (elementPtr != nullptr) { element = elementPtr->second; diff --git a/wxdata/source/scwx/awips/text_product_message.cpp b/wxdata/source/scwx/awips/text_product_message.cpp index a27e593d..9b2a9149 100644 --- a/wxdata/source/scwx/awips/text_product_message.cpp +++ b/wxdata/source/scwx/awips/text_product_message.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -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: // * _xM__day_mon_
_year @@ -101,6 +103,91 @@ std::shared_ptr 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(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(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((endDate.year() - 1y).count())} / + December / day {dayOfMonth}} + + hours {beginHour} + minutes {beginMinute}; + } + else + { + // Back up one month + eventBegin = + sys_days {endDate.year() / + month {static_cast( + (endDate.month() - month {1}).count())} / + day {dayOfMonth}} + + hours {beginHour} + minutes {beginMinute}; + } + } + } + } + } + + return eventBegin; +} + size_t TextProductMessage::data_size() const { return 0; diff --git a/wxdata/source/scwx/provider/aws_level2_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_data_provider.cpp index 53b0f85f..6ac939c0 100644 --- a/wxdata/source/scwx/provider/aws_level2_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_data_provider.cpp @@ -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_); } diff --git a/wxdata/source/scwx/provider/aws_level3_data_provider.cpp b/wxdata/source/scwx/provider/aws_level3_data_provider.cpp index 327f4c9f..029dd5db 100644 --- a/wxdata/source/scwx/provider/aws_level3_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level3_data_provider.cpp @@ -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)); } diff --git a/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp b/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp index 4d763135..f6dbf79a 100644 --- a/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp @@ -9,6 +9,7 @@ #include #include +#include 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 +std::vector +AwsNexradDataProvider::GetTimePointsByDate( + std::chrono::system_clock::time_point date) +{ + const auto day = std::chrono::floor(date); + + std::vector 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 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 @@ -269,16 +325,16 @@ std::pair AwsNexradDataProvider::Refresh() // yesterday, to ensure we haven't missed any objects near midnight if (p->refreshDate_ < today) { - auto [newObjects, totalObjects] = ListObjects(yesterday); - allNewObjects = newObjects; - allTotalObjects = totalObjects; + auto [success, newObjects, totalObjects] = ListObjects(yesterday); + allNewObjects = newObjects; + allTotalObjects = totalObjects; if (totalObjects > 0) { p->refreshDate_ = yesterday; } } - auto [newObjects, totalObjects] = ListObjects(today); + auto [success, newObjects, totalObjects] = ListObjects(today); allNewObjects += newObjects; allTotalObjects += totalObjects; if (totalObjects > 0) diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index d12b763a..e2ee86f9 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -233,6 +233,11 @@ target_compile_options(wxdata PRIVATE $<$>:-Wall -Wextra -Wpedantic -Werror> ) +if (MSVC) + # Produce PDB file for debug + target_compile_options(wxdata PRIVATE "$<$:/Zi>") +endif() + target_link_libraries(wxdata PUBLIC aws-cpp-sdk-core aws-cpp-sdk-s3 cpr::cpr