diff --git a/.gitmodules b/.gitmodules
index 880712a3..bfaddd87 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -31,3 +31,9 @@
[submodule "external/date"]
path = external/date
url = https://github.com/HowardHinnant/date.git
+[submodule "external/units"]
+ path = external/units
+ url = https://github.com/nholthaus/units.git
+[submodule "external/textflowcpp"]
+ path = external/textflowcpp
+ url = https://github.com/catchorg/textflowcpp.git
diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md
index b5a5b93a..4e6fffa0 100644
--- a/ACKNOWLEDGEMENTS.md
+++ b/ACKNOWLEDGEMENTS.md
@@ -18,6 +18,7 @@ Supercell Wx uses code from the following dependencies:
| [CSS Color Parser](https://github.com/deanm/css-color-parser-js) | [MIT License](https://spdx.org/licenses/MIT.html) | Ported to C++ for MapLibre Native |
| [Date](https://github.com/HowardHinnant/date) | [MIT License](https://spdx.org/licenses/MIT.html) |
| [Dear ImGui](https://github.com/ocornut/imgui) | [MIT License](https://spdx.org/licenses/MIT.html) |
+| [fontconfig](http://fontconfig.org/) | [MIT License](https://spdx.org/licenses/MIT.html) |
| [FreeType](https://freetype.org/) | [Freetype Project License](https://spdx.org/licenses/FTL.html) |
| [FreeType GL](https://github.com/rougier/freetype-gl) | [BSD 2-Clause with views sentence](https://spdx.org/licenses/BSD-2-Clause-Views.html) |
| [GeographicLib](https://geographiclib.sourceforge.io/) | [MIT License](https://spdx.org/licenses/MIT.html) |
@@ -36,6 +37,8 @@ Supercell Wx uses code from the following dependencies:
| [spdlog](https://github.com/gabime/spdlog) | [MIT License](https://spdx.org/licenses/MIT.html) |
| [SQLite](https://www.sqlite.org/) | Public Domain |
| [stb](https://github.com/nothings/stb) | Public Domain |
+| [TextFlowCpp](https://github.com/catchorg/textflowcpp) | [Boost Software License 1.0](https://spdx.org/licenses/BSL-1.0.html) |
+| [Units](https://github.com/nholthaus/units) | [MIT License](https://spdx.org/licenses/MIT.html) |
| [Vulkan SDK](https://www.vulkan.org/) | [Apache License 2.0](https://spdx.org/licenses/Apache-2.0.html) |
| [zlib](https://zlib.net/) | [zlib License](https://spdx.org/licenses/Zlib.html) |
@@ -55,7 +58,9 @@ Supercell Wx uses assets from the following sources:
| Source | License | Notes |
| ------ | ------- | ----- |
+| Alte DIN 1451 Mittelschrift | SIL Open Font License |
| [Font Awesome Free](https://fontawesome.com/) | CC BY 4.0 License |
+| [Inconsolata](https://fonts.google.com/specimen/Inconsolata) | SIL Open Font License |
| [NOAA's Weather and Climate Toolkit](https://www.ncdc.noaa.gov/wct/) | Public Domain | Default Color Tables |
| [Supercell thunderstorm with dramatic clouds](https://www.shutterstock.com/image-photo/supercell-thunderstorm-dramatic-clouds-1354353521) | Shutterstock Standard License | Photo by John Sirlin
diff --git a/conanfile.py b/conanfile.py
index 4a803580..af9b5b90 100644
--- a/conanfile.py
+++ b/conanfile.py
@@ -4,6 +4,7 @@ class SupercellWxConan(ConanFile):
settings = ("os", "compiler", "build_type", "arch")
requires = ("boost/1.81.0",
"cpr/1.9.3",
+ "fontconfig/2.14.2",
"freetype/2.12.1",
"geographiclib/1.52",
"glew/2.2.0",
diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt
index 65b2ef20..3c0be1cf 100644
--- a/external/CMakeLists.txt
+++ b/external/CMakeLists.txt
@@ -10,7 +10,9 @@ set_property(DIRECTORY
hsluv-c.cmake
imgui.cmake
mapbox-gl-native.cmake
- stb.cmake)
+ stb.cmake
+ textflowcpp.cmake
+ units.cmake)
include(aws-sdk-cpp.cmake)
include(date.cmake)
@@ -19,3 +21,5 @@ include(hsluv-c.cmake)
include(imgui.cmake)
include(mapbox-gl-native.cmake)
include(stb.cmake)
+include(textflowcpp.cmake)
+include(units.cmake)
diff --git a/external/mapbox-gl-native b/external/mapbox-gl-native
index fbb06ff5..3e85454f 160000
--- a/external/mapbox-gl-native
+++ b/external/mapbox-gl-native
@@ -1 +1 @@
-Subproject commit fbb06ff53e74d3a81b434b84fff1a5dfe4b2d3c7
+Subproject commit 3e85454fe5e571e7b235131912bb867ef9d75c3c
diff --git a/external/stb b/external/stb
index 8b5f1f37..5736b15f 160000
--- a/external/stb
+++ b/external/stb
@@ -1 +1 @@
-Subproject commit 8b5f1f37b5b75829fc72d38e7b5d4bcbf8a26d55
+Subproject commit 5736b15f7ea0ffb08dd38af21067c314d6a3aae9
diff --git a/external/textflowcpp b/external/textflowcpp
new file mode 160000
index 00000000..12010ddc
--- /dev/null
+++ b/external/textflowcpp
@@ -0,0 +1 @@
+Subproject commit 12010ddc8d15538ceea20622d22977e7c5a25da5
diff --git a/external/textflowcpp.cmake b/external/textflowcpp.cmake
new file mode 100644
index 00000000..1e36da18
--- /dev/null
+++ b/external/textflowcpp.cmake
@@ -0,0 +1,4 @@
+cmake_minimum_required(VERSION 3.20)
+set(PROJECT_NAME scwx-textflowcpp)
+
+set(TEXTFLOWCPP_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/textflowcpp PARENT_SCOPE)
diff --git a/external/units b/external/units
new file mode 160000
index 00000000..da6dd917
--- /dev/null
+++ b/external/units
@@ -0,0 +1 @@
+Subproject commit da6dd9176e8515323c75030d5e51ee19cf6c9afd
diff --git a/external/units.cmake b/external/units.cmake
new file mode 100644
index 00000000..d037ae54
--- /dev/null
+++ b/external/units.cmake
@@ -0,0 +1,4 @@
+cmake_minimum_required(VERSION 3.20)
+set(PROJECT_NAME scwx-units)
+
+add_subdirectory(units)
diff --git a/scwx-qt/gl/color.frag b/scwx-qt/gl/color.frag
index 003c6c8d..1266306b 100644
--- a/scwx-qt/gl/color.frag
+++ b/scwx-qt/gl/color.frag
@@ -1,5 +1,5 @@
#version 330 core
-in vec4 color;
+smooth in vec4 color;
layout (location = 0) out vec4 fragColor;
diff --git a/scwx-qt/gl/color.vert b/scwx-qt/gl/color.vert
index 5c8d1ff7..f849710c 100644
--- a/scwx-qt/gl/color.vert
+++ b/scwx-qt/gl/color.vert
@@ -4,7 +4,7 @@ layout (location = 1) in vec4 aColor;
uniform mat4 uMVPMatrix;
-out vec4 color;
+smooth out vec4 color;
void main()
{
diff --git a/scwx-qt/gl/geo_line.vert b/scwx-qt/gl/geo_line.vert
index ea7d6351..487b8041 100644
--- a/scwx-qt/gl/geo_line.vert
+++ b/scwx-qt/gl/geo_line.vert
@@ -8,15 +8,15 @@
layout (location = 0) in vec2 aLatLong;
layout (location = 1) in vec2 aXYOffset;
-layout (location = 2) in vec2 aTexCoord;
+layout (location = 2) in vec3 aTexCoord;
layout (location = 3) in vec4 aModulate;
uniform mat4 uMVPMatrix;
uniform mat4 uMapMatrix;
uniform vec2 uMapScreenCoord;
-smooth out vec2 texCoord;
-flat out vec4 modulate;
+smooth out vec3 texCoord;
+smooth out vec4 color;
vec2 latLngToScreenCoordinate(in vec2 latLng)
{
@@ -31,7 +31,7 @@ void main()
{
// Pass the texture coordinate and color modulate to the fragment shader
texCoord = aTexCoord;
- modulate = aModulate;
+ color = aModulate;
vec2 p = latLngToScreenCoordinate(aLatLong) - uMapScreenCoord;
diff --git a/scwx-qt/gl/geo_texture2d.vert b/scwx-qt/gl/geo_texture2d.vert
new file mode 100644
index 00000000..7977af95
--- /dev/null
+++ b/scwx-qt/gl/geo_texture2d.vert
@@ -0,0 +1,65 @@
+#version 330 core
+
+#define DEGREES_MAX 360.0f
+#define LATITUDE_MAX 85.051128779806604f
+#define LONGITUDE_MAX 180.0f
+#define PI 3.1415926535897932384626433f
+#define RAD2DEG 57.295779513082320876798156332941f
+#define DEG2RAD 0.0174532925199432957692369055556f
+
+layout (location = 0) in vec2 aLatLong;
+layout (location = 1) in vec2 aXYOffset;
+layout (location = 2) in vec3 aTexCoord;
+layout (location = 3) in vec4 aModulate;
+layout (location = 4) in float aAngleDeg;
+layout (location = 5) in int aThreshold;
+layout (location = 6) in ivec2 aTimeRange;
+
+uniform mat4 uMVPMatrix;
+uniform mat4 uMapMatrix;
+uniform vec2 uMapScreenCoord;
+
+out VertexData
+{
+ int threshold;
+ vec3 texCoord;
+ vec4 color;
+ ivec2 timeRange;
+} vsOut;
+
+smooth out vec3 texCoord;
+smooth out vec4 color;
+
+vec2 latLngToScreenCoordinate(in vec2 latLng)
+{
+ vec2 p;
+ latLng.x = clamp(latLng.x, -LATITUDE_MAX, LATITUDE_MAX);
+ p.xy = vec2(LONGITUDE_MAX + latLng.y,
+ -(LONGITUDE_MAX - RAD2DEG * log(tan(PI / 4 + latLng.x * PI / DEGREES_MAX))));
+ return p;
+}
+
+void main()
+{
+ // Pass the threshold and time range to the geometry shader
+ vsOut.threshold = aThreshold;
+ vsOut.timeRange = aTimeRange;
+
+ // Pass the texture coordinate and color modulate to the geometry and
+ // fragment shaders
+ vsOut.texCoord = aTexCoord;
+ vsOut.color = aModulate;
+ texCoord = aTexCoord;
+ color = aModulate;
+
+ vec2 p = latLngToScreenCoordinate(aLatLong) - uMapScreenCoord;
+
+ // Rotate clockwise
+ float angle = aAngleDeg * DEG2RAD;
+ mat2 rotate = mat2(cos(angle), -sin(angle),
+ sin(angle), cos(angle));
+
+ // Transform the position to screen coordinates
+ gl_Position = uMapMatrix * vec4(p, 0.0f, 1.0f) +
+ uMVPMatrix * vec4(rotate * aXYOffset, 0.0f, 0.0f);
+}
diff --git a/scwx-qt/gl/map_color.vert b/scwx-qt/gl/map_color.vert
new file mode 100644
index 00000000..4319310f
--- /dev/null
+++ b/scwx-qt/gl/map_color.vert
@@ -0,0 +1,38 @@
+#version 330 core
+
+layout (location = 0) in vec2 aScreenCoord;
+layout (location = 1) in vec2 aXYOffset;
+layout (location = 2) in vec4 aColor;
+layout (location = 3) in int aThreshold;
+layout (location = 4) in ivec2 aTimeRange;
+
+uniform mat4 uMVPMatrix;
+uniform mat4 uMapMatrix;
+uniform vec2 uMapScreenCoord;
+
+out VertexData
+{
+ int threshold;
+ vec3 texCoord;
+ vec4 color;
+ ivec2 timeRange;
+} vsOut;
+
+smooth out vec4 color;
+
+void main()
+{
+ // Pass the threshold and time range to the geometry shader
+ vsOut.threshold = aThreshold;
+ vsOut.timeRange = aTimeRange;
+
+ // Pass the color to the geometry and fragment shaders
+ vsOut.color = aColor;
+ color = aColor;
+
+ vec2 p = aScreenCoord - uMapScreenCoord;
+
+ // Transform the position to screen coordinates
+ gl_Position = uMapMatrix * vec4(p, 0.0f, 1.0f) +
+ uMVPMatrix * vec4(aXYOffset, 0.0f, 0.0f);
+}
diff --git a/scwx-qt/gl/texture2d.frag b/scwx-qt/gl/texture2d.frag
index 16ab8960..725d66ad 100644
--- a/scwx-qt/gl/texture2d.frag
+++ b/scwx-qt/gl/texture2d.frag
@@ -6,11 +6,11 @@ precision mediump float;
uniform sampler2D uTexture;
smooth in vec2 texCoord;
-flat in vec4 modulate;
+smooth in vec4 color;
layout (location = 0) out vec4 fragColor;
void main()
{
- fragColor = texture(uTexture, texCoord) * modulate;
+ fragColor = texture(uTexture, texCoord) * color;
}
diff --git a/scwx-qt/gl/texture2d_array.frag b/scwx-qt/gl/texture2d_array.frag
new file mode 100644
index 00000000..b73bfda8
--- /dev/null
+++ b/scwx-qt/gl/texture2d_array.frag
@@ -0,0 +1,16 @@
+#version 330 core
+
+// Lower the default precision to medium
+precision mediump float;
+
+uniform sampler2DArray uTexture;
+
+smooth in vec3 texCoord;
+smooth in vec4 color;
+
+layout (location = 0) out vec4 fragColor;
+
+void main()
+{
+ fragColor = texture(uTexture, texCoord) * color;
+}
diff --git a/scwx-qt/gl/threshold.geom b/scwx-qt/gl/threshold.geom
new file mode 100644
index 00000000..dec09b01
--- /dev/null
+++ b/scwx-qt/gl/threshold.geom
@@ -0,0 +1,46 @@
+#version 330 core
+
+layout (triangles) in;
+layout (triangle_strip, max_vertices = 3) out;
+
+uniform float uMapDistance;
+uniform int uSelectedTime;
+
+in VertexData
+{
+ int threshold;
+ vec3 texCoord;
+ vec4 color;
+ ivec2 timeRange;
+} gsIn[];
+
+smooth out vec3 texCoord;
+smooth out vec4 color;
+
+void main()
+{
+ if ((gsIn[0].threshold <= 0 || // If Threshold: 0 was specified, no threshold
+ gsIn[0].threshold >= uMapDistance || // If Threshold is above current map distance
+ gsIn[0].threshold >= 999) && // If Threshold: 999 was specified (or greater), no threshold
+ (gsIn[0].timeRange[0] == 0 || // If there is no start time specified
+ (gsIn[0].timeRange[0] <= uSelectedTime && // If the selected time is after the start time
+ uSelectedTime < gsIn[0].timeRange[1]))) // If the selected time is before the end time
+ {
+ gl_Position = gl_in[0].gl_Position;
+ texCoord = gsIn[0].texCoord;
+ color = gsIn[0].color;
+ EmitVertex();
+
+ gl_Position = gl_in[1].gl_Position;
+ texCoord = gsIn[1].texCoord;
+ color = gsIn[1].color;
+
+ EmitVertex();
+ gl_Position = gl_in[2].gl_Position;
+ texCoord = gsIn[2].texCoord;
+ color = gsIn[2].color;
+
+ EmitVertex();
+ EndPrimitive();
+ }
+}
diff --git a/scwx-qt/res/fonts/Inconsolata-Regular.ttf b/scwx-qt/res/fonts/Inconsolata-Regular.ttf
new file mode 100644
index 00000000..0d879bf3
Binary files /dev/null and b/scwx-qt/res/fonts/Inconsolata-Regular.ttf differ
diff --git a/scwx-qt/res/icons/font-awesome-6/angle-down-solid.svg b/scwx-qt/res/icons/font-awesome-6/angle-down-solid.svg
new file mode 100644
index 00000000..c877d491
--- /dev/null
+++ b/scwx-qt/res/icons/font-awesome-6/angle-down-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scwx-qt/res/icons/font-awesome-6/angle-up-solid.svg b/scwx-qt/res/icons/font-awesome-6/angle-up-solid.svg
new file mode 100644
index 00000000..da43225c
--- /dev/null
+++ b/scwx-qt/res/icons/font-awesome-6/angle-up-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scwx-qt/res/icons/font-awesome-6/angles-down-solid.svg b/scwx-qt/res/icons/font-awesome-6/angles-down-solid.svg
new file mode 100644
index 00000000..68671b02
--- /dev/null
+++ b/scwx-qt/res/icons/font-awesome-6/angles-down-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scwx-qt/res/icons/font-awesome-6/angles-up-solid.svg b/scwx-qt/res/icons/font-awesome-6/angles-up-solid.svg
new file mode 100644
index 00000000..74692e85
--- /dev/null
+++ b/scwx-qt/res/icons/font-awesome-6/angles-up-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scwx-qt/res/icons/font-awesome-6/earth-americas-solid.svg b/scwx-qt/res/icons/font-awesome-6/earth-americas-solid.svg
new file mode 100644
index 00000000..f427a71e
--- /dev/null
+++ b/scwx-qt/res/icons/font-awesome-6/earth-americas-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scwx-qt/res/icons/font-awesome-6/font-solid.svg b/scwx-qt/res/icons/font-awesome-6/font-solid.svg
new file mode 100644
index 00000000..2fa4599c
--- /dev/null
+++ b/scwx-qt/res/icons/font-awesome-6/font-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scwx-qt/res/icons/font-awesome-6/layer-group-solid.svg b/scwx-qt/res/icons/font-awesome-6/layer-group-solid.svg
new file mode 100644
index 00000000..a6366610
--- /dev/null
+++ b/scwx-qt/res/icons/font-awesome-6/layer-group-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake
index 3cae5e6c..50767c34 100644
--- a/scwx-qt/scwx-qt.cmake
+++ b/scwx-qt/scwx-qt.cmake
@@ -12,6 +12,7 @@ set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Boost)
+find_package(Fontconfig)
find_package(Freetype)
find_package(geographiclib)
find_package(glm)
@@ -46,7 +47,8 @@ set(HDR_CONFIG source/scwx/qt/config/county_database.hpp
source/scwx/qt/config/radar_site.hpp)
set(SRC_CONFIG source/scwx/qt/config/county_database.cpp
source/scwx/qt/config/radar_site.cpp)
-set(SRC_EXTERNAL source/scwx/qt/external/stb_rect_pack.cpp)
+set(SRC_EXTERNAL source/scwx/qt/external/stb_image.cpp
+ source/scwx/qt/external/stb_rect_pack.cpp)
set(HDR_GL source/scwx/qt/gl/gl.hpp
source/scwx/qt/gl/gl_context.hpp
source/scwx/qt/gl/shader_program.hpp
@@ -56,18 +58,34 @@ set(SRC_GL source/scwx/qt/gl/gl_context.cpp
source/scwx/qt/gl/text_shader.cpp)
set(HDR_GL_DRAW source/scwx/qt/gl/draw/draw_item.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
+ source/scwx/qt/gl/draw/placefile_lines.hpp
+ source/scwx/qt/gl/draw/placefile_polygons.hpp
+ source/scwx/qt/gl/draw/placefile_text.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_line.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
+ source/scwx/qt/gl/draw/placefile_polygons.cpp
+ source/scwx/qt/gl/draw/placefile_text.cpp
+ source/scwx/qt/gl/draw/placefile_triangles.cpp
source/scwx/qt/gl/draw/rectangle.cpp)
-set(HDR_MANAGER source/scwx/qt/manager/radar_product_manager.hpp
+set(HDR_MANAGER source/scwx/qt/manager/font_manager.hpp
+ source/scwx/qt/manager/placefile_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
source/scwx/qt/manager/settings_manager.hpp
source/scwx/qt/manager/text_event_manager.hpp
source/scwx/qt/manager/timeline_manager.hpp
source/scwx/qt/manager/update_manager.hpp)
-set(SRC_MANAGER source/scwx/qt/manager/radar_product_manager.cpp
+set(SRC_MANAGER source/scwx/qt/manager/font_manager.cpp
+ source/scwx/qt/manager/placefile_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
source/scwx/qt/manager/settings_manager.cpp
@@ -84,6 +102,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/placefile_layer.hpp
source/scwx/qt/map/radar_product_layer.hpp
source/scwx/qt/map/radar_range_layer.hpp)
set(SRC_MAP source/scwx/qt/map/alert_layer.cpp
@@ -95,11 +114,14 @@ 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/placefile_layer.cpp
source/scwx/qt/map/radar_product_layer.cpp
source/scwx/qt/map/radar_range_layer.cpp)
set(HDR_MODEL source/scwx/qt/model/alert_model.hpp
source/scwx/qt/model/alert_proxy_model.hpp
source/scwx/qt/model/imgui_context_model.hpp
+ source/scwx/qt/model/layer_model.hpp
+ source/scwx/qt/model/placefile_model.hpp
source/scwx/qt/model/radar_product_model.hpp
source/scwx/qt/model/radar_site_model.hpp
source/scwx/qt/model/tree_item.hpp
@@ -107,6 +129,8 @@ set(HDR_MODEL source/scwx/qt/model/alert_model.hpp
set(SRC_MODEL source/scwx/qt/model/alert_model.cpp
source/scwx/qt/model/alert_proxy_model.cpp
source/scwx/qt/model/imgui_context_model.cpp
+ source/scwx/qt/model/layer_model.cpp
+ source/scwx/qt/model/placefile_model.cpp
source/scwx/qt/model/radar_product_model.cpp
source/scwx/qt/model/radar_site_model.cpp
source/scwx/qt/model/tree_item.cpp
@@ -122,6 +146,7 @@ set(HDR_SETTINGS source/scwx/qt/settings/general_settings.hpp
source/scwx/qt/settings/settings_interface_base.hpp
source/scwx/qt/settings/settings_variable.hpp
source/scwx/qt/settings/settings_variable_base.hpp
+ source/scwx/qt/settings/text_settings.hpp
source/scwx/qt/settings/ui_settings.hpp)
set(SRC_SETTINGS source/scwx/qt/settings/general_settings.cpp
source/scwx/qt/settings/map_settings.cpp
@@ -132,19 +157,26 @@ set(SRC_SETTINGS source/scwx/qt/settings/general_settings.cpp
source/scwx/qt/settings/settings_interface_base.cpp
source/scwx/qt/settings/settings_variable.cpp
source/scwx/qt/settings/settings_variable_base.cpp
+ source/scwx/qt/settings/text_settings.cpp
source/scwx/qt/settings/ui_settings.cpp)
set(HDR_TYPES source/scwx/qt/types/alert_types.hpp
source/scwx/qt/types/font_types.hpp
source/scwx/qt/types/github_types.hpp
+ source/scwx/qt/types/imgui_font.hpp
+ source/scwx/qt/types/layer_types.hpp
source/scwx/qt/types/map_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_event_key.hpp
+ source/scwx/qt/types/text_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
+ source/scwx/qt/types/layer_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_event_key.cpp
+ source/scwx/qt/types/text_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
@@ -153,9 +185,14 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp
source/scwx/qt/ui/flow_layout.hpp
source/scwx/qt/ui/imgui_debug_dialog.hpp
source/scwx/qt/ui/imgui_debug_widget.hpp
+ source/scwx/qt/ui/layer_dialog.hpp
+ source/scwx/qt/ui/left_elided_item_delegate.hpp
source/scwx/qt/ui/level2_products_widget.hpp
source/scwx/qt/ui/level2_settings_widget.hpp
source/scwx/qt/ui/level3_products_widget.hpp
+ source/scwx/qt/ui/open_url_dialog.hpp
+ source/scwx/qt/ui/placefile_dialog.hpp
+ source/scwx/qt/ui/placefile_settings_widget.hpp
source/scwx/qt/ui/radar_site_dialog.hpp
source/scwx/qt/ui/settings_dialog.hpp
source/scwx/qt/ui/update_dialog.hpp)
@@ -167,9 +204,14 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp
source/scwx/qt/ui/flow_layout.cpp
source/scwx/qt/ui/imgui_debug_dialog.cpp
source/scwx/qt/ui/imgui_debug_widget.cpp
+ source/scwx/qt/ui/layer_dialog.cpp
+ source/scwx/qt/ui/left_elided_item_delegate.cpp
source/scwx/qt/ui/level2_products_widget.cpp
source/scwx/qt/ui/level2_settings_widget.cpp
source/scwx/qt/ui/level3_products_widget.cpp
+ source/scwx/qt/ui/open_url_dialog.cpp
+ source/scwx/qt/ui/placefile_dialog.cpp
+ source/scwx/qt/ui/placefile_settings_widget.cpp
source/scwx/qt/ui/radar_site_dialog.cpp
source/scwx/qt/ui/settings_dialog.cpp
source/scwx/qt/ui/update_dialog.cpp)
@@ -179,6 +221,10 @@ set(UI_UI source/scwx/qt/ui/about_dialog.ui
source/scwx/qt/ui/animation_dock_widget.ui
source/scwx/qt/ui/collapsible_group.ui
source/scwx/qt/ui/imgui_debug_dialog.ui
+ source/scwx/qt/ui/layer_dialog.ui
+ source/scwx/qt/ui/open_url_dialog.ui
+ source/scwx/qt/ui/placefile_dialog.ui
+ source/scwx/qt/ui/placefile_settings_widget.ui
source/scwx/qt/ui/radar_site_dialog.ui
source/scwx/qt/ui/settings_dialog.ui
source/scwx/qt/ui/update_dialog.ui)
@@ -187,22 +233,30 @@ set(HDR_UTIL source/scwx/qt/util/color.hpp
source/scwx/qt/util/font.hpp
source/scwx/qt/util/font_buffer.hpp
source/scwx/qt/util/geographic_lib.hpp
+ source/scwx/qt/util/imgui.hpp
source/scwx/qt/util/json.hpp
+ source/scwx/qt/util/maplibre.hpp
+ source/scwx/qt/util/network.hpp
source/scwx/qt/util/streams.hpp
source/scwx/qt/util/texture_atlas.hpp
source/scwx/qt/util/q_file_buffer.hpp
source/scwx/qt/util/q_file_input_stream.hpp
- source/scwx/qt/util/time.hpp)
+ source/scwx/qt/util/time.hpp
+ source/scwx/qt/util/tooltip.hpp)
set(SRC_UTIL source/scwx/qt/util/color.cpp
source/scwx/qt/util/file.cpp
source/scwx/qt/util/font.cpp
source/scwx/qt/util/font_buffer.cpp
source/scwx/qt/util/geographic_lib.cpp
+ source/scwx/qt/util/imgui.cpp
source/scwx/qt/util/json.cpp
+ source/scwx/qt/util/maplibre.cpp
+ source/scwx/qt/util/network.cpp
source/scwx/qt/util/texture_atlas.cpp
source/scwx/qt/util/q_file_buffer.cpp
source/scwx/qt/util/q_file_input_stream.cpp
- source/scwx/qt/util/time.cpp)
+ source/scwx/qt/util/time.cpp
+ source/scwx/qt/util/tooltip.cpp)
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
@@ -221,13 +275,17 @@ set(RESOURCE_FILES scwx-qt.qrc)
set(SHADER_FILES gl/color.frag
gl/color.vert
gl/geo_line.vert
+ gl/geo_texture2d.vert
+ gl/map_color.vert
gl/radar.frag
gl/radar.vert
gl/text.frag
gl/text.vert
gl/texture1d.frag
gl/texture1d.vert
- gl/texture2d.frag)
+ gl/texture2d.frag
+ gl/texture2d_array.frag
+ gl/threshold.geom)
set(CMAKE_FILES scwx-qt.cmake)
@@ -386,7 +444,8 @@ target_include_directories(scwx-qt PUBLIC ${scwx-qt_SOURCE_DIR}/source
${FTGL_INCLUDE_DIR}
${IMGUI_INCLUDE_DIRS}
${MBGL_INCLUDE_DIR}
- ${STB_INCLUDE_DIR})
+ ${STB_INCLUDE_DIR}
+ ${TEXTFLOWCPP_INCLUDE_DIR})
target_include_directories(supercell-wx PUBLIC ${scwx-qt_SOURCE_DIR}/source)
@@ -432,6 +491,7 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets
Boost::timer
qmaplibregl
$<$:opengl32>
+ Fontconfig::Fontconfig
freetype-gl
GeographicLib::GeographicLib
glm::glm
diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc
index 3939ddf4..10d8b0a2 100644
--- a/scwx-qt/scwx-qt.qrc
+++ b/scwx-qt/scwx-qt.qrc
@@ -3,6 +3,8 @@
gl/color.frag
gl/color.vert
gl/geo_line.vert
+ gl/geo_texture2d.vert
+ gl/map_color.vert
gl/radar.frag
gl/radar.vert
gl/text.frag
@@ -10,19 +12,29 @@
gl/texture1d.frag
gl/texture1d.vert
gl/texture2d.frag
+ gl/texture2d_array.frag
+ gl/threshold.geom
res/config/radar_sites.json
res/fonts/din1451alt.ttf
res/fonts/din1451alt_g.ttf
+ res/fonts/Inconsolata-Regular.ttf
res/icons/scwx-256.ico
res/icons/scwx-256.png
+ res/icons/font-awesome-6/angle-down-solid.svg
res/icons/font-awesome-6/angle-left-solid.svg
res/icons/font-awesome-6/angle-right-solid.svg
+ res/icons/font-awesome-6/angle-up-solid.svg
+ res/icons/font-awesome-6/angles-down-solid.svg
+ res/icons/font-awesome-6/angles-up-solid.svg
res/icons/font-awesome-6/backward-step-solid.svg
res/icons/font-awesome-6/book-solid.svg
res/icons/font-awesome-6/discord.svg
+ res/icons/font-awesome-6/earth-americas-solid.svg
+ res/icons/font-awesome-6/font-solid.svg
res/icons/font-awesome-6/forward-step-solid.svg
res/icons/font-awesome-6/gears-solid.svg
res/icons/font-awesome-6/github.svg
+ res/icons/font-awesome-6/layer-group-solid.svg
res/icons/font-awesome-6/palette-solid.svg
res/icons/font-awesome-6/pause-solid.svg
res/icons/font-awesome-6/play-solid.svg
diff --git a/scwx-qt/source/scwx/qt/external/stb_image.cpp b/scwx-qt/source/scwx/qt/external/stb_image.cpp
new file mode 100644
index 00000000..f611b488
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/external/stb_image.cpp
@@ -0,0 +1,22 @@
+#define STB_IMAGE_IMPLEMENTATION
+#define STBI_ASSERT(x)
+#define STBI_FAILURE_USERMSG
+
+#if defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wunused-but-set-variable"
+#endif
+
+#if defined(_MSC_VER)
+# pragma warning(push, 0)
+#endif
+
+#include
+
+#if defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+#if defined(_MSC_VER)
+# pragma warning(pop)
+#endif
diff --git a/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp b/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp
index b2c92279..21dab7b0 100644
--- a/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp
+++ b/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp
@@ -1,4 +1,5 @@
#include
+#include
#include
@@ -41,6 +42,27 @@ DrawItem::~DrawItem() = default;
DrawItem::DrawItem(DrawItem&&) noexcept = default;
DrawItem& DrawItem::operator=(DrawItem&&) noexcept = default;
+void DrawItem::Render(
+ const QMapLibreGL::CustomLayerRenderParameters& /* params */)
+{
+}
+
+void DrawItem::Render(const QMapLibreGL::CustomLayerRenderParameters& params,
+ bool /* textureAtlasChanged */)
+{
+ Render(params);
+}
+
+bool DrawItem::RunMousePicking(
+ const QMapLibreGL::CustomLayerRenderParameters& /* params */,
+ const QPointF& /* mouseLocalPos */,
+ const QPointF& /* mouseGlobalPos */,
+ const glm::vec2& /* mouseCoords */)
+{
+ // By default, the draw item is not picked
+ return false;
+}
+
void DrawItem::UseDefaultProjection(
const QMapLibreGL::CustomLayerRenderParameters& params,
GLint uMVPMatrixLocation)
@@ -54,21 +76,21 @@ void DrawItem::UseDefaultProjection(
uMVPMatrixLocation, 1, GL_FALSE, glm::value_ptr(projection));
}
-// TODO: Refactor to utility class
-static glm::vec2
-LatLongToScreenCoordinate(const QMapLibreGL::Coordinate& coordinate)
+void DrawItem::UseRotationProjection(
+ const QMapLibreGL::CustomLayerRenderParameters& params,
+ GLint uMVPMatrixLocation)
{
- static constexpr double RAD2DEG_D = 180.0 / M_PI;
+ glm::mat4 projection = glm::ortho(0.0f,
+ static_cast(params.width),
+ 0.0f,
+ static_cast(params.height));
- double latitude = std::clamp(
- coordinate.first, -mbgl::util::LATITUDE_MAX, mbgl::util::LATITUDE_MAX);
- glm::vec2 screen {
- mbgl::util::LONGITUDE_MAX + coordinate.second,
- -(mbgl::util::LONGITUDE_MAX -
- RAD2DEG_D *
- std::log(std::tan(M_PI / 4.0 +
- latitude * M_PI / mbgl::util::DEGREES_MAX)))};
- return screen;
+ projection = glm::rotate(projection,
+ glm::radians(params.bearing),
+ glm::vec3(0.0f, 0.0f, 1.0f));
+
+ p->gl_.glUniformMatrix4fv(
+ uMVPMatrixLocation, 1, GL_FALSE, glm::value_ptr(projection));
}
void DrawItem::UseMapProjection(
@@ -78,21 +100,11 @@ void DrawItem::UseMapProjection(
{
OpenGLFunctions& gl = p->gl_;
- // TODO: Refactor to utility class
- const float scale = std::pow(2.0, params.zoom) * 2.0f *
- mbgl::util::tileSize_D / mbgl::util::DEGREES_MAX;
- const float xScale = scale / params.width;
- const float yScale = scale / params.height;
-
- glm::mat4 uMVPMatrix(1.0f);
- uMVPMatrix = glm::scale(uMVPMatrix, glm::vec3(xScale, yScale, 1.0f));
- uMVPMatrix = glm::rotate(uMVPMatrix,
- glm::radians(params.bearing),
- glm::vec3(0.0f, 0.0f, 1.0f));
+ const glm::mat4 uMVPMatrix = util::maplibre::GetMapMatrix(params);
gl.glUniform2fv(uMapScreenCoordLocation,
1,
- glm::value_ptr(LatLongToScreenCoordinate(
+ glm::value_ptr(util::maplibre::LatLongToScreenCoordinate(
{params.latitude, params.longitude})));
gl.glUniformMatrix4fv(
diff --git a/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp b/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp
index de94ebe7..b4d7d4ce 100644
--- a/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp
+++ b/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp
@@ -5,6 +5,7 @@
#include
#include
+#include
namespace scwx
{
@@ -28,14 +29,34 @@ public:
DrawItem& operator=(DrawItem&&) noexcept;
virtual void Initialize() = 0;
- virtual void
- Render(const QMapLibreGL::CustomLayerRenderParameters& params) = 0;
- virtual void Deinitialize() = 0;
+ virtual void Render(const QMapLibreGL::CustomLayerRenderParameters& params);
+ virtual void Render(const QMapLibreGL::CustomLayerRenderParameters& params,
+ bool textureAtlasChanged);
+ virtual void Deinitialize() = 0;
+
+ /**
+ * @brief Run mouse picking on the draw item.
+ *
+ * @param [in] params Custom layer render parameters
+ * @param [in] mouseLocalPos Mouse cursor widget position
+ * @param [in] mouseGlobalPos Mouse cursor screen position
+ * @param [in] mouseCoords Mouse cursor location in map screen coordinates
+ *
+ * @return true if the draw item was picked, otherwise false
+ */
+ virtual bool
+ RunMousePicking(const QMapLibreGL::CustomLayerRenderParameters& params,
+ const QPointF& mouseLocalPos,
+ const QPointF& mouseGlobalPos,
+ const glm::vec2& mouseCoords);
protected:
void
UseDefaultProjection(const QMapLibreGL::CustomLayerRenderParameters& params,
GLint uMVPMatrixLocation);
+ void
+ UseRotationProjection(const QMapLibreGL::CustomLayerRenderParameters& params,
+ GLint uMVPMatrixLocation);
void UseMapProjection(const QMapLibreGL::CustomLayerRenderParameters& params,
GLint uMVPMatrixLocation,
GLint uMapScreenCoordLocation);
diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_line.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_line.cpp
index a009f6bb..17d84a79 100644
--- a/scwx-qt/source/scwx/qt/gl/draw/geo_line.cpp
+++ b/scwx-qt/source/scwx/qt/gl/draw/geo_line.cpp
@@ -23,10 +23,12 @@ 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 = 10;
+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:
@@ -90,8 +92,8 @@ void GeoLine::Initialize()
{
gl::OpenGLFunctions& gl = p->context_->gl();
- p->shaderProgram_ = p->context_->GetShaderProgram(":/gl/geo_line.vert",
- ":/gl/texture2d.frag");
+ p->shaderProgram_ = p->context_->GetShaderProgram(
+ ":/gl/geo_line.vert", ":/gl/texture2d_array.frag");
p->uMVPMatrixLocation_ =
gl.glGetUniformLocation(p->shaderProgram_->id(), "uMVPMatrix");
@@ -115,7 +117,7 @@ void GeoLine::Initialize()
}
p->texture_ =
- util::TextureAtlas::Instance().GetTextureAttributes("lines/default-1x7");
+ util::TextureAtlas::Instance().GetTextureAttributes(kTextureName);
gl.glGenVertexArrays(1, &p->vao_);
gl.glGenBuffers(1, &p->vbo_);
@@ -145,7 +147,7 @@ void GeoLine::Initialize()
// aTexCoord
gl.glVertexAttribPointer(2,
- 2,
+ 3,
GL_FLOAT,
GL_FALSE,
kPointsPerVertex * sizeof(float),
@@ -158,7 +160,7 @@ void GeoLine::Initialize()
GL_FLOAT,
GL_FALSE,
kPointsPerVertex * sizeof(float),
- reinterpret_cast(6 * sizeof(float)));
+ reinterpret_cast(7 * sizeof(float)));
gl.glEnableVertexAttribArray(3);
p->dirty_ = true;
@@ -248,6 +250,9 @@ void GeoLine::Impl::Update()
{
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_;
@@ -259,6 +264,8 @@ void GeoLine::Impl::Update()
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_;
@@ -284,12 +291,12 @@ void GeoLine::Impl::Update()
{ //
// Line
{
- {lx, by, -ox, -oy, ls, bt, mc0, mc1, mc2, mc3}, // BL
- {lx, by, +ox, +oy, ls, tt, mc0, mc1, mc2, mc3}, // TL
- {rx, ty, -ox, -oy, rs, bt, mc0, mc1, mc2, mc3}, // BR
- {rx, ty, -ox, -oy, rs, bt, mc0, mc1, mc2, mc3}, // BR
- {rx, ty, +ox, +oy, rs, tt, mc0, mc1, mc2, mc3}, // TR
- {lx, by, +ox, +oy, ls, tt, mc0, mc1, mc2, mc3} // TL
+ {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,
diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp
new file mode 100644
index 00000000..36c792db
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp
@@ -0,0 +1,788 @@
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+
+namespace scwx
+{
+namespace qt
+{
+namespace gl
+{
+namespace draw
+{
+
+static const std::string logPrefix_ = "scwx::qt::gl::draw::placefile_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 PlacefileIconInfo
+{
+ PlacefileIconInfo(
+ const std::shared_ptr& iconFile,
+ const std::string& baseUrlString) :
+ iconFile_ {iconFile}
+ {
+ // Resolve using base URL
+ auto baseUrl = QUrl::fromUserInput(QString::fromStdString(baseUrlString));
+ auto relativeUrl = QUrl(QDir::fromNativeSeparators(
+ QString::fromStdString(iconFile->filename_)));
+ resolvedUrl_ = baseUrl.resolved(relativeUrl).toString().toStdString();
+ }
+
+ void UpdateTextureInfo();
+
+ std::string resolvedUrl_;
+ std::shared_ptr iconFile_;
+ util::TextureAttributes texture_ {};
+ std::size_t rows_ {};
+ std::size_t columns_ {};
+ std::size_t numIcons_ {};
+ float scaledWidth_ {};
+ float scaledHeight_ {};
+};
+
+class PlacefileIcons::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 dirty_ {false};
+ bool thresholded_ {false};
+
+ std::chrono::system_clock::time_point selectedTime_ {};
+
+ std::mutex iconMutex_;
+
+ boost::unordered_flat_map
+ currentIconFiles_ {};
+ boost::unordered_flat_map newIconFiles_ {};
+
+ 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_;
+};
+
+PlacefileIcons::PlacefileIcons(const std::shared_ptr& context) :
+ DrawItem(context->gl()), p(std::make_unique(context))
+{
+}
+PlacefileIcons::~PlacefileIcons() = default;
+
+PlacefileIcons::PlacefileIcons(PlacefileIcons&&) noexcept = default;
+PlacefileIcons& PlacefileIcons::operator=(PlacefileIcons&&) noexcept = default;
+
+void PlacefileIcons::set_selected_time(
+ std::chrono::system_clock::time_point selectedTime)
+{
+ p->selectedTime_ = selectedTime;
+}
+
+void PlacefileIcons::set_thresholded(bool thresholded)
+{
+ p->thresholded_ = thresholded;
+}
+
+void PlacefileIcons::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 PlacefileIcons::Render(
+ const QMapLibreGL::CustomLayerRenderParameters& params,
+ bool textureAtlasChanged)
+{
+ 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 PlacefileIcons::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->currentIconFiles_.clear();
+ p->currentHoverIcons_.clear();
+ p->currentIconBuffer_.clear();
+ p->currentIntegerBuffer_.clear();
+ p->textureBuffer_.clear();
+}
+
+void PlacefileIconInfo::UpdateTextureInfo()
+{
+ texture_ = util::TextureAtlas::Instance().GetTextureAttributes(resolvedUrl_);
+
+ if (iconFile_->iconWidth_ > 0 && iconFile_->iconHeight_ > 0)
+ {
+ columns_ = texture_.size_.x / iconFile_->iconWidth_;
+ rows_ = texture_.size_.y / iconFile_->iconHeight_;
+ }
+ else
+ {
+ columns_ = 0u;
+ rows_ = 0u;
+ }
+
+ 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_ = iconFile_->iconWidth_ * xFactor;
+ scaledHeight_ = iconFile_->iconHeight_ * yFactor;
+}
+
+void PlacefileIcons::StartIcons()
+{
+ // Clear the new buffer
+ p->newIconList_.clear();
+ p->newValidIconList_.clear();
+ p->newIconFiles_.clear();
+ p->newIconBuffer_.clear();
+ p->newIntegerBuffer_.clear();
+ p->newHoverIcons_.clear();
+}
+
+void PlacefileIcons::SetIconFiles(
+ const std::vector>& iconFiles,
+ const std::string& baseUrl)
+{
+ // Populate icon file map
+ for (auto& file : iconFiles)
+ {
+ p->newIconFiles_.emplace(
+ std::piecewise_construct,
+ std::tuple {file->fileNumber_},
+ std::forward_as_tuple(PlacefileIconInfo {file, baseUrl}));
+ }
+}
+
+void PlacefileIcons::AddIcon(
+ const std::shared_ptr& di)
+{
+ if (di != nullptr)
+ {
+ p->newIconList_.emplace_back(di);
+ }
+}
+
+void PlacefileIcons::FinishIcons()
+{
+ // Update icon files
+ for (auto& iconFile : p->newIconFiles_)
+ {
+ iconFile.second.UpdateTextureInfo();
+ }
+
+ // Update buffers
+ p->UpdateBuffers();
+
+ std::unique_lock lock {p->iconMutex_};
+
+ // Swap buffers
+ p->currentIconList_.swap(p->newValidIconList_);
+ p->currentIconFiles_.swap(p->newIconFiles_);
+ p->currentIconBuffer_.swap(p->newIconBuffer_);
+ p->currentIntegerBuffer_.swap(p->newIntegerBuffer_);
+ p->currentHoverIcons_.swap(p->newHoverIcons_);
+
+ // Clear the new buffers
+ p->newIconList_.clear();
+ p->newValidIconList_.clear();
+ p->newIconFiles_.clear();
+ p->newIconBuffer_.clear();
+ p->newIntegerBuffer_.clear();
+ p->newHoverIcons_.clear();
+
+ // Mark the draw item dirty
+ p->dirty_ = true;
+}
+
+void PlacefileIcons::Impl::UpdateBuffers()
+{
+ newIconBuffer_.clear();
+ newIconBuffer_.reserve(newIconList_.size() * kIconBufferLength);
+ newIntegerBuffer_.clear();
+ newIntegerBuffer_.reserve(newIconList_.size() * kVerticesPerRectangle *
+ kIntegersPerVertex_);
+
+ for (auto& di : newIconList_)
+ {
+ auto it = newIconFiles_.find(di->fileNumber_);
+ if (it == newIconFiles_.cend())
+ {
+ // No file found
+ logger_->trace("Could not find file number: {}", di->fileNumber_);
+ continue;
+ }
+
+ auto& icon = it->second;
+
+ // Validate icon
+ if (di->iconNumber_ == 0 || di->iconNumber_ > icon.numIcons_)
+ {
+ // No icon found
+ logger_->trace("Invalid icon number: {}", di->iconNumber_);
+ 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.iconFile_->iconWidth_);
+ const float ih = static_cast(icon.iconFile_->iconHeight_);
+
+ // Hot X/Y (zero-based icon center)
+ const float hx = static_cast(icon.iconFile_->hotX_);
+ const float hy = static_cast(icon.iconFile_->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 PlacefileIcons::Impl::UpdateTextureBuffer()
+{
+ textureBuffer_.clear();
+ textureBuffer_.reserve(currentIconList_.size() * kTextureBufferLength);
+
+ for (auto& di : currentIconList_)
+ {
+ auto it = currentIconFiles_.find(di->fileNumber_);
+ if (it == currentIconFiles_.cend())
+ {
+ // No file found. Should not get here, but insert empty data to match
+ // up with data already buffered
+ logger_->error("Could not find file number: {}", di->fileNumber_);
+
+ // 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->iconNumber_ == 0 || di->iconNumber_ > icon.numIcons_)
+ {
+ // No icon found
+ logger_->trace("Invalid icon number: {}", di->iconNumber_);
+
+ // 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->iconNumber_ - 1) / icon.columns_;
+ const std::size_t iconColumn = (di->iconNumber_ - 1) % 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 PlacefileIcons::Impl::Update(bool textureAtlasChanged)
+{
+ gl::OpenGLFunctions& gl = context_->gl();
+
+ // If the texture atlas has changed
+ if (dirty_ || textureAtlasChanged)
+ {
+ // Update texture coordinates
+ for (auto& iconFile : currentIconFiles_)
+ {
+ iconFile.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);
+ }
+
+ // 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 PlacefileIcons::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 ((
+ // Placefile is thresholded
+ mapDistance > units::length::meters {0.0} &&
+
+ // Placefile 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) ||
+
+ (
+ // Line 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/placefile_icons.hpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.hpp
new file mode 100644
index 00000000..16ac08cd
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.hpp
@@ -0,0 +1,80 @@
+#pragma once
+
+#include
+#include
+#include
+
+#include
+
+namespace scwx
+{
+namespace qt
+{
+namespace gl
+{
+namespace draw
+{
+
+class PlacefileIcons : public DrawItem
+{
+public:
+ explicit PlacefileIcons(const std::shared_ptr& context);
+ ~PlacefileIcons();
+
+ PlacefileIcons(const PlacefileIcons&) = delete;
+ PlacefileIcons& operator=(const PlacefileIcons&) = delete;
+
+ PlacefileIcons(PlacefileIcons&&) noexcept;
+ PlacefileIcons& operator=(PlacefileIcons&&) 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;
+
+ /**
+ * Resets and prepares the draw item for adding a new set of icons.
+ */
+ void StartIcons();
+
+ /**
+ * Configures the textures for drawing the placefile icons.
+ *
+ * @param [in] iconFiles A list of icon files
+ * @param [in] baseUrl The base URL of the placefile
+ */
+ void SetIconFiles(
+ const std::vector>&
+ iconFiles,
+ const std::string& baseUrl);
+
+ /**
+ * Adds a placefile icon to the internal draw list.
+ *
+ * @param [in] di Placefile icon
+ */
+ void AddIcon(const std::shared_ptr& di);
+
+ /**
+ * 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/gl/draw/placefile_images.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp
new file mode 100644
index 00000000..1b46bd99
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp
@@ -0,0 +1,487 @@
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+namespace scwx
+{
+namespace qt
+{
+namespace gl
+{
+namespace draw
+{
+
+static const std::string logPrefix_ = "scwx::qt::gl::draw::placefile_images";
+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 = 8;
+static constexpr std::size_t kPointsPerTexCoord = 3;
+static constexpr std::size_t kImageBufferLength =
+ 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 PlacefileImageInfo
+{
+ PlacefileImageInfo(const std::string& imageFile,
+ const std::string& baseUrlString)
+ {
+ // Resolve using base URL
+ auto baseUrl = QUrl::fromUserInput(QString::fromStdString(baseUrlString));
+ auto relativeUrl =
+ QUrl(QDir::fromNativeSeparators(QString::fromStdString(imageFile)));
+ resolvedUrl_ = baseUrl.resolved(relativeUrl).toString().toStdString();
+ }
+
+ void UpdateTextureInfo();
+
+ std::string resolvedUrl_;
+ util::TextureAttributes texture_ {};
+ float scaledWidth_ {};
+ float scaledHeight_ {};
+};
+
+class PlacefileImages::Impl
+{
+public:
+ 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_;
+
+ std::string baseUrl_ {};
+
+ bool dirty_ {false};
+ bool thresholded_ {false};
+
+ std::chrono::system_clock::time_point selectedTime_ {};
+
+ std::mutex imageMutex_;
+
+ boost::unordered_flat_map
+ currentImageFiles_ {};
+ boost::unordered_flat_map newImageFiles_ {};
+
+ std::vector>
+ currentImageList_ {};
+ std::vector>
+ newImageList_ {};
+
+ std::vector currentImageBuffer_ {};
+ std::vector currentIntegerBuffer_ {};
+ std::vector newImageBuffer_ {};
+ std::vector newIntegerBuffer_ {};
+
+ std::vector textureBuffer_ {};
+
+ std::shared_ptr shaderProgram_;
+ GLint uMVPMatrixLocation_;
+ GLint uMapMatrixLocation_;
+ GLint uMapScreenCoordLocation_;
+ GLint uMapDistanceLocation_;
+ GLint uSelectedTimeLocation_;
+
+ GLuint vao_;
+ std::array vbo_;
+
+ GLsizei numVertices_;
+};
+
+PlacefileImages::PlacefileImages(const std::shared_ptr& context) :
+ DrawItem(context->gl()), p(std::make_unique(context))
+{
+}
+PlacefileImages::~PlacefileImages() = default;
+
+PlacefileImages::PlacefileImages(PlacefileImages&&) noexcept = default;
+PlacefileImages&
+PlacefileImages::operator=(PlacefileImages&&) noexcept = default;
+
+void PlacefileImages::set_selected_time(
+ std::chrono::system_clock::time_point selectedTime)
+{
+ p->selectedTime_ = selectedTime;
+}
+
+void PlacefileImages::set_thresholded(bool thresholded)
+{
+ p->thresholded_ = thresholded;
+}
+
+void PlacefileImages::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);
+
+ 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,
+ kIntegersPerVertex_ * sizeof(GLint),
+ 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 PlacefileImages::Render(
+ const QMapLibreGL::CustomLayerRenderParameters& params,
+ bool textureAtlasChanged)
+{
+ std::unique_lock lock {p->imageMutex_};
+
+ if (!p->currentImageList_.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 images
+ gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_);
+ }
+}
+
+void PlacefileImages::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->imageMutex_};
+
+ p->currentImageList_.clear();
+ p->currentImageFiles_.clear();
+ p->currentImageBuffer_.clear();
+ p->currentIntegerBuffer_.clear();
+ p->textureBuffer_.clear();
+}
+
+void PlacefileImageInfo::UpdateTextureInfo()
+{
+ texture_ = util::TextureAtlas::Instance().GetTextureAttributes(resolvedUrl_);
+
+ scaledWidth_ = texture_.sRight_ - texture_.sLeft_;
+ scaledHeight_ = texture_.tBottom_ - texture_.tTop_;
+}
+
+void PlacefileImages::StartImages(const std::string& baseUrl)
+{
+ p->baseUrl_ = baseUrl;
+
+ // Clear the new buffer
+ p->newImageList_.clear();
+ p->newImageFiles_.clear();
+ p->newImageBuffer_.clear();
+ p->newIntegerBuffer_.clear();
+}
+
+void PlacefileImages::AddImage(
+ const std::shared_ptr& di)
+{
+ if (di != nullptr)
+ {
+ p->newImageList_.emplace_back(di);
+ }
+}
+
+void PlacefileImages::FinishImages()
+{
+ // Update buffers
+ p->UpdateBuffers();
+
+ std::unique_lock lock {p->imageMutex_};
+
+ // Swap buffers
+ p->currentImageList_.swap(p->newImageList_);
+ p->currentImageFiles_.swap(p->newImageFiles_);
+ p->currentImageBuffer_.swap(p->newImageBuffer_);
+ p->currentIntegerBuffer_.swap(p->newIntegerBuffer_);
+
+ // Clear the new buffers
+ p->newImageList_.clear();
+ p->newImageFiles_.clear();
+ p->newImageBuffer_.clear();
+ p->newIntegerBuffer_.clear();
+
+ // Mark the draw item dirty
+ p->dirty_ = true;
+}
+
+void PlacefileImages::Impl::UpdateBuffers()
+{
+ newImageBuffer_.clear();
+ newImageBuffer_.reserve(newImageList_.size() * kImageBufferLength);
+ newIntegerBuffer_.clear();
+ newIntegerBuffer_.reserve(newImageList_.size() * kVerticesPerRectangle *
+ kIntegersPerVertex_);
+ newImageFiles_.clear();
+
+ // Fixed modulate color
+ static const float mc0 = 1.0f;
+ static const float mc1 = 1.0f;
+ static const float mc2 = 1.0f;
+ static const float mc3 = 1.0f;
+
+ for (auto& di : newImageList_)
+ {
+ // Populate image file map
+ newImageFiles_.emplace(
+ std::piecewise_construct,
+ std::tuple {di->imageFile_},
+ std::forward_as_tuple(PlacefileImageInfo {di->imageFile_, baseUrl_}));
+
+ // 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());
+
+ // Limit processing to groups of 3 (triangles)
+ std::size_t numElements = di->elements_.size() - di->elements_.size() % 3;
+ for (std::size_t i = 0; i < numElements; ++i)
+ {
+ auto& element = di->elements_[i];
+
+ // Latitude and longitude coordinates in degrees
+ const float lat = static_cast(element.latitude_);
+ const float lon = static_cast(element.longitude_);
+
+ // Base X/Y offsets in pixels
+ const float x = static_cast(element.x_);
+ const float y = static_cast(element.y_);
+
+ newImageBuffer_.insert(newImageBuffer_.end(),
+ {lat, lon, x, y, mc0, mc1, mc2, mc3});
+ newIntegerBuffer_.insert(newIntegerBuffer_.end(),
+ {thresholdValue, startTime, endTime});
+ }
+ }
+}
+
+void PlacefileImages::Impl::UpdateTextureBuffer()
+{
+ textureBuffer_.clear();
+ textureBuffer_.reserve(currentImageList_.size() * kTextureBufferLength);
+
+ for (auto& di : currentImageList_)
+ {
+ // Get placefile image info. The key should always be found in the map, as
+ // it is populated when the placefile is updated.
+ auto it = currentImageFiles_.find(di->imageFile_);
+ const PlacefileImageInfo& image = (it == currentImageFiles_.cend()) ?
+ currentImageFiles_.cbegin()->second :
+ it->second;
+
+ const float r = static_cast(image.texture_.layerId_);
+
+ // Limit processing to groups of 3 (triangles)
+ std::size_t numElements = di->elements_.size() - di->elements_.size() % 3;
+ for (std::size_t i = 0; i < numElements; ++i)
+ {
+ auto& element = di->elements_[i];
+
+ // Texture coordinates
+ const float s =
+ image.texture_.sLeft_ + (image.scaledWidth_ * element.tu_);
+ const float t =
+ image.texture_.tTop_ + (image.scaledHeight_ * element.tv_);
+
+ textureBuffer_.insert(textureBuffer_.end(), {s, t, r});
+ }
+ }
+}
+
+void PlacefileImages::Impl::Update(bool textureAtlasChanged)
+{
+ gl::OpenGLFunctions& gl = context_->gl();
+
+ // If the texture atlas has changed
+ if (dirty_ || textureAtlasChanged)
+ {
+ // Update texture coordinates
+ for (auto& imageFile : currentImageFiles_)
+ {
+ imageFile.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);
+ }
+
+ // If buffers need updating
+ if (dirty_)
+ {
+ // Buffer vertex data
+ gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]);
+ gl.glBufferData(GL_ARRAY_BUFFER,
+ sizeof(float) * currentImageBuffer_.size(),
+ currentImageBuffer_.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(currentImageBuffer_.size() / kPointsPerVertex);
+ }
+
+ dirty_ = false;
+}
+
+} // namespace draw
+} // namespace gl
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_images.hpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_images.hpp
new file mode 100644
index 00000000..d99c43a0
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_images.hpp
@@ -0,0 +1,62 @@
+#pragma once
+
+#include
+#include
+#include
+
+namespace scwx
+{
+namespace qt
+{
+namespace gl
+{
+namespace draw
+{
+
+class PlacefileImages : public DrawItem
+{
+public:
+ explicit PlacefileImages(const std::shared_ptr& context);
+ ~PlacefileImages();
+
+ PlacefileImages(const PlacefileImages&) = delete;
+ PlacefileImages& operator=(const PlacefileImages&) = delete;
+
+ PlacefileImages(PlacefileImages&&) noexcept;
+ PlacefileImages& operator=(PlacefileImages&&) 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;
+
+ /**
+ * Resets and prepares the draw item for adding a new set of images.
+ */
+ void StartImages(const std::string& baseUrl);
+
+ /**
+ * Adds a placefile image to the internal draw list.
+ *
+ * @param [in] di Placefile image
+ */
+ void AddImage(const std::shared_ptr& di);
+
+ /**
+ * Finalizes the draw item after adding new images.
+ */
+ void FinishImages();
+
+private:
+ class Impl;
+
+ std::unique_ptr p;
+};
+
+} // namespace draw
+} // namespace gl
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp
new file mode 100644
index 00000000..df0eff48
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp
@@ -0,0 +1,601 @@
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+namespace scwx
+{
+namespace qt
+{
+namespace gl
+{
+namespace draw
+{
+
+static const std::string logPrefix_ = "scwx::qt::gl::draw::placefile_lines";
+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 kBufferLength =
+ kNumTriangles * kVerticesPerTriangle * kPointsPerVertex;
+
+// Threshold, start time, end time
+static constexpr std::size_t kIntegersPerVertex_ = 3;
+
+static const boost::gil::rgba8_pixel_t kBlack_ {0, 0, 0, 255};
+
+class PlacefileLines::Impl
+{
+public:
+ struct LineHoverEntry
+ {
+ std::shared_ptr di_;
+
+ glm::vec2 p1_;
+ glm::vec2 p2_;
+ 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 BufferLine(const std::shared_ptr& di,
+ const gr::Placefile::LineDrawItem::Element& e1,
+ const gr::Placefile::LineDrawItem::Element& e2,
+ const float width,
+ const units::angle::degrees angle,
+ const boost::gil::rgba8_pixel_t color,
+ const GLint threshold,
+ const GLint startTime,
+ const GLint endTime,
+ bool bufferHover = false);
+ void
+ UpdateBuffers(const std::shared_ptr& di);
+ void Update();
+
+ std::shared_ptr context_;
+
+ bool dirty_ {false};
+ bool thresholded_ {false};
+
+ std::chrono::system_clock::time_point selectedTime_ {};
+
+ std::mutex lineMutex_ {};
+
+ std::size_t currentNumLines_ {};
+ std::size_t newNumLines_ {};
+
+ std::vector currentLinesBuffer_ {};
+ std::vector currentIntegerBuffer_ {};
+ std::vector newLinesBuffer_ {};
+ std::vector newIntegerBuffer_ {};
+
+ std::vector currentHoverLines_ {};
+ std::vector newHoverLines_ {};
+
+ std::shared_ptr shaderProgram_;
+ GLint uMVPMatrixLocation_;
+ GLint uMapMatrixLocation_;
+ GLint uMapScreenCoordLocation_;
+ GLint uMapDistanceLocation_;
+ GLint uSelectedTimeLocation_;
+
+ GLuint vao_;
+ std::array vbo_;
+
+ GLsizei numVertices_;
+};
+
+PlacefileLines::PlacefileLines(const std::shared_ptr& context) :
+ DrawItem(context->gl()), p(std::make_unique(context))
+{
+}
+PlacefileLines::~PlacefileLines() = default;
+
+PlacefileLines::PlacefileLines(PlacefileLines&&) noexcept = default;
+PlacefileLines& PlacefileLines::operator=(PlacefileLines&&) noexcept = default;
+
+void PlacefileLines::set_selected_time(
+ std::chrono::system_clock::time_point selectedTime)
+{
+ p->selectedTime_ = selectedTime;
+}
+
+void PlacefileLines::set_thresholded(bool thresholded)
+{
+ p->thresholded_ = thresholded;
+}
+
+void PlacefileLines::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(2, 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);
+
+ // aThreshold
+ gl.glVertexAttribIPointer(5, //
+ 1,
+ GL_INT,
+ kIntegersPerVertex_ * sizeof(GLint),
+ 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 PlacefileLines::Render(
+ const QMapLibreGL::CustomLayerRenderParameters& params)
+{
+ std::unique_lock lock {p->lineMutex_};
+
+ if (p->currentNumLines_ > 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 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()));
+
+ // Draw icons
+ gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_);
+ }
+}
+
+void PlacefileLines::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 PlacefileLines::StartLines()
+{
+ // Clear the new buffers
+ p->newLinesBuffer_.clear();
+ p->newIntegerBuffer_.clear();
+ p->newHoverLines_.clear();
+
+ p->newNumLines_ = 0u;
+}
+
+void PlacefileLines::AddLine(
+ const std::shared_ptr& di)
+{
+ if (di != nullptr && !di->elements_.empty())
+ {
+ p->UpdateBuffers(di);
+ p->newNumLines_ += (di->elements_.size() - 1) * 2;
+ }
+}
+
+void PlacefileLines::FinishLines()
+{
+ std::unique_lock lock {p->lineMutex_};
+
+ // Swap buffers
+ p->currentLinesBuffer_.swap(p->newLinesBuffer_);
+ p->currentIntegerBuffer_.swap(p->newIntegerBuffer_);
+ p->currentHoverLines_.swap(p->newHoverLines_);
+
+ // Clear the new buffers
+ p->newLinesBuffer_.clear();
+ p->newIntegerBuffer_.clear();
+ p->newHoverLines_.clear();
+
+ // Update the number of lines
+ p->currentNumLines_ = p->newNumLines_;
+ p->numVertices_ =
+ static_cast(p->currentNumLines_ * kVerticesPerRectangle);
+
+ // Mark the draw item dirty
+ p->dirty_ = true;
+}
+
+void PlacefileLines::Impl::UpdateBuffers(
+ const std::shared_ptr& 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());
+
+ std::vector> angles {};
+ angles.reserve(di->elements_.size() - 1);
+
+ // For each element pair inside a Line statement, render a black line
+ for (std::size_t i = 0; i < di->elements_.size() - 1; ++i)
+ {
+ // Latitude and longitude coordinates in degrees
+ const float lat1 = static_cast(di->elements_[i].latitude_);
+ const float lon1 = static_cast(di->elements_[i].longitude_);
+ const float lat2 = static_cast(di->elements_[i + 1].latitude_);
+ const float lon2 = static_cast(di->elements_[i + 1].longitude_);
+
+ // Calculate angle
+ const units::angle::degrees angle =
+ util::GeographicLib::GetAngle(lat1, lon1, lat2, lon2);
+ angles.push_back(angle);
+
+ // Buffer line with hover text
+ BufferLine(di,
+ di->elements_[i],
+ di->elements_[i + 1],
+ di->width_ + 2,
+ angle,
+ kBlack_,
+ thresholdValue,
+ startTime,
+ endTime,
+ true);
+ }
+
+ // For each element pair inside a Line statement, render a colored line
+ for (std::size_t i = 0; i < di->elements_.size() - 1; ++i)
+ {
+ BufferLine(di,
+ di->elements_[i],
+ di->elements_[i + 1],
+ di->width_,
+ angles[i],
+ di->color_,
+ thresholdValue,
+ startTime,
+ endTime);
+ }
+}
+
+void PlacefileLines::Impl::BufferLine(
+ const std::shared_ptr& di,
+ const gr::Placefile::LineDrawItem::Element& e1,
+ const gr::Placefile::LineDrawItem::Element& e2,
+ const float width,
+ const units::angle::degrees angle,
+ const boost::gil::rgba8_pixel_t color,
+ const GLint threshold,
+ const GLint startTime,
+ const GLint endTime,
+ bool bufferHover)
+{
+ // Latitude and longitude coordinates in degrees
+ const float lat1 = static_cast(e1.latitude_);
+ const float lon1 = static_cast(e1.longitude_);
+ const float lat2 = static_cast(e2.latitude_);
+ const float lon2 = static_cast(e2.longitude_);
+
+ // TODO: Base X/Y offsets in pixels
+ // const float x1 = static_cast(e1.x_);
+ // const float y1 = static_cast(e1.y_);
+ // const float x2 = static_cast(e2.x_);
+ // const float y2 = static_cast(e2.y_);
+
+ // Angle
+ const float a = static_cast(angle.value());
+
+ // Final X/Y offsets in pixels
+ const float hw = width * 0.5f;
+ const float lx = -hw;
+ const float rx = +hw;
+ const float ty = +hw;
+ const float by = -hw;
+
+ // Modulate color
+ const float mc0 = color[0] / 255.0f;
+ const float mc1 = color[1] / 255.0f;
+ const float mc2 = color[2] / 255.0f;
+ const float mc3 = color[3] / 255.0f;
+
+ // 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(),
+ {threshold,
+ startTime,
+ endTime,
+ threshold,
+ startTime,
+ endTime,
+ threshold,
+ startTime,
+ endTime,
+ threshold,
+ startTime,
+ endTime,
+ threshold,
+ startTime,
+ endTime,
+ threshold,
+ startTime,
+ endTime});
+
+ if (bufferHover && !di->hoverText_.empty())
+ {
+ const units::angle::radians radians = angle;
+
+ const auto sc1 = util::maplibre::LatLongToScreenCoordinate({lat1, lon1});
+ const auto sc2 = util::maplibre::LatLongToScreenCoordinate({lat2, lon2});
+
+ 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 {-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 PlacefileLines::Impl::Update()
+{
+ // If the placefile has 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 PlacefileLines::RunMousePicking(
+ const QMapLibreGL::CustomLayerRenderParameters& params,
+ const QPointF& /* mouseLocalPos */,
+ const QPointF& mouseGlobalPos,
+ const glm::vec2& mouseCoords)
+{
+ 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(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 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 {0.0} &&
+
+ // Placefile threshold is < 999 nmi
+ static_cast(std::round(
+ units::length::nautical_miles {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
diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.hpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.hpp
new file mode 100644
index 00000000..c4f9c7ad
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.hpp
@@ -0,0 +1,66 @@
+#pragma once
+
+#include
+#include
+#include
+
+namespace scwx
+{
+namespace qt
+{
+namespace gl
+{
+namespace draw
+{
+
+class PlacefileLines : public DrawItem
+{
+public:
+ explicit PlacefileLines(const std::shared_ptr& context);
+ ~PlacefileLines();
+
+ PlacefileLines(const PlacefileLines&) = delete;
+ PlacefileLines& operator=(const PlacefileLines&) = delete;
+
+ PlacefileLines(PlacefileLines&&) noexcept;
+ PlacefileLines& operator=(PlacefileLines&&) 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) override;
+
+ /**
+ * Resets and prepares the draw item for adding a new set of lines.
+ */
+ void StartLines();
+
+ /**
+ * Adds a placefile line to the internal draw list.
+ *
+ * @param [in] di Placefile line
+ */
+ void AddLine(const std::shared_ptr& di);
+
+ /**
+ * Finalizes the draw item after adding new lines.
+ */
+ void FinishLines();
+
+private:
+ class Impl;
+
+ std::unique_ptr p;
+};
+
+} // namespace draw
+} // namespace gl
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp
new file mode 100644
index 00000000..24a96fe6
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp
@@ -0,0 +1,486 @@
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+
+#if defined(_WIN32)
+typedef void (*_GLUfuncptr)(void);
+#endif
+
+namespace scwx
+{
+namespace qt
+{
+namespace gl
+{
+namespace draw
+{
+
+static const std::string logPrefix_ = "scwx::qt::gl::draw::placefile_polygons";
+static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
+
+static constexpr std::size_t kVerticesPerTriangle = 3;
+static constexpr std::size_t kPointsPerVertex = 8;
+
+// Threshold, start time, end time
+static constexpr std::size_t kIntegersPerVertex_ = 3;
+
+static constexpr std::size_t kTessVertexScreenX_ = 0;
+static constexpr std::size_t kTessVertexScreenY_ = 1;
+static constexpr std::size_t kTessVertexScreenZ_ = 2;
+static constexpr std::size_t kTessVertexXOffset_ = 3;
+static constexpr std::size_t kTessVertexYOffset_ = 4;
+static constexpr std::size_t kTessVertexR_ = 5;
+static constexpr std::size_t kTessVertexG_ = 6;
+static constexpr std::size_t kTessVertexB_ = 7;
+static constexpr std::size_t kTessVertexA_ = 8;
+static constexpr std::size_t kTessVertexSize_ = kTessVertexA_ + 1;
+
+typedef std::array TessVertexArray;
+
+class PlacefilePolygons::Impl
+{
+public:
+ 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}
+ {
+ tessellator_ = gluNewTess();
+
+ gluTessCallback(tessellator_, //
+ GLU_TESS_COMBINE_DATA,
+ (_GLUfuncptr) &TessellateCombineCallback);
+ gluTessCallback(tessellator_, //
+ GLU_TESS_VERTEX_DATA,
+ (_GLUfuncptr) &TessellateVertexCallback);
+
+ // Force GLU_TRIANGLES
+ gluTessCallback(tessellator_, //
+ GLU_TESS_EDGE_FLAG,
+ []() {});
+
+ gluTessCallback(tessellator_, //
+ GLU_TESS_ERROR,
+ (_GLUfuncptr) &TessellateErrorCallback);
+ }
+
+ ~Impl() { gluDeleteTess(tessellator_); }
+
+ void Update();
+
+ void Tessellate(const std::shared_ptr& di);
+
+ static void TessellateCombineCallback(GLdouble coords[3],
+ void* vertexData[4],
+ GLfloat weight[4],
+ void** outData,
+ void* polygonData);
+ static void TessellateVertexCallback(void* vertexData, void* polygonData);
+ static void TessellateErrorCallback(GLenum errorCode);
+
+ std::shared_ptr context_;
+
+ bool dirty_ {false};
+ bool thresholded_ {false};
+
+ std::chrono::system_clock::time_point selectedTime_ {};
+
+ boost::container::stable_vector tessCombineBuffer_ {};
+
+ std::mutex bufferMutex_ {};
+ std::vector currentBuffer_ {};
+ std::vector currentIntegerBuffer_ {};
+ std::vector newBuffer_ {};
+ std::vector newIntegerBuffer_ {};
+
+ GLUtesselator* tessellator_;
+
+ std::shared_ptr shaderProgram_;
+ GLint uMVPMatrixLocation_;
+ GLint uMapMatrixLocation_;
+ GLint uMapScreenCoordLocation_;
+ GLint uMapDistanceLocation_;
+ GLint uSelectedTimeLocation_;
+
+ GLuint vao_;
+ std::array vbo_;
+
+ GLsizei numVertices_;
+
+ GLint currentThreshold_ {};
+ GLint currentStartTime_ {};
+ GLint currentEndTime_ {};
+};
+
+PlacefilePolygons::PlacefilePolygons(
+ const std::shared_ptr& context) :
+ DrawItem(context->gl()), p(std::make_unique(context))
+{
+}
+PlacefilePolygons::~PlacefilePolygons() = default;
+
+PlacefilePolygons::PlacefilePolygons(PlacefilePolygons&&) noexcept = default;
+PlacefilePolygons&
+PlacefilePolygons::operator=(PlacefilePolygons&&) noexcept = default;
+
+void PlacefilePolygons::set_selected_time(
+ std::chrono::system_clock::time_point selectedTime)
+{
+ p->selectedTime_ = selectedTime;
+}
+
+void PlacefilePolygons::set_thresholded(bool thresholded)
+{
+ p->thresholded_ = thresholded;
+}
+
+void PlacefilePolygons::Initialize()
+{
+ gl::OpenGLFunctions& gl = p->context_->gl();
+
+ p->shaderProgram_ = p->context_->GetShaderProgram(
+ {{GL_VERTEX_SHADER, ":/gl/map_color.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(2, 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);
+
+ // aScreenCoord
+ 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);
+
+ // aColor
+ gl.glVertexAttribPointer(2,
+ 4,
+ GL_FLOAT,
+ GL_FALSE,
+ kPointsPerVertex * sizeof(float),
+ reinterpret_cast(4 * sizeof(float)));
+ gl.glEnableVertexAttribArray(2);
+
+ gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]);
+ gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+
+ // aThreshold
+ gl.glVertexAttribIPointer(3, //
+ 1,
+ GL_INT,
+ kIntegersPerVertex_ * sizeof(GLint),
+ static_cast(0));
+ gl.glEnableVertexAttribArray(3);
+
+ // aTimeRange
+ gl.glVertexAttribIPointer(4, //
+ 2,
+ GL_INT,
+ kIntegersPerVertex_ * sizeof(GLint),
+ reinterpret_cast(1 * sizeof(GLint)));
+ gl.glEnableVertexAttribArray(4);
+
+ p->dirty_ = true;
+}
+
+void PlacefilePolygons::Render(
+ const QMapLibreGL::CustomLayerRenderParameters& params)
+{
+ if (!p->currentBuffer_.empty())
+ {
+ 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 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()));
+
+ // Draw icons
+ gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_);
+ }
+}
+
+void PlacefilePolygons::Deinitialize()
+{
+ gl::OpenGLFunctions& gl = p->context_->gl();
+
+ gl.glDeleteVertexArrays(1, &p->vao_);
+ gl.glDeleteBuffers(2, p->vbo_.data());
+
+ std::unique_lock lock {p->bufferMutex_};
+
+ // Clear the current buffers
+ p->currentBuffer_.clear();
+ p->currentIntegerBuffer_.clear();
+}
+
+void PlacefilePolygons::StartPolygons()
+{
+ // Clear the new buffers
+ p->newBuffer_.clear();
+ p->newIntegerBuffer_.clear();
+}
+
+void PlacefilePolygons::AddPolygon(
+ const std::shared_ptr& di)
+{
+ if (di != nullptr)
+ {
+ p->Tessellate(di);
+ }
+}
+
+void PlacefilePolygons::FinishPolygons()
+{
+ std::unique_lock lock {p->bufferMutex_};
+
+ // Swap buffers
+ p->currentBuffer_.swap(p->newBuffer_);
+ p->currentIntegerBuffer_.swap(p->newIntegerBuffer_);
+
+ // Clear the new buffers
+ p->newBuffer_.clear();
+ p->newIntegerBuffer_.clear();
+
+ // Mark the draw item dirty
+ p->dirty_ = true;
+}
+
+void PlacefilePolygons::Impl::Update()
+{
+ if (dirty_)
+ {
+ gl::OpenGLFunctions& gl = context_->gl();
+
+ std::unique_lock lock {bufferMutex_};
+
+ // Buffer vertex data
+ gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]);
+ gl.glBufferData(GL_ARRAY_BUFFER,
+ sizeof(GLfloat) * currentBuffer_.size(),
+ currentBuffer_.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);
+
+ numVertices_ =
+ static_cast(currentBuffer_.size() / kPointsPerVertex);
+
+ dirty_ = false;
+ }
+}
+
+void PlacefilePolygons::Impl::Tessellate(
+ const std::shared_ptr& di)
+{
+ // Vertex storage
+ boost::container::stable_vector vertices {};
+
+ // Default color to "Color" statement
+ boost::gil::rgba8_pixel_t lastColor = di->color_;
+
+ // Current threshold
+ units::length::nautical_miles threshold = di->threshold_;
+ currentThreshold_ = static_cast(std::round(threshold.value()));
+
+ // Start and end time
+ currentStartTime_ =
+ static_cast(std::chrono::duration_cast(
+ di->startTime_.time_since_epoch())
+ .count());
+ currentEndTime_ =
+ static_cast(std::chrono::duration_cast(
+ di->endTime_.time_since_epoch())
+ .count());
+
+ gluTessBeginPolygon(tessellator_, this);
+
+ for (auto& contour : di->contours_)
+ {
+ gluTessBeginContour(tessellator_);
+
+ for (auto& element : contour)
+ {
+ // Calculate screen coordinate
+ auto screenCoordinate = util::maplibre::LatLongToScreenCoordinate(
+ {element.latitude_, element.longitude_});
+
+ // Update the most recent color if specified
+ if (element.color_.has_value())
+ {
+ lastColor = element.color_.value();
+ }
+
+ // Add vertex to temporary storage
+ auto& vertex =
+ vertices.emplace_back(TessVertexArray {screenCoordinate.x,
+ screenCoordinate.y,
+ 0.0, // z
+ element.x_,
+ element.y_,
+ lastColor[0] / 255.0,
+ lastColor[1] / 255.0,
+ lastColor[2] / 255.0,
+ lastColor[3] / 255.0});
+
+ // Tessellate vertex
+ gluTessVertex(tessellator_, vertex.data(), vertex.data());
+ }
+
+ gluTessEndContour(tessellator_);
+ }
+
+ gluTessEndPolygon(tessellator_);
+
+ // Clear temporary storage
+ tessCombineBuffer_.clear();
+
+ // Remove extra vertices that don't correspond to a full triangle
+ while (newBuffer_.size() % kVerticesPerTriangle != 0)
+ {
+ newBuffer_.pop_back();
+ newIntegerBuffer_.pop_back();
+ }
+}
+
+void PlacefilePolygons::Impl::TessellateCombineCallback(GLdouble coords[3],
+ void* vertexData[4],
+ GLfloat w[4],
+ void** outData,
+ void* polygonData)
+{
+ static constexpr std::size_t r = kTessVertexR_;
+ static constexpr std::size_t a = kTessVertexA_;
+
+ Impl* self = static_cast(polygonData);
+
+ // Create new vertex data with given coordinates and interpolated color
+ auto& newVertexData = self->tessCombineBuffer_.emplace_back( //
+ TessVertexArray {
+ coords[0],
+ coords[1],
+ coords[2],
+ 0.0, // offsetX
+ 0.0, // offsetY
+ 0.0, // r
+ 0.0, // g
+ 0.0, // b
+ 0.0 // a
+ });
+
+ for (std::size_t i = 0; i < 4; ++i)
+ {
+ GLdouble* d = static_cast