mirror of
				https://github.com/ciphervance/supercell-wx.git
				synced 2025-10-31 13:40:05 +00:00 
			
		
		
		
	Merge pull request #257 from dpaulat/feature/enhanced-alerts-2
Alert Animation, Hover and Selection
This commit is contained in:
		
						commit
						822d0762a5
					
				
					 18 changed files with 937 additions and 465 deletions
				
			
		|  | @ -6,6 +6,7 @@ | |||
| 
 | ||||
| #include <execution> | ||||
| 
 | ||||
| #include <boost/unordered/unordered_flat_set.hpp> | ||||
| #include <units/angle.h> | ||||
| 
 | ||||
| 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<double>       threshold_ {}; | ||||
|  | @ -46,6 +49,7 @@ struct GeoLineDrawItem | |||
|    float                        width_ {5.0}; | ||||
|    units::angle::degrees<float> angle_ {}; | ||||
|    std::string                  hoverText_ {}; | ||||
|    GeoLines::HoverCallback      hoverCallback_ {nullptr}; | ||||
| }; | ||||
| 
 | ||||
| class GeoLines::Impl | ||||
|  | @ -53,7 +57,7 @@ class GeoLines::Impl | |||
| public: | ||||
|    struct LineHoverEntry | ||||
|    { | ||||
|       std::shared_ptr<const GeoLineDrawItem> di_; | ||||
|       std::shared_ptr<GeoLineDrawItem> di_; | ||||
| 
 | ||||
|       glm::vec2 p1_; | ||||
|       glm::vec2 p2_; | ||||
|  | @ -81,6 +85,12 @@ public: | |||
|    void BufferLine(const std::shared_ptr<const GeoLineDrawItem>& di); | ||||
|    void Update(); | ||||
|    void UpdateBuffers(); | ||||
|    void UpdateModifiedLineBuffers(); | ||||
|    void UpdateSingleBuffer(const std::shared_ptr<GeoLineDrawItem>& di, | ||||
|                            std::size_t                             lineIndex, | ||||
|                            std::vector<float>&                     linesBuffer, | ||||
|                            std::vector<GLint>&          integerBuffer, | ||||
|                            std::vector<LineHoverEntry>& hoverLines); | ||||
| 
 | ||||
|    std::shared_ptr<GlContext> context_; | ||||
| 
 | ||||
|  | @ -88,6 +98,8 @@ public: | |||
|    bool dirty_ {false}; | ||||
|    bool thresholded_ {false}; | ||||
| 
 | ||||
|    boost::unordered_flat_set<std::shared_ptr<GeoLineDrawItem>> 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<void*>(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<GLsizei>(p->vbo_.size()), p->vbo_.data()); | ||||
| 
 | ||||
|    std::unique_lock lock {p->lineMutex_}; | ||||
| 
 | ||||
|  | @ -314,43 +333,104 @@ void GeoLines::SetLineLocation(const std::shared_ptr<GeoLineDrawItem>& 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<GeoLineDrawItem>& 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<GeoLineDrawItem>& 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<GeoLineDrawItem>& di, | ||||
|                             float                                   width) | ||||
| { | ||||
|    di->width_ = width; | ||||
|    if (di->width_ != width) | ||||
|    { | ||||
|       di->width_ = width; | ||||
|       p->dirtyLines_.insert(di); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void GeoLines::SetLineVisible(const std::shared_ptr<GeoLineDrawItem>& di, | ||||
|                               bool                                    visible) | ||||
| { | ||||
|    di->visible_ = visible; | ||||
|    if (di->visible_ != visible) | ||||
|    { | ||||
|       di->visible_ = visible; | ||||
|       p->dirtyLines_.insert(di); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void GeoLines::SetLineHoverCallback(const std::shared_ptr<GeoLineDrawItem>& di, | ||||
|                                     const HoverCallback& callback) | ||||
| { | ||||
|    if (di->hoverCallback_ != nullptr || callback != nullptr) | ||||
|    { | ||||
|       di->hoverCallback_ = callback; | ||||
|       p->dirtyLines_.insert(di); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void GeoLines::SetLineHoverText(const std::shared_ptr<GeoLineDrawItem>& 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<GeoLineDrawItem>& di, | ||||
|                                 std::chrono::system_clock::time_point startTime) | ||||
| { | ||||
|    if (di->startTime_ != startTime) | ||||
|    { | ||||
|       di->startTime_ = | ||||
|          std::chrono::time_point_cast<std::chrono::seconds, | ||||
|                                       std::chrono::system_clock>(startTime); | ||||
|       p->dirtyLines_.insert(di); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void GeoLines::SetLineEndTime(const std::shared_ptr<GeoLineDrawItem>& di, | ||||
|                               std::chrono::system_clock::time_point   endTime) | ||||
| { | ||||
|    if (di->endTime_ != endTime) | ||||
|    { | ||||
|       di->endTime_ = | ||||
|          std::chrono::time_point_cast<std::chrono::seconds, | ||||
|                                       std::chrono::system_clock>(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<const GeoLineDrawItem>& di) | ||||
| void GeoLines::Impl::UpdateSingleBuffer( | ||||
|    const std::shared_ptr<GeoLineDrawItem>& di, | ||||
|    std::size_t                             lineIndex, | ||||
|    std::vector<float>&                     lineBuffer, | ||||
|    std::vector<GLint>&                     integerBuffer, | ||||
|    std::vector<LineHoverEntry>&            hoverLines) | ||||
| { | ||||
|    // Threshold value
 | ||||
|    units::length::nautical_miles<double> 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<GLint>(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<double> 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<types::EventHandler>& /* eventHandler */) | ||||
|    std::shared_ptr<types::EventHandler>& 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<GeoLineDrawItem>& di, | ||||
|    const std::function<void(QEvent*)>&     eventHandler) | ||||
| { | ||||
|    di->event_ = eventHandler; | ||||
| } | ||||
| 
 | ||||
| } // namespace draw
 | ||||
| } // namespace gl
 | ||||
| } // namespace qt
 | ||||
|  |  | |||
|  | @ -19,6 +19,10 @@ struct GeoLineDrawItem; | |||
| class GeoLines : public DrawItem | ||||
| { | ||||
| public: | ||||
|    typedef std::function<void(std::shared_ptr<GeoLineDrawItem>&, | ||||
|                               const QPointF&)> | ||||
|       HoverCallback; | ||||
| 
 | ||||
|    explicit GeoLines(std::shared_ptr<GlContext> 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<GeoLineDrawItem>& di, | ||||
|                                float latitude1, | ||||
|                                float longitude1, | ||||
|                                float latitude2, | ||||
|                                float longitude2); | ||||
|    void SetLineLocation(const std::shared_ptr<GeoLineDrawItem>& 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<GeoLineDrawItem>& di, | ||||
|                                boost::gil::rgba8_pixel_t               color); | ||||
|    void SetLineModulate(const std::shared_ptr<GeoLineDrawItem>& 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<GeoLineDrawItem>& di, | ||||
|                                boost::gil::rgba32f_pixel_t modulate); | ||||
|    void SetLineModulate(const std::shared_ptr<GeoLineDrawItem>& 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<GeoLineDrawItem>& di, | ||||
|                             float                                   width); | ||||
|    void SetLineWidth(const std::shared_ptr<GeoLineDrawItem>& di, float width); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the visibility of the geo line. | ||||
|     * | ||||
|     * @param [in] visible | ||||
|     */ | ||||
|    static void SetLineVisible(const std::shared_ptr<GeoLineDrawItem>& di, | ||||
|                               bool                                    visible); | ||||
|    void SetLineVisible(const std::shared_ptr<GeoLineDrawItem>& 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<GeoLineDrawItem>& 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<GeoLineDrawItem>& di, | ||||
|                                 const std::string&                      text); | ||||
|    void SetLineHoverText(const std::shared_ptr<GeoLineDrawItem>& 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<GeoLineDrawItem>& 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<GeoLineDrawItem>& 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<GeoLineDrawItem>& di, | ||||
|                         const std::function<void(QEvent*)>&     eventHandler); | ||||
| 
 | ||||
| private: | ||||
|    class Impl; | ||||
|    std::unique_ptr<Impl> p; | ||||
|  |  | |||
|  | @ -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_; | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -1,17 +1,23 @@ | |||
| #include <scwx/qt/map/alert_layer.hpp> | ||||
| #include <scwx/qt/gl/draw/geo_lines.hpp> | ||||
| #include <scwx/qt/manager/text_event_manager.hpp> | ||||
| #include <scwx/qt/manager/timeline_manager.hpp> | ||||
| #include <scwx/qt/settings/palette_settings.hpp> | ||||
| #include <scwx/qt/types/layer_types.hpp> | ||||
| #include <scwx/qt/util/color.hpp> | ||||
| #include <scwx/qt/util/tooltip.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| #include <scwx/util/threads.hpp> | ||||
| 
 | ||||
| #include <chrono> | ||||
| #include <shared_mutex> | ||||
| #include <mutex> | ||||
| #include <unordered_map> | ||||
| #include <unordered_set> | ||||
| 
 | ||||
| #include <boost/asio/steady_timer.hpp> | ||||
| #include <boost/algorithm/string/join.hpp> | ||||
| #include <boost/asio/system_timer.hpp> | ||||
| #include <boost/asio/thread_pool.hpp> | ||||
| #include <boost/container/stable_vector.hpp> | ||||
| #include <boost/container_hash/hash.hpp> | ||||
| #include <QEvent> | ||||
| 
 | ||||
| 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<std::string> | ||||
| AddAlertLayer(std::shared_ptr<QMapLibre::Map> 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<QMapLibre::Feature> {})}}; | ||||
| static const std::vector<awips::Phenomenon> 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<class Key> | ||||
| struct AlertTypeHash; | ||||
|  | @ -58,203 +42,315 @@ struct AlertTypeHash<std::pair<awips::Phenomenon, bool>> | |||
| 
 | ||||
| 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<const awips::Segment>            segment_; | ||||
|       types::TextEventKey                              key_; | ||||
|       std::shared_ptr<const awips::TextProductMessage> message_; | ||||
|       std::chrono::system_clock::time_point            segmentBegin_; | ||||
|       std::chrono::system_clock::time_point            segmentEnd_; | ||||
| 
 | ||||
|       SegmentRecord( | ||||
|          const std::shared_ptr<const awips::Segment>&            segment, | ||||
|          const types::TextEventKey&                              key, | ||||
|          const std::shared_ptr<const awips::TextProductMessage>& 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<AlertLayerHandler> Instance(); | ||||
|    std::unordered_map< | ||||
|       std::pair<awips::Phenomenon, bool>, | ||||
|       boost::container::stable_vector<std::shared_ptr<SegmentRecord>>, | ||||
|       AlertTypeHash<std::pair<awips::Phenomenon, bool>>> | ||||
|       segmentsByType_ {}; | ||||
| 
 | ||||
|    std::list<QMapLibre::Feature>* FeatureList(awips::Phenomenon phenomenon, | ||||
|                                               bool              alertActive); | ||||
|    std::unordered_map< | ||||
|       types::TextEventKey, | ||||
|       boost::container::stable_vector<std::shared_ptr<SegmentRecord>>, | ||||
|       types::TextEventHash<types::TextEventKey>> | ||||
|       segmentsByKey_ {}; | ||||
| 
 | ||||
|    void HandleAlert(const types::TextEventKey& key, size_t messageIndex); | ||||
|    void UpdateAlerts(); | ||||
| 
 | ||||
|    std::shared_ptr<manager::TextEventManager> textEventManager_; | ||||
|    static AlertLayerHandler& Instance(); | ||||
| 
 | ||||
|    boost::asio::steady_timer alertUpdateTimer_; | ||||
|    std::unordered_map<std::pair<awips::Phenomenon, bool>, | ||||
|                       QVariantMap, | ||||
|                       AlertTypeHash<std::pair<awips::Phenomenon, bool>>> | ||||
|       alertSourceMap_; | ||||
|    std::unordered_multimap<types::TextEventKey, | ||||
|                            std::tuple<awips::Phenomenon, | ||||
|                                       bool, | ||||
|                                       std::list<QMapLibre::Feature>::iterator, | ||||
|                                       std::chrono::system_clock::time_point>, | ||||
|                            types::TextEventHash<types::TextEventKey>> | ||||
|                      featureMap_; | ||||
|    std::shared_mutex alertMutex_; | ||||
|    std::shared_ptr<manager::TextEventManager> textEventManager_ { | ||||
|       manager::TextEventManager::Instance()}; | ||||
| 
 | ||||
|    std::shared_mutex alertMutex_ {}; | ||||
| 
 | ||||
| signals: | ||||
|    void AlertAdded(const std::shared_ptr<SegmentRecord>& segmentRecord, | ||||
|                    awips::Phenomenon                     phenomenon); | ||||
|    void AlertUpdated(const std::shared_ptr<SegmentRecord>& segmentRecord); | ||||
|    void AlertsUpdated(awips::Phenomenon phenomenon, bool alertActive); | ||||
| }; | ||||
| 
 | ||||
| class AlertLayerImpl : public QObject | ||||
| class AlertLayer::Impl | ||||
| { | ||||
|    Q_OBJECT | ||||
| public: | ||||
|    explicit AlertLayerImpl(std::shared_ptr<MapContext> context) : | ||||
|        context_ {context}, alertLayerHandler_ {AlertLayerHandler::Instance()} | ||||
|    explicit Impl(AlertLayer*                 self, | ||||
|                  std::shared_ptr<MapContext> context, | ||||
|                  awips::Phenomenon           phenomenon) : | ||||
|        self_ {self}, | ||||
|        phenomenon_ {phenomenon}, | ||||
|        geoLines_ {{false, std::make_shared<gl::draw::GeoLines>(context)}, | ||||
|                   {true, std::make_shared<gl::draw::GeoLines>(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<MapContext>        context_; | ||||
|    std::shared_ptr<AlertLayerHandler> alertLayerHandler_; | ||||
|       receiver_ = nullptr; | ||||
| 
 | ||||
|       std::unique_lock lock(linesMutex_); | ||||
|    }; | ||||
| 
 | ||||
|    void AddAlert( | ||||
|       const std::shared_ptr<AlertLayerHandler::SegmentRecord>& segmentRecord); | ||||
|    void UpdateAlert( | ||||
|       const std::shared_ptr<AlertLayerHandler::SegmentRecord>& segmentRecord); | ||||
|    void ConnectAlertHandlerSignals(); | ||||
|    void ConnectSignals(); | ||||
|    void HandleGeoLinesEvent(std::shared_ptr<gl::draw::GeoLineDrawItem>& di, | ||||
|                             QEvent*                                     ev); | ||||
|    void HandleGeoLinesHover(std::shared_ptr<gl::draw::GeoLineDrawItem>& di, | ||||
|                             const QPointF& mouseGlobalPos); | ||||
|    void ScheduleRefresh(); | ||||
| 
 | ||||
|    void AddLine(std::shared_ptr<gl::draw::GeoLines>&        geoLines, | ||||
|                 std::shared_ptr<gl::draw::GeoLineDrawItem>& 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<gl::draw::GeoLines>&   geoLines, | ||||
|                  const std::vector<common::Coordinate>& 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<gl::draw::GeoLineDrawItem>>& 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<QObject> receiver_ {std::make_unique<QObject>()}; | ||||
| 
 | ||||
|    std::unordered_map<bool, std::shared_ptr<gl::draw::GeoLines>> geoLines_; | ||||
| 
 | ||||
|    std::unordered_map<std::shared_ptr<const AlertLayerHandler::SegmentRecord>, | ||||
|                       boost::container::stable_vector< | ||||
|                          std::shared_ptr<gl::draw::GeoLineDrawItem>>> | ||||
|       linesBySegment_ {}; | ||||
|    std::unordered_map<std::shared_ptr<const gl::draw::GeoLineDrawItem>, | ||||
|                       std::shared_ptr<const AlertLayerHandler::SegmentRecord>> | ||||
|               segmentsByLine_; | ||||
|    std::mutex linesMutex_ {}; | ||||
| 
 | ||||
|    std::unordered_map<bool, boost::gil::rgba32f_pixel_t> lineColor_; | ||||
| 
 | ||||
|    std::chrono::system_clock::time_point selectedTime_ {}; | ||||
| 
 | ||||
|    std::shared_ptr<const gl::draw::GeoLineDrawItem> lastHoverDi_ {nullptr}; | ||||
|    std::string                                      tooltip_ {}; | ||||
| }; | ||||
| 
 | ||||
| AlertLayer::AlertLayer(std::shared_ptr<MapContext> context) : | ||||
|     p(std::make_unique<AlertLayerImpl>(context)) | ||||
| AlertLayer::AlertLayer(std::shared_ptr<MapContext> context, | ||||
|                        awips::Phenomenon           phenomenon) : | ||||
|     DrawLayer(context), p(std::make_unique<Impl>(this, context, phenomenon)) | ||||
| { | ||||
|    for (auto alertActive : {false, true}) | ||||
|    { | ||||
|       auto& geoLines = p->geoLines_.at(alertActive); | ||||
| 
 | ||||
|       AddDrawItem(geoLines); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| AlertLayer::~AlertLayer() = default; | ||||
| 
 | ||||
| std::vector<std::string> AlertLayer::AddLayers(awips::Phenomenon  phenomenon, | ||||
|                                                const std::string& before) | ||||
| void AlertLayer::InitializeHandler() | ||||
| { | ||||
|    logger_->debug("AddLayers(): {}", awips::GetPhenomenonCode(phenomenon)); | ||||
|    static bool ftt = true; | ||||
| 
 | ||||
|    std::vector<std::string> 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<QMapLibre::Feature>* | ||||
| AlertLayerHandler::FeatureList(awips::Phenomenon phenomenon, bool alertActive) | ||||
| void AlertLayer::Initialize() | ||||
| { | ||||
|    std::list<QMapLibre::Feature>* 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<std::list<QMapLibre::Feature>*>( | ||||
|          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<std::pair<awips::Phenomenon, bool>, | ||||
|                       AlertTypeHash<std::pair<awips::Phenomenon, bool>>> | ||||
|       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> segmentRecord = | ||||
|          std::make_shared<SegmentRecord>(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<std::pair<awips::Phenomenon, bool>, | ||||
|                       AlertTypeHash<std::pair<awips::Phenomenon, bool>>> | ||||
|       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<AlertLayerHandler::SegmentRecord>& 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<AlertLayerHandler::SegmentRecord>& 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::minutes>( | ||||
|          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<AlertLayerHandler::SegmentRecord>& 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<gl::draw::GeoLineDrawItem>> {}); | ||||
| 
 | ||||
|    // 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<AlertLayerHandler::SegmentRecord>& 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<gl::draw::GeoLines>&   geoLines, | ||||
|    const std::vector<common::Coordinate>& 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<gl::draw::GeoLineDrawItem>>& | ||||
|       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<gl::draw::GeoLines>& geoLines, | ||||
|                                std::shared_ptr<gl::draw::GeoLineDrawItem>& 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<gl::draw::GeoLineDrawItem>& 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> AlertLayerHandler::Instance() | ||||
| void AlertLayer::Impl::HandleGeoLinesHover( | ||||
|    std::shared_ptr<gl::draw::GeoLineDrawItem>& di, | ||||
|    const QPointF&                              mouseGlobalPos) | ||||
| { | ||||
|    static std::weak_ptr<AlertLayerHandler> alertLayerHandlerReference {}; | ||||
|    static std::mutex                       instanceMutex {}; | ||||
| 
 | ||||
|    std::unique_lock lock(instanceMutex); | ||||
| 
 | ||||
|    std::shared_ptr<AlertLayerHandler> alertLayerHandler = | ||||
|       alertLayerHandlerReference.lock(); | ||||
| 
 | ||||
|    if (alertLayerHandler == nullptr) | ||||
|    if (di != lastHoverDi_) | ||||
|    { | ||||
|       alertLayerHandler          = std::make_shared<AlertLayerHandler>(); | ||||
|       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<std::string> | ||||
| AddAlertLayer(std::shared_ptr<QMapLibre::Map> 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<QMapLibre::CoordinatesCollection> { | ||||
|          std::initializer_list<QMapLibre::Coordinates> {{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<std::pair<awips::Phenomenon, bool>>::operator()( | ||||
|  |  | |||
|  | @ -1,7 +1,8 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <scwx/awips/phenomenon.hpp> | ||||
| #include <scwx/qt/map/map_context.hpp> | ||||
| #include <scwx/qt/map/draw_layer.hpp> | ||||
| #include <scwx/qt/types/text_event_key.hpp> | ||||
| 
 | ||||
| #include <memory> | ||||
| #include <string> | ||||
|  | @ -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<MapContext> context); | ||||
|    explicit AlertLayer(std::shared_ptr<MapContext> context, | ||||
|                        scwx::awips::Phenomenon     phenomenon); | ||||
|    ~AlertLayer(); | ||||
| 
 | ||||
|    std::vector<std::string> 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<AlertLayerImpl> p; | ||||
|    class Impl; | ||||
|    std::unique_ptr<Impl> p; | ||||
| }; | ||||
| 
 | ||||
| } // namespace map
 | ||||
|  |  | |||
|  | @ -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_); | ||||
| 
 | ||||
|  |  | |||
|  | @ -79,7 +79,6 @@ public: | |||
|        imGuiRendererInitialized_ {false}, | ||||
|        radarProductManager_ {nullptr}, | ||||
|        radarProductLayer_ {nullptr}, | ||||
|        alertLayer_ {std::make_shared<AlertLayer>(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<manager::RadarProductManager> radarProductManager_; | ||||
| 
 | ||||
|    std::shared_ptr<RadarProductLayer>   radarProductLayer_; | ||||
|    std::shared_ptr<AlertLayer>          alertLayer_; | ||||
|    std::shared_ptr<OverlayLayer>        overlayLayer_; | ||||
|    std::shared_ptr<OverlayProductLayer> overlayProductLayer_ {nullptr}; | ||||
|    std::shared_ptr<PlacefileLayer>      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<awips::Phenomenon>(description), before); | ||||
|       layerList_.insert(layerList_.end(), newLayers.cbegin(), newLayers.cend()); | ||||
|       auto phenomenon = std::get<awips::Phenomenon>(description); | ||||
| 
 | ||||
|       std::shared_ptr<AlertLayer> alertLayer = | ||||
|          std::make_shared<AlertLayer>(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) | ||||
|    { | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ | |||
| #include <scwx/qt/config/radar_site.hpp> | ||||
| #include <scwx/qt/types/map_types.hpp> | ||||
| #include <scwx/qt/types/radar_product_record.hpp> | ||||
| #include <scwx/qt/types/text_event_key.hpp> | ||||
| 
 | ||||
| #include <chrono> | ||||
| #include <memory> | ||||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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<manager::PlacefileManager> placefileManager = | ||||
|       manager::PlacefileManager::Instance(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <scwx/qt/types/text_event_key.hpp> | ||||
| 
 | ||||
| #include <QDockWidget> | ||||
| 
 | ||||
| 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; | ||||
|  |  | |||
|  | @ -29,6 +29,15 @@ boost::gil::rgba8_pixel_t ToRgba8PixelT(const std::string& argbString) | |||
|                                      static_cast<uint8_t>(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
 | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ | |||
| #include <scwx/awips/ugc.hpp> | ||||
| #include <scwx/awips/wmo_header.hpp> | ||||
| 
 | ||||
| #include <chrono> | ||||
| #include <cstdint> | ||||
| #include <memory> | ||||
| #include <string> | ||||
|  | @ -57,6 +58,7 @@ struct SegmentHeader | |||
| 
 | ||||
| struct Segment | ||||
| { | ||||
|    std::shared_ptr<WmoHeader>             wmoHeader_ {}; | ||||
|    std::optional<SegmentHeader>           header_ {}; | ||||
|    std::vector<std::string>               productContent_ {}; | ||||
|    std::optional<CodedLocation>           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; | ||||
|  |  | |||
|  | @ -104,16 +104,14 @@ std::shared_ptr<const Segment> TextProductMessage::segment(size_t s) const | |||
|    return p->segments_[s]; | ||||
| } | ||||
| 
 | ||||
| std::chrono::system_clock::time_point | ||||
| TextProductMessage::segment_event_begin(std::size_t s) const | ||||
| std::chrono::system_clock::time_point 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<days>(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> segment = std::make_shared<Segment>(); | ||||
|       segment->wmoHeader_              = p->wmoHeader_; | ||||
| 
 | ||||
|       if (i == 0) | ||||
|       { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Dan Paulat
						Dan Paulat