diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index d213b4fd..30e3b699 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -963,6 +963,13 @@ void MainWindowImpl::ConnectMapSignals() } }, Qt::QueuedConnection); + connect( + mapWidget, + &map::MapWidget::IncomingLevel2ElevationChanged, + this, + [this](std::optional) + { level2SettingsWidget_->UpdateSettings(activeMap_); }, + Qt::QueuedConnection); } } diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 48584a2d..81f0fc52 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -14,6 +15,7 @@ #include #include #include +#include #if defined(_MSC_VER) # pragma warning(push, 0) @@ -66,7 +68,9 @@ static const std::string kDefaultLevel3Product_ {"N0B"}; static constexpr std::size_t kTimerPlaces_ {6u}; static constexpr std::chrono::seconds kFastRetryInterval_ {15}; +static constexpr std::chrono::seconds kFastRetryIntervalChunks_ {3}; static constexpr std::chrono::seconds kSlowRetryInterval_ {120}; +static constexpr std::chrono::seconds kSlowRetryIntervalChunks_ {20}; static std::unordered_map> instanceMap_; @@ -84,21 +88,25 @@ class ProviderManager : public QObject Q_OBJECT public: explicit ProviderManager(RadarProductManager* self, - const std::string& radarId, - common::RadarProductGroup group) : - ProviderManager(self, radarId, group, "???") - { - } - explicit ProviderManager(RadarProductManager* self, - const std::string& radarId, + std::string radarId, common::RadarProductGroup group, - const std::string& product) : - radarId_ {radarId}, group_ {group}, product_ {product} + std::string product = "???", + bool isChunks = false) : + radarId_ {std::move(radarId)}, + group_ {group}, + product_ {std::move(product)}, + isChunks_ {isChunks} { connect(this, &ProviderManager::NewDataAvailable, self, - &RadarProductManager::NewDataAvailable); + [this, self](common::RadarProductGroup group, + const std::string& product, + std::chrono::system_clock::time_point latestTime) + { + Q_EMIT self->NewDataAvailable( + group, product, isChunks_, latestTime); + }); } ~ProviderManager() { threadPool_.join(); }; @@ -111,10 +119,12 @@ public: const std::string radarId_; const common::RadarProductGroup group_; const std::string product_; + const bool isChunks_; bool refreshEnabled_ {false}; boost::asio::steady_timer refreshTimer_ {threadPool_}; std::mutex refreshTimerMutex_ {}; std::shared_ptr provider_ {nullptr}; + size_t refreshCount_ {0}; signals: void NewDataAvailable(common::RadarProductGroup group, @@ -133,7 +143,9 @@ public: level3ProductsInitialized_ {false}, radarSite_ {config::RadarSite::Get(radarId)}, level2ProviderManager_ {std::make_shared( - self_, radarId_, common::RadarProductGroup::Level2)} + self_, radarId_, common::RadarProductGroup::Level2)}, + level2ChunksProviderManager_ {std::make_shared( + self_, radarId_, common::RadarProductGroup::Level2, "???", true)} { if (radarSite_ == nullptr) { @@ -143,10 +155,24 @@ public: level2ProviderManager_->provider_ = provider::NexradDataProviderFactory::CreateLevel2DataProvider(radarId); + level2ChunksProviderManager_->provider_ = + provider::NexradDataProviderFactory::CreateLevel2ChunksDataProvider( + radarId); + + auto level2ChunksProvider = + std::dynamic_pointer_cast( + level2ChunksProviderManager_->provider_); + if (level2ChunksProvider != nullptr) + { + level2ChunksProvider->SetLevel2DataProvider( + std::dynamic_pointer_cast( + level2ProviderManager_->provider_)); + } } ~RadarProductManagerImpl() { level2ProviderManager_->Disable(); + level2ChunksProviderManager_->Disable(); std::shared_lock lock(level3ProviderManagerMutex_); std::for_each(std::execution::par_unseq, @@ -172,9 +198,10 @@ public: std::shared_ptr GetLevel3ProviderManager(const std::string& product); - void EnableRefresh(boost::uuids::uuid uuid, - std::shared_ptr providerManager, - bool enabled); + void EnableRefresh( + boost::uuids::uuid uuid, + const std::set>& providerManagers, + bool enabled); void RefreshData(std::shared_ptr providerManager); void RefreshDataSync(std::shared_ptr providerManager); @@ -250,6 +277,7 @@ public: std::shared_mutex level3ProductRecordMutex_ {}; std::shared_ptr level2ProviderManager_; + std::shared_ptr level2ChunksProviderManager_; std::unordered_map> level3ProviderManagerMap_ {}; std::shared_mutex level3ProviderManagerMutex_ {}; @@ -262,8 +290,10 @@ public: common::Level3ProductCategoryMap availableCategoryMap_ {}; std::shared_mutex availableCategoryMutex_ {}; + std::optional incomingLevel2Elevation_ {}; + std::unordered_map, + std::set>, boost::hash> refreshMap_ {}; std::shared_mutex refreshMapMutex_ {}; @@ -441,6 +471,11 @@ float RadarProductManager::gate_size() const return (is_tdwr()) ? 150.0f : 250.0f; } +std::optional RadarProductManager::incoming_level_2_elevation() const +{ + return p->incomingLevel2Elevation_; +} + std::string RadarProductManager::radar_id() const { return p->radarId_; @@ -637,7 +672,10 @@ void RadarProductManager::EnableRefresh(common::RadarProductGroup group, { if (group == common::RadarProductGroup::Level2) { - p->EnableRefresh(uuid, p->level2ProviderManager_, enabled); + p->EnableRefresh( + uuid, + {p->level2ProviderManager_, p->level2ChunksProviderManager_}, + enabled); } else { @@ -660,7 +698,7 @@ void RadarProductManager::EnableRefresh(common::RadarProductGroup group, availableProducts.cend(), product) != availableProducts.cend()) { - p->EnableRefresh(uuid, providerManager, enabled); + p->EnableRefresh(uuid, {providerManager}, enabled); } } catch (const std::exception& ex) @@ -672,50 +710,45 @@ void RadarProductManager::EnableRefresh(common::RadarProductGroup group, } void RadarProductManagerImpl::EnableRefresh( - boost::uuids::uuid uuid, - std::shared_ptr providerManager, - bool enabled) + boost::uuids::uuid uuid, + const std::set>& providerManagers, + bool enabled) { // Lock the refresh map std::unique_lock lock {refreshMapMutex_}; - auto currentProviderManager = refreshMap_.find(uuid); - if (currentProviderManager != refreshMap_.cend()) + auto currentProviderManagers = refreshMap_.find(uuid); + if (currentProviderManagers != refreshMap_.cend()) { - // If the enabling refresh for a different product, or disabling refresh - if (currentProviderManager->second != providerManager || !enabled) + for (const auto& currentProviderManager : currentProviderManagers->second) { - // Determine number of entries in the map for the current provider - // manager - auto currentProviderManagerCount = std::count_if( - refreshMap_.cbegin(), - refreshMap_.cend(), - [&](const auto& provider) - { return provider.second == currentProviderManager->second; }); - - // If this is the last reference to the provider in the refresh map - if (currentProviderManagerCount == 1) + currentProviderManager->refreshCount_ -= 1; + // If the enabling refresh for a different product, or disabling + // refresh + if (!providerManagers.contains(currentProviderManager) || !enabled) { - // Disable current provider - currentProviderManager->second->Disable(); - } - - // Dissociate uuid from current provider manager - refreshMap_.erase(currentProviderManager); - - // If we are enabling a new provider manager - if (enabled) - { - // Associate uuid to providerManager - refreshMap_.emplace(uuid, providerManager); + // If this is the last reference to the provider in the refresh map + if (currentProviderManager->refreshCount_ == 0) + { + // Disable current provider + currentProviderManager->Disable(); + } } } + + // Dissociate uuid from current provider managers + refreshMap_.erase(currentProviderManagers); } - else if (enabled) + + if (enabled) { - // We are enabling a new provider manager + // We are enabling provider managers // Associate uuid to provider manager - refreshMap_.emplace(uuid, providerManager); + refreshMap_.emplace(uuid, providerManagers); + for (const auto& providerManager : providerManagers) + { + providerManager->refreshCount_ += 1; + } } // Release the refresh map mutex @@ -723,13 +756,15 @@ void RadarProductManagerImpl::EnableRefresh( // We have already handled a disable request by this point. If enabling, and // the provider manager refresh isn't already enabled, enable it. - if (enabled && providerManager->refreshEnabled_ != enabled) + if (enabled) { - providerManager->refreshEnabled_ = enabled; - - if (enabled) + for (const auto& providerManager : providerManagers) { - RefreshData(providerManager); + if (providerManager->refreshEnabled_ != enabled) + { + providerManager->refreshEnabled_ = enabled; + RefreshData(providerManager); + } } } } @@ -737,7 +772,7 @@ void RadarProductManagerImpl::EnableRefresh( void RadarProductManagerImpl::RefreshData( std::shared_ptr providerManager) { - logger_->debug("RefreshData: {}", providerManager->name()); + logger_->trace("RefreshData: {}", providerManager->name()); { std::unique_lock lock(providerManager->refreshTimerMutex_); @@ -765,13 +800,18 @@ void RadarProductManagerImpl::RefreshDataSync( auto [newObjects, totalObjects] = providerManager->provider_->Refresh(); - std::chrono::milliseconds interval = kFastRetryInterval_; + // Level2 chunked data is updated quickly and uses a faster interval + const std::chrono::milliseconds fastRetryInterval = + providerManager->isChunks_ ? kFastRetryIntervalChunks_ : + kFastRetryInterval_; + const std::chrono::milliseconds slowRetryInterval = + providerManager->isChunks_ ? kSlowRetryIntervalChunks_ : + kSlowRetryInterval_; + std::chrono::milliseconds interval = fastRetryInterval; if (totalObjects > 0) { - std::string key = providerManager->provider_->FindLatestKey(); - auto latestTime = providerManager->provider_->GetTimePointByKey(key); - + auto latestTime = providerManager->provider_->FindLatestTime(); auto updatePeriod = providerManager->provider_->update_period(); auto lastModified = providerManager->provider_->last_modified(); auto sinceLastModified = std::chrono::system_clock::now() - lastModified; @@ -786,12 +826,12 @@ void RadarProductManagerImpl::RefreshDataSync( { // If it has been at least 5 update periods since the file has // been last modified, slow the retry period - interval = kSlowRetryInterval_; + interval = slowRetryInterval; } - else if (interval < std::chrono::milliseconds {kFastRetryInterval_}) + else if (interval < std::chrono::milliseconds {fastRetryInterval}) { // The interval should be no quicker than the fast retry interval - interval = kFastRetryInterval_; + interval = fastRetryInterval; } if (newObjects > 0) @@ -805,14 +845,14 @@ void RadarProductManagerImpl::RefreshDataSync( logger_->info("[{}] No data found", providerManager->name()); // If no data is found, retry at the slow retry interval - interval = kSlowRetryInterval_; + interval = slowRetryInterval; } std::unique_lock const lock(providerManager->refreshTimerMutex_); if (providerManager->refreshEnabled_) { - logger_->debug( + logger_->trace( "[{}] Scheduled refresh in {:%M:%S}", providerManager->name(), std::chrono::duration_cast(interval)); @@ -861,10 +901,13 @@ RadarProductManager::GetActiveVolumeTimes( std::shared_lock refreshLock {p->refreshMapMutex_}; // For each entry in the refresh map (refresh is enabled) - for (auto& refreshEntry : p->refreshMap_) + for (auto& refreshSet : p->refreshMap_) { - // Add the provider for the current entry - providers.insert(refreshEntry.second->provider_); + for (const auto& refreshEntry : refreshSet.second) + { + // Add the provider for the current entry + providers.insert(refreshEntry->provider_); + } } // Unlock the refresh map @@ -923,7 +966,7 @@ void RadarProductManagerImpl::LoadProviderData( std::mutex& loadDataMutex, const std::shared_ptr& request) { - logger_->debug("LoadProviderData: {}, {}", + logger_->trace("LoadProviderData: {}, {}", providerManager->name(), scwx::util::TimeString(time)); @@ -943,7 +986,7 @@ void RadarProductManagerImpl::LoadProviderData( if (existingRecord != nullptr) { - logger_->debug( + logger_->trace( "Data previously loaded, loading from data cache"); } } @@ -951,13 +994,8 @@ void RadarProductManagerImpl::LoadProviderData( if (existingRecord == nullptr) { - std::string key = providerManager->provider_->FindKey(time); - - if (!key.empty()) - { - nexradFile = providerManager->provider_->LoadObjectByKey(key); - } - else + nexradFile = providerManager->provider_->LoadObjectByTime(time); + if (nexradFile == nullptr) { logger_->warn("Attempting to load object without key: {}", scwx::util::TimeString(time)); @@ -979,7 +1017,7 @@ void RadarProductManager::LoadLevel2Data( std::chrono::system_clock::time_point time, const std::shared_ptr& request) { - logger_->debug("LoadLevel2Data: {}", scwx::util::TimeString(time)); + logger_->trace("LoadLevel2Data: {}", scwx::util::TimeString(time)); p->LoadProviderData(time, p->level2ProviderManager_, @@ -1163,6 +1201,10 @@ void RadarProductManagerImpl::PopulateLevel2ProductTimes( level2ProductRecords_, level2ProductRecordMutex_, time); + PopulateProductTimes(level2ChunksProviderManager_, + level2ProductRecords_, + level2ProductRecordMutex_, + time); } void RadarProductManagerImpl::PopulateLevel3ProductTimes( @@ -1399,7 +1441,7 @@ std::shared_ptr RadarProductManagerImpl::StoreRadarProductRecord( std::shared_ptr record) { - logger_->debug("StoreRadarProductRecord()"); + logger_->trace("StoreRadarProductRecord()"); std::shared_ptr storedRecord = nullptr; @@ -1418,7 +1460,7 @@ RadarProductManagerImpl::StoreRadarProductRecord( if (storedRecord != nullptr) { - logger_->debug( + logger_->error( "Level 2 product previously loaded, loading from cache"); } } @@ -1503,37 +1545,87 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, std::vector elevationCuts {}; std::chrono::system_clock::time_point foundTime {}; - auto records = p->GetLevel2ProductRecords(time); + const bool isEpox = time == std::chrono::system_clock::time_point {}; + bool needArchive = true; + static const auto maxChunkDelay = std::chrono::minutes(10); + const std::chrono::system_clock::time_point firstValidChunkTime = + (isEpox ? std::chrono::system_clock::now() : time) - maxChunkDelay; - for (auto& recordPair : records) + // See if we have this one in the chunk provider. + auto chunkFile = std::dynamic_pointer_cast( + p->level2ChunksProviderManager_->provider_->LoadObjectByTime(time)); + if (chunkFile != nullptr) { - auto& record = recordPair.second; + std::tie(radarData, elevationCut, elevationCuts) = + chunkFile->GetElevationScan(dataBlockType, elevation, time); - if (record != nullptr) + if (radarData != nullptr) { - std::shared_ptr recordRadarData = nullptr; - float recordElevationCut = 0.0f; - std::vector recordElevationCuts; + auto& radarData0 = (*radarData)[0]; + foundTime = std::chrono::floor( + scwx::util::TimePoint(radarData0->modified_julian_date(), + radarData0->collection_time())); - std::tie(recordRadarData, recordElevationCut, recordElevationCuts) = - record->level2_file()->GetElevationScan( - dataBlockType, elevation, time); - - if (recordRadarData != nullptr) + const std::optional incomingElevation = + std::dynamic_pointer_cast( + p->level2ChunksProviderManager_->provider_) + ->GetCurrentElevation(); + if (incomingElevation != p->incomingLevel2Elevation_) { - auto& radarData0 = (*recordRadarData)[0]; - auto collectionTime = std::chrono::floor( - scwx::util::TimePoint(radarData0->modified_julian_date(), - radarData0->collection_time())); + p->incomingLevel2Elevation_ = incomingElevation; + Q_EMIT IncomingLevel2ElevationChanged(incomingElevation); + } - // Find the newest radar data, not newer than the selected time - if (radarData == nullptr || - (collectionTime <= time && foundTime < collectionTime)) + if (foundTime >= firstValidChunkTime) + { + needArchive = false; + } + } + } + + // It is not in the chunk provider, so get it from the archive + if (needArchive) + { + auto records = p->GetLevel2ProductRecords(time); + for (auto& recordPair : records) + { + auto& record = recordPair.second; + + if (record != nullptr) + { + std::shared_ptr recordRadarData = + nullptr; + float recordElevationCut = 0.0f; + std::vector recordElevationCuts; + + std::tie(recordRadarData, recordElevationCut, recordElevationCuts) = + record->level2_file()->GetElevationScan( + dataBlockType, elevation, time); + + if (recordRadarData != nullptr) { - radarData = recordRadarData; - elevationCut = recordElevationCut; - elevationCuts = std::move(recordElevationCuts); - foundTime = collectionTime; + auto& radarData0 = (*recordRadarData)[0]; + auto collectionTime = std::chrono::floor( + scwx::util::TimePoint(radarData0->modified_julian_date(), + radarData0->collection_time())); + + // Find the newest radar data, not newer than the selected time + if (radarData == nullptr || + (collectionTime <= time && foundTime < collectionTime) || + (isEpox && foundTime < collectionTime)) + { + radarData = recordRadarData; + elevationCut = recordElevationCut; + elevationCuts = std::move(recordElevationCuts); + foundTime = collectionTime; + + if (!p->incomingLevel2Elevation_.has_value()) + { + p->incomingLevel2Elevation_ = {}; + Q_EMIT IncomingLevel2ElevationChanged( + p->incomingLevel2Elevation_); + } + } } } } diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp index ee54f147..e8c72193 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp @@ -43,10 +43,11 @@ public: [[nodiscard]] const std::vector& coordinates(common::RadialSize radialSize, bool smoothingEnabled) const; - [[nodiscard]] const scwx::util::time_zone* default_time_zone() const; - [[nodiscard]] bool is_tdwr() const; - [[nodiscard]] float gate_size() const; - [[nodiscard]] std::string radar_id() const; + [[nodiscard]] const scwx::util::time_zone* default_time_zone() const; + [[nodiscard]] float gate_size() const; + [[nodiscard]] std::optional incoming_level_2_elevation() const; + [[nodiscard]] bool is_tdwr() const; + [[nodiscard]] std::string radar_id() const; [[nodiscard]] std::shared_ptr radar_site() const; void Initialize(); @@ -147,7 +148,9 @@ signals: void Level3ProductsChanged(); void NewDataAvailable(common::RadarProductGroup group, const std::string& product, + bool isChunks, std::chrono::system_clock::time_point latestTime); + void IncomingLevel2ElevationChanged(std::optional incomingElevation); private: std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index dafa801c..e8d0e380 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -656,6 +656,11 @@ std::vector MapWidget::GetElevationCuts() const } } +std::optional MapWidget::GetIncomingLevel2Elevation() const +{ + return p->radarProductManager_->incoming_level_2_elevation(); +} + common::Level2Product MapWidgetImpl::GetLevel2ProductOrDefault(const std::string& productName) const { @@ -1796,6 +1801,14 @@ void MapWidgetImpl::RadarProductManagerConnect() { if (radarProductManager_ != nullptr) { + connect(radarProductManager_.get(), + &manager::RadarProductManager::IncomingLevel2ElevationChanged, + this, + [this](std::optional incomingElevation) + { + Q_EMIT widget_->IncomingLevel2ElevationChanged( + incomingElevation); + }); connect(radarProductManager_.get(), &manager::RadarProductManager::Level3ProductsChanged, this, @@ -1830,6 +1843,7 @@ void MapWidgetImpl::RadarProductManagerConnect() this, [this](common::RadarProductGroup group, const std::string& product, + bool isChunks, std::chrono::system_clock::time_point latestTime) { if (autoRefreshEnabled_ && @@ -1837,71 +1851,81 @@ void MapWidgetImpl::RadarProductManagerConnect() (group == common::RadarProductGroup::Level2 || context_->radar_product() == product)) { - // Create file request - std::shared_ptr request = - std::make_shared( - radarProductManager_->radar_id()); - - // File request callback - if (autoUpdateEnabled_) + if (isChunks && autoUpdateEnabled_) { - connect( - request.get(), - &request::NexradFileRequest::RequestComplete, - this, - [=, - this](std::shared_ptr request) - { - // Select loaded record - auto record = request->radar_product_record(); + // Level 2 products may have multiple time points, + // ensure the latest is selected + widget_->SelectRadarProduct(group, product); + } + else + { + // Create file request + const std::shared_ptr request = + std::make_shared( + radarProductManager_->radar_id()); - // Validate record, and verify current map context - // still displays site and product - if (record != nullptr && - radarProductManager_ != nullptr && - radarProductManager_->radar_id() == - request->current_radar_site() && - context_->radar_product_group() == group && - (group == common::RadarProductGroup::Level2 || - context_->radar_product() == product)) + // File request callback + if (autoUpdateEnabled_) + { + connect( + request.get(), + &request::NexradFileRequest::RequestComplete, + this, + [group, product, this]( + const std::shared_ptr& + request) + { + // Select loaded record + auto record = request->radar_product_record(); + + // Validate record, and verify current map context + // still displays site and product + if (record != nullptr && + radarProductManager_ != nullptr && + radarProductManager_->radar_id() == + request->current_radar_site() && + context_->radar_product_group() == group && + (group == common::RadarProductGroup::Level2 || + context_->radar_product() == product)) + { + if (group == common::RadarProductGroup::Level2) + { + // Level 2 products may have multiple time + // points, ensure the latest is selected + widget_->SelectRadarProduct(group, product); + } + else + { + widget_->SelectRadarProduct(record); + } + } + }); + } + + // Load file + boost::asio::post( + threadPool_, + [group, latestTime, request, product, this]() + { + try { if (group == common::RadarProductGroup::Level2) { - // Level 2 products may have multiple time points, - // ensure the latest is selected - widget_->SelectRadarProduct(group, product); + radarProductManager_->LoadLevel2Data(latestTime, + request); } else { - widget_->SelectRadarProduct(record); + radarProductManager_->LoadLevel3Data( + product, latestTime, request); } } + catch (const std::exception& ex) + { + logger_->error(ex.what()); + } }); } - - // Load file - boost::asio::post( - threadPool_, - [=, this]() - { - try - { - if (group == common::RadarProductGroup::Level2) - { - radarProductManager_->LoadLevel2Data(latestTime, - request); - } - else - { - radarProductManager_->LoadLevel3Data( - product, latestTime, request); - } - } - catch (const std::exception& ex) - { - logger_->error(ex.what()); - } - }); } }, Qt::QueuedConnection); @@ -1916,6 +1940,10 @@ void MapWidgetImpl::RadarProductManagerDisconnect() &manager::RadarProductManager::NewDataAvailable, this, nullptr); + disconnect(radarProductManager_.get(), + &manager::RadarProductManager::IncomingLevel2ElevationChanged, + this, + nullptr); } } diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp index 845832e4..d474cd2e 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.hpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp @@ -45,6 +45,7 @@ public: GetAvailableLevel3Categories(); [[nodiscard]] std::optional GetElevation() const; [[nodiscard]] std::vector GetElevationCuts() const; + [[nodiscard]] std::optional GetIncomingLevel2Elevation() const; [[nodiscard]] std::vector GetLevel3Products(); [[nodiscard]] std::string GetMapStyle() const; [[nodiscard]] common::RadarProductGroup GetRadarProductGroup() const; @@ -184,6 +185,7 @@ signals: void RadarSweepUpdated(); void RadarSweepNotUpdated(types::NoUpdateReason reason); void WidgetPainted(); + void IncomingLevel2ElevationChanged(std::optional incomingElevation); }; } // namespace map diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index 0d2b7125..7bda264f 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -159,8 +159,6 @@ void RadarProductLayer::Initialize() void RadarProductLayer::UpdateSweep() { - logger_->debug("UpdateSweep()"); - gl::OpenGLFunctions& gl = context()->gl(); boost::timer::cpu_timer timer; @@ -172,9 +170,10 @@ void RadarProductLayer::UpdateSweep() std::try_to_lock); if (!sweepLock.owns_lock()) { - logger_->debug("Sweep locked, deferring update"); + logger_->trace("Sweep locked, deferring update"); return; } + logger_->debug("UpdateSweep()"); p->sweepNeedsUpdate_ = false; diff --git a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp index 1b851e72..3d530733 100644 --- a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -29,16 +30,15 @@ public: explicit Level2SettingsWidgetImpl(Level2SettingsWidget* self) : self_ {self}, layout_ {new QVBoxLayout(self)}, - elevationGroupBox_ {}, elevationButtons_ {}, - elevationCuts_ {}, - elevationButtonsChanged_ {false}, - resizeElevationButtons_ {false}, - settingsGroupBox_ {}, - declutterCheckBox_ {} + elevationCuts_ {} { + // NOLINTBEGIN(cppcoreguidelines-owning-memory) Qt takes care of this layout_->setContentsMargins(0, 0, 0, 0); + incomingElevationLabel_ = new QLabel("", self); + layout_->addWidget(incomingElevationLabel_); + elevationGroupBox_ = new QGroupBox(tr("Elevation"), self); new ui::FlowLayout(elevationGroupBox_); layout_->addWidget(elevationGroupBox_); @@ -51,6 +51,7 @@ public: settingsLayout->addWidget(declutterCheckBox_); settingsGroupBox_->setVisible(false); + // NOLINTEND(cppcoreguidelines-owning-memory) Qt takes care of this QObject::connect(hotkeyManager_.get(), &manager::HotkeyManager::HotkeyPressed, @@ -66,14 +67,15 @@ public: Level2SettingsWidget* self_; QLayout* layout_; - QGroupBox* elevationGroupBox_; + QGroupBox* elevationGroupBox_ {}; + QLabel* incomingElevationLabel_ {}; std::list elevationButtons_; std::vector elevationCuts_; - bool elevationButtonsChanged_; - bool resizeElevationButtons_; + bool elevationButtonsChanged_ {}; + bool resizeElevationButtons_ {}; - QGroupBox* settingsGroupBox_; - QCheckBox* declutterCheckBox_; + QGroupBox* settingsGroupBox_ {}; + QCheckBox* declutterCheckBox_ {}; float currentElevation_ {}; QToolButton* currentElevationButton_ {nullptr}; @@ -240,12 +242,29 @@ void Level2SettingsWidget::UpdateElevationSelection(float elevation) p->currentElevationButton_ = newElevationButton; } +void Level2SettingsWidget::UpdateIncomingElevation( + std::optional incomingElevation) +{ + if (incomingElevation.has_value()) + { + p->incomingElevationLabel_->setText( + "Incoming Elevation: " + QString::number(*incomingElevation, 'f', 1) + + common::Characters::DEGREE); + } + else + { + p->incomingElevationLabel_->setText("Incoming Elevation: None"); + } +} + void Level2SettingsWidget::UpdateSettings(map::MapWidget* activeMap) { std::optional currentElevationOption = activeMap->GetElevation(); const float currentElevation = currentElevationOption.has_value() ? *currentElevationOption : 0.0f; - std::vector elevationCuts = activeMap->GetElevationCuts(); + const std::vector elevationCuts = activeMap->GetElevationCuts(); + const std::optional incomingElevation = + activeMap->GetIncomingLevel2Elevation(); if (p->elevationCuts_ != elevationCuts) { @@ -279,6 +298,7 @@ void Level2SettingsWidget::UpdateSettings(map::MapWidget* activeMap) } UpdateElevationSelection(currentElevation); + UpdateIncomingElevation(incomingElevation); } } // namespace ui diff --git a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.hpp b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.hpp index ce2e443f..32f788bb 100644 --- a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.hpp +++ b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.hpp @@ -2,6 +2,8 @@ #include +#include + namespace scwx { namespace qt @@ -23,6 +25,7 @@ public: void showEvent(QShowEvent* event) override; void UpdateElevationSelection(float elevation); + void UpdateIncomingElevation(std::optional incomingElevation); void UpdateSettings(map::MapWidget* activeMap); signals: diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 6344dff0..9ead358b 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -561,7 +561,7 @@ void Level2ProductView::ComputeSweep() Q_EMIT SweepNotComputed(types::NoUpdateReason::NotLoaded); return; } - if (radarData == p->elevationScan_ && + if ((radarData == p->elevationScan_) && smoothingEnabled == p->lastSmoothingEnabled_ && (showSmoothedRangeFolding == p->lastShowSmoothedRangeFolding_ || !smoothingEnabled)) diff --git a/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp b/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp index c33494c2..3200dcca 100644 --- a/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp @@ -116,8 +116,9 @@ void OverlayProductView::Impl::ConnectRadarProductManager() radarProductManager_.get(), &manager::RadarProductManager::NewDataAvailable, self_, - [this](common::RadarProductGroup group, - const std::string& product, + [this](common::RadarProductGroup group, + const std::string& product, + bool /*isChunks*/, std::chrono::system_clock::time_point latestTime) { if (autoRefreshEnabled_ && diff --git a/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp new file mode 100644 index 00000000..abd70787 --- /dev/null +++ b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include +#include + +#include + +namespace Aws::S3 +{ +class S3Client; +} // namespace Aws::S3 + +namespace scwx::provider +{ + +/** + * @brief AWS Level 2 Data Provider + */ +class AwsLevel2ChunksDataProvider : public NexradDataProvider +{ +public: + explicit AwsLevel2ChunksDataProvider(const std::string& radarSite); + explicit AwsLevel2ChunksDataProvider(const std::string& radarSite, + const std::string& bucketName, + const std::string& region); + ~AwsLevel2ChunksDataProvider() override; + + AwsLevel2ChunksDataProvider(const AwsLevel2ChunksDataProvider&) = delete; + AwsLevel2ChunksDataProvider& + operator=(const AwsLevel2ChunksDataProvider&) = delete; + + AwsLevel2ChunksDataProvider(AwsLevel2ChunksDataProvider&&) noexcept; + AwsLevel2ChunksDataProvider& + operator=(AwsLevel2ChunksDataProvider&&) noexcept; + + [[nodiscard]] std::chrono::system_clock::time_point + GetTimePointByKey(const std::string& key) const override; + + [[nodiscard]] size_t cache_size() const override; + + [[nodiscard]] std::chrono::system_clock::time_point + last_modified() const override; + [[nodiscard]] std::chrono::seconds update_period() const override; + + std::string FindKey(std::chrono::system_clock::time_point time) override; + std::string FindLatestKey() override; + std::chrono::system_clock::time_point FindLatestTime() override; + std::vector + GetTimePointsByDate(std::chrono::system_clock::time_point date) override; + std::tuple + ListObjects(std::chrono::system_clock::time_point date) override; + std::shared_ptr + LoadObjectByKey(const std::string& key) override; + std::shared_ptr + LoadObjectByTime(std::chrono::system_clock::time_point time) override; + std::pair Refresh() override; + + void RequestAvailableProducts() override; + std::vector GetAvailableProducts() override; + + std::optional GetCurrentElevation(); + + void SetLevel2DataProvider( + const std::shared_ptr& provider); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace scwx::provider diff --git a/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp b/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp index 462d293d..d6ddcd7c 100644 --- a/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp +++ b/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp @@ -39,12 +39,15 @@ public: std::string FindKey(std::chrono::system_clock::time_point time) override; std::string FindLatestKey() override; + std::chrono::system_clock::time_point FindLatestTime() override; std::vector GetTimePointsByDate(std::chrono::system_clock::time_point date) override; std::tuple ListObjects(std::chrono::system_clock::time_point date) override; std::shared_ptr LoadObjectByKey(const std::string& key) override; + std::shared_ptr + LoadObjectByTime(std::chrono::system_clock::time_point time) override; std::pair Refresh() override; protected: diff --git a/wxdata/include/scwx/provider/nexrad_data_provider.hpp b/wxdata/include/scwx/provider/nexrad_data_provider.hpp index 14a75815..2a7320d2 100644 --- a/wxdata/include/scwx/provider/nexrad_data_provider.hpp +++ b/wxdata/include/scwx/provider/nexrad_data_provider.hpp @@ -59,6 +59,13 @@ public: */ virtual std::string FindLatestKey() = 0; + /** + * Finds the most recent time in the cache. + * + * @return NEXRAD data key + */ + virtual std::chrono::system_clock::time_point FindLatestTime() = 0; + /** * Lists NEXRAD objects for the date supplied, and adds them to the cache. * @@ -81,6 +88,16 @@ public: virtual std::shared_ptr LoadObjectByKey(const std::string& key) = 0; + /** + * Loads a NEXRAD file object at the given time + * + * @param time NEXRAD time + * + * @return NEXRAD data + */ + virtual std::shared_ptr + LoadObjectByTime(std::chrono::system_clock::time_point time) = 0; + /** * Lists NEXRAD objects for the current date, and adds them to the cache. If * no objects have been added to the cache for the current date, the previous diff --git a/wxdata/include/scwx/provider/nexrad_data_provider_factory.hpp b/wxdata/include/scwx/provider/nexrad_data_provider_factory.hpp index bdd51b9e..510ee14c 100644 --- a/wxdata/include/scwx/provider/nexrad_data_provider_factory.hpp +++ b/wxdata/include/scwx/provider/nexrad_data_provider_factory.hpp @@ -27,6 +27,9 @@ public: static std::shared_ptr CreateLevel2DataProvider(const std::string& radarSite); + static std::shared_ptr + CreateLevel2ChunksDataProvider(const std::string& radarSite); + static std::shared_ptr CreateLevel3DataProvider(const std::string& radarSite, const std::string& product); diff --git a/wxdata/include/scwx/wsr88d/ar2v_file.hpp b/wxdata/include/scwx/wsr88d/ar2v_file.hpp index 1f3ab0cc..64319283 100644 --- a/wxdata/include/scwx/wsr88d/ar2v_file.hpp +++ b/wxdata/include/scwx/wsr88d/ar2v_file.hpp @@ -32,6 +32,9 @@ public: Ar2vFile(Ar2vFile&&) noexcept; Ar2vFile& operator=(Ar2vFile&&) noexcept; + Ar2vFile(const std::shared_ptr& current, + const std::shared_ptr& last); + std::uint32_t julian_date() const; std::uint32_t milliseconds() const; std::string icao() const; @@ -53,6 +56,9 @@ public: bool LoadFile(const std::string& filename); bool LoadData(std::istream& is); + bool LoadLDMRecords(std::istream& is); + bool IndexFile(); + private: std::unique_ptr p; }; diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp new file mode 100644 index 00000000..74a9afd7 --- /dev/null +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -0,0 +1,783 @@ +#include "scwx/wsr88d/rda/digital_radar_data.hpp" +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +// Avoid circular refrence errors in boost +// NOLINTBEGIN(misc-header-include-cycle) +#if defined(_MSC_VER) +# pragma warning(push, 0) +#endif + +#include +#include +#include + +#if defined(_MSC_VER) +# pragma warning(pop) +#endif +// NOLINTEND(misc-header-include-cycle) + +#if (__cpp_lib_chrono < 201907L) +# include +#endif + +namespace scwx::provider +{ +static const std::string logPrefix_ = + "scwx::provider::aws_level2_chunks_data_provider"; +static const auto logger_ = util::Logger::Create(logPrefix_); + +static const std::string kDefaultBucketName_ = "unidata-nexrad-level2-chunks"; +static const std::string kDefaultRegion_ = "us-east-1"; + +class AwsLevel2ChunksDataProvider::Impl +{ +public: + struct ScanRecord + { + explicit ScanRecord(std::string prefix, bool valid = true) : + valid_ {valid}, + prefix_ {std::move(prefix)}, + nexradFile_ {}, + lastModified_ {}, + lastKey_ {""} + { + } + ~ScanRecord() = default; + ScanRecord(const ScanRecord&) = default; + ScanRecord(ScanRecord&&) = default; + ScanRecord& operator=(const ScanRecord&) = default; + ScanRecord& operator=(ScanRecord&&) = default; + + bool valid_; + std::string prefix_; + std::shared_ptr nexradFile_; + std::chrono::system_clock::time_point time_; + std::chrono::system_clock::time_point lastModified_; + std::chrono::system_clock::time_point secondLastModified_; + std::string lastKey_; + int nextFile_ {1}; + bool hasAllFiles_ {false}; + }; + + explicit Impl(AwsLevel2ChunksDataProvider* self, + std::string radarSite, + std::string bucketName, + std::string region) : + radarSite_ {std::move(radarSite)}, + bucketName_ {std::move(bucketName)}, + region_ {std::move(region)}, + client_ {nullptr}, + scanTimes_ {}, + lastScan_ {"", false}, + currentScan_ {"", false}, + scansMutex_ {}, + lastTimeListed_ {}, + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) about average + updatePeriod_ {7}, + level2DataProvider_ {}, + self_ {self} + { + // Disable HTTP request for region + util::SetEnvironment("AWS_EC2_METADATA_DISABLED", "true"); + + // Use anonymous credentials + const Aws::Auth::AWSCredentials credentials {}; + + Aws::Client::ClientConfiguration config; + config.region = region_; + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) arbitrary + config.connectTimeoutMs = 10000; + + client_ = std::make_shared( + credentials, + Aws::MakeShared( + Aws::S3::S3Client::GetAllocationTag()), + config); + } + ~Impl() = default; + Impl(const Impl&) = delete; + Impl(Impl&&) = delete; + Impl& operator=(const Impl&) = delete; + Impl& operator=(Impl&&) = delete; + + std::chrono::system_clock::time_point GetScanTime(const std::string& prefix); + int GetScanNumber(const std::string& prefix); + + bool LoadScan(Impl::ScanRecord& scanRecord); + std::tuple ListObjects(); + + std::string radarSite_; + std::string bucketName_; + std::string region_; + std::shared_ptr client_; + + std::mutex refreshMutex_; + + std::unordered_map + scanTimes_; + ScanRecord lastScan_; + ScanRecord currentScan_; + std::shared_mutex scansMutex_; + std::chrono::system_clock::time_point lastTimeListed_; + + std::chrono::seconds updatePeriod_; + + std::weak_ptr level2DataProvider_; + + AwsLevel2ChunksDataProvider* self_; +}; + +AwsLevel2ChunksDataProvider::AwsLevel2ChunksDataProvider( + const std::string& radarSite) : + AwsLevel2ChunksDataProvider(radarSite, kDefaultBucketName_, kDefaultRegion_) +{ +} + +AwsLevel2ChunksDataProvider::AwsLevel2ChunksDataProvider( + const std::string& radarSite, + const std::string& bucketName, + const std::string& region) : + p(std::make_unique(this, radarSite, bucketName, region)) +{ +} + +AwsLevel2ChunksDataProvider::~AwsLevel2ChunksDataProvider() = default; + +std::chrono::system_clock::time_point +AwsLevel2ChunksDataProvider::GetTimePointByKey(const std::string& key) const +{ + std::chrono::system_clock::time_point time {}; + + const size_t lastSeparator = key.rfind('/'); + const size_t offset = + (lastSeparator == std::string::npos) ? 0 : lastSeparator + 1; + + // Filename format is YYYYMMDD-TTTTTT-AAA-B + static const size_t formatSize = std::string("YYYYMMDD-TTTTTT").size(); + + if (key.size() >= offset + formatSize) + { + using namespace std::chrono; + +#if (__cpp_lib_chrono < 201907L) + using namespace date; +#endif + + static const std::string timeFormat {"%Y%m%d-%H%M%S"}; + + std::string timeStr {key.substr(offset, formatSize)}; + std::istringstream in {timeStr}; + in >> parse(timeFormat, time); + + if (in.fail()) + { + logger_->warn("Invalid time: \"{}\"", timeStr); + } + } + else + { + logger_->warn("Time not parsable from key: \"{}\"", key); + } + + return time; +} + +size_t AwsLevel2ChunksDataProvider::cache_size() const +{ + return 2; +} + +std::chrono::system_clock::time_point +AwsLevel2ChunksDataProvider::last_modified() const +{ + // There is a slight delay between the "modified time" and when it is + // actually available. Radar product manager uses this as available time + static const auto extra = std::chrono::seconds(2); + + const std::shared_lock lock(p->scansMutex_); + if (p->currentScan_.valid_ && p->currentScan_.lastModified_ != + std::chrono::system_clock::time_point {}) + { + return p->currentScan_.lastModified_ + extra; + } + else if (p->lastScan_.valid_ && p->lastScan_.lastModified_ != + std::chrono::system_clock::time_point {}) + { + return p->lastScan_.lastModified_ + extra; + } + else + { + return {}; + } +} +std::chrono::seconds AwsLevel2ChunksDataProvider::update_period() const +{ + const std::shared_lock lock(p->scansMutex_); + // get update period from time between chunks + if (p->currentScan_.valid_ && p->currentScan_.nextFile_ > 2) + { + auto delta = + p->currentScan_.lastModified_ - p->currentScan_.secondLastModified_; + return std::chrono::duration_cast(delta); + } + else if (p->lastScan_.valid_ && p->lastScan_.nextFile_ > 2) + { + auto delta = + p->lastScan_.lastModified_ - p->lastScan_.secondLastModified_; + return std::chrono::duration_cast(delta); + } + + // default to a set update period + return p->updatePeriod_; +} + +std::string +AwsLevel2ChunksDataProvider::FindKey(std::chrono::system_clock::time_point time) +{ + logger_->debug("FindKey: {}", util::TimeString(time)); + + const std::shared_lock lock(p->scansMutex_); + if (p->currentScan_.valid_ && time >= p->currentScan_.time_) + { + return p->currentScan_.prefix_; + } + else if (p->lastScan_.valid_ && time >= p->lastScan_.time_) + { + return p->lastScan_.prefix_; + } + + return {}; +} + +std::string AwsLevel2ChunksDataProvider::FindLatestKey() +{ + const std::shared_lock lock(p->scansMutex_); + if (!p->currentScan_.valid_) + { + return ""; + } + + return p->currentScan_.prefix_; +} + +std::chrono::system_clock::time_point +AwsLevel2ChunksDataProvider::FindLatestTime() +{ + const std::shared_lock lock(p->scansMutex_); + if (!p->currentScan_.valid_) + { + return {}; + } + + return p->currentScan_.time_; +} + +std::vector +AwsLevel2ChunksDataProvider::GetTimePointsByDate( + std::chrono::system_clock::time_point /*date*/) +{ + return {}; +} + +std::chrono::system_clock::time_point +AwsLevel2ChunksDataProvider::Impl::GetScanTime(const std::string& prefix) +{ + using namespace std::chrono; + const auto& scanTimeIt = scanTimes_.find(prefix); // O(log(n)) + if (scanTimeIt != scanTimes_.cend()) + { + // If the time is greater than 2 hours ago, it may be a new scan + auto replaceBy = system_clock::now() - hours {2}; + if (scanTimeIt->second > replaceBy) + { + return scanTimeIt->second; + } + } + + Aws::S3::Model::ListObjectsV2Request request; + request.SetBucket(bucketName_); + request.SetPrefix(prefix); + request.SetDelimiter("/"); + request.SetMaxKeys(1); + + auto outcome = client_->ListObjectsV2(request); + if (outcome.IsSuccess()) + { + auto timePoint = self_->GetTimePointByKey( + outcome.GetResult().GetContents().at(0).GetKey()); + scanTimes_.insert_or_assign(prefix, timePoint); + return timePoint; + } + + return {}; +} + +std::tuple +AwsLevel2ChunksDataProvider::Impl::ListObjects() +{ + size_t newObjects = 0; + const size_t totalObjects = 0; + + const std::chrono::system_clock::time_point now = + std::chrono::system_clock::now(); + + if (currentScan_.valid_ && !currentScan_.hasAllFiles_ && + lastTimeListed_ + std::chrono::minutes(2) > now) + { + return {true, newObjects, totalObjects}; + } + logger_->trace("ListObjects"); + lastTimeListed_ = now; + + const std::string prefix = radarSite_ + "/"; + + Aws::S3::Model::ListObjectsV2Request request; + request.SetBucket(bucketName_); + request.SetPrefix(prefix); + request.SetDelimiter("/"); + + auto outcome = client_->ListObjectsV2(request); + + if (outcome.IsSuccess()) + { + auto& scans = outcome.GetResult().GetCommonPrefixes(); + logger_->trace("Found {} scans", scans.size()); + + if (scans.size() > 0) + { + // find latest scan + auto scanNumberMap = std::map(); + + for (auto& scan : scans) // O(n log(n)) n <= 999 + { + const std::string& scanPrefix = scan.GetPrefix(); + scanNumberMap.insert_or_assign(GetScanNumber(scanPrefix), + scanPrefix); + } + + // Start with last scan + int previousScanNumber = scanNumberMap.crbegin()->first; + const int firstScanNumber = scanNumberMap.cbegin()->first; + + // Look for a gap in scan numbers. This indicates that is the latest + // scan. + + auto possibleLastNumbers = std::unordered_set(); + // This indicates that highest number scan may be the last scan + // (including if there is only 1 scan) + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) + if (previousScanNumber != 999 || firstScanNumber != 1) + { + possibleLastNumbers.emplace(previousScanNumber); + } + // Have already checked scan with highest number, so skip first + previousScanNumber = firstScanNumber; + bool first = true; + for (const auto& scan : scanNumberMap) + { + if (first) + { + first = false; + continue; + } + if (scan.first != previousScanNumber + 1) + { + possibleLastNumbers.emplace(previousScanNumber); + } + previousScanNumber = scan.first; + } + + if (possibleLastNumbers.empty()) + { + logger_->warn("Could not find last scan"); + return {false, 0, 0}; + } + + int lastScanNumber = -1; + std::chrono::system_clock::time_point lastScanTime = {}; + std::string lastScanPrefix; + + for (const int scanNumber : possibleLastNumbers) + { + const std::string& scanPrefix = scanNumberMap.at(scanNumber); + auto scanTime = GetScanTime(scanPrefix); + if (scanTime > lastScanTime) + { + lastScanTime = scanTime; + lastScanPrefix = scanPrefix; + lastScanNumber = scanNumber; + } + } + + const int secondLastScanNumber = + // 999 is the last file possible + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) + lastScanNumber == 1 ? 999 : lastScanNumber - 1; + + const auto& secondLastScanPrefix = + scanNumberMap.find(secondLastScanNumber); + + if (!currentScan_.valid_ || currentScan_.prefix_ != lastScanPrefix) + { + if (currentScan_.valid_ && + (secondLastScanPrefix == scanNumberMap.cend() || + currentScan_.prefix_ == secondLastScanPrefix->second)) + { + lastScan_ = currentScan_; + } + else if (secondLastScanPrefix != scanNumberMap.cend()) + { + lastScan_.valid_ = true; + lastScan_.prefix_ = secondLastScanPrefix->second; + lastScan_.nexradFile_ = nullptr; + lastScan_.time_ = GetScanTime(secondLastScanPrefix->second); + lastScan_.lastModified_ = {}; + lastScan_.secondLastModified_ = {}; + lastScan_.lastKey_ = ""; + lastScan_.nextFile_ = 1; + lastScan_.hasAllFiles_ = false; + newObjects += 1; + } + + currentScan_.valid_ = true; + currentScan_.prefix_ = lastScanPrefix; + currentScan_.nexradFile_ = nullptr; + currentScan_.time_ = lastScanTime; + currentScan_.lastModified_ = {}; + currentScan_.secondLastModified_ = {}; + currentScan_.lastKey_ = ""; + currentScan_.nextFile_ = 1; + currentScan_.hasAllFiles_ = false; + newObjects += 1; + } + } + } + + return {true, newObjects, totalObjects}; +} + +std::tuple +AwsLevel2ChunksDataProvider::ListObjects(std::chrono::system_clock::time_point) +{ + return {true, 0, 0}; +} + +std::shared_ptr +AwsLevel2ChunksDataProvider::LoadObjectByKey(const std::string& /*prefix*/) +{ + return nullptr; +} + +bool AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) +{ + if (!scanRecord.valid_) + { + logger_->warn("Tried to load scan which was not listed yet"); + return false; + } + else if (scanRecord.hasAllFiles_) + { + return false; + } + + Aws::S3::Model::ListObjectsV2Request listRequest; + listRequest.SetBucket(bucketName_); + listRequest.SetPrefix(scanRecord.prefix_); + listRequest.SetDelimiter("/"); + if (!scanRecord.lastKey_.empty()) + { + listRequest.SetStartAfter(scanRecord.lastKey_); + } + + auto listOutcome = client_->ListObjectsV2(listRequest); + if (!listOutcome.IsSuccess()) + { + logger_->warn("Could not find scan at {}", scanRecord.prefix_); + return false; + } + + bool hasNew = false; + auto& chunks = listOutcome.GetResult().GetContents(); + logger_->trace("Found {} new chunks.", chunks.size()); + for (const auto& chunk : chunks) + { + const std::string& key = chunk.GetKey(); + + // KIND/585/20250324-134727-001-S + // KIND/5/20250324-134727-001-S + static const size_t firstSlash = std::string("KIND/").size(); + const size_t secondSlash = key.find('/', firstSlash); + static const size_t startNumberPosOffset = + std::string("/20250324-134727-").size(); + const size_t startNumberPos = secondSlash + startNumberPosOffset; + const std::string& keyNumberStr = key.substr(startNumberPos, 3); + const int keyNumber = std::stoi(keyNumberStr); + // As far as order goes, only the first one matters. This may cause some + // issues if keys come in out of order, but usually they just skip chunks + if (scanRecord.nextFile_ == 1 && keyNumber != scanRecord.nextFile_) + { + logger_->warn("Chunk found that was not in order {} {}", + scanRecord.lastKey_, + key); + continue; + } + + // Now we want the ending char + // KIND/585/20250324-134727-001-S + static const size_t charPos = std::string("/20250324-134727-001-").size(); + if (secondSlash + charPos >= key.size()) + { + logger_->warn("Chunk key was not long enough"); + continue; + } + const char keyChar = key[secondSlash + charPos]; + + Aws::S3::Model::GetObjectRequest objectRequest; + objectRequest.SetBucket(bucketName_); + objectRequest.SetKey(key); + + auto outcome = client_->GetObject(objectRequest); + + if (!outcome.IsSuccess()) + { + logger_->warn("Could not get object: {}", + outcome.GetError().GetMessage()); + return hasNew; + } + + auto& body = outcome.GetResultWithOwnership().GetBody(); + + switch (keyChar) + { + case 'S': + { // First chunk + scanRecord.nexradFile_ = std::make_shared(); + if (!scanRecord.nexradFile_->LoadData(body)) + { + logger_->warn("Failed to load first chunk"); + return hasNew; + } + break; + } + case 'I': + { // Middle chunk + if (!scanRecord.nexradFile_->LoadLDMRecords(body)) + { + logger_->warn("Failed to load middle chunk"); + return hasNew; + } + break; + } + case 'E': + { // Last chunk + if (!scanRecord.nexradFile_->LoadLDMRecords(body)) + { + logger_->warn("Failed to load last chunk"); + return hasNew; + } + scanRecord.hasAllFiles_ = true; + break; + } + default: + logger_->warn("Could not load chunk with unknown char"); + return hasNew; + } + hasNew = true; + + const std::chrono::seconds lastModifiedSeconds { + outcome.GetResult().GetLastModified().Seconds()}; + const std::chrono::system_clock::time_point lastModified { + lastModifiedSeconds}; + + scanRecord.secondLastModified_ = scanRecord.lastModified_; + scanRecord.lastModified_ = lastModified; + + scanRecord.nextFile_ = keyNumber + 1; + scanRecord.lastKey_ = key; + } + + if (scanRecord.nexradFile_ == nullptr) + { + logger_->warn("Could not load file"); + } + else if (hasNew) + { + scanRecord.nexradFile_->IndexFile(); + } + + return hasNew; +} + +std::shared_ptr +AwsLevel2ChunksDataProvider::LoadObjectByTime( + std::chrono::system_clock::time_point time) +{ + const std::unique_lock lock(p->scansMutex_); + static const std::chrono::system_clock::time_point epoch {}; + + if (p->currentScan_.valid_ && + (time == epoch || time >= p->currentScan_.time_)) + { + return std::make_shared(p->currentScan_.nexradFile_, + p->lastScan_.nexradFile_); + } + else if (p->lastScan_.valid_ && time >= p->lastScan_.time_) + { + return p->lastScan_.nexradFile_; + } + else + { + return nullptr; + } +} + +int AwsLevel2ChunksDataProvider::Impl::GetScanNumber(const std::string& prefix) +{ + // KIND/585/20250324-134727-001-S + static const size_t firstSlash = std::string("KIND/").size(); + const std::string& prefixNumberStr = prefix.substr(firstSlash, 3); + return std::stoi(prefixNumberStr); +} + +std::pair AwsLevel2ChunksDataProvider::Refresh() +{ + using namespace std::chrono; + + boost::timer::cpu_timer timer {}; + timer.start(); + + const std::unique_lock lock(p->refreshMutex_); + const std::unique_lock scanLock(p->scansMutex_); + + auto [success, newObjects, totalObjects] = p->ListObjects(); + + auto threadPool = boost::asio::thread_pool(3); + bool newCurrent = false; + bool newLast = false; + if (p->currentScan_.valid_) + { + boost::asio::post(threadPool, + [this, &newCurrent]() + { newCurrent = p->LoadScan(p->currentScan_); }); + totalObjects += 1; + } + + if (p->lastScan_.valid_) + { + totalObjects += 1; + boost::asio::post( + threadPool, + [this, &newLast]() + { + if (!p->lastScan_.hasAllFiles_) + { + // If we have chunks, use chunks + if (p->lastScan_.nextFile_ != 1) + { + newLast = p->LoadScan(p->lastScan_); + } + else + { + auto level2DataProvider = p->level2DataProvider_.lock(); + if (level2DataProvider != nullptr) + { + level2DataProvider->ListObjects(p->lastScan_.time_); + p->lastScan_.nexradFile_ = + std::dynamic_pointer_cast( + level2DataProvider->LoadObjectByTime( + p->lastScan_.time_)); + if (p->lastScan_.nexradFile_ != nullptr) + { + p->lastScan_.hasAllFiles_ = true; + } + } + // Fall back to chunks if files did not load + newLast = p->lastScan_.nexradFile_ != nullptr || + p->LoadScan(p->lastScan_); + } + } + }); + } + + threadPool.join(); + if (newCurrent) + { + newObjects += 1; + } + if (newLast) + { + newObjects += 1; + } + + timer.stop(); + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) format to 6 digits + logger_->debug("Refresh() in {}", timer.format(6, "%ws")); + return std::make_pair(newObjects, totalObjects); +} + +void AwsLevel2ChunksDataProvider::RequestAvailableProducts() {} +std::vector AwsLevel2ChunksDataProvider::GetAvailableProducts() +{ + return {}; +} + +AwsLevel2ChunksDataProvider::AwsLevel2ChunksDataProvider( + AwsLevel2ChunksDataProvider&&) noexcept = default; +AwsLevel2ChunksDataProvider& AwsLevel2ChunksDataProvider::operator=( + AwsLevel2ChunksDataProvider&&) noexcept = default; + +std::optional AwsLevel2ChunksDataProvider::GetCurrentElevation() +{ + if (!p->currentScan_.valid_ || p->currentScan_.nexradFile_ == nullptr) + { + // Does not have any scan elevation. + return {}; + } + + auto vcpData = p->currentScan_.nexradFile_->vcp_data(); + auto radarData = p->currentScan_.nexradFile_->radar_data(); + if (radarData.size() == 0) + { + // Does not have any scan elevation. + return {}; + } + + const auto& lastElevation = radarData.crbegin(); + const std::shared_ptr digitalRadarData0 = + std::dynamic_pointer_cast( + lastElevation->second->cbegin()->second); + + if (vcpData != nullptr) + { + return static_cast(vcpData->elevation_angle(lastElevation->first)); + } + else if (digitalRadarData0 != nullptr) + { + return digitalRadarData0->elevation_angle().value(); + } + + return {}; +} + +void AwsLevel2ChunksDataProvider::SetLevel2DataProvider( + const std::shared_ptr& provider) +{ + p->level2DataProvider_ = provider; +} + +} // namespace scwx::provider diff --git a/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp b/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp index c4ac523b..dceb45d8 100644 --- a/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp @@ -170,6 +170,11 @@ std::string AwsNexradDataProvider::FindLatestKey() return key; } +std::chrono::system_clock::time_point AwsNexradDataProvider::FindLatestTime() +{ + return GetTimePointByKey(FindLatestKey()); +} + std::vector AwsNexradDataProvider::GetTimePointsByDate( std::chrono::system_clock::time_point date) @@ -327,6 +332,20 @@ AwsNexradDataProvider::LoadObjectByKey(const std::string& key) return nexradFile; } +std::shared_ptr AwsNexradDataProvider::LoadObjectByTime( + std::chrono::system_clock::time_point time) +{ + const std::string key = FindKey(time); + if (key.empty()) + { + return nullptr; + } + else + { + return LoadObjectByKey(key); + } +} + std::pair AwsNexradDataProvider::Refresh() { using namespace std::chrono; diff --git a/wxdata/source/scwx/provider/nexrad_data_provider_factory.cpp b/wxdata/source/scwx/provider/nexrad_data_provider_factory.cpp index 5e75fd96..dcaf7c9f 100644 --- a/wxdata/source/scwx/provider/nexrad_data_provider_factory.cpp +++ b/wxdata/source/scwx/provider/nexrad_data_provider_factory.cpp @@ -1,5 +1,6 @@ #include #include +#include #include namespace scwx @@ -17,6 +18,13 @@ NexradDataProviderFactory::CreateLevel2DataProvider( return std::make_unique(radarSite); } +std::shared_ptr +NexradDataProviderFactory::CreateLevel2ChunksDataProvider( + const std::string& radarSite) +{ + return std::make_unique(radarSite); +} + std::shared_ptr NexradDataProviderFactory::CreateLevel3DataProvider( const std::string& radarSite, const std::string& product) diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index ed976c24..f4a71c15 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -137,7 +138,7 @@ Ar2vFile::GetElevationScan(rda::DataBlockType dataBlockType, float elevation, std::chrono::system_clock::time_point time) const { - logger_->debug("GetElevationScan: {} degrees", elevation); + logger_->trace("GetElevationScan: {} degrees", elevation); std::shared_ptr elevationScan = nullptr; float elevationCut = 0.0f; @@ -272,7 +273,7 @@ bool Ar2vFile::LoadData(std::istream& is) std::size_t Ar2vFileImpl::DecompressLDMRecords(std::istream& is) { - logger_->debug("Decompressing LDM Records"); + logger_->trace("Decompressing LDM Records"); std::size_t numRecords = 0; @@ -320,14 +321,14 @@ std::size_t Ar2vFileImpl::DecompressLDMRecords(std::istream& is) ++numRecords; } - logger_->debug("Decompressed {} LDM Records", numRecords); + logger_->trace("Decompressed {} LDM Records", numRecords); return numRecords; } void Ar2vFileImpl::ParseLDMRecords() { - logger_->debug("Parsing LDM Records"); + logger_->trace("Parsing LDM Records"); std::size_t count = 0; @@ -444,13 +445,11 @@ void Ar2vFileImpl::ProcessRadarData( void Ar2vFileImpl::IndexFile() { - logger_->debug("Indexing file"); - - constexpr float scaleFactor = 8.0f / 0.043945f; + logger_->trace("Indexing file"); for (auto& elevationCut : radarData_) { - std::uint16_t elevationAngle {}; + float elevationAngle {}; rda::WaveformType waveformType = rda::WaveformType::Unknown; std::shared_ptr& radial0 = @@ -466,14 +465,15 @@ void Ar2vFileImpl::IndexFile() if (vcpData_ != nullptr) { - elevationAngle = vcpData_->elevation_angle_raw(elevationCut.first); - waveformType = vcpData_->waveform_type(elevationCut.first); + elevationAngle = + static_cast(vcpData_->elevation_angle(elevationCut.first)); + waveformType = vcpData_->waveform_type(elevationCut.first); } else if ((digitalRadarData0 = std::dynamic_pointer_cast(radial0)) != nullptr) { - elevationAngle = digitalRadarData0->elevation_angle_raw(); + elevationAngle = digitalRadarData0->elevation_angle().value(); } else { @@ -501,19 +501,236 @@ void Ar2vFileImpl::IndexFile() auto time = util::TimePoint(radial0->modified_julian_date(), radial0->collection_time()); - // NOLINTNEXTLINE This conversion is accurate - float elevationAngleConverted = elevationAngle / scaleFactor; - // Any elevation above 90 degrees should be interpreted as a - // negative angle - // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) - if (elevationAngleConverted > 90) - { - elevationAngleConverted -= 360; - } - // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + index_[dataBlockType][elevationAngle][time] = elevationCut.second; + } + } + } +} - index_[dataBlockType][elevationAngleConverted][time] = - elevationCut.second; +bool Ar2vFile::LoadLDMRecords(std::istream& is) +{ + const size_t decompressedRecords = p->DecompressLDMRecords(is); + if (decompressedRecords == 0) + { + p->ParseLDMRecord(is); + } + else + { + p->ParseLDMRecords(); + } + + return true; +} + +bool Ar2vFile::IndexFile() +{ + p->IndexFile(); + return true; +} + +// NOLINTNEXTLINE +bool IsRadarDataIncomplete( + const std::shared_ptr& radarData) +{ + // Assume the data is incomplete when the delta between the first and last + // angles is greater than 2.5 degrees. + constexpr units::degrees kIncompleteDataAngleThreshold_ {2.5}; + + const units::degrees firstAngle = + radarData->cbegin()->second->azimuth_angle(); + const units::degrees lastAngle = + radarData->crbegin()->second->azimuth_angle(); + const units::degrees angleDelta = + common::GetAngleDelta(firstAngle, lastAngle); + + return angleDelta > kIncompleteDataAngleThreshold_; +} + +Ar2vFile::Ar2vFile(const std::shared_ptr& current, + const std::shared_ptr& last) : + Ar2vFile() +{ + // This is only used to index right now, so not a huge deal + p->vcpData_ = nullptr; + + // Reconstruct index from the other's indexes + if (current != nullptr) + { + for (const auto& type : current->p->index_) + { + for (const auto& elevation : type.second) + { + // Get the most recent scan + const auto& mostRecent = elevation.second.crbegin(); + if (mostRecent == elevation.second.crend()) + { + continue; + } + + // Add previous scans for stepping back in time + for (auto scan = ++(elevation.second.rbegin()); + scan != elevation.second.rend(); + ++scan) + { + p->index_[type.first][elevation.first][scan->first] = + scan->second; + } + + // Merge this scan with the last one if it is incomplete + if (IsRadarDataIncomplete(mostRecent->second)) + { + std::shared_ptr secondMostRecent = nullptr; + + // check if this volume scan has an earlier elevation scan + auto possibleSecondMostRecent = elevation.second.rbegin(); + ++possibleSecondMostRecent; + + if (possibleSecondMostRecent == elevation.second.rend()) + { + if (last == nullptr) + { + // Nothing to merge with + p->index_[type.first][elevation.first][mostRecent->first] = + mostRecent->second; + continue; + } + + // get the scan from the last scan + auto elevationScan = + std::get>( + last->GetElevationScan( + type.first, elevation.first, {})); + if (elevationScan == nullptr) + { + // Nothing to merge with + p->index_[type.first][elevation.first][mostRecent->first] = + mostRecent->second; + continue; + } + + secondMostRecent = elevationScan; + } + else + { + secondMostRecent = possibleSecondMostRecent->second; + } + + // Make the new scan + auto newScan = std::make_shared(); + + // Copy over the new radials + for (const auto& radial : *(mostRecent->second)) + { + (*newScan)[radial.first] = radial.second; + } + + /* Correctly order the old radials. The radials need to be in + * order for the rendering to work, and the index needs to start + * at 0 and increase by one from there. Since the new radial + * should have index 0, the old radial needs to be reshaped to + * match the new radials indexing. + */ + + const double lowestAzm = + mostRecent->second->cbegin()->second->azimuth_angle().value(); + const double heighestAzm = mostRecent->second->crbegin() + ->second->azimuth_angle() + .value(); + std::uint16_t index = mostRecent->second->crbegin()->first + 1; + + // Sort by the azimuth. Makes the rest of this way easier + auto secondMostRecentAzmMap = + std::map>(); + for (const auto& radial : *secondMostRecent) + { + secondMostRecentAzmMap[radial.second->azimuth_angle() + .value()] = radial.second; + } + + if (lowestAzm <= heighestAzm) // New scan does not contain 0/360 + { + // Get the radials following the new radials + for (const auto& radial : secondMostRecentAzmMap) + { + if (radial.first > heighestAzm) + { + (*newScan)[index] = radial.second; + ++index; + } + } + // Get the radials before the new radials + for (const auto& radial : secondMostRecentAzmMap) + { + if (radial.first < lowestAzm) + { + (*newScan)[index] = radial.second; + ++index; + } + else + { + break; + } + } + } + else // New scan includes 0/360 + { + // The radials will already be in the right order + for (const auto& radial : secondMostRecentAzmMap) + { + if (radial.first > heighestAzm && radial.first < lowestAzm) + { + (*newScan)[index] = radial.second; + ++index; + } + } + } + + p->index_[type.first][elevation.first][mostRecent->first] = + newScan; + } + else + { + p->index_[type.first][elevation.first][mostRecent->first] = + mostRecent->second; + } + } + } + } + + // Go though last, adding other elevations + if (last != nullptr) + { + for (const auto& type : last->p->index_) + { + // Find the highest elevation this type has for the current scan + // Start below any reasonable elevation + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) + float highestCurrentElevation = -90; + const auto& elevationScans = p->index_.find(type.first); + if (elevationScans != p->index_.cend()) + { + const auto& highestElevation = elevationScans->second.crbegin(); + if (highestElevation != elevationScans->second.crend()) + { + // Add a slight offset to ensure good floating point compare. + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) + highestCurrentElevation = highestElevation->first + 0.01f; + } + } + + for (const auto& elevation : type.second) + { + // Only add elevations above the current scan's elevation + if (elevation.first > highestCurrentElevation) + { + const auto& mostRecent = elevation.second.crbegin(); + if (mostRecent == elevation.second.crend()) + { + continue; + } + p->index_[type.first][elevation.first][mostRecent->first] = + mostRecent->second; + } } } } diff --git a/wxdata/source/scwx/wsr88d/rda/digital_radar_data.cpp b/wxdata/source/scwx/wsr88d/rda/digital_radar_data.cpp index ebadf4f5..8f2643af 100644 --- a/wxdata/source/scwx/wsr88d/rda/digital_radar_data.cpp +++ b/wxdata/source/scwx/wsr88d/rda/digital_radar_data.cpp @@ -154,7 +154,18 @@ std::uint16_t DigitalRadarData::elevation_angle_raw() const units::degrees DigitalRadarData::elevation_angle() const { - return units::degrees {p->elevationAngle_ * kAngleDataScale}; + // NOLINTNEXTLINE This conversion is accurate + float elevationAngleConverted = p->elevationAngle_ * kAngleDataScale; + // Any elevation above 90 degrees should be interpreted as a + // negative angle + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + if (elevationAngleConverted > 90) + { + elevationAngleConverted -= 360; + } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + + return units::degrees {elevationAngleConverted}; } std::uint16_t DigitalRadarData::elevation_number() const diff --git a/wxdata/source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp b/wxdata/source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp index d2f3dec2..b2159648 100644 --- a/wxdata/source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp +++ b/wxdata/source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp @@ -220,7 +220,19 @@ uint16_t VolumeCoveragePatternData::number_of_base_tilts() const double VolumeCoveragePatternData::elevation_angle(uint16_t e) const { - return p->elevationCuts_[e].elevationAngle_ * ANGLE_DATA_SCALE; + + double elevationAngleConverted = + p->elevationCuts_[e].elevationAngle_ * ANGLE_DATA_SCALE; + // Any elevation above 90 degrees should be interpreted as a + // negative angle + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + if (elevationAngleConverted > 90) + { + elevationAngleConverted -= 360; + } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + + return elevationAngleConverted; } uint16_t VolumeCoveragePatternData::elevation_angle_raw(uint16_t e) const diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 8d2e15b4..2c062f4b 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -60,6 +60,7 @@ set(HDR_NETWORK include/scwx/network/cpr.hpp set(SRC_NETWORK source/scwx/network/cpr.cpp source/scwx/network/dir_list.cpp) set(HDR_PROVIDER include/scwx/provider/aws_level2_data_provider.hpp + include/scwx/provider/aws_level2_chunks_data_provider.hpp include/scwx/provider/aws_level3_data_provider.hpp include/scwx/provider/aws_nexrad_data_provider.hpp include/scwx/provider/iem_api_provider.hpp @@ -68,6 +69,7 @@ set(HDR_PROVIDER include/scwx/provider/aws_level2_data_provider.hpp include/scwx/provider/nexrad_data_provider_factory.hpp include/scwx/provider/warnings_provider.hpp) set(SRC_PROVIDER source/scwx/provider/aws_level2_data_provider.cpp + source/scwx/provider/aws_level2_chunks_data_provider.cpp source/scwx/provider/aws_level3_data_provider.cpp source/scwx/provider/aws_nexrad_data_provider.cpp source/scwx/provider/iem_api_provider.cpp