mirror of
				https://github.com/ciphervance/supercell-wx.git
				synced 2025-10-31 00:30:05 +00:00 
			
		
		
		
	Merge pull request #52 from dpaulat/feature/timeline-animation
Initial Timeline Animation Implementation
This commit is contained in:
		
						commit
						287903f180
					
				
					 41 changed files with 1970 additions and 113 deletions
				
			
		
							
								
								
									
										31
									
								
								.github/workflows/ci.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										31
									
								
								.github/workflows/ci.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
							
								
								
									
										9
									
								
								external/mapbox-gl-native.cmake
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								external/mapbox-gl-native.cmake
									
										
									
									
										vendored
									
									
								
							|  | @ -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) | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										1
									
								
								scwx-qt/res/icons/font-awesome-6/pause-solid.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								scwx-qt/res/icons/font-awesome-6/pause-solid.svg
									
										
									
									
									
										Normal 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 | 
							
								
								
									
										1
									
								
								scwx-qt/res/icons/font-awesome-6/play-solid.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								scwx-qt/res/icons/font-awesome-6/play-solid.svg
									
										
									
									
									
										Normal 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 | 
|  | @ -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 | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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); | ||||
|  | @ -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<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("?"); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -76,6 +76,7 @@ | |||
|      <string>&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 &Toolbox</string> | ||||
|    </property> | ||||
|   </action> | ||||
|   <action name="actionResourceExplorer"> | ||||
|  | @ -429,6 +430,11 @@ | |||
|     <string>&Check for Updates</string> | ||||
|    </property> | ||||
|   </action> | ||||
|   <action name="actionAnimationToolbox"> | ||||
|    <property name="text"> | ||||
|     <string>A&nimation Toolbox</string> | ||||
|    </property> | ||||
|   </action> | ||||
|  </widget> | ||||
|  <resources> | ||||
|   <include location="../../../../scwx-qt.qrc"/> | ||||
|  |  | |||
|  | @ -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_; | ||||
|  | @ -244,8 +247,8 @@ public: | |||
|    std::unordered_map<boost::uuids::uuid, | ||||
|                       std::shared_ptr<ProviderManager>, | ||||
|                       boost::hash<boost::uuids::uuid>> | ||||
|               refreshMap_ {}; | ||||
|    std::mutex refreshMapMutex_ {}; | ||||
|                      refreshMap_ {}; | ||||
|    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,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<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_); | ||||
|  |  | |||
|  | @ -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,7 +126,15 @@ public: | |||
| 
 | ||||
|    common::Level3ProductCategoryMap GetAvailableLevel3Categories(); | ||||
|    std::vector<std::string>         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<types::RadarProductRecord> record); | ||||
|  |  | |||
							
								
								
									
										541
									
								
								scwx-qt/source/scwx/qt/manager/timeline_manager.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										541
									
								
								scwx-qt/source/scwx/qt/manager/timeline_manager.cpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										57
									
								
								scwx-qt/source/scwx/qt/manager/timeline_manager.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								scwx-qt/source/scwx/qt/manager/timeline_manager.hpp
									
										
									
									
									
										Normal 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
 | ||||
|  | @ -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_; | ||||
|    common::Level2Product selectedLevel2Product_; | ||||
| 
 | ||||
|    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,20 +901,23 @@ void MapWidgetImpl::RadarProductManagerConnect() | |||
|                   std::make_shared<request::NexradFileRequest>(); | ||||
| 
 | ||||
|                // File request callback
 | ||||
|                connect( | ||||
|                   request.get(), | ||||
|                   &request::NexradFileRequest::RequestComplete, | ||||
|                   this, | ||||
|                   [this](std::shared_ptr<request::NexradFileRequest> 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::NexradFileRequest> request) | ||||
|                      { | ||||
|                         widget_->SelectRadarProduct(record); | ||||
|                      } | ||||
|                   }); | ||||
|                         // Select loaded record
 | ||||
|                         auto record = request->radar_product_record(); | ||||
| 
 | ||||
|                         if (record != nullptr) | ||||
|                         { | ||||
|                            widget_->SelectRadarProduct(record); | ||||
|                         } | ||||
|                      }); | ||||
|                } | ||||
| 
 | ||||
|                // Load file
 | ||||
|                scwx::util::async( | ||||
|  |  | |||
|  | @ -34,15 +34,16 @@ public: | |||
|    explicit MapWidget(const QMapLibreGL::Settings&); | ||||
|    ~MapWidget(); | ||||
| 
 | ||||
|    common::Level3ProductCategoryMap   GetAvailableLevel3Categories(); | ||||
|    float                              GetElevation() const; | ||||
|    std::vector<float>                 GetElevationCuts() const; | ||||
|    std::vector<std::string>           GetLevel3Products(); | ||||
|    std::string                        GetMapStyle() const; | ||||
|    common::RadarProductGroup          GetRadarProductGroup() const; | ||||
|    std::string                        GetRadarProductName() const; | ||||
|    std::shared_ptr<config::RadarSite> GetRadarSite() const; | ||||
|    std::uint16_t                      GetVcp() const; | ||||
|    common::Level3ProductCategoryMap      GetAvailableLevel3Categories(); | ||||
|    float                                 GetElevation() const; | ||||
|    std::vector<float>                    GetElevationCuts() const; | ||||
|    std::vector<std::string>              GetLevel3Products(); | ||||
|    std::string                           GetMapStyle() const; | ||||
|    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(); | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|    { | ||||
|  |  | |||
							
								
								
									
										22
									
								
								scwx-qt/source/scwx/qt/types/map_types.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								scwx-qt/source/scwx/qt/types/map_types.cpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										28
									
								
								scwx-qt/source/scwx/qt/types/map_types.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								scwx-qt/source/scwx/qt/types/map_types.hpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										219
									
								
								scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										56
									
								
								scwx-qt/source/scwx/qt/ui/animation_dock_widget.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								scwx-qt/source/scwx/qt/ui/animation_dock_widget.hpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										290
									
								
								scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										290
									
								
								scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui
									
										
									
									
									
										Normal 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> | ||||
							
								
								
									
										24
									
								
								scwx-qt/source/scwx/qt/util/time.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								scwx-qt/source/scwx/qt/util/time.cpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										25
									
								
								scwx-qt/source/scwx/qt/util/time.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								scwx-qt/source/scwx/qt/util/time.hpp
									
										
									
									
									
										Normal 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
 | ||||
|  | @ -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 || //
 | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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()"); | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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 | ||||
| { | ||||
|  |  | |||
|  | @ -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()"); | ||||
|  |  | |||
|  | @ -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<const void*, std::size_t, std::size_t> | ||||
|    GetMomentData() const = 0; | ||||
|    virtual std::tuple<const void*, std::size_t, std::size_t> | ||||
|    GetCfpMomentData() const; | ||||
|                                          GetCfpMomentData() const; | ||||
|    std::chrono::system_clock::time_point GetSelectedTime() const; | ||||
| 
 | ||||
| protected: | ||||
|    virtual void ConnectRadarProductManager()    = 0; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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<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(); | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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,9 +62,10 @@ 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, | ||||
|                                                                            key); | ||||
|    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) | ||||
|    { | ||||
|       element = elementPtr->second; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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_); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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)); | ||||
| } | ||||
|  |  | |||
|  | @ -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,16 +325,16 @@ 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); | ||||
|       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) | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Dan Paulat
						Dan Paulat