#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace scwx { namespace qt { namespace map { static const std::string logPrefix_ = "scwx::qt::map::alert_layer"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static const boost::gil::rgba32f_pixel_t kBlack_ {0.0f, 0.0f, 0.0f, 1.0f}; template struct AlertTypeHash; template<> struct AlertTypeHash> { size_t operator()(const std::pair& x) const; }; static bool IsAlertActive(const std::shared_ptr& segment); class AlertLayerHandler : public QObject { Q_OBJECT public: struct SegmentRecord { std::shared_ptr segment_; types::TextEventKey key_; std::shared_ptr message_; std::chrono::system_clock::time_point segmentBegin_; std::chrono::system_clock::time_point segmentEnd_; SegmentRecord( const std::shared_ptr& segment, const types::TextEventKey& key, const std::shared_ptr& message) : segment_ {segment}, key_ {key}, message_ {message}, segmentBegin_ {segment->event_begin()}, segmentEnd_ {segment->event_end()} { } }; explicit AlertLayerHandler() { connect(textEventManager_.get(), &manager::TextEventManager::AlertUpdated, this, [this](const types::TextEventKey& key, std::size_t messageIndex) { HandleAlert(key, messageIndex); }); } ~AlertLayerHandler() { disconnect(textEventManager_.get(), nullptr, this, nullptr); std::unique_lock lock(alertMutex_); } std::unordered_map< std::pair, boost::container::stable_vector>, AlertTypeHash>> segmentsByType_ {}; std::unordered_map< types::TextEventKey, boost::container::stable_vector>, types::TextEventHash> segmentsByKey_ {}; void HandleAlert(const types::TextEventKey& key, size_t messageIndex); static AlertLayerHandler& Instance(); std::shared_ptr textEventManager_ { manager::TextEventManager::Instance()}; std::shared_mutex alertMutex_ {}; signals: void AlertAdded(const std::shared_ptr& segmentRecord, awips::Phenomenon phenomenon); void AlertUpdated(const std::shared_ptr& segmentRecord); void AlertsUpdated(awips::Phenomenon phenomenon, bool alertActive); }; class AlertLayer::Impl { public: struct LineData { boost::gil::rgba32f_pixel_t borderColor_ {}; boost::gil::rgba32f_pixel_t highlightColor_ {}; boost::gil::rgba32f_pixel_t lineColor_ {}; std::size_t borderWidth_ {}; std::size_t highlightWidth_ {}; std::size_t lineWidth_ {}; }; explicit Impl(AlertLayer* self, std::shared_ptr context, awips::Phenomenon phenomenon) : self_ {self}, phenomenon_ {phenomenon}, ibw_ {awips::ibw::GetImpactBasedWarningInfo(phenomenon)}, geoLines_ {{false, std::make_shared(context)}, {true, std::make_shared(context)}} { UpdateLineData(); ConnectSignals(); ScheduleRefresh(); } ~Impl() { std::unique_lock refreshLock(refreshMutex_); refreshTimer_.cancel(); refreshLock.unlock(); threadPool_.join(); receiver_ = nullptr; std::unique_lock lock(linesMutex_); }; void AddAlert( const std::shared_ptr& segmentRecord); void UpdateAlert( const std::shared_ptr& segmentRecord); void ConnectAlertHandlerSignals(); void ConnectSignals(); void HandleGeoLinesEvent(std::weak_ptr& di, QEvent* ev); void HandleGeoLinesHover(const std::shared_ptr& di, const QPointF& mouseGlobalPos); void ScheduleRefresh(); LineData& GetLineData(const std::shared_ptr& segment, bool alertActive); void UpdateLineData(); void AddLine(std::shared_ptr& geoLines, std::shared_ptr& di, const common::Coordinate& p1, const common::Coordinate& p2, boost::gil::rgba32f_pixel_t color, float width, std::chrono::system_clock::time_point startTime, std::chrono::system_clock::time_point endTime, bool enableHover); void AddLines(std::shared_ptr& geoLines, const std::vector& coordinates, boost::gil::rgba32f_pixel_t color, float width, std::chrono::system_clock::time_point startTime, std::chrono::system_clock::time_point endTime, bool enableHover, boost::container::stable_vector< std::shared_ptr>& drawItems); void UpdateLines(); static LineData CreateLineData(const settings::LineSettings& lineSettings); boost::asio::thread_pool threadPool_ {1u}; AlertLayer* self_; boost::asio::system_timer refreshTimer_ {threadPool_}; std::mutex refreshMutex_; const awips::Phenomenon phenomenon_; const awips::ibw::ImpactBasedWarningInfo& ibw_; std::unique_ptr receiver_ {std::make_unique()}; std::unordered_map> geoLines_; std::unordered_map, boost::container::stable_vector< std::shared_ptr>> linesBySegment_ {}; std::unordered_map, std::shared_ptr> segmentsByLine_; std::mutex linesMutex_ {}; std::unordered_map threatCategoryLineData_; LineData observedLineData_ {}; LineData tornadoPossibleLineData_ {}; LineData inactiveLineData_ {}; std::chrono::system_clock::time_point selectedTime_ {}; std::shared_ptr lastHoverDi_ {nullptr}; std::string tooltip_ {}; std::vector connections_ {}; }; AlertLayer::AlertLayer(std::shared_ptr context, awips::Phenomenon phenomenon) : DrawLayer( context, fmt::format("AlertLayer {}", awips::GetPhenomenonText(phenomenon))), p(std::make_unique(this, context, phenomenon)) { for (auto alertActive : {false, true}) { auto& geoLines = p->geoLines_.at(alertActive); AddDrawItem(geoLines); } } AlertLayer::~AlertLayer() = default; void AlertLayer::InitializeHandler() { static bool ftt = true; if (ftt) { logger_->debug("Initializing handler"); AlertLayerHandler::Instance(); ftt = false; } } void AlertLayer::Initialize() { logger_->debug("Initialize: {}", awips::GetPhenomenonText(p->phenomenon_)); DrawLayer::Initialize(); auto& alertLayerHandler = AlertLayerHandler::Instance(); // Take a shared lock to prevent handling additional alerts while populating // initial lists std::shared_lock lock {alertLayerHandler.alertMutex_}; for (auto alertActive : {false, true}) { auto& geoLines = p->geoLines_.at(alertActive); geoLines->StartLines(); // Populate initial segments auto segmentsIt = alertLayerHandler.segmentsByType_.find({p->phenomenon_, alertActive}); if (segmentsIt != alertLayerHandler.segmentsByType_.cend()) { for (auto& segment : segmentsIt->second) { p->AddAlert(segment); } } geoLines->FinishLines(); } p->ConnectAlertHandlerSignals(); } void AlertLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) { gl::OpenGLFunctions& gl = context()->gl(); for (auto alertActive : {false, true}) { p->geoLines_.at(alertActive)->set_selected_time(p->selectedTime_); } DrawLayer::Render(params); SCWX_GL_CHECK_ERROR(); } void AlertLayer::Deinitialize() { logger_->debug("Deinitialize: {}", awips::GetPhenomenonText(p->phenomenon_)); DrawLayer::Deinitialize(); } bool IsAlertActive(const std::shared_ptr& segment) { auto& vtec = segment->header_->vtecString_.front(); auto action = vtec.pVtec_.action(); bool alertActive = (action != awips::PVtec::Action::Canceled); return alertActive; } void AlertLayerHandler::HandleAlert(const types::TextEventKey& key, size_t messageIndex) { logger_->trace("HandleAlert: {}", key.ToString()); std::unordered_set, AlertTypeHash>> alertsUpdated {}; auto message = textEventManager_->message_list(key).at(messageIndex); // Determine start time for first segment std::chrono::system_clock::time_point segmentBegin {}; if (message->segment_count() > 0) { segmentBegin = message->segment(0)->event_begin(); } // Take a unique mutex before modifying segments std::unique_lock lock {alertMutex_}; // Update any existing segments with new end time auto& segmentsForKey = segmentsByKey_[key]; for (auto& segmentRecord : segmentsForKey) { if (segmentRecord->segmentEnd_ > segmentBegin) { segmentRecord->segmentEnd_ = segmentBegin; Q_EMIT AlertUpdated(segmentRecord); } } // Process new segments for (auto& segment : message->segments()) { if (!segment->codedLocation_.has_value()) { // Cannot handle a segment without a location continue; } auto& vtec = segment->header_->vtecString_.front(); awips::Phenomenon phenomenon = vtec.pVtec_.phenomenon(); bool alertActive = IsAlertActive(segment); auto& segmentsForType = segmentsByType_[{key.phenomenon_, alertActive}]; // Insert segment into lists std::shared_ptr segmentRecord = std::make_shared(segment, key, message); segmentsForKey.push_back(segmentRecord); segmentsForType.push_back(segmentRecord); Q_EMIT AlertAdded(segmentRecord, phenomenon); alertsUpdated.emplace(phenomenon, alertActive); } // Release the lock after completing segment updates lock.unlock(); for (auto& alert : alertsUpdated) { // Emit signal for each updated alert type Q_EMIT AlertsUpdated(alert.first, alert.second); } } void AlertLayer::Impl::ConnectAlertHandlerSignals() { auto& alertLayerHandler = AlertLayerHandler::Instance(); QObject::connect( &alertLayerHandler, &AlertLayerHandler::AlertAdded, receiver_.get(), [this]( const std::shared_ptr& segmentRecord, awips::Phenomenon phenomenon) { if (phenomenon == phenomenon_) { AddAlert(segmentRecord); } }); QObject::connect( &alertLayerHandler, &AlertLayerHandler::AlertUpdated, receiver_.get(), [this]( const std::shared_ptr& segmentRecord) { if (segmentRecord->key_.phenomenon_ == phenomenon_) { UpdateAlert(segmentRecord); } }); } void AlertLayer::Impl::ConnectSignals() { auto& alertPaletteSettings = settings::PaletteSettings::Instance().alert_palette(phenomenon_); auto timelineManager = manager::TimelineManager::Instance(); QObject::connect(timelineManager.get(), &manager::TimelineManager::SelectedTimeUpdated, receiver_.get(), [this](std::chrono::system_clock::time_point dateTime) { selectedTime_ = dateTime; }); connections_.push_back(alertPaletteSettings.changed_signal().connect( [this]() { UpdateLineData(); UpdateLines(); })); } void AlertLayer::Impl::ScheduleRefresh() { using namespace std::chrono_literals; // Take a unique lock before refreshing std::unique_lock lock(refreshMutex_); // Expires at the top of the next minute std::chrono::system_clock::time_point now = std::chrono::floor( std::chrono::system_clock::now()); refreshTimer_.expires_at(now + 1min); refreshTimer_.async_wait( [this](const boost::system::error_code& e) { if (e == boost::asio::error::operation_aborted) { logger_->debug("Refresh timer cancelled"); } else if (e != boost::system::errc::success) { logger_->warn("Refresh timer error: {}", e.message()); } else { Q_EMIT self_->NeedsRendering(); ScheduleRefresh(); } }); } void AlertLayer::Impl::AddAlert( const std::shared_ptr& segmentRecord) { auto& segment = segmentRecord->segment_; bool alertActive = IsAlertActive(segment); auto& startTime = segmentRecord->segmentBegin_; auto& endTime = segmentRecord->segmentEnd_; auto& lineData = GetLineData(segment, alertActive); auto& geoLines = geoLines_.at(alertActive); const auto& coordinates = segment->codedLocation_->coordinates(); // Take a mutex before modifying lines by segment std::unique_lock lock {linesMutex_}; // Add draw items only if the segment has not already been added auto drawItems = linesBySegment_.try_emplace( segmentRecord, boost::container::stable_vector< std::shared_ptr> {}); // If draw items were added if (drawItems.second) { const float borderWidth = lineData.borderWidth_; const float highlightWidth = lineData.highlightWidth_; const float lineWidth = lineData.lineWidth_; const float totalHighlightWidth = lineWidth + (highlightWidth * 2.0f); const float totalBorderWidth = totalHighlightWidth + (borderWidth * 2.0f); constexpr bool borderHover = true; constexpr bool highlightHover = false; constexpr bool lineHover = false; // Add border AddLines(geoLines, coordinates, lineData.borderColor_, totalBorderWidth, startTime, endTime, borderHover, drawItems.first->second); // Add border to segmentsByLine_ for (auto& di : drawItems.first->second) { segmentsByLine_.insert({di, segmentRecord}); } // Add highlight AddLines(geoLines, coordinates, lineData.highlightColor_, totalHighlightWidth, startTime, endTime, highlightHover, drawItems.first->second); // Add line AddLines(geoLines, coordinates, lineData.lineColor_, lineWidth, startTime, endTime, lineHover, drawItems.first->second); } } void AlertLayer::Impl::UpdateAlert( const std::shared_ptr& segmentRecord) { // Take a mutex before referencing lines iterators and stable vector std::unique_lock lock {linesMutex_}; auto it = linesBySegment_.find(segmentRecord); if (it != linesBySegment_.cend()) { auto& segment = segmentRecord->segment_; bool alertActive = IsAlertActive(segment); auto& geoLines = geoLines_.at(alertActive); auto& lines = it->second; for (auto& line : lines) { geoLines->SetLineStartTime(line, segmentRecord->segmentBegin_); geoLines->SetLineEndTime(line, segmentRecord->segmentEnd_); } } } void AlertLayer::Impl::AddLines( std::shared_ptr& geoLines, const std::vector& coordinates, boost::gil::rgba32f_pixel_t color, float width, std::chrono::system_clock::time_point startTime, std::chrono::system_clock::time_point endTime, bool enableHover, boost::container::stable_vector>& drawItems) { for (std::size_t i = 0, j = 1; i < coordinates.size(); ++i, ++j) { if (j >= coordinates.size()) { j = 0; // Ignore repeated coordinates at the end if (coordinates[i] == coordinates[j]) { break; } } auto di = geoLines->AddLine(); AddLine(geoLines, di, coordinates[i], coordinates[j], color, width, startTime, endTime, enableHover); drawItems.push_back(di); } } void AlertLayer::Impl::AddLine(std::shared_ptr& geoLines, std::shared_ptr& di, const common::Coordinate& p1, const common::Coordinate& p2, boost::gil::rgba32f_pixel_t color, float width, std::chrono::system_clock::time_point startTime, std::chrono::system_clock::time_point endTime, bool enableHover) { geoLines->SetLineLocation( di, p1.latitude_, p1.longitude_, p2.latitude_, p2.longitude_); geoLines->SetLineModulate(di, color); geoLines->SetLineWidth(di, width); geoLines->SetLineStartTime(di, startTime); geoLines->SetLineEndTime(di, endTime); if (enableHover) { geoLines->SetLineHoverCallback( di, std::bind(&AlertLayer::Impl::HandleGeoLinesHover, this, std::placeholders::_1, std::placeholders::_2)); const std::weak_ptr diWeak = di; gl::draw::GeoLines::RegisterEventHandler( di, std::bind(&AlertLayer::Impl::HandleGeoLinesEvent, this, diWeak, std::placeholders::_1)); } } void AlertLayer::Impl::UpdateLines() { std::unique_lock lock {linesMutex_}; for (auto& segmentLine : linesBySegment_) { auto& segmentRecord = segmentLine.first; auto& geoLineDrawItems = segmentLine.second; auto& segment = segmentRecord->segment_; bool alertActive = IsAlertActive(segment); auto& lineData = GetLineData(segment, alertActive); auto& geoLines = geoLines_.at(alertActive); const float borderWidth = lineData.borderWidth_; const float highlightWidth = lineData.highlightWidth_; const float lineWidth = lineData.lineWidth_; const float totalHighlightWidth = lineWidth + (highlightWidth * 2.0f); const float totalBorderWidth = totalHighlightWidth + (borderWidth * 2.0f); // Border, highlight and line std::size_t linesPerType = geoLineDrawItems.size() / 3; // Border for (auto& borderLine : geoLineDrawItems | std::views::take(linesPerType)) { geoLines->SetLineModulate(borderLine, lineData.borderColor_); geoLines->SetLineWidth(borderLine, totalBorderWidth); } // Highlight for (auto& highlightLine : geoLineDrawItems | std::views::drop(linesPerType) | std::views::take(linesPerType)) { geoLines->SetLineModulate(highlightLine, lineData.highlightColor_); geoLines->SetLineWidth(highlightLine, totalHighlightWidth); } // Line for (auto& line : geoLineDrawItems | std::views::drop(linesPerType * 2)) { geoLines->SetLineModulate(line, lineData.lineColor_); geoLines->SetLineWidth(line, lineWidth); } } } void AlertLayer::Impl::HandleGeoLinesEvent( std::weak_ptr& diWeak, QEvent* ev) { const std::shared_ptr di = diWeak.lock(); if (di == nullptr) { return; } switch (ev->type()) { case QEvent::Type::MouseButtonPress: { auto it = segmentsByLine_.find(di); if (it != segmentsByLine_.cend()) { // Display alert dialog logger_->debug("Selected alert: {}", it->second->key_.ToString()); Q_EMIT self_->AlertSelected(it->second->key_); } break; } default: break; } } void AlertLayer::Impl::HandleGeoLinesHover( const std::shared_ptr& di, const QPointF& mouseGlobalPos) { if (di != lastHoverDi_) { auto it = segmentsByLine_.find(di); if (it != segmentsByLine_.cend()) { tooltip_ = boost::algorithm::join(it->second->segment_->productContent_, "\n"); } else { tooltip_.clear(); } lastHoverDi_ = di; } if (!tooltip_.empty()) { util::tooltip::Show(tooltip_, mouseGlobalPos); } } AlertLayer::Impl::LineData AlertLayer::Impl::CreateLineData(const settings::LineSettings& lineSettings) { return LineData { .borderColor_ {lineSettings.GetBorderColorRgba32f()}, .highlightColor_ {lineSettings.GetHighlightColorRgba32f()}, .lineColor_ {lineSettings.GetLineColorRgba32f()}, .borderWidth_ = static_cast(lineSettings.border_width().GetValue()), .highlightWidth_ = static_cast(lineSettings.highlight_width().GetValue()), .lineWidth_ = static_cast(lineSettings.line_width().GetValue())}; } void AlertLayer::Impl::UpdateLineData() { auto& alertPalette = settings::PaletteSettings().Instance().alert_palette(phenomenon_); for (auto threatCategory : ibw_.threatCategories_) { auto& palette = alertPalette.threat_category(threatCategory); threatCategoryLineData_.insert_or_assign(threatCategory, CreateLineData(palette)); } if (ibw_.hasObservedTag_) { observedLineData_ = CreateLineData(alertPalette.observed()); } if (ibw_.hasTornadoPossibleTag_) { tornadoPossibleLineData_ = CreateLineData(alertPalette.tornado_possible()); } inactiveLineData_ = CreateLineData(alertPalette.inactive()); } AlertLayer::Impl::LineData& AlertLayer::Impl::GetLineData( const std::shared_ptr& segment, bool alertActive) { if (!alertActive) { return inactiveLineData_; } for (auto& threatCategory : ibw_.threatCategories_) { if (segment->threatCategory_ == threatCategory) { if (threatCategory == awips::ibw::ThreatCategory::Base) { if (ibw_.hasObservedTag_ && segment->observed_) { return observedLineData_; } if (ibw_.hasTornadoPossibleTag_ && segment->tornadoPossible_) { return tornadoPossibleLineData_; } } return threatCategoryLineData_.at(threatCategory); } } return threatCategoryLineData_.at(awips::ibw::ThreatCategory::Base); }; AlertLayerHandler& AlertLayerHandler::Instance() { static AlertLayerHandler alertLayerHandler_ {}; return alertLayerHandler_; } size_t AlertTypeHash>::operator()( const std::pair& x) const { size_t seed = 0; boost::hash_combine(seed, x.first); boost::hash_combine(seed, x.second); return seed; } } // namespace map } // namespace qt } // namespace scwx #include "alert_layer.moc"