mirror of
				https://github.com/ciphervance/supercell-wx.git
				synced 2025-10-31 03:50:05 +00:00 
			
		
		
		
	Merge pull request #146 from dpaulat/feature/storm-tracks
Storm Tracking Information
This commit is contained in:
		
						commit
						0a1754db84
					
				
					 53 changed files with 3858 additions and 720 deletions
				
			
		|  | @ -61,8 +61,9 @@ set(SRC_GL source/scwx/qt/gl/gl_context.cpp | |||
|            source/scwx/qt/gl/shader_program.cpp) | ||||
| set(HDR_GL_DRAW source/scwx/qt/gl/draw/draw_item.hpp | ||||
|                 source/scwx/qt/gl/draw/geo_icons.hpp | ||||
|                 source/scwx/qt/gl/draw/geo_line.hpp | ||||
|                 source/scwx/qt/gl/draw/geo_lines.hpp | ||||
|                 source/scwx/qt/gl/draw/icons.hpp | ||||
|                 source/scwx/qt/gl/draw/linked_vectors.hpp | ||||
|                 source/scwx/qt/gl/draw/placefile_icons.hpp | ||||
|                 source/scwx/qt/gl/draw/placefile_images.hpp | ||||
|                 source/scwx/qt/gl/draw/placefile_lines.hpp | ||||
|  | @ -72,8 +73,9 @@ set(HDR_GL_DRAW source/scwx/qt/gl/draw/draw_item.hpp | |||
|                 source/scwx/qt/gl/draw/rectangle.hpp) | ||||
| set(SRC_GL_DRAW source/scwx/qt/gl/draw/draw_item.cpp | ||||
|                 source/scwx/qt/gl/draw/geo_icons.cpp | ||||
|                 source/scwx/qt/gl/draw/geo_line.cpp | ||||
|                 source/scwx/qt/gl/draw/geo_lines.cpp | ||||
|                 source/scwx/qt/gl/draw/icons.cpp | ||||
|                 source/scwx/qt/gl/draw/linked_vectors.cpp | ||||
|                 source/scwx/qt/gl/draw/placefile_icons.cpp | ||||
|                 source/scwx/qt/gl/draw/placefile_images.cpp | ||||
|                 source/scwx/qt/gl/draw/placefile_lines.cpp | ||||
|  | @ -115,6 +117,7 @@ set(HDR_MAP source/scwx/qt/map/alert_layer.hpp | |||
|             source/scwx/qt/map/map_settings.hpp | ||||
|             source/scwx/qt/map/map_widget.hpp | ||||
|             source/scwx/qt/map/overlay_layer.hpp | ||||
|             source/scwx/qt/map/overlay_product_layer.hpp | ||||
|             source/scwx/qt/map/placefile_layer.hpp | ||||
|             source/scwx/qt/map/radar_product_layer.hpp | ||||
|             source/scwx/qt/map/radar_range_layer.hpp | ||||
|  | @ -128,6 +131,7 @@ set(SRC_MAP source/scwx/qt/map/alert_layer.cpp | |||
|             source/scwx/qt/map/map_provider.cpp | ||||
|             source/scwx/qt/map/map_widget.cpp | ||||
|             source/scwx/qt/map/overlay_layer.cpp | ||||
|             source/scwx/qt/map/overlay_product_layer.cpp | ||||
|             source/scwx/qt/map/placefile_layer.cpp | ||||
|             source/scwx/qt/map/radar_product_layer.cpp | ||||
|             source/scwx/qt/map/radar_range_layer.cpp | ||||
|  | @ -154,6 +158,7 @@ set(HDR_SETTINGS source/scwx/qt/settings/audio_settings.hpp | |||
|                  source/scwx/qt/settings/general_settings.hpp | ||||
|                  source/scwx/qt/settings/map_settings.hpp | ||||
|                  source/scwx/qt/settings/palette_settings.hpp | ||||
|                  source/scwx/qt/settings/product_settings.hpp | ||||
|                  source/scwx/qt/settings/settings_category.hpp | ||||
|                  source/scwx/qt/settings/settings_container.hpp | ||||
|                  source/scwx/qt/settings/settings_definitions.hpp | ||||
|  | @ -167,6 +172,7 @@ set(SRC_SETTINGS source/scwx/qt/settings/audio_settings.cpp | |||
|                  source/scwx/qt/settings/general_settings.cpp | ||||
|                  source/scwx/qt/settings/map_settings.cpp | ||||
|                  source/scwx/qt/settings/palette_settings.cpp | ||||
|                  source/scwx/qt/settings/product_settings.cpp | ||||
|                  source/scwx/qt/settings/settings_category.cpp | ||||
|                  source/scwx/qt/settings/settings_container.cpp | ||||
|                  source/scwx/qt/settings/settings_interface.cpp | ||||
|  | @ -298,12 +304,14 @@ set(HDR_VIEW source/scwx/qt/view/level2_product_view.hpp | |||
|              source/scwx/qt/view/level3_product_view.hpp | ||||
|              source/scwx/qt/view/level3_radial_view.hpp | ||||
|              source/scwx/qt/view/level3_raster_view.hpp | ||||
|              source/scwx/qt/view/overlay_product_view.hpp | ||||
|              source/scwx/qt/view/radar_product_view.hpp | ||||
|              source/scwx/qt/view/radar_product_view_factory.hpp) | ||||
| set(SRC_VIEW source/scwx/qt/view/level2_product_view.cpp | ||||
|              source/scwx/qt/view/level3_product_view.cpp | ||||
|              source/scwx/qt/view/level3_radial_view.cpp | ||||
|              source/scwx/qt/view/level3_raster_view.cpp | ||||
|              source/scwx/qt/view/overlay_product_view.cpp | ||||
|              source/scwx/qt/view/radar_product_view.cpp | ||||
|              source/scwx/qt/view/radar_product_view_factory.cpp) | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,314 +0,0 @@ | |||
| #include <scwx/qt/gl/draw/geo_line.hpp> | ||||
| #include <scwx/qt/util/geographic_lib.hpp> | ||||
| #include <scwx/qt/util/texture_atlas.hpp> | ||||
| #include <scwx/common/geographic.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| 
 | ||||
| #include <numbers> | ||||
| #include <optional> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace qt | ||||
| { | ||||
| namespace gl | ||||
| { | ||||
| namespace draw | ||||
| { | ||||
| 
 | ||||
| static const std::string logPrefix_ = "scwx::qt::gl::draw::geo_line"; | ||||
| static const auto        logger_    = scwx::util::Logger::Create(logPrefix_); | ||||
| 
 | ||||
| static constexpr size_t kNumRectangles        = 1; | ||||
| 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      = 11; | ||||
| static constexpr size_t kBufferLength = | ||||
|    kNumTriangles * kVerticesPerTriangle * kPointsPerVertex; | ||||
| 
 | ||||
| static const std::string kTextureName = "lines/default-1x7"; | ||||
| 
 | ||||
| class GeoLine::Impl | ||||
| { | ||||
| public: | ||||
|    explicit Impl(std::shared_ptr<GlContext> context) : | ||||
|        context_ {context}, | ||||
|        geodesic_ {util::GeographicLib::DefaultGeodesic()}, | ||||
|        dirty_ {false}, | ||||
|        visible_ {true}, | ||||
|        points_ {}, | ||||
|        angle_ {}, | ||||
|        width_ {7.0f}, | ||||
|        modulateColor_ {std::nullopt}, | ||||
|        shaderProgram_ {nullptr}, | ||||
|        uMVPMatrixLocation_(GL_INVALID_INDEX), | ||||
|        uMapMatrixLocation_(GL_INVALID_INDEX), | ||||
|        uMapScreenCoordLocation_(GL_INVALID_INDEX), | ||||
|        texture_ {}, | ||||
|        vao_ {GL_INVALID_INDEX}, | ||||
|        vbo_ {GL_INVALID_INDEX} | ||||
|    { | ||||
|    } | ||||
| 
 | ||||
|    ~Impl() {} | ||||
| 
 | ||||
|    std::shared_ptr<GlContext> context_; | ||||
| 
 | ||||
|    const GeographicLib::Geodesic& geodesic_; | ||||
| 
 | ||||
|    bool dirty_; | ||||
| 
 | ||||
|    bool                              visible_; | ||||
|    std::array<common::Coordinate, 2> points_; | ||||
|    float                             angle_; | ||||
|    float                             width_; | ||||
| 
 | ||||
|    std::optional<boost::gil::rgba8_pixel_t> modulateColor_; | ||||
| 
 | ||||
|    std::shared_ptr<ShaderProgram> shaderProgram_; | ||||
|    GLint                          uMVPMatrixLocation_; | ||||
|    GLint                          uMapMatrixLocation_; | ||||
|    GLint                          uMapScreenCoordLocation_; | ||||
| 
 | ||||
|    util::TextureAttributes texture_; | ||||
| 
 | ||||
|    GLuint vao_; | ||||
|    GLuint vbo_; | ||||
| 
 | ||||
|    void Update(); | ||||
| }; | ||||
| 
 | ||||
| GeoLine::GeoLine(std::shared_ptr<GlContext> context) : | ||||
|     DrawItem(context->gl()), p(std::make_unique<Impl>(context)) | ||||
| { | ||||
| } | ||||
| GeoLine::~GeoLine() = default; | ||||
| 
 | ||||
| GeoLine::GeoLine(GeoLine&&) noexcept            = default; | ||||
| GeoLine& GeoLine::operator=(GeoLine&&) noexcept = default; | ||||
| 
 | ||||
| void GeoLine::Initialize() | ||||
| { | ||||
|    gl::OpenGLFunctions& gl = p->context_->gl(); | ||||
| 
 | ||||
|    p->shaderProgram_ = p->context_->GetShaderProgram( | ||||
|       ":/gl/geo_line.vert", ":/gl/texture2d_array.frag"); | ||||
| 
 | ||||
|    p->uMVPMatrixLocation_ = | ||||
|       gl.glGetUniformLocation(p->shaderProgram_->id(), "uMVPMatrix"); | ||||
|    if (p->uMVPMatrixLocation_ == -1) | ||||
|    { | ||||
|       logger_->warn("Could not find uMVPMatrix"); | ||||
|    } | ||||
| 
 | ||||
|    p->uMapMatrixLocation_ = | ||||
|       gl.glGetUniformLocation(p->shaderProgram_->id(), "uMapMatrix"); | ||||
|    if (p->uMapMatrixLocation_ == -1) | ||||
|    { | ||||
|       logger_->warn("Could not find uMapMatrix"); | ||||
|    } | ||||
| 
 | ||||
|    p->uMapScreenCoordLocation_ = | ||||
|       gl.glGetUniformLocation(p->shaderProgram_->id(), "uMapScreenCoord"); | ||||
|    if (p->uMapScreenCoordLocation_ == -1) | ||||
|    { | ||||
|       logger_->warn("Could not find uMapScreenCoord"); | ||||
|    } | ||||
| 
 | ||||
|    p->texture_ = | ||||
|       util::TextureAtlas::Instance().GetTextureAttributes(kTextureName); | ||||
| 
 | ||||
|    gl.glGenVertexArrays(1, &p->vao_); | ||||
|    gl.glGenBuffers(1, &p->vbo_); | ||||
| 
 | ||||
|    gl.glBindVertexArray(p->vao_); | ||||
|    gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_); | ||||
|    gl.glBufferData( | ||||
|       GL_ARRAY_BUFFER, sizeof(float) * kBufferLength, nullptr, GL_DYNAMIC_DRAW); | ||||
| 
 | ||||
|    // aLatLong
 | ||||
|    gl.glVertexAttribPointer(0, | ||||
|                             2, | ||||
|                             GL_FLOAT, | ||||
|                             GL_FALSE, | ||||
|                             kPointsPerVertex * sizeof(float), | ||||
|                             static_cast<void*>(0)); | ||||
|    gl.glEnableVertexAttribArray(0); | ||||
| 
 | ||||
|    // aXYOffset
 | ||||
|    gl.glVertexAttribPointer(1, | ||||
|                             2, | ||||
|                             GL_FLOAT, | ||||
|                             GL_FALSE, | ||||
|                             kPointsPerVertex * sizeof(float), | ||||
|                             reinterpret_cast<void*>(2 * sizeof(float))); | ||||
|    gl.glEnableVertexAttribArray(1); | ||||
| 
 | ||||
|    // aTexCoord
 | ||||
|    gl.glVertexAttribPointer(2, | ||||
|                             3, | ||||
|                             GL_FLOAT, | ||||
|                             GL_FALSE, | ||||
|                             kPointsPerVertex * sizeof(float), | ||||
|                             reinterpret_cast<void*>(4 * sizeof(float))); | ||||
|    gl.glEnableVertexAttribArray(2); | ||||
| 
 | ||||
|    // aModulate
 | ||||
|    gl.glVertexAttribPointer(3, | ||||
|                             4, | ||||
|                             GL_FLOAT, | ||||
|                             GL_FALSE, | ||||
|                             kPointsPerVertex * sizeof(float), | ||||
|                             reinterpret_cast<void*>(7 * sizeof(float))); | ||||
|    gl.glEnableVertexAttribArray(3); | ||||
| 
 | ||||
|    p->dirty_ = true; | ||||
| } | ||||
| 
 | ||||
| void GeoLine::Render(const QMapLibreGL::CustomLayerRenderParameters& params) | ||||
| { | ||||
|    if (p->visible_) | ||||
|    { | ||||
|       gl::OpenGLFunctions& gl = p->context_->gl(); | ||||
| 
 | ||||
|       gl.glBindVertexArray(p->vao_); | ||||
|       gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_); | ||||
| 
 | ||||
|       p->Update(); | ||||
|       p->shaderProgram_->Use(); | ||||
|       UseDefaultProjection(params, p->uMVPMatrixLocation_); | ||||
|       UseMapProjection( | ||||
|          params, p->uMapMatrixLocation_, p->uMapScreenCoordLocation_); | ||||
| 
 | ||||
|       // Draw line
 | ||||
|       gl.glDrawArrays(GL_TRIANGLES, 0, 6); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void GeoLine::Deinitialize() | ||||
| { | ||||
|    gl::OpenGLFunctions& gl = p->context_->gl(); | ||||
| 
 | ||||
|    gl.glDeleteVertexArrays(1, &p->vao_); | ||||
|    gl.glDeleteBuffers(1, &p->vbo_); | ||||
| } | ||||
| 
 | ||||
| void GeoLine::SetPoints(float latitude1, | ||||
|                         float longitude1, | ||||
|                         float latitude2, | ||||
|                         float longitude2) | ||||
| { | ||||
|    if (p->points_[0].latitude_ != latitude1 || | ||||
|        p->points_[0].longitude_ != longitude1 || | ||||
|        p->points_[1].latitude_ != latitude2 || | ||||
|        p->points_[1].longitude_ != longitude2) | ||||
|    { | ||||
|       p->points_[0] = {latitude1, longitude1}; | ||||
|       p->points_[1] = {latitude2, longitude2}; | ||||
| 
 | ||||
|       double azi1; // Azimuth at point 1 (degrees)
 | ||||
|       double azi2; // (Forward) azimuth at point 2 (degrees)
 | ||||
|       p->geodesic_.Inverse(p->points_[0].latitude_, | ||||
|                            p->points_[0].longitude_, | ||||
|                            p->points_[1].latitude_, | ||||
|                            p->points_[1].longitude_, | ||||
|                            azi1, | ||||
|                            azi2); | ||||
|       p->angle_ = -azi1 * std::numbers::pi / 180.0; | ||||
| 
 | ||||
|       p->dirty_ = true; | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void GeoLine::SetModulateColor(boost::gil::rgba8_pixel_t color) | ||||
| { | ||||
|    if (p->modulateColor_ != color) | ||||
|    { | ||||
|       p->modulateColor_ = color; | ||||
|       p->dirty_         = true; | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void GeoLine::SetWidth(float width) | ||||
| { | ||||
|    if (p->width_ != width) | ||||
|    { | ||||
|       p->width_ = width; | ||||
|       p->dirty_ = true; | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void GeoLine::SetVisible(bool visible) | ||||
| { | ||||
|    p->visible_ = visible; | ||||
| } | ||||
| 
 | ||||
| void GeoLine::Impl::Update() | ||||
| { | ||||
|    if (dirty_) | ||||
|    { | ||||
|       gl::OpenGLFunctions& gl = context_->gl(); | ||||
| 
 | ||||
|       texture_ = | ||||
|          util::TextureAtlas::Instance().GetTextureAttributes(kTextureName); | ||||
| 
 | ||||
|       // Latitude and longitude coordinates in degrees
 | ||||
|       const float lx = points_[0].latitude_; | ||||
|       const float rx = points_[1].latitude_; | ||||
|       const float by = points_[0].longitude_; | ||||
|       const float ty = points_[1].longitude_; | ||||
| 
 | ||||
|       // Offset x/y in pixels
 | ||||
|       const float ox = width_ * 0.5f * cosf(angle_); | ||||
|       const float oy = width_ * 0.5f * sinf(angle_); | ||||
| 
 | ||||
|       // Texture coordinates
 | ||||
|       static constexpr float r = 0.0f; | ||||
| 
 | ||||
|       const float ls = texture_.sLeft_; | ||||
|       const float rs = texture_.sRight_; | ||||
|       const float tt = texture_.tTop_; | ||||
|       const float bt = texture_.tBottom_; | ||||
| 
 | ||||
|       float mc0 = 1.0f; | ||||
|       float mc1 = 1.0f; | ||||
|       float mc2 = 1.0f; | ||||
|       float mc3 = 1.0f; | ||||
| 
 | ||||
|       if (modulateColor_.has_value()) | ||||
|       { | ||||
|          boost::gil::rgba8_pixel_t& mc = modulateColor_.value(); | ||||
| 
 | ||||
|          mc0 = mc[0] / 255.0f; | ||||
|          mc1 = mc[1] / 255.0f; | ||||
|          mc2 = mc[2] / 255.0f; | ||||
|          mc3 = mc[3] / 255.0f; | ||||
|       } | ||||
| 
 | ||||
|       const float buffer[kNumRectangles][kVerticesPerRectangle] | ||||
|                         [kPointsPerVertex] = //
 | ||||
|          {                                   //
 | ||||
|           // Line
 | ||||
|           { | ||||
|              {lx, by, -ox, -oy, ls, bt, r, mc0, mc1, mc2, mc3}, // BL
 | ||||
|              {lx, by, +ox, +oy, ls, tt, r, mc0, mc1, mc2, mc3}, // TL
 | ||||
|              {rx, ty, -ox, -oy, rs, bt, r, mc0, mc1, mc2, mc3}, // BR
 | ||||
|              {rx, ty, -ox, -oy, rs, bt, r, mc0, mc1, mc2, mc3}, // BR
 | ||||
|              {rx, ty, +ox, +oy, rs, tt, r, mc0, mc1, mc2, mc3}, // TR
 | ||||
|              {lx, by, +ox, +oy, ls, tt, r, mc0, mc1, mc2, mc3}  // TL
 | ||||
|           }}; | ||||
| 
 | ||||
|       gl.glBufferData(GL_ARRAY_BUFFER, | ||||
|                       sizeof(float) * kBufferLength, | ||||
|                       buffer, | ||||
|                       GL_DYNAMIC_DRAW); | ||||
| 
 | ||||
|       dirty_ = false; | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| } // namespace draw
 | ||||
| } // namespace gl
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
|  | @ -1,77 +0,0 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <scwx/qt/gl/gl_context.hpp> | ||||
| #include <scwx/qt/gl/draw/draw_item.hpp> | ||||
| 
 | ||||
| #include <boost/gil.hpp> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace qt | ||||
| { | ||||
| namespace gl | ||||
| { | ||||
| namespace draw | ||||
| { | ||||
| 
 | ||||
| class GeoLine : public DrawItem | ||||
| { | ||||
| public: | ||||
|    explicit GeoLine(std::shared_ptr<GlContext> context); | ||||
|    ~GeoLine(); | ||||
| 
 | ||||
|    GeoLine(const GeoLine&)            = delete; | ||||
|    GeoLine& operator=(const GeoLine&) = delete; | ||||
| 
 | ||||
|    GeoLine(GeoLine&&) noexcept; | ||||
|    GeoLine& operator=(GeoLine&&) noexcept; | ||||
| 
 | ||||
|    void Initialize() override; | ||||
|    void Render(const QMapLibreGL::CustomLayerRenderParameters& params) override; | ||||
|    void Deinitialize() override; | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the geographic coordinate endpoints associated with the line. | ||||
|     * | ||||
|     * @param latitude1 Latitude of the first endpoint in degrees | ||||
|     * @param longitude1 Longitude of the first endpoint in degrees | ||||
|     * @param latitude2 Latitude of the second endpoint in degrees | ||||
|     * @param longitude2 Longitude of the second endpoint in degrees | ||||
|     */ | ||||
|    void SetPoints(float latitude1, | ||||
|                   float longitude1, | ||||
|                   float latitude2, | ||||
|                   float longitude2); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the modulate color of the line. If specified, the texture color will | ||||
|     * be multiplied by the modulate color to produce the result. | ||||
|     * | ||||
|     * @param color Modulate color (RGBA) | ||||
|     */ | ||||
|    void SetModulateColor(boost::gil::rgba8_pixel_t color); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the width of the line. | ||||
|     * | ||||
|     * @param width Width in pixels | ||||
|     */ | ||||
|    void SetWidth(float width); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the visibility of the line. | ||||
|     * | ||||
|     * @param visible | ||||
|     */ | ||||
|    void SetVisible(bool visible); | ||||
| 
 | ||||
| private: | ||||
|    class Impl; | ||||
| 
 | ||||
|    std::unique_ptr<Impl> p; | ||||
| }; | ||||
| 
 | ||||
| } // namespace draw
 | ||||
| } // namespace gl
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
							
								
								
									
										620
									
								
								scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										620
									
								
								scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,620 @@ | |||
| #include <scwx/qt/gl/draw/geo_lines.hpp> | ||||
| #include <scwx/qt/util/geographic_lib.hpp> | ||||
| #include <scwx/qt/util/maplibre.hpp> | ||||
| #include <scwx/qt/util/tooltip.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| 
 | ||||
| #include <execution> | ||||
| 
 | ||||
| #include <units/angle.h> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace qt | ||||
| { | ||||
| namespace gl | ||||
| { | ||||
| namespace draw | ||||
| { | ||||
| 
 | ||||
| static const std::string logPrefix_ = "scwx::qt::gl::draw::geo_lines"; | ||||
| static const auto        logger_    = scwx::util::Logger::Create(logPrefix_); | ||||
| 
 | ||||
| static constexpr size_t kNumRectangles        = 1; | ||||
| 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 = | ||||
|    kNumTriangles * kVerticesPerTriangle * kPointsPerVertex; | ||||
| 
 | ||||
| // Threshold, start time, end time
 | ||||
| static constexpr std::size_t kIntegersPerVertex_ = 3; | ||||
| 
 | ||||
| struct GeoLineDrawItem | ||||
| { | ||||
|    bool                                        visible_ {true}; | ||||
|    units::length::nautical_miles<double>       threshold_ {}; | ||||
|    std::chrono::sys_time<std::chrono::seconds> startTime_ {}; | ||||
|    std::chrono::sys_time<std::chrono::seconds> endTime_ {}; | ||||
| 
 | ||||
|    boost::gil::rgba32f_pixel_t  modulate_ {1.0f, 1.0f, 1.0f, 1.0f}; | ||||
|    float                        latitude1_ {}; | ||||
|    float                        longitude1_ {}; | ||||
|    float                        latitude2_ {}; | ||||
|    float                        longitude2_ {}; | ||||
|    float                        width_ {5.0}; | ||||
|    units::angle::degrees<float> angle_ {}; | ||||
|    std::string                  hoverText_ {}; | ||||
| }; | ||||
| 
 | ||||
| class GeoLines::Impl | ||||
| { | ||||
| public: | ||||
|    struct LineHoverEntry | ||||
|    { | ||||
|       std::shared_ptr<const GeoLineDrawItem> di_; | ||||
| 
 | ||||
|       glm::vec2 p1_; | ||||
|       glm::vec2 p2_; | ||||
|       glm::vec2 otl_; | ||||
|       glm::vec2 otr_; | ||||
|       glm::vec2 obl_; | ||||
|       glm::vec2 obr_; | ||||
|    }; | ||||
| 
 | ||||
|    explicit Impl(std::shared_ptr<GlContext> context) : | ||||
|        context_ {context}, | ||||
|        shaderProgram_ {nullptr}, | ||||
|        uMVPMatrixLocation_(GL_INVALID_INDEX), | ||||
|        uMapMatrixLocation_(GL_INVALID_INDEX), | ||||
|        uMapScreenCoordLocation_(GL_INVALID_INDEX), | ||||
|        uMapDistanceLocation_(GL_INVALID_INDEX), | ||||
|        uSelectedTimeLocation_(GL_INVALID_INDEX), | ||||
|        vao_ {GL_INVALID_INDEX}, | ||||
|        vbo_ {GL_INVALID_INDEX} | ||||
|    { | ||||
|    } | ||||
| 
 | ||||
|    ~Impl() {} | ||||
| 
 | ||||
|    void BufferLine(const std::shared_ptr<const GeoLineDrawItem>& di); | ||||
|    void Update(); | ||||
|    void UpdateBuffers(); | ||||
| 
 | ||||
|    std::shared_ptr<GlContext> context_; | ||||
| 
 | ||||
|    bool visible_ {true}; | ||||
|    bool dirty_ {false}; | ||||
|    bool thresholded_ {false}; | ||||
| 
 | ||||
|    std::chrono::system_clock::time_point selectedTime_ {}; | ||||
| 
 | ||||
|    std::mutex lineMutex_ {}; | ||||
| 
 | ||||
|    std::vector<std::shared_ptr<GeoLineDrawItem>> currentLineList_ {}; | ||||
|    std::vector<std::shared_ptr<GeoLineDrawItem>> newLineList_ {}; | ||||
| 
 | ||||
|    std::vector<float> currentLinesBuffer_ {}; | ||||
|    std::vector<GLint> currentIntegerBuffer_ {}; | ||||
|    std::vector<float> newLinesBuffer_ {}; | ||||
|    std::vector<GLint> newIntegerBuffer_ {}; | ||||
| 
 | ||||
|    std::vector<LineHoverEntry> currentHoverLines_ {}; | ||||
|    std::vector<LineHoverEntry> newHoverLines_ {}; | ||||
| 
 | ||||
|    std::shared_ptr<ShaderProgram> shaderProgram_; | ||||
|    GLint                          uMVPMatrixLocation_; | ||||
|    GLint                          uMapMatrixLocation_; | ||||
|    GLint                          uMapScreenCoordLocation_; | ||||
|    GLint                          uMapDistanceLocation_; | ||||
|    GLint                          uSelectedTimeLocation_; | ||||
| 
 | ||||
|    GLuint                vao_; | ||||
|    std::array<GLuint, 2> vbo_; | ||||
| }; | ||||
| 
 | ||||
| GeoLines::GeoLines(std::shared_ptr<GlContext> context) : | ||||
|     DrawItem(context->gl()), p(std::make_unique<Impl>(context)) | ||||
| { | ||||
| } | ||||
| GeoLines::~GeoLines() = default; | ||||
| 
 | ||||
| GeoLines::GeoLines(GeoLines&&) noexcept            = default; | ||||
| GeoLines& GeoLines::operator=(GeoLines&&) noexcept = default; | ||||
| 
 | ||||
| void GeoLines::set_selected_time( | ||||
|    std::chrono::system_clock::time_point selectedTime) | ||||
| { | ||||
|    p->selectedTime_ = selectedTime; | ||||
| } | ||||
| 
 | ||||
| void GeoLines::set_thresholded(bool thresholded) | ||||
| { | ||||
|    p->thresholded_ = thresholded; | ||||
| } | ||||
| 
 | ||||
| void GeoLines::Initialize() | ||||
| { | ||||
|    gl::OpenGLFunctions& gl = p->context_->gl(); | ||||
| 
 | ||||
|    p->shaderProgram_ = p->context_->GetShaderProgram( | ||||
|       {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"}, | ||||
|        {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"}, | ||||
|        {GL_FRAGMENT_SHADER, ":/gl/color.frag"}}); | ||||
| 
 | ||||
|    p->uMVPMatrixLocation_ = p->shaderProgram_->GetUniformLocation("uMVPMatrix"); | ||||
|    p->uMapMatrixLocation_ = p->shaderProgram_->GetUniformLocation("uMapMatrix"); | ||||
|    p->uMapScreenCoordLocation_ = | ||||
|       p->shaderProgram_->GetUniformLocation("uMapScreenCoord"); | ||||
|    p->uMapDistanceLocation_ = | ||||
|       p->shaderProgram_->GetUniformLocation("uMapDistance"); | ||||
|    p->uSelectedTimeLocation_ = | ||||
|       p->shaderProgram_->GetUniformLocation("uSelectedTime"); | ||||
| 
 | ||||
|    gl.glGenVertexArrays(1, &p->vao_); | ||||
|    gl.glGenBuffers(static_cast<GLsizei>(p->vbo_.size()), p->vbo_.data()); | ||||
| 
 | ||||
|    gl.glBindVertexArray(p->vao_); | ||||
|    gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); | ||||
|    gl.glBufferData( | ||||
|       GL_ARRAY_BUFFER, sizeof(float) * kBufferLength, nullptr, GL_DYNAMIC_DRAW); | ||||
| 
 | ||||
|    // aLatLong
 | ||||
|    gl.glVertexAttribPointer(0, | ||||
|                             2, | ||||
|                             GL_FLOAT, | ||||
|                             GL_FALSE, | ||||
|                             kPointsPerVertex * sizeof(float), | ||||
|                             static_cast<void*>(0)); | ||||
|    gl.glEnableVertexAttribArray(0); | ||||
| 
 | ||||
|    // aXYOffset
 | ||||
|    gl.glVertexAttribPointer(1, | ||||
|                             2, | ||||
|                             GL_FLOAT, | ||||
|                             GL_FALSE, | ||||
|                             kPointsPerVertex * sizeof(float), | ||||
|                             reinterpret_cast<void*>(2 * sizeof(float))); | ||||
|    gl.glEnableVertexAttribArray(1); | ||||
| 
 | ||||
|    // aModulate
 | ||||
|    gl.glVertexAttribPointer(3, | ||||
|                             4, | ||||
|                             GL_FLOAT, | ||||
|                             GL_FALSE, | ||||
|                             kPointsPerVertex * sizeof(float), | ||||
|                             reinterpret_cast<void*>(4 * sizeof(float))); | ||||
|    gl.glEnableVertexAttribArray(3); | ||||
| 
 | ||||
|    // aAngle
 | ||||
|    gl.glVertexAttribPointer(4, | ||||
|                             1, | ||||
|                             GL_FLOAT, | ||||
|                             GL_FALSE, | ||||
|                             kPointsPerVertex * sizeof(float), | ||||
|                             reinterpret_cast<void*>(8 * sizeof(float))); | ||||
|    gl.glEnableVertexAttribArray(4); | ||||
| 
 | ||||
|    gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); | ||||
|    gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); | ||||
| 
 | ||||
|    // aThreshold
 | ||||
|    gl.glVertexAttribIPointer(5, //
 | ||||
|                              1, | ||||
|                              GL_INT, | ||||
|                              kIntegersPerVertex_ * sizeof(GLint), | ||||
|                              static_cast<void*>(0)); | ||||
|    gl.glEnableVertexAttribArray(5); | ||||
| 
 | ||||
|    // aTimeRange
 | ||||
|    gl.glVertexAttribIPointer(6, //
 | ||||
|                              2, | ||||
|                              GL_INT, | ||||
|                              kIntegersPerVertex_ * sizeof(GLint), | ||||
|                              reinterpret_cast<void*>(1 * sizeof(GLint))); | ||||
|    gl.glEnableVertexAttribArray(6); | ||||
| 
 | ||||
|    p->dirty_ = true; | ||||
| } | ||||
| 
 | ||||
| void GeoLines::Render(const QMapLibreGL::CustomLayerRenderParameters& params) | ||||
| { | ||||
|    if (!p->visible_) | ||||
|    { | ||||
|       return; | ||||
|    } | ||||
| 
 | ||||
|    std::unique_lock lock {p->lineMutex_}; | ||||
| 
 | ||||
|    if (p->currentLineList_.size() > 0) | ||||
|    { | ||||
|       gl::OpenGLFunctions& gl = p->context_->gl(); | ||||
| 
 | ||||
|       gl.glBindVertexArray(p->vao_); | ||||
| 
 | ||||
|       p->Update(); | ||||
|       p->shaderProgram_->Use(); | ||||
|       UseRotationProjection(params, p->uMVPMatrixLocation_); | ||||
|       UseMapProjection( | ||||
|          params, p->uMapMatrixLocation_, p->uMapScreenCoordLocation_); | ||||
| 
 | ||||
|       if (p->thresholded_) | ||||
|       { | ||||
|          // If thresholding is enabled, set the map distance
 | ||||
|          units::length::nautical_miles<float> mapDistance = | ||||
|             util::maplibre::GetMapDistance(params); | ||||
|          gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); | ||||
|       } | ||||
|       else | ||||
|       { | ||||
|          // If thresholding is disabled, set the map distance to 0
 | ||||
|          gl.glUniform1f(p->uMapDistanceLocation_, 0.0f); | ||||
|       } | ||||
| 
 | ||||
|       // Selected time
 | ||||
|       std::chrono::system_clock::time_point selectedTime = | ||||
|          (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? | ||||
|             std::chrono::system_clock::now() : | ||||
|             p->selectedTime_; | ||||
|       gl.glUniform1i( | ||||
|          p->uSelectedTimeLocation_, | ||||
|          static_cast<GLint>(std::chrono::duration_cast<std::chrono::minutes>( | ||||
|                                selectedTime.time_since_epoch()) | ||||
|                                .count())); | ||||
| 
 | ||||
|       // Draw icons
 | ||||
|       gl.glDrawArrays(GL_TRIANGLES, | ||||
|                       0, | ||||
|                       static_cast<GLsizei>(p->currentLineList_.size() * | ||||
|                                            kVerticesPerRectangle)); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void GeoLines::Deinitialize() | ||||
| { | ||||
|    gl::OpenGLFunctions& gl = p->context_->gl(); | ||||
| 
 | ||||
|    gl.glDeleteVertexArrays(1, &p->vao_); | ||||
|    gl.glDeleteBuffers(2, p->vbo_.data()); | ||||
| 
 | ||||
|    std::unique_lock lock {p->lineMutex_}; | ||||
| 
 | ||||
|    p->currentLinesBuffer_.clear(); | ||||
|    p->currentIntegerBuffer_.clear(); | ||||
|    p->currentHoverLines_.clear(); | ||||
| } | ||||
| 
 | ||||
| void GeoLines::SetVisible(bool visible) | ||||
| { | ||||
|    p->visible_ = visible; | ||||
| } | ||||
| 
 | ||||
| void GeoLines::StartLines() | ||||
| { | ||||
|    // Clear the new buffers
 | ||||
|    p->newLineList_.clear(); | ||||
|    p->newLinesBuffer_.clear(); | ||||
|    p->newIntegerBuffer_.clear(); | ||||
|    p->newHoverLines_.clear(); | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<GeoLineDrawItem> GeoLines::AddLine() | ||||
| { | ||||
|    return p->newLineList_.emplace_back(std::make_shared<GeoLineDrawItem>()); | ||||
| } | ||||
| 
 | ||||
| void GeoLines::SetLineLocation(const std::shared_ptr<GeoLineDrawItem>& di, | ||||
|                                float latitude1, | ||||
|                                float longitude1, | ||||
|                                float latitude2, | ||||
|                                float longitude2) | ||||
| { | ||||
|    di->latitude1_  = latitude1; | ||||
|    di->longitude1_ = longitude1; | ||||
|    di->latitude2_  = latitude2; | ||||
|    di->longitude2_ = longitude2; | ||||
| } | ||||
| 
 | ||||
| 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}; | ||||
| } | ||||
| 
 | ||||
| void GeoLines::SetLineModulate(const std::shared_ptr<GeoLineDrawItem>& di, | ||||
|                                boost::gil::rgba32f_pixel_t             modulate) | ||||
| { | ||||
|    di->modulate_ = modulate; | ||||
| } | ||||
| 
 | ||||
| void GeoLines::SetLineWidth(const std::shared_ptr<GeoLineDrawItem>& di, | ||||
|                             float                                   width) | ||||
| { | ||||
|    di->width_ = width; | ||||
| } | ||||
| 
 | ||||
| void GeoLines::SetLineVisible(const std::shared_ptr<GeoLineDrawItem>& di, | ||||
|                               bool                                    visible) | ||||
| { | ||||
|    di->visible_ = visible; | ||||
| } | ||||
| 
 | ||||
| void GeoLines::SetLineHoverText(const std::shared_ptr<GeoLineDrawItem>& di, | ||||
|                                 const std::string&                      text) | ||||
| { | ||||
|    di->hoverText_ = text; | ||||
| } | ||||
| 
 | ||||
| void GeoLines::FinishLines() | ||||
| { | ||||
|    // Update buffers
 | ||||
|    p->UpdateBuffers(); | ||||
| 
 | ||||
|    std::unique_lock lock {p->lineMutex_}; | ||||
| 
 | ||||
|    // Swap buffers
 | ||||
|    p->currentLineList_.swap(p->newLineList_); | ||||
|    p->currentLinesBuffer_.swap(p->newLinesBuffer_); | ||||
|    p->currentIntegerBuffer_.swap(p->newIntegerBuffer_); | ||||
|    p->currentHoverLines_.swap(p->newHoverLines_); | ||||
| 
 | ||||
|    // Clear the new buffers, except the full line list (used to update buffers
 | ||||
|    // without re-adding lines)
 | ||||
|    p->newLinesBuffer_.clear(); | ||||
|    p->newIntegerBuffer_.clear(); | ||||
|    p->newHoverLines_.clear(); | ||||
| 
 | ||||
|    // Mark the draw item dirty
 | ||||
|    p->dirty_ = true; | ||||
| } | ||||
| 
 | ||||
| void GeoLines::Impl::UpdateBuffers() | ||||
| { | ||||
|    newLinesBuffer_.clear(); | ||||
|    newLinesBuffer_.reserve(newLineList_.size() * kBufferLength); | ||||
|    newIntegerBuffer_.clear(); | ||||
|    newIntegerBuffer_.reserve(newLineList_.size() * kVerticesPerRectangle * | ||||
|                              kIntegersPerVertex_); | ||||
|    newHoverLines_.clear(); | ||||
| 
 | ||||
|    for (auto& di : newLineList_) | ||||
|    { | ||||
|       BufferLine(di); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void GeoLines::Impl::BufferLine( | ||||
|    const std::shared_ptr<const GeoLineDrawItem>& di) | ||||
| { | ||||
|    // Threshold value
 | ||||
|    units::length::nautical_miles<double> threshold = di->threshold_; | ||||
|    GLint thresholdValue = static_cast<GLint>(std::round(threshold.value())); | ||||
| 
 | ||||
|    // Start and end time
 | ||||
|    GLint startTime = | ||||
|       static_cast<GLint>(std::chrono::duration_cast<std::chrono::minutes>( | ||||
|                             di->startTime_.time_since_epoch()) | ||||
|                             .count()); | ||||
|    GLint endTime = | ||||
|       static_cast<GLint>(std::chrono::duration_cast<std::chrono::minutes>( | ||||
|                             di->endTime_.time_since_epoch()) | ||||
|                             .count()); | ||||
| 
 | ||||
|    // Latitude and longitude coordinates in degrees
 | ||||
|    const float lat1 = di->latitude1_; | ||||
|    const float lon1 = di->longitude1_; | ||||
|    const float lat2 = di->latitude2_; | ||||
|    const float lon2 = di->longitude2_; | ||||
| 
 | ||||
|    // TODO: Base X/Y offsets in pixels
 | ||||
|    // const float x1 = static_cast<float>(di->x1_);
 | ||||
|    // const float y1 = static_cast<float>(di->y1_);
 | ||||
|    // const float x2 = static_cast<float>(di->x2_);
 | ||||
|    // const float y2 = static_cast<float>(di->y2_);
 | ||||
| 
 | ||||
|    // Angle
 | ||||
|    const units::angle::degrees<double> angle = | ||||
|       util::GeographicLib::GetAngle(lat1, lon1, lat2, lon2); | ||||
|    const float a = static_cast<float>(angle.value()); | ||||
| 
 | ||||
|    // Final X/Y offsets in pixels
 | ||||
|    const float hw = di->width_ * 0.5f; | ||||
|    const float lx = -hw; | ||||
|    const float rx = +hw; | ||||
|    const float ty = +hw; | ||||
|    const float by = -hw; | ||||
| 
 | ||||
|    // Modulate color
 | ||||
|    const float mc0 = di->modulate_[0]; | ||||
|    const float mc1 = di->modulate_[1]; | ||||
|    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}); | ||||
| 
 | ||||
|    if (!di->hoverText_.empty()) | ||||
|    { | ||||
|       const units::angle::radians<double> radians = angle; | ||||
| 
 | ||||
|       const auto sc1 = util::maplibre::LatLongToScreenCoordinate({lat1, lon1}); | ||||
|       const auto sc2 = util::maplibre::LatLongToScreenCoordinate({lat2, lon2}); | ||||
| 
 | ||||
|       const float cosAngle = cosf(static_cast<float>(radians.value())); | ||||
|       const float sinAngle = sinf(static_cast<float>(radians.value())); | ||||
| 
 | ||||
|       const glm::mat2 rotate {cosAngle, -sinAngle, sinAngle, cosAngle}; | ||||
| 
 | ||||
|       const glm::vec2 otl = rotate * glm::vec2 {-hw, +hw}; | ||||
|       const glm::vec2 otr = rotate * glm::vec2 {+hw, +hw}; | ||||
|       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}); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void GeoLines::Impl::Update() | ||||
| { | ||||
|    // If the lines have been updated
 | ||||
|    if (dirty_) | ||||
|    { | ||||
|       gl::OpenGLFunctions& gl = context_->gl(); | ||||
| 
 | ||||
|       // Buffer lines data
 | ||||
|       gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); | ||||
|       gl.glBufferData(GL_ARRAY_BUFFER, | ||||
|                       sizeof(float) * currentLinesBuffer_.size(), | ||||
|                       currentLinesBuffer_.data(), | ||||
|                       GL_DYNAMIC_DRAW); | ||||
| 
 | ||||
|       // Buffer threshold data
 | ||||
|       gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); | ||||
|       gl.glBufferData(GL_ARRAY_BUFFER, | ||||
|                       sizeof(GLint) * currentIntegerBuffer_.size(), | ||||
|                       currentIntegerBuffer_.data(), | ||||
|                       GL_DYNAMIC_DRAW); | ||||
|    } | ||||
| 
 | ||||
|    dirty_ = false; | ||||
| } | ||||
| 
 | ||||
| bool GeoLines::RunMousePicking( | ||||
|    const QMapLibreGL::CustomLayerRenderParameters& params, | ||||
|    const QPointF& /* mouseLocalPos */, | ||||
|    const QPointF&   mouseGlobalPos, | ||||
|    const glm::vec2& mouseCoords, | ||||
|    const common::Coordinate& /* mouseGeoCoords */, | ||||
|    std::shared_ptr<types::EventHandler>& /* eventHandler */) | ||||
| { | ||||
|    std::unique_lock lock {p->lineMutex_}; | ||||
| 
 | ||||
|    bool itemPicked = false; | ||||
| 
 | ||||
|    // Calculate map scale, remove width and height from original calculation
 | ||||
|    glm::vec2 scale = util::maplibre::GetMapScale(params); | ||||
|    scale = 2.0f / glm::vec2 {scale.x * params.width, scale.y * params.height}; | ||||
| 
 | ||||
|    // Scale and rotate the identity matrix to create the map matrix
 | ||||
|    glm::mat4 mapMatrix {1.0f}; | ||||
|    mapMatrix = glm::scale(mapMatrix, glm::vec3 {scale, 1.0f}); | ||||
|    mapMatrix = glm::rotate(mapMatrix, | ||||
|                            glm::radians<float>(params.bearing), | ||||
|                            glm::vec3(0.0f, 0.0f, 1.0f)); | ||||
| 
 | ||||
|    units::length::meters<double> mapDistance = | ||||
|       (p->thresholded_) ? util::maplibre::GetMapDistance(params) : | ||||
|                           units::length::meters<double> {0.0}; | ||||
| 
 | ||||
|    // If no time has been selected, use the current time
 | ||||
|    std::chrono::system_clock::time_point selectedTime = | ||||
|       (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? | ||||
|          std::chrono::system_clock::now() : | ||||
|          p->selectedTime_; | ||||
| 
 | ||||
|    // For each pickable line
 | ||||
|    auto it = std::find_if( | ||||
|       std::execution::par_unseq, | ||||
|       p->currentHoverLines_.crbegin(), | ||||
|       p->currentHoverLines_.crend(), | ||||
|       [&mapDistance, &selectedTime, &mapMatrix, &mouseCoords](const auto& line) | ||||
|       { | ||||
|          if (( | ||||
|                 // Placefile is thresholded
 | ||||
|                 mapDistance > units::length::meters<double> {0.0} && | ||||
| 
 | ||||
|                 // Placefile threshold is < 999 nmi
 | ||||
|                 static_cast<int>(std::round( | ||||
|                    units::length::nautical_miles<double> {line.di_->threshold_} | ||||
|                       .value())) < 999 && | ||||
| 
 | ||||
|                 // Map distance is beyond the threshold
 | ||||
|                 line.di_->threshold_ < mapDistance) || | ||||
| 
 | ||||
|              ( | ||||
|                 // Line has a start time
 | ||||
|                 line.di_->startTime_ != | ||||
|                    std::chrono::system_clock::time_point {} && | ||||
| 
 | ||||
|                 // The time range has not yet started
 | ||||
|                 (selectedTime < line.di_->startTime_ || | ||||
| 
 | ||||
|                  // The time range has ended
 | ||||
|                  line.di_->endTime_ <= selectedTime))) | ||||
|          { | ||||
|             // Line is not pickable
 | ||||
|             return false; | ||||
|          } | ||||
| 
 | ||||
|          // Initialize vertices
 | ||||
|          glm::vec2 bl = line.p1_; | ||||
|          glm::vec2 br = bl; | ||||
|          glm::vec2 tl = line.p2_; | ||||
|          glm::vec2 tr = tl; | ||||
| 
 | ||||
|          // Calculate offsets
 | ||||
|          // - Rotated offset is half the line width (pixels) in each direction
 | ||||
|          // - Multiply the offset by the scaled and rotated map matrix
 | ||||
|          const glm::vec2 otl = mapMatrix * glm::vec4 {line.otl_, 0.0f, 1.0f}; | ||||
|          const glm::vec2 obl = mapMatrix * glm::vec4 {line.obl_, 0.0f, 1.0f}; | ||||
|          const glm::vec2 obr = mapMatrix * glm::vec4 {line.obr_, 0.0f, 1.0f}; | ||||
|          const glm::vec2 otr = mapMatrix * glm::vec4 {line.otr_, 0.0f, 1.0f}; | ||||
| 
 | ||||
|          // Offset vertices
 | ||||
|          tl += otl; | ||||
|          bl += obl; | ||||
|          br += obr; | ||||
|          tr += otr; | ||||
| 
 | ||||
|          // TODO: X/Y offsets
 | ||||
| 
 | ||||
|          // Test point against polygon bounds
 | ||||
|          return util::maplibre::IsPointInPolygon({tl, bl, br, tr}, mouseCoords); | ||||
|       }); | ||||
| 
 | ||||
|    if (it != p->currentHoverLines_.crend()) | ||||
|    { | ||||
|       itemPicked = true; | ||||
|       util::tooltip::Show(it->di_->hoverText_, mouseGlobalPos); | ||||
|    } | ||||
| 
 | ||||
|    return itemPicked; | ||||
| } | ||||
| 
 | ||||
| } // namespace draw
 | ||||
| } // namespace gl
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
							
								
								
									
										140
									
								
								scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,140 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <scwx/qt/gl/gl_context.hpp> | ||||
| #include <scwx/qt/gl/draw/draw_item.hpp> | ||||
| 
 | ||||
| #include <boost/gil.hpp> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace qt | ||||
| { | ||||
| namespace gl | ||||
| { | ||||
| namespace draw | ||||
| { | ||||
| 
 | ||||
| struct GeoLineDrawItem; | ||||
| 
 | ||||
| class GeoLines : public DrawItem | ||||
| { | ||||
| public: | ||||
|    explicit GeoLines(std::shared_ptr<GlContext> context); | ||||
|    ~GeoLines(); | ||||
| 
 | ||||
|    GeoLines(const GeoLines&)            = delete; | ||||
|    GeoLines& operator=(const GeoLines&) = delete; | ||||
| 
 | ||||
|    GeoLines(GeoLines&&) noexcept; | ||||
|    GeoLines& operator=(GeoLines&&) noexcept; | ||||
| 
 | ||||
|    void set_selected_time(std::chrono::system_clock::time_point selectedTime); | ||||
|    void set_thresholded(bool thresholded); | ||||
| 
 | ||||
|    void Initialize() override; | ||||
|    void Render(const QMapLibreGL::CustomLayerRenderParameters& params) override; | ||||
|    void Deinitialize() override; | ||||
| 
 | ||||
|    bool | ||||
|    RunMousePicking(const QMapLibreGL::CustomLayerRenderParameters& params, | ||||
|                    const QPointF&                        mouseLocalPos, | ||||
|                    const QPointF&                        mouseGlobalPos, | ||||
|                    const glm::vec2&                      mouseCoords, | ||||
|                    const common::Coordinate&             mouseGeoCoords, | ||||
|                    std::shared_ptr<types::EventHandler>& eventHandler) override; | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the visibility of the geo lines. | ||||
|     * | ||||
|     * @param [in] visible Line visibility | ||||
|     */ | ||||
|    void SetVisible(bool visible); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Resets and prepares the draw item for adding a new set of lines. | ||||
|     */ | ||||
|    void StartLines(); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Adds a geo line to the internal draw list. | ||||
|     * | ||||
|     * @return Geo line draw item | ||||
|     */ | ||||
|    std::shared_ptr<GeoLineDrawItem> AddLine(); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the location of a geo line. | ||||
|     * | ||||
|     * @param [in] di Geo line draw item | ||||
|     * @param [in] latitude1 The latitude of the first endpoint of the geo line | ||||
|     * in degrees. | ||||
|     * @param [in] longitude1 The longitude of the first endpoint of the geo line | ||||
|     * in degrees. | ||||
|     * @param [in] latitude2 The latitude of the second endpoint of the geo line | ||||
|     * in degrees. | ||||
|     * @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); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the modulate color of a geo line. | ||||
|     * | ||||
|     * @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); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the modulate color of a geo line. | ||||
|     * | ||||
|     * @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); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the width of the geo line. | ||||
|     * | ||||
|     * @param [in] width Width in pixels | ||||
|     */ | ||||
|    static 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); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the hover text of a geo line. | ||||
|     * | ||||
|     * @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); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Finalizes the draw item after adding new lines. | ||||
|     */ | ||||
|    void FinishLines(); | ||||
| 
 | ||||
| private: | ||||
|    class Impl; | ||||
|    std::unique_ptr<Impl> p; | ||||
| }; | ||||
| 
 | ||||
| } // namespace draw
 | ||||
| } // namespace gl
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
							
								
								
									
										358
									
								
								scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										358
									
								
								scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,358 @@ | |||
| #include <scwx/qt/gl/draw/linked_vectors.hpp> | ||||
| #include <scwx/qt/gl/draw/geo_lines.hpp> | ||||
| #include <scwx/qt/util/geographic_lib.hpp> | ||||
| #include <scwx/wsr88d/rpg/linked_vector_packet.hpp> | ||||
| 
 | ||||
| #include <boost/iterator/zip_iterator.hpp> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace qt | ||||
| { | ||||
| namespace gl | ||||
| { | ||||
| namespace draw | ||||
| { | ||||
| 
 | ||||
| static const std::string logPrefix_ = "scwx::qt::gl::draw::linked_vectors"; | ||||
| 
 | ||||
| static const boost::gil::rgba32f_pixel_t kBlack {0.0f, 0.0f, 0.0f, 1.0f}; | ||||
| 
 | ||||
| struct LinkedVectorDrawItem | ||||
| { | ||||
|    LinkedVectorDrawItem( | ||||
|       const common::Coordinate& center, | ||||
|       const std::shared_ptr<const wsr88d::rpg::LinkedVectorPacket>& | ||||
|          vectorPacket) | ||||
|    { | ||||
|       coordinates_.push_back(util::GeographicLib::GetCoordinate( | ||||
|          center, vectorPacket->start_i_km(), vectorPacket->start_j_km())); | ||||
| 
 | ||||
|       const auto endI = vectorPacket->end_i_km(); | ||||
|       const auto endJ = vectorPacket->end_j_km(); | ||||
| 
 | ||||
|       std::for_each( | ||||
|          boost::make_zip_iterator( | ||||
|             boost::make_tuple(endI.begin(), endJ.begin())), | ||||
|          boost::make_zip_iterator(boost::make_tuple(endI.end(), endJ.end())), | ||||
|          [this, | ||||
|           ¢er](const boost::tuple<units::length::kilometers<double>, | ||||
|                                       units::length::kilometers<double>>& p) | ||||
|          { | ||||
|             coordinates_.push_back(util::GeographicLib::GetCoordinate( | ||||
|                center, p.get<0>(), p.get<1>())); | ||||
|          }); | ||||
|    } | ||||
| 
 | ||||
|    std::vector<std::shared_ptr<GeoLineDrawItem>> borderDrawItems_ {}; | ||||
|    std::vector<std::shared_ptr<GeoLineDrawItem>> lineDrawItems_ {}; | ||||
| 
 | ||||
|    std::vector<common::Coordinate> coordinates_ {}; | ||||
| 
 | ||||
|    boost::gil::rgba32f_pixel_t modulate_ {1.0f, 1.0f, 1.0f, 1.0f}; | ||||
|    float                       width_ {5.0f}; | ||||
|    bool                        visible_ {true}; | ||||
|    std::string                 hoverText_ {}; | ||||
| 
 | ||||
|    bool                                  ticksEnabled_ {false}; | ||||
|    units::length::nautical_miles<double> tickRadius_ {1.0}; | ||||
|    units::length::nautical_miles<double> tickRadiusIncrement_ {0.0}; | ||||
| }; | ||||
| 
 | ||||
| class LinkedVectors::Impl | ||||
| { | ||||
| public: | ||||
|    explicit Impl(std::shared_ptr<GlContext> context) : | ||||
|        context_ {context}, geoLines_ {std::make_shared<GeoLines>(context)} | ||||
|    { | ||||
|    } | ||||
| 
 | ||||
|    ~Impl() {} | ||||
| 
 | ||||
|    std::shared_ptr<GlContext> context_; | ||||
| 
 | ||||
|    bool borderEnabled_ {true}; | ||||
|    bool visible_ {true}; | ||||
| 
 | ||||
|    std::vector<std::shared_ptr<LinkedVectorDrawItem>> vectorList_ {}; | ||||
|    std::shared_ptr<GeoLines>                          geoLines_; | ||||
| }; | ||||
| 
 | ||||
| LinkedVectors::LinkedVectors(std::shared_ptr<GlContext> context) : | ||||
|     DrawItem(context->gl()), p(std::make_unique<Impl>(context)) | ||||
| { | ||||
| } | ||||
| LinkedVectors::~LinkedVectors() = default; | ||||
| 
 | ||||
| LinkedVectors::LinkedVectors(LinkedVectors&&) noexcept            = default; | ||||
| LinkedVectors& LinkedVectors::operator=(LinkedVectors&&) noexcept = default; | ||||
| 
 | ||||
| void LinkedVectors::set_selected_time( | ||||
|    std::chrono::system_clock::time_point selectedTime) | ||||
| { | ||||
|    p->geoLines_->set_selected_time(selectedTime); | ||||
| } | ||||
| 
 | ||||
| void LinkedVectors::set_thresholded(bool thresholded) | ||||
| { | ||||
|    p->geoLines_->set_thresholded(thresholded); | ||||
| } | ||||
| 
 | ||||
| void LinkedVectors::Initialize() | ||||
| { | ||||
|    p->geoLines_->Initialize(); | ||||
| } | ||||
| 
 | ||||
| void LinkedVectors::Render( | ||||
|    const QMapLibreGL::CustomLayerRenderParameters& params) | ||||
| { | ||||
|    if (!p->visible_) | ||||
|    { | ||||
|       return; | ||||
|    } | ||||
| 
 | ||||
|    p->geoLines_->Render(params); | ||||
| } | ||||
| 
 | ||||
| void LinkedVectors::Deinitialize() | ||||
| { | ||||
|    p->geoLines_->Deinitialize(); | ||||
| } | ||||
| 
 | ||||
| void LinkedVectors::SetBorderEnabled(bool enabled) | ||||
| { | ||||
|    p->borderEnabled_ = enabled; | ||||
| } | ||||
| 
 | ||||
| void LinkedVectors::SetVisible(bool visible) | ||||
| { | ||||
|    p->visible_ = visible; | ||||
| } | ||||
| 
 | ||||
| void LinkedVectors::StartVectors() | ||||
| { | ||||
|    // Start a new set of geo lines
 | ||||
|    p->geoLines_->StartLines(); | ||||
|    p->vectorList_.clear(); | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<LinkedVectorDrawItem> LinkedVectors::AddVector( | ||||
|    const common::Coordinate&                                     center, | ||||
|    const std::shared_ptr<const wsr88d::rpg::LinkedVectorPacket>& vectorPacket) | ||||
| { | ||||
|    return p->vectorList_.emplace_back( | ||||
|       std::make_shared<LinkedVectorDrawItem>(center, vectorPacket)); | ||||
| } | ||||
| 
 | ||||
| void LinkedVectors::SetVectorModulate( | ||||
|    const std::shared_ptr<LinkedVectorDrawItem>& 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}; | ||||
| } | ||||
| 
 | ||||
| void LinkedVectors::SetVectorModulate( | ||||
|    const std::shared_ptr<LinkedVectorDrawItem>& di, | ||||
|    boost::gil::rgba32f_pixel_t                  modulate) | ||||
| { | ||||
|    di->modulate_ = modulate; | ||||
| } | ||||
| 
 | ||||
| void LinkedVectors::SetVectorWidth( | ||||
|    const std::shared_ptr<LinkedVectorDrawItem>& di, float width) | ||||
| { | ||||
|    di->width_ = width; | ||||
| } | ||||
| 
 | ||||
| void LinkedVectors::SetVectorVisible( | ||||
|    const std::shared_ptr<LinkedVectorDrawItem>& di, bool visible) | ||||
| { | ||||
|    di->visible_ = visible; | ||||
| } | ||||
| 
 | ||||
| void LinkedVectors::SetVectorHoverText( | ||||
|    const std::shared_ptr<LinkedVectorDrawItem>& di, const std::string& text) | ||||
| { | ||||
|    di->hoverText_ = text; | ||||
| } | ||||
| 
 | ||||
| void LinkedVectors::SetVectorTicksEnabled( | ||||
|    const std::shared_ptr<LinkedVectorDrawItem>& di, bool enabled) | ||||
| { | ||||
|    di->ticksEnabled_ = enabled; | ||||
| } | ||||
| 
 | ||||
| void LinkedVectors::SetVectorTickRadius( | ||||
|    const std::shared_ptr<LinkedVectorDrawItem>& di, | ||||
|    units::length::meters<double>                radius) | ||||
| { | ||||
|    di->tickRadius_ = radius; | ||||
| } | ||||
| 
 | ||||
| void LinkedVectors::SetVectorTickRadiusIncrement( | ||||
|    const std::shared_ptr<LinkedVectorDrawItem>& di, | ||||
|    units::length::meters<double>                radiusIncrement) | ||||
| { | ||||
|    di->tickRadiusIncrement_ = radiusIncrement; | ||||
| } | ||||
| 
 | ||||
| void LinkedVectors::FinishVectors() | ||||
| { | ||||
|    // Generate borders
 | ||||
|    if (p->borderEnabled_) | ||||
|    { | ||||
|       for (auto& di : p->vectorList_) | ||||
|       { | ||||
|          auto tickRadius = di->tickRadius_; | ||||
| 
 | ||||
|          for (std::size_t i = 0; i < di->coordinates_.size() - 1; ++i) | ||||
|          { | ||||
|             auto borderLine = p->geoLines_->AddLine(); | ||||
| 
 | ||||
|             const common::Coordinate& coordinate1 = di->coordinates_[i]; | ||||
|             const common::Coordinate& coordinate2 = di->coordinates_[i + 1]; | ||||
| 
 | ||||
|             const double& latitude1  = coordinate1.latitude_; | ||||
|             const double& longitude1 = coordinate1.longitude_; | ||||
|             const double& latitude2  = coordinate2.latitude_; | ||||
|             const double& longitude2 = coordinate2.longitude_; | ||||
| 
 | ||||
|             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_); | ||||
| 
 | ||||
|             di->borderDrawItems_.emplace_back(std::move(borderLine)); | ||||
| 
 | ||||
|             if (di->ticksEnabled_) | ||||
|             { | ||||
|                auto angle = util::GeographicLib::GetAngle( | ||||
|                   latitude1, longitude1, latitude2, longitude2); | ||||
|                auto angle1 = angle + units::angle::degrees<double>(90.0); | ||||
|                auto angle2 = angle - units::angle::degrees<double>(90.0); | ||||
| 
 | ||||
|                auto tickCoord1 = util::GeographicLib::GetCoordinate( | ||||
|                   coordinate2, angle1, tickRadius); | ||||
|                auto tickCoord2 = util::GeographicLib::GetCoordinate( | ||||
|                   coordinate2, angle2, tickRadius); | ||||
| 
 | ||||
|                const double& tickLat1 = tickCoord1.latitude_; | ||||
|                const double& tickLon1 = tickCoord1.longitude_; | ||||
|                const double& tickLat2 = tickCoord2.latitude_; | ||||
|                const double& tickLon2 = tickCoord2.longitude_; | ||||
| 
 | ||||
|                auto tickBorderLine = p->geoLines_->AddLine(); | ||||
| 
 | ||||
|                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_); | ||||
| 
 | ||||
|                tickRadius += di->tickRadiusIncrement_; | ||||
|             } | ||||
|          } | ||||
|       } | ||||
|    } | ||||
| 
 | ||||
|    // Generate geo lines
 | ||||
|    for (auto& di : p->vectorList_) | ||||
|    { | ||||
|       auto tickRadius = di->tickRadius_; | ||||
| 
 | ||||
|       for (std::size_t i = 0; i < di->coordinates_.size() - 1; ++i) | ||||
|       { | ||||
|          auto geoLine = p->geoLines_->AddLine(); | ||||
| 
 | ||||
|          const common::Coordinate& coordinate1 = di->coordinates_[i]; | ||||
|          const common::Coordinate& coordinate2 = di->coordinates_[i + 1]; | ||||
| 
 | ||||
|          const double& latitude1  = coordinate1.latitude_; | ||||
|          const double& longitude1 = coordinate1.longitude_; | ||||
|          const double& latitude2  = coordinate2.latitude_; | ||||
|          const double& longitude2 = coordinate2.longitude_; | ||||
| 
 | ||||
|          GeoLines::SetLineLocation( | ||||
|             geoLine, latitude1, longitude1, latitude2, longitude2); | ||||
| 
 | ||||
|          GeoLines::SetLineModulate(geoLine, di->modulate_); | ||||
|          GeoLines::SetLineWidth(geoLine, di->width_); | ||||
|          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_); | ||||
|          } | ||||
| 
 | ||||
|          di->lineDrawItems_.emplace_back(std::move(geoLine)); | ||||
| 
 | ||||
|          if (di->ticksEnabled_) | ||||
|          { | ||||
|             auto angle = util::GeographicLib::GetAngle( | ||||
|                latitude1, longitude1, latitude2, longitude2); | ||||
|             auto angle1 = angle + units::angle::degrees<double>(90.0); | ||||
|             auto angle2 = angle - units::angle::degrees<double>(90.0); | ||||
| 
 | ||||
|             auto tickCoord1 = util::GeographicLib::GetCoordinate( | ||||
|                coordinate2, angle1, tickRadius); | ||||
|             auto tickCoord2 = util::GeographicLib::GetCoordinate( | ||||
|                coordinate2, angle2, tickRadius); | ||||
| 
 | ||||
|             const double& tickLat1 = tickCoord1.latitude_; | ||||
|             const double& tickLon1 = tickCoord1.longitude_; | ||||
|             const double& tickLat2 = tickCoord2.latitude_; | ||||
|             const double& tickLon2 = tickCoord2.longitude_; | ||||
| 
 | ||||
|             auto tickGeoLine = p->geoLines_->AddLine(); | ||||
| 
 | ||||
|             GeoLines::SetLineLocation( | ||||
|                tickGeoLine, tickLat1, tickLon1, tickLat2, tickLon2); | ||||
| 
 | ||||
|             GeoLines::SetLineModulate(tickGeoLine, di->modulate_); | ||||
|             GeoLines::SetLineWidth(tickGeoLine, di->width_); | ||||
|             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_); | ||||
|             } | ||||
| 
 | ||||
|             tickRadius += di->tickRadiusIncrement_; | ||||
|          } | ||||
|       } | ||||
|    } | ||||
| 
 | ||||
|    // Finish geo lines
 | ||||
|    p->geoLines_->FinishLines(); | ||||
| } | ||||
| 
 | ||||
| bool LinkedVectors::RunMousePicking( | ||||
|    const QMapLibreGL::CustomLayerRenderParameters& params, | ||||
|    const QPointF&                                  mouseLocalPos, | ||||
|    const QPointF&                                  mouseGlobalPos, | ||||
|    const glm::vec2&                                mouseCoords, | ||||
|    const common::Coordinate&                       mouseGeoCoords, | ||||
|    std::shared_ptr<types::EventHandler>&           eventHandler) | ||||
| { | ||||
|    return p->geoLines_->RunMousePicking(params, | ||||
|                                         mouseLocalPos, | ||||
|                                         mouseGlobalPos, | ||||
|                                         mouseCoords, | ||||
|                                         mouseGeoCoords, | ||||
|                                         eventHandler); | ||||
| } | ||||
| 
 | ||||
| } // namespace draw
 | ||||
| } // namespace gl
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
							
								
								
									
										179
									
								
								scwx-qt/source/scwx/qt/gl/draw/linked_vectors.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								scwx-qt/source/scwx/qt/gl/draw/linked_vectors.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,179 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <scwx/qt/gl/gl_context.hpp> | ||||
| #include <scwx/qt/gl/draw/draw_item.hpp> | ||||
| 
 | ||||
| #include <boost/gil.hpp> | ||||
| #include <units/length.h> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace wsr88d | ||||
| { | ||||
| namespace rpg | ||||
| { | ||||
| 
 | ||||
| class LinkedVectorPacket; | ||||
| 
 | ||||
| } // namespace rpg
 | ||||
| } // namespace wsr88d
 | ||||
| 
 | ||||
| namespace qt | ||||
| { | ||||
| namespace gl | ||||
| { | ||||
| namespace draw | ||||
| { | ||||
| 
 | ||||
| struct LinkedVectorDrawItem; | ||||
| 
 | ||||
| class LinkedVectors : public DrawItem | ||||
| { | ||||
| public: | ||||
|    explicit LinkedVectors(std::shared_ptr<GlContext> context); | ||||
|    ~LinkedVectors(); | ||||
| 
 | ||||
|    LinkedVectors(const LinkedVectors&)            = delete; | ||||
|    LinkedVectors& operator=(const LinkedVectors&) = delete; | ||||
| 
 | ||||
|    LinkedVectors(LinkedVectors&&) noexcept; | ||||
|    LinkedVectors& operator=(LinkedVectors&&) noexcept; | ||||
| 
 | ||||
|    void set_selected_time(std::chrono::system_clock::time_point selectedTime); | ||||
|    void set_thresholded(bool thresholded); | ||||
| 
 | ||||
|    void Initialize() override; | ||||
|    void Render(const QMapLibreGL::CustomLayerRenderParameters& params) override; | ||||
|    void Deinitialize() override; | ||||
| 
 | ||||
|    bool | ||||
|    RunMousePicking(const QMapLibreGL::CustomLayerRenderParameters& params, | ||||
|                    const QPointF&                        mouseLocalPos, | ||||
|                    const QPointF&                        mouseGlobalPos, | ||||
|                    const glm::vec2&                      mouseCoords, | ||||
|                    const common::Coordinate&             mouseGeoCoords, | ||||
|                    std::shared_ptr<types::EventHandler>& eventHandler) override; | ||||
| 
 | ||||
|    /**
 | ||||
|     * Enables or disables the border around each line in a linked vector. | ||||
|     * | ||||
|     * @param [in] enabled Border visibility | ||||
|     */ | ||||
|    void SetBorderEnabled(bool enabled); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the visibility of the linked vectors. | ||||
|     * | ||||
|     * @param [in] visible Line visibility | ||||
|     */ | ||||
|    void SetVisible(bool visible); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Resets and prepares the draw item for adding a new set of linked vectors. | ||||
|     */ | ||||
|    void StartVectors(); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Adds a linked vector to the internal draw list. | ||||
|     * | ||||
|     * @param [in] center Center coordinate on which the linked vectors are based | ||||
|     * @param [in] vectorPacket Linked vector packet containing start and end | ||||
|     * points | ||||
|     * | ||||
|     * @return Linked vector draw item | ||||
|     */ | ||||
|    std::shared_ptr<LinkedVectorDrawItem> | ||||
|    AddVector(const common::Coordinate& center, | ||||
|              const std::shared_ptr<const wsr88d::rpg::LinkedVectorPacket>& | ||||
|                 vectorPacket); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the modulate color of a linked vector. | ||||
|     * | ||||
|     * @param [in] di Linked vector draw item | ||||
|     * @param [in] modulate Modulate color | ||||
|     */ | ||||
|    static void | ||||
|    SetVectorModulate(const std::shared_ptr<LinkedVectorDrawItem>& di, | ||||
|                      boost::gil::rgba8_pixel_t                    color); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the modulate color of a linked vector. | ||||
|     * | ||||
|     * @param [in] di Linked vector draw item | ||||
|     * @param [in] modulate Modulate color | ||||
|     */ | ||||
|    static void | ||||
|    SetVectorModulate(const std::shared_ptr<LinkedVectorDrawItem>& di, | ||||
|                      boost::gil::rgba32f_pixel_t                  modulate); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the width of the linked vector. | ||||
|     * | ||||
|     * @param [in] width Width in pixels | ||||
|     */ | ||||
|    static void SetVectorWidth(const std::shared_ptr<LinkedVectorDrawItem>& di, | ||||
|                               float width); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the visibility of the linked vector. | ||||
|     * | ||||
|     * @param [in] visible | ||||
|     */ | ||||
|    static void SetVectorVisible(const std::shared_ptr<LinkedVectorDrawItem>& di, | ||||
|                                 bool visible); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the hover text of a linked vector. | ||||
|     * | ||||
|     * @param [in] di Linked vector draw item | ||||
|     * @param [in] text Hover text | ||||
|     */ | ||||
|    static void | ||||
|    SetVectorHoverText(const std::shared_ptr<LinkedVectorDrawItem>& di, | ||||
|                       const std::string&                           text); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the presence of ticks on the linked vector. | ||||
|     * | ||||
|     * @param [in] di Linked vector draw item | ||||
|     * @param [in] enabled Ticks enabled | ||||
|     */ | ||||
|    static void | ||||
|    SetVectorTicksEnabled(const std::shared_ptr<LinkedVectorDrawItem>& di, | ||||
|                          bool                                         enabled); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the tick radius of the linked vector. | ||||
|     * | ||||
|     * @param [in] di Linked vector draw item | ||||
|     * @param [in] radius Length of the tick extending beyond the linked vector | ||||
|     */ | ||||
|    static void | ||||
|    SetVectorTickRadius(const std::shared_ptr<LinkedVectorDrawItem>& di, | ||||
|                        units::length::meters<double>                radius); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Sets the tick radius increment of the linked vector. | ||||
|     * | ||||
|     * @param [in] di Linked vector draw item | ||||
|     * @param [in] radiusIncrement Length increment of each tick beyond the first | ||||
|     */ | ||||
|    static void | ||||
|    SetVectorTickRadiusIncrement(const std::shared_ptr<LinkedVectorDrawItem>& di, | ||||
|                                 units::length::meters<double> radiusIncrement); | ||||
| 
 | ||||
|    /**
 | ||||
|     * Finalizes the draw item after adding new linked vectors. | ||||
|     */ | ||||
|    void FinishVectors(); | ||||
| 
 | ||||
| private: | ||||
|    class Impl; | ||||
|    std::unique_ptr<Impl> p; | ||||
| }; | ||||
| 
 | ||||
| } // namespace draw
 | ||||
| } // namespace gl
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
|  | @ -11,6 +11,7 @@ | |||
| #include <scwx/qt/types/qt_types.hpp> | ||||
| #include <scwx/qt/ui/setup/setup_wizard.hpp> | ||||
| #include <scwx/network/cpr.hpp> | ||||
| #include <scwx/util/environment.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| #include <scwx/util/threads.hpp> | ||||
| 
 | ||||
|  | @ -19,6 +20,7 @@ | |||
| #include <fmt/format.h> | ||||
| #include <spdlog/spdlog.h> | ||||
| #include <QApplication> | ||||
| #include <QStandardPaths> | ||||
| #include <QTranslator> | ||||
| 
 | ||||
| static const std::string logPrefix_ = "scwx::main"; | ||||
|  | @ -45,6 +47,11 @@ int main(int argc, char* argv[]) | |||
|       QCoreApplication::installTranslator(&translator); | ||||
|    } | ||||
| 
 | ||||
|    if (!scwx::util::GetEnvironment("SCWX_TEST").empty()) | ||||
|    { | ||||
|       QStandardPaths::setTestModeEnabled(true); | ||||
|    } | ||||
| 
 | ||||
|    // Start the io_context main loop
 | ||||
|    boost::asio::io_context& ioContext = scwx::util::io_context(); | ||||
|    auto                     work      = boost::asio::make_work_guard(ioContext); | ||||
|  |  | |||
|  | @ -62,7 +62,8 @@ static constexpr uint32_t NUM_COORIDNATES_1_DEGREE = | |||
| 
 | ||||
| static const std::string kDefaultLevel3Product_ {"N0B"}; | ||||
| 
 | ||||
| static constexpr std::chrono::seconds kRetryInterval_ {15}; | ||||
| static constexpr std::chrono::seconds kFastRetryInterval_ {15}; | ||||
| static constexpr std::chrono::seconds kSlowRetryInterval_ {120}; | ||||
| 
 | ||||
| static std::unordered_map<std::string, std::weak_ptr<RadarProductManager>> | ||||
|                          instanceMap_; | ||||
|  | @ -638,10 +639,12 @@ void RadarProductManagerImpl::RefreshData( | |||
|       threadPool_, | ||||
|       [=, this]() | ||||
|       { | ||||
|          using namespace std::chrono_literals; | ||||
| 
 | ||||
|          auto [newObjects, totalObjects] = | ||||
|             providerManager->provider_->Refresh(); | ||||
| 
 | ||||
|          std::chrono::milliseconds interval = kRetryInterval_; | ||||
|          std::chrono::milliseconds interval = kFastRetryInterval_; | ||||
| 
 | ||||
|          if (totalObjects > 0) | ||||
|          { | ||||
|  | @ -651,12 +654,25 @@ void RadarProductManagerImpl::RefreshData( | |||
| 
 | ||||
|             auto updatePeriod = providerManager->provider_->update_period(); | ||||
|             auto lastModified = providerManager->provider_->last_modified(); | ||||
|             auto sinceLastModified = | ||||
|                std::chrono::system_clock::now() - lastModified; | ||||
| 
 | ||||
|             // For the default interval, assume products are updated at a
 | ||||
|             // constant rate. Expect the next product at a time based on the
 | ||||
|             // previous two.
 | ||||
|             interval = std::chrono::duration_cast<std::chrono::milliseconds>( | ||||
|                updatePeriod - | ||||
|                (std::chrono::system_clock::now() - lastModified)); | ||||
|             if (interval < std::chrono::milliseconds {kRetryInterval_}) | ||||
|                updatePeriod - sinceLastModified); | ||||
| 
 | ||||
|             if (updatePeriod > 0s && sinceLastModified > updatePeriod * 5) | ||||
|             { | ||||
|                interval = kRetryInterval_; | ||||
|                // If it has been at least 5 update periods since the file has
 | ||||
|                // been last modified, slow the retry period
 | ||||
|                interval = kSlowRetryInterval_; | ||||
|             } | ||||
|             else if (interval < std::chrono::milliseconds {kFastRetryInterval_}) | ||||
|             { | ||||
|                // The interval should be no quicker than the fast retry interval
 | ||||
|                interval = kFastRetryInterval_; | ||||
|             } | ||||
| 
 | ||||
|             if (newObjects > 0) | ||||
|  | @ -669,10 +685,10 @@ void RadarProductManagerImpl::RefreshData( | |||
|          } | ||||
|          else if (providerManager->refreshEnabled_) | ||||
|          { | ||||
|             logger_->info("[{}] No data found, disabling refresh", | ||||
|                           providerManager->name()); | ||||
|             logger_->info("[{}] No data found", providerManager->name()); | ||||
| 
 | ||||
|             providerManager->refreshEnabled_ = false; | ||||
|             // If no data is found, retry at the slow retry interval
 | ||||
|             interval = kSlowRetryInterval_; | ||||
|          } | ||||
| 
 | ||||
|          if (providerManager->refreshEnabled_) | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ | |||
| #include <scwx/qt/settings/general_settings.hpp> | ||||
| #include <scwx/qt/settings/map_settings.hpp> | ||||
| #include <scwx/qt/settings/palette_settings.hpp> | ||||
| #include <scwx/qt/settings/product_settings.hpp> | ||||
| #include <scwx/qt/settings/text_settings.hpp> | ||||
| #include <scwx/qt/settings/ui_settings.hpp> | ||||
| #include <scwx/qt/util/json.hpp> | ||||
|  | @ -116,6 +117,7 @@ void SettingsManager::Shutdown() | |||
| 
 | ||||
|    dataChanged |= settings::GeneralSettings::Instance().Shutdown(); | ||||
|    dataChanged |= settings::MapSettings::Instance().Shutdown(); | ||||
|    dataChanged |= settings::ProductSettings::Instance().Shutdown(); | ||||
|    dataChanged |= settings::UiSettings::Instance().Shutdown(); | ||||
| 
 | ||||
|    if (dataChanged) | ||||
|  | @ -132,6 +134,7 @@ boost::json::value SettingsManager::Impl::ConvertSettingsToJson() | |||
|    settings::AudioSettings::Instance().WriteJson(settingsJson); | ||||
|    settings::MapSettings::Instance().WriteJson(settingsJson); | ||||
|    settings::PaletteSettings::Instance().WriteJson(settingsJson); | ||||
|    settings::ProductSettings::Instance().WriteJson(settingsJson); | ||||
|    settings::TextSettings::Instance().WriteJson(settingsJson); | ||||
|    settings::UiSettings::Instance().WriteJson(settingsJson); | ||||
| 
 | ||||
|  | @ -146,6 +149,7 @@ void SettingsManager::Impl::GenerateDefaultSettings() | |||
|    settings::AudioSettings::Instance().SetDefaults(); | ||||
|    settings::MapSettings::Instance().SetDefaults(); | ||||
|    settings::PaletteSettings::Instance().SetDefaults(); | ||||
|    settings::ProductSettings::Instance().SetDefaults(); | ||||
|    settings::TextSettings::Instance().SetDefaults(); | ||||
|    settings::UiSettings::Instance().SetDefaults(); | ||||
| } | ||||
|  | @ -161,6 +165,7 @@ bool SettingsManager::Impl::LoadSettings( | |||
|    jsonDirty |= !settings::AudioSettings::Instance().ReadJson(settingsJson); | ||||
|    jsonDirty |= !settings::MapSettings::Instance().ReadJson(settingsJson); | ||||
|    jsonDirty |= !settings::PaletteSettings::Instance().ReadJson(settingsJson); | ||||
|    jsonDirty |= !settings::ProductSettings::Instance().ReadJson(settingsJson); | ||||
|    jsonDirty |= !settings::TextSettings::Instance().ReadJson(settingsJson); | ||||
|    jsonDirty |= !settings::UiSettings::Instance().ReadJson(settingsJson); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| #include <scwx/qt/map/color_table_layer.hpp> | ||||
| #include <scwx/qt/gl/shader_program.hpp> | ||||
| #include <scwx/qt/view/radar_product_view.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| 
 | ||||
| #if defined(_MSC_VER) | ||||
|  |  | |||
|  | @ -1,4 +1,7 @@ | |||
| #include <scwx/qt/map/map_context.hpp> | ||||
| #include <scwx/qt/map/map_settings.hpp> | ||||
| #include <scwx/qt/view/overlay_product_view.hpp> | ||||
| #include <scwx/qt/view/radar_product_view.hpp> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
|  | @ -11,26 +14,23 @@ class MapContext::Impl | |||
| { | ||||
| public: | ||||
|    explicit Impl(std::shared_ptr<view::RadarProductView> radarProductView) : | ||||
|        map_ {}, | ||||
|        settings_ {}, | ||||
|        pixelRatio_ {1.0f}, | ||||
|        radarProductView_ {radarProductView}, | ||||
|        radarProductGroup_ {common::RadarProductGroup::Unknown}, | ||||
|        radarProduct_ {"???"}, | ||||
|        radarProductCode_ {0} | ||||
|        radarProductView_ {radarProductView} | ||||
|    { | ||||
|    } | ||||
| 
 | ||||
|    ~Impl() {} | ||||
| 
 | ||||
|    std::weak_ptr<QMapLibreGL::Map>          map_; | ||||
|    MapSettings                              settings_; | ||||
|    float                                    pixelRatio_; | ||||
|    std::shared_ptr<view::RadarProductView>  radarProductView_; | ||||
|    common::RadarProductGroup                radarProductGroup_; | ||||
|    std::string                              radarProduct_; | ||||
|    int16_t                                  radarProductCode_; | ||||
|    std::weak_ptr<QMapLibreGL::Map> map_ {}; | ||||
|    MapSettings                     settings_ {}; | ||||
|    float                           pixelRatio_ {1.0f}; | ||||
|    common::RadarProductGroup       radarProductGroup_ { | ||||
|       common::RadarProductGroup::Unknown}; | ||||
|    std::string                              radarProduct_ {"???"}; | ||||
|    int16_t                                  radarProductCode_ {0}; | ||||
|    QMapLibreGL::CustomLayerRenderParameters renderParameters_ {}; | ||||
| 
 | ||||
|    std::shared_ptr<view::OverlayProductView> overlayProductView_ {nullptr}; | ||||
|    std::shared_ptr<view::RadarProductView>   radarProductView_; | ||||
| }; | ||||
| 
 | ||||
| MapContext::MapContext( | ||||
|  | @ -58,6 +58,12 @@ float MapContext::pixel_ratio() const | |||
|    return p->pixelRatio_; | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<view::OverlayProductView> | ||||
| MapContext::overlay_product_view() const | ||||
| { | ||||
|    return p->overlayProductView_; | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<view::RadarProductView> MapContext::radar_product_view() const | ||||
| { | ||||
|    return p->radarProductView_; | ||||
|  | @ -83,18 +89,24 @@ QMapLibreGL::CustomLayerRenderParameters MapContext::render_parameters() const | |||
|    return p->renderParameters_; | ||||
| } | ||||
| 
 | ||||
| void MapContext::set_map(std::shared_ptr<QMapLibreGL::Map> map) | ||||
| void MapContext::set_map(const std::shared_ptr<QMapLibreGL::Map>& map) | ||||
| { | ||||
|    p->map_ = map; | ||||
| } | ||||
| 
 | ||||
| void MapContext::set_overlay_product_view( | ||||
|    const std::shared_ptr<view::OverlayProductView>& overlayProductView) | ||||
| { | ||||
|    p->overlayProductView_ = overlayProductView; | ||||
| } | ||||
| 
 | ||||
| void MapContext::set_pixel_ratio(float pixelRatio) | ||||
| { | ||||
|    p->pixelRatio_ = pixelRatio; | ||||
| } | ||||
| 
 | ||||
| void MapContext::set_radar_product_view( | ||||
|    std::shared_ptr<view::RadarProductView> radarProductView) | ||||
|    const std::shared_ptr<view::RadarProductView>& radarProductView) | ||||
| { | ||||
|    p->radarProductView_ = radarProductView; | ||||
| } | ||||
|  |  | |||
|  | @ -1,8 +1,7 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <scwx/qt/gl/gl_context.hpp> | ||||
| #include <scwx/qt/map/map_settings.hpp> | ||||
| #include <scwx/qt/view/radar_product_view.hpp> | ||||
| #include <scwx/common/products.hpp> | ||||
| 
 | ||||
| #include <QMapLibreGL/QMapLibreGL> | ||||
| 
 | ||||
|  | @ -10,9 +9,19 @@ namespace scwx | |||
| { | ||||
| namespace qt | ||||
| { | ||||
| namespace view | ||||
| { | ||||
| 
 | ||||
| class OverlayProductView; | ||||
| class RadarProductView; | ||||
| 
 | ||||
| } // namespace view
 | ||||
| 
 | ||||
| namespace map | ||||
| { | ||||
| 
 | ||||
| struct MapSettings; | ||||
| 
 | ||||
| class MapContext : public gl::GlContext | ||||
| { | ||||
| public: | ||||
|  | @ -29,16 +38,19 @@ public: | |||
|    std::weak_ptr<QMapLibreGL::Map>           map() const; | ||||
|    MapSettings&                              settings(); | ||||
|    float                                     pixel_ratio() const; | ||||
|    std::shared_ptr<view::OverlayProductView> overlay_product_view() const; | ||||
|    std::shared_ptr<view::RadarProductView>   radar_product_view() const; | ||||
|    common::RadarProductGroup                 radar_product_group() const; | ||||
|    std::string                               radar_product() const; | ||||
|    int16_t                                   radar_product_code() const; | ||||
|    QMapLibreGL::CustomLayerRenderParameters  render_parameters() const; | ||||
| 
 | ||||
|    void set_map(std::shared_ptr<QMapLibreGL::Map> map); | ||||
|    void set_map(const std::shared_ptr<QMapLibreGL::Map>& map); | ||||
|    void set_overlay_product_view( | ||||
|       const std::shared_ptr<view::OverlayProductView>& overlayProductView); | ||||
|    void set_pixel_ratio(float pixelRatio); | ||||
|    void set_radar_product_view( | ||||
|       std::shared_ptr<view::RadarProductView> radarProductView); | ||||
|       const std::shared_ptr<view::RadarProductView>& radarProductView); | ||||
|    void set_radar_product_group(common::RadarProductGroup radarProductGroup); | ||||
|    void set_radar_product(const std::string& radarProduct); | ||||
|    void set_radar_product_code(int16_t radarProductCode); | ||||
|  |  | |||
|  | @ -7,7 +7,9 @@ | |||
| #include <scwx/qt/map/color_table_layer.hpp> | ||||
| #include <scwx/qt/map/layer_wrapper.hpp> | ||||
| #include <scwx/qt/map/map_provider.hpp> | ||||
| #include <scwx/qt/map/map_settings.hpp> | ||||
| #include <scwx/qt/map/overlay_layer.hpp> | ||||
| #include <scwx/qt/map/overlay_product_layer.hpp> | ||||
| #include <scwx/qt/map/placefile_layer.hpp> | ||||
| #include <scwx/qt/map/radar_product_layer.hpp> | ||||
| #include <scwx/qt/map/radar_range_layer.hpp> | ||||
|  | @ -19,6 +21,7 @@ | |||
| #include <scwx/qt/util/file.hpp> | ||||
| #include <scwx/qt/util/maplibre.hpp> | ||||
| #include <scwx/qt/util/tooltip.hpp> | ||||
| #include <scwx/qt/view/overlay_product_view.hpp> | ||||
| #include <scwx/qt/view/radar_product_view_factory.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| #include <scwx/util/time.hpp> | ||||
|  | @ -86,6 +89,14 @@ public: | |||
|        prevBearing_ {0.0}, | ||||
|        prevPitch_ {0.0} | ||||
|    { | ||||
|       // Create views
 | ||||
|       auto overlayProductView = std::make_shared<view::OverlayProductView>(); | ||||
|       overlayProductView->SetAutoRefresh(autoRefreshEnabled_); | ||||
|       overlayProductView->SetAutoUpdate(autoUpdateEnabled_); | ||||
| 
 | ||||
|       // Initialize context
 | ||||
|       context_->set_overlay_product_view(overlayProductView); | ||||
| 
 | ||||
|       auto& generalSettings = settings::GeneralSettings::Instance(); | ||||
| 
 | ||||
|       SetRadarSite(generalSettings.default_radar_site().GetValue()); | ||||
|  | @ -184,6 +195,7 @@ public: | |||
|    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_; | ||||
|    std::shared_ptr<ColorTableLayer>     colorTableLayer_; | ||||
|    std::shared_ptr<RadarSiteLayer>      radarSiteLayer_ {nullptr}; | ||||
|  | @ -439,7 +451,7 @@ std::chrono::system_clock::time_point MapWidget::GetSelectedTime() const | |||
|    if (radarProductView != nullptr) | ||||
|    { | ||||
|       // Select the time associated with the active radar product
 | ||||
|       time = radarProductView->GetSelectedTime(); | ||||
|       time = radarProductView->selected_time(); | ||||
|    } | ||||
| 
 | ||||
|    return time; | ||||
|  | @ -618,6 +630,9 @@ void MapWidget::SelectTime(std::chrono::system_clock::time_point time) | |||
| { | ||||
|    auto radarProductView = p->context_->radar_product_view(); | ||||
| 
 | ||||
|    // Update other views
 | ||||
|    p->context_->overlay_product_view()->SelectTime(time); | ||||
| 
 | ||||
|    // If there is an active radar product view
 | ||||
|    if (radarProductView != nullptr) | ||||
|    { | ||||
|  | @ -651,12 +666,16 @@ void MapWidget::SetAutoRefresh(bool enabled) | |||
|             true, | ||||
|             p->uuid_); | ||||
|       } | ||||
| 
 | ||||
|       p->context_->overlay_product_view()->SetAutoRefresh(enabled); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void MapWidget::SetAutoUpdate(bool enabled) | ||||
| { | ||||
|    p->autoUpdateEnabled_ = enabled; | ||||
| 
 | ||||
|    p->context_->overlay_product_view()->SetAutoUpdate(enabled); | ||||
| } | ||||
| 
 | ||||
| void MapWidget::SetMapLocation(double latitude, | ||||
|  | @ -912,6 +931,16 @@ void MapWidgetImpl::AddLayer(types::LayerType        type, | |||
|    { | ||||
|       switch (std::get<types::DataLayer>(description)) | ||||
|       { | ||||
|       // If there is a radar product view, create the overlay product layer
 | ||||
|       case types::DataLayer::OverlayProduct: | ||||
|          if (radarProductView != nullptr) | ||||
|          { | ||||
|             overlayProductLayer_ = | ||||
|                std::make_shared<OverlayProductLayer>(context_); | ||||
|             AddLayer(layerName, overlayProductLayer_, before); | ||||
|          } | ||||
|          break; | ||||
| 
 | ||||
|       // If there is a radar product view, create the radar range layer
 | ||||
|       case types::DataLayer::RadarRange: | ||||
|          if (radarProductView != nullptr) | ||||
|  | @ -1492,6 +1521,10 @@ void MapWidgetImpl::SetRadarSite(const std::string& radarSite) | |||
|       // Set new RadarProductManager
 | ||||
|       radarProductManager_ = manager::RadarProductManager::Instance(radarSite); | ||||
| 
 | ||||
|       // Update views
 | ||||
|       context_->overlay_product_view()->set_radar_product_manager( | ||||
|          radarProductManager_); | ||||
| 
 | ||||
|       // Connect signals to new RadarProductManager
 | ||||
|       RadarProductManagerConnect(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,7 +3,9 @@ | |||
| #include <scwx/qt/gl/draw/icons.hpp> | ||||
| #include <scwx/qt/gl/draw/rectangle.hpp> | ||||
| #include <scwx/qt/manager/position_manager.hpp> | ||||
| #include <scwx/qt/map/map_settings.hpp> | ||||
| #include <scwx/qt/types/texture_types.hpp> | ||||
| #include <scwx/qt/view/radar_product_view.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| #include <scwx/util/time.hpp> | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										443
									
								
								scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										443
									
								
								scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,443 @@ | |||
| #include <scwx/qt/map/overlay_product_layer.hpp> | ||||
| #include <scwx/qt/gl/draw/linked_vectors.hpp> | ||||
| #include <scwx/qt/manager/radar_product_manager.hpp> | ||||
| #include <scwx/qt/settings/product_settings.hpp> | ||||
| #include <scwx/qt/view/overlay_product_view.hpp> | ||||
| #include <scwx/wsr88d/rpg/linked_vector_packet.hpp> | ||||
| #include <scwx/wsr88d/rpg/rpg_types.hpp> | ||||
| #include <scwx/wsr88d/rpg/scit_data_packet.hpp> | ||||
| #include <scwx/wsr88d/rpg/storm_id_symbol_packet.hpp> | ||||
| #include <scwx/wsr88d/rpg/storm_tracking_information_message.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| #include <scwx/util/time.hpp> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace qt | ||||
| { | ||||
| namespace map | ||||
| { | ||||
| 
 | ||||
| static const std::string logPrefix_ = "scwx::qt::map::overlay_product_layer"; | ||||
| static const auto        logger_    = scwx::util::Logger::Create(logPrefix_); | ||||
| 
 | ||||
| class OverlayProductLayer::Impl | ||||
| { | ||||
| public: | ||||
|    explicit Impl(OverlayProductLayer*               self, | ||||
|                  const std::shared_ptr<MapContext>& context) : | ||||
|        self_ {self}, | ||||
|        linkedVectors_ {std::make_shared<gl::draw::LinkedVectors>(context)} | ||||
|    { | ||||
|       auto& productSettings = settings::ProductSettings::Instance(); | ||||
| 
 | ||||
|       productSettings.sti_forecast_enabled().RegisterValueStagedCallback( | ||||
|          [=, this](const bool& value) | ||||
|          { | ||||
|             stiForecastEnabled_ = value; | ||||
|             stiNeedsUpdate_     = true; | ||||
| 
 | ||||
|             Q_EMIT self_->NeedsRendering(); | ||||
|          }); | ||||
|       productSettings.sti_past_enabled().RegisterValueStagedCallback( | ||||
|          [=, this](const bool& value) | ||||
|          { | ||||
|             stiPastEnabled_ = value; | ||||
|             stiNeedsUpdate_ = true; | ||||
| 
 | ||||
|             Q_EMIT self_->NeedsRendering(); | ||||
|          }); | ||||
| 
 | ||||
|       stiForecastEnabled_ = | ||||
|          productSettings.sti_forecast_enabled().GetStagedOrValue(); | ||||
|       stiPastEnabled_ = productSettings.sti_past_enabled().GetStagedOrValue(); | ||||
|    } | ||||
|    ~Impl() = default; | ||||
| 
 | ||||
|    void UpdateStormTrackingInformation(); | ||||
| 
 | ||||
|    static void HandleLinkedVectorPacket( | ||||
|       const std::shared_ptr<const wsr88d::rpg::Packet>& packet, | ||||
|       const common::Coordinate&                         center, | ||||
|       const std::string&                                hoverText, | ||||
|       boost::gil::rgba32f_pixel_t                       color, | ||||
|       units::length::nautical_miles<float>              tickRadius, | ||||
|       units::length::nautical_miles<float>              tickRadiusIncrement, | ||||
|       std::shared_ptr<gl::draw::LinkedVectors>&         linkedVectors); | ||||
|    void HandleScitDataPacket( | ||||
|       const std::shared_ptr<const wsr88d::rpg::StormTrackingInformationMessage>& | ||||
|                                                         sti, | ||||
|       const std::shared_ptr<const wsr88d::rpg::Packet>& packet, | ||||
|       const common::Coordinate&                         center, | ||||
|       const std::string&                                stormId, | ||||
|       const std::string&                                hoverText, | ||||
|       std::shared_ptr<gl::draw::LinkedVectors>&         linkedVectors); | ||||
| 
 | ||||
|    static void HandleStormIdPacket( | ||||
|       const std::shared_ptr<const wsr88d::rpg::StormTrackingInformationMessage>& | ||||
|                                                         sti, | ||||
|       const std::shared_ptr<const wsr88d::rpg::Packet>& packet, | ||||
|       std::string&                                      stormId, | ||||
|       std::string&                                      hoverText); | ||||
| 
 | ||||
|    static std::string BuildHoverText( | ||||
|       const std::shared_ptr< | ||||
|          const scwx::wsr88d::rpg::StormTrackingInformationMessage>& sti, | ||||
|       std::string&                                                  stormId); | ||||
| 
 | ||||
|    OverlayProductLayer* self_; | ||||
| 
 | ||||
|    bool stiForecastEnabled_ {true}; | ||||
|    bool stiPastEnabled_ {true}; | ||||
| 
 | ||||
|    bool stiNeedsUpdate_ {false}; | ||||
| 
 | ||||
|    std::shared_ptr<gl::draw::LinkedVectors> linkedVectors_; | ||||
| }; | ||||
| 
 | ||||
| OverlayProductLayer::OverlayProductLayer(std::shared_ptr<MapContext> context) : | ||||
|     DrawLayer(context), p(std::make_unique<Impl>(this, context)) | ||||
| { | ||||
|    auto overlayProductView = context->overlay_product_view(); | ||||
|    connect(overlayProductView.get(), | ||||
|            &view::OverlayProductView::ProductUpdated, | ||||
|            this, | ||||
|            [this](std::string product) | ||||
|            { | ||||
|               if (product == "NST") | ||||
|               { | ||||
|                  p->stiNeedsUpdate_ = true; | ||||
|                  Q_EMIT NeedsRendering(); | ||||
|               } | ||||
|            }); | ||||
| 
 | ||||
|    AddDrawItem(p->linkedVectors_); | ||||
| } | ||||
| 
 | ||||
| OverlayProductLayer::~OverlayProductLayer() = default; | ||||
| 
 | ||||
| void OverlayProductLayer::Initialize() | ||||
| { | ||||
|    logger_->debug("Initialize()"); | ||||
| 
 | ||||
|    p->UpdateStormTrackingInformation(); | ||||
| 
 | ||||
|    DrawLayer::Initialize(); | ||||
| } | ||||
| 
 | ||||
| void OverlayProductLayer::Render( | ||||
|    const QMapLibreGL::CustomLayerRenderParameters& params) | ||||
| { | ||||
|    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(); | ||||
|    } | ||||
| 
 | ||||
|    DrawLayer::Render(params); | ||||
| 
 | ||||
|    SCWX_GL_CHECK_ERROR(); | ||||
| } | ||||
| 
 | ||||
| void OverlayProductLayer::Deinitialize() | ||||
| { | ||||
|    logger_->debug("Deinitialize()"); | ||||
| 
 | ||||
|    DrawLayer::Deinitialize(); | ||||
| } | ||||
| 
 | ||||
| void OverlayProductLayer::Impl::UpdateStormTrackingInformation() | ||||
| { | ||||
|    logger_->debug("Update Storm Tracking Information"); | ||||
| 
 | ||||
|    stiNeedsUpdate_ = false; | ||||
| 
 | ||||
|    auto overlayProductView  = self_->context()->overlay_product_view(); | ||||
|    auto radarProductManager = overlayProductView->radar_product_manager(); | ||||
|    auto message             = overlayProductView->radar_product_message("NST"); | ||||
| 
 | ||||
|    float latitude  = 0.0f; | ||||
|    float longitude = 0.0f; | ||||
| 
 | ||||
|    std::shared_ptr<wsr88d::rpg::StormTrackingInformationMessage> sti = nullptr; | ||||
|    std::shared_ptr<wsr88d::rpg::ProductSymbologyBlock>           psb = nullptr; | ||||
|    if (message != nullptr) | ||||
|    { | ||||
|       sti = std::dynamic_pointer_cast< | ||||
|          wsr88d::rpg::StormTrackingInformationMessage>(message); | ||||
|    } | ||||
|    if (sti != nullptr) | ||||
|    { | ||||
|       psb = sti->symbology_block(); | ||||
|    } | ||||
| 
 | ||||
|    linkedVectors_->StartVectors(); | ||||
| 
 | ||||
|    if (psb != nullptr) | ||||
|    { | ||||
|       std::shared_ptr<config::RadarSite> radarSite = nullptr; | ||||
|       if (radarProductManager != nullptr) | ||||
|       { | ||||
|          radarSite = radarProductManager->radar_site(); | ||||
|       } | ||||
|       if (radarSite != nullptr) | ||||
|       { | ||||
|          latitude  = radarSite->latitude(); | ||||
|          longitude = radarSite->longitude(); | ||||
|       } | ||||
| 
 | ||||
|       std::string stormId = "?"; | ||||
|       std::string hoverText {}; | ||||
| 
 | ||||
|       for (std::size_t i = 0; i < psb->number_of_layers(); ++i) | ||||
|       { | ||||
|          auto packetList = psb->packet_list(static_cast<std::uint16_t>(i)); | ||||
|          for (auto& packet : packetList) | ||||
|          { | ||||
|             switch (packet->packet_code()) | ||||
|             { | ||||
|             case static_cast<std::uint16_t>(wsr88d::rpg::PacketCode::StormId): | ||||
|                HandleStormIdPacket(sti, packet, stormId, hoverText); | ||||
|                break; | ||||
| 
 | ||||
|             case static_cast<std::uint16_t>( | ||||
|                wsr88d::rpg::PacketCode::ScitPastData): | ||||
|             case static_cast<std::uint16_t>( | ||||
|                wsr88d::rpg::PacketCode::ScitForecastData): | ||||
|                HandleScitDataPacket(sti, | ||||
|                                     packet, | ||||
|                                     {latitude, longitude}, | ||||
|                                     stormId, | ||||
|                                     hoverText, | ||||
|                                     linkedVectors_); | ||||
|                break; | ||||
| 
 | ||||
|             default: | ||||
|                logger_->trace("Ignoring packet type: {}", | ||||
|                               packet->packet_code()); | ||||
|                break; | ||||
|             } | ||||
|          } | ||||
|       } | ||||
|    } | ||||
|    else | ||||
|    { | ||||
|       logger_->trace("No Storm Tracking Information found"); | ||||
|    } | ||||
| 
 | ||||
|    linkedVectors_->FinishVectors(); | ||||
| } | ||||
| 
 | ||||
| void OverlayProductLayer::Impl::HandleStormIdPacket( | ||||
|    const std::shared_ptr<const wsr88d::rpg::StormTrackingInformationMessage>& | ||||
|                                                      sti, | ||||
|    const std::shared_ptr<const wsr88d::rpg::Packet>& packet, | ||||
|    std::string&                                      stormId, | ||||
|    std::string&                                      hoverText) | ||||
| { | ||||
|    auto stormIdPacket = | ||||
|       std::dynamic_pointer_cast<const wsr88d::rpg::StormIdSymbolPacket>(packet); | ||||
| 
 | ||||
|    if (stormIdPacket != nullptr && stormIdPacket->RecordCount() > 0) | ||||
|    { | ||||
|       stormId   = stormIdPacket->storm_id(0); | ||||
|       hoverText = BuildHoverText(sti, stormId); | ||||
|    } | ||||
|    else | ||||
|    { | ||||
|       logger_->warn("Invalid Storm ID Packet"); | ||||
| 
 | ||||
|       stormId = "?"; | ||||
|       hoverText.clear(); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void OverlayProductLayer::Impl::HandleScitDataPacket( | ||||
|    const std::shared_ptr<const wsr88d::rpg::StormTrackingInformationMessage>& | ||||
|                                                      sti, | ||||
|    const std::shared_ptr<const wsr88d::rpg::Packet>& packet, | ||||
|    const common::Coordinate&                         center, | ||||
|    const std::string&                                stormId, | ||||
|    const std::string&                                hoverText, | ||||
|    std::shared_ptr<gl::draw::LinkedVectors>&         linkedVectors) | ||||
| { | ||||
|    auto scitDataPacket = | ||||
|       std::dynamic_pointer_cast<const wsr88d::rpg::ScitDataPacket>(packet); | ||||
| 
 | ||||
|    if (scitDataPacket != nullptr) | ||||
|    { | ||||
|       boost::gil::rgba32f_pixel_t color {1.0f, 1.0f, 1.0f, 1.0f}; | ||||
| 
 | ||||
|       units::length::nautical_miles<float> tickRadius {0.5f}; | ||||
|       units::length::nautical_miles<float> tickRadiusIncrement {0.0f}; | ||||
| 
 | ||||
|       auto stiRecord = sti->sti_record(stormId); | ||||
| 
 | ||||
|       if (scitDataPacket->packet_code() == | ||||
|           static_cast<std::uint16_t>(wsr88d::rpg::PacketCode::ScitPastData)) | ||||
|       { | ||||
|          if (!stiPastEnabled_) | ||||
|          { | ||||
|             return; | ||||
|          } | ||||
| 
 | ||||
|          // If this is past data, the default tick radius and increment with a
 | ||||
|          // darker color
 | ||||
|          color = {0.5f, 0.5f, 0.5f, 1.0f}; | ||||
|       } | ||||
|       else | ||||
|       { | ||||
|          if (!stiForecastEnabled_) | ||||
|          { | ||||
|             return; | ||||
|          } | ||||
| 
 | ||||
|          if (stiRecord != nullptr && stiRecord->meanError_.has_value()) | ||||
|          { | ||||
|             // If this is forecast data, use the mean error as the radius
 | ||||
|             // (minimum of the default value), incrementing by the mean error
 | ||||
|             tickRadiusIncrement = stiRecord->meanError_.value(); | ||||
|             tickRadius          = std::max(tickRadius, tickRadiusIncrement); | ||||
|          } | ||||
|       } | ||||
| 
 | ||||
|       for (auto& subpacket : scitDataPacket->packet_list()) | ||||
|       { | ||||
|          switch (subpacket->packet_code()) | ||||
|          { | ||||
|          case static_cast<std::uint16_t>( | ||||
|             wsr88d::rpg::PacketCode::LinkedVectorNoValue): | ||||
|             HandleLinkedVectorPacket(subpacket, | ||||
|                                      center, | ||||
|                                      hoverText, | ||||
|                                      color, | ||||
|                                      tickRadius, | ||||
|                                      tickRadiusIncrement, | ||||
|                                      linkedVectors); | ||||
|             break; | ||||
| 
 | ||||
|          default: | ||||
|             logger_->trace("Ignoring SCIT subpacket type: {}", | ||||
|                            subpacket->packet_code()); | ||||
|             break; | ||||
|          } | ||||
|       } | ||||
|    } | ||||
|    else | ||||
|    { | ||||
|       logger_->warn("Invalid SCIT Data Packet"); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void OverlayProductLayer::Impl::HandleLinkedVectorPacket( | ||||
|    const std::shared_ptr<const wsr88d::rpg::Packet>& packet, | ||||
|    const common::Coordinate&                         center, | ||||
|    const std::string&                                hoverText, | ||||
|    boost::gil::rgba32f_pixel_t                       color, | ||||
|    units::length::nautical_miles<float>              tickRadius, | ||||
|    units::length::nautical_miles<float>              tickRadiusIncrement, | ||||
|    std::shared_ptr<gl::draw::LinkedVectors>&         linkedVectors) | ||||
| { | ||||
|    auto linkedVectorPacket = | ||||
|       std::dynamic_pointer_cast<const wsr88d::rpg::LinkedVectorPacket>(packet); | ||||
| 
 | ||||
|    if (linkedVectorPacket != nullptr) | ||||
|    { | ||||
|       auto di = linkedVectors->AddVector(center, linkedVectorPacket); | ||||
|       gl::draw::LinkedVectors::SetVectorWidth(di, 1.0f); | ||||
|       gl::draw::LinkedVectors::SetVectorModulate(di, color); | ||||
|       gl::draw::LinkedVectors::SetVectorHoverText(di, hoverText); | ||||
|       gl::draw::LinkedVectors::SetVectorTicksEnabled(di, true); | ||||
|       gl::draw::LinkedVectors::SetVectorTickRadius(di, tickRadius); | ||||
|       gl::draw::LinkedVectors::SetVectorTickRadiusIncrement( | ||||
|          di, tickRadiusIncrement); | ||||
|    } | ||||
|    else | ||||
|    { | ||||
|       logger_->warn("Invalid Linked Vector Packet"); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| std::string OverlayProductLayer::Impl::BuildHoverText( | ||||
|    const std::shared_ptr< | ||||
|       const scwx::wsr88d::rpg::StormTrackingInformationMessage>& sti, | ||||
|    std::string&                                                  stormId) | ||||
| { | ||||
|    std::string hoverText = fmt::format("Storm ID: {}", stormId); | ||||
| 
 | ||||
|    auto stiRecord = sti->sti_record(stormId); | ||||
| 
 | ||||
|    if (stiRecord != nullptr) | ||||
|    { | ||||
|       if (stiRecord->direction_.has_value() && stiRecord->speed_.has_value()) | ||||
|       { | ||||
|          hoverText += | ||||
|             fmt::format("\nMovement: {} @ {}", | ||||
|                         units::to_string(stiRecord->direction_.value()), | ||||
|                         units::to_string(stiRecord->speed_.value())); | ||||
|       } | ||||
| 
 | ||||
|       if (stiRecord->maxDbz_.has_value() && | ||||
|           stiRecord->maxDbzHeight_.has_value()) | ||||
|       { | ||||
|          hoverText += | ||||
|             fmt::format("\nMax dBZ: {} ({} kft)", | ||||
|                         stiRecord->maxDbz_.value(), | ||||
|                         stiRecord->maxDbzHeight_.value().value() / 1000.0f); | ||||
|       } | ||||
| 
 | ||||
|       if (stiRecord->forecastError_.has_value()) | ||||
|       { | ||||
|          hoverText += | ||||
|             fmt::format("\nForecast Error: {}", | ||||
|                         units::to_string(stiRecord->forecastError_.value())); | ||||
|       } | ||||
| 
 | ||||
|       if (stiRecord->meanError_.has_value()) | ||||
|       { | ||||
|          hoverText += | ||||
|             fmt::format("\nMean Error: {}", | ||||
|                         units::to_string(stiRecord->meanError_.value())); | ||||
|       } | ||||
|    } | ||||
| 
 | ||||
|    auto dateTime = sti->date_time(); | ||||
|    if (dateTime.has_value()) | ||||
|    { | ||||
|       hoverText += | ||||
|          fmt::format("\nDate/Time: {}", util::TimeString(dateTime.value())); | ||||
|    } | ||||
| 
 | ||||
|    auto forecastInterval = sti->forecast_interval(); | ||||
|    if (forecastInterval.has_value()) | ||||
|    { | ||||
|       hoverText += fmt::format("\nForecast Interval: {} min", | ||||
|                                forecastInterval.value().count()); | ||||
|    } | ||||
| 
 | ||||
|    return hoverText; | ||||
| } | ||||
| 
 | ||||
| bool OverlayProductLayer::RunMousePicking( | ||||
|    const QMapLibreGL::CustomLayerRenderParameters& params, | ||||
|    const QPointF&                                  mouseLocalPos, | ||||
|    const QPointF&                                  mouseGlobalPos, | ||||
|    const glm::vec2&                                mouseCoords, | ||||
|    const common::Coordinate&                       mouseGeoCoords, | ||||
|    std::shared_ptr<types::EventHandler>&           eventHandler) | ||||
| { | ||||
|    return DrawLayer::RunMousePicking(params, | ||||
|                                      mouseLocalPos, | ||||
|                                      mouseGlobalPos, | ||||
|                                      mouseCoords, | ||||
|                                      mouseGeoCoords, | ||||
|                                      eventHandler); | ||||
| } | ||||
| 
 | ||||
| } // namespace map
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
							
								
								
									
										37
									
								
								scwx-qt/source/scwx/qt/map/overlay_product_layer.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								scwx-qt/source/scwx/qt/map/overlay_product_layer.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <scwx/qt/map/draw_layer.hpp> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace qt | ||||
| { | ||||
| namespace map | ||||
| { | ||||
| 
 | ||||
| class OverlayProductLayer : public DrawLayer | ||||
| { | ||||
| public: | ||||
|    explicit OverlayProductLayer(std::shared_ptr<MapContext> context); | ||||
|    ~OverlayProductLayer(); | ||||
| 
 | ||||
|    void Initialize() override final; | ||||
|    void Render(const QMapLibreGL::CustomLayerRenderParameters&) override final; | ||||
|    void Deinitialize() override final; | ||||
| 
 | ||||
|    bool RunMousePicking( | ||||
|       const QMapLibreGL::CustomLayerRenderParameters& params, | ||||
|       const QPointF&                                  mouseLocalPos, | ||||
|       const QPointF&                                  mouseGlobalPos, | ||||
|       const glm::vec2&                                mouseCoords, | ||||
|       const common::Coordinate&                       mouseGeoCoords, | ||||
|       std::shared_ptr<types::EventHandler>& eventHandler) override final; | ||||
| 
 | ||||
| private: | ||||
|    class Impl; | ||||
|    std::unique_ptr<Impl> p; | ||||
| }; | ||||
| 
 | ||||
| } // namespace map
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
|  | @ -2,6 +2,7 @@ | |||
| #include <scwx/qt/gl/shader_program.hpp> | ||||
| #include <scwx/qt/util/maplibre.hpp> | ||||
| #include <scwx/qt/util/tooltip.hpp> | ||||
| #include <scwx/qt/view/radar_product_view.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| 
 | ||||
| #include <execution> | ||||
|  |  | |||
|  | @ -50,6 +50,7 @@ static const std::vector<types::LayerInfo> kDefaultLayers_ { | |||
|    {types::LayerType::Alert, awips::Phenomenon::FlashFlood, true}, | ||||
|    {types::LayerType::Alert, awips::Phenomenon::Marine, true}, | ||||
|    {types::LayerType::Map, types::MapLayer::MapSymbology, false}, | ||||
|    {types::LayerType::Data, types::DataLayer::OverlayProduct, true}, | ||||
|    {types::LayerType::Radar, std::monostate {}, true}, | ||||
|    {types::LayerType::Map, types::MapLayer::MapUnderlay, false}, | ||||
| }; | ||||
|  | @ -243,9 +244,6 @@ void LayerModel::Impl::ValidateLayerSettings(types::LayerVector& layers) | |||
| 
 | ||||
|    // Validate immovable layers
 | ||||
|    std::vector<types::LayerVector::iterator> immovableIterators {}; | ||||
|    types::LayerVector::iterator              radarSiteIterator {}; | ||||
|    types::LayerVector::iterator              mapSymbologyIterator {}; | ||||
|    types::LayerVector::iterator              mapUnderlayIterator {}; | ||||
|    for (auto& immovableLayer : kImmovableLayers_) | ||||
|    { | ||||
|       // Set the default displayed state for a layer that is not found
 | ||||
|  | @ -288,102 +286,35 @@ void LayerModel::Impl::ValidateLayerSettings(types::LayerVector& layers) | |||
|          it->displayed_ = displayed; | ||||
|       } | ||||
| 
 | ||||
|       // Store positional iterators
 | ||||
|       if (it->type_ == types::LayerType::Information) | ||||
|       { | ||||
|          switch (std::get<types::InformationLayer>(it->description_)) | ||||
|          { | ||||
|          case types::InformationLayer::RadarSite: | ||||
|             radarSiteIterator = it; | ||||
|             break; | ||||
| 
 | ||||
|          default: | ||||
|             break; | ||||
|          } | ||||
|       } | ||||
|       else if (it->type_ == types::LayerType::Map) | ||||
|       { | ||||
|          switch (std::get<types::MapLayer>(it->description_)) | ||||
|          { | ||||
|          case types::MapLayer::MapSymbology: | ||||
|             mapSymbologyIterator = it; | ||||
|             break; | ||||
| 
 | ||||
|          case types::MapLayer::MapUnderlay: | ||||
|             mapUnderlayIterator = it; | ||||
|             break; | ||||
| 
 | ||||
|          default: | ||||
|             break; | ||||
|          } | ||||
|       } | ||||
| 
 | ||||
|       // Add the immovable iterator to the list
 | ||||
|       immovableIterators.push_back(it); | ||||
|    } | ||||
| 
 | ||||
|    // Validate data layers
 | ||||
|    std::vector<types::LayerVector::iterator> dataIterators {}; | ||||
|    for (const auto& dataLayer : types::DataLayerIterator()) | ||||
|    // Validate the remainder of the default layer list
 | ||||
|    auto previousLayer = layers.end(); | ||||
|    for (auto defaultIt = kDefaultLayers_.rbegin(); | ||||
|         defaultIt != kDefaultLayers_.rend(); | ||||
|         ++defaultIt) | ||||
|    { | ||||
|       // Find the data layer
 | ||||
|       auto it = std::find_if(layers.begin(), | ||||
|       // Find the default layer in the current layer list
 | ||||
|       auto currentIt = | ||||
|          std::find_if(layers.begin(), | ||||
|                       layers.end(), | ||||
|                              [&dataLayer](const types::LayerInfo& layer) | ||||
|                       [&defaultIt](const types::LayerInfo& layer) | ||||
|                       { | ||||
|                                 return layer.type_ == types::LayerType::Data && | ||||
|                                        std::get<types::DataLayer>( | ||||
|                                           layer.description_) == dataLayer; | ||||
|                          return layer.type_ == defaultIt->type_ && | ||||
|                                 layer.description_ == defaultIt->description_; | ||||
|                       }); | ||||
| 
 | ||||
|       if (it == layers.end()) | ||||
|       // If the default layer was not found in the current layer list
 | ||||
|       if (currentIt == layers.end()) | ||||
|       { | ||||
|          // If this is the first data layer, insert after the radar site layer,
 | ||||
|          // otherwise, insert after the previous data layer
 | ||||
|          types::LayerVector::iterator insertPosition = | ||||
|             dataIterators.empty() ? radarSiteIterator + 1 : | ||||
|                                     dataIterators.back() + 1; | ||||
|          it = | ||||
|             layers.insert(insertPosition, {types::LayerType::Data, dataLayer}); | ||||
|          // Insert before the previously found layer
 | ||||
|          currentIt = layers.insert(previousLayer, *defaultIt); | ||||
|       } | ||||
| 
 | ||||
|       dataIterators.push_back(it); | ||||
|    } | ||||
| 
 | ||||
|    // Validate alert layers
 | ||||
|    std::vector<types::LayerVector::iterator> alertIterators {}; | ||||
|    for (auto& phenomenon : kAlertPhenomena_) | ||||
|    { | ||||
|       // Find the alert layer
 | ||||
|       auto it = std::find_if(layers.begin(), | ||||
|                              layers.end(), | ||||
|                              [&phenomenon](const types::LayerInfo& layer) | ||||
|                              { | ||||
|                                 return layer.type_ == types::LayerType::Alert && | ||||
|                                        std::get<awips::Phenomenon>( | ||||
|                                           layer.description_) == phenomenon; | ||||
|                              }); | ||||
| 
 | ||||
|       if (it == layers.end()) | ||||
|       { | ||||
|          // Insert before the map symbology layer
 | ||||
|          it = layers.insert(mapSymbologyIterator, | ||||
|                             {types::LayerType::Alert, phenomenon}); | ||||
|       } | ||||
| 
 | ||||
|       alertIterators.push_back(it); | ||||
|    } | ||||
| 
 | ||||
|    // Validate the radar layer
 | ||||
|    auto it = std::find_if(layers.begin(), | ||||
|                           layers.end(), | ||||
|                           [](const types::LayerInfo& layer) | ||||
|                           { return layer.type_ == types::LayerType::Radar; }); | ||||
|    if (it == layers.end()) | ||||
|    { | ||||
|       // Insert before the map underlay layer
 | ||||
|       it = layers.insert(mapUnderlayIterator, | ||||
|                          {types::LayerType::Radar, std::monostate {}}); | ||||
|       // Store the current layer as the previous
 | ||||
|       previousLayer = currentIt; | ||||
|    } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										75
									
								
								scwx-qt/source/scwx/qt/settings/product_settings.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								scwx-qt/source/scwx/qt/settings/product_settings.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,75 @@ | |||
| #include <scwx/qt/settings/product_settings.hpp> | ||||
| #include <scwx/qt/settings/settings_container.hpp> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace qt | ||||
| { | ||||
| namespace settings | ||||
| { | ||||
| 
 | ||||
| static const std::string logPrefix_ = "scwx::qt::settings::product_settings"; | ||||
| 
 | ||||
| class ProductSettings::Impl | ||||
| { | ||||
| public: | ||||
|    explicit Impl() | ||||
|    { | ||||
|       stiForecastEnabled_.SetDefault(true); | ||||
|       stiPastEnabled_.SetDefault(true); | ||||
|    } | ||||
| 
 | ||||
|    ~Impl() {} | ||||
| 
 | ||||
|    SettingsVariable<bool> stiForecastEnabled_ {"sti_forecast_enabled"}; | ||||
|    SettingsVariable<bool> stiPastEnabled_ {"sti_past_enabled"}; | ||||
| }; | ||||
| 
 | ||||
| ProductSettings::ProductSettings() : | ||||
|     SettingsCategory("product"), p(std::make_unique<Impl>()) | ||||
| { | ||||
|    RegisterVariables({&p->stiForecastEnabled_, &p->stiPastEnabled_}); | ||||
|    SetDefaults(); | ||||
| } | ||||
| ProductSettings::~ProductSettings() = default; | ||||
| 
 | ||||
| ProductSettings::ProductSettings(ProductSettings&&) noexcept = default; | ||||
| ProductSettings& | ||||
| ProductSettings::operator=(ProductSettings&&) noexcept = default; | ||||
| 
 | ||||
| SettingsVariable<bool>& ProductSettings::sti_forecast_enabled() const | ||||
| { | ||||
|    return p->stiForecastEnabled_; | ||||
| } | ||||
| 
 | ||||
| SettingsVariable<bool>& ProductSettings::sti_past_enabled() const | ||||
| { | ||||
|    return p->stiPastEnabled_; | ||||
| } | ||||
| 
 | ||||
| bool ProductSettings::Shutdown() | ||||
| { | ||||
|    bool dataChanged = false; | ||||
| 
 | ||||
|    // Commit settings that are managed separate from the settings dialog
 | ||||
|    dataChanged |= p->stiForecastEnabled_.Commit(); | ||||
|    dataChanged |= p->stiPastEnabled_.Commit(); | ||||
| 
 | ||||
|    return dataChanged; | ||||
| } | ||||
| 
 | ||||
| ProductSettings& ProductSettings::Instance() | ||||
| { | ||||
|    static ProductSettings generalSettings_; | ||||
|    return generalSettings_; | ||||
| } | ||||
| 
 | ||||
| bool operator==(const ProductSettings& lhs, const ProductSettings& rhs) | ||||
| { | ||||
|    return (lhs.p->stiForecastEnabled_ == rhs.p->stiForecastEnabled_ && | ||||
|            lhs.p->stiPastEnabled_ == rhs.p->stiPastEnabled_); | ||||
| } | ||||
| 
 | ||||
| } // namespace settings
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
							
								
								
									
										45
									
								
								scwx-qt/source/scwx/qt/settings/product_settings.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								scwx-qt/source/scwx/qt/settings/product_settings.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <scwx/qt/settings/settings_category.hpp> | ||||
| #include <scwx/qt/settings/settings_variable.hpp> | ||||
| 
 | ||||
| #include <memory> | ||||
| #include <string> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace qt | ||||
| { | ||||
| namespace settings | ||||
| { | ||||
| 
 | ||||
| class ProductSettings : public SettingsCategory | ||||
| { | ||||
| public: | ||||
|    explicit ProductSettings(); | ||||
|    ~ProductSettings(); | ||||
| 
 | ||||
|    ProductSettings(const ProductSettings&)            = delete; | ||||
|    ProductSettings& operator=(const ProductSettings&) = delete; | ||||
| 
 | ||||
|    ProductSettings(ProductSettings&&) noexcept; | ||||
|    ProductSettings& operator=(ProductSettings&&) noexcept; | ||||
| 
 | ||||
|    SettingsVariable<bool>& sti_forecast_enabled() const; | ||||
|    SettingsVariable<bool>& sti_past_enabled() const; | ||||
| 
 | ||||
|    static ProductSettings& Instance(); | ||||
| 
 | ||||
|    friend bool operator==(const ProductSettings& lhs, | ||||
|                           const ProductSettings& rhs); | ||||
| 
 | ||||
|    bool Shutdown(); | ||||
| 
 | ||||
| private: | ||||
|    class Impl; | ||||
|    std::unique_ptr<Impl> p; | ||||
| }; | ||||
| 
 | ||||
| } // namespace settings
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
|  | @ -23,7 +23,9 @@ static const std::unordered_map<LayerType, std::string> layerTypeName_ { | |||
|    {LayerType::Unknown, "?"}}; | ||||
| 
 | ||||
| static const std::unordered_map<DataLayer, std::string> dataLayerName_ { | ||||
|    {DataLayer::RadarRange, "Radar Range"}, {DataLayer::Unknown, "?"}}; | ||||
|    {DataLayer::OverlayProduct, "Overlay Product"}, | ||||
|    {DataLayer::RadarRange, "Radar Range"}, | ||||
|    {DataLayer::Unknown, "?"}}; | ||||
| 
 | ||||
| static const std::unordered_map<InformationLayer, std::string> | ||||
|    informationLayerName_ {{InformationLayer::MapOverlay, "Map Overlay"}, | ||||
|  |  | |||
|  | @ -31,11 +31,12 @@ enum class LayerType | |||
| 
 | ||||
| enum class DataLayer | ||||
| { | ||||
|    OverlayProduct, | ||||
|    RadarRange, | ||||
|    Unknown | ||||
| }; | ||||
| typedef scwx::util:: | ||||
|    Iterator<DataLayer, DataLayer::RadarRange, DataLayer::RadarRange> | ||||
|    Iterator<DataLayer, DataLayer::OverlayProduct, DataLayer::RadarRange> | ||||
|       DataLayerIterator; | ||||
| 
 | ||||
| enum class InformationLayer | ||||
|  |  | |||
|  | @ -1,12 +1,16 @@ | |||
| #include <scwx/qt/ui/level3_products_widget.hpp> | ||||
| #include <scwx/qt/ui/flow_layout.hpp> | ||||
| #include <scwx/qt/settings/product_settings.hpp> | ||||
| #include <scwx/qt/settings/settings_interface.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| 
 | ||||
| #include <execution> | ||||
| #include <shared_mutex> | ||||
| 
 | ||||
| #include <QCheckBox> | ||||
| #include <QMenu> | ||||
| #include <QToolButton> | ||||
| #include <QVBoxLayout> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
|  | @ -25,13 +29,17 @@ class Level3ProductsWidgetImpl : public QObject | |||
| public: | ||||
|    explicit Level3ProductsWidgetImpl(Level3ProductsWidget* self) : | ||||
|        self_ {self}, | ||||
|        layout_ {new ui::FlowLayout(self)}, | ||||
|        layout_ {new QVBoxLayout(self)}, | ||||
|        productsWidget_ {new QWidget(self)}, | ||||
|        productsLayout_ {new ui::FlowLayout(productsWidget_)}, | ||||
|        categoryButtons_ {}, | ||||
|        productTiltMap_ {}, | ||||
|        awipsProductMap_ {}, | ||||
|        awipsProductMutex_ {} | ||||
|    { | ||||
|       layout_->setContentsMargins(0, 0, 0, 0); | ||||
|       layout_->addWidget(productsWidget_); | ||||
|       productsLayout_->setContentsMargins(0, 0, 0, 0); | ||||
| 
 | ||||
|       for (common::Level3ProductCategory category : | ||||
|            common::Level3ProductCategoryIterator()) | ||||
|  | @ -42,7 +50,7 @@ public: | |||
|          toolButton->setStatusTip( | ||||
|             tr(common::GetLevel3CategoryDescription(category).c_str())); | ||||
|          toolButton->setPopupMode(QToolButton::MenuButtonPopup); | ||||
|          layout_->addWidget(toolButton); | ||||
|          productsLayout_->addWidget(toolButton); | ||||
|          categoryButtons_.push_back(toolButton); | ||||
| 
 | ||||
|          QObject::connect(toolButton, | ||||
|  | @ -99,6 +107,26 @@ public: | |||
| 
 | ||||
|          toolButton->setEnabled(false); | ||||
|       } | ||||
| 
 | ||||
|       // Storm Tracking Information
 | ||||
|       QCheckBox* stiPastEnableCheckBox     = new QCheckBox(); | ||||
|       QCheckBox* stiForecastEnableCheckBox = new QCheckBox(); | ||||
| 
 | ||||
|       stiPastEnableCheckBox->setText(QObject::tr("Storm Tracks (Past)")); | ||||
|       stiForecastEnableCheckBox->setText( | ||||
|          QObject::tr("Storm Tracks (Forecast)")); | ||||
| 
 | ||||
|       layout_->addWidget(stiPastEnableCheckBox); | ||||
|       layout_->addWidget(stiForecastEnableCheckBox); | ||||
| 
 | ||||
|       auto& productSettings = settings::ProductSettings::Instance(); | ||||
| 
 | ||||
|       stiPastEnabled_.SetSettingsVariable(productSettings.sti_past_enabled()); | ||||
|       stiForecastEnabled_.SetSettingsVariable( | ||||
|          productSettings.sti_forecast_enabled()); | ||||
| 
 | ||||
|       stiPastEnabled_.SetEditWidget(stiPastEnableCheckBox); | ||||
|       stiForecastEnabled_.SetEditWidget(stiForecastEnableCheckBox); | ||||
|    } | ||||
|    ~Level3ProductsWidgetImpl() = default; | ||||
| 
 | ||||
|  | @ -109,6 +137,8 @@ public: | |||
| 
 | ||||
|    Level3ProductsWidget*   self_; | ||||
|    QLayout*                layout_; | ||||
|    QWidget*                productsWidget_; | ||||
|    QLayout*                productsLayout_; | ||||
|    std::list<QToolButton*> categoryButtons_; | ||||
|    std::unordered_map<common::Level3ProductCategory, | ||||
|                       std::unordered_map<std::string, QMenu*>> | ||||
|  | @ -118,6 +148,9 @@ public: | |||
| 
 | ||||
|    std::unordered_map<QAction*, std::string> awipsProductMap_; | ||||
|    std::shared_mutex                         awipsProductMutex_; | ||||
| 
 | ||||
|    settings::SettingsInterface<bool> stiPastEnabled_ {}; | ||||
|    settings::SettingsInterface<bool> stiForecastEnabled_ {}; | ||||
| }; | ||||
| 
 | ||||
| Level3ProductsWidget::Level3ProductsWidget(QWidget* parent) : | ||||
|  |  | |||
|  | @ -1,6 +1,8 @@ | |||
| #include <scwx/qt/util/geographic_lib.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| 
 | ||||
| #include <numbers> | ||||
| 
 | ||||
| #include <GeographicLib/Gnomonic.hpp> | ||||
| #include <geos/algorithm/PointLocation.h> | ||||
| #include <geos/geom/CoordinateSequence.h> | ||||
|  | @ -90,6 +92,42 @@ GetAngle(double lat1, double lon1, double lat2, double lon2) | |||
|    return units::angle::degrees<double> {azi1}; | ||||
| } | ||||
| 
 | ||||
| common::Coordinate GetCoordinate(const common::Coordinate&     center, | ||||
|                                  units::angle::degrees<double> angle, | ||||
|                                  units::length::meters<double> distance) | ||||
| { | ||||
|    double latitude; | ||||
|    double longitude; | ||||
| 
 | ||||
|    DefaultGeodesic().Direct(center.latitude_, | ||||
|                             center.longitude_, | ||||
|                             angle.value(), | ||||
|                             distance.value(), | ||||
|                             latitude, | ||||
|                             longitude); | ||||
| 
 | ||||
|    return {latitude, longitude}; | ||||
| } | ||||
| 
 | ||||
| common::Coordinate GetCoordinate(const common::Coordinate& center, | ||||
|                                  units::meters<double>     i, | ||||
|                                  units::meters<double>     j) | ||||
| { | ||||
|    // Calculate polar coordinates based on i and j
 | ||||
|    const double angle = | ||||
|       std::atan2(i.value(), j.value()) * 180.0 / std::numbers::pi; | ||||
|    const double range = | ||||
|       std::sqrt(i.value() * i.value() + j.value() * j.value()); | ||||
| 
 | ||||
|    double latitude; | ||||
|    double longitude; | ||||
| 
 | ||||
|    DefaultGeodesic().Direct( | ||||
|       center.latitude_, center.longitude_, angle, range, latitude, longitude); | ||||
| 
 | ||||
|    return {latitude, longitude}; | ||||
| } | ||||
| 
 | ||||
| units::length::meters<double> | ||||
| GetDistance(double lat1, double lon1, double lat2, double lon2) | ||||
| { | ||||
|  |  | |||
|  | @ -49,6 +49,34 @@ bool AreaContainsPoint(const std::vector<common::Coordinate>& area, | |||
| units::angle::degrees<double> | ||||
| GetAngle(double lat1, double lon1, double lat2, double lon2); | ||||
| 
 | ||||
| /**
 | ||||
|  * Get a coordinate from a polar coordinate offset. | ||||
|  * | ||||
|  * @param [in] center The center coordinate from which the angle and distance | ||||
|  * are given | ||||
|  * @param [in] angle The angle at which the destination coordinate lies | ||||
|  * @param [in] distance The distance from the center coordinate to the | ||||
|  * destination coordinate | ||||
|  * | ||||
|  * @return offset coordinate | ||||
|  */ | ||||
| common::Coordinate GetCoordinate(const common::Coordinate&     center, | ||||
|                                  units::angle::degrees<double> angle, | ||||
|                                  units::length::meters<double> distance); | ||||
| 
 | ||||
| /**
 | ||||
|  * Get a coordinate from an (i, j) offset. | ||||
|  * | ||||
|  * @param [in] center The center coordinate from which i and j are offset | ||||
|  * @param [in] i The easting offset in meters | ||||
|  * @param [in] j The northing offset in meters | ||||
|  * | ||||
|  * @return offset coordinate | ||||
|  */ | ||||
| common::Coordinate GetCoordinate(const common::Coordinate& center, | ||||
|                                  units::meters<double>     i, | ||||
|                                  units::meters<double>     j); | ||||
| 
 | ||||
| /**
 | ||||
|  * Get the distance between two points. | ||||
|  * | ||||
|  |  | |||
							
								
								
									
										318
									
								
								scwx-qt/source/scwx/qt/view/overlay_product_view.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										318
									
								
								scwx-qt/source/scwx/qt/view/overlay_product_view.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,318 @@ | |||
| #include <scwx/qt/view/overlay_product_view.hpp> | ||||
| #include <scwx/qt/manager/radar_product_manager.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| #include <scwx/util/time.hpp> | ||||
| 
 | ||||
| #include <mutex> | ||||
| 
 | ||||
| #include <boost/asio.hpp> | ||||
| #include <boost/uuid/random_generator.hpp> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace qt | ||||
| { | ||||
| namespace view | ||||
| { | ||||
| 
 | ||||
| static const std::string logPrefix_ = "scwx::qt::view::overlay_product_view"; | ||||
| static const auto        logger_    = util::Logger::Create(logPrefix_); | ||||
| 
 | ||||
| static const std::string kNst_ = "NST"; | ||||
| 
 | ||||
| class OverlayProductView::Impl | ||||
| { | ||||
| public: | ||||
|    explicit Impl(OverlayProductView* self) : self_ {self} {} | ||||
|    ~Impl() { threadPool_.join(); } | ||||
| 
 | ||||
|    void ConnectRadarProductManager(); | ||||
|    void DisconnectRadarProductManager(); | ||||
|    void LoadProduct(const std::string&                    product, | ||||
|                     std::chrono::system_clock::time_point time, | ||||
|                     bool                                  autoUpdate); | ||||
|    void ResetProducts(); | ||||
|    void Update(const std::string& product); | ||||
|    void UpdateAutoRefresh(bool enabled) const; | ||||
| 
 | ||||
|    OverlayProductView* self_; | ||||
|    boost::uuids::uuid  uuid_ {boost::uuids::random_generator()()}; | ||||
| 
 | ||||
|    boost::asio::thread_pool threadPool_ {1u}; | ||||
| 
 | ||||
|    bool autoRefreshEnabled_ {false}; | ||||
|    bool autoUpdateEnabled_ {false}; | ||||
| 
 | ||||
|    std::chrono::system_clock::time_point selectedTime_ {}; | ||||
| 
 | ||||
|    std::shared_ptr<manager::RadarProductManager> radarProductManager_ {nullptr}; | ||||
| 
 | ||||
|    std::unordered_map<std::string, std::shared_ptr<wsr88d::rpg::Level3Message>> | ||||
|               messageMap_ {}; | ||||
|    std::mutex messageMutex_ {}; | ||||
| }; | ||||
| 
 | ||||
| OverlayProductView::OverlayProductView() : p(std::make_unique<Impl>(this)) {}; | ||||
| OverlayProductView::~OverlayProductView() = default; | ||||
| 
 | ||||
| std::shared_ptr<manager::RadarProductManager> | ||||
| OverlayProductView::radar_product_manager() const | ||||
| { | ||||
|    return p->radarProductManager_; | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<wsr88d::rpg::Level3Message> | ||||
| OverlayProductView::radar_product_message(const std::string& product) const | ||||
| { | ||||
|    std::unique_lock lock {p->messageMutex_}; | ||||
| 
 | ||||
|    auto it = p->messageMap_.find(product); | ||||
|    if (it != p->messageMap_.cend()) | ||||
|    { | ||||
|       return it->second; | ||||
|    } | ||||
| 
 | ||||
|    return nullptr; | ||||
| } | ||||
| 
 | ||||
| std::chrono::system_clock::time_point OverlayProductView::selected_time() const | ||||
| { | ||||
|    return p->selectedTime_; | ||||
| } | ||||
| 
 | ||||
| void OverlayProductView::set_radar_product_manager( | ||||
|    const std::shared_ptr<manager::RadarProductManager>& radarProductManager) | ||||
| { | ||||
|    p->UpdateAutoRefresh(false); | ||||
|    p->DisconnectRadarProductManager(); | ||||
|    p->ResetProducts(); | ||||
| 
 | ||||
|    p->radarProductManager_ = radarProductManager; | ||||
| 
 | ||||
|    p->ConnectRadarProductManager(); | ||||
|    p->UpdateAutoRefresh(p->autoRefreshEnabled_); | ||||
| } | ||||
| 
 | ||||
| void OverlayProductView::Impl::ConnectRadarProductManager() | ||||
| { | ||||
|    connect(radarProductManager_.get(), | ||||
|            &manager::RadarProductManager::DataReloaded, | ||||
|            self_, | ||||
|            [this](std::shared_ptr<types::RadarProductRecord> record) | ||||
|            { | ||||
|               if (record->radar_product_group() == | ||||
|                      common::RadarProductGroup::Level3 && | ||||
|                   record->radar_product() == kNst_ && | ||||
|                   std::chrono::floor<std::chrono::seconds>(record->time()) == | ||||
|                      selectedTime_) | ||||
|               { | ||||
|                  // If the data associated with the currently selected time is
 | ||||
|                  // reloaded, update the view
 | ||||
|                  Update(record->radar_product()); | ||||
|               } | ||||
|            }); | ||||
| 
 | ||||
|    connect( | ||||
|       radarProductManager_.get(), | ||||
|       &manager::RadarProductManager::NewDataAvailable, | ||||
|       self_, | ||||
|       [this](common::RadarProductGroup             group, | ||||
|              const std::string&                    product, | ||||
|              std::chrono::system_clock::time_point latestTime) | ||||
|       { | ||||
|          if (autoRefreshEnabled_ && | ||||
|              group == common::RadarProductGroup::Level3 && product == kNst_) | ||||
|          { | ||||
|             LoadProduct(product, latestTime, autoUpdateEnabled_); | ||||
|          } | ||||
|       }, | ||||
|       Qt::QueuedConnection); | ||||
| } | ||||
| 
 | ||||
| void OverlayProductView::Impl::DisconnectRadarProductManager() | ||||
| { | ||||
|    if (radarProductManager_ != nullptr) | ||||
|    { | ||||
|       disconnect(radarProductManager_.get(), | ||||
|                  &manager::RadarProductManager::NewDataAvailable, | ||||
|                  self_, | ||||
|                  nullptr); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void OverlayProductView::Impl::LoadProduct( | ||||
|    const std::string&                    product, | ||||
|    std::chrono::system_clock::time_point time, | ||||
|    bool                                  autoUpdate) | ||||
| { | ||||
|    logger_->debug( | ||||
|       "Load Product: {}, {}, {}", product, util::TimeString(time), autoUpdate); | ||||
| 
 | ||||
|    // Create file request
 | ||||
|    std::shared_ptr<request::NexradFileRequest> request = | ||||
|       std::make_shared<request::NexradFileRequest>( | ||||
|          radarProductManager_->radar_id()); | ||||
| 
 | ||||
|    if (autoUpdate) | ||||
|    { | ||||
|       connect( | ||||
|          request.get(), | ||||
|          &request::NexradFileRequest::RequestComplete, | ||||
|          self_, | ||||
|          [=, this](std::shared_ptr<request::NexradFileRequest> request) | ||||
|          { | ||||
|             using namespace std::chrono_literals; | ||||
| 
 | ||||
|             // Select loaded record
 | ||||
|             const auto& record = request->radar_product_record(); | ||||
| 
 | ||||
|             std::shared_ptr<wsr88d::Level3File>         level3File = nullptr; | ||||
|             std::shared_ptr<wsr88d::rpg::Level3Message> message    = nullptr; | ||||
| 
 | ||||
|             if (record != nullptr) | ||||
|             { | ||||
|                level3File = record->level3_file(); | ||||
|             } | ||||
|             if (level3File != nullptr) | ||||
|             { | ||||
|                message = level3File->message(); | ||||
|             } | ||||
| 
 | ||||
|             // Validate record
 | ||||
|             if (message != nullptr) | ||||
|             { | ||||
|                const auto& header      = message->header(); | ||||
|                auto        productTime = util::TimePoint( | ||||
|                   header.date_of_message(), header.time_of_message() * 1000); | ||||
| 
 | ||||
|                // If the record is from the last 30 minutes
 | ||||
|                if (productTime + 30min >= std::chrono::system_clock::now() || | ||||
|                    (selectedTime_ != std::chrono::system_clock::time_point {} && | ||||
|                     productTime + 30min >= selectedTime_)) | ||||
|                { | ||||
|                   // Store loaded record
 | ||||
|                   std::unique_lock lock {messageMutex_}; | ||||
| 
 | ||||
|                   auto it = messageMap_.find(product); | ||||
|                   if (it == messageMap_.cend() || it->second != message) | ||||
|                   { | ||||
|                      messageMap_.insert_or_assign(product, message); | ||||
| 
 | ||||
|                      lock.unlock(); | ||||
| 
 | ||||
|                      Q_EMIT self_->ProductUpdated(product); | ||||
|                   } | ||||
|                } | ||||
|                else | ||||
|                { | ||||
|                   // If product is more than 30 minutes old, discard
 | ||||
|                   std::unique_lock lock {messageMutex_}; | ||||
|                   std::size_t      elementsRemoved = messageMap_.erase(product); | ||||
|                   lock.unlock(); | ||||
| 
 | ||||
|                   if (elementsRemoved > 0) | ||||
|                   { | ||||
|                      Q_EMIT self_->ProductUpdated(product); | ||||
|                   } | ||||
| 
 | ||||
|                   logger_->trace("Discarding stale data: {}", | ||||
|                                  util::TimeString(productTime)); | ||||
|                } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                // If the product doesn't exist, erase the stale product
 | ||||
|                std::unique_lock lock {messageMutex_}; | ||||
|                std::size_t      elementsRemoved = messageMap_.erase(product); | ||||
|                lock.unlock(); | ||||
| 
 | ||||
|                if (elementsRemoved > 0) | ||||
|                { | ||||
|                   Q_EMIT self_->ProductUpdated(product); | ||||
|                } | ||||
| 
 | ||||
|                logger_->trace("Removing stale product"); | ||||
|             } | ||||
|          }); | ||||
|    } | ||||
| 
 | ||||
|    // Load file
 | ||||
|    boost::asio::post( | ||||
|       threadPool_, | ||||
|       [=, this]() | ||||
|       { radarProductManager_->LoadLevel3Data(product, time, request); }); | ||||
| } | ||||
| 
 | ||||
| void OverlayProductView::Impl::ResetProducts() | ||||
| { | ||||
|    std::unique_lock lock {messageMutex_}; | ||||
|    messageMap_.clear(); | ||||
|    lock.unlock(); | ||||
| 
 | ||||
|    Q_EMIT self_->ProductUpdated(kNst_); | ||||
| } | ||||
| 
 | ||||
| void OverlayProductView::SelectTime(std::chrono::system_clock::time_point time) | ||||
| { | ||||
|    if (time != p->selectedTime_) | ||||
|    { | ||||
|       p->selectedTime_ = time; | ||||
|       p->Update(kNst_); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void OverlayProductView::SetAutoRefresh(bool enabled) | ||||
| { | ||||
|    if (p->autoRefreshEnabled_ != enabled) | ||||
|    { | ||||
|       p->autoRefreshEnabled_ = enabled; | ||||
|       p->UpdateAutoRefresh(enabled); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void OverlayProductView::Impl::Update(const std::string& product) | ||||
| { | ||||
|    // Retrieve message from Radar Product Manager
 | ||||
|    std::shared_ptr<wsr88d::rpg::Level3Message> message; | ||||
|    std::chrono::system_clock::time_point       requestedTime {selectedTime_}; | ||||
|    std::chrono::system_clock::time_point       foundTime; | ||||
|    std::tie(message, foundTime) = | ||||
|       radarProductManager_->GetLevel3Data(product, requestedTime); | ||||
| 
 | ||||
|    if (message == nullptr) | ||||
|    { | ||||
|       logger_->debug("{} data not found", product); | ||||
|       return; | ||||
|    } | ||||
| 
 | ||||
|    std::unique_lock lock {messageMutex_}; | ||||
| 
 | ||||
|    // Update message in map
 | ||||
|    auto it = messageMap_.find(product); | ||||
|    if (it == messageMap_.cend() || it->second != message) | ||||
|    { | ||||
|       messageMap_.insert_or_assign(product, message); | ||||
| 
 | ||||
|       lock.unlock(); | ||||
| 
 | ||||
|       Q_EMIT self_->ProductUpdated(product); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void OverlayProductView::Impl::UpdateAutoRefresh(bool enabled) const | ||||
| { | ||||
|    if (radarProductManager_ != nullptr) | ||||
|    { | ||||
|       radarProductManager_->EnableRefresh( | ||||
|          common::RadarProductGroup::Level3, kNst_, enabled, uuid_); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void OverlayProductView::SetAutoUpdate(bool enabled) | ||||
| { | ||||
|    p->autoUpdateEnabled_ = enabled; | ||||
| } | ||||
| 
 | ||||
| } // namespace view
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
							
								
								
									
										62
									
								
								scwx-qt/source/scwx/qt/view/overlay_product_view.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								scwx-qt/source/scwx/qt/view/overlay_product_view.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <chrono> | ||||
| #include <memory> | ||||
| 
 | ||||
| #include <QObject> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace wsr88d | ||||
| { | ||||
| namespace rpg | ||||
| { | ||||
| 
 | ||||
| class Level3Message; | ||||
| 
 | ||||
| } // namespace rpg
 | ||||
| } // namespace wsr88d
 | ||||
| 
 | ||||
| namespace qt | ||||
| { | ||||
| namespace manager | ||||
| { | ||||
| 
 | ||||
| class RadarProductManager; | ||||
| 
 | ||||
| } // namespace manager
 | ||||
| 
 | ||||
| namespace view | ||||
| { | ||||
| 
 | ||||
| class OverlayProductView : public QObject | ||||
| { | ||||
|    Q_OBJECT | ||||
| 
 | ||||
| public: | ||||
|    explicit OverlayProductView(); | ||||
|    virtual ~OverlayProductView(); | ||||
| 
 | ||||
|    std::shared_ptr<manager::RadarProductManager> radar_product_manager() const; | ||||
|    std::shared_ptr<wsr88d::rpg::Level3Message> | ||||
|    radar_product_message(const std::string& product) const; | ||||
|    std::chrono::system_clock::time_point selected_time() const; | ||||
| 
 | ||||
|    void set_radar_product_manager( | ||||
|       const std::shared_ptr<manager::RadarProductManager>& radarProductManager); | ||||
| 
 | ||||
|    void SelectTime(std::chrono::system_clock::time_point time); | ||||
|    void SetAutoRefresh(bool enabled); | ||||
|    void SetAutoUpdate(bool enabled); | ||||
| 
 | ||||
| signals: | ||||
|    void ProductUpdated(std::string product); | ||||
| 
 | ||||
| private: | ||||
|    class Impl; | ||||
|    std::unique_ptr<Impl> p; | ||||
| }; | ||||
| 
 | ||||
| } // namespace view
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
|  | @ -150,11 +150,6 @@ RadarProductView::GetDescriptionFields() const | |||
|    return {}; | ||||
| } | ||||
| 
 | ||||
| std::chrono::system_clock::time_point RadarProductView::GetSelectedTime() const | ||||
| { | ||||
|    return p->selectedTime_; | ||||
| } | ||||
| 
 | ||||
| void RadarProductView::ComputeSweep() | ||||
| { | ||||
|    logger_->debug("ComputeSweep()"); | ||||
|  |  | |||
|  | @ -76,8 +76,6 @@ public: | |||
|                                 GetDataLevelCode(std::uint16_t level) const = 0; | ||||
|    virtual std::optional<float> GetDataValue(std::uint16_t level) const     = 0; | ||||
| 
 | ||||
|    std::chrono::system_clock::time_point GetSelectedTime() const; | ||||
| 
 | ||||
|    virtual std::vector<std::pair<std::string, std::string>> | ||||
|    GetDescriptionFields() const; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1 +1 @@ | |||
| Subproject commit 90331f7654586302b223d88329846929514bc883 | ||||
| Subproject commit 0446ff708b387728faf8d2dbb3862a757067c2ee | ||||
|  | @ -1,5 +1,6 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <optional> | ||||
| #include <string> | ||||
| #include <vector> | ||||
| 
 | ||||
|  | @ -29,5 +30,14 @@ std::vector<std::string> ParseTokens(const std::string&       s, | |||
| 
 | ||||
| std::string ToString(const std::vector<std::string>& v); | ||||
| 
 | ||||
| template<typename T> | ||||
| std::optional<T> TryParseNumeric(const std::string& str); | ||||
| 
 | ||||
| #if defined(STRINGS_IMPLEMENTATION) | ||||
| template std::optional<std::uint16_t> TryParseNumeric(const std::string& str); | ||||
| template std::optional<std::uint32_t> TryParseNumeric(const std::string& str); | ||||
| template std::optional<float>         TryParseNumeric(const std::string& str); | ||||
| #endif | ||||
| 
 | ||||
| } // namespace util
 | ||||
| } // namespace scwx
 | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <chrono> | ||||
| #include <optional> | ||||
| 
 | ||||
| #if !defined(_MSC_VER) | ||||
| #   include <date/tz.h> | ||||
|  | @ -24,5 +25,9 @@ std::string TimeString(std::chrono::system_clock::time_point time, | |||
|                        const time_zone*                      timeZone = nullptr, | ||||
|                        bool                                  epochValid = true); | ||||
| 
 | ||||
| template<typename T> | ||||
| std::optional<std::chrono::sys_time<T>> | ||||
| TryParseDateTime(const std::string& dateTimeFormat, const std::string& str); | ||||
| 
 | ||||
| } // namespace util
 | ||||
| } // namespace scwx
 | ||||
|  |  | |||
|  | @ -1,9 +1,11 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <scwx/awips/message.hpp> | ||||
| #include <scwx/wsr88d/rpg/packet.hpp> | ||||
| 
 | ||||
| #include <cstdint> | ||||
| #include <memory> | ||||
| #include <vector> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
|  | @ -31,6 +33,8 @@ public: | |||
| 
 | ||||
|    size_t data_size() const override; | ||||
| 
 | ||||
|    const std::vector<std::vector<std::shared_ptr<Packet>>>& page_list() const; | ||||
| 
 | ||||
|    bool Parse(std::istream& is); | ||||
| 
 | ||||
|    static constexpr size_t SIZE = 102u; | ||||
|  |  | |||
|  | @ -6,6 +6,8 @@ | |||
| #include <memory> | ||||
| #include <optional> | ||||
| 
 | ||||
| #include <units/length.h> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace wsr88d | ||||
|  | @ -27,11 +29,21 @@ public: | |||
|    LinkedVectorPacket(LinkedVectorPacket&&) noexcept; | ||||
|    LinkedVectorPacket& operator=(LinkedVectorPacket&&) noexcept; | ||||
| 
 | ||||
|    uint16_t                packet_code() const override; | ||||
|    uint16_t                length_of_block() const; | ||||
|    std::optional<uint16_t> value_of_vector() const; | ||||
|    std::uint16_t                packet_code() const override; | ||||
|    std::uint16_t                length_of_block() const; | ||||
|    std::optional<std::uint16_t> value_of_vector() const; | ||||
| 
 | ||||
|    size_t data_size() const override; | ||||
|    std::int16_t              start_i() const; | ||||
|    std::int16_t              start_j() const; | ||||
|    std::vector<std::int16_t> end_i() const; | ||||
|    std::vector<std::int16_t> end_j() const; | ||||
| 
 | ||||
|    units::kilometers<double>              start_i_km() const; | ||||
|    units::kilometers<double>              start_j_km() const; | ||||
|    std::vector<units::kilometers<double>> end_i_km() const; | ||||
|    std::vector<units::kilometers<double>> end_j_km() const; | ||||
| 
 | ||||
|    std::size_t data_size() const override; | ||||
| 
 | ||||
|    bool Parse(std::istream& is) override; | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										64
									
								
								wxdata/include/scwx/wsr88d/rpg/rpg_types.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								wxdata/include/scwx/wsr88d/rpg/rpg_types.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,64 @@ | |||
| #pragma once | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace wsr88d | ||||
| { | ||||
| namespace rpg | ||||
| { | ||||
| 
 | ||||
| enum class PacketCode : std::uint16_t | ||||
| { | ||||
|    TextNoValue                          = 1, | ||||
|    SpecialSymbol                        = 2, | ||||
|    MesocycloneSymbol3                   = 3, | ||||
|    WindBarbData                         = 4, | ||||
|    VectorArrowData                      = 5, | ||||
|    LinkedVectorNoValue                  = 6, | ||||
|    UnlinkedVectorNoValue                = 7, | ||||
|    TextUniform                          = 8, | ||||
|    LinkedVectorUniform                  = 9, | ||||
|    UnlinkedVectorUniform                = 10, | ||||
|    MesocycloneSymbol11                  = 11, | ||||
|    TornadoVortexSignatureSymbol         = 12, | ||||
|    HailPositiveSymbol                   = 13, | ||||
|    HailProbableSymbol                   = 14, | ||||
|    StormId                              = 15, | ||||
|    DigitalRadialDataArray               = 16, | ||||
|    DigitalPrecipitationDataArray        = 17, | ||||
|    PrecipitationRateDataArray           = 18, | ||||
|    HdaHailSymbol                        = 19, | ||||
|    PointFeatureSymbol                   = 20, | ||||
|    CellTrendData                        = 21, | ||||
|    CellTrendVolumeScanTimes             = 22, | ||||
|    ScitPastData                         = 23, | ||||
|    ScitForecastData                     = 24, | ||||
|    StiCircle                            = 25, | ||||
|    ElevatedTornadoVortexSignatureSymbol = 26, | ||||
|    GenericData28                        = 28, | ||||
|    GenericData29                        = 29, | ||||
|    SetColorLevel                        = 0x0802, | ||||
|    LinkedContourVector                  = 0x0E03, | ||||
|    UnlinkedContourVector                = 0x3501, | ||||
|    MapMessage0E23                       = 0x0E23, | ||||
|    MapMessage3521                       = 0x3521, | ||||
|    MapMessage4E00                       = 0x4E00, | ||||
|    MapMessage4E01                       = 0x4E01, | ||||
|    RadialData                           = 0xAF1F, | ||||
|    RasterDataBA07                       = 0xBA07, | ||||
|    RasterDataBA0F                       = 0xBA0F | ||||
| }; | ||||
| 
 | ||||
| enum class SpecialSymbol | ||||
| { | ||||
|    PastStormCellPosition, | ||||
|    CurrentStormCellPosition, | ||||
|    ForecastStormCellPosition, | ||||
|    PastMdaPosition, | ||||
|    ForecastMdaPosition, | ||||
|    None | ||||
| }; | ||||
| 
 | ||||
| } // namespace rpg
 | ||||
| } // namespace wsr88d
 | ||||
| } // namespace scwx
 | ||||
							
								
								
									
										43
									
								
								wxdata/include/scwx/wsr88d/rpg/scit_data_packet.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								wxdata/include/scwx/wsr88d/rpg/scit_data_packet.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <scwx/wsr88d/rpg/special_graphic_symbol_packet.hpp> | ||||
| 
 | ||||
| #include <cstdint> | ||||
| #include <memory> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace wsr88d | ||||
| { | ||||
| namespace rpg | ||||
| { | ||||
| 
 | ||||
| class ScitDataPacket : public SpecialGraphicSymbolPacket | ||||
| { | ||||
| public: | ||||
|    explicit ScitDataPacket(); | ||||
|    ~ScitDataPacket(); | ||||
| 
 | ||||
|    ScitDataPacket(const ScitDataPacket&)            = delete; | ||||
|    ScitDataPacket& operator=(const ScitDataPacket&) = delete; | ||||
| 
 | ||||
|    ScitDataPacket(ScitDataPacket&&) noexcept; | ||||
|    ScitDataPacket& operator=(ScitDataPacket&&) noexcept; | ||||
| 
 | ||||
|    std::vector<std::shared_ptr<Packet>> packet_list() const; | ||||
| 
 | ||||
|    std::size_t RecordCount() const override; | ||||
| 
 | ||||
|    static std::shared_ptr<ScitDataPacket> Create(std::istream& is); | ||||
| 
 | ||||
| protected: | ||||
|    bool ParseData(std::istream& is) override; | ||||
| 
 | ||||
| private: | ||||
|    class Impl; | ||||
|    std::unique_ptr<Impl> p; | ||||
| }; | ||||
| 
 | ||||
| } // namespace rpg
 | ||||
| } // namespace wsr88d
 | ||||
| } // namespace scwx
 | ||||
|  | @ -1,44 +0,0 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <scwx/wsr88d/rpg/special_graphic_symbol_packet.hpp> | ||||
| 
 | ||||
| #include <cstdint> | ||||
| #include <memory> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace wsr88d | ||||
| { | ||||
| namespace rpg | ||||
| { | ||||
| 
 | ||||
| class ScitForecastDataPacketImpl; | ||||
| 
 | ||||
| class ScitForecastDataPacket : public SpecialGraphicSymbolPacket | ||||
| { | ||||
| public: | ||||
|    explicit ScitForecastDataPacket(); | ||||
|    ~ScitForecastDataPacket(); | ||||
| 
 | ||||
|    ScitForecastDataPacket(const ScitForecastDataPacket&) = delete; | ||||
|    ScitForecastDataPacket& operator=(const ScitForecastDataPacket&) = delete; | ||||
| 
 | ||||
|    ScitForecastDataPacket(ScitForecastDataPacket&&) noexcept; | ||||
|    ScitForecastDataPacket& operator=(ScitForecastDataPacket&&) noexcept; | ||||
| 
 | ||||
|    const std::vector<uint8_t>& data() const; | ||||
| 
 | ||||
|    size_t RecordCount() const override; | ||||
| 
 | ||||
|    static std::shared_ptr<ScitForecastDataPacket> Create(std::istream& is); | ||||
| 
 | ||||
| protected: | ||||
|    bool ParseData(std::istream& is) override; | ||||
| 
 | ||||
| private: | ||||
|    std::unique_ptr<ScitForecastDataPacketImpl> p; | ||||
| }; | ||||
| 
 | ||||
| } // namespace rpg
 | ||||
| } // namespace wsr88d
 | ||||
| } // namespace scwx
 | ||||
|  | @ -0,0 +1,92 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <scwx/wsr88d/rpg/graphic_product_message.hpp> | ||||
| 
 | ||||
| #include <optional> | ||||
| 
 | ||||
| #include <units/angle.h> | ||||
| #include <units/length.h> | ||||
| #include <units/velocity.h> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace wsr88d | ||||
| { | ||||
| namespace rpg | ||||
| { | ||||
| 
 | ||||
| class StormTrackingInformationMessage : public GraphicProductMessage | ||||
| { | ||||
| public: | ||||
|    struct StiRecord | ||||
|    { | ||||
|       struct Position | ||||
|       { | ||||
|          std::optional<units::angle::degrees<std::uint16_t>> azimuth_ {}; | ||||
|          std::optional<units::length::nautical_miles<std::uint16_t>> range_ {}; | ||||
|       }; | ||||
| 
 | ||||
|       Position                                             currentPosition_ {}; | ||||
|       std::optional<units::angle::degrees<std::uint16_t>>  direction_; | ||||
|       std::optional<units::velocity::knots<std::uint16_t>> speed_; | ||||
| 
 | ||||
|       std::array<Position, 4> forecastPosition_ {}; | ||||
| 
 | ||||
|       std::optional<units::length::nautical_miles<float>> forecastError_ {}; | ||||
|       std::optional<units::length::nautical_miles<float>> meanError_ {}; | ||||
| 
 | ||||
|       std::optional<std::int16_t>                       maxDbz_ {}; | ||||
|       std::optional<units::length::feet<std::uint32_t>> maxDbzHeight_ {}; | ||||
|    }; | ||||
| 
 | ||||
|    explicit StormTrackingInformationMessage(); | ||||
|    ~StormTrackingInformationMessage(); | ||||
| 
 | ||||
|    StormTrackingInformationMessage(const StormTrackingInformationMessage&) = | ||||
|       delete; | ||||
|    StormTrackingInformationMessage& | ||||
|    operator=(const StormTrackingInformationMessage&) = delete; | ||||
| 
 | ||||
|    StormTrackingInformationMessage(StormTrackingInformationMessage&&) noexcept; | ||||
|    StormTrackingInformationMessage& | ||||
|    operator=(StormTrackingInformationMessage&&) noexcept; | ||||
| 
 | ||||
|    std::optional<std::uint16_t>                               radar_id() const; | ||||
|    std::optional<std::chrono::sys_time<std::chrono::seconds>> date_time() const; | ||||
|    std::optional<std::uint16_t> num_storm_cells() const; | ||||
| 
 | ||||
|    std::optional<units::angle::degrees<std::uint16_t>> | ||||
|    default_direction() const; | ||||
|    std::optional<units::velocity::meters_per_second<float>> | ||||
|                                                 minimum_speed() const; | ||||
|    std::optional<units::velocity::knots<float>> default_speed() const; | ||||
|    std::optional<units::length::kilometers<std::uint16_t>> | ||||
|                                        allowable_error() const; | ||||
|    std::optional<std::chrono::minutes> maximum_time() const; | ||||
|    std::optional<std::chrono::minutes> forecast_interval() const; | ||||
|    std::optional<std::uint16_t>        number_of_past_volumes() const; | ||||
|    std::optional<std::uint16_t>        number_of_intervals() const; | ||||
|    std::optional<units::velocity::meters_per_second<float>> | ||||
|                                        correlation_speed() const; | ||||
|    std::optional<std::chrono::minutes> error_interval() const; | ||||
| 
 | ||||
|    std::optional<units::length::kilometers<float>> filter_kernel_size() const; | ||||
|    std::optional<float>                            filter_fraction() const; | ||||
|    std::optional<bool> reflectivity_filtered() const; | ||||
| 
 | ||||
|    std::shared_ptr<const StiRecord> | ||||
|    sti_record(const std::string& stormId) const; | ||||
| 
 | ||||
|    bool Parse(std::istream& is) override; | ||||
| 
 | ||||
|    static std::shared_ptr<StormTrackingInformationMessage> | ||||
|    Create(Level3MessageHeader&& header, std::istream& is); | ||||
| 
 | ||||
| private: | ||||
|    class Impl; | ||||
|    std::unique_ptr<Impl> p; | ||||
| }; | ||||
| 
 | ||||
| } // namespace rpg
 | ||||
| } // namespace wsr88d
 | ||||
| } // namespace scwx
 | ||||
|  | @ -31,6 +31,8 @@ public: | |||
| 
 | ||||
|    size_t data_size() const override; | ||||
| 
 | ||||
|    const std::vector<std::vector<std::string>>& page_list() const; | ||||
| 
 | ||||
|    bool Parse(std::istream& is); | ||||
|    bool Parse(std::istream& is, bool skipHeader); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <scwx/wsr88d/rpg/packet.hpp> | ||||
| #include <scwx/wsr88d/rpg/rpg_types.hpp> | ||||
| 
 | ||||
| #include <cstdint> | ||||
| #include <memory> | ||||
|  | @ -14,8 +15,6 @@ namespace wsr88d | |||
| namespace rpg | ||||
| { | ||||
| 
 | ||||
| class TextAndSpecialSymbolPacketImpl; | ||||
| 
 | ||||
| class TextAndSpecialSymbolPacket : public Packet | ||||
| { | ||||
| public: | ||||
|  | @ -29,21 +28,23 @@ public: | |||
|    TextAndSpecialSymbolPacket(TextAndSpecialSymbolPacket&&) noexcept; | ||||
|    TextAndSpecialSymbolPacket& operator=(TextAndSpecialSymbolPacket&&) noexcept; | ||||
| 
 | ||||
|    uint16_t                packet_code() const override; | ||||
|    uint16_t                length_of_block() const; | ||||
|    std::optional<uint16_t> value_of_text() const; | ||||
|    int16_t                 start_i() const; | ||||
|    int16_t                 start_j() const; | ||||
|    std::uint16_t                packet_code() const override; | ||||
|    std::uint16_t                length_of_block() const; | ||||
|    std::optional<std::uint16_t> value_of_text() const; | ||||
|    std::int16_t                 start_i() const; | ||||
|    std::int16_t                 start_j() const; | ||||
|    std::string                  text() const; | ||||
|    SpecialSymbol                special_symbol() const; | ||||
| 
 | ||||
|    size_t data_size() const override; | ||||
|    std::size_t data_size() const override; | ||||
| 
 | ||||
|    bool Parse(std::istream& is) override; | ||||
| 
 | ||||
|    static std::shared_ptr<TextAndSpecialSymbolPacket> Create(std::istream& is); | ||||
| 
 | ||||
| private: | ||||
|    std::unique_ptr<TextAndSpecialSymbolPacketImpl> p; | ||||
|    class Impl; | ||||
|    std::unique_ptr<Impl> p; | ||||
| }; | ||||
| 
 | ||||
| } // namespace rpg
 | ||||
|  |  | |||
|  | @ -1,6 +1,9 @@ | |||
| #define STRINGS_IMPLEMENTATION | ||||
| 
 | ||||
| #include <scwx/util/strings.hpp> | ||||
| 
 | ||||
| #include <boost/algorithm/string/trim.hpp> | ||||
| #include <boost/lexical_cast.hpp> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
|  | @ -88,5 +91,22 @@ std::string ToString(const std::vector<std::string>& v) | |||
|    return value; | ||||
| } | ||||
| 
 | ||||
| template<typename T> | ||||
| std::optional<T> TryParseNumeric(const std::string& str) | ||||
| { | ||||
|    std::optional<T> value = std::nullopt; | ||||
| 
 | ||||
|    try | ||||
|    { | ||||
|       auto trimmed = boost::algorithm::trim_copy(str); | ||||
|       value        = boost::lexical_cast<T>(trimmed); | ||||
|    } | ||||
|    catch (const std::exception&) | ||||
|    { | ||||
|    } | ||||
| 
 | ||||
|    return value; | ||||
| } | ||||
| 
 | ||||
| } // namespace util
 | ||||
| } // namespace scwx
 | ||||
|  |  | |||
|  | @ -10,6 +10,12 @@ | |||
| 
 | ||||
| #include <scwx/util/time.hpp> | ||||
| 
 | ||||
| #include <sstream> | ||||
| 
 | ||||
| #if !defined(_MSC_VER) | ||||
| #   include <date/date.h> | ||||
| #endif | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace util | ||||
|  | @ -55,5 +61,33 @@ std::string TimeString(std::chrono::system_clock::time_point time, | |||
|    return os.str(); | ||||
| } | ||||
| 
 | ||||
| template<typename T> | ||||
| std::optional<std::chrono::sys_time<T>> | ||||
| TryParseDateTime(const std::string& dateTimeFormat, const std::string& str) | ||||
| { | ||||
|    using namespace std::chrono; | ||||
| 
 | ||||
| #if !defined(_MSC_VER) | ||||
|    using namespace date; | ||||
| #endif | ||||
| 
 | ||||
|    std::optional<std::chrono::sys_time<T>> value = std::nullopt; | ||||
|    std::chrono::sys_time<T>                dateTime; | ||||
|    std::istringstream                      ssDateTime {str}; | ||||
| 
 | ||||
|    ssDateTime >> parse(dateTimeFormat, dateTime); | ||||
| 
 | ||||
|    if (!ssDateTime.fail()) | ||||
|    { | ||||
|       value = dateTime; | ||||
|    } | ||||
| 
 | ||||
|    return value; | ||||
| } | ||||
| 
 | ||||
| template std::optional<std::chrono::sys_time<std::chrono::seconds>> | ||||
| TryParseDateTime<std::chrono::seconds>(const std::string& dateTimeFormat, | ||||
|                                        const std::string& str); | ||||
| 
 | ||||
| } // namespace util
 | ||||
| } // namespace scwx
 | ||||
|  |  | |||
|  | @ -58,6 +58,12 @@ size_t GraphicAlphanumericBlock::data_size() const | |||
|    return p->lengthOfBlock_; | ||||
| } | ||||
| 
 | ||||
| const std::vector<std::vector<std::shared_ptr<Packet>>>& | ||||
| GraphicAlphanumericBlock::page_list() const | ||||
| { | ||||
|    return p->pageList_; | ||||
| } | ||||
| 
 | ||||
| bool GraphicAlphanumericBlock::Parse(std::istream& is) | ||||
| { | ||||
|    bool blockValid = true; | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ | |||
| #include <scwx/wsr88d/rpg/general_status_message.hpp> | ||||
| #include <scwx/wsr88d/rpg/graphic_product_message.hpp> | ||||
| #include <scwx/wsr88d/rpg/radar_coded_message.hpp> | ||||
| #include <scwx/wsr88d/rpg/storm_tracking_information_message.hpp> | ||||
| #include <scwx/wsr88d/rpg/tabular_product_message.hpp> | ||||
| 
 | ||||
| #include <unordered_map> | ||||
|  | @ -44,7 +45,7 @@ static const std::unordered_map<int, CreateLevel3MessageFunction> // | |||
|             {51, GraphicProductMessage::Create}, | ||||
|             {56, GraphicProductMessage::Create}, | ||||
|             {57, GraphicProductMessage::Create}, | ||||
|             {58, GraphicProductMessage::Create}, | ||||
|             {58, StormTrackingInformationMessage::Create}, | ||||
|             {59, GraphicProductMessage::Create}, | ||||
|             {61, GraphicProductMessage::Create}, | ||||
|             {62, TabularProductMessage::Create}, | ||||
|  |  | |||
|  | @ -71,6 +71,56 @@ std::optional<uint16_t> LinkedVectorPacket::value_of_vector() const | |||
|    return value; | ||||
| } | ||||
| 
 | ||||
| std::int16_t LinkedVectorPacket::start_i() const | ||||
| { | ||||
|    return p->startI_; | ||||
| } | ||||
| 
 | ||||
| std::int16_t LinkedVectorPacket::start_j() const | ||||
| { | ||||
|    return p->startJ_; | ||||
| } | ||||
| 
 | ||||
| std::vector<std::int16_t> LinkedVectorPacket::end_i() const | ||||
| { | ||||
|    return p->endI_; | ||||
| } | ||||
| 
 | ||||
| std::vector<std::int16_t> LinkedVectorPacket::end_j() const | ||||
| { | ||||
|    return p->endJ_; | ||||
| } | ||||
| 
 | ||||
| units::kilometers<double> LinkedVectorPacket::start_i_km() const | ||||
| { | ||||
|    return units::kilometers<double> {p->startI_ * 0.25}; | ||||
| } | ||||
| 
 | ||||
| units::kilometers<double> LinkedVectorPacket::start_j_km() const | ||||
| { | ||||
|    return units::kilometers<double> {p->startJ_ * 0.25}; | ||||
| } | ||||
| 
 | ||||
| std::vector<units::kilometers<double>> LinkedVectorPacket::end_i_km() const | ||||
| { | ||||
|    std::vector<units::kilometers<double>> endI; | ||||
|    for (const auto& i : p->endI_) | ||||
|    { | ||||
|       endI.emplace_back(units::kilometers<double> {i * 0.25}); | ||||
|    } | ||||
|    return endI; | ||||
| } | ||||
| 
 | ||||
| std::vector<units::kilometers<double>> LinkedVectorPacket::end_j_km() const | ||||
| { | ||||
|    std::vector<units::kilometers<double>> endJ; | ||||
|    for (const auto& j : p->endJ_) | ||||
|    { | ||||
|       endJ.emplace_back(units::kilometers<double> {j * 0.25}); | ||||
|    } | ||||
|    return endJ; | ||||
| } | ||||
| 
 | ||||
| size_t LinkedVectorPacket::data_size() const | ||||
| { | ||||
|    return p->lengthOfBlock_ + 4u; | ||||
|  |  | |||
|  | @ -15,7 +15,7 @@ | |||
| #include <scwx/wsr88d/rpg/precipitation_rate_data_array_packet.hpp> | ||||
| #include <scwx/wsr88d/rpg/radial_data_packet.hpp> | ||||
| #include <scwx/wsr88d/rpg/raster_data_packet.hpp> | ||||
| #include <scwx/wsr88d/rpg/scit_forecast_data_packet.hpp> | ||||
| #include <scwx/wsr88d/rpg/scit_data_packet.hpp> | ||||
| #include <scwx/wsr88d/rpg/set_color_level_packet.hpp> | ||||
| #include <scwx/wsr88d/rpg/sti_circle_symbol_packet.hpp> | ||||
| #include <scwx/wsr88d/rpg/storm_id_symbol_packet.hpp> | ||||
|  | @ -63,8 +63,8 @@ static const std::unordered_map<unsigned int, CreateMessageFunction> create_ { | |||
|    {20, PointFeatureSymbolPacket::Create}, | ||||
|    {21, CellTrendDataPacket::Create}, | ||||
|    {22, CellTrendVolumeScanTimes::Create}, | ||||
|    {23, ScitForecastDataPacket::Create}, | ||||
|    {24, ScitForecastDataPacket::Create}, | ||||
|    {23, ScitDataPacket::Create}, | ||||
|    {24, ScitDataPacket::Create}, | ||||
|    {25, StiCircleSymbolPacket::Create}, | ||||
|    {26, PointGraphicSymbolPacket::Create}, | ||||
|    {28, GenericDataPacket::Create}, | ||||
|  |  | |||
							
								
								
									
										113
									
								
								wxdata/source/scwx/wsr88d/rpg/scit_data_packet.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								wxdata/source/scwx/wsr88d/rpg/scit_data_packet.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,113 @@ | |||
| #include <scwx/wsr88d/rpg/scit_data_packet.hpp> | ||||
| #include <scwx/wsr88d/rpg/packet_factory.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| 
 | ||||
| #include <istream> | ||||
| #include <set> | ||||
| #include <string> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace wsr88d | ||||
| { | ||||
| namespace rpg | ||||
| { | ||||
| 
 | ||||
| static const std::string logPrefix_ = "scwx::wsr88d::rpg::scit_data_packet"; | ||||
| static const auto        logger_    = util::Logger::Create(logPrefix_); | ||||
| 
 | ||||
| static const std::set<std::uint16_t> packetCodes_ = {23, 24}; | ||||
| 
 | ||||
| class ScitDataPacket::Impl | ||||
| { | ||||
| public: | ||||
|    explicit Impl() {} | ||||
|    ~Impl() = default; | ||||
| 
 | ||||
|    std::vector<std::shared_ptr<Packet>> packetList_ {}; | ||||
| }; | ||||
| 
 | ||||
| ScitDataPacket::ScitDataPacket() : p(std::make_unique<Impl>()) {} | ||||
| ScitDataPacket::~ScitDataPacket() = default; | ||||
| 
 | ||||
| ScitDataPacket::ScitDataPacket(ScitDataPacket&&) noexcept            = default; | ||||
| ScitDataPacket& ScitDataPacket::operator=(ScitDataPacket&&) noexcept = default; | ||||
| 
 | ||||
| std::vector<std::shared_ptr<Packet>> ScitDataPacket::packet_list() const | ||||
| { | ||||
|    return p->packetList_; | ||||
| } | ||||
| 
 | ||||
| size_t ScitDataPacket::RecordCount() const | ||||
| { | ||||
|    return p->packetList_.size(); | ||||
| } | ||||
| 
 | ||||
| bool ScitDataPacket::ParseData(std::istream& is) | ||||
| { | ||||
|    bool blockValid = true; | ||||
| 
 | ||||
|    if (!packetCodes_.contains(packet_code())) | ||||
|    { | ||||
|       logger_->warn("Invalid packet code: {}", packet_code()); | ||||
|       blockValid = false; | ||||
|    } | ||||
| 
 | ||||
|    if (blockValid) | ||||
|    { | ||||
|       std::uint32_t  bytesRead     = 0; | ||||
|       std::uint32_t  lengthOfBlock = length_of_block(); | ||||
|       std::streampos dataStart     = is.tellg(); | ||||
|       std::streampos dataEnd = | ||||
|          dataStart + static_cast<std::streamoff>(lengthOfBlock); | ||||
| 
 | ||||
|       while (bytesRead < lengthOfBlock) | ||||
|       { | ||||
|          std::shared_ptr<Packet> packet = PacketFactory::Create(is); | ||||
|          if (packet != nullptr) | ||||
|          { | ||||
|             p->packetList_.push_back(packet); | ||||
|             bytesRead += static_cast<std::uint32_t>(packet->data_size()); | ||||
|          } | ||||
|          else | ||||
|          { | ||||
|             break; | ||||
|          } | ||||
|       } | ||||
| 
 | ||||
|       if (bytesRead < lengthOfBlock) | ||||
|       { | ||||
|          logger_->trace("Block bytes read smaller than size: {} < {} bytes", | ||||
|                         bytesRead, | ||||
|                         lengthOfBlock); | ||||
|          blockValid = false; | ||||
|          is.seekg(dataEnd, std::ios_base::beg); | ||||
|       } | ||||
|       if (bytesRead > lengthOfBlock) | ||||
|       { | ||||
|          logger_->warn("Block bytes read larger than size: {} > {} bytes", | ||||
|                        bytesRead, | ||||
|                        lengthOfBlock); | ||||
|          blockValid = false; | ||||
|          is.seekg(dataEnd, std::ios_base::beg); | ||||
|       } | ||||
|    } | ||||
| 
 | ||||
|    return blockValid; | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<ScitDataPacket> ScitDataPacket::Create(std::istream& is) | ||||
| { | ||||
|    std::shared_ptr<ScitDataPacket> packet = std::make_shared<ScitDataPacket>(); | ||||
| 
 | ||||
|    if (!packet->Parse(is)) | ||||
|    { | ||||
|       packet.reset(); | ||||
|    } | ||||
| 
 | ||||
|    return packet; | ||||
| } | ||||
| 
 | ||||
| } // namespace rpg
 | ||||
| } // namespace wsr88d
 | ||||
| } // namespace scwx
 | ||||
|  | @ -1,88 +0,0 @@ | |||
| #include <scwx/wsr88d/rpg/scit_forecast_data_packet.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| 
 | ||||
| #include <istream> | ||||
| #include <set> | ||||
| #include <string> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace wsr88d | ||||
| { | ||||
| namespace rpg | ||||
| { | ||||
| 
 | ||||
| static const std::string logPrefix_ = | ||||
|    "scwx::wsr88d::rpg::scit_forecast_data_packet"; | ||||
| static const auto logger_ = util::Logger::Create(logPrefix_); | ||||
| 
 | ||||
| static const std::set<uint16_t> packetCodes_ = {23, 24}; | ||||
| 
 | ||||
| class ScitForecastDataPacketImpl | ||||
| { | ||||
| public: | ||||
|    explicit ScitForecastDataPacketImpl() : data_ {}, recordCount_ {0} {} | ||||
|    ~ScitForecastDataPacketImpl() = default; | ||||
| 
 | ||||
|    std::vector<uint8_t> data_; | ||||
|    size_t               recordCount_; | ||||
| }; | ||||
| 
 | ||||
| ScitForecastDataPacket::ScitForecastDataPacket() : | ||||
|     p(std::make_unique<ScitForecastDataPacketImpl>()) | ||||
| { | ||||
| } | ||||
| ScitForecastDataPacket::~ScitForecastDataPacket() = default; | ||||
| 
 | ||||
| ScitForecastDataPacket::ScitForecastDataPacket( | ||||
|    ScitForecastDataPacket&&) noexcept = default; | ||||
| ScitForecastDataPacket& | ||||
| ScitForecastDataPacket::operator=(ScitForecastDataPacket&&) noexcept = default; | ||||
| 
 | ||||
| const std::vector<uint8_t>& ScitForecastDataPacket::data() const | ||||
| { | ||||
|    return p->data_; | ||||
| } | ||||
| 
 | ||||
| size_t ScitForecastDataPacket::RecordCount() const | ||||
| { | ||||
|    return p->recordCount_; | ||||
| } | ||||
| 
 | ||||
| bool ScitForecastDataPacket::ParseData(std::istream& is) | ||||
| { | ||||
|    bool blockValid = true; | ||||
| 
 | ||||
|    if (!packetCodes_.contains(packet_code())) | ||||
|    { | ||||
|       logger_->warn("Invalid packet code: {}", packet_code()); | ||||
|       blockValid = false; | ||||
|    } | ||||
| 
 | ||||
|    if (blockValid) | ||||
|    { | ||||
|       p->recordCount_ = length_of_block(); | ||||
|       p->data_.resize(p->recordCount_); | ||||
|       is.read(reinterpret_cast<char*>(p->data_.data()), p->recordCount_); | ||||
|    } | ||||
| 
 | ||||
|    return blockValid; | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<ScitForecastDataPacket> | ||||
| ScitForecastDataPacket::Create(std::istream& is) | ||||
| { | ||||
|    std::shared_ptr<ScitForecastDataPacket> packet = | ||||
|       std::make_shared<ScitForecastDataPacket>(); | ||||
| 
 | ||||
|    if (!packet->Parse(is)) | ||||
|    { | ||||
|       packet.reset(); | ||||
|    } | ||||
| 
 | ||||
|    return packet; | ||||
| } | ||||
| 
 | ||||
| } // namespace rpg
 | ||||
| } // namespace wsr88d
 | ||||
| } // namespace scwx
 | ||||
|  | @ -0,0 +1,767 @@ | |||
| #include <scwx/wsr88d/rpg/storm_tracking_information_message.hpp> | ||||
| #include <scwx/wsr88d/rpg/text_and_special_symbol_packet.hpp> | ||||
| #include <scwx/wsr88d/rpg/rpg_types.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| #include <scwx/util/strings.hpp> | ||||
| #include <scwx/util/time.hpp> | ||||
| 
 | ||||
| #include <unordered_map> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace wsr88d | ||||
| { | ||||
| namespace rpg | ||||
| { | ||||
| 
 | ||||
| static const std::string logPrefix_ = | ||||
|    "scwx::wsr88d::rpg::storm_tracking_information_message"; | ||||
| static const auto logger_ = util::Logger::Create(logPrefix_); | ||||
| 
 | ||||
| class StormTrackingInformationMessage::Impl | ||||
| { | ||||
| public: | ||||
|    explicit Impl() {} | ||||
|    ~Impl() = default; | ||||
| 
 | ||||
|    std::shared_ptr<StiRecord>& GetOrCreateStiRecord(const std::string& stormId); | ||||
| 
 | ||||
|    void ParseGraphicBlock( | ||||
|       const std::shared_ptr<const GraphicAlphanumericBlock>& block); | ||||
|    void ParseTabularBlock( | ||||
|       const std::shared_ptr<const TabularAlphanumericBlock>& block); | ||||
| 
 | ||||
|    void ParseStormPositionForecastPage(const std::vector<std::string>& page); | ||||
|    void ParseStormCellTrackingDataPage(const std::vector<std::string>& page); | ||||
| 
 | ||||
|    void HandleTextUniformPacket(std::shared_ptr<const Packet> packet, | ||||
|                                 std::vector<std::string>&     stormIds); | ||||
| 
 | ||||
|    // STORM POSITION/FORECAST
 | ||||
|    std::optional<std::uint16_t>                               radarId_ {}; | ||||
|    std::optional<std::chrono::sys_time<std::chrono::seconds>> dateTime_ {}; | ||||
|    std::optional<std::uint16_t>                               numStormCells_ {}; | ||||
| 
 | ||||
|    // STORM CELL TRACKING/FORECAST ADAPTATION DATA
 | ||||
|    std::optional<units::angle::degrees<std::uint16_t>> defaultDirection_ {}; | ||||
|    std::optional<units::velocity::meters_per_second<float>> minimumSpeed_ {}; | ||||
|    std::optional<units::velocity::knots<float>>             defaultSpeed_ {}; | ||||
|    std::optional<units::length::kilometers<std::uint16_t>>  allowableError_ {}; | ||||
|    std::optional<std::chrono::minutes>                      maximumTime_ {}; | ||||
|    std::optional<std::chrono::minutes> forecastInterval_ {}; | ||||
|    std::optional<std::uint16_t>        numberOfPastVolumes_ {}; | ||||
|    std::optional<std::uint16_t>        numberOfIntervals_ {}; | ||||
|    std::optional<units::velocity::meters_per_second<float>> | ||||
|                                        correlationSpeed_ {}; | ||||
|    std::optional<std::chrono::minutes> errorInterval_ {}; | ||||
| 
 | ||||
|    // SCIT REFLECTIVITY MEDIAN FILTER
 | ||||
|    std::optional<units::length::kilometers<float>> filterKernelSize_ {}; | ||||
|    std::optional<float>                            filterFraction_ {}; | ||||
|    std::optional<bool>                             reflectivityFiltered_ {}; | ||||
| 
 | ||||
|    std::unordered_map<std::string, std::shared_ptr<StiRecord>> stiRecords_ {}; | ||||
| }; | ||||
| 
 | ||||
| StormTrackingInformationMessage::StormTrackingInformationMessage() : | ||||
|     p(std::make_unique<Impl>()) | ||||
| { | ||||
| } | ||||
| StormTrackingInformationMessage::~StormTrackingInformationMessage() = default; | ||||
| 
 | ||||
| StormTrackingInformationMessage::StormTrackingInformationMessage( | ||||
|    StormTrackingInformationMessage&&) noexcept = default; | ||||
| StormTrackingInformationMessage& StormTrackingInformationMessage::operator=( | ||||
|    StormTrackingInformationMessage&&) noexcept = default; | ||||
| 
 | ||||
| std::optional<std::uint16_t> StormTrackingInformationMessage::radar_id() const | ||||
| { | ||||
|    return p->radarId_; | ||||
| } | ||||
| 
 | ||||
| std::optional<std::chrono::sys_time<std::chrono::seconds>> | ||||
| StormTrackingInformationMessage::date_time() const | ||||
| { | ||||
|    return p->dateTime_; | ||||
| } | ||||
| 
 | ||||
| std::optional<std::uint16_t> | ||||
| StormTrackingInformationMessage::num_storm_cells() const | ||||
| { | ||||
|    return p->numStormCells_; | ||||
| } | ||||
| 
 | ||||
| std::optional<units::angle::degrees<std::uint16_t>> | ||||
| StormTrackingInformationMessage::default_direction() const | ||||
| { | ||||
|    return p->defaultDirection_; | ||||
| } | ||||
| 
 | ||||
| std::optional<units::velocity::meters_per_second<float>> | ||||
| StormTrackingInformationMessage::minimum_speed() const | ||||
| { | ||||
|    return p->minimumSpeed_; | ||||
| } | ||||
| 
 | ||||
| std::optional<units::velocity::knots<float>> | ||||
| StormTrackingInformationMessage::default_speed() const | ||||
| { | ||||
|    return p->defaultSpeed_; | ||||
| } | ||||
| 
 | ||||
| std::optional<units::length::kilometers<std::uint16_t>> | ||||
| StormTrackingInformationMessage::allowable_error() const | ||||
| { | ||||
|    return p->allowableError_; | ||||
| } | ||||
| 
 | ||||
| std::optional<std::chrono::minutes> | ||||
| StormTrackingInformationMessage::maximum_time() const | ||||
| { | ||||
|    return p->maximumTime_; | ||||
| } | ||||
| 
 | ||||
| std::optional<std::chrono::minutes> | ||||
| StormTrackingInformationMessage::forecast_interval() const | ||||
| { | ||||
|    return p->forecastInterval_; | ||||
| } | ||||
| 
 | ||||
| std::optional<std::uint16_t> | ||||
| StormTrackingInformationMessage::number_of_past_volumes() const | ||||
| { | ||||
|    return p->numberOfPastVolumes_; | ||||
| } | ||||
| 
 | ||||
| std::optional<std::uint16_t> | ||||
| StormTrackingInformationMessage::number_of_intervals() const | ||||
| { | ||||
|    return p->numberOfIntervals_; | ||||
| } | ||||
| 
 | ||||
| std::optional<units::velocity::meters_per_second<float>> | ||||
| StormTrackingInformationMessage::correlation_speed() const | ||||
| { | ||||
|    return p->correlationSpeed_; | ||||
| } | ||||
| 
 | ||||
| std::optional<std::chrono::minutes> | ||||
| StormTrackingInformationMessage::error_interval() const | ||||
| { | ||||
|    return p->errorInterval_; | ||||
| } | ||||
| 
 | ||||
| std::optional<units::length::kilometers<float>> | ||||
| StormTrackingInformationMessage::filter_kernel_size() const | ||||
| { | ||||
|    return p->filterKernelSize_; | ||||
| } | ||||
| 
 | ||||
| std::optional<float> StormTrackingInformationMessage::filter_fraction() const | ||||
| { | ||||
|    return p->filterFraction_; | ||||
| } | ||||
| 
 | ||||
| std::optional<bool> | ||||
| StormTrackingInformationMessage::reflectivity_filtered() const | ||||
| { | ||||
|    return p->reflectivityFiltered_; | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<const StormTrackingInformationMessage::StiRecord> | ||||
| StormTrackingInformationMessage::sti_record(const std::string& stormId) const | ||||
| { | ||||
|    std::shared_ptr<const StiRecord> record = nullptr; | ||||
| 
 | ||||
|    auto it = p->stiRecords_.find(stormId); | ||||
|    if (it != p->stiRecords_.cend()) | ||||
|    { | ||||
|       record = it->second; | ||||
|    } | ||||
| 
 | ||||
|    return record; | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<StormTrackingInformationMessage::StiRecord>& | ||||
| StormTrackingInformationMessage::Impl::GetOrCreateStiRecord( | ||||
|    const std::string& stormId) | ||||
| { | ||||
|    auto& record = stiRecords_[stormId]; | ||||
|    if (record == nullptr) | ||||
|    { | ||||
|       record = std::make_shared<StiRecord>(); | ||||
|    } | ||||
|    return record; | ||||
| } | ||||
| 
 | ||||
| bool StormTrackingInformationMessage::Parse(std::istream& is) | ||||
| { | ||||
|    bool dataValid = GraphicProductMessage::Parse(is); | ||||
| 
 | ||||
|    std::shared_ptr<GraphicAlphanumericBlock> graphicBlock = nullptr; | ||||
|    std::shared_ptr<TabularAlphanumericBlock> tabularBlock = nullptr; | ||||
| 
 | ||||
|    if (dataValid) | ||||
|    { | ||||
|       graphicBlock = graphic_block(); | ||||
|       tabularBlock = tabular_block(); | ||||
|    } | ||||
| 
 | ||||
|    if (graphicBlock != nullptr) | ||||
|    { | ||||
|       p->ParseGraphicBlock(graphicBlock); | ||||
|    } | ||||
| 
 | ||||
|    if (tabularBlock != nullptr) | ||||
|    { | ||||
|       p->ParseTabularBlock(tabularBlock); | ||||
|    } | ||||
| 
 | ||||
|    return dataValid; | ||||
| } | ||||
| 
 | ||||
| void StormTrackingInformationMessage::Impl::ParseGraphicBlock( | ||||
|    const std::shared_ptr<const GraphicAlphanumericBlock>& block) | ||||
| { | ||||
|    for (auto& page : block->page_list()) | ||||
|    { | ||||
|       std::vector<std::string> stormIds {}; | ||||
| 
 | ||||
|       for (auto& packet : page) | ||||
|       { | ||||
|          switch (packet->packet_code()) | ||||
|          { | ||||
|          case static_cast<std::uint16_t>(wsr88d::rpg::PacketCode::TextUniform): | ||||
|             HandleTextUniformPacket(packet, stormIds); | ||||
|             break; | ||||
| 
 | ||||
|          default: | ||||
|             logger_->trace("Ignoring graphic alphanumeric packet type: {}", | ||||
|                            packet->packet_code()); | ||||
|             break; | ||||
|          } | ||||
|       } | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void StormTrackingInformationMessage::Impl::HandleTextUniformPacket( | ||||
|    std::shared_ptr<const Packet> packet, std::vector<std::string>& stormIds) | ||||
| { | ||||
|    auto textPacket = | ||||
|       std::dynamic_pointer_cast<const wsr88d::rpg::TextAndSpecialSymbolPacket>( | ||||
|          packet); | ||||
| 
 | ||||
|    if (textPacket != nullptr && textPacket->text().size() >= 69) | ||||
|    { | ||||
|       auto text = textPacket->text(); | ||||
| 
 | ||||
|       // " STORM ID        D7        H3        K5        N5        U6        E7"
 | ||||
|       if (text.starts_with(" STORM ID")) | ||||
|       { | ||||
|          static constexpr std::size_t kMaxStormIds = 6; | ||||
|          static constexpr std::size_t kStartOffset = 17; | ||||
|          static constexpr std::size_t kStride      = 10; | ||||
| 
 | ||||
|          stormIds.clear(); | ||||
| 
 | ||||
|          for (std::size_t i = 0, offset = kStartOffset; i < kMaxStormIds; | ||||
|               ++i, offset += kStride) | ||||
|          { | ||||
|             std::string stormId = text.substr(offset, 2); | ||||
| 
 | ||||
|             if (std::isupper(stormId[0]) && std::isdigit(stormId[1])) | ||||
|             { | ||||
|                stormIds.push_back(stormId); | ||||
|             } | ||||
|          } | ||||
|       } | ||||
| 
 | ||||
|       // " AZ/RAN    242/ 77    45/ 36   180/139   175/126    23/110    25/ 83"
 | ||||
|       else if (text.starts_with(" AZ/RAN")) | ||||
|       { | ||||
|          static constexpr std::size_t kAzStartOffset  = 11; | ||||
|          static constexpr std::size_t kRanStartOffset = 15; | ||||
|          static constexpr std::size_t kStride         = 10; | ||||
| 
 | ||||
|          for (std::size_t i         = 0, | ||||
|                           azOffset  = kAzStartOffset, | ||||
|                           ranOffset = kRanStartOffset; | ||||
|               i < stormIds.size(); | ||||
|               ++i, azOffset += kStride, ranOffset += kStride) | ||||
|          { | ||||
|             auto& record = GetOrCreateStiRecord(stormIds[i]); | ||||
| 
 | ||||
|             if (!record->currentPosition_.azimuth_.has_value()) | ||||
|             { | ||||
|                // Current Position: Azimuth (Degrees) (I3)
 | ||||
|                auto azimuth = util::TryParseNumeric<std::uint16_t>( | ||||
|                   text.substr(azOffset, 3)); | ||||
|                if (azimuth.has_value()) | ||||
|                { | ||||
|                   record->currentPosition_.azimuth_ = | ||||
|                      units::angle::degrees<std::uint16_t> {azimuth.value()}; | ||||
|                } | ||||
|             } | ||||
| 
 | ||||
|             if (!record->currentPosition_.range_.has_value()) | ||||
|             { | ||||
|                // Current Position: Range (Nautical Miles) (I3)
 | ||||
|                auto range = util::TryParseNumeric<std::uint16_t>( | ||||
|                   text.substr(ranOffset, 3)); | ||||
|                if (range.has_value()) | ||||
|                { | ||||
|                   record->currentPosition_.range_ = | ||||
|                      units::length::nautical_miles<std::uint16_t> { | ||||
|                         range.value()}; | ||||
|                } | ||||
|             } | ||||
|          } | ||||
|       } | ||||
| 
 | ||||
|       // " FCST MVT  262/ 56   249/ 48   234/ 46   228/ 48   227/ 66   242/ 48"
 | ||||
|       else if (text.starts_with(" FCST MVT")) | ||||
|       { | ||||
|          static constexpr std::size_t kDirStartOffset   = 11; | ||||
|          static constexpr std::size_t kSpeedStartOffset = 15; | ||||
|          static constexpr std::size_t kStride           = 10; | ||||
| 
 | ||||
|          for (std::size_t i           = 0, | ||||
|                           dirOffset   = kDirStartOffset, | ||||
|                           speedOffset = kSpeedStartOffset; | ||||
|               i < stormIds.size(); | ||||
|               ++i, dirOffset += kStride, speedOffset += kStride) | ||||
|          { | ||||
|             auto& record = GetOrCreateStiRecord(stormIds[i]); | ||||
| 
 | ||||
|             if (!record->direction_.has_value()) | ||||
|             { | ||||
|                // Movement: Direction (Degrees) (I3)
 | ||||
|                auto direction = util::TryParseNumeric<std::uint16_t>( | ||||
|                   text.substr(dirOffset, 3)); | ||||
|                if (direction.has_value()) | ||||
|                { | ||||
|                   record->direction_ = | ||||
|                      units::angle::degrees<std::uint16_t> {direction.value()}; | ||||
|                } | ||||
|             } | ||||
| 
 | ||||
|             if (!record->speed_.has_value()) | ||||
|             { | ||||
|                // Movement: Speed (Knots) (I3)
 | ||||
|                auto speed = util::TryParseNumeric<std::uint16_t>( | ||||
|                   text.substr(speedOffset, 3)); | ||||
|                if (speed.has_value()) | ||||
|                { | ||||
|                   record->speed_ = | ||||
|                      units::velocity::knots<std::uint16_t> {speed.value()}; | ||||
|                } | ||||
|             } | ||||
|          } | ||||
|       } | ||||
| 
 | ||||
|       // " ERR/MEAN  4.5/ 2.9  0.8/ 1.7  1.4/ 1.4  1.3/ 1.3  1.4/ 1.7  1.2/ 0.8"
 | ||||
|       else if (text.starts_with(" ERR/MEAN")) | ||||
|       { | ||||
|          static constexpr std::size_t kErrStartOffset  = 10; | ||||
|          static constexpr std::size_t kMeanStartOffset = 15; | ||||
|          static constexpr std::size_t kStride          = 10; | ||||
| 
 | ||||
|          for (std::size_t i          = 0, | ||||
|                           errOffset  = kErrStartOffset, | ||||
|                           meanOffset = kMeanStartOffset; | ||||
|               i < stormIds.size(); | ||||
|               ++i, errOffset += kStride, meanOffset += kStride) | ||||
|          { | ||||
|             auto& record = GetOrCreateStiRecord(stormIds[i]); | ||||
| 
 | ||||
|             if (!record->forecastError_.has_value()) | ||||
|             { | ||||
|                // Forecast Error (Nautical Miles) (F4.1)
 | ||||
|                auto forecastError = | ||||
|                   util::TryParseNumeric<float>(text.substr(errOffset, 4)); | ||||
|                if (forecastError.has_value()) | ||||
|                { | ||||
|                   record->forecastError_ = | ||||
|                      units::length::nautical_miles<float> { | ||||
|                         forecastError.value()}; | ||||
|                } | ||||
|             } | ||||
| 
 | ||||
|             if (!record->meanError_.has_value()) | ||||
|             { | ||||
|                // Mean Error (Nautical Miles) (F4.1)
 | ||||
|                auto meanError = | ||||
|                   util::TryParseNumeric<float>(text.substr(meanOffset, 4)); | ||||
|                if (meanError.has_value()) | ||||
|                { | ||||
|                   record->meanError_ = | ||||
|                      units::length::nautical_miles<float> {meanError.value()}; | ||||
|                } | ||||
|             } | ||||
|          } | ||||
|       } | ||||
| 
 | ||||
|       // " DBZM HGT   55  7.5   56  4.2   48 20.6   51 17.4   51 14.0   54  8.9"
 | ||||
|       else if (text.starts_with(" DBZM HGT")) | ||||
|       { | ||||
|          static constexpr std::size_t kDbzmStartOffset = 12; | ||||
|          static constexpr std::size_t kHgtStartOffset  = 15; | ||||
|          static constexpr std::size_t kStride          = 10; | ||||
| 
 | ||||
|          for (std::size_t i          = 0, | ||||
|                           dbzmOffset = kDbzmStartOffset, | ||||
|                           hgtOffset  = kHgtStartOffset; | ||||
|               i < stormIds.size(); | ||||
|               ++i, dbzmOffset += kStride, hgtOffset += kStride) | ||||
|          { | ||||
|             auto& record = GetOrCreateStiRecord(stormIds[i]); | ||||
| 
 | ||||
|             // Maximum dBZ (I2)
 | ||||
|             record->maxDbz_ = | ||||
|                util::TryParseNumeric<std::uint16_t>(text.substr(dbzmOffset, 2)); | ||||
| 
 | ||||
|             // Maximum dBZ Height (Feet) (F4.1)
 | ||||
|             auto height = | ||||
|                util::TryParseNumeric<float>(text.substr(hgtOffset, 4)); | ||||
|             if (height.has_value()) | ||||
|             { | ||||
|                record->maxDbzHeight_ = units::length::feet<std::uint32_t> { | ||||
|                   static_cast<std::uint32_t>(height.value() * 1000.0f)}; | ||||
|             } | ||||
|          } | ||||
|       } | ||||
|    } | ||||
|    else | ||||
|    { | ||||
|       logger_->debug("No storm text present"); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void StormTrackingInformationMessage::Impl::ParseTabularBlock( | ||||
|    const std::shared_ptr<const TabularAlphanumericBlock>& block) | ||||
| { | ||||
|    static const std::string kStormPositionForecast_ = "STORM POSITION/FORECAST"; | ||||
|    static const std::string kStormCellTrackingData_ = | ||||
|       "STORM CELL TRACKING/FORECAST ADAPTATION DATA"; | ||||
| 
 | ||||
|    for (auto& page : block->page_list()) | ||||
|    { | ||||
|       if (page.empty()) | ||||
|       { | ||||
|          logger_->warn("Unexpected empty page"); | ||||
|          continue; | ||||
|       } | ||||
| 
 | ||||
|       if (page[0].find(kStormPositionForecast_) != std::string::npos) | ||||
|       { | ||||
|          ParseStormPositionForecastPage(page); | ||||
|       } | ||||
|       else if (page[0].find(kStormCellTrackingData_) != std::string::npos) | ||||
|       { | ||||
|          ParseStormCellTrackingDataPage(page); | ||||
|       } | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( | ||||
|    const std::vector<std::string>& page) | ||||
| { | ||||
|    for (std::size_t i = 1; i < page.size(); ++i) | ||||
|    { | ||||
|       const std::string& line = page[i]; | ||||
| 
 | ||||
|       // clang-format off
 | ||||
|       // "     RADAR ID 308  DATE/TIME 12:11:21/02:15:38   NUMBER OF STORM CELLS  34"
 | ||||
|       // clang-format on
 | ||||
|       if (i == 1 && line.size() >= 74) | ||||
|       { | ||||
|          if (!radarId_.has_value()) | ||||
|          { | ||||
|             // Radar ID (I3)
 | ||||
|             radarId_ = util::TryParseNumeric<std::uint16_t>(line.substr(14, 3)); | ||||
|          } | ||||
|          if (!dateTime_.has_value()) | ||||
|          { | ||||
|             static const std::string kDateTimeFormat_ {"%m:%d:%y/%H:%M:%S"}; | ||||
| 
 | ||||
|             dateTime_ = util::TryParseDateTime<std::chrono::seconds>( | ||||
|                kDateTimeFormat_, line.substr(29, 17)); | ||||
|          } | ||||
|          if (!numStormCells_.has_value()) | ||||
|          { | ||||
|             // Number of Storm Cells (I3)
 | ||||
|             numStormCells_ = | ||||
|                util::TryParseNumeric<std::uint16_t>(line.substr(71, 3)); | ||||
|          } | ||||
|       } | ||||
|       // clang-format off
 | ||||
|       // "  V6     183/147   234/ 63     178/137   172/129   166/122   159/117    0.7/ 0.7"
 | ||||
|       // clang-format on
 | ||||
|       else if (i >= 7 && line.size() >= 80) | ||||
|       { | ||||
|          std::string stormId = line.substr(2, 2); | ||||
| 
 | ||||
|          if (std::isupper(stormId[0]) && std::isdigit(stormId[1])) | ||||
|          { | ||||
|             auto& record = GetOrCreateStiRecord(stormId); | ||||
| 
 | ||||
|             if (!record->currentPosition_.azimuth_.has_value()) | ||||
|             { | ||||
|                // Current Position: Azimuth (Degrees) (I3)
 | ||||
|                auto azimuth = | ||||
|                   util::TryParseNumeric<std::uint16_t>(line.substr(9, 3)); | ||||
|                if (azimuth.has_value()) | ||||
|                { | ||||
|                   record->currentPosition_.azimuth_ = | ||||
|                      units::angle::degrees<std::uint16_t> {azimuth.value()}; | ||||
|                } | ||||
|             } | ||||
|             if (!record->currentPosition_.range_.has_value()) | ||||
|             { | ||||
|                // Current Position: Range (Nautical Miles) (I3)
 | ||||
|                auto range = | ||||
|                   util::TryParseNumeric<std::uint16_t>(line.substr(13, 3)); | ||||
|                if (range.has_value()) | ||||
|                { | ||||
|                   record->currentPosition_.range_ = | ||||
|                      units::length::nautical_miles<std::uint16_t> { | ||||
|                         range.value()}; | ||||
|                } | ||||
|             } | ||||
|             if (!record->direction_.has_value()) | ||||
|             { | ||||
|                // Movement: Direction (Degrees) (I3)
 | ||||
|                auto direction = | ||||
|                   util::TryParseNumeric<std::uint16_t>(line.substr(19, 3)); | ||||
|                if (direction.has_value()) | ||||
|                { | ||||
|                   record->direction_ = | ||||
|                      units::angle::degrees<std::uint16_t> {direction.value()}; | ||||
|                } | ||||
|             } | ||||
|             if (!record->speed_.has_value()) | ||||
|             { | ||||
|                // Movement: Speed (Knots) (I3)
 | ||||
|                auto speed = | ||||
|                   util::TryParseNumeric<std::uint16_t>(line.substr(23, 3)); | ||||
|                if (speed.has_value()) | ||||
|                { | ||||
|                   record->speed_ = | ||||
|                      units::velocity::knots<std::uint16_t> {speed.value()}; | ||||
|                } | ||||
|             } | ||||
|             for (std::size_t j = 0; j < record->forecastPosition_.size(); ++j) | ||||
|             { | ||||
|                const std::size_t positionOffset = j * 10; | ||||
| 
 | ||||
|                if (!record->forecastPosition_[j].azimuth_.has_value()) | ||||
|                { | ||||
|                   // Forecast Position: Azimuth (Degrees) (I3)
 | ||||
|                   std::size_t offset = 31 + positionOffset; | ||||
| 
 | ||||
|                   auto azimuth = util::TryParseNumeric<std::uint16_t>( | ||||
|                      line.substr(offset, 3)); | ||||
|                   if (azimuth.has_value()) | ||||
|                   { | ||||
|                      record->forecastPosition_[j].azimuth_ = | ||||
|                         units::angle::degrees<std::uint16_t> {azimuth.value()}; | ||||
|                   } | ||||
|                } | ||||
|                if (!record->forecastPosition_[j].range_.has_value()) | ||||
|                { | ||||
|                   // Forecast Position: Range (Nautical Miles) (I3)
 | ||||
|                   std::size_t offset = 35 + positionOffset; | ||||
| 
 | ||||
|                   auto range = util::TryParseNumeric<std::uint16_t>( | ||||
|                      line.substr(offset, 3)); | ||||
|                   if (range.has_value()) | ||||
|                   { | ||||
|                      record->forecastPosition_[j].range_ = | ||||
|                         units::length::nautical_miles<std::uint16_t> { | ||||
|                            range.value()}; | ||||
|                   } | ||||
|                } | ||||
|             } | ||||
|             if (!record->forecastError_.has_value()) | ||||
|             { | ||||
|                // Forecast Error (Nautical Miles) (F4.1)
 | ||||
|                auto forecastError = | ||||
|                   util::TryParseNumeric<float>(line.substr(71, 4)); | ||||
|                if (forecastError.has_value()) | ||||
|                { | ||||
|                   record->forecastError_ = | ||||
|                      units::length::nautical_miles<float> { | ||||
|                         forecastError.value()}; | ||||
|                } | ||||
|             } | ||||
|             if (!record->meanError_.has_value()) | ||||
|             { | ||||
|                // Mean Error (Nautical Miles) (F4.1)
 | ||||
|                auto meanError = | ||||
|                   util::TryParseNumeric<float>(line.substr(76, 4)); | ||||
|                if (meanError.has_value()) | ||||
|                { | ||||
|                   record->meanError_ = | ||||
|                      units::length::nautical_miles<float> {meanError.value()}; | ||||
|                } | ||||
|             } | ||||
|          } | ||||
|       } | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| void StormTrackingInformationMessage::Impl::ParseStormCellTrackingDataPage( | ||||
|    const std::vector<std::string>& page) | ||||
| { | ||||
|    for (std::size_t i = 1; i < page.size(); ++i) | ||||
|    { | ||||
|       const std::string& line = page[i]; | ||||
| 
 | ||||
|       // clang-format off
 | ||||
|       // "    260   (DEG) DEFAULT (DIRECTION)      2.5   (M/S) THRESH (MINIMUM SPEED)"
 | ||||
|       // clang-format on
 | ||||
|       if (i == 2 && line.size() >= 75) | ||||
|       { | ||||
|          // Default Direction (Degrees) (I3)
 | ||||
|          auto direction = | ||||
|             util::TryParseNumeric<std::uint16_t>(line.substr(4, 3)); | ||||
|          if (direction.has_value()) | ||||
|          { | ||||
|             defaultDirection_ = | ||||
|                units::angle::degrees<std::uint16_t> {direction.value()}; | ||||
|          } | ||||
| 
 | ||||
|          // Minimum Speed (Threshold) (m/s) (F4.1)
 | ||||
|          auto threshold = util::TryParseNumeric<float>(line.substr(40, 4)); | ||||
|          if (threshold.has_value()) | ||||
|          { | ||||
|             minimumSpeed_ = | ||||
|                units::velocity::meters_per_second<float> {threshold.value()}; | ||||
|          } | ||||
|       } | ||||
| 
 | ||||
|       // "   36.0   (KTS) DEFAULT (SPEED)           20    (KM) ALLOWABLE ERROR"
 | ||||
|       if (i == 3 && line.size() >= 68) | ||||
|       { | ||||
|          // Default Speed (Knots) (F4.1)
 | ||||
|          auto speed = util::TryParseNumeric<float>(line.substr(3, 4)); | ||||
|          if (speed.has_value()) | ||||
|          { | ||||
|             defaultSpeed_ = units::velocity::knots<float> {speed.value()}; | ||||
|          } | ||||
| 
 | ||||
|          // Allowable Error (Kilometers) (I2)
 | ||||
|          auto error = util::TryParseNumeric<std::uint16_t>(line.substr(42, 2)); | ||||
|          if (error.has_value()) | ||||
|          { | ||||
|             allowableError_ = | ||||
|                units::length::kilometers<std::uint16_t> {error.value()}; | ||||
|          } | ||||
|       } | ||||
| 
 | ||||
|       // clang-format off
 | ||||
|       // "     20   (MIN) TIME (MAXIMUM)            15   (MIN) FORECAST INTERVAL"
 | ||||
|       // clang-format on
 | ||||
|       if (i == 4 && line.size() >= 70) | ||||
|       { | ||||
|          // Maximum Time (Minutes) (I5)
 | ||||
|          auto time = util::TryParseNumeric<std::uint32_t>(line.substr(2, 5)); | ||||
|          if (time.has_value()) | ||||
|          { | ||||
|             maximumTime_ = std::chrono::minutes {time.value()}; | ||||
|          } | ||||
| 
 | ||||
|          // Forecast Interval (Minutes) (I2)
 | ||||
|          auto interval = | ||||
|             util::TryParseNumeric<std::uint16_t>(line.substr(42, 2)); | ||||
|          if (interval.has_value()) | ||||
|          { | ||||
|             forecastInterval_ = std::chrono::minutes {interval.value()}; | ||||
|          } | ||||
|       } | ||||
| 
 | ||||
|       // clang-format off
 | ||||
|       // "     10         NUMBER OF PAST VOLUMES     4         NUMBER OF INTERVALS"
 | ||||
|       // clang-format on
 | ||||
|       if (i == 5 && line.size() >= 72) | ||||
|       { | ||||
|          // Number of Past Volumes (I2)
 | ||||
|          numberOfPastVolumes_ = | ||||
|             util::TryParseNumeric<std::uint16_t>(line.substr(5, 2)); | ||||
| 
 | ||||
|          // Number of Intervals (I2)
 | ||||
|          numberOfIntervals_ = | ||||
|             util::TryParseNumeric<std::uint16_t>(line.substr(42, 2)); | ||||
|       } | ||||
| 
 | ||||
|       // "   30.0   (M/S) CORRELATION SPEED         15   (MIN) ERROR INTERVAL"
 | ||||
|       if (i == 6 && line.size() >= 67) | ||||
|       { | ||||
|          // Correlation Speed (m/s) (F4.1)
 | ||||
|          auto speed = util::TryParseNumeric<float>(line.substr(3, 4)); | ||||
|          if (speed.has_value()) | ||||
|          { | ||||
|             correlationSpeed_ = | ||||
|                units::velocity::meters_per_second<float> {speed.value()}; | ||||
|          } | ||||
| 
 | ||||
|          // Error Interval (Minutes) (I2)
 | ||||
|          auto interval = | ||||
|             util::TryParseNumeric<std::uint16_t>(line.substr(42, 2)); | ||||
|          if (interval.has_value()) | ||||
|          { | ||||
|             errorInterval_ = std::chrono::minutes {interval.value()}; | ||||
|          } | ||||
|       } | ||||
| 
 | ||||
|       // clang-format off
 | ||||
|       // "    7.0   (KM)  FILTER KERNEL SIZE       0.5         THRESH (FILTER FRACTION)"
 | ||||
|       // clang-format on
 | ||||
|       if (i == 11 && line.size() >= 77) | ||||
|       { | ||||
|          // Filter Kernel Size (Kilometers) (F4.1)
 | ||||
|          auto kernelSize = util::TryParseNumeric<float>(line.substr(3, 4)); | ||||
|          if (kernelSize.has_value()) | ||||
|          { | ||||
|             filterKernelSize_ = | ||||
|                units::length::kilometers<float> {kernelSize.value()}; | ||||
|          } | ||||
| 
 | ||||
|          // Minimum Speed (Threshold) (m/s) (F4.1)
 | ||||
|          filterFraction_ = util::TryParseNumeric<float>(line.substr(40, 4)); | ||||
|       } | ||||
| 
 | ||||
|       // "    Yes         REFLECTIVITY FILTERED"
 | ||||
|       if (i == 12 && line.size() >= 37) | ||||
|       { | ||||
|          if (line.substr(4, 3) == "Yes") | ||||
|          { | ||||
|             reflectivityFiltered_ = true; | ||||
|          } | ||||
|          else if (line.substr(5, 2) == "No") | ||||
|          { | ||||
|             reflectivityFiltered_ = false; | ||||
|          } | ||||
|       } | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<StormTrackingInformationMessage> | ||||
| StormTrackingInformationMessage::Create(Level3MessageHeader&& header, | ||||
|                                         std::istream&         is) | ||||
| { | ||||
|    std::shared_ptr<StormTrackingInformationMessage> message = | ||||
|       std::make_shared<StormTrackingInformationMessage>(); | ||||
|    message->set_header(std::move(header)); | ||||
| 
 | ||||
|    if (!message->Parse(is)) | ||||
|    { | ||||
|       message.reset(); | ||||
|    } | ||||
| 
 | ||||
|    return message; | ||||
| } | ||||
| 
 | ||||
| } // namespace rpg
 | ||||
| } // namespace wsr88d
 | ||||
| } // namespace scwx
 | ||||
|  | @ -68,6 +68,12 @@ size_t TabularAlphanumericBlock::data_size() const | |||
|    return p->lengthOfBlock_; | ||||
| } | ||||
| 
 | ||||
| const std::vector<std::vector<std::string>>& | ||||
| TabularAlphanumericBlock::page_list() const | ||||
| { | ||||
|    return p->pageList_; | ||||
| } | ||||
| 
 | ||||
| bool TabularAlphanumericBlock::Parse(std::istream& is) | ||||
| { | ||||
|    return Parse(is, false); | ||||
|  |  | |||
|  | @ -15,31 +15,23 @@ static const std::string logPrefix_ = | |||
|    "scwx::wsr88d::rpg::text_and_special_symbol_packet"; | ||||
| static const auto logger_ = util::Logger::Create(logPrefix_); | ||||
| 
 | ||||
| class TextAndSpecialSymbolPacketImpl | ||||
| class TextAndSpecialSymbolPacket::Impl | ||||
| { | ||||
| public: | ||||
|    explicit TextAndSpecialSymbolPacketImpl() : | ||||
|        packetCode_ {0}, | ||||
|        lengthOfBlock_ {0}, | ||||
|        valueOfText_ {0}, | ||||
|        startI_ {0}, | ||||
|        startJ_ {0}, | ||||
|        text_ {} | ||||
|    { | ||||
|    } | ||||
|    ~TextAndSpecialSymbolPacketImpl() = default; | ||||
|    explicit Impl() {} | ||||
|    ~Impl() = default; | ||||
| 
 | ||||
|    uint16_t packetCode_; | ||||
|    uint16_t lengthOfBlock_; | ||||
|    uint16_t valueOfText_; | ||||
|    int16_t  startI_; | ||||
|    int16_t  startJ_; | ||||
|    std::uint16_t packetCode_ {0}; | ||||
|    std::uint16_t lengthOfBlock_ {0}; | ||||
|    std::uint16_t valueOfText_ {0}; | ||||
|    std::int16_t  startI_ {0}; | ||||
|    std::int16_t  startJ_ {0}; | ||||
| 
 | ||||
|    std::string text_; | ||||
|    std::string text_ {}; | ||||
| }; | ||||
| 
 | ||||
| TextAndSpecialSymbolPacket::TextAndSpecialSymbolPacket() : | ||||
|     p(std::make_unique<TextAndSpecialSymbolPacketImpl>()) | ||||
|     p(std::make_unique<Impl>()) | ||||
| { | ||||
| } | ||||
| TextAndSpecialSymbolPacket::~TextAndSpecialSymbolPacket() = default; | ||||
|  | @ -49,19 +41,19 @@ TextAndSpecialSymbolPacket::TextAndSpecialSymbolPacket( | |||
| TextAndSpecialSymbolPacket& TextAndSpecialSymbolPacket::operator=( | ||||
|    TextAndSpecialSymbolPacket&&) noexcept = default; | ||||
| 
 | ||||
| uint16_t TextAndSpecialSymbolPacket::packet_code() const | ||||
| std::uint16_t TextAndSpecialSymbolPacket::packet_code() const | ||||
| { | ||||
|    return p->packetCode_; | ||||
| } | ||||
| 
 | ||||
| uint16_t TextAndSpecialSymbolPacket::length_of_block() const | ||||
| std::uint16_t TextAndSpecialSymbolPacket::length_of_block() const | ||||
| { | ||||
|    return p->lengthOfBlock_; | ||||
| } | ||||
| 
 | ||||
| std::optional<uint16_t> TextAndSpecialSymbolPacket::value_of_text() const | ||||
| std::optional<std::uint16_t> TextAndSpecialSymbolPacket::value_of_text() const | ||||
| { | ||||
|    std::optional<uint16_t> value; | ||||
|    std::optional<std::uint16_t> value; | ||||
| 
 | ||||
|    if (p->packetCode_ == 8) | ||||
|    { | ||||
|  | @ -71,12 +63,12 @@ std::optional<uint16_t> TextAndSpecialSymbolPacket::value_of_text() const | |||
|    return value; | ||||
| } | ||||
| 
 | ||||
| int16_t TextAndSpecialSymbolPacket::start_i() const | ||||
| std::int16_t TextAndSpecialSymbolPacket::start_i() const | ||||
| { | ||||
|    return p->startI_; | ||||
| } | ||||
| 
 | ||||
| int16_t TextAndSpecialSymbolPacket::start_j() const | ||||
| std::int16_t TextAndSpecialSymbolPacket::start_j() const | ||||
| { | ||||
|    return p->startJ_; | ||||
| } | ||||
|  | @ -86,7 +78,43 @@ std::string TextAndSpecialSymbolPacket::text() const | |||
|    return p->text_; | ||||
| } | ||||
| 
 | ||||
| size_t TextAndSpecialSymbolPacket::data_size() const | ||||
| SpecialSymbol TextAndSpecialSymbolPacket::special_symbol() const | ||||
| { | ||||
|    SpecialSymbol symbol = SpecialSymbol::None; | ||||
| 
 | ||||
|    if (!p->text_.empty()) | ||||
|    { | ||||
|       switch (p->text_.at(0)) | ||||
|       { | ||||
|       case '!': // 0x21
 | ||||
|          symbol = SpecialSymbol::PastStormCellPosition; | ||||
|          break; | ||||
| 
 | ||||
|       case '"': // 0x22
 | ||||
|          symbol = SpecialSymbol::CurrentStormCellPosition; | ||||
|          break; | ||||
| 
 | ||||
|       case '#': // 0x23
 | ||||
|          symbol = SpecialSymbol::ForecastStormCellPosition; | ||||
|          break; | ||||
| 
 | ||||
|       case '$': // 0x24
 | ||||
|          symbol = SpecialSymbol::PastMdaPosition; | ||||
|          break; | ||||
| 
 | ||||
|       case '%': // 0x25
 | ||||
|          symbol = SpecialSymbol::ForecastMdaPosition; | ||||
|          break; | ||||
| 
 | ||||
|       default: | ||||
|          break; | ||||
|       } | ||||
|    } | ||||
| 
 | ||||
|    return symbol; | ||||
| } | ||||
| 
 | ||||
| std::size_t TextAndSpecialSymbolPacket::data_size() const | ||||
| { | ||||
|    return p->lengthOfBlock_ + 4u; | ||||
| } | ||||
|  |  | |||
|  | @ -152,11 +152,13 @@ set(HDR_WSR88D_RPG include/scwx/wsr88d/rpg/ccb_header.hpp | |||
|                    include/scwx/wsr88d/rpg/radar_coded_message.hpp | ||||
|                    include/scwx/wsr88d/rpg/radial_data_packet.hpp | ||||
|                    include/scwx/wsr88d/rpg/raster_data_packet.hpp | ||||
|                    include/scwx/wsr88d/rpg/scit_forecast_data_packet.hpp | ||||
|                    include/scwx/wsr88d/rpg/rpg_types.hpp | ||||
|                    include/scwx/wsr88d/rpg/scit_data_packet.hpp | ||||
|                    include/scwx/wsr88d/rpg/set_color_level_packet.hpp | ||||
|                    include/scwx/wsr88d/rpg/special_graphic_symbol_packet.hpp | ||||
|                    include/scwx/wsr88d/rpg/sti_circle_symbol_packet.hpp | ||||
|                    include/scwx/wsr88d/rpg/storm_id_symbol_packet.hpp | ||||
|                    include/scwx/wsr88d/rpg/storm_tracking_information_message.hpp | ||||
|                    include/scwx/wsr88d/rpg/tabular_alphanumeric_block.hpp | ||||
|                    include/scwx/wsr88d/rpg/tabular_product_message.hpp | ||||
|                    include/scwx/wsr88d/rpg/text_and_special_symbol_packet.hpp | ||||
|  | @ -191,11 +193,12 @@ set(SRC_WSR88D_RPG source/scwx/wsr88d/rpg/ccb_header.cpp | |||
|                    source/scwx/wsr88d/rpg/radar_coded_message.cpp | ||||
|                    source/scwx/wsr88d/rpg/radial_data_packet.cpp | ||||
|                    source/scwx/wsr88d/rpg/raster_data_packet.cpp | ||||
|                    source/scwx/wsr88d/rpg/scit_forecast_data_packet.cpp | ||||
|                    source/scwx/wsr88d/rpg/scit_data_packet.cpp | ||||
|                    source/scwx/wsr88d/rpg/set_color_level_packet.cpp | ||||
|                    source/scwx/wsr88d/rpg/special_graphic_symbol_packet.cpp | ||||
|                    source/scwx/wsr88d/rpg/sti_circle_symbol_packet.cpp | ||||
|                    source/scwx/wsr88d/rpg/storm_id_symbol_packet.cpp | ||||
|                    source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp | ||||
|                    source/scwx/wsr88d/rpg/tabular_alphanumeric_block.cpp | ||||
|                    source/scwx/wsr88d/rpg/tabular_product_message.cpp | ||||
|                    source/scwx/wsr88d/rpg/text_and_special_symbol_packet.cpp | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Dan Paulat
						Dan Paulat