#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace scwx::qt::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 Q_DISABLE_COPY_MOVE(AlertLayerHandler) 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, types::TextEventKey key, const std::shared_ptr& message) : segment_ {segment}, key_ {std::move(key)}, message_ {message}, segmentBegin_ {segment->event_begin()}, segmentEnd_ {segment->event_end()} { } }; explicit AlertLayerHandler() { connect(textEventManager_.get(), &manager::TextEventManager::AlertUpdated, this, &AlertLayerHandler::HandleAlert); connect(textEventManager_.get(), &manager::TextEventManager::AlertsRemoved, this, &AlertLayerHandler::HandleAlertsRemoved); } ~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, boost::uuids::uuid uuid); void HandleAlertsRemoved( const std::unordered_set>& keys); 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 AlertsRemoved(awips::Phenomenon phenomenon); 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, const std::shared_ptr& glContext, awips::Phenomenon phenomenon) : self_ {self}, phenomenon_ {phenomenon}, ibw_ {awips::ibw::GetImpactBasedWarningInfo(phenomenon)}, geoLines_ {{false, std::make_shared(glContext)}, {true, std::make_shared(glContext)}} { UpdateLineData(); ConnectSignals(); ScheduleRefresh(); } ~Impl() { std::unique_lock refreshLock(refreshMutex_); refreshEnabled_ = false; refreshTimer_.cancel(); refreshLock.unlock(); threadPool_.stop(); threadPool_.join(); receiver_ = nullptr; std::unique_lock lock(linesMutex_); }; Impl(const Impl&) = delete; Impl& operator=(const Impl&) = delete; Impl(const Impl&&) = delete; Impl& operator=(const Impl&&) = delete; 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, const 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, const 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 PopulateLines(bool alertActive); void RepopulateLines(); void UpdateLines(); static LineData CreateLineData(const settings::LineSettings& lineSettings); boost::asio::thread_pool threadPool_ {1u}; AlertLayer* self_; std::atomic refreshEnabled_ {true}; 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::mutex receiverMutex_ {}; 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(const std::shared_ptr& glContext, awips::Phenomenon phenomenon) : DrawLayer( glContext, fmt::format("AlertLayer {}", awips::GetPhenomenonText(phenomenon))), p(std::make_unique(this, glContext, 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(const std::shared_ptr& mapContext) { logger_->debug("Initialize: {}", awips::GetPhenomenonText(p->phenomenon_)); DrawLayer::Initialize(mapContext); auto& alertLayerHandler = AlertLayerHandler::Instance(); p->selectedTime_ = manager::TimelineManager::Instance()->GetSelectedTime(); // Take a shared lock to prevent handling additional alerts while populating // initial lists std::shared_lock lock {alertLayerHandler.alertMutex_}; for (auto alertActive : {false, true}) { p->PopulateLines(alertActive); } p->ConnectAlertHandlerSignals(); } void AlertLayer::Render(const std::shared_ptr& mapContext, const QMapLibre::CustomLayerRenderParameters& params) { for (auto alertActive : {false, true}) { p->geoLines_.at(alertActive)->set_selected_time(p->selectedTime_); } DrawLayer::Render(mapContext, 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, boost::uuids::uuid uuid) { logger_->trace("HandleAlert: {}", key.ToString()); std::unordered_set, AlertTypeHash>> alertsUpdated {}; const auto& messageList = textEventManager_->message_list(key); // Find message by UUID instead of index, as the message index could have // changed between the signal being emitted and the handler being called auto messageIt = std::find_if(messageList.cbegin(), messageList.cend(), [&uuid](const auto& message) { return uuid == message->uuid(); }); if (messageIt == messageList.cend()) { logger_->warn( "Could not find alert uuid: {} ({})", key.ToString(), messageIndex); return; } auto& message = *messageIt; auto nextMessageIt = std::next(messageIt); // Store the current message index messageIndex = static_cast(std::distance(messageList.cbegin(), messageIt)); // Determine start time for first segment std::chrono::system_clock::time_point segmentBegin {}; if (message->segment_count() > 0) { segmentBegin = message->segment(0)->event_begin(); } // Determine the start time for the first segment of the next message std::optional nextMessageBegin {}; if (nextMessageIt != messageList.cend()) { nextMessageBegin = (*nextMessageIt) ->wmo_header() ->GetDateTime((*nextMessageIt)->segment(0)->event_begin()); } // Take a unique mutex before modifying segments std::unique_lock lock {alertMutex_}; // Update any existing earlier segments with new end time auto& segmentsForKey = segmentsByKey_[key]; for (auto& segmentRecord : segmentsForKey) { // Determine if the segment is earlier than the current message auto it = std::find( messageList.cbegin(), messageList.cend(), segmentRecord->message_); auto segmentIndex = static_cast(std::distance(messageList.cbegin(), it)); if (segmentIndex < messageIndex && 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); // Update segment end time to be no later than the begin time of the next // message (if present) if (nextMessageBegin.has_value() && segmentRecord->segmentEnd_ > nextMessageBegin) { segmentRecord->segmentEnd_ = nextMessageBegin.value(); } 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 AlertLayerHandler::HandleAlertsRemoved( const std::unordered_set>& keys) { logger_->trace("HandleAlertsRemoved: {} keys", keys.size()); std::set alertsRemoved {}; // Take a unique lock before modifying segments std::unique_lock lock {alertMutex_}; for (const auto& key : keys) { // Remove segments associated with the key auto segmentsIt = segmentsByKey_.find(key); if (segmentsIt != segmentsByKey_.end()) { for (const auto& segmentRecord : segmentsIt->second) { auto& segment = segmentRecord->segment_; const bool alertActive = IsAlertActive(segment); // Remove from segmentsByType_ auto typeIt = segmentsByType_.find({key.phenomenon_, alertActive}); if (typeIt != segmentsByType_.end()) { auto& segmentsForType = typeIt->second; segmentsForType.erase(std::remove(segmentsForType.begin(), segmentsForType.end(), segmentRecord), segmentsForType.end()); // If no segments remain for this type, erase the entry if (segmentsForType.empty()) { segmentsByType_.erase(typeIt); } } alertsRemoved.emplace(key.phenomenon_); } // Remove the key from segmentsByKey_ segmentsByKey_.erase(segmentsIt); } } // Release the lock after completing segment updates lock.unlock(); // Emit signal to notify that alerts have been removed for (auto& alert : alertsRemoved) { Q_EMIT AlertsRemoved(alert); } } 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_) { // Only process one signal at a time const std::unique_lock lock {receiverMutex_}; AddAlert(segmentRecord); } }); QObject::connect( &alertLayerHandler, &AlertLayerHandler::AlertUpdated, receiver_.get(), [this]( const std::shared_ptr& segmentRecord) { if (segmentRecord->key_.phenomenon_ == phenomenon_) { // Only process one signal at a time const std::unique_lock lock {receiverMutex_}; UpdateAlert(segmentRecord); } }); QObject::connect(&alertLayerHandler, &AlertLayerHandler::AlertsRemoved, receiver_.get(), [this](awips::Phenomenon phenomenon) { if (phenomenon == phenomenon_) { // Only process one signal at a time const std::unique_lock lock {receiverMutex_}; // Re-populate the lines if multiple alerts were // removed RepopulateLines(); } }); } 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(); if (refreshEnabled_) { 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 auto borderWidth = static_cast(lineData.borderWidth_); const auto highlightWidth = static_cast(lineData.highlightWidth_); const auto lineWidth = static_cast(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); } Q_EMIT self_->NeedsRendering(); } 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_); } } Q_EMIT self_->NeedsRendering(); } void AlertLayer::Impl::AddLines( std::shared_ptr& geoLines, const std::vector& coordinates, const 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, const 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, static_cast(p1.latitude_), static_cast(p1.longitude_), static_cast(p2.latitude_), static_cast(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::PopulateLines(bool alertActive) { auto& alertLayerHandler = AlertLayerHandler::Instance(); auto& geoLines = geoLines_.at(alertActive); geoLines->StartLines(); // Populate initial segments auto segmentsIt = alertLayerHandler.segmentsByType_.find({phenomenon_, alertActive}); if (segmentsIt != alertLayerHandler.segmentsByType_.cend()) { for (auto& segment : segmentsIt->second) { AddAlert(segment); } } geoLines->FinishLines(); } void AlertLayer::Impl::RepopulateLines() { auto& alertLayerHandler = AlertLayerHandler::Instance(); // Take a shared lock to prevent handling additional alerts while populating // initial lists const std::shared_lock alertLock {alertLayerHandler.alertMutex_}; linesBySegment_.clear(); segmentsByLine_.clear(); for (auto alertActive : {false, true}) { PopulateLines(alertActive); } Q_EMIT self_->NeedsRendering(); } 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 auto borderWidth = static_cast(lineData.borderWidth_); const auto highlightWidth = static_cast(lineData.highlightWidth_); const auto lineWidth = static_cast(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 scwx::qt::map #include "alert_layer.moc"