diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef95a8a3..245da98b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: msvc_version: 2022 qt_version: 6.6.0 qt_arch: win64_msvc2019_64 - qt_modules: qtimageformats + qt_modules: qtimageformats qtpositioning qt_tools: '' conan_arch: x86_64 conan_compiler: Visual Studio @@ -46,7 +46,7 @@ jobs: compiler: gcc qt_version: 6.6.0 qt_arch: gcc_64 - qt_modules: qtimageformats + qt_modules: qtimageformats qtpositioning qt_tools: '' conan_arch: x86_64 conan_compiler: gcc diff --git a/scwx-qt/res/textures/images/crosshairs-24.png b/scwx-qt/res/textures/images/crosshairs-24.png new file mode 100644 index 00000000..f2a7ae2d Binary files /dev/null and b/scwx-qt/res/textures/images/crosshairs-24.png differ diff --git a/scwx-qt/res/textures/images/crosshairs-32.png b/scwx-qt/res/textures/images/crosshairs-32.png new file mode 100644 index 00000000..6163b3ae Binary files /dev/null and b/scwx-qt/res/textures/images/crosshairs-32.png differ diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 650d94f7..6e38717e 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -25,6 +25,7 @@ find_package(QT NAMES Qt6 Network OpenGL OpenGLWidgets + Positioning Widgets REQUIRED) find_package(Qt${QT_VERSION_MAJOR} @@ -33,6 +34,7 @@ find_package(Qt${QT_VERSION_MAJOR} Network OpenGL OpenGLWidgets + Positioning Widgets REQUIRED) @@ -55,6 +57,7 @@ set(HDR_GL source/scwx/qt/gl/gl.hpp 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/placefile_icons.hpp source/scwx/qt/gl/draw/placefile_images.hpp @@ -64,6 +67,7 @@ set(HDR_GL_DRAW source/scwx/qt/gl/draw/draw_item.hpp source/scwx/qt/gl/draw/placefile_triangles.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/placefile_icons.cpp source/scwx/qt/gl/draw/placefile_images.cpp @@ -74,6 +78,7 @@ set(SRC_GL_DRAW source/scwx/qt/gl/draw/draw_item.cpp source/scwx/qt/gl/draw/rectangle.cpp) set(HDR_MANAGER source/scwx/qt/manager/font_manager.hpp source/scwx/qt/manager/placefile_manager.hpp + source/scwx/qt/manager/position_manager.hpp source/scwx/qt/manager/radar_product_manager.hpp source/scwx/qt/manager/radar_product_manager_notifier.hpp source/scwx/qt/manager/resource_manager.hpp @@ -83,6 +88,7 @@ set(HDR_MANAGER source/scwx/qt/manager/font_manager.hpp source/scwx/qt/manager/update_manager.hpp) set(SRC_MANAGER source/scwx/qt/manager/font_manager.cpp source/scwx/qt/manager/placefile_manager.cpp + source/scwx/qt/manager/position_manager.cpp source/scwx/qt/manager/radar_product_manager.cpp source/scwx/qt/manager/radar_product_manager_notifier.cpp source/scwx/qt/manager/resource_manager.cpp @@ -166,7 +172,8 @@ set(HDR_TYPES source/scwx/qt/types/alert_types.hpp source/scwx/qt/types/qt_types.hpp source/scwx/qt/types/radar_product_record.hpp source/scwx/qt/types/text_event_key.hpp - source/scwx/qt/types/text_types.hpp) + source/scwx/qt/types/text_types.hpp + source/scwx/qt/types/texture_types.hpp) set(SRC_TYPES source/scwx/qt/types/alert_types.cpp source/scwx/qt/types/github_types.cpp source/scwx/qt/types/imgui_font.cpp @@ -174,7 +181,8 @@ set(SRC_TYPES source/scwx/qt/types/alert_types.cpp source/scwx/qt/types/map_types.cpp source/scwx/qt/types/radar_product_record.cpp source/scwx/qt/types/text_event_key.cpp - source/scwx/qt/types/text_types.cpp) + source/scwx/qt/types/text_types.cpp + source/scwx/qt/types/texture_types.cpp) set(HDR_UI source/scwx/qt/ui/about_dialog.hpp source/scwx/qt/ui/alert_dialog.hpp source/scwx/qt/ui/alert_dock_widget.hpp @@ -503,6 +511,7 @@ endif() target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::OpenGLWidgets + Qt${QT_VERSION_MAJOR}::Positioning Boost::json Boost::timer qmaplibregl diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc index 298ef828..3e7e55dd 100644 --- a/scwx-qt/scwx-qt.qrc +++ b/scwx-qt/scwx-qt.qrc @@ -59,5 +59,6 @@ res/palettes/wct/ZDR.pal res/textures/lines/default-1x7.png res/textures/lines/test-pattern.png + res/textures/images/crosshairs-24.png diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp new file mode 100644 index 00000000..fb5dad75 --- /dev/null +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp @@ -0,0 +1,894 @@ +#include +#include +#include +#include +#include + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace gl +{ +namespace draw +{ + +static const std::string logPrefix_ = "scwx::qt::gl::draw::geo_icons"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +static constexpr std::size_t kNumRectangles = 1; +static constexpr std::size_t kNumTriangles = kNumRectangles * 2; +static constexpr std::size_t kVerticesPerTriangle = 3; +static constexpr std::size_t kVerticesPerRectangle = kVerticesPerTriangle * 2; +static constexpr std::size_t kPointsPerVertex = 9; +static constexpr std::size_t kPointsPerTexCoord = 3; +static constexpr std::size_t kIconBufferLength = + kNumTriangles * kVerticesPerTriangle * kPointsPerVertex; +static constexpr std::size_t kTextureBufferLength = + kNumTriangles * kVerticesPerTriangle * kPointsPerTexCoord; + +// Threshold, start time, end time +static constexpr std::size_t kIntegersPerVertex_ = 3; + +struct IconInfo +{ + IconInfo(const std::string& iconSheet, + std::size_t iconWidth, + std::size_t iconHeight, + std::int32_t hotX, + std::int32_t hotY) : + iconSheet_ {iconSheet}, + iconWidth_ {iconWidth}, + iconHeight_ {iconHeight}, + hotX_ {hotX}, + hotY_ {hotY} + { + } + + void UpdateTextureInfo(); + + std::string iconSheet_; + std::size_t iconWidth_; + std::size_t iconHeight_; + std::int32_t hotX_; + std::int32_t hotY_; + util::TextureAttributes texture_ {}; + std::size_t rows_ {}; + std::size_t columns_ {}; + std::size_t numIcons_ {}; + float scaledWidth_ {}; + float scaledHeight_ {}; +}; + +struct GeoIconDrawItem +{ + units::length::nautical_miles threshold_ {}; + std::chrono::sys_time startTime_ {}; + std::chrono::sys_time endTime_ {}; + + boost::gil::rgba8_pixel_t modulate_ {255, 255, 255, 255}; + double latitude_ {}; + double longitude_ {}; + double x_ {}; + double y_ {}; + units::degrees angle_ {}; + std::string iconSheet_ {}; + std::size_t iconIndex_ {}; + std::string hoverText_ {}; +}; + +class GeoIcons::Impl +{ +public: + struct IconHoverEntry + { + std::shared_ptr di_; + + glm::vec2 p_; + glm::vec2 otl_; + glm::vec2 otr_; + glm::vec2 obl_; + glm::vec2 obr_; + }; + + explicit Impl(const std::shared_ptr& 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}, + numVertices_ {0} + { + } + + ~Impl() {} + + void UpdateBuffers(); + void UpdateTextureBuffer(); + void Update(bool textureAtlasChanged); + + std::shared_ptr context_; + + bool visible_ {true}; + bool dirty_ {false}; + bool thresholded_ {false}; + bool lastTextureAtlasChanged_ {false}; + + std::chrono::system_clock::time_point selectedTime_ {}; + + std::mutex iconMutex_; + + boost::unordered_flat_map currentIconSheets_ {}; + boost::unordered_flat_map newIconSheets_ {}; + + std::vector> currentIconList_ {}; + std::vector> newIconList_ {}; + std::vector> newValidIconList_ {}; + + std::vector currentIconBuffer_ {}; + std::vector currentIntegerBuffer_ {}; + std::vector newIconBuffer_ {}; + std::vector newIntegerBuffer_ {}; + + std::vector textureBuffer_ {}; + + std::vector currentHoverIcons_ {}; + std::vector newHoverIcons_ {}; + + std::shared_ptr shaderProgram_; + GLint uMVPMatrixLocation_; + GLint uMapMatrixLocation_; + GLint uMapScreenCoordLocation_; + GLint uMapDistanceLocation_; + GLint uSelectedTimeLocation_; + + GLuint vao_; + std::array vbo_; + + GLsizei numVertices_; +}; + +GeoIcons::GeoIcons(const std::shared_ptr& context) : + DrawItem(context->gl()), p(std::make_unique(context)) +{ +} +GeoIcons::~GeoIcons() = default; + +GeoIcons::GeoIcons(GeoIcons&&) noexcept = default; +GeoIcons& GeoIcons::operator=(GeoIcons&&) noexcept = default; + +void GeoIcons::set_selected_time( + std::chrono::system_clock::time_point selectedTime) +{ + p->selectedTime_ = selectedTime; +} + +void GeoIcons::set_thresholded(bool thresholded) +{ + p->thresholded_ = thresholded; +} + +void GeoIcons::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/texture2d_array.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(p->vbo_.size()), p->vbo_.data()); + + gl.glBindVertexArray(p->vao_); + gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); + gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + + // aLatLong + gl.glVertexAttribPointer(0, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + static_cast(0)); + gl.glEnableVertexAttribArray(0); + + // aXYOffset + gl.glVertexAttribPointer(1, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(2 * sizeof(float))); + gl.glEnableVertexAttribArray(1); + + // aModulate + gl.glVertexAttribPointer(3, + 4, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(4 * sizeof(float))); + gl.glEnableVertexAttribArray(3); + + // aAngle + gl.glVertexAttribPointer(4, + 1, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(8 * sizeof(float))); + gl.glEnableVertexAttribArray(4); + + gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); + gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + + // aTexCoord + gl.glVertexAttribPointer(2, + 3, + GL_FLOAT, + GL_FALSE, + kPointsPerTexCoord * sizeof(float), + static_cast(0)); + gl.glEnableVertexAttribArray(2); + + gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[2]); + gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + + // aThreshold + gl.glVertexAttribIPointer(5, // + 1, + GL_INT, + 0, + static_cast(0)); + gl.glEnableVertexAttribArray(5); + + // aTimeRange + gl.glVertexAttribIPointer(6, // + 2, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(1 * sizeof(GLint))); + gl.glEnableVertexAttribArray(6); + + p->dirty_ = true; +} + +void GeoIcons::Render(const QMapLibreGL::CustomLayerRenderParameters& params, + bool textureAtlasChanged) +{ + if (!p->visible_) + { + if (textureAtlasChanged) + { + p->lastTextureAtlasChanged_ = true; + } + + return; + } + + std::unique_lock lock {p->iconMutex_}; + + if (!p->currentIconList_.empty()) + { + gl::OpenGLFunctions& gl = p->context_->gl(); + + gl.glBindVertexArray(p->vao_); + + p->Update(textureAtlasChanged); + 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 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(std::chrono::duration_cast( + selectedTime.time_since_epoch()) + .count())); + + // Interpolate texture coordinates + gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + // Draw icons + gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); + } +} + +void GeoIcons::Deinitialize() +{ + gl::OpenGLFunctions& gl = p->context_->gl(); + + gl.glDeleteVertexArrays(1, &p->vao_); + gl.glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); + + std::unique_lock lock {p->iconMutex_}; + + p->currentIconList_.clear(); + p->currentIconSheets_.clear(); + p->currentHoverIcons_.clear(); + p->currentIconBuffer_.clear(); + p->currentIntegerBuffer_.clear(); + p->textureBuffer_.clear(); +} + +void IconInfo::UpdateTextureInfo() +{ + texture_ = util::TextureAtlas::Instance().GetTextureAttributes(iconSheet_); + + if (iconWidth_ > 0 && iconHeight_ > 0) + { + columns_ = texture_.size_.x / iconWidth_; + rows_ = texture_.size_.y / iconHeight_; + } + else + { + columns_ = 1u; + rows_ = 1u; + + iconWidth_ = static_cast(texture_.size_.x); + iconHeight_ = static_cast(texture_.size_.y); + } + + if (hotX_ == -1 || hotY_ == -1) + { + hotX_ = static_cast(iconWidth_ / 2); + hotY_ = static_cast(iconHeight_ / 2); + } + + numIcons_ = columns_ * rows_; + + // Pixel size + float xFactor = 0.0f; + float yFactor = 0.0f; + + if (texture_.size_.x > 0 && texture_.size_.y > 0) + { + xFactor = (texture_.sRight_ - texture_.sLeft_) / texture_.size_.x; + yFactor = (texture_.tBottom_ - texture_.tTop_) / texture_.size_.y; + } + + scaledWidth_ = iconWidth_ * xFactor; + scaledHeight_ = iconHeight_ * yFactor; +} + +void GeoIcons::SetVisible(bool visible) +{ + p->visible_ = visible; +} + +void GeoIcons::StartIconSheets() +{ + // Clear the new buffer + p->newIconSheets_.clear(); +} + +void GeoIcons::AddIconSheet(const std::string& name, + std::size_t iconWidth, + std::size_t iconHeight, + std::int32_t hotX, + std::int32_t hotY) +{ + // Populate icon sheet map + p->newIconSheets_.emplace(std::piecewise_construct, + std::tuple {name}, + std::forward_as_tuple(IconInfo { + name, iconWidth, iconHeight, hotX, hotY})); +} + +void GeoIcons::FinishIconSheets() +{ + // Update icon sheets + for (auto& iconSheet : p->newIconSheets_) + { + iconSheet.second.UpdateTextureInfo(); + } + + std::unique_lock lock {p->iconMutex_}; + + // Swap buffers + p->currentIconSheets_.swap(p->newIconSheets_); + + // Clear the new buffers + p->newIconSheets_.clear(); + + // Mark the draw item dirty + p->dirty_ = true; +} + +void GeoIcons::StartIcons() +{ + // Clear the new buffer + p->newIconList_.clear(); + p->newValidIconList_.clear(); + p->newIconBuffer_.clear(); + p->newIntegerBuffer_.clear(); + p->newHoverIcons_.clear(); +} + +std::shared_ptr GeoIcons::AddIcon() +{ + return p->newIconList_.emplace_back(std::make_shared()); +} + +void GeoIcons::SetIconTexture(const std::shared_ptr& di, + const std::string& iconSheet, + std::size_t iconIndex) +{ + di->iconSheet_ = iconSheet; + di->iconIndex_ = iconIndex; +} + +void GeoIcons::SetIconLocation(const std::shared_ptr& di, + units::angle::degrees latitude, + units::angle::degrees longitude, + double xOffset, + double yOffset) +{ + di->latitude_ = latitude.value(); + di->longitude_ = longitude.value(); + di->x_ = xOffset; + di->y_ = yOffset; +} + +void GeoIcons::SetIconLocation(const std::shared_ptr& di, + double latitude, + double longitude, + double xOffset, + double yOffset) +{ + di->latitude_ = latitude; + di->longitude_ = longitude; + di->x_ = xOffset; + di->y_ = yOffset; +} + +void GeoIcons::SetIconAngle(const std::shared_ptr& di, + units::angle::degrees angle) +{ + di->angle_ = angle; +} + +void GeoIcons::SetIconModulate(const std::shared_ptr& di, + boost::gil::rgba8_pixel_t modulate) +{ + di->modulate_ = modulate; +} + +void GeoIcons::SetIconHoverText(const std::shared_ptr& di, + const std::string& text) +{ + di->hoverText_ = text; +} + +void GeoIcons::FinishIcons() +{ + // Update buffers + p->UpdateBuffers(); + + std::unique_lock lock {p->iconMutex_}; + + // Swap buffers + p->currentIconList_.swap(p->newValidIconList_); + p->currentIconBuffer_.swap(p->newIconBuffer_); + p->currentIntegerBuffer_.swap(p->newIntegerBuffer_); + p->currentHoverIcons_.swap(p->newHoverIcons_); + + // Clear the new buffers, except the full icon list (used to update buffers + // without re-adding icons) + p->newValidIconList_.clear(); + p->newIconBuffer_.clear(); + p->newIntegerBuffer_.clear(); + p->newHoverIcons_.clear(); + + // Mark the draw item dirty + p->dirty_ = true; +} + +void GeoIcons::Impl::UpdateBuffers() +{ + newIconBuffer_.clear(); + newIconBuffer_.reserve(newIconList_.size() * kIconBufferLength); + newIntegerBuffer_.clear(); + newIntegerBuffer_.reserve(newIconList_.size() * kVerticesPerRectangle * + kIntegersPerVertex_); + newValidIconList_.clear(); + newHoverIcons_.clear(); + + for (auto& di : newIconList_) + { + auto it = currentIconSheets_.find(di->iconSheet_); + if (it == currentIconSheets_.cend()) + { + // No icon sheet found + logger_->warn("Could not find icon sheet: {}", di->iconSheet_); + continue; + } + + auto& icon = it->second; + + // Validate icon + if (di->iconIndex_ >= icon.numIcons_) + { + // No icon found + logger_->warn("Invalid icon index: {}", di->iconIndex_); + continue; + } + + // Icon is valid, add to valid icon list + newValidIconList_.push_back(di); + + // Threshold value + units::length::nautical_miles threshold = di->threshold_; + GLint thresholdValue = static_cast(std::round(threshold.value())); + + // Start and end time + GLint startTime = + static_cast(std::chrono::duration_cast( + di->startTime_.time_since_epoch()) + .count()); + GLint endTime = + static_cast(std::chrono::duration_cast( + di->endTime_.time_since_epoch()) + .count()); + + // Latitude and longitude coordinates in degrees + const float lat = static_cast(di->latitude_); + const float lon = static_cast(di->longitude_); + + // Base X/Y offsets in pixels + const float x = static_cast(di->x_); + const float y = static_cast(di->y_); + + // Icon size + const float iw = static_cast(icon.iconWidth_); + const float ih = static_cast(icon.iconHeight_); + + // Hot X/Y (zero-based icon center) + const float hx = static_cast(icon.hotX_); + const float hy = static_cast(icon.hotY_); + + // Final X/Y offsets in pixels + const float lx = std::roundf(x - hx); + const float rx = std::roundf(lx + iw); + const float ty = std::roundf(y + hy); + const float by = std::roundf(ty - ih); + + // Angle in degrees + units::angle::degrees angle = di->angle_; + const float a = angle.value(); + + // Modulate color + const float mc0 = di->modulate_[0] / 255.0f; + const float mc1 = di->modulate_[1] / 255.0f; + const float mc2 = di->modulate_[2] / 255.0f; + const float mc3 = di->modulate_[3] / 255.0f; + + newIconBuffer_.insert(newIconBuffer_.end(), + { + // Icon + lat, lon, lx, by, mc0, mc1, mc2, mc3, a, // BL + lat, lon, lx, ty, mc0, mc1, mc2, mc3, a, // TL + lat, lon, rx, by, mc0, mc1, mc2, mc3, a, // BR + lat, lon, rx, by, mc0, mc1, mc2, mc3, a, // BR + lat, lon, rx, ty, mc0, mc1, mc2, mc3, a, // TR + lat, lon, 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 radians = angle; + + const auto sc = util::maplibre::LatLongToScreenCoordinate({lat, lon}); + + const float cosAngle = cosf(static_cast(radians.value())); + const float sinAngle = sinf(static_cast(radians.value())); + + const glm::mat2 rotate {cosAngle, -sinAngle, sinAngle, cosAngle}; + + const glm::vec2 otl = rotate * glm::vec2 {lx, ty}; + const glm::vec2 otr = rotate * glm::vec2 {rx, ty}; + const glm::vec2 obl = rotate * glm::vec2 {lx, by}; + const glm::vec2 obr = rotate * glm::vec2 {rx, by}; + + newHoverIcons_.emplace_back( + IconHoverEntry {di, sc, otl, otr, obl, obr}); + } + } +} + +void GeoIcons::Impl::UpdateTextureBuffer() +{ + textureBuffer_.clear(); + textureBuffer_.reserve(currentIconList_.size() * kTextureBufferLength); + + for (auto& di : currentIconList_) + { + auto it = currentIconSheets_.find(di->iconSheet_); + if (it == currentIconSheets_.cend()) + { + // No file found. Should not get here, but insert empty data to match + // up with data already buffered + logger_->error("Could not find icon sheet: {}", di->iconSheet_); + + // clang-format off + textureBuffer_.insert( + textureBuffer_.end(), + { + // Icon + 0.0f, 0.0f, 0.0f, // BL + 0.0f, 0.0f, 0.0f, // TL + 0.0f, 0.0f, 0.0f, // BR + 0.0f, 0.0f, 0.0f, // BR + 0.0f, 0.0f, 0.0f, // TR + 0.0f, 0.0f, 0.0f // TL + }); + // clang-format on + + continue; + } + + auto& icon = it->second; + + // Validate icon + if (di->iconIndex_ >= icon.numIcons_) + { + // No icon found + logger_->error("Invalid icon index: {}", di->iconIndex_); + + // Will get here if a texture changes, and the texture shrunk such that + // the icon is no longer found + + // clang-format off + textureBuffer_.insert( + textureBuffer_.end(), + { + // Icon + 0.0f, 0.0f, 0.0f, // BL + 0.0f, 0.0f, 0.0f, // TL + 0.0f, 0.0f, 0.0f, // BR + 0.0f, 0.0f, 0.0f, // BR + 0.0f, 0.0f, 0.0f, // TR + 0.0f, 0.0f, 0.0f // TL + }); + // clang-format on + + continue; + } + + // Texture coordinates + const std::size_t iconRow = (di->iconIndex_) / icon.columns_; + const std::size_t iconColumn = (di->iconIndex_) % icon.columns_; + + const float iconX = iconColumn * icon.scaledWidth_; + const float iconY = iconRow * icon.scaledHeight_; + + const float ls = icon.texture_.sLeft_ + iconX; + const float rs = ls + icon.scaledWidth_; + const float tt = icon.texture_.tTop_ + iconY; + const float bt = tt + icon.scaledHeight_; + const float r = static_cast(icon.texture_.layerId_); + + // clang-format off + textureBuffer_.insert( + textureBuffer_.end(), + { + // Icon + ls, bt, r, // BL + ls, tt, r, // TL + rs, bt, r, // BR + rs, bt, r, // BR + rs, tt, r, // TR + ls, tt, r // TL + }); + // clang-format on + } +} + +void GeoIcons::Impl::Update(bool textureAtlasChanged) +{ + gl::OpenGLFunctions& gl = context_->gl(); + + // If the texture atlas has changed + if (dirty_ || textureAtlasChanged || lastTextureAtlasChanged_) + { + // Update texture coordinates + for (auto& iconSheet : currentIconSheets_) + { + iconSheet.second.UpdateTextureInfo(); + } + + // Update OpenGL texture buffer data + UpdateTextureBuffer(); + + // Buffer texture data + gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); + gl.glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * textureBuffer_.size(), + textureBuffer_.data(), + GL_DYNAMIC_DRAW); + + lastTextureAtlasChanged_ = false; + } + + // If buffers need updating + if (dirty_) + { + // Buffer vertex data + gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); + gl.glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * currentIconBuffer_.size(), + currentIconBuffer_.data(), + GL_DYNAMIC_DRAW); + + // Buffer threshold data + gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]); + gl.glBufferData(GL_ARRAY_BUFFER, + sizeof(GLint) * currentIntegerBuffer_.size(), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); + + numVertices_ = + static_cast(currentIconBuffer_.size() / kPointsPerVertex); + } + + dirty_ = false; +} + +bool GeoIcons::RunMousePicking( + const QMapLibreGL::CustomLayerRenderParameters& params, + const QPointF& /* mouseLocalPos */, + const QPointF& mouseGlobalPos, + const glm::vec2& mouseCoords) +{ + std::unique_lock lock {p->iconMutex_}; + + 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(params.bearing), + glm::vec3(0.0f, 0.0f, 1.0f)); + + units::length::meters mapDistance = + (p->thresholded_) ? util::maplibre::GetMapDistance(params) : + units::length::meters {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 icon + auto it = std::find_if( + std::execution::par_unseq, + p->currentHoverIcons_.crbegin(), + p->currentHoverIcons_.crend(), + [&mapDistance, &selectedTime, &mapMatrix, &mouseCoords](const auto& icon) + { + if (( + // Geo icon is thresholded + mapDistance > units::length::meters {0.0} && + + // Geo icon threshold is < 999 nmi + static_cast(std::round( + units::length::nautical_miles {icon.di_->threshold_} + .value())) < 999 && + + // Map distance is beyond the threshold + icon.di_->threshold_ < mapDistance) || + + ( + // Geo icon has a start time + icon.di_->startTime_ != + std::chrono::system_clock::time_point {} && + + // The time range has not yet started + (selectedTime < icon.di_->startTime_ || + + // The time range has ended + icon.di_->endTime_ <= selectedTime))) + { + // Icon is not pickable + return false; + } + + // Initialize vertices + glm::vec2 bl = icon.p_; + glm::vec2 br = bl; + glm::vec2 tl = br; + glm::vec2 tr = tl; + + // Calculate offsets + // - Rotated offset is based on final X/Y offsets (pixels) + // - Multiply the offset by the scaled and rotated map matrix + const glm::vec2 otl = mapMatrix * glm::vec4 {icon.otl_, 0.0f, 1.0f}; + const glm::vec2 obl = mapMatrix * glm::vec4 {icon.obl_, 0.0f, 1.0f}; + const glm::vec2 obr = mapMatrix * glm::vec4 {icon.obr_, 0.0f, 1.0f}; + const glm::vec2 otr = mapMatrix * glm::vec4 {icon.otr_, 0.0f, 1.0f}; + + // Offset vertices + tl += otl; + bl += obl; + br += obr; + tr += otr; + + // Test point against polygon bounds + return util::maplibre::IsPointInPolygon({tl, bl, br, tr}, mouseCoords); + }); + + if (it != p->currentHoverIcons_.crend()) + { + itemPicked = true; + util::tooltip::Show(it->di_->hoverText_, mouseGlobalPos); + } + + return itemPicked; +} + +} // namespace draw +} // namespace gl +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp new file mode 100644 index 00000000..9d4bc9a7 --- /dev/null +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp @@ -0,0 +1,176 @@ +#pragma once + +#include +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace gl +{ +namespace draw +{ + +struct GeoIconDrawItem; + +class GeoIcons : public DrawItem +{ +public: + explicit GeoIcons(const std::shared_ptr& context); + ~GeoIcons(); + + GeoIcons(const GeoIcons&) = delete; + GeoIcons& operator=(const GeoIcons&) = delete; + + GeoIcons(GeoIcons&&) noexcept; + GeoIcons& operator=(GeoIcons&&) 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, + bool textureAtlasChanged) override; + void Deinitialize() override; + + bool RunMousePicking(const QMapLibreGL::CustomLayerRenderParameters& params, + const QPointF& mouseLocalPos, + const QPointF& mouseGlobalPos, + const glm::vec2& mouseCoords) override; + + /** + * Sets the visibility of the geo icons. + * + * @param [in] visible Icon visibility + */ + void SetVisible(bool visible); + + /** + * Resets and prepares the draw item for adding a new set of icon sheets. + */ + void StartIconSheets(); + + /** + * Adds an icon sheet for drawing the geo icons. The icon sheet must already + * exist in the texture atlas. + * + * @param [in] name The name of the icon sheet in the texture atlas + * @param [in] iconWidth The width of each icon in the icon sheet. Default is + * 0 for a single icon. + * @param [in] iconHeight The height of each icon in the icon sheet. Default + * is 0 for a single icon. + * @param [in] hotX The zero-based center of the each icon in the icon sheet. + * Default is -1 to center the icon. + * @param [in] hotY The zero-based center of the each icon in the icon sheet. + * Default is -1 to center the icon. + */ + void AddIconSheet(const std::string& name, + std::size_t iconWidth = 0, + std::size_t iconHeight = 0, + std::int32_t hotX = -1, + std::int32_t hotY = -1); + + /** + * Resets and prepares the draw item for adding a new set of icon sheets. + */ + void FinishIconSheets(); + + /** + * Resets and prepares the draw item for adding a new set of icons. + */ + void StartIcons(); + + /** + * Adds a geo icon to the internal draw list. + * + * @return Geo icon draw item + */ + std::shared_ptr AddIcon(); + + /** + * Sets the texture of a geo icon. + * + * @param [in] di Geo icon draw item + * @param [in] iconSheet The name of the icon sheet in the texture atlas + * @param [in] iconIndex The zero-based index of the icon in the icon sheet + */ + static void SetIconTexture(const std::shared_ptr& di, + const std::string& iconSheet, + std::size_t iconIndex); + + /** + * Sets the location of a geo icon. + * + * @param [in] di Geo icon draw item + * @param [in] latitude The latitude of the geo icon in degrees. + * @param [in] longitude The longitude of the geo icon in degrees. + * @param [in] xOffset The x-offset of the geo icon in pixels. Default is 0. + * @param [in] yOffset The y-offset of the geo icon in pixels. Default is 0. + */ + static void SetIconLocation(const std::shared_ptr& di, + units::angle::degrees latitude, + units::angle::degrees longitude, + double xOffset = 0.0, + double yOffset = 0.0); + + /** + * Sets the location of a geo icon. + * + * @param [in] di Geo icon draw item + * @param [in] latitude The latitude of the geo icon in degrees. + * @param [in] longitude The longitude of the geo icon in degrees. + * @param [in] xOffset The x-offset of the geo icon in pixels. Default is 0. + * @param [in] yOffset The y-offset of the geo icon in pixels. Default is 0. + */ + static void SetIconLocation(const std::shared_ptr& di, + double latitude, + double longitude, + double xOffset = 0.0, + double yOffset = 0.0); + + /** + * Sets the angle of a geo icon. + * + * @param [in] di Geo icon draw item + * @param [in] angle Angle in degrees + */ + static void SetIconAngle(const std::shared_ptr& di, + units::angle::degrees angle); + + /** + * Sets the modulate color of a geo icon. + * + * @param [in] di Geo icon draw item + * @param [in] modulate Modulate color + */ + static void SetIconModulate(const std::shared_ptr& di, + boost::gil::rgba8_pixel_t modulate); + + /** + * Sets the hover text of a geo icon. + * + * @param [in] di Geo icon draw item + * @param [in] text Hover text + */ + static void SetIconHoverText(const std::shared_ptr& di, + const std::string& text); + + /** + * Finalizes the draw item after adding new icons. + */ + void FinishIcons(); + +private: + class Impl; + + std::unique_ptr p; +}; + +} // namespace draw +} // namespace gl +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index d07ca41c..3f4765b4 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -82,6 +83,7 @@ public: settingsDialog_ {nullptr}, updateDialog_ {nullptr}, placefileManager_ {manager::PlacefileManager::Instance()}, + positionManager_ {manager::PositionManager::Instance()}, textEventManager_ {manager::TextEventManager::Instance()}, timelineManager_ {manager::TimelineManager::Instance()}, updateManager_ {manager::UpdateManager::Instance()}, @@ -117,6 +119,11 @@ public: settings_.setApiKey(QString {mapProviderApiKey.c_str()}); settings_.setCacheDatabasePath(QString {cacheDbPath.c_str()}); settings_.setCacheDatabaseMaximumSize(20 * 1024 * 1024); + + if (settings::GeneralSettings::Instance().track_location().GetValue()) + { + positionManager_->TrackLocation(true); + } } ~MainWindowImpl() { threadPool_.join(); } @@ -172,6 +179,7 @@ public: ui::UpdateDialog* updateDialog_; std::shared_ptr placefileManager_; + std::shared_ptr positionManager_; std::shared_ptr textEventManager_; std::shared_ptr timelineManager_; std::shared_ptr updateManager_; @@ -244,9 +252,13 @@ MainWindow::MainWindow(QWidget* parent) : p->mapSettingsGroup_ = new ui::CollapsibleGroup(tr("Map Settings"), this); p->mapSettingsGroup_->GetContentsLayout()->addWidget(ui->mapStyleLabel); p->mapSettingsGroup_->GetContentsLayout()->addWidget(ui->mapStyleComboBox); + p->mapSettingsGroup_->GetContentsLayout()->addWidget( + ui->trackLocationCheckBox); ui->radarToolboxScrollAreaContents->layout()->replaceWidget( ui->mapSettingsGroupBox, p->mapSettingsGroup_); ui->mapSettingsGroupBox->setVisible(false); + ui->trackLocationCheckBox->setChecked( + settings::GeneralSettings::Instance().track_location().GetValue()); // Add Level 2 Products p->level2ProductsGroup_ = @@ -846,6 +858,19 @@ void MainWindowImpl::ConnectOtherSignals() } } }); + connect(mainWindow_->ui->trackLocationCheckBox, + &QCheckBox::stateChanged, + mainWindow_, + [this](int state) + { + bool trackingEnabled = (state == Qt::CheckState::Checked); + + settings::GeneralSettings::Instance().track_location().StageValue( + trackingEnabled); + + // Turn on location tracking + positionManager_->TrackLocation(trackingEnabled); + }); connect(level2ProductsWidget_, &ui::Level2ProductsWidget::RadarProductSelected, mainWindow_, diff --git a/scwx-qt/source/scwx/qt/main/main_window.ui b/scwx-qt/source/scwx/qt/main/main_window.ui index 9cd627eb..56c659f3 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.ui +++ b/scwx-qt/source/scwx/qt/main/main_window.ui @@ -39,7 +39,7 @@ 0 0 1024 - 21 + 22 @@ -142,7 +142,7 @@ 0 0 157 - 702 + 697 @@ -244,6 +244,13 @@ + + + + Track Location + + + diff --git a/scwx-qt/source/scwx/qt/manager/position_manager.cpp b/scwx-qt/source/scwx/qt/manager/position_manager.cpp new file mode 100644 index 00000000..23357444 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/position_manager.cpp @@ -0,0 +1,138 @@ +#include +#include +#include + +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +static const std::string logPrefix_ = "scwx::qt::manager::position_manager"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class PositionManager::Impl +{ +public: + explicit Impl(PositionManager* self) : + self_ {self}, trackingUuid_ {boost::uuids::random_generator()()} + { + // TODO: macOS requires permission + geoPositionInfoSource_ = + QGeoPositionInfoSource::createDefaultSource(self); + + if (geoPositionInfoSource_ != nullptr) + { + logger_->debug("Using position source: {}", + geoPositionInfoSource_->sourceName().toStdString()); + + QObject::connect(geoPositionInfoSource_, + &QGeoPositionInfoSource::positionUpdated, + self_, + [this](const QGeoPositionInfo& info) + { + auto coordinate = info.coordinate(); + + if (coordinate != position_.coordinate()) + { + logger_->debug("Position updated: {}, {}", + coordinate.latitude(), + coordinate.longitude()); + } + + position_ = info; + + Q_EMIT self_->PositionUpdated(info); + }); + } + } + + ~Impl() {} + + PositionManager* self_; + + boost::uuids::uuid trackingUuid_; + bool trackingEnabled_ {false}; + + std::set uuids_ {}; + + QGeoPositionInfoSource* geoPositionInfoSource_ {}; + QGeoPositionInfo position_ {}; +}; + +PositionManager::PositionManager() : p(std::make_unique(this)) {} +PositionManager::~PositionManager() = default; + +QGeoPositionInfo PositionManager::position() const +{ + return p->position_; +} + +bool PositionManager::IsLocationTracked() +{ + return p->trackingEnabled_; +} + +void PositionManager::EnablePositionUpdates(boost::uuids::uuid uuid, + bool enabled) +{ + if (p->geoPositionInfoSource_ == nullptr) + { + return; + } + + if (enabled) + { + if (p->uuids_.empty()) + { + p->geoPositionInfoSource_->startUpdates(); + } + + p->uuids_.insert(uuid); + } + else + { + p->uuids_.erase(uuid); + + if (p->uuids_.empty()) + { + p->geoPositionInfoSource_->stopUpdates(); + } + } +} + +void PositionManager::TrackLocation(bool trackingEnabled) +{ + p->trackingEnabled_ = trackingEnabled; + EnablePositionUpdates(p->trackingUuid_, trackingEnabled); + Q_EMIT LocationTrackingChanged(trackingEnabled); +} + +std::shared_ptr PositionManager::Instance() +{ + static std::weak_ptr positionManagerReference_ {}; + static std::mutex instanceMutex_ {}; + + std::unique_lock lock(instanceMutex_); + + std::shared_ptr positionManager = + positionManagerReference_.lock(); + + if (positionManager == nullptr) + { + positionManager = std::make_shared(); + positionManagerReference_ = positionManager; + } + + return positionManager; +} + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/position_manager.hpp b/scwx-qt/source/scwx/qt/manager/position_manager.hpp new file mode 100644 index 00000000..367114c5 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/position_manager.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include +#include + +class QGeoPositionInfo; + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +class PositionManager : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(PositionManager) + +public: + explicit PositionManager(); + ~PositionManager(); + + QGeoPositionInfo position() const; + + bool IsLocationTracked(); + + void EnablePositionUpdates(boost::uuids::uuid uuid, bool enabled); + void TrackLocation(bool trackingEnabled); + + static std::shared_ptr Instance(); + +signals: + void LocationTrackingChanged(bool trackingEnabled); + void PositionUpdated(const QGeoPositionInfo& info); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp index 3048fc6c..df4b4f77 100644 --- a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -91,10 +92,19 @@ static void LoadFonts() static void LoadTextures() { util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); - textureAtlas.RegisterTexture("lines/default-1x7", - ":/res/textures/lines/default-1x7.png"); - textureAtlas.RegisterTexture("lines/test-pattern", - ":/res/textures/lines/test-pattern.png"); + + for (auto imageTexture : types::ImageTextureIterator()) + { + textureAtlas.RegisterTexture(GetTextureName(imageTexture), + GetTexturePath(imageTexture)); + } + + for (auto lineTexture : types::LineTextureIterator()) + { + textureAtlas.RegisterTexture(GetTextureName(lineTexture), + GetTexturePath(lineTexture)); + } + textureAtlas.BuildAtlas(2048, 2048); } diff --git a/scwx-qt/source/scwx/qt/map/generic_layer.hpp b/scwx-qt/source/scwx/qt/map/generic_layer.hpp index 239d271b..355680b8 100644 --- a/scwx-qt/source/scwx/qt/map/generic_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/generic_layer.hpp @@ -45,6 +45,9 @@ public: const QPointF& mouseGlobalPos, const glm::vec2& mouseCoords); +signals: + void NeedsRendering(); + protected: std::shared_ptr context() const; diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 7c543bdd..a897748b 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -969,6 +969,11 @@ void MapWidgetImpl::AddLayer(const std::string& id, layerList_.push_back(id); genericLayers_.push_back(layer); + + connect(layer.get(), + &GenericLayer::NeedsRendering, + widget_, + [this]() { widget_->update(); }); } catch (const std::exception&) { diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index c0577063..fdf17463 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -1,25 +1,19 @@ #include +#include #include -#include +#include +#include #include #include #include -#include #if defined(_MSC_VER) # pragma warning(push, 0) #endif -#include -#include -#include -#include -#include -#include -#include #include -#include +#include #if !defined(_MSC_VER) # include @@ -44,13 +38,24 @@ class OverlayLayerImpl public: explicit OverlayLayerImpl(std::shared_ptr context) : activeBoxOuter_ {std::make_shared(context)}, - activeBoxInner_ {std::make_shared(context)} + activeBoxInner_ {std::make_shared(context)}, + icons_ {std::make_shared(context)}, + locationIconName_ { + types::GetTextureName(types::ImageTexture::Crosshairs24)} { } ~OverlayLayerImpl() = default; + std::shared_ptr positionManager_ { + manager::PositionManager::Instance()}; + QGeoPositionInfo currentPosition_ {}; + std::shared_ptr activeBoxOuter_; std::shared_ptr activeBoxInner_; + std::shared_ptr icons_; + + const std::string& locationIconName_; + std::shared_ptr locationIcon_ {}; std::string sweepTimeString_ {}; bool sweepTimeNeedsUpdate_ {true}; @@ -62,6 +67,7 @@ OverlayLayer::OverlayLayer(std::shared_ptr context) : { AddDrawItem(p->activeBoxOuter_); AddDrawItem(p->activeBoxInner_); + AddDrawItem(p->icons_); p->activeBoxOuter_->SetPosition(0.0f, 0.0f); } @@ -83,6 +89,45 @@ void OverlayLayer::Initialize() this, &OverlayLayer::UpdateSweepTimeNextFrame); } + + p->currentPosition_ = p->positionManager_->position(); + auto coordinate = p->currentPosition_.coordinate(); + + p->icons_->StartIconSheets(); + p->icons_->AddIconSheet(p->locationIconName_); + p->icons_->FinishIconSheets(); + + p->icons_->StartIcons(); + p->locationIcon_ = p->icons_->AddIcon(); + gl::draw::GeoIcons::SetIconTexture( + p->locationIcon_, p->locationIconName_, 0); + gl::draw::GeoIcons::SetIconAngle(p->locationIcon_, + units::angle::degrees {45.0}); + gl::draw::GeoIcons::SetIconLocation( + p->locationIcon_, coordinate.latitude(), coordinate.longitude()); + p->icons_->FinishIcons(); + + connect(p->positionManager_.get(), + &manager::PositionManager::LocationTrackingChanged, + this, + [this]() { Q_EMIT NeedsRendering(); }); + connect(p->positionManager_.get(), + &manager::PositionManager::PositionUpdated, + this, + [this](const QGeoPositionInfo& position) + { + auto coordinate = position.coordinate(); + if (position.isValid() && + p->currentPosition_.coordinate() != coordinate) + { + gl::draw::GeoIcons::SetIconLocation(p->locationIcon_, + coordinate.latitude(), + coordinate.longitude()); + p->icons_->FinishIcons(); + Q_EMIT NeedsRendering(); + } + p->currentPosition_ = position; + }); } void OverlayLayer::Render( @@ -95,6 +140,9 @@ void OverlayLayer::Render( context()->set_render_parameters(params); + // Set OpenGL blend mode for transparency + gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + p->sweepTimePicked_ = false; if (p->sweepTimeNeedsUpdate_ && radarProductView != nullptr) @@ -127,6 +175,10 @@ void OverlayLayer::Render( p->activeBoxInner_->SetBorder(1.0f * pixelRatio, {255, 255, 255, 255}); } + // Location Icon + p->icons_->SetVisible(p->currentPosition_.isValid() && + p->positionManager_->IsLocationTracked()); + DrawLayer::Render(params); if (radarProductView != nullptr) @@ -208,6 +260,17 @@ void OverlayLayer::Deinitialize() this, &OverlayLayer::UpdateSweepTimeNextFrame); } + + disconnect(p->positionManager_.get(), + &manager::PositionManager::LocationTrackingChanged, + this, + nullptr); + disconnect(p->positionManager_.get(), + &manager::PositionManager::PositionUpdated, + this, + nullptr); + + p->locationIcon_ = nullptr; } bool OverlayLayer::RunMousePicking( diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index 997d4e06..0eab43e9 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -42,6 +42,7 @@ public: mapProvider_.SetDefault(defaultMapProviderValue); mapboxApiKey_.SetDefault("?"); maptilerApiKey_.SetDefault("?"); + trackLocation_.SetDefault(false); updateNotificationsEnabled_.SetDefault(true); fontSizes_.SetElementMinimum(1); @@ -118,6 +119,7 @@ public: SettingsVariable mapProvider_ {"map_provider"}; SettingsVariable mapboxApiKey_ {"mapbox_api_key"}; SettingsVariable maptilerApiKey_ {"maptiler_api_key"}; + SettingsVariable trackLocation_ {"track_location"}; SettingsVariable updateNotificationsEnabled_ {"update_notifications"}; }; @@ -137,6 +139,7 @@ GeneralSettings::GeneralSettings() : &p->mapProvider_, &p->mapboxApiKey_, &p->maptilerApiKey_, + &p->trackLocation_, &p->updateNotificationsEnabled_}); SetDefaults(); } @@ -212,6 +215,11 @@ SettingsVariable& GeneralSettings::maptiler_api_key() const return p->maptilerApiKey_; } +SettingsVariable& GeneralSettings::track_location() const +{ + return p->trackLocation_; +} + SettingsVariable& GeneralSettings::update_notifications_enabled() const { return p->updateNotificationsEnabled_; @@ -225,6 +233,7 @@ bool GeneralSettings::Shutdown() dataChanged |= p->loopDelay_.Commit(); dataChanged |= p->loopSpeed_.Commit(); dataChanged |= p->loopTime_.Commit(); + dataChanged |= p->trackLocation_.Commit(); return dataChanged; } @@ -250,6 +259,7 @@ bool operator==(const GeneralSettings& lhs, const GeneralSettings& rhs) lhs.p->mapProvider_ == rhs.p->mapProvider_ && lhs.p->mapboxApiKey_ == rhs.p->mapboxApiKey_ && lhs.p->maptilerApiKey_ == rhs.p->maptilerApiKey_ && + lhs.p->trackLocation_ == rhs.p->trackLocation_ && lhs.p->updateNotificationsEnabled_ == rhs.p->updateNotificationsEnabled_); } diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.hpp b/scwx-qt/source/scwx/qt/settings/general_settings.hpp index 57045ac6..9c6256b7 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.hpp @@ -38,6 +38,7 @@ public: SettingsVariable& map_provider() const; SettingsVariable& mapbox_api_key() const; SettingsVariable& maptiler_api_key() const; + SettingsVariable& track_location() const; SettingsVariable& update_notifications_enabled() const; static GeneralSettings& Instance(); diff --git a/scwx-qt/source/scwx/qt/types/texture_types.cpp b/scwx-qt/source/scwx/qt/types/texture_types.cpp new file mode 100644 index 00000000..5f2123ad --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/texture_types.cpp @@ -0,0 +1,50 @@ +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace types +{ + +struct TextureInfo +{ + std::string name_ {}; + std::string path_ {}; +}; + +static const std::unordered_map imageTextureInfo_ { + {ImageTexture::Crosshairs24, + {"images/crosshairs-24", ":/res/textures/images/crosshairs-24.png"}}}; + +static const std::unordered_map lineTextureInfo_ { + {LineTexture::Default1x7, + {"lines/default-1x7", ":/res/textures/lines/default-1x7.png"}}, + {LineTexture::TestPattern, + {"lines/test-pattern", ":/res/textures/lines/test-pattern.png"}}}; + +const std::string& GetTextureName(ImageTexture imageTexture) +{ + return imageTextureInfo_.at(imageTexture).name_; +} + +const std::string& GetTextureName(LineTexture lineTexture) +{ + return lineTextureInfo_.at(lineTexture).name_; +} + +const std::string& GetTexturePath(ImageTexture imageTexture) +{ + return imageTextureInfo_.at(imageTexture).path_; +} + +const std::string& GetTexturePath(LineTexture lineTexture) +{ + return lineTextureInfo_.at(lineTexture).path_; +} + +} // namespace types +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/texture_types.hpp b/scwx-qt/source/scwx/qt/types/texture_types.hpp new file mode 100644 index 00000000..c40f7141 --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/texture_types.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace types +{ + +enum class ImageTexture +{ + Crosshairs24, + Crosshairs32 +}; +typedef scwx::util::Iterator + ImageTextureIterator; + +enum class LineTexture +{ + Default1x7, + TestPattern +}; +typedef scwx::util:: + Iterator + LineTextureIterator; + +const std::string& GetTextureName(ImageTexture imageTexture); +const std::string& GetTextureName(LineTexture lineTexture); +const std::string& GetTexturePath(ImageTexture imageTexture); +const std::string& GetTexturePath(LineTexture lineTexture); + +} // namespace types +} // namespace qt +} // namespace scwx diff --git a/test/data b/test/data index fc428fa1..49a5bf59 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit fc428fa1460b3d5ce04646727e379e2f4f90f5ec +Subproject commit 49a5bf59c30822b585b5726d7262b8e4ed4f10a7