diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp index e415d24e..d9f2c023 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp @@ -6,6 +6,7 @@ #include +#include #include namespace scwx @@ -25,13 +26,15 @@ static constexpr size_t kNumTriangles = kNumRectangles * 2; static constexpr size_t kVerticesPerTriangle = 3; static constexpr size_t kVerticesPerRectangle = kVerticesPerTriangle * 2; static constexpr size_t kPointsPerVertex = 9; -static constexpr size_t kBufferLength = +static constexpr size_t kLineBufferLength_ = kNumTriangles * kVerticesPerTriangle * kPointsPerVertex; -// Threshold, start time, end time -static constexpr std::size_t kIntegersPerVertex_ = 3; +// Threshold, start time, end time, displayed +static constexpr std::size_t kIntegersPerVertex_ = 4; +static constexpr std::size_t kIntegerBufferLength_ = + kNumTriangles * kVerticesPerTriangle * kIntegersPerVertex_; -struct GeoLineDrawItem +struct GeoLineDrawItem : types::EventHandler { bool visible_ {true}; units::length::nautical_miles threshold_ {}; @@ -46,6 +49,7 @@ struct GeoLineDrawItem float width_ {5.0}; units::angle::degrees angle_ {}; std::string hoverText_ {}; + GeoLines::HoverCallback hoverCallback_ {nullptr}; }; class GeoLines::Impl @@ -53,7 +57,7 @@ class GeoLines::Impl public: struct LineHoverEntry { - std::shared_ptr di_; + std::shared_ptr di_; glm::vec2 p1_; glm::vec2 p2_; @@ -81,6 +85,12 @@ public: void BufferLine(const std::shared_ptr& di); void Update(); void UpdateBuffers(); + void UpdateModifiedLineBuffers(); + void UpdateSingleBuffer(const std::shared_ptr& di, + std::size_t lineIndex, + std::vector& linesBuffer, + std::vector& integerBuffer, + std::vector& hoverLines); std::shared_ptr context_; @@ -88,6 +98,8 @@ public: bool dirty_ {false}; bool thresholded_ {false}; + boost::unordered_flat_set> dirtyLines_ {}; + std::chrono::system_clock::time_point selectedTime_ {}; std::mutex lineMutex_ {}; @@ -136,8 +148,7 @@ void GeoLines::set_thresholded(bool thresholded) void GeoLines::Initialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - auto& gl30 = p->context_->gl30(); + gl::OpenGLFunctions& gl = p->context_->gl(); p->shaderProgram_ = p->context_->GetShaderProgram( {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"}, @@ -158,8 +169,10 @@ void GeoLines::Initialize() gl.glBindVertexArray(p->vao_); gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); - gl.glBufferData( - GL_ARRAY_BUFFER, sizeof(float) * kBufferLength, nullptr, GL_DYNAMIC_DRAW); + gl.glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * kLineBufferLength_, + nullptr, + GL_DYNAMIC_DRAW); // aLatLong gl.glVertexAttribPointer(0, @@ -217,7 +230,13 @@ void GeoLines::Initialize() gl.glEnableVertexAttribArray(6); // aDisplayed - gl30.glVertexAttribI1i(7, 1); + gl.glVertexAttribPointer(7, + 1, + GL_INT, + GL_FALSE, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(3 * sizeof(float))); + gl.glEnableVertexAttribArray(7); p->dirty_ = true; } @@ -231,7 +250,7 @@ void GeoLines::Render(const QMapLibre::CustomLayerRenderParameters& params) std::unique_lock lock {p->lineMutex_}; - if (p->currentLineList_.size() > 0) + if (p->newLineList_.size() > 0) { gl::OpenGLFunctions& gl = p->context_->gl(); @@ -280,7 +299,7 @@ void GeoLines::Deinitialize() gl::OpenGLFunctions& gl = p->context_->gl(); gl.glDeleteVertexArrays(1, &p->vao_); - gl.glDeleteBuffers(2, p->vbo_.data()); + gl.glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); std::unique_lock lock {p->lineMutex_}; @@ -314,43 +333,104 @@ void GeoLines::SetLineLocation(const std::shared_ptr& di, float latitude2, float longitude2) { - di->latitude1_ = latitude1; - di->longitude1_ = longitude1; - di->latitude2_ = latitude2; - di->longitude2_ = longitude2; + if (di->latitude1_ != latitude1 || di->longitude1_ != longitude1 || + di->latitude2_ != latitude1 || di->longitude2_ != longitude1) + { + di->latitude1_ = latitude1; + di->longitude1_ = longitude1; + di->latitude2_ = latitude2; + di->longitude2_ = longitude2; + p->dirtyLines_.insert(di); + } } void GeoLines::SetLineModulate(const std::shared_ptr& di, boost::gil::rgba8_pixel_t modulate) { - di->modulate_ = {modulate[0] / 255.0f, - modulate[1] / 255.0f, - modulate[2] / 255.0f, - modulate[3] / 255.0f}; + boost::gil::rgba32f_pixel_t newModulate = {modulate[0] / 255.0f, + modulate[1] / 255.0f, + modulate[2] / 255.0f, + modulate[3] / 255.0f}; + + if (di->modulate_ != newModulate) + { + di->modulate_ = newModulate; + p->dirtyLines_.insert(di); + } } void GeoLines::SetLineModulate(const std::shared_ptr& di, boost::gil::rgba32f_pixel_t modulate) { - di->modulate_ = modulate; + if (di->modulate_ != modulate) + { + di->modulate_ = modulate; + p->dirtyLines_.insert(di); + } } void GeoLines::SetLineWidth(const std::shared_ptr& di, float width) { - di->width_ = width; + if (di->width_ != width) + { + di->width_ = width; + p->dirtyLines_.insert(di); + } } void GeoLines::SetLineVisible(const std::shared_ptr& di, bool visible) { - di->visible_ = visible; + if (di->visible_ != visible) + { + di->visible_ = visible; + p->dirtyLines_.insert(di); + } +} + +void GeoLines::SetLineHoverCallback(const std::shared_ptr& di, + const HoverCallback& callback) +{ + if (di->hoverCallback_ != nullptr || callback != nullptr) + { + di->hoverCallback_ = callback; + p->dirtyLines_.insert(di); + } } void GeoLines::SetLineHoverText(const std::shared_ptr& di, const std::string& text) { - di->hoverText_ = text; + if (di->hoverText_ != text) + { + di->hoverText_ = text; + p->dirtyLines_.insert(di); + } +} + +void GeoLines::SetLineStartTime(const std::shared_ptr& di, + std::chrono::system_clock::time_point startTime) +{ + if (di->startTime_ != startTime) + { + di->startTime_ = + std::chrono::time_point_cast(startTime); + p->dirtyLines_.insert(di); + } +} + +void GeoLines::SetLineEndTime(const std::shared_ptr& di, + std::chrono::system_clock::time_point endTime) +{ + if (di->endTime_ != endTime) + { + di->endTime_ = + std::chrono::time_point_cast(endTime); + p->dirtyLines_.insert(di); + } } void GeoLines::FinishLines() @@ -361,7 +441,7 @@ void GeoLines::FinishLines() std::unique_lock lock {p->lineMutex_}; // Swap buffers - p->currentLineList_.swap(p->newLineList_); + p->currentLineList_ = p->newLineList_; p->currentLinesBuffer_.swap(p->newLinesBuffer_); p->currentIntegerBuffer_.swap(p->newIntegerBuffer_); p->currentHoverLines_.swap(p->newHoverLines_); @@ -379,20 +459,69 @@ void GeoLines::FinishLines() void GeoLines::Impl::UpdateBuffers() { newLinesBuffer_.clear(); - newLinesBuffer_.reserve(newLineList_.size() * kBufferLength); + newLinesBuffer_.reserve(newLineList_.size() * kLineBufferLength_); newIntegerBuffer_.clear(); newIntegerBuffer_.reserve(newLineList_.size() * kVerticesPerRectangle * kIntegersPerVertex_); newHoverLines_.clear(); - for (auto& di : newLineList_) + for (std::size_t i = 0; i < newLineList_.size(); ++i) { - BufferLine(di); + auto& di = newLineList_[i]; + + // Update line buffer + UpdateSingleBuffer( + di, i, newLinesBuffer_, newIntegerBuffer_, newHoverLines_); + } + + // All lines have been updated + dirtyLines_.clear(); +} + +void GeoLines::Impl::UpdateModifiedLineBuffers() +{ + // Synchronize line list + currentLineList_ = newLineList_; + currentLinesBuffer_.resize(currentLineList_.size() * kLineBufferLength_); + currentIntegerBuffer_.resize(currentLineList_.size() * + kVerticesPerRectangle * kIntegersPerVertex_); + + // Update buffers for modified lines + for (auto& di : dirtyLines_) + { + // Find modified line in the current list + auto it = + std::find(currentLineList_.cbegin(), currentLineList_.cend(), di); + + // Ignore invalid lines + if (it == currentLineList_.cend()) + { + continue; + } + + auto lineIndex = std::distance(currentLineList_.cbegin(), it); + + UpdateSingleBuffer(di, + lineIndex, + currentLinesBuffer_, + currentIntegerBuffer_, + currentHoverLines_); + } + + // Clear list of modified lines + if (!dirtyLines_.empty()) + { + dirtyLines_.clear(); + dirty_ = true; } } -void GeoLines::Impl::BufferLine( - const std::shared_ptr& di) +void GeoLines::Impl::UpdateSingleBuffer( + const std::shared_ptr& di, + std::size_t lineIndex, + std::vector& lineBuffer, + std::vector& integerBuffer, + std::vector& hoverLines) { // Threshold value units::length::nautical_miles threshold = di->threshold_; @@ -438,38 +567,66 @@ void GeoLines::Impl::BufferLine( const float mc2 = di->modulate_[2]; const float mc3 = di->modulate_[3]; - // Update buffers - newLinesBuffer_.insert(newLinesBuffer_.end(), - { - // Line - lat1, lon1, lx, by, mc0, mc1, mc2, mc3, a, // BL - lat2, lon2, lx, ty, mc0, mc1, mc2, mc3, a, // TL - lat1, lon1, rx, by, mc0, mc1, mc2, mc3, a, // BR - lat1, lon1, rx, by, mc0, mc1, mc2, mc3, a, // BR - lat2, lon2, rx, ty, mc0, mc1, mc2, mc3, a, // TR - lat2, lon2, lx, ty, mc0, mc1, mc2, mc3, a // TL - }); - newIntegerBuffer_.insert(newIntegerBuffer_.end(), - {thresholdValue, - startTime, - endTime, - thresholdValue, - startTime, - endTime, - thresholdValue, - startTime, - endTime, - thresholdValue, - startTime, - endTime, - thresholdValue, - startTime, - endTime, - thresholdValue, - startTime, - endTime}); + // Visibility + const GLint v = static_cast(di->visible_); - if (!di->hoverText_.empty()) + // Initiailize line data + const auto lineData = { + // Line + lat1, lon1, lx, by, mc0, mc1, mc2, mc3, a, // BL + lat2, lon2, lx, ty, mc0, mc1, mc2, mc3, a, // TL + lat1, lon1, rx, by, mc0, mc1, mc2, mc3, a, // BR + lat1, lon1, rx, by, mc0, mc1, mc2, mc3, a, // BR + lat2, lon2, rx, ty, mc0, mc1, mc2, mc3, a, // TR + lat2, lon2, lx, ty, mc0, mc1, mc2, mc3, a // TL + }; + const auto integerData = {thresholdValue, startTime, endTime, v, + thresholdValue, startTime, endTime, v, + thresholdValue, startTime, endTime, v, + thresholdValue, startTime, endTime, v, + thresholdValue, startTime, endTime, v, + thresholdValue, startTime, endTime, v}; + + // Buffer position data + auto lineBufferPosition = lineBuffer.end(); + auto lineBufferOffset = lineIndex * kLineBufferLength_; + + auto integerBufferPosition = integerBuffer.end(); + auto integerBufferOffset = lineIndex * kIntegerBufferLength_; + + if (lineBufferOffset < lineBuffer.size()) + { + lineBufferPosition = lineBuffer.begin() + lineBufferOffset; + } + if (integerBufferOffset < integerBuffer.size()) + { + integerBufferPosition = integerBuffer.begin() + integerBufferOffset; + } + + if (lineBufferPosition == lineBuffer.cend()) + { + lineBuffer.insert(lineBufferPosition, lineData); + } + else + { + std::copy(lineData.begin(), lineData.end(), lineBufferPosition); + } + + if (integerBufferPosition == integerBuffer.cend()) + { + integerBuffer.insert(integerBufferPosition, integerData); + } + else + { + std::copy(integerData.begin(), integerData.end(), integerBufferPosition); + } + + auto hoverIt = std::find_if(hoverLines.begin(), + hoverLines.end(), + [&di](auto& entry) { return entry.di_ == di; }); + + if (di->visible_ && (!di->hoverText_.empty() || + di->hoverCallback_ != nullptr || di->event_ != nullptr)) { const units::angle::radians radians = angle; @@ -486,13 +643,31 @@ void GeoLines::Impl::BufferLine( const glm::vec2 obl = rotate * glm::vec2 {-hw, -hw}; const glm::vec2 obr = rotate * glm::vec2 {+hw, -hw}; - newHoverLines_.emplace_back( - LineHoverEntry {di, sc1, sc2, otl, otr, obl, obr}); + if (hoverIt == hoverLines.end()) + { + hoverLines.emplace_back( + LineHoverEntry {di, sc1, sc2, otl, otr, obl, obr}); + } + else + { + hoverIt->p1_ = sc1; + hoverIt->p2_ = sc2; + hoverIt->otl_ = otl; + hoverIt->otr_ = otr; + hoverIt->obl_ = obl; + hoverIt->obr_ = obr; + } + } + else if (hoverIt != hoverLines.end()) + { + hoverLines.erase(hoverIt); } } void GeoLines::Impl::Update() { + UpdateModifiedLineBuffers(); + // If the lines have been updated if (dirty_) { @@ -522,7 +697,7 @@ bool GeoLines::RunMousePicking( const QPointF& mouseGlobalPos, const glm::vec2& mouseCoords, const common::Coordinate& /* mouseGeoCoords */, - std::shared_ptr& /* eventHandler */) + std::shared_ptr& eventHandler) { std::unique_lock lock {p->lineMutex_}; @@ -552,8 +727,8 @@ bool GeoLines::RunMousePicking( // For each pickable line auto it = std::find_if( std::execution::par_unseq, - p->currentHoverLines_.crbegin(), - p->currentHoverLines_.crend(), + p->currentHoverLines_.rbegin(), + p->currentHoverLines_.rend(), [&mapDistance, &selectedTime, &mapMatrix, &mouseCoords](const auto& line) { if (( @@ -612,12 +787,34 @@ bool GeoLines::RunMousePicking( if (it != p->currentHoverLines_.crend()) { itemPicked = true; - util::tooltip::Show(it->di_->hoverText_, mouseGlobalPos); + + if (!it->di_->hoverText_.empty()) + { + // Show tooltip + util::tooltip::Show(it->di_->hoverText_, mouseGlobalPos); + } + else if (it->di_->hoverCallback_ != nullptr) + { + it->di_->hoverCallback_(it->di_, mouseGlobalPos); + } + + if (it->di_->event_ != nullptr) + { + // Register event handler + eventHandler = it->di_; + } } return itemPicked; } +void GeoLines::RegisterEventHandler( + const std::shared_ptr& di, + const std::function& eventHandler) +{ + di->event_ = eventHandler; +} + } // namespace draw } // namespace gl } // namespace qt diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp index 6422ab4a..b7b792d0 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp @@ -19,6 +19,10 @@ struct GeoLineDrawItem; class GeoLines : public DrawItem { public: + typedef std::function&, + const QPointF&)> + HoverCallback; + explicit GeoLines(std::shared_ptr context); ~GeoLines(); @@ -75,11 +79,11 @@ public: * @param [in] longitude2 The longitude of the second endpoint of the geo * line in degrees. */ - static void SetLineLocation(const std::shared_ptr& di, - float latitude1, - float longitude1, - float latitude2, - float longitude2); + void SetLineLocation(const std::shared_ptr& di, + float latitude1, + float longitude1, + float latitude2, + float longitude2); /** * Sets the modulate color of a geo line. @@ -87,8 +91,8 @@ public: * @param [in] di Geo line draw item * @param [in] modulate Modulate color */ - static void SetLineModulate(const std::shared_ptr& di, - boost::gil::rgba8_pixel_t color); + void SetLineModulate(const std::shared_ptr& di, + boost::gil::rgba8_pixel_t color); /** * Sets the modulate color of a geo line. @@ -96,24 +100,32 @@ public: * @param [in] di Geo line draw item * @param [in] modulate Modulate color */ - static void SetLineModulate(const std::shared_ptr& di, - boost::gil::rgba32f_pixel_t modulate); + void SetLineModulate(const std::shared_ptr& di, + boost::gil::rgba32f_pixel_t modulate); /** * Sets the width of the geo line. * * @param [in] width Width in pixels */ - static void SetLineWidth(const std::shared_ptr& di, - float width); + void SetLineWidth(const std::shared_ptr& di, float width); /** * Sets the visibility of the geo line. * * @param [in] visible */ - static void SetLineVisible(const std::shared_ptr& di, - bool visible); + void SetLineVisible(const std::shared_ptr& di, + bool visible); + + /** + * Sets the hover callback enable of a geo line. + * + * @param [in] di Geo line draw item + * @param [in] enabled Hover enabled + */ + void SetLineHoverCallback(const std::shared_ptr& di, + const HoverCallback& callback); /** * Sets the hover text of a geo line. @@ -121,14 +133,42 @@ public: * @param [in] di Geo line draw item * @param [in] text Hover text */ - static void SetLineHoverText(const std::shared_ptr& di, - const std::string& text); + void SetLineHoverText(const std::shared_ptr& di, + const std::string& text); + + /** + * Sets the start time of a geo line. + * + * @param [in] di Geo line draw item + * @param [in] startTime Start time + */ + void SetLineStartTime(const std::shared_ptr& di, + std::chrono::system_clock::time_point startTime); + + /** + * Sets the end time of a geo line. + * + * @param [in] di Geo line draw item + * @param [in] endTime End time + */ + void SetLineEndTime(const std::shared_ptr& di, + std::chrono::system_clock::time_point endTime); /** * Finalizes the draw item after adding new lines. */ void FinishLines(); + /** + * Registers an event handler for a geo line. + * + * @param [in] di Geo line draw item + * @param [in] eventHandler Event handler function + */ + static void + RegisterEventHandler(const std::shared_ptr& di, + const std::function& eventHandler); + private: class Impl; std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp index be053e00..3dbab117 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp @@ -219,13 +219,13 @@ void LinkedVectors::FinishVectors() const double& latitude2 = coordinate2.latitude_; const double& longitude2 = coordinate2.longitude_; - GeoLines::SetLineLocation( + p->geoLines_->SetLineLocation( borderLine, latitude1, longitude1, latitude2, longitude2); - GeoLines::SetLineModulate(borderLine, kBlack); - GeoLines::SetLineWidth(borderLine, di->width_ + 2.0f); - GeoLines::SetLineVisible(borderLine, di->visible_); - GeoLines::SetLineHoverText(borderLine, di->hoverText_); + p->geoLines_->SetLineModulate(borderLine, kBlack); + p->geoLines_->SetLineWidth(borderLine, di->width_ + 2.0f); + p->geoLines_->SetLineVisible(borderLine, di->visible_); + p->geoLines_->SetLineHoverText(borderLine, di->hoverText_); di->borderDrawItems_.emplace_back(std::move(borderLine)); @@ -248,13 +248,13 @@ void LinkedVectors::FinishVectors() auto tickBorderLine = p->geoLines_->AddLine(); - GeoLines::SetLineLocation( + p->geoLines_->SetLineLocation( tickBorderLine, tickLat1, tickLon1, tickLat2, tickLon2); - GeoLines::SetLineModulate(tickBorderLine, kBlack); - GeoLines::SetLineWidth(tickBorderLine, di->width_ + 2.0f); - GeoLines::SetLineVisible(tickBorderLine, di->visible_); - GeoLines::SetLineHoverText(tickBorderLine, di->hoverText_); + p->geoLines_->SetLineModulate(tickBorderLine, kBlack); + p->geoLines_->SetLineWidth(tickBorderLine, di->width_ + 2.0f); + p->geoLines_->SetLineVisible(tickBorderLine, di->visible_); + p->geoLines_->SetLineHoverText(tickBorderLine, di->hoverText_); tickRadius += di->tickRadiusIncrement_; } @@ -279,17 +279,17 @@ void LinkedVectors::FinishVectors() const double& latitude2 = coordinate2.latitude_; const double& longitude2 = coordinate2.longitude_; - GeoLines::SetLineLocation( + p->geoLines_->SetLineLocation( geoLine, latitude1, longitude1, latitude2, longitude2); - GeoLines::SetLineModulate(geoLine, di->modulate_); - GeoLines::SetLineWidth(geoLine, di->width_); - GeoLines::SetLineVisible(geoLine, di->visible_); + p->geoLines_->SetLineModulate(geoLine, di->modulate_); + p->geoLines_->SetLineWidth(geoLine, di->width_); + p->geoLines_->SetLineVisible(geoLine, di->visible_); // If the border is not enabled, this line must have hover text instead if (!p->borderEnabled_) { - GeoLines::SetLineHoverText(geoLine, di->hoverText_); + p->geoLines_->SetLineHoverText(geoLine, di->hoverText_); } di->lineDrawItems_.emplace_back(std::move(geoLine)); @@ -313,17 +313,17 @@ void LinkedVectors::FinishVectors() auto tickGeoLine = p->geoLines_->AddLine(); - GeoLines::SetLineLocation( + p->geoLines_->SetLineLocation( tickGeoLine, tickLat1, tickLon1, tickLat2, tickLon2); - GeoLines::SetLineModulate(tickGeoLine, di->modulate_); - GeoLines::SetLineWidth(tickGeoLine, di->width_); - GeoLines::SetLineVisible(tickGeoLine, di->visible_); + p->geoLines_->SetLineModulate(tickGeoLine, di->modulate_); + p->geoLines_->SetLineWidth(tickGeoLine, di->width_); + p->geoLines_->SetLineVisible(tickGeoLine, di->visible_); // If the border is not enabled, this line must have hover text if (!p->borderEnabled_) { - GeoLines::SetLineHoverText(tickGeoLine, di->hoverText_); + p->geoLines_->SetLineHoverText(tickGeoLine, di->hoverText_); } tickRadius += di->tickRadiusIncrement_; diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 718f0e3a..4046e3bd 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -830,6 +830,10 @@ void MainWindowImpl::ConnectMapSignals() { for (const auto& mapWidget : maps_) { + connect(mapWidget, + &map::MapWidget::AlertSelected, + alertDockWidget_, + &ui::AlertDockWidget::SelectAlert); connect(mapWidget, &map::MapWidget::MapParametersChanged, this, diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index a7f727f0..cc8bc29b 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -1,17 +1,23 @@ #include +#include #include +#include #include -#include #include +#include #include -#include #include -#include +#include +#include #include -#include +#include +#include +#include +#include #include +#include namespace scwx { @@ -23,29 +29,7 @@ namespace map static const std::string logPrefix_ = "scwx::qt::map::alert_layer"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); -static std::vector -AddAlertLayer(std::shared_ptr map, - awips::Phenomenon phenomenon, - bool alertActive, - const QString& beforeLayer); -static QMapLibre::Feature -CreateFeature(const awips::CodedLocation& codedLocation); -static QMapLibre::Coordinate -GetMapboxCoordinate(const common::Coordinate& coordinate); -static QMapLibre::Coordinates - GetMapboxCoordinates(const awips::CodedLocation& codedLocation); -static QString GetSourceId(awips::Phenomenon phenomenon, bool alertActive); -static QString GetSuffix(awips::Phenomenon phenomenon, bool alertActive); - -static const QVariantMap kEmptyFeatureCollection_ { - {"type", "geojson"}, - {"data", QVariant::fromValue(std::list {})}}; -static const std::vector kAlertPhenomena_ { - awips::Phenomenon::Marine, - awips::Phenomenon::FlashFlood, - awips::Phenomenon::SevereThunderstorm, - awips::Phenomenon::SnowSquall, - awips::Phenomenon::Tornado}; +static const boost::gil::rgba32f_pixel_t kBlack_ {0.0f, 0.0f, 0.0f, 1.0f}; template struct AlertTypeHash; @@ -58,203 +42,315 @@ struct AlertTypeHash> class AlertLayerHandler : public QObject { - Q_OBJECT public : - explicit AlertLayerHandler() : - textEventManager_ {manager::TextEventManager::Instance()}, - alertUpdateTimer_ {scwx::util::io_context()}, - alertSourceMap_ {}, - featureMap_ {} + Q_OBJECT +public: + struct SegmentRecord { - for (auto& phenomenon : kAlertPhenomena_) - { - for (bool alertActive : {false, true}) - { - alertSourceMap_.emplace(std::make_pair(phenomenon, alertActive), - kEmptyFeatureCollection_); - } - } + 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, - &AlertLayerHandler::HandleAlert); + [this](const types::TextEventKey& key, std::size_t messageIndex) + { HandleAlert(key, messageIndex); }); } ~AlertLayerHandler() { + disconnect(textEventManager_.get(), nullptr, this, nullptr); + std::unique_lock lock(alertMutex_); - alertUpdateTimer_.cancel(); } - static std::shared_ptr Instance(); + std::unordered_map< + std::pair, + boost::container::stable_vector>, + AlertTypeHash>> + segmentsByType_ {}; - std::list* FeatureList(awips::Phenomenon phenomenon, - bool alertActive); + std::unordered_map< + types::TextEventKey, + boost::container::stable_vector>, + types::TextEventHash> + segmentsByKey_ {}; void HandleAlert(const types::TextEventKey& key, size_t messageIndex); - void UpdateAlerts(); - std::shared_ptr textEventManager_; + static AlertLayerHandler& Instance(); - boost::asio::steady_timer alertUpdateTimer_; - std::unordered_map, - QVariantMap, - AlertTypeHash>> - alertSourceMap_; - std::unordered_multimap::iterator, - std::chrono::system_clock::time_point>, - types::TextEventHash> - featureMap_; - std::shared_mutex alertMutex_; + 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 AlertLayerImpl : public QObject +class AlertLayer::Impl { - Q_OBJECT public: - explicit AlertLayerImpl(std::shared_ptr context) : - context_ {context}, alertLayerHandler_ {AlertLayerHandler::Instance()} + explicit Impl(AlertLayer* self, + std::shared_ptr context, + awips::Phenomenon phenomenon) : + self_ {self}, + phenomenon_ {phenomenon}, + geoLines_ {{false, std::make_shared(context)}, + {true, std::make_shared(context)}} { - connect(alertLayerHandler_.get(), - &AlertLayerHandler::AlertsUpdated, - this, - &AlertLayerImpl::UpdateSource); + auto& paletteSettings = settings::PaletteSettings::Instance(); + + for (auto alertActive : {false, true}) + { + lineColor_.emplace( + alertActive, + util::color::ToRgba32fPixelT( + paletteSettings.alert_color(phenomenon_, alertActive) + .GetValue())); + } + + ConnectSignals(); + ScheduleRefresh(); } - ~AlertLayerImpl() {}; + ~Impl() + { + std::unique_lock refreshLock(refreshMutex_); + refreshTimer_.cancel(); + refreshLock.unlock(); - void UpdateSource(awips::Phenomenon phenomenon, bool alertActive); + threadPool_.join(); - std::shared_ptr context_; - std::shared_ptr alertLayerHandler_; + 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::shared_ptr& di, + QEvent* ev); + void HandleGeoLinesHover(std::shared_ptr& di, + const QPointF& mouseGlobalPos); + void ScheduleRefresh(); + + 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); + + boost::asio::thread_pool threadPool_ {1u}; + + AlertLayer* self_; + + boost::asio::system_timer refreshTimer_ {threadPool_}; + std::mutex refreshMutex_; + + const awips::Phenomenon phenomenon_; + + 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 lineColor_; + + std::chrono::system_clock::time_point selectedTime_ {}; + + std::shared_ptr lastHoverDi_ {nullptr}; + std::string tooltip_ {}; }; -AlertLayer::AlertLayer(std::shared_ptr context) : - p(std::make_unique(context)) +AlertLayer::AlertLayer(std::shared_ptr context, + awips::Phenomenon phenomenon) : + DrawLayer(context), p(std::make_unique(this, context, phenomenon)) { + for (auto alertActive : {false, true}) + { + auto& geoLines = p->geoLines_.at(alertActive); + + AddDrawItem(geoLines); + } } AlertLayer::~AlertLayer() = default; -std::vector AlertLayer::AddLayers(awips::Phenomenon phenomenon, - const std::string& before) +void AlertLayer::InitializeHandler() { - logger_->debug("AddLayers(): {}", awips::GetPhenomenonCode(phenomenon)); + static bool ftt = true; - std::vector layers {}; - - auto map = p->context_->map().lock(); - if (map == nullptr) + if (ftt) { - return layers; + logger_->debug("Initializing handler"); + AlertLayerHandler::Instance(); + ftt = false; } - - const QString beforeLayer {QString::fromStdString(before)}; - - // Add/update GeoJSON sources and create layers - for (bool alertActive : {false, true}) - { - p->UpdateSource(phenomenon, alertActive); - auto newLayers = AddAlertLayer(map, phenomenon, alertActive, beforeLayer); - layers.insert(layers.end(), newLayers.cbegin(), newLayers.cend()); - } - - return layers; } -std::list* -AlertLayerHandler::FeatureList(awips::Phenomenon phenomenon, bool alertActive) +void AlertLayer::Initialize() { - std::list* featureList = nullptr; + logger_->debug("Initialize: {}", awips::GetPhenomenonText(p->phenomenon_)); - auto key = std::make_pair(phenomenon, alertActive); - auto it = alertSourceMap_.find(key); - if (it != alertSourceMap_.cend()) + 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}) { - featureList = reinterpret_cast*>( - it->second["data"].data()); + 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(); } - return featureList; + 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(); } void AlertLayerHandler::HandleAlert(const types::TextEventKey& key, size_t messageIndex) { - // Skip alert if there are more messages to be processed - if (messageIndex + 1 < textEventManager_->message_count(key)) - { - return; - } + logger_->trace("HandleAlert: {}", key.ToString()); - auto message = textEventManager_->message_list(key).at(messageIndex); std::unordered_set, AlertTypeHash>> alertsUpdated {}; - // Take a unique lock before modifying feature lists - std::unique_lock lock(alertMutex_); + auto message = textEventManager_->message_list(key).at(messageIndex); - // Remove existing features for key - auto existingFeatures = featureMap_.equal_range(key); - for (auto it = existingFeatures.first; it != existingFeatures.second; ++it) + // Determine start time for first segment + std::chrono::system_clock::time_point segmentBegin {}; + if (message->segment_count() > 0) { - auto& [phenomenon, alertActive, featureIt, eventEnd] = it->second; - auto featureList = FeatureList(phenomenon, alertActive); - if (featureList != nullptr) - { - // Remove existing feature for key - featureList->erase(featureIt); + segmentBegin = message->segment(0)->event_begin(); + } - // Mark alert type as updated - alertsUpdated.emplace(phenomenon, alertActive); + // 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); } } - featureMap_.erase(existingFeatures.first, existingFeatures.second); - for (auto segment : message->segments()) + // 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(); auto action = vtec.pVtec_.action(); awips::Phenomenon phenomenon = vtec.pVtec_.phenomenon(); - auto eventEnd = vtec.pVtec_.event_end(); bool alertActive = (action != awips::PVtec::Action::Canceled); - // If the event has ended, skip it - if (eventEnd < std::chrono::system_clock::now()) - { - continue; - } + auto& segmentsForType = segmentsByType_[{key.phenomenon_, alertActive}]; - auto featureList = FeatureList(phenomenon, alertActive); - if (featureList != nullptr) - { - // Add alert location to polygon list - auto featureIt = featureList->emplace( - featureList->cend(), - CreateFeature(segment->codedLocation_.value())); + // Insert segment into lists + std::shared_ptr segmentRecord = + std::make_shared(segment, key, message); - // Store iterator for created feature in feature map - featureMap_.emplace(std::piecewise_construct, - std::forward_as_tuple(key), - std::forward_as_tuple( - phenomenon, alertActive, featureIt, eventEnd)); + segmentsForKey.push_back(segmentRecord); + segmentsForType.push_back(segmentRecord); - // Mark alert type as updated - alertsUpdated.emplace(phenomenon, alertActive); - } + Q_EMIT AlertAdded(segmentRecord, phenomenon); + + alertsUpdated.emplace(phenomenon, alertActive); } - // Release the lock after completing feature list updates + // Release the lock after completing segment updates lock.unlock(); for (auto& alert : alertsUpdated) @@ -264,219 +360,288 @@ void AlertLayerHandler::HandleAlert(const types::TextEventKey& key, } } -void AlertLayerHandler::UpdateAlerts() +void AlertLayer::Impl::ConnectAlertHandlerSignals() { - logger_->trace("UpdateAlerts"); + auto& alertLayerHandler = AlertLayerHandler::Instance(); - // Take a unique lock before modifying feature lists - std::unique_lock lock(alertMutex_); - - std::unordered_set, - AlertTypeHash>> - alertsUpdated {}; - - // Evaluate each rendered feature for expiration - for (auto it = featureMap_.begin(); it != featureMap_.end();) - { - auto& [phenomenon, alertActive, featureIt, eventEnd] = it->second; - - // If the event has ended, remove it from the feature list - if (eventEnd < std::chrono::system_clock::now()) + QObject::connect( + &alertLayerHandler, + &AlertLayerHandler::AlertAdded, + receiver_.get(), + [this]( + const std::shared_ptr& segmentRecord, + awips::Phenomenon phenomenon) { - logger_->debug("Alert expired: {}", it->first.ToString()); - - auto featureList = FeatureList(phenomenon, alertActive); - if (featureList != nullptr) + if (phenomenon == phenomenon_) { - // Remove existing feature for key - featureList->erase(featureIt); - - // Mark alert type as updated - alertsUpdated.emplace(phenomenon, alertActive); + AddAlert(segmentRecord); } - - // Erase current item and increment iterator - it = featureMap_.erase(it); - } - else + }); + QObject::connect( + &alertLayerHandler, + &AlertLayerHandler::AlertUpdated, + receiver_.get(), + [this]( + const std::shared_ptr& segmentRecord) { - // Current item is not expired, continue - ++it; - } - } - - for (auto& alert : alertsUpdated) - { - // Emit signal for each updated alert type - Q_EMIT AlertsUpdated(alert.first, alert.second); - } - - using namespace std::chrono; - alertUpdateTimer_.expires_after(15s); - alertUpdateTimer_.async_wait( - [this](const boost::system::error_code& e) - { - if (e == boost::asio::error::operation_aborted) + if (segmentRecord->key_.phenomenon_ == phenomenon_) { - logger_->debug("Alert update timer cancelled"); - } - else if (e != boost::system::errc::success) - { - logger_->warn("Alert update timer error: {}", e.message()); - } - else - { - try - { - UpdateAlerts(); - } - catch (const std::exception& ex) - { - logger_->error(ex.what()); - } + UpdateAlert(segmentRecord); } }); } -void AlertLayerImpl::UpdateSource(awips::Phenomenon phenomenon, - bool alertActive) +void AlertLayer::Impl::ConnectSignals() { - auto map = context_->map().lock(); - if (map == nullptr) + auto timelineManager = manager::TimelineManager::Instance(); + + QObject::connect(timelineManager.get(), + &manager::TimelineManager::SelectedTimeUpdated, + receiver_.get(), + [this](std::chrono::system_clock::time_point dateTime) + { selectedTime_ = dateTime; }); +} + +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_; + + auto& vtec = segment->header_->vtecString_.front(); + auto action = vtec.pVtec_.action(); + bool alertActive = (action != awips::PVtec::Action::Canceled); + auto& startTime = segmentRecord->segmentBegin_; + auto& endTime = segmentRecord->segmentEnd_; + + auto& lineColor = lineColor_.at(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) { - return; + // Add border + AddLines(geoLines, + coordinates, + kBlack_, + 5.0f, + startTime, + endTime, + true, + drawItems.first->second); + + // Add only border to segmentsByLine_ + for (auto& di : drawItems.first->second) + { + segmentsByLine_.insert({di, segmentRecord}); + } + + // Add line + AddLines(geoLines, + coordinates, + lineColor, + 3.0f, + startTime, + endTime, + false, + 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_; + + auto& vtec = segment->header_->vtecString_.front(); + auto action = vtec.pVtec_.action(); + bool alertActive = (action != awips::PVtec::Action::Canceled); + + 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)); + + gl::draw::GeoLines::RegisterEventHandler( + di, + std::bind(&AlertLayer::Impl::HandleGeoLinesEvent, + this, + di, + std::placeholders::_1)); + } +} + +void AlertLayer::Impl::HandleGeoLinesEvent( + std::shared_ptr& di, QEvent* ev) +{ + 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; } - // Take a shared lock before using feature lists - std::shared_lock lock(alertLayerHandler_->alertMutex_); - - // Update source, relies on alert source being defined - map->updateSource(GetSourceId(phenomenon, alertActive), - alertLayerHandler_->alertSourceMap_.at( - std::make_pair(phenomenon, alertActive))); + default: + break; + } } -std::shared_ptr AlertLayerHandler::Instance() +void AlertLayer::Impl::HandleGeoLinesHover( + std::shared_ptr& di, + const QPointF& mouseGlobalPos) { - static std::weak_ptr alertLayerHandlerReference {}; - static std::mutex instanceMutex {}; - - std::unique_lock lock(instanceMutex); - - std::shared_ptr alertLayerHandler = - alertLayerHandlerReference.lock(); - - if (alertLayerHandler == nullptr) + if (di != lastHoverDi_) { - alertLayerHandler = std::make_shared(); - alertLayerHandlerReference = alertLayerHandler; + auto it = segmentsByLine_.find(di); + if (it != segmentsByLine_.cend()) + { + tooltip_ = + boost::algorithm::join(it->second->segment_->productContent_, "\n"); + } + else + { + tooltip_.clear(); + } - alertLayerHandler->UpdateAlerts(); + lastHoverDi_ = di; } - return alertLayerHandler; -} - -static std::vector -AddAlertLayer(std::shared_ptr map, - awips::Phenomenon phenomenon, - bool alertActive, - const QString& beforeLayer) -{ - settings::PaletteSettings& paletteSettings = - settings::PaletteSettings::Instance(); - - QString layerPrefix = QString::fromStdString( - types::GetLayerName(types::LayerType::Alert, phenomenon)); - - QString sourceId = GetSourceId(phenomenon, alertActive); - QString idSuffix = GetSuffix(phenomenon, alertActive); - auto outlineColor = util::color::ToRgba8PixelT( - paletteSettings.alert_color(phenomenon, alertActive).GetValue()); - - QString bgLayerId = QString("%1::bg-%2").arg(layerPrefix).arg(idSuffix); - QString fgLayerId = QString("%1::fg-%2").arg(layerPrefix).arg(idSuffix); - - if (map->layerExists(bgLayerId)) + if (!tooltip_.empty()) { - map->removeLayer(bgLayerId); + util::tooltip::Show(tooltip_, mouseGlobalPos); } - if (map->layerExists(fgLayerId)) - { - map->removeLayer(fgLayerId); - } - - const float opacity = outlineColor[3] / 255.0f; - - map->addLayer( - bgLayerId, {{"type", "line"}, {"source", sourceId}}, beforeLayer); - map->setLayoutProperty(bgLayerId, "line-join", "round"); - map->setLayoutProperty(bgLayerId, "line-cap", "round"); - map->setPaintProperty(bgLayerId, "line-color", "rgba(0, 0, 0, 255)"); - map->setPaintProperty(bgLayerId, "line-opacity", QString("%1").arg(opacity)); - map->setPaintProperty(bgLayerId, "line-width", "5"); - - map->addLayer( - fgLayerId, {{"type", "line"}, {"source", sourceId}}, beforeLayer); - map->setLayoutProperty(fgLayerId, "line-join", "round"); - map->setLayoutProperty(fgLayerId, "line-cap", "round"); - map->setPaintProperty(fgLayerId, - "line-color", - QString("rgba(%1, %2, %3, %4)") - .arg(outlineColor[0]) - .arg(outlineColor[1]) - .arg(outlineColor[2]) - .arg(outlineColor[3])); - map->setPaintProperty(fgLayerId, "line-opacity", QString("%1").arg(opacity)); - map->setPaintProperty(fgLayerId, "line-width", "3"); - - return {bgLayerId.toStdString(), fgLayerId.toStdString()}; } -static QMapLibre::Feature -CreateFeature(const awips::CodedLocation& codedLocation) +AlertLayerHandler& AlertLayerHandler::Instance() { - auto mapboxCoordinates = GetMapboxCoordinates(codedLocation); - - return QMapLibre::Feature { - QMapLibre::Feature::PolygonType, - std::initializer_list { - std::initializer_list {{mapboxCoordinates}}}}; -} - -static QMapLibre::Coordinate -GetMapboxCoordinate(const common::Coordinate& coordinate) -{ - return {coordinate.latitude_, coordinate.longitude_}; -} - -static QMapLibre::Coordinates -GetMapboxCoordinates(const awips::CodedLocation& codedLocation) -{ - auto scwxCoordinates = codedLocation.coordinates(); - QMapLibre::Coordinates mapboxCoordinates(scwxCoordinates.size() + 1u); - - std::transform(scwxCoordinates.cbegin(), - scwxCoordinates.cend(), - mapboxCoordinates.begin(), - [](auto& coordinate) -> QMapLibre::Coordinate - { return GetMapboxCoordinate(coordinate); }); - - mapboxCoordinates.back() = GetMapboxCoordinate(scwxCoordinates.front()); - - return mapboxCoordinates; -} - -static QString GetSourceId(awips::Phenomenon phenomenon, bool alertActive) -{ - return QString("alertPolygon-%1").arg(GetSuffix(phenomenon, alertActive)); -} - -static QString GetSuffix(awips::Phenomenon phenomenon, bool alertActive) -{ - return QString("-%1.%2") - .arg(QString::fromStdString(awips::GetPhenomenonCode(phenomenon))) - .arg(alertActive); + static AlertLayerHandler alertLayerHandler_ {}; + return alertLayerHandler_; } size_t AlertTypeHash>::operator()( diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.hpp b/scwx-qt/source/scwx/qt/map/alert_layer.hpp index 6ce681a9..99609210 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.hpp @@ -1,7 +1,8 @@ #pragma once #include -#include +#include +#include #include #include @@ -14,19 +15,28 @@ namespace qt namespace map { -class AlertLayerImpl; - -class AlertLayer +class AlertLayer : public DrawLayer { + Q_OBJECT + Q_DISABLE_COPY_MOVE(AlertLayer) + public: - explicit AlertLayer(std::shared_ptr context); + explicit AlertLayer(std::shared_ptr context, + scwx::awips::Phenomenon phenomenon); ~AlertLayer(); - std::vector AddLayers(awips::Phenomenon phenomenon, - const std::string& before = {}); + void Initialize() override final; + void Render(const QMapLibre::CustomLayerRenderParameters&) override final; + void Deinitialize() override final; + + static void InitializeHandler(); + +signals: + void AlertSelected(const types::TextEventKey& key); private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; } // namespace map diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.cpp b/scwx-qt/source/scwx/qt/map/draw_layer.cpp index 92254307..7d03d13a 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.cpp @@ -55,6 +55,9 @@ void DrawLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) bool textureAtlasChanged = newTextureAtlasBuildCount != p->textureAtlasBuildCount_; + // Set OpenGL blend mode for transparency + gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + gl.glActiveTexture(GL_TEXTURE0); gl.glBindTexture(GL_TEXTURE_2D_ARRAY, p->textureAtlas_); diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 3b0358d5..e718bc0e 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -79,7 +79,6 @@ public: imGuiRendererInitialized_ {false}, radarProductManager_ {nullptr}, radarProductLayer_ {nullptr}, - alertLayer_ {std::make_shared(context_)}, overlayLayer_ {nullptr}, placefileLayer_ {nullptr}, colorTableLayer_ {nullptr}, @@ -100,6 +99,9 @@ public: overlayProductView->SetAutoRefresh(autoRefreshEnabled_); overlayProductView->SetAutoUpdate(autoUpdateEnabled_); + // Initialize AlertLayerHandler + map::AlertLayer::InitializeHandler(); + auto& generalSettings = settings::GeneralSettings::Instance(); // Initialize context @@ -218,7 +220,6 @@ public: std::shared_ptr radarProductManager_; std::shared_ptr radarProductLayer_; - std::shared_ptr alertLayer_; std::shared_ptr overlayLayer_; std::shared_ptr overlayProductLayer_ {nullptr}; std::shared_ptr placefileLayer_; @@ -1180,10 +1181,17 @@ void MapWidgetImpl::AddLayer(types::LayerType type, } else if (type == types::LayerType::Alert) { - // Add the alert layer for the phenomenon - auto newLayers = alertLayer_->AddLayers( - std::get(description), before); - layerList_.insert(layerList_.end(), newLayers.cbegin(), newLayers.cend()); + auto phenomenon = std::get(description); + + std::shared_ptr alertLayer = + std::make_shared(context_, phenomenon); + AddLayer(fmt::format("alert.{}", awips::GetPhenomenonCode(phenomenon)), + alertLayer, + before); + connect(alertLayer.get(), + &AlertLayer::AlertSelected, + widget_, + &MapWidget::AlertSelected); } else if (type == types::LayerType::Placefile) { diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp index d47c55ab..40f7df77 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.hpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -150,6 +151,7 @@ private slots: void mapChanged(QMapLibre::Map::MapChange); signals: + void AlertSelected(const types::TextEventKey& key); void Level3ProductsChanged(); void MapParametersChanged(double latitude, double longitude, diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 4d47eca6..b2fada95 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -300,9 +300,6 @@ void OverlayLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) context()->set_render_parameters(params); - // Set OpenGL blend mode for transparency - gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - p->sweepTimePicked_ = false; if (radarProductView != nullptr) diff --git a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp index c8312c7d..64745fd5 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp @@ -143,9 +143,6 @@ void OverlayProductLayer::Render( { gl::OpenGLFunctions& gl = context()->gl(); - // Set OpenGL blend mode for transparency - gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - if (p->stiNeedsUpdate_) { p->UpdateStormTrackingInformation(); diff --git a/scwx-qt/source/scwx/qt/map/placefile_layer.cpp b/scwx-qt/source/scwx/qt/map/placefile_layer.cpp index 28cf53e2..be2d9a18 100644 --- a/scwx-qt/source/scwx/qt/map/placefile_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/placefile_layer.cpp @@ -129,9 +129,6 @@ void PlacefileLayer::Render( { gl::OpenGLFunctions& gl = context()->gl(); - // Set OpenGL blend mode for transparency - gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - std::shared_ptr placefileManager = manager::PlacefileManager::Instance(); diff --git a/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp b/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp index e827469c..61fd160a 100644 --- a/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp @@ -114,6 +114,13 @@ void AlertDockWidget::HandleMapUpdate(double latitude, double longitude) } } +void AlertDockWidget::SelectAlert(const types::TextEventKey& key) +{ + // View alert + p->alertDialog_->SelectAlert(key); + p->alertDialog_->show(); +} + void AlertDockWidgetImpl::ConnectSignals() { connect(self_->ui->alertFilter, diff --git a/scwx-qt/source/scwx/qt/ui/alert_dock_widget.hpp b/scwx-qt/source/scwx/qt/ui/alert_dock_widget.hpp index 6ccdd5f4..237bed27 100644 --- a/scwx-qt/source/scwx/qt/ui/alert_dock_widget.hpp +++ b/scwx-qt/source/scwx/qt/ui/alert_dock_widget.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include namespace Ui @@ -32,6 +34,7 @@ signals: public slots: void HandleMapUpdate(double latitude, double longitude); + void SelectAlert(const types::TextEventKey& key); private: friend class AlertDockWidgetImpl; diff --git a/scwx-qt/source/scwx/qt/util/color.cpp b/scwx-qt/source/scwx/qt/util/color.cpp index c1246843..6e193dc9 100644 --- a/scwx-qt/source/scwx/qt/util/color.cpp +++ b/scwx-qt/source/scwx/qt/util/color.cpp @@ -29,6 +29,15 @@ boost::gil::rgba8_pixel_t ToRgba8PixelT(const std::string& argbString) static_cast(qAlpha(color))}; } +boost::gil::rgba32f_pixel_t ToRgba32fPixelT(const std::string& argbString) +{ + boost::gil::rgba8_pixel_t rgba8Pixel = ToRgba8PixelT(argbString); + return boost::gil::rgba32f_pixel_t {rgba8Pixel[0] / 255.0f, + rgba8Pixel[1] / 255.0f, + rgba8Pixel[2] / 255.0f, + rgba8Pixel[3] / 255.0f}; +} + } // namespace color } // namespace util } // namespace qt diff --git a/scwx-qt/source/scwx/qt/util/color.hpp b/scwx-qt/source/scwx/qt/util/color.hpp index d38f033f..73ca07f1 100644 --- a/scwx-qt/source/scwx/qt/util/color.hpp +++ b/scwx-qt/source/scwx/qt/util/color.hpp @@ -29,6 +29,16 @@ std::string ToArgbString(const boost::gil::rgba8_pixel_t& color); */ boost::gil::rgba8_pixel_t ToRgba8PixelT(const std::string& argbString); +/** + * Converts an ARGB string used by Qt libraries to a Boost.GIL 32-bit RGBA + * floating point pixel. + * + * @param argbString ARGB string in the format #AARRGGBB + * + * @return RGBA32 floating point pixel + */ +boost::gil::rgba32f_pixel_t ToRgba32fPixelT(const std::string& argbString); + } // namespace color } // namespace util } // namespace qt diff --git a/wxdata/include/scwx/awips/text_product_message.hpp b/wxdata/include/scwx/awips/text_product_message.hpp index a83f7b82..d402cfd5 100644 --- a/wxdata/include/scwx/awips/text_product_message.hpp +++ b/wxdata/include/scwx/awips/text_product_message.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -57,6 +58,7 @@ struct SegmentHeader struct Segment { + std::shared_ptr wmoHeader_ {}; std::optional header_ {}; std::vector productContent_ {}; std::optional codedLocation_ {}; @@ -73,6 +75,9 @@ struct Segment Segment(Segment&&) noexcept = default; Segment& operator=(Segment&&) noexcept = default; + + std::chrono::system_clock::time_point event_begin() const; + std::chrono::system_clock::time_point event_end() const; }; class TextProductMessageImpl; diff --git a/wxdata/source/scwx/awips/text_product_message.cpp b/wxdata/source/scwx/awips/text_product_message.cpp index 541c6806..5128aee8 100644 --- a/wxdata/source/scwx/awips/text_product_message.cpp +++ b/wxdata/source/scwx/awips/text_product_message.cpp @@ -104,16 +104,14 @@ std::shared_ptr TextProductMessage::segment(size_t s) const return p->segments_[s]; } -std::chrono::system_clock::time_point -TextProductMessage::segment_event_begin(std::size_t s) const +std::chrono::system_clock::time_point Segment::event_begin() const { std::chrono::system_clock::time_point eventBegin {}; - auto& header = segment(s)->header_; - if (header.has_value() && !header->vtecString_.empty()) + if (header_.has_value() && !header_->vtecString_.empty()) { // Determine event begin from P-VTEC string - eventBegin = header->vtecString_[0].pVtec_.event_begin(); + eventBegin = header_->vtecString_[0].pVtec_.event_begin(); // If event begin is 000000T0000Z if (eventBegin == std::chrono::system_clock::time_point {}) @@ -122,13 +120,13 @@ TextProductMessage::segment_event_begin(std::size_t s) const // Determine event end from P-VTEC string system_clock::time_point eventEnd = - header->vtecString_[0].pVtec_.event_end(); + header_->vtecString_[0].pVtec_.event_end(); auto endDays = floor(eventEnd); year_month_day endDate {endDays}; // Determine WMO date/time - std::string wmoDateTime = wmo_header()->date_time(); + std::string wmoDateTime = wmoHeader_->date_time(); bool wmoDateTimeValid = false; unsigned int dayOfMonth = 0; @@ -189,6 +187,25 @@ TextProductMessage::segment_event_begin(std::size_t s) const return eventBegin; } +std::chrono::system_clock::time_point Segment::event_end() const +{ + std::chrono::system_clock::time_point eventEnd {}; + + if (header_.has_value() && !header_->vtecString_.empty()) + { + // Determine event begin from P-VTEC string + eventEnd = header_->vtecString_[0].pVtec_.event_end(); + } + + return eventEnd; +} + +std::chrono::system_clock::time_point +TextProductMessage::segment_event_begin(std::size_t s) const +{ + return segment(s)->event_begin(); +} + size_t TextProductMessage::data_size() const { return 0; @@ -211,6 +228,7 @@ bool TextProductMessage::Parse(std::istream& is) } std::shared_ptr segment = std::make_shared(); + segment->wmoHeader_ = p->wmoHeader_; if (i == 0) {