Merge pull request #146 from dpaulat/feature/storm-tracks

Storm Tracking Information
This commit is contained in:
Dan Paulat 2024-02-25 01:53:59 -06:00 committed by GitHub
commit 0a1754db84
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 3858 additions and 720 deletions

View file

@ -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)

View file

@ -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

View file

@ -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

View 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

View 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

View 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,
&center](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

View 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

View file

@ -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);

View file

@ -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_)

View file

@ -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);

View file

@ -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)

View file

@ -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;
}

View file

@ -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:
@ -26,19 +35,22 @@ public:
MapContext(MapContext&&) noexcept;
MapContext& operator=(MapContext&&) noexcept;
std::weak_ptr<QMapLibreGL::Map> map() const;
MapSettings& settings();
float pixel_ratio() 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;
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);

View file

@ -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());
@ -181,12 +192,13 @@ public:
manager::PlacefileManager::Instance()};
std::shared_ptr<manager::RadarProductManager> radarProductManager_;
std::shared_ptr<RadarProductLayer> radarProductLayer_;
std::shared_ptr<AlertLayer> alertLayer_;
std::shared_ptr<OverlayLayer> overlayLayer_;
std::shared_ptr<PlacefileLayer> placefileLayer_;
std::shared_ptr<ColorTableLayer> colorTableLayer_;
std::shared_ptr<RadarSiteLayer> radarSiteLayer_ {nullptr};
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};
std::list<std::shared_ptr<PlacefileLayer>> placefileLayers_ {};
@ -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();

View file

@ -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>

View 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

View 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

View file

@ -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>

View file

@ -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(),
layers.end(),
[&dataLayer](const types::LayerInfo& layer)
{
return layer.type_ == types::LayerType::Data &&
std::get<types::DataLayer>(
layer.description_) == dataLayer;
});
// Find the default layer in the current layer list
auto currentIt =
std::find_if(layers.begin(),
layers.end(),
[&defaultIt](const types::LayerInfo& layer)
{
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;
}
}

View 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

View 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

View file

@ -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"},

View file

@ -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

View file

@ -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) :

View file

@ -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)
{

View file

@ -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.
*

View 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

View 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

View file

@ -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()");

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -6,6 +6,8 @@
#include <memory>
#include <optional>
#include <units/length.h>
namespace scwx
{
namespace wsr88d
@ -21,17 +23,27 @@ public:
explicit LinkedVectorPacket();
~LinkedVectorPacket();
LinkedVectorPacket(const LinkedVectorPacket&) = delete;
LinkedVectorPacket(const LinkedVectorPacket&) = delete;
LinkedVectorPacket& operator=(const LinkedVectorPacket&) = delete;
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;

View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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);

View file

@ -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::string text() 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

View file

@ -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

View file

@ -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

View file

@ -44,7 +44,7 @@ GraphicAlphanumericBlock::GraphicAlphanumericBlock() :
GraphicAlphanumericBlock::~GraphicAlphanumericBlock() = default;
GraphicAlphanumericBlock::GraphicAlphanumericBlock(
GraphicAlphanumericBlock&&) noexcept = default;
GraphicAlphanumericBlock&&) noexcept = default;
GraphicAlphanumericBlock& GraphicAlphanumericBlock::operator=(
GraphicAlphanumericBlock&&) noexcept = default;
@ -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;

View file

@ -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},

View file

@ -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;

View file

@ -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},

View 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

View file

@ -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

View file

@ -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

View file

@ -54,7 +54,7 @@ TabularAlphanumericBlock::TabularAlphanumericBlock() :
TabularAlphanumericBlock::~TabularAlphanumericBlock() = default;
TabularAlphanumericBlock::TabularAlphanumericBlock(
TabularAlphanumericBlock&&) noexcept = default;
TabularAlphanumericBlock&&) noexcept = default;
TabularAlphanumericBlock& TabularAlphanumericBlock::operator=(
TabularAlphanumericBlock&&) noexcept = default;
@ -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);

View file

@ -15,53 +15,45 @@ 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;
TextAndSpecialSymbolPacket::TextAndSpecialSymbolPacket(
TextAndSpecialSymbolPacket&&) noexcept = default;
TextAndSpecialSymbolPacket&&) noexcept = default;
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;
}

View file

@ -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