Merge pull request #87 from dpaulat/feature/placefiles

Placefiles
This commit is contained in:
Dan Paulat 2023-11-05 07:50:09 -06:00 committed by GitHub
commit 3ce23e510c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
155 changed files with 13910 additions and 887 deletions

6
.gitmodules vendored
View file

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

View file

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

View file

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

View file

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

@ -1 +1 @@
Subproject commit fbb06ff53e74d3a81b434b84fff1a5dfe4b2d3c7
Subproject commit 3e85454fe5e571e7b235131912bb867ef9d75c3c

2
external/stb vendored

@ -1 +1 @@
Subproject commit 8b5f1f37b5b75829fc72d38e7b5d4bcbf8a26d55
Subproject commit 5736b15f7ea0ffb08dd38af21067c314d6a3aae9

1
external/textflowcpp vendored Submodule

@ -0,0 +1 @@
Subproject commit 12010ddc8d15538ceea20622d22977e7c5a25da5

4
external/textflowcpp.cmake vendored Normal file
View file

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

1
external/units vendored Submodule

@ -0,0 +1 @@
Subproject commit da6dd9176e8515323c75030d5e51ee19cf6c9afd

4
external/units.cmake vendored Normal file
View file

@ -0,0 +1,4 @@
cmake_minimum_required(VERSION 3.20)
set(PROJECT_NAME scwx-units)
add_subdirectory(units)

View file

@ -1,5 +1,5 @@
#version 330 core
in vec4 color;
smooth in vec4 color;
layout (location = 0) out vec4 fragColor;

View file

@ -4,7 +4,7 @@ layout (location = 1) in vec4 aColor;
uniform mat4 uMVPMatrix;
out vec4 color;
smooth out vec4 color;
void main()
{

View file

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

View file

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

38
scwx-qt/gl/map_color.vert Normal file
View file

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

View file

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

View file

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

46
scwx-qt/gl/threshold.geom Normal file
View file

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

Binary file not shown.

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M201.4 342.6c12.5 12.5 32.8 12.5 45.3 0l160-160c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L224 274.7 86.6 137.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160z"/></svg>

After

Width:  |  Height:  |  Size: 416 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M201.4 137.4c12.5-12.5 32.8-12.5 45.3 0l160 160c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L224 205.3 86.6 342.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l160-160z"/></svg>

After

Width:  |  Height:  |  Size: 416 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M246.6 470.6c-12.5 12.5-32.8 12.5-45.3 0l-160-160c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L224 402.7 361.4 265.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3l-160 160zm160-352l-160 160c-12.5 12.5-32.8 12.5-45.3 0l-160-160c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L224 210.7 361.4 73.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3z"/></svg>

After

Width:  |  Height:  |  Size: 583 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M246.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L224 109.3 361.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160zm160 352l-160-160c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L224 301.3 361.4 438.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3z"/></svg>

After

Width:  |  Height:  |  Size: 583 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M57.7 193l9.4 16.4c8.3 14.5 21.9 25.2 38 29.8L163 255.7c17.2 4.9 29 20.6 29 38.5v39.9c0 11 6.2 21 16 25.9s16 14.9 16 25.9v39c0 15.6 14.9 26.9 29.9 22.6c16.1-4.6 28.6-17.5 32.7-33.8l2.8-11.2c4.2-16.9 15.2-31.4 30.3-40l8.1-4.6c15-8.5 24.2-24.5 24.2-41.7v-8.3c0-12.7-5.1-24.9-14.1-33.9l-3.9-3.9c-9-9-21.2-14.1-33.9-14.1H257c-11.1 0-22.1-2.9-31.8-8.4l-34.5-19.7c-4.3-2.5-7.6-6.5-9.2-11.2c-3.2-9.6 1.1-20 10.2-24.5l5.9-3c6.6-3.3 14.3-3.9 21.3-1.5l23.2 7.7c8.2 2.7 17.2-.4 21.9-7.5c4.7-7 4.2-16.3-1.2-22.8l-13.6-16.3c-10-12-9.9-29.5 .3-41.3l15.7-18.3c8.8-10.3 10.2-25 3.5-36.7l-2.4-4.2c-3.5-.2-6.9-.3-10.4-.3C163.1 48 84.4 108.9 57.7 193zM464 256c0-36.8-9.6-71.4-26.4-101.5L412 164.8c-15.7 6.3-23.8 23.8-18.5 39.8l16.9 50.7c3.5 10.4 12 18.3 22.6 20.9l29.1 7.3c1.2-9 1.8-18.2 1.8-27.5zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z"/></svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M254 52.8C249.3 40.3 237.3 32 224 32s-25.3 8.3-30 20.8L57.8 416H32c-17.7 0-32 14.3-32 32s14.3 32 32 32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32h-1.8l18-48H303.8l18 48H320c-17.7 0-32 14.3-32 32s14.3 32 32 32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H390.2L254 52.8zM279.8 304H168.2L224 155.1 279.8 304z"/></svg>

After

Width:  |  Height:  |  Size: 544 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M264.5 5.2c14.9-6.9 32.1-6.9 47 0l218.6 101c8.5 3.9 13.9 12.4 13.9 21.8s-5.4 17.9-13.9 21.8l-218.6 101c-14.9 6.9-32.1 6.9-47 0L45.9 149.8C37.4 145.8 32 137.3 32 128s5.4-17.9 13.9-21.8L264.5 5.2zM476.9 209.6l53.2 24.6c8.5 3.9 13.9 12.4 13.9 21.8s-5.4 17.9-13.9 21.8l-218.6 101c-14.9 6.9-32.1 6.9-47 0L45.9 277.8C37.4 273.8 32 265.3 32 256s5.4-17.9 13.9-21.8l53.2-24.6 152 70.2c23.4 10.8 50.4 10.8 73.8 0l152-70.2zm-152 198.2l152-70.2 53.2 24.6c8.5 3.9 13.9 12.4 13.9 21.8s-5.4 17.9-13.9 21.8l-218.6 101c-14.9 6.9-32.1 6.9-47 0L45.9 405.8C37.4 401.8 32 393.3 32 384s5.4-17.9 13.9-21.8l53.2-24.6 152 70.2c23.4 10.8 50.4 10.8 73.8 0z"/></svg>

After

Width:  |  Height:  |  Size: 877 B

View file

@ -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
$<$<CXX_COMPILER_ID:MSVC>:opengl32>
Fontconfig::Fontconfig
freetype-gl
GeographicLib::GeographicLib
glm::glm

View file

@ -3,6 +3,8 @@
<file>gl/color.frag</file>
<file>gl/color.vert</file>
<file>gl/geo_line.vert</file>
<file>gl/geo_texture2d.vert</file>
<file>gl/map_color.vert</file>
<file>gl/radar.frag</file>
<file>gl/radar.vert</file>
<file>gl/text.frag</file>
@ -10,19 +12,29 @@
<file>gl/texture1d.frag</file>
<file>gl/texture1d.vert</file>
<file>gl/texture2d.frag</file>
<file>gl/texture2d_array.frag</file>
<file>gl/threshold.geom</file>
<file>res/config/radar_sites.json</file>
<file>res/fonts/din1451alt.ttf</file>
<file>res/fonts/din1451alt_g.ttf</file>
<file>res/fonts/Inconsolata-Regular.ttf</file>
<file>res/icons/scwx-256.ico</file>
<file>res/icons/scwx-256.png</file>
<file>res/icons/font-awesome-6/angle-down-solid.svg</file>
<file>res/icons/font-awesome-6/angle-left-solid.svg</file>
<file>res/icons/font-awesome-6/angle-right-solid.svg</file>
<file>res/icons/font-awesome-6/angle-up-solid.svg</file>
<file>res/icons/font-awesome-6/angles-down-solid.svg</file>
<file>res/icons/font-awesome-6/angles-up-solid.svg</file>
<file>res/icons/font-awesome-6/backward-step-solid.svg</file>
<file>res/icons/font-awesome-6/book-solid.svg</file>
<file>res/icons/font-awesome-6/discord.svg</file>
<file>res/icons/font-awesome-6/earth-americas-solid.svg</file>
<file>res/icons/font-awesome-6/font-solid.svg</file>
<file>res/icons/font-awesome-6/forward-step-solid.svg</file>
<file>res/icons/font-awesome-6/gears-solid.svg</file>
<file>res/icons/font-awesome-6/github.svg</file>
<file>res/icons/font-awesome-6/layer-group-solid.svg</file>
<file>res/icons/font-awesome-6/palette-solid.svg</file>
<file>res/icons/font-awesome-6/pause-solid.svg</file>
<file>res/icons/font-awesome-6/play-solid.svg</file>

View file

@ -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 <stb_image.h>
#if defined(__GNUC__)
# pragma GCC diagnostic pop
#endif
#if defined(_MSC_VER)
# pragma warning(pop)
#endif

View file

@ -1,4 +1,5 @@
#include <scwx/qt/gl/draw/draw_item.hpp>
#include <scwx/qt/util/maplibre.hpp>
#include <string>
@ -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<float>(params.width),
0.0f,
static_cast<float>(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<float>(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<float>(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(

View file

@ -5,6 +5,7 @@
#include <memory>
#include <QMapLibreGL/QMapLibreGL>
#include <glm/gtc/type_ptr.hpp>
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 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);

View file

@ -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<void*>(6 * sizeof(float)));
reinterpret_cast<void*>(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,

View file

@ -0,0 +1,788 @@
#include <scwx/qt/gl/draw/placefile_icons.hpp>
#include <scwx/qt/util/maplibre.hpp>
#include <scwx/qt/util/texture_atlas.hpp>
#include <scwx/qt/util/tooltip.hpp>
#include <scwx/util/logger.hpp>
#include <execution>
#include <QDir>
#include <QUrl>
#include <boost/unordered/unordered_flat_map.hpp>
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<const gr::Placefile::IconFile>& 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<const gr::Placefile::IconFile> 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<const gr::Placefile::IconDrawItem> di_;
glm::vec2 p_;
glm::vec2 otl_;
glm::vec2 otr_;
glm::vec2 obl_;
glm::vec2 obr_;
};
explicit Impl(const std::shared_ptr<GlContext>& context) :
context_ {context},
shaderProgram_ {nullptr},
uMVPMatrixLocation_(GL_INVALID_INDEX),
uMapMatrixLocation_(GL_INVALID_INDEX),
uMapScreenCoordLocation_(GL_INVALID_INDEX),
uMapDistanceLocation_(GL_INVALID_INDEX),
uSelectedTimeLocation_(GL_INVALID_INDEX),
vao_ {GL_INVALID_INDEX},
vbo_ {GL_INVALID_INDEX},
numVertices_ {0}
{
}
~Impl() {}
void UpdateBuffers();
void UpdateTextureBuffer();
void Update(bool textureAtlasChanged);
std::shared_ptr<GlContext> context_;
bool dirty_ {false};
bool thresholded_ {false};
std::chrono::system_clock::time_point selectedTime_ {};
std::mutex iconMutex_;
boost::unordered_flat_map<std::size_t, PlacefileIconInfo>
currentIconFiles_ {};
boost::unordered_flat_map<std::size_t, PlacefileIconInfo> newIconFiles_ {};
std::vector<std::shared_ptr<const gr::Placefile::IconDrawItem>>
currentIconList_ {};
std::vector<std::shared_ptr<const gr::Placefile::IconDrawItem>>
newIconList_ {};
std::vector<std::shared_ptr<const gr::Placefile::IconDrawItem>>
newValidIconList_ {};
std::vector<float> currentIconBuffer_ {};
std::vector<GLint> currentIntegerBuffer_ {};
std::vector<float> newIconBuffer_ {};
std::vector<GLint> newIntegerBuffer_ {};
std::vector<float> textureBuffer_ {};
std::vector<IconHoverEntry> currentHoverIcons_ {};
std::vector<IconHoverEntry> newHoverIcons_ {};
std::shared_ptr<ShaderProgram> shaderProgram_;
GLint uMVPMatrixLocation_;
GLint uMapMatrixLocation_;
GLint uMapScreenCoordLocation_;
GLint uMapDistanceLocation_;
GLint uSelectedTimeLocation_;
GLuint vao_;
std::array<GLuint, 3> vbo_;
GLsizei numVertices_;
};
PlacefileIcons::PlacefileIcons(const std::shared_ptr<GlContext>& context) :
DrawItem(context->gl()), p(std::make_unique<Impl>(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<GLsizei>(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<void*>(0));
gl.glEnableVertexAttribArray(0);
// aXYOffset
gl.glVertexAttribPointer(1,
2,
GL_FLOAT,
GL_FALSE,
kPointsPerVertex * sizeof(float),
reinterpret_cast<void*>(2 * sizeof(float)));
gl.glEnableVertexAttribArray(1);
// aModulate
gl.glVertexAttribPointer(3,
4,
GL_FLOAT,
GL_FALSE,
kPointsPerVertex * sizeof(float),
reinterpret_cast<void*>(4 * sizeof(float)));
gl.glEnableVertexAttribArray(3);
// aAngle
gl.glVertexAttribPointer(4,
1,
GL_FLOAT,
GL_FALSE,
kPointsPerVertex * sizeof(float),
reinterpret_cast<void*>(8 * sizeof(float)));
gl.glEnableVertexAttribArray(4);
gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]);
gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
// aTexCoord
gl.glVertexAttribPointer(2,
3,
GL_FLOAT,
GL_FALSE,
kPointsPerTexCoord * sizeof(float),
static_cast<void*>(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<void*>(0));
gl.glEnableVertexAttribArray(5);
// aTimeRange
gl.glVertexAttribIPointer(6, //
2,
GL_INT,
kIntegersPerVertex_ * sizeof(GLint),
reinterpret_cast<void*>(1 * sizeof(GLint)));
gl.glEnableVertexAttribArray(6);
p->dirty_ = true;
}
void 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<float> mapDistance =
util::maplibre::GetMapDistance(params);
gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value());
}
else
{
// If thresholding is disabled, set the map distance to 0
gl.glUniform1f(p->uMapDistanceLocation_, 0.0f);
}
// Selected time
std::chrono::system_clock::time_point selectedTime =
(p->selectedTime_ == std::chrono::system_clock::time_point {}) ?
std::chrono::system_clock::now() :
p->selectedTime_;
gl.glUniform1i(
p->uSelectedTimeLocation_,
static_cast<GLint>(std::chrono::duration_cast<std::chrono::minutes>(
selectedTime.time_since_epoch())
.count()));
// 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<GLsizei>(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<std::shared_ptr<const gr::Placefile::IconFile>>& 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<gr::Placefile::IconDrawItem>& 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<double> threshold = di->threshold_;
GLint thresholdValue = static_cast<GLint>(std::round(threshold.value()));
// Start and end time
GLint startTime =
static_cast<GLint>(std::chrono::duration_cast<std::chrono::minutes>(
di->startTime_.time_since_epoch())
.count());
GLint endTime =
static_cast<GLint>(std::chrono::duration_cast<std::chrono::minutes>(
di->endTime_.time_since_epoch())
.count());
// Latitude and longitude coordinates in degrees
const float lat = static_cast<float>(di->latitude_);
const float lon = static_cast<float>(di->longitude_);
// Base X/Y offsets in pixels
const float x = static_cast<float>(di->x_);
const float y = static_cast<float>(di->y_);
// Icon size
const float iw = static_cast<float>(icon.iconFile_->iconWidth_);
const float ih = static_cast<float>(icon.iconFile_->iconHeight_);
// Hot X/Y (zero-based icon center)
const float hx = static_cast<float>(icon.iconFile_->hotX_);
const float hy = static_cast<float>(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<float> 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<double> radians = angle;
const auto sc = util::maplibre::LatLongToScreenCoordinate({lat, lon});
const float cosAngle = cosf(static_cast<float>(radians.value()));
const float sinAngle = sinf(static_cast<float>(radians.value()));
const glm::mat2 rotate {cosAngle, -sinAngle, sinAngle, cosAngle};
const glm::vec2 otl = rotate * glm::vec2 {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<float>(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<GLsizei>(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<float>(params.bearing),
glm::vec3(0.0f, 0.0f, 1.0f));
units::length::meters<double> mapDistance =
(p->thresholded_) ? util::maplibre::GetMapDistance(params) :
units::length::meters<double> {0.0};
// If no time has been selected, use the current time
std::chrono::system_clock::time_point selectedTime =
(p->selectedTime_ == std::chrono::system_clock::time_point {}) ?
std::chrono::system_clock::now() :
p->selectedTime_;
// For each pickable 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<double> {0.0} &&
// Placefile threshold is < 999 nmi
static_cast<int>(std::round(
units::length::nautical_miles<double> {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

View file

@ -0,0 +1,80 @@
#pragma once
#include <scwx/qt/gl/gl_context.hpp>
#include <scwx/qt/gl/draw/draw_item.hpp>
#include <scwx/gr/placefile.hpp>
#include <boost/gil.hpp>
namespace scwx
{
namespace qt
{
namespace gl
{
namespace draw
{
class PlacefileIcons : public DrawItem
{
public:
explicit PlacefileIcons(const std::shared_ptr<GlContext>& 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<std::shared_ptr<const gr::Placefile::IconFile>>&
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<gr::Placefile::IconDrawItem>& di);
/**
* Finalizes the draw item after adding new icons.
*/
void FinishIcons();
private:
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace draw
} // namespace gl
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,487 @@
#include <scwx/qt/gl/draw/placefile_images.hpp>
#include <scwx/qt/util/maplibre.hpp>
#include <scwx/qt/util/texture_atlas.hpp>
#include <scwx/util/logger.hpp>
#include <QDir>
#include <QUrl>
#include <boost/unordered/unordered_flat_map.hpp>
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<GlContext>& context) :
context_ {context},
shaderProgram_ {nullptr},
uMVPMatrixLocation_(GL_INVALID_INDEX),
uMapMatrixLocation_(GL_INVALID_INDEX),
uMapScreenCoordLocation_(GL_INVALID_INDEX),
uMapDistanceLocation_(GL_INVALID_INDEX),
uSelectedTimeLocation_(GL_INVALID_INDEX),
vao_ {GL_INVALID_INDEX},
vbo_ {GL_INVALID_INDEX},
numVertices_ {0}
{
}
~Impl() {}
void UpdateBuffers();
void UpdateTextureBuffer();
void Update(bool textureAtlasChanged);
std::shared_ptr<GlContext> context_;
std::string baseUrl_ {};
bool dirty_ {false};
bool thresholded_ {false};
std::chrono::system_clock::time_point selectedTime_ {};
std::mutex imageMutex_;
boost::unordered_flat_map<std::string, PlacefileImageInfo>
currentImageFiles_ {};
boost::unordered_flat_map<std::string, PlacefileImageInfo> newImageFiles_ {};
std::vector<std::shared_ptr<const gr::Placefile::ImageDrawItem>>
currentImageList_ {};
std::vector<std::shared_ptr<const gr::Placefile::ImageDrawItem>>
newImageList_ {};
std::vector<float> currentImageBuffer_ {};
std::vector<GLint> currentIntegerBuffer_ {};
std::vector<float> newImageBuffer_ {};
std::vector<GLint> newIntegerBuffer_ {};
std::vector<float> textureBuffer_ {};
std::shared_ptr<ShaderProgram> shaderProgram_;
GLint uMVPMatrixLocation_;
GLint uMapMatrixLocation_;
GLint uMapScreenCoordLocation_;
GLint uMapDistanceLocation_;
GLint uSelectedTimeLocation_;
GLuint vao_;
std::array<GLuint, 3> vbo_;
GLsizei numVertices_;
};
PlacefileImages::PlacefileImages(const std::shared_ptr<GlContext>& context) :
DrawItem(context->gl()), p(std::make_unique<Impl>(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<GLsizei>(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<void*>(0));
gl.glEnableVertexAttribArray(0);
// aXYOffset
gl.glVertexAttribPointer(1,
2,
GL_FLOAT,
GL_FALSE,
kPointsPerVertex * sizeof(float),
reinterpret_cast<void*>(2 * sizeof(float)));
gl.glEnableVertexAttribArray(1);
// aModulate
gl.glVertexAttribPointer(3,
4,
GL_FLOAT,
GL_FALSE,
kPointsPerVertex * sizeof(float),
reinterpret_cast<void*>(4 * sizeof(float)));
gl.glEnableVertexAttribArray(3);
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<void*>(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<void*>(0));
gl.glEnableVertexAttribArray(5);
// aTimeRange
gl.glVertexAttribIPointer(6, //
2,
GL_INT,
kIntegersPerVertex_ * sizeof(GLint),
reinterpret_cast<void*>(1 * sizeof(GLint)));
gl.glEnableVertexAttribArray(6);
p->dirty_ = true;
}
void 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<float> mapDistance =
util::maplibre::GetMapDistance(params);
gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value());
}
else
{
// If thresholding is disabled, set the map distance to 0
gl.glUniform1f(p->uMapDistanceLocation_, 0.0f);
}
// Selected time
std::chrono::system_clock::time_point selectedTime =
(p->selectedTime_ == std::chrono::system_clock::time_point {}) ?
std::chrono::system_clock::now() :
p->selectedTime_;
gl.glUniform1i(
p->uSelectedTimeLocation_,
static_cast<GLint>(std::chrono::duration_cast<std::chrono::minutes>(
selectedTime.time_since_epoch())
.count()));
// 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<GLsizei>(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<gr::Placefile::ImageDrawItem>& 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<double> threshold = di->threshold_;
GLint thresholdValue = static_cast<GLint>(std::round(threshold.value()));
// Start and end time
GLint startTime =
static_cast<GLint>(std::chrono::duration_cast<std::chrono::minutes>(
di->startTime_.time_since_epoch())
.count());
GLint endTime =
static_cast<GLint>(std::chrono::duration_cast<std::chrono::minutes>(
di->endTime_.time_since_epoch())
.count());
// 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<float>(element.latitude_);
const float lon = static_cast<float>(element.longitude_);
// Base X/Y offsets in pixels
const float x = static_cast<float>(element.x_);
const float y = static_cast<float>(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<float>(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<GLsizei>(currentImageBuffer_.size() / kPointsPerVertex);
}
dirty_ = false;
}
} // namespace draw
} // namespace gl
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,62 @@
#pragma once
#include <scwx/qt/gl/gl_context.hpp>
#include <scwx/qt/gl/draw/draw_item.hpp>
#include <scwx/gr/placefile.hpp>
namespace scwx
{
namespace qt
{
namespace gl
{
namespace draw
{
class PlacefileImages : public DrawItem
{
public:
explicit PlacefileImages(const std::shared_ptr<GlContext>& 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<gr::Placefile::ImageDrawItem>& di);
/**
* Finalizes the draw item after adding new images.
*/
void FinishImages();
private:
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace draw
} // namespace gl
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,601 @@
#include <scwx/qt/gl/draw/placefile_lines.hpp>
#include <scwx/qt/util/geographic_lib.hpp>
#include <scwx/qt/util/maplibre.hpp>
#include <scwx/qt/util/tooltip.hpp>
#include <scwx/util/logger.hpp>
#include <execution>
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<const gr::Placefile::LineDrawItem> 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<GlContext>& context) :
context_ {context},
shaderProgram_ {nullptr},
uMVPMatrixLocation_(GL_INVALID_INDEX),
uMapMatrixLocation_(GL_INVALID_INDEX),
uMapScreenCoordLocation_(GL_INVALID_INDEX),
uMapDistanceLocation_(GL_INVALID_INDEX),
uSelectedTimeLocation_(GL_INVALID_INDEX),
vao_ {GL_INVALID_INDEX},
vbo_ {GL_INVALID_INDEX},
numVertices_ {0}
{
}
~Impl() {}
void BufferLine(const std::shared_ptr<const gr::Placefile::LineDrawItem>& di,
const gr::Placefile::LineDrawItem::Element& e1,
const gr::Placefile::LineDrawItem::Element& e2,
const float width,
const units::angle::degrees<double> 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<const gr::Placefile::LineDrawItem>& di);
void Update();
std::shared_ptr<GlContext> 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<float> currentLinesBuffer_ {};
std::vector<GLint> currentIntegerBuffer_ {};
std::vector<float> newLinesBuffer_ {};
std::vector<GLint> newIntegerBuffer_ {};
std::vector<LineHoverEntry> currentHoverLines_ {};
std::vector<LineHoverEntry> newHoverLines_ {};
std::shared_ptr<ShaderProgram> shaderProgram_;
GLint uMVPMatrixLocation_;
GLint uMapMatrixLocation_;
GLint uMapScreenCoordLocation_;
GLint uMapDistanceLocation_;
GLint uSelectedTimeLocation_;
GLuint vao_;
std::array<GLuint, 2> vbo_;
GLsizei numVertices_;
};
PlacefileLines::PlacefileLines(const std::shared_ptr<GlContext>& context) :
DrawItem(context->gl()), p(std::make_unique<Impl>(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<void*>(0));
gl.glEnableVertexAttribArray(0);
// aXYOffset
gl.glVertexAttribPointer(1,
2,
GL_FLOAT,
GL_FALSE,
kPointsPerVertex * sizeof(float),
reinterpret_cast<void*>(2 * sizeof(float)));
gl.glEnableVertexAttribArray(1);
// aModulate
gl.glVertexAttribPointer(3,
4,
GL_FLOAT,
GL_FALSE,
kPointsPerVertex * sizeof(float),
reinterpret_cast<void*>(4 * sizeof(float)));
gl.glEnableVertexAttribArray(3);
// aAngle
gl.glVertexAttribPointer(4,
1,
GL_FLOAT,
GL_FALSE,
kPointsPerVertex * sizeof(float),
reinterpret_cast<void*>(8 * sizeof(float)));
gl.glEnableVertexAttribArray(4);
gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]);
gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
// aThreshold
gl.glVertexAttribIPointer(5, //
1,
GL_INT,
kIntegersPerVertex_ * sizeof(GLint),
static_cast<void*>(0));
gl.glEnableVertexAttribArray(5);
// aTimeRange
gl.glVertexAttribIPointer(6, //
2,
GL_INT,
kIntegersPerVertex_ * sizeof(GLint),
reinterpret_cast<void*>(1 * sizeof(GLint)));
gl.glEnableVertexAttribArray(6);
p->dirty_ = true;
}
void 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<float> mapDistance =
util::maplibre::GetMapDistance(params);
gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value());
}
else
{
// If thresholding is disabled, set the map distance to 0
gl.glUniform1f(p->uMapDistanceLocation_, 0.0f);
}
// Selected time
std::chrono::system_clock::time_point selectedTime =
(p->selectedTime_ == std::chrono::system_clock::time_point {}) ?
std::chrono::system_clock::now() :
p->selectedTime_;
gl.glUniform1i(
p->uSelectedTimeLocation_,
static_cast<GLint>(std::chrono::duration_cast<std::chrono::minutes>(
selectedTime.time_since_epoch())
.count()));
// Draw icons
gl.glDrawArrays(GL_TRIANGLES, 0, 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<gr::Placefile::LineDrawItem>& 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<GLsizei>(p->currentNumLines_ * kVerticesPerRectangle);
// Mark the draw item dirty
p->dirty_ = true;
}
void PlacefileLines::Impl::UpdateBuffers(
const std::shared_ptr<const gr::Placefile::LineDrawItem>& di)
{
// Threshold value
units::length::nautical_miles<double> threshold = di->threshold_;
GLint thresholdValue = static_cast<GLint>(std::round(threshold.value()));
// Start and end time
GLint startTime =
static_cast<GLint>(std::chrono::duration_cast<std::chrono::minutes>(
di->startTime_.time_since_epoch())
.count());
GLint endTime =
static_cast<GLint>(std::chrono::duration_cast<std::chrono::minutes>(
di->endTime_.time_since_epoch())
.count());
std::vector<units::angle::degrees<double>> 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<float>(di->elements_[i].latitude_);
const float lon1 = static_cast<float>(di->elements_[i].longitude_);
const float lat2 = static_cast<float>(di->elements_[i + 1].latitude_);
const float lon2 = static_cast<float>(di->elements_[i + 1].longitude_);
// Calculate angle
const units::angle::degrees<double> 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<const gr::Placefile::LineDrawItem>& di,
const gr::Placefile::LineDrawItem::Element& e1,
const gr::Placefile::LineDrawItem::Element& e2,
const float width,
const units::angle::degrees<double> 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<float>(e1.latitude_);
const float lon1 = static_cast<float>(e1.longitude_);
const float lat2 = static_cast<float>(e2.latitude_);
const float lon2 = static_cast<float>(e2.longitude_);
// TODO: Base X/Y offsets in pixels
// const float x1 = static_cast<float>(e1.x_);
// const float y1 = static_cast<float>(e1.y_);
// const float x2 = static_cast<float>(e2.x_);
// const float y2 = static_cast<float>(e2.y_);
// Angle
const float a = static_cast<float>(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<double> radians = angle;
const auto sc1 = util::maplibre::LatLongToScreenCoordinate({lat1, lon1});
const auto sc2 = util::maplibre::LatLongToScreenCoordinate({lat2, lon2});
const float cosAngle = cosf(static_cast<float>(radians.value()));
const float sinAngle = sinf(static_cast<float>(radians.value()));
const glm::mat2 rotate {cosAngle, -sinAngle, sinAngle, cosAngle};
const glm::vec2 otl = rotate * glm::vec2 {-hw, +hw};
const glm::vec2 otr = rotate * glm::vec2 {+hw, +hw};
const glm::vec2 obl = rotate * glm::vec2 {-hw, -hw};
const glm::vec2 obr = rotate * glm::vec2 {+hw, -hw};
newHoverLines_.emplace_back(
LineHoverEntry {di, sc1, sc2, otl, otr, obl, obr});
}
}
void 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<float>(params.bearing),
glm::vec3(0.0f, 0.0f, 1.0f));
units::length::meters<double> mapDistance =
(p->thresholded_) ? util::maplibre::GetMapDistance(params) :
units::length::meters<double> {0.0};
// If no time has been selected, use the current time
std::chrono::system_clock::time_point selectedTime =
(p->selectedTime_ == std::chrono::system_clock::time_point {}) ?
std::chrono::system_clock::now() :
p->selectedTime_;
// For each pickable line
auto it = std::find_if(
std::execution::par_unseq,
p->currentHoverLines_.crbegin(),
p->currentHoverLines_.crend(),
[&mapDistance, &selectedTime, &mapMatrix, &mouseCoords](const auto& line)
{
if ((
// Placefile is thresholded
mapDistance > units::length::meters<double> {0.0} &&
// Placefile threshold is < 999 nmi
static_cast<int>(std::round(
units::length::nautical_miles<double> {line.di_->threshold_}
.value())) < 999 &&
// Map distance is beyond the threshold
line.di_->threshold_ < mapDistance) ||
(
// Line has a start time
line.di_->startTime_ !=
std::chrono::system_clock::time_point {} &&
// The time range has not yet started
(selectedTime < line.di_->startTime_ ||
// The time range has ended
line.di_->endTime_ <= selectedTime)))
{
// Line is not pickable
return false;
}
// Initialize vertices
glm::vec2 bl = line.p1_;
glm::vec2 br = bl;
glm::vec2 tl = line.p2_;
glm::vec2 tr = tl;
// Calculate offsets
// - Rotated offset is half the line width (pixels) in each direction
// - Multiply the offset by the scaled and rotated map matrix
const glm::vec2 otl = mapMatrix * glm::vec4 {line.otl_, 0.0f, 1.0f};
const glm::vec2 obl = mapMatrix * glm::vec4 {line.obl_, 0.0f, 1.0f};
const glm::vec2 obr = mapMatrix * glm::vec4 {line.obr_, 0.0f, 1.0f};
const glm::vec2 otr = mapMatrix * glm::vec4 {line.otr_, 0.0f, 1.0f};
// Offset vertices
tl += otl;
bl += obl;
br += obr;
tr += otr;
// TODO: X/Y offsets
// Test point against polygon bounds
return util::maplibre::IsPointInPolygon({tl, bl, br, tr}, mouseCoords);
});
if (it != p->currentHoverLines_.crend())
{
itemPicked = true;
util::tooltip::Show(it->di_->hoverText_, mouseGlobalPos);
}
return itemPicked;
}
} // namespace draw
} // namespace gl
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,66 @@
#pragma once
#include <scwx/qt/gl/gl_context.hpp>
#include <scwx/qt/gl/draw/draw_item.hpp>
#include <scwx/gr/placefile.hpp>
namespace scwx
{
namespace qt
{
namespace gl
{
namespace draw
{
class PlacefileLines : public DrawItem
{
public:
explicit PlacefileLines(const std::shared_ptr<GlContext>& 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<gr::Placefile::LineDrawItem>& di);
/**
* Finalizes the draw item after adding new lines.
*/
void FinishLines();
private:
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace draw
} // namespace gl
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,486 @@
#include <scwx/qt/gl/draw/placefile_polygons.hpp>
#include <scwx/qt/util/maplibre.hpp>
#include <scwx/util/logger.hpp>
#include <mutex>
#include <GL/glu.h>
#include <boost/container/stable_vector.hpp>
#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<GLdouble, kTessVertexSize_> TessVertexArray;
class PlacefilePolygons::Impl
{
public:
explicit Impl(const std::shared_ptr<GlContext>& context) :
context_ {context},
shaderProgram_ {nullptr},
uMVPMatrixLocation_(GL_INVALID_INDEX),
uMapMatrixLocation_(GL_INVALID_INDEX),
uMapScreenCoordLocation_(GL_INVALID_INDEX),
uMapDistanceLocation_(GL_INVALID_INDEX),
uSelectedTimeLocation_(GL_INVALID_INDEX),
vao_ {GL_INVALID_INDEX},
vbo_ {GL_INVALID_INDEX},
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<gr::Placefile::PolygonDrawItem>& 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<GlContext> context_;
bool dirty_ {false};
bool thresholded_ {false};
std::chrono::system_clock::time_point selectedTime_ {};
boost::container::stable_vector<TessVertexArray> tessCombineBuffer_ {};
std::mutex bufferMutex_ {};
std::vector<GLfloat> currentBuffer_ {};
std::vector<GLint> currentIntegerBuffer_ {};
std::vector<GLfloat> newBuffer_ {};
std::vector<GLint> newIntegerBuffer_ {};
GLUtesselator* tessellator_;
std::shared_ptr<ShaderProgram> shaderProgram_;
GLint uMVPMatrixLocation_;
GLint uMapMatrixLocation_;
GLint uMapScreenCoordLocation_;
GLint uMapDistanceLocation_;
GLint uSelectedTimeLocation_;
GLuint vao_;
std::array<GLuint, 2> vbo_;
GLsizei numVertices_;
GLint currentThreshold_ {};
GLint currentStartTime_ {};
GLint currentEndTime_ {};
};
PlacefilePolygons::PlacefilePolygons(
const std::shared_ptr<GlContext>& context) :
DrawItem(context->gl()), p(std::make_unique<Impl>(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<void*>(0));
gl.glEnableVertexAttribArray(0);
// aXYOffset
gl.glVertexAttribPointer(1,
2,
GL_FLOAT,
GL_FALSE,
kPointsPerVertex * sizeof(float),
reinterpret_cast<void*>(2 * sizeof(float)));
gl.glEnableVertexAttribArray(1);
// aColor
gl.glVertexAttribPointer(2,
4,
GL_FLOAT,
GL_FALSE,
kPointsPerVertex * sizeof(float),
reinterpret_cast<void*>(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<void*>(0));
gl.glEnableVertexAttribArray(3);
// aTimeRange
gl.glVertexAttribIPointer(4, //
2,
GL_INT,
kIntegersPerVertex_ * sizeof(GLint),
reinterpret_cast<void*>(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<float> mapDistance =
util::maplibre::GetMapDistance(params);
gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value());
}
else
{
// If thresholding is disabled, set the map distance to 0
gl.glUniform1f(p->uMapDistanceLocation_, 0.0f);
}
// Selected time
std::chrono::system_clock::time_point selectedTime =
(p->selectedTime_ == std::chrono::system_clock::time_point {}) ?
std::chrono::system_clock::now() :
p->selectedTime_;
gl.glUniform1i(
p->uSelectedTimeLocation_,
static_cast<GLint>(std::chrono::duration_cast<std::chrono::minutes>(
selectedTime.time_since_epoch())
.count()));
// Draw icons
gl.glDrawArrays(GL_TRIANGLES, 0, 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<gr::Placefile::PolygonDrawItem>& 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<GLsizei>(currentBuffer_.size() / kPointsPerVertex);
dirty_ = false;
}
}
void PlacefilePolygons::Impl::Tessellate(
const std::shared_ptr<gr::Placefile::PolygonDrawItem>& di)
{
// Vertex storage
boost::container::stable_vector<TessVertexArray> vertices {};
// Default color to "Color" statement
boost::gil::rgba8_pixel_t lastColor = di->color_;
// Current threshold
units::length::nautical_miles<double> threshold = di->threshold_;
currentThreshold_ = static_cast<GLint>(std::round(threshold.value()));
// Start and end time
currentStartTime_ =
static_cast<GLint>(std::chrono::duration_cast<std::chrono::minutes>(
di->startTime_.time_since_epoch())
.count());
currentEndTime_ =
static_cast<GLint>(std::chrono::duration_cast<std::chrono::minutes>(
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<Impl*>(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<GLdouble*>(vertexData[i]);
if (d != nullptr)
{
for (std::size_t color = r; color <= a; ++color)
{
newVertexData[color] += w[i] * d[color];
}
}
}
// Return new vertex data
*outData = &newVertexData;
}
void PlacefilePolygons::Impl::TessellateVertexCallback(void* vertexData,
void* polygonData)
{
Impl* self = static_cast<Impl*>(polygonData);
GLdouble* data = static_cast<GLdouble*>(vertexData);
// Buffer vertex
self->newBuffer_.insert(self->newBuffer_.end(),
{static_cast<float>(data[kTessVertexScreenX_]),
static_cast<float>(data[kTessVertexScreenY_]),
static_cast<float>(data[kTessVertexXOffset_]),
static_cast<float>(data[kTessVertexYOffset_]),
static_cast<float>(data[kTessVertexR_]),
static_cast<float>(data[kTessVertexG_]),
static_cast<float>(data[kTessVertexB_]),
static_cast<float>(data[kTessVertexA_])});
self->newIntegerBuffer_.insert(self->newIntegerBuffer_.end(),
{self->currentThreshold_,
self->currentStartTime_,
self->currentEndTime_});
}
void PlacefilePolygons::Impl::TessellateErrorCallback(GLenum errorCode)
{
logger_->error("GL Error: {}", errorCode);
}
} // namespace draw
} // namespace gl
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,63 @@
#pragma once
#include <scwx/qt/gl/gl_context.hpp>
#include <scwx/qt/gl/draw/draw_item.hpp>
#include <scwx/gr/placefile.hpp>
#include <boost/gil.hpp>
namespace scwx
{
namespace qt
{
namespace gl
{
namespace draw
{
class PlacefilePolygons : public DrawItem
{
public:
explicit PlacefilePolygons(const std::shared_ptr<GlContext>& context);
~PlacefilePolygons();
PlacefilePolygons(const PlacefilePolygons&) = delete;
PlacefilePolygons& operator=(const PlacefilePolygons&) = delete;
PlacefilePolygons(PlacefilePolygons&&) noexcept;
PlacefilePolygons& operator=(PlacefilePolygons&&) 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;
/**
* Resets and prepares the draw item for adding a new set of polygons.
*/
void StartPolygons();
/**
* Adds a placefile polygon to the internal draw list.
*
* @param [in] di Placefile polygon
*/
void AddPolygon(const std::shared_ptr<gr::Placefile::PolygonDrawItem>& di);
/**
* Finalizes the draw item after adding new polygons.
*/
void FinishPolygons();
private:
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace draw
} // namespace gl
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,311 @@
#include <scwx/qt/gl/draw/placefile_text.hpp>
#include <scwx/qt/manager/font_manager.hpp>
#include <scwx/qt/manager/placefile_manager.hpp>
#include <scwx/qt/settings/text_settings.hpp>
#include <scwx/qt/util/maplibre.hpp>
#include <scwx/qt/util/tooltip.hpp>
#include <scwx/util/logger.hpp>
#include <fmt/format.h>
#include <imgui.h>
#include <mbgl/util/constants.hpp>
namespace scwx
{
namespace qt
{
namespace gl
{
namespace draw
{
static const std::string logPrefix_ = "scwx::qt::gl::draw::placefile_text";
static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
class PlacefileText::Impl
{
public:
explicit Impl(const std::shared_ptr<GlContext>& context,
const std::string& placefileName) :
context_ {context}, placefileName_ {placefileName}
{
}
~Impl() {}
void RenderTextDrawItem(
const QMapLibreGL::CustomLayerRenderParameters& params,
const std::shared_ptr<const gr::Placefile::TextDrawItem>& di);
void RenderText(const QMapLibreGL::CustomLayerRenderParameters& params,
const std::string& text,
const std::string& hoverText,
boost::gil::rgba8_pixel_t color,
float x,
float y);
std::shared_ptr<GlContext> context_;
std::string placefileName_;
bool thresholded_ {false};
std::chrono::system_clock::time_point selectedTime_ {};
std::uint32_t textId_ {};
glm::vec2 mapScreenCoordLocation_ {};
float mapScale_ {1.0f};
float mapBearingCos_ {1.0f};
float mapBearingSin_ {0.0f};
float halfWidth_ {};
float halfHeight_ {};
std::string hoverText_ {};
units::length::nautical_miles<double> mapDistance_ {};
std::mutex listMutex_ {};
std::vector<std::shared_ptr<const gr::Placefile::TextDrawItem>> textList_ {};
std::vector<std::shared_ptr<const gr::Placefile::TextDrawItem>> newList_ {};
std::vector<std::shared_ptr<types::ImGuiFont>> fonts_ {};
std::vector<std::shared_ptr<types::ImGuiFont>> newFonts_ {};
};
PlacefileText::PlacefileText(const std::shared_ptr<GlContext>& context,
const std::string& placefileName) :
DrawItem(context->gl()), p(std::make_unique<Impl>(context, placefileName))
{
}
PlacefileText::~PlacefileText() = default;
PlacefileText::PlacefileText(PlacefileText&&) noexcept = default;
PlacefileText& PlacefileText::operator=(PlacefileText&&) noexcept = default;
void PlacefileText::set_placefile_name(const std::string& placefileName)
{
p->placefileName_ = placefileName;
}
void PlacefileText::set_selected_time(
std::chrono::system_clock::time_point selectedTime)
{
p->selectedTime_ = selectedTime;
}
void PlacefileText::set_thresholded(bool thresholded)
{
p->thresholded_ = thresholded;
}
void PlacefileText::Initialize() {}
void PlacefileText::Render(
const QMapLibreGL::CustomLayerRenderParameters& params)
{
std::unique_lock lock {p->listMutex_};
if (!p->textList_.empty())
{
// Reset text ID per frame
p->textId_ = 0;
p->hoverText_.clear();
// Update map screen coordinate and scale information
p->mapScreenCoordLocation_ = util::maplibre::LatLongToScreenCoordinate(
{params.latitude, params.longitude});
p->mapScale_ = std::pow(2.0, params.zoom) * mbgl::util::tileSize_D /
mbgl::util::DEGREES_MAX;
p->mapBearingCos_ = cosf(params.bearing * common::kDegreesToRadians);
p->mapBearingSin_ = sinf(params.bearing * common::kDegreesToRadians);
p->halfWidth_ = params.width * 0.5f;
p->halfHeight_ = params.height * 0.5f;
p->mapDistance_ = util::maplibre::GetMapDistance(params);
for (auto& di : p->textList_)
{
p->RenderTextDrawItem(params, di);
}
}
}
void PlacefileText::Impl::RenderTextDrawItem(
const QMapLibreGL::CustomLayerRenderParameters& params,
const std::shared_ptr<const gr::Placefile::TextDrawItem>& di)
{
// If no time has been selected, use the current time
std::chrono::system_clock::time_point selectedTime =
(selectedTime_ == std::chrono::system_clock::time_point {}) ?
std::chrono::system_clock::now() :
selectedTime_;
if ((!thresholded_ || mapDistance_ <= di->threshold_) &&
(di->startTime_ == std::chrono::system_clock::time_point {} ||
(di->startTime_ <= selectedTime && selectedTime < di->endTime_)))
{
const auto screenCoordinates = (util::maplibre::LatLongToScreenCoordinate(
{di->latitude_, di->longitude_}) -
mapScreenCoordLocation_) *
mapScale_;
// Rotate text according to map rotation
float rotatedX = screenCoordinates.x;
float rotatedY = screenCoordinates.y;
if (params.bearing != 0.0)
{
rotatedX = screenCoordinates.x * mapBearingCos_ -
screenCoordinates.y * mapBearingSin_;
rotatedY = screenCoordinates.x * mapBearingSin_ +
screenCoordinates.y * mapBearingCos_;
}
// Clamp font number to 0-8
std::size_t fontNumber = std::clamp<std::size_t>(di->fontNumber_, 0, 8);
// Set the font for the drop shadow and text
ImGui::PushFont(fonts_[fontNumber]->font());
if (settings::TextSettings::Instance()
.placefile_text_drop_shadow_enabled()
.GetValue())
{
// Draw a drop shadow 1 pixel to the lower right, in black, with the
// original transparency level
RenderText(params,
di->text_,
{},
boost::gil::rgba8_pixel_t {0, 0, 0, di->color_[3]},
rotatedX + di->x_ + halfWidth_ + 1.0f,
rotatedY + di->y_ + halfHeight_ - 1.0f);
}
// Draw the text
RenderText(params,
di->text_,
di->hoverText_,
di->color_,
rotatedX + di->x_ + halfWidth_,
rotatedY + di->y_ + halfHeight_);
// Reset the font
ImGui::PopFont();
}
}
void PlacefileText::Impl::RenderText(
const QMapLibreGL::CustomLayerRenderParameters& params,
const std::string& text,
const std::string& hoverText,
boost::gil::rgba8_pixel_t color,
float x,
float y)
{
const std::string windowName {
fmt::format("PlacefileText-{}-{}", placefileName_, ++textId_)};
// Convert screen to ImGui coordinates
y = params.height - y;
// Setup "window" to hold text
ImGui::SetNextWindowPos(
ImVec2 {x, y}, ImGuiCond_Always, ImVec2 {0.5f, 0.5f});
ImGui::Begin(windowName.c_str(),
nullptr,
ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav |
ImGuiWindowFlags_NoBackground);
// Render text
ImGui::PushStyleColor(ImGuiCol_Text,
IM_COL32(color[0], color[1], color[2], color[3]));
ImGui::TextUnformatted(text.c_str());
ImGui::PopStyleColor();
// Store hover text for mouse picking pass
if (!hoverText.empty() && ImGui::IsItemHovered())
{
hoverText_ = hoverText;
}
// End window
ImGui::End();
}
void PlacefileText::Deinitialize()
{
std::unique_lock lock {p->listMutex_};
// Clear the text list
p->textList_.clear();
}
bool PlacefileText::RunMousePicking(
const QMapLibreGL::CustomLayerRenderParameters& /* params */,
const QPointF& /* mouseLocalPos */,
const QPointF& mouseGlobalPos,
const glm::vec2& /* mouseCoords */)
{
bool itemPicked = false;
// Create tooltip for hover text
if (!p->hoverText_.empty())
{
itemPicked = true;
util::tooltip::Show(p->hoverText_, mouseGlobalPos);
}
return itemPicked;
}
void PlacefileText::StartText()
{
// Clear the new list
p->newList_.clear();
}
void PlacefileText::SetFonts(
const boost::unordered_flat_map<std::size_t,
std::shared_ptr<types::ImGuiFont>>& fonts)
{
auto defaultFont = manager::FontManager::Instance().GetImGuiFont(
types::FontCategory::Default);
// Valid font numbers are from 1 to 8, use 0 for the default font
for (std::size_t i = 0; i <= 8; ++i)
{
auto it = (i > 0) ? fonts.find(i) : fonts.cend();
if (it != fonts.cend())
{
p->newFonts_.push_back(it->second);
}
else
{
p->newFonts_.push_back(defaultFont);
}
}
}
void PlacefileText::AddText(
const std::shared_ptr<gr::Placefile::TextDrawItem>& di)
{
if (di != nullptr)
{
p->newList_.emplace_back(di);
}
}
void PlacefileText::FinishText()
{
std::unique_lock lock {p->listMutex_};
// Swap text lists
p->textList_.swap(p->newList_);
p->fonts_.swap(p->newFonts_);
// Clear the new list
p->newList_.clear();
p->newFonts_.clear();
}
} // namespace draw
} // namespace gl
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,81 @@
#pragma once
#include <scwx/qt/gl/gl_context.hpp>
#include <scwx/qt/gl/draw/draw_item.hpp>
#include <scwx/qt/types/imgui_font.hpp>
#include <scwx/gr/placefile.hpp>
#include <boost/unordered/unordered_flat_map.hpp>
namespace scwx
{
namespace qt
{
namespace gl
{
namespace draw
{
class PlacefileText : public DrawItem
{
public:
explicit PlacefileText(const std::shared_ptr<GlContext>& context,
const std::string& placefileName);
~PlacefileText();
PlacefileText(const PlacefileText&) = delete;
PlacefileText& operator=(const PlacefileText&) = delete;
PlacefileText(PlacefileText&&) noexcept;
PlacefileText& operator=(PlacefileText&&) noexcept;
void set_placefile_name(const std::string& placefileName);
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 text.
*/
void StartText();
/**
* Configures the fonts for drawing the placefile text.
*
* @param [in] fonts A map of ImGui fonts
*/
void
SetFonts(const boost::unordered_flat_map<std::size_t,
std::shared_ptr<types::ImGuiFont>>&
fonts);
/**
* Adds placefile text to the internal draw list.
*
* @param [in] di Placefile icon
*/
void AddText(const std::shared_ptr<gr::Placefile::TextDrawItem>& di);
/**
* Finalizes the draw item after adding new text.
*/
void FinishText();
private:
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace draw
} // namespace gl
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,351 @@
#include <scwx/qt/gl/draw/placefile_triangles.hpp>
#include <scwx/qt/util/maplibre.hpp>
#include <scwx/util/logger.hpp>
#include <mutex>
namespace scwx
{
namespace qt
{
namespace gl
{
namespace draw
{
static const std::string logPrefix_ = "scwx::qt::gl::draw::placefile_triangles";
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;
class PlacefileTriangles::Impl
{
public:
explicit Impl(const std::shared_ptr<GlContext>& context) :
context_ {context},
shaderProgram_ {nullptr},
uMVPMatrixLocation_(GL_INVALID_INDEX),
uMapMatrixLocation_(GL_INVALID_INDEX),
uMapScreenCoordLocation_(GL_INVALID_INDEX),
uMapDistanceLocation_(GL_INVALID_INDEX),
uSelectedTimeLocation_(GL_INVALID_INDEX),
vao_ {GL_INVALID_INDEX},
vbo_ {GL_INVALID_INDEX},
numVertices_ {0}
{
}
~Impl() {}
void UpdateBuffers(
const std::shared_ptr<const gr::Placefile::TrianglesDrawItem>& di);
void Update();
std::shared_ptr<GlContext> context_;
bool dirty_ {false};
bool thresholded_ {false};
std::chrono::system_clock::time_point selectedTime_ {};
std::mutex bufferMutex_ {};
std::vector<GLfloat> currentBuffer_ {};
std::vector<GLint> currentIntegerBuffer_ {};
std::vector<GLfloat> newBuffer_ {};
std::vector<GLint> newIntegerBuffer_ {};
std::shared_ptr<ShaderProgram> shaderProgram_;
GLint uMVPMatrixLocation_;
GLint uMapMatrixLocation_;
GLint uMapScreenCoordLocation_;
GLint uMapDistanceLocation_;
GLint uSelectedTimeLocation_;
GLuint vao_;
std::array<GLuint, 2> vbo_;
GLsizei numVertices_;
};
PlacefileTriangles::PlacefileTriangles(
const std::shared_ptr<GlContext>& context) :
DrawItem(context->gl()), p(std::make_unique<Impl>(context))
{
}
PlacefileTriangles::~PlacefileTriangles() = default;
PlacefileTriangles::PlacefileTriangles(PlacefileTriangles&&) noexcept = default;
PlacefileTriangles&
PlacefileTriangles::operator=(PlacefileTriangles&&) noexcept = default;
void PlacefileTriangles::set_selected_time(
std::chrono::system_clock::time_point selectedTime)
{
p->selectedTime_ = selectedTime;
}
void PlacefileTriangles::set_thresholded(bool thresholded)
{
p->thresholded_ = thresholded;
}
void PlacefileTriangles::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<void*>(0));
gl.glEnableVertexAttribArray(0);
// aXYOffset
gl.glVertexAttribPointer(1,
2,
GL_FLOAT,
GL_FALSE,
kPointsPerVertex * sizeof(float),
reinterpret_cast<void*>(2 * sizeof(float)));
gl.glEnableVertexAttribArray(1);
// aColor
gl.glVertexAttribPointer(2,
4,
GL_FLOAT,
GL_FALSE,
kPointsPerVertex * sizeof(float),
reinterpret_cast<void*>(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<void*>(0));
gl.glEnableVertexAttribArray(3);
// aTimeRange
gl.glVertexAttribIPointer(4, //
2,
GL_INT,
kIntegersPerVertex_ * sizeof(GLint),
reinterpret_cast<void*>(1 * sizeof(GLint)));
gl.glEnableVertexAttribArray(4);
p->dirty_ = true;
}
void PlacefileTriangles::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<float> mapDistance =
util::maplibre::GetMapDistance(params);
gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value());
}
else
{
// If thresholding is disabled, set the map distance to 0
gl.glUniform1f(p->uMapDistanceLocation_, 0.0f);
}
// Selected time
std::chrono::system_clock::time_point selectedTime =
(p->selectedTime_ == std::chrono::system_clock::time_point {}) ?
std::chrono::system_clock::now() :
p->selectedTime_;
gl.glUniform1i(
p->uSelectedTimeLocation_,
static_cast<GLint>(std::chrono::duration_cast<std::chrono::minutes>(
selectedTime.time_since_epoch())
.count()));
// Draw icons
gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_);
}
}
void PlacefileTriangles::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 PlacefileTriangles::StartTriangles()
{
// Clear the new buffers
p->newBuffer_.clear();
p->newIntegerBuffer_.clear();
}
void PlacefileTriangles::AddTriangles(
const std::shared_ptr<gr::Placefile::TrianglesDrawItem>& di)
{
if (di != nullptr)
{
p->UpdateBuffers(di);
}
}
void PlacefileTriangles::FinishTriangles()
{
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 PlacefileTriangles::Impl::UpdateBuffers(
const std::shared_ptr<const gr::Placefile::TrianglesDrawItem>& di)
{
// Threshold value
units::length::nautical_miles<double> threshold = di->threshold_;
GLint thresholdValue = static_cast<GLint>(std::round(threshold.value()));
// Start and end time
GLint startTime =
static_cast<GLint>(std::chrono::duration_cast<std::chrono::minutes>(
di->startTime_.time_since_epoch())
.count());
GLint endTime =
static_cast<GLint>(std::chrono::duration_cast<std::chrono::minutes>(
di->endTime_.time_since_epoch())
.count());
// Default color to "Color" statement
boost::gil::rgba8_pixel_t lastColor = di->color_;
// For each element inside a Triangles statement, add a vertex
for (auto& element : di->elements_)
{
// Calculate screen coordinate
auto screenCoordinate = util::maplibre::LatLongToScreenCoordinate(
{element.latitude_, element.longitude_});
// X/Y offset in pixels
const float x = static_cast<float>(element.x_);
const float y = static_cast<float>(element.y_);
// Update the most recent color if specified
if (element.color_.has_value())
{
lastColor = element.color_.value();
}
// Color value
const float r = lastColor[0] / 255.0f;
const float g = lastColor[1] / 255.0f;
const float b = lastColor[2] / 255.0f;
const float a = lastColor[3] / 255.0f;
newBuffer_.insert(
newBuffer_.end(),
{screenCoordinate.x, screenCoordinate.y, x, y, r, g, b, a});
newIntegerBuffer_.insert(newIntegerBuffer_.end(),
{thresholdValue, startTime, endTime});
}
// Remove extra vertices that don't correspond to a full triangle
while (newBuffer_.size() % kVerticesPerTriangle != 0)
{
newBuffer_.pop_back();
newIntegerBuffer_.pop_back();
}
}
void PlacefileTriangles::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<GLsizei>(currentBuffer_.size() / kPointsPerVertex);
dirty_ = false;
}
}
} // namespace draw
} // namespace gl
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,62 @@
#pragma once
#include <scwx/qt/gl/gl_context.hpp>
#include <scwx/qt/gl/draw/draw_item.hpp>
#include <scwx/gr/placefile.hpp>
namespace scwx
{
namespace qt
{
namespace gl
{
namespace draw
{
class PlacefileTriangles : public DrawItem
{
public:
explicit PlacefileTriangles(const std::shared_ptr<GlContext>& context);
~PlacefileTriangles();
PlacefileTriangles(const PlacefileTriangles&) = delete;
PlacefileTriangles& operator=(const PlacefileTriangles&) = delete;
PlacefileTriangles(PlacefileTriangles&&) noexcept;
PlacefileTriangles& operator=(PlacefileTriangles&&) 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;
/**
* Resets and prepares the draw item for adding a new set of triangles.
*/
void StartTriangles();
/**
* Adds placefile triangles to the internal draw list.
*
* @param [in] di Placefile triangles
*/
void
AddTriangles(const std::shared_ptr<gr::Placefile::TrianglesDrawItem>& di);
/**
* Finalizes the draw item after adding new triangles.
*/
void FinishTriangles();
private:
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace draw
} // namespace gl
} // namespace qt
} // namespace scwx

View file

@ -1,8 +1,9 @@
#include <scwx/qt/gl/gl_context.hpp>
#include <scwx/qt/util/texture_atlas.hpp>
#include <scwx/util/hash.hpp>
#include <scwx/util/logger.hpp>
#include <boost/container_hash/hash.hpp>
namespace scwx
{
namespace qt
@ -25,16 +26,23 @@ public:
}
~Impl() {}
void InitializeGL();
static std::size_t
GetShaderKey(std::initializer_list<std::pair<GLenum, std::string>> shaders);
gl::OpenGLFunctions gl_;
std::unordered_map<std::pair<std::string, std::string>,
std::shared_ptr<gl::ShaderProgram>,
scwx::util::hash<std::pair<std::string, std::string>>>
bool glInitialized_ {false};
std::unordered_map<std::size_t, std::shared_ptr<gl::ShaderProgram>>
shaderProgramMap_;
std::mutex shaderProgramMutex_;
GLuint textureAtlas_;
std::mutex textureMutex_;
std::uint64_t textureBufferCount_ {};
};
GlContext::GlContext() : p(std::make_unique<Impl>()) {}
@ -48,11 +56,35 @@ gl::OpenGLFunctions& GlContext::gl()
return p->gl_;
}
std::uint64_t GlContext::texture_buffer_count() const
{
return p->textureBufferCount_;
}
void GlContext::Impl::InitializeGL()
{
if (glInitialized_)
{
return;
}
gl_.glGenTextures(1, &textureAtlas_);
glInitialized_ = true;
}
std::shared_ptr<gl::ShaderProgram>
GlContext::GetShaderProgram(const std::string& vertexPath,
const std::string& fragmentPath)
{
const std::pair<std::string, std::string> key {vertexPath, fragmentPath};
return GetShaderProgram(
{{GL_VERTEX_SHADER, vertexPath}, {GL_FRAGMENT_SHADER, fragmentPath}});
}
std::shared_ptr<gl::ShaderProgram> GlContext::GetShaderProgram(
std::initializer_list<std::pair<GLenum, std::string>> shaders)
{
const auto key = Impl::GetShaderKey(shaders);
std::shared_ptr<gl::ShaderProgram> shaderProgram;
std::unique_lock lock(p->shaderProgramMutex_);
@ -62,7 +94,7 @@ GlContext::GetShaderProgram(const std::string& vertexPath,
if (it == p->shaderProgramMap_.end())
{
shaderProgram = std::make_shared<gl::ShaderProgram>(p->gl_);
shaderProgram->Load(vertexPath, fragmentPath);
shaderProgram->Load(shaders);
p->shaderProgramMap_[key] = shaderProgram;
}
else
@ -75,16 +107,33 @@ GlContext::GetShaderProgram(const std::string& vertexPath,
GLuint GlContext::GetTextureAtlas()
{
p->InitializeGL();
std::unique_lock lock(p->textureMutex_);
if (p->textureAtlas_ == GL_INVALID_INDEX)
auto& textureAtlas = util::TextureAtlas::Instance();
if (p->textureBufferCount_ != textureAtlas.BuildCount())
{
p->textureAtlas_ = util::TextureAtlas::Instance().BufferAtlas(p->gl_);
p->textureBufferCount_ = textureAtlas.BuildCount();
textureAtlas.BufferAtlas(p->gl_, p->textureAtlas_);
}
return p->textureAtlas_;
}
std::size_t GlContext::Impl::GetShaderKey(
std::initializer_list<std::pair<GLenum, std::string>> shaders)
{
std::size_t seed = 0;
for (auto& shader : shaders)
{
boost::hash_combine(seed, shader.first);
boost::hash_combine(seed, shader.second);
}
return seed;
}
} // namespace gl
} // namespace qt
} // namespace scwx

View file

@ -24,9 +24,13 @@ public:
gl::OpenGLFunctions& gl();
std::uint64_t texture_buffer_count() const;
std::shared_ptr<gl::ShaderProgram>
GetShaderProgram(const std::string& vertexPath,
const std::string& fragmentPath);
std::shared_ptr<gl::ShaderProgram> GetShaderProgram(
std::initializer_list<std::pair<GLenum, std::string>> shaders);
GLuint GetTextureAtlas();

View file

@ -15,6 +15,11 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
static constexpr GLsizei kInfoLogBufSize = 512;
static const std::unordered_map<GLenum, std::string> kShaderNames_ {
{GL_VERTEX_SHADER, "vertex"},
{GL_GEOMETRY_SHADER, "geometry"},
{GL_FRAGMENT_SHADER, "fragment"}};
class ShaderProgram::Impl
{
public:
@ -30,6 +35,8 @@ public:
gl_.glDeleteProgram(id_);
}
static std::string ShaderName(GLenum type);
OpenGLFunctions& gl_;
GLuint id_;
@ -49,10 +56,37 @@ GLuint ShaderProgram::id() const
return p->id_;
}
GLint ShaderProgram::GetUniformLocation(const std::string& name)
{
GLint location = p->gl_.glGetUniformLocation(p->id_, name.c_str());
if (location == -1)
{
logger_->warn("Could not find {}", name);
}
return location;
}
std::string ShaderProgram::Impl::ShaderName(GLenum type)
{
auto it = kShaderNames_.find(type);
if (it != kShaderNames_.cend())
{
return it->second;
}
return fmt::format("{:#06x}", type);
}
bool ShaderProgram::Load(const std::string& vertexPath,
const std::string& fragmentPath)
{
logger_->debug("Load: {}, {}", vertexPath, fragmentPath);
return Load({{GL_VERTEX_SHADER, vertexPath}, //
{GL_FRAGMENT_SHADER, fragmentPath}});
}
bool ShaderProgram::Load(
std::initializer_list<std::pair<GLenum, std::string>> shaders)
{
logger_->debug("Load()");
OpenGLFunctions& gl = p->gl_;
@ -61,81 +95,59 @@ bool ShaderProgram::Load(const std::string& vertexPath,
char infoLog[kInfoLogBufSize];
GLsizei logLength;
QFile vertexFile(vertexPath.c_str());
QFile fragmentFile(fragmentPath.c_str());
std::vector<GLuint> shaderIds {};
vertexFile.open(QIODevice::ReadOnly | QIODevice::Text);
fragmentFile.open(QIODevice::ReadOnly | QIODevice::Text);
if (!vertexFile.isOpen())
for (auto& shader : shaders)
{
logger_->error("Could not load vertex shader: {}", vertexPath);
return false;
logger_->debug("Loading {} shader: {}",
Impl::ShaderName(shader.first),
shader.second);
QFile file(shader.second.c_str());
file.open(QIODevice::ReadOnly | QIODevice::Text);
if (!file.isOpen())
{
logger_->error("Could not load shader");
success = false;
break;
}
if (!fragmentFile.isOpen())
{
logger_->error("Could not load fragment shader: {}", fragmentPath);
return false;
}
QTextStream shaderStream(&file);
shaderStream.setEncoding(QStringConverter::Utf8);
QTextStream vertexShaderStream(&vertexFile);
QTextStream fragmentShaderStream(&fragmentFile);
std::string shaderSource = shaderStream.readAll().toStdString();
const char* shaderSourceC = shaderSource.c_str();
vertexShaderStream.setEncoding(QStringConverter::Utf8);
fragmentShaderStream.setEncoding(QStringConverter::Utf8);
std::string vertexShaderSource = vertexShaderStream.readAll().toStdString();
std::string fragmentShaderSource =
fragmentShaderStream.readAll().toStdString();
const char* vertexShaderSourceC = vertexShaderSource.c_str();
const char* fragmentShaderSourceC = fragmentShaderSource.c_str();
// Create a vertex shader
GLuint vertexShader = gl.glCreateShader(GL_VERTEX_SHADER);
// Create a shader
GLuint shaderId = gl.glCreateShader(shader.first);
shaderIds.push_back(shaderId);
// Attach the shader source code and compile the shader
gl.glShaderSource(vertexShader, 1, &vertexShaderSourceC, NULL);
gl.glCompileShader(vertexShader);
gl.glShaderSource(shaderId, 1, &shaderSourceC, NULL);
gl.glCompileShader(shaderId);
// Check for errors
gl.glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &glSuccess);
gl.glGetShaderInfoLog(vertexShader, kInfoLogBufSize, &logLength, infoLog);
gl.glGetShaderiv(shaderId, GL_COMPILE_STATUS, &glSuccess);
gl.glGetShaderInfoLog(shaderId, kInfoLogBufSize, &logLength, infoLog);
if (!glSuccess)
{
logger_->error("Vertex shader compilation failed: {}", infoLog);
logger_->error("Shader compilation failed: {}", infoLog);
success = false;
break;
}
else if (logLength > 0)
{
logger_->error("Vertex shader compiled with warnings: {}", infoLog);
logger_->error("Shader compiled with warnings: {}", infoLog);
}
// Create a fragment shader
GLuint fragmentShader = gl.glCreateShader(GL_FRAGMENT_SHADER);
// Attach the shader source and compile the shader
gl.glShaderSource(fragmentShader, 1, &fragmentShaderSourceC, NULL);
gl.glCompileShader(fragmentShader);
// Check for errors
gl.glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &glSuccess);
gl.glGetShaderInfoLog(fragmentShader, kInfoLogBufSize, &logLength, infoLog);
if (!glSuccess)
{
logger_->error("Fragment shader compilation failed: {}", infoLog);
success = false;
}
else if (logLength > 0)
{
logger_->error("Fragment shader compiled with warnings: {}", infoLog);
}
if (success)
{
gl.glAttachShader(p->id_, vertexShader);
gl.glAttachShader(p->id_, fragmentShader);
for (auto& shaderId : shaderIds)
{
gl.glAttachShader(p->id_, shaderId);
}
gl.glLinkProgram(p->id_);
// Check for errors
@ -153,8 +165,10 @@ bool ShaderProgram::Load(const std::string& vertexPath,
}
// Delete shaders
gl.glDeleteShader(vertexShader);
gl.glDeleteShader(fragmentShader);
for (auto& shaderId : shaderIds)
{
gl.glDeleteShader(shaderId);
}
return success;
}

View file

@ -30,7 +30,10 @@ public:
GLuint id() const;
GLint GetUniformLocation(const std::string& name);
bool Load(const std::string& vertexPath, const std::string& fragmentPath);
bool Load(std::initializer_list<std::pair<GLenum, std::string>> shaderPaths);
void Use() const;

View file

@ -1,13 +1,18 @@
#define NOMINMAX
#include <scwx/qt/config/radar_site.hpp>
#include <scwx/qt/main/main_window.hpp>
#include <scwx/qt/main/versions.hpp>
#include <scwx/qt/manager/radar_product_manager.hpp>
#include <scwx/qt/manager/resource_manager.hpp>
#include <scwx/qt/manager/settings_manager.hpp>
#include <scwx/network/cpr.hpp>
#include <scwx/util/logger.hpp>
#include <scwx/util/threads.hpp>
#include <aws/core/Aws.h>
#include <boost/asio.hpp>
#include <fmt/format.h>
#include <spdlog/spdlog.h>
#include <QApplication>
#include <QTranslator>
@ -26,6 +31,8 @@ int main(int argc, char* argv[])
QApplication a(argc, argv);
QCoreApplication::setApplicationName("Supercell Wx");
scwx::network::cpr::SetUserAgent(
fmt::format("SupercellWx/{}", scwx::qt::main::kVersionString_));
// Enable internationalization support
QTranslator translator;
@ -62,7 +69,7 @@ int main(int argc, char* argv[])
// Initialize application
scwx::qt::config::RadarSite::Initialize();
scwx::qt::manager::SettingsManager::Initialize();
scwx::qt::manager::SettingsManager::Instance().Initialize();
scwx::qt::manager::ResourceManager::Initialize();
// Run Qt main loop
@ -82,7 +89,7 @@ int main(int argc, char* argv[])
// Shutdown application
scwx::qt::manager::ResourceManager::Shutdown();
scwx::qt::manager::SettingsManager::Shutdown();
scwx::qt::manager::SettingsManager::Instance().Shutdown();
// Shutdown AWS SDK
Aws::ShutdownAPI(awsSdkOptions);

View file

@ -5,14 +5,16 @@
#include <scwx/qt/main/application.hpp>
#include <scwx/qt/main/versions.hpp>
#include <scwx/qt/manager/placefile_manager.hpp>
#include <scwx/qt/manager/radar_product_manager.hpp>
#include <scwx/qt/manager/settings_manager.hpp>
#include <scwx/qt/manager/text_event_manager.hpp>
#include <scwx/qt/manager/timeline_manager.hpp>
#include <scwx/qt/manager/update_manager.hpp>
#include <scwx/qt/map/map_provider.hpp>
#include <scwx/qt/map/map_widget.hpp>
#include <scwx/qt/model/radar_product_model.hpp>
#include <scwx/qt/settings/general_settings.hpp>
#include <scwx/qt/settings/map_settings.hpp>
#include <scwx/qt/settings/ui_settings.hpp>
#include <scwx/qt/ui/about_dialog.hpp>
#include <scwx/qt/ui/alert_dock_widget.hpp>
@ -20,9 +22,11 @@
#include <scwx/qt/ui/collapsible_group.hpp>
#include <scwx/qt/ui/flow_layout.hpp>
#include <scwx/qt/ui/imgui_debug_dialog.hpp>
#include <scwx/qt/ui/layer_dialog.hpp>
#include <scwx/qt/ui/level2_products_widget.hpp>
#include <scwx/qt/ui/level2_settings_widget.hpp>
#include <scwx/qt/ui/level3_products_widget.hpp>
#include <scwx/qt/ui/placefile_dialog.hpp>
#include <scwx/qt/ui/radar_site_dialog.hpp>
#include <scwx/qt/ui/settings_dialog.hpp>
#include <scwx/qt/ui/update_dialog.hpp>
@ -75,10 +79,13 @@ public:
animationDockWidget_ {nullptr},
aboutDialog_ {nullptr},
imGuiDebugDialog_ {nullptr},
layerDialog_ {nullptr},
placefileDialog_ {nullptr},
radarSiteDialog_ {nullptr},
settingsDialog_ {nullptr},
updateDialog_ {nullptr},
radarProductModel_ {nullptr},
placefileManager_ {manager::PlacefileManager::Instance()},
textEventManager_ {manager::TextEventManager::Instance()},
timelineManager_ {manager::TimelineManager::Instance()},
updateManager_ {manager::UpdateManager::Instance()},
@ -87,10 +94,8 @@ public:
elevationButtonsChanged_ {false},
resizeElevationButtons_ {false}
{
mapProvider_ =
map::GetMapProvider(manager::SettingsManager::general_settings()
.map_provider()
.GetValue());
mapProvider_ = map::GetMapProvider(
settings::GeneralSettings::Instance().map_provider().GetValue());
const map::MapProviderInfo& mapProviderInfo =
map::GetMapProviderInfo(mapProvider_);
@ -164,11 +169,14 @@ public:
ui::AnimationDockWidget* animationDockWidget_;
ui::AboutDialog* aboutDialog_;
ui::ImGuiDebugDialog* imGuiDebugDialog_;
ui::LayerDialog* layerDialog_;
ui::PlacefileDialog* placefileDialog_;
ui::RadarSiteDialog* radarSiteDialog_;
ui::SettingsDialog* settingsDialog_;
ui::UpdateDialog* updateDialog_;
std::unique_ptr<model::RadarProductModel> radarProductModel_;
std::shared_ptr<manager::PlacefileManager> placefileManager_;
std::shared_ptr<manager::TextEventManager> textEventManager_;
std::shared_ptr<manager::TimelineManager> timelineManager_;
std::shared_ptr<manager::UpdateManager> updateManager_;
@ -227,7 +235,7 @@ MainWindow::MainWindow(QWidget* parent) :
ui->actionAlerts->setVisible(false);
ui->menuDebug->menuAction()->setVisible(
manager::SettingsManager::general_settings().debug_enabled().GetValue());
settings::GeneralSettings::Instance().debug_enabled().GetValue());
// Configure Resource Explorer Dock
ui->resourceExplorerDock->setVisible(false);
@ -241,6 +249,12 @@ MainWindow::MainWindow(QWidget* parent) :
// Radar Site Dialog
p->radarSiteDialog_ = new ui::RadarSiteDialog(this);
// Placefile Manager Dialog
p->placefileDialog_ = new ui::PlacefileDialog(this);
// Layer Dialog
p->layerDialog_ = new ui::LayerDialog(this);
// Settings Dialog
p->settingsDialog_ = new ui::SettingsDialog(this);
@ -303,7 +317,7 @@ MainWindow::MainWindow(QWidget* parent) :
// Update Dialog
p->updateDialog_ = new ui::UpdateDialog(this);
auto& mapSettings = manager::SettingsManager::map_settings();
auto& mapSettings = settings::MapSettings::Instance();
for (size_t i = 0; i < p->maps_.size(); i++)
{
p->SelectRadarProduct(p->maps_.at(i),
@ -441,11 +455,26 @@ void MainWindow::on_actionExit_triggered()
close();
}
void MainWindow::on_actionPlacefileManager_triggered()
{
p->placefileDialog_->show();
}
void MainWindow::on_actionLayerManager_triggered()
{
p->layerDialog_->show();
}
void MainWindow::on_actionImGuiDebug_triggered()
{
p->imGuiDebugDialog_->show();
}
void MainWindow::on_actionDumpLayerList_triggered()
{
p->activeMap_->DumpLayerList();
}
void MainWindow::on_actionDumpRadarProductRecords_triggered()
{
manager::RadarProductManager::DumpRecords();
@ -579,7 +608,7 @@ void MainWindow::on_resourceTreeView_doubleClicked(const QModelIndex& index)
void MainWindowImpl::AsyncSetup()
{
auto& generalSettings = manager::SettingsManager::general_settings();
auto& generalSettings = settings::GeneralSettings::Instance();
// Check for updates
if (generalSettings.update_notifications_enabled().GetValue())
@ -592,7 +621,7 @@ void MainWindowImpl::AsyncSetup()
void MainWindowImpl::ConfigureMapLayout()
{
auto& generalSettings = manager::SettingsManager::general_settings();
auto& generalSettings = settings::GeneralSettings::Instance();
const int64_t gridWidth = generalSettings.grid_width().GetValue();
const int64_t gridHeight = generalSettings.grid_height().GetValue();
@ -626,7 +655,7 @@ void MainWindowImpl::ConfigureMapLayout()
{
if (maps_.at(mapIndex) == nullptr)
{
maps_[mapIndex] = new map::MapWidget(settings_);
maps_[mapIndex] = new map::MapWidget(mapIndex, settings_);
}
hs->addWidget(maps_[mapIndex]);
@ -643,7 +672,7 @@ void MainWindowImpl::ConfigureMapLayout()
void MainWindowImpl::ConfigureMapStyles()
{
const auto& mapProviderInfo = map::GetMapProviderInfo(mapProvider_);
auto& mapSettings = manager::SettingsManager::map_settings();
auto& mapSettings = settings::MapSettings::Instance();
for (std::size_t i = 0; i < maps_.size(); i++)
{
@ -816,6 +845,15 @@ void MainWindowImpl::ConnectAnimationSignals()
timelineManager_.get(),
&manager::TimelineManager::AnimationStepEnd);
connect(timelineManager_.get(),
&manager::TimelineManager::SelectedTimeUpdated,
[this]()
{
for (auto map : maps_)
{
map->update();
}
});
connect(timelineManager_.get(),
&manager::TimelineManager::VolumeTimeUpdated,
[this](std::chrono::system_clock::time_point dateTime)
@ -885,8 +923,7 @@ void MainWindowImpl::ConnectOtherSignals()
{
if (maps_[i] == activeMap_)
{
auto& mapSettings =
manager::SettingsManager::map_settings();
auto& mapSettings = settings::MapSettings::Instance();
mapSettings.map_style(i).StageValue(text.toStdString());
break;
}
@ -1063,7 +1100,7 @@ void MainWindowImpl::UpdateMapStyle(const std::string& styleName)
{
if (maps_[i] == activeMap_)
{
auto& mapSettings = manager::SettingsManager::map_settings();
auto& mapSettings = settings::MapSettings::Instance();
mapSettings.map_style(i).StageValue(styleName);
break;
}
@ -1113,6 +1150,8 @@ void MainWindowImpl::UpdateRadarSite()
timelineManager_->SetRadarSite("?");
}
placefileManager_->SetRadarSite(radarSite);
}
void MainWindowImpl::UpdateVcp()

View file

@ -36,7 +36,10 @@ private slots:
void on_actionOpenTextEvent_triggered();
void on_actionSettings_triggered();
void on_actionExit_triggered();
void on_actionPlacefileManager_triggered();
void on_actionLayerManager_triggered();
void on_actionImGuiDebug_triggered();
void on_actionDumpLayerList_triggered();
void on_actionDumpRadarProductRecords_triggered();
void on_actionUserManual_triggered();
void on_actionDiscord_triggered();

View file

@ -85,10 +85,19 @@
</property>
<addaction name="actionImGuiDebug"/>
<addaction name="separator"/>
<addaction name="actionDumpLayerList"/>
<addaction name="actionDumpRadarProductRecords"/>
</widget>
<widget class="QMenu" name="menuTools">
<property name="title">
<string>&amp;Tools</string>
</property>
<addaction name="actionPlacefileManager"/>
<addaction name="actionLayerManager"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuView"/>
<addaction name="menuTools"/>
<addaction name="menuDebug"/>
<addaction name="menuHelp"/>
</widget>
@ -415,6 +424,29 @@
<string>&amp;Check for Updates</string>
</property>
</action>
<action name="actionPlacefileManager">
<property name="icon">
<iconset resource="../../../../scwx-qt.qrc">
<normaloff>:/res/icons/font-awesome-6/earth-americas-solid.svg</normaloff>:/res/icons/font-awesome-6/earth-americas-solid.svg</iconset>
</property>
<property name="text">
<string>&amp;Placefile Manager</string>
</property>
</action>
<action name="actionLayerManager">
<property name="icon">
<iconset resource="../../../../scwx-qt.qrc">
<normaloff>:/res/icons/font-awesome-6/layer-group-solid.svg</normaloff>:/res/icons/font-awesome-6/layer-group-solid.svg</iconset>
</property>
<property name="text">
<string>&amp;Layer Manager</string>
</property>
</action>
<action name="actionDumpLayerList">
<property name="text">
<string>Dump &amp;Layer List</string>
</property>
</action>
</widget>
<resources>
<include location="../../../../scwx-qt.qrc"/>

View file

@ -0,0 +1,550 @@
#include <scwx/qt/manager/font_manager.hpp>
#include <scwx/qt/manager/settings_manager.hpp>
#include <scwx/qt/settings/text_settings.hpp>
#include <scwx/util/environment.hpp>
#include <scwx/util/logger.hpp>
#include <filesystem>
#include <fstream>
#include <QFile>
#include <QFileInfo>
#include <QFontDatabase>
#include <QGuiApplication>
#include <QStandardPaths>
#include <boost/container_hash/hash.hpp>
#include <boost/unordered/unordered_flat_map.hpp>
#include <boost/unordered/unordered_flat_set.hpp>
#include <fontconfig/fontconfig.h>
namespace scwx
{
namespace qt
{
namespace manager
{
static const std::string logPrefix_ = "scwx::qt::manager::font_manager";
static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
static const std::string kFcTrueType_ {"TrueType"};
struct FontRecord
{
std::string family_ {};
std::string style_ {};
std::string filename_ {};
};
typedef std::pair<FontRecord, units::font_size::pixels<int>> FontRecordPair;
template<class Key>
struct FontRecordHash;
template<>
struct FontRecordHash<FontRecordPair>
{
size_t operator()(const FontRecordPair& x) const;
};
class FontManager::Impl
{
public:
explicit Impl(FontManager* self) : self_ {self}
{
InitializeEnvironment();
InitializeFontCache();
InitializeFontconfig();
ConnectSignals();
}
~Impl() { FinalizeFontconfig(); }
void ConnectSignals();
void FinalizeFontconfig();
void InitializeEnvironment();
void InitializeFontCache();
void InitializeFontconfig();
void UpdateImGuiFont(types::FontCategory fontCategory);
void UpdateQFont(types::FontCategory fontCategory);
const std::vector<char>& GetRawFontData(const std::string& filename);
static FontRecord MatchFontFile(const std::string& family,
const std::vector<std::string>& styles);
FontManager* self_;
std::string fontCachePath_ {};
std::shared_mutex imguiFontAtlasMutex_ {};
std::uint64_t imguiFontsBuildCount_ {};
boost::unordered_flat_map<FontRecordPair,
std::shared_ptr<types::ImGuiFont>,
FontRecordHash<FontRecordPair>>
imguiFonts_ {};
std::shared_mutex imguiFontsMutex_ {};
boost::unordered_flat_map<std::string, std::vector<char>> rawFontData_ {};
std::mutex rawFontDataMutex_ {};
std::shared_ptr<types::ImGuiFont> defaultFont_ {};
boost::unordered_flat_map<types::FontCategory,
std::shared_ptr<types::ImGuiFont>>
fontCategoryImguiFontMap_ {};
boost::unordered_flat_map<types::FontCategory, QFont>
fontCategoryQFontMap_ {};
std::mutex fontCategoryMutex_ {};
boost::unordered_flat_set<types::FontCategory> dirtyFonts_ {};
std::mutex dirtyFontsMutex_ {};
boost::unordered_flat_map<types::Font, int> fontIds_ {};
};
FontManager::FontManager() : p(std::make_unique<Impl>(this)) {}
FontManager::~FontManager() {};
void FontManager::Impl::ConnectSignals()
{
auto& textSettings = settings::TextSettings::Instance();
for (auto fontCategory : types::FontCategoryIterator())
{
textSettings.font_family(fontCategory)
.RegisterValueChangedCallback(
[this, fontCategory](const auto&)
{
std::unique_lock lock {dirtyFontsMutex_};
dirtyFonts_.insert(fontCategory);
});
textSettings.font_style(fontCategory)
.RegisterValueChangedCallback(
[this, fontCategory](const auto&)
{
std::unique_lock lock {dirtyFontsMutex_};
dirtyFonts_.insert(fontCategory);
});
textSettings.font_point_size(fontCategory)
.RegisterValueChangedCallback(
[this, fontCategory](const auto&)
{
std::unique_lock lock {dirtyFontsMutex_};
dirtyFonts_.insert(fontCategory);
});
}
QObject::connect(
&SettingsManager::Instance(),
&SettingsManager::SettingsSaved,
self_,
[this]()
{
std::scoped_lock lock {dirtyFontsMutex_, fontCategoryMutex_};
for (auto fontCategory : dirtyFonts_)
{
UpdateImGuiFont(fontCategory);
UpdateQFont(fontCategory);
}
dirtyFonts_.clear();
});
}
void FontManager::InitializeFonts()
{
for (auto fontCategory : types::FontCategoryIterator())
{
p->UpdateImGuiFont(fontCategory);
p->UpdateQFont(fontCategory);
}
}
void FontManager::Impl::UpdateImGuiFont(types::FontCategory fontCategory)
{
auto& textSettings = settings::TextSettings::Instance();
auto family = textSettings.font_family(fontCategory).GetValue();
auto styles = textSettings.font_style(fontCategory).GetValue();
units::font_size::points<double> size {
textSettings.font_point_size(fontCategory).GetValue()};
fontCategoryImguiFontMap_.insert_or_assign(
fontCategory, self_->LoadImGuiFont(family, {styles}, size));
}
void FontManager::Impl::UpdateQFont(types::FontCategory fontCategory)
{
auto& textSettings = settings::TextSettings::Instance();
auto family = textSettings.font_family(fontCategory).GetValue();
auto styles = textSettings.font_style(fontCategory).GetValue();
units::font_size::points<double> size {
textSettings.font_point_size(fontCategory).GetValue()};
QFont font = QFontDatabase::font(QString::fromStdString(family),
QString::fromStdString(styles),
static_cast<int>(size.value()));
font.setPointSizeF(size.value());
fontCategoryQFontMap_.insert_or_assign(fontCategory, font);
}
std::shared_mutex& FontManager::imgui_font_atlas_mutex()
{
return p->imguiFontAtlasMutex_;
}
std::uint64_t FontManager::imgui_fonts_build_count() const
{
return p->imguiFontsBuildCount_;
}
int FontManager::GetFontId(types::Font font) const
{
auto it = p->fontIds_.find(font);
if (it != p->fontIds_.cend())
{
return it->second;
}
return -1;
}
std::shared_ptr<types::ImGuiFont>
FontManager::GetImGuiFont(types::FontCategory fontCategory)
{
std::unique_lock lock {p->fontCategoryMutex_};
auto it = p->fontCategoryImguiFontMap_.find(fontCategory);
if (it != p->fontCategoryImguiFontMap_.cend())
{
return it->second;
}
return p->defaultFont_;
}
QFont FontManager::GetQFont(types::FontCategory fontCategory)
{
std::unique_lock lock {p->fontCategoryMutex_};
auto it = p->fontCategoryQFontMap_.find(fontCategory);
if (it != p->fontCategoryQFontMap_.cend())
{
return it->second;
}
return QGuiApplication::font();
}
std::shared_ptr<types::ImGuiFont>
FontManager::LoadImGuiFont(const std::string& family,
const std::vector<std::string>& styles,
units::font_size::points<double> size,
bool loadIfNotFound)
{
const std::string styleString = fmt::format("{}", fmt::join(styles, " "));
const std::string fontString =
fmt::format("{}-{}:{}", family, size.value(), styleString);
logger_->debug("LoadFontResource: {}", fontString);
FontRecord fontRecord = Impl::MatchFontFile(family, styles);
// Only allow whole pixels, and clamp to 6-72 pt
units::font_size::pixels<double> pixels {size};
units::font_size::pixels<int> imFontSize {
std::clamp(static_cast<int>(pixels.value()), 8, 96)};
auto imguiFontKey = std::make_pair(fontRecord, imFontSize);
// Search for a loaded ImGui font
{
std::shared_lock imguiFontLock {p->imguiFontsMutex_};
// Search for the associated ImGui font
auto it = p->imguiFonts_.find(imguiFontKey);
if (it != p->imguiFonts_.end())
{
return it->second;
}
// No ImGui font was found, we need to create one
}
// No font was found, return an empty shared pointer if not loading
if (!loadIfNotFound)
{
return nullptr;
}
// Get raw font data
const auto& rawFontData = p->GetRawFontData(fontRecord.filename_);
// The font atlas mutex might already be locked within an ImGui render frame.
// Lock the font atlas mutex before the fonts mutex to prevent deadlock.
std::unique_lock imguiFontAtlasLock {p->imguiFontAtlasMutex_};
std::unique_lock imguiFontsLock {p->imguiFontsMutex_};
// Search for the associated ImGui font again, to prevent loading the same
// font twice
auto it = p->imguiFonts_.find(imguiFontKey);
if (it != p->imguiFonts_.end())
{
return it->second;
}
// Define a name for the ImGui font
std::string fontName;
try
{
fontName = fmt::format(
"{}:{}",
std::filesystem::path(fontRecord.filename_).filename().string(),
imFontSize.value());
}
catch (const std::exception& ex)
{
logger_->warn(ex.what());
fontName = fmt::format("{}:{}", fontRecord.filename_, imFontSize.value());
}
// Create an ImGui font
std::shared_ptr<types::ImGuiFont> imguiFont =
std::make_shared<types::ImGuiFont>(fontName, rawFontData, imFontSize);
// Store the ImGui font
p->imguiFonts_.insert_or_assign(imguiFontKey, imguiFont);
// Increment ImGui font build count
++p->imguiFontsBuildCount_;
// Return the ImGui font
return imguiFont;
}
const std::vector<char>&
FontManager::Impl::GetRawFontData(const std::string& filename)
{
std::unique_lock rawFontDataLock {rawFontDataMutex_};
auto it = rawFontData_.find(filename);
if (it != rawFontData_.end())
{
// Raw font data has already been loaded
return it->second;
}
// Raw font data needs to be loaded
std::basic_ifstream<char> ifs {filename, std::ios::binary};
ifs.seekg(0, std::ios_base::end);
std::size_t dataSize = ifs.tellg();
ifs.seekg(0, std::ios_base::beg);
// Store the font data in a buffer
std::vector<char> buffer {};
buffer.reserve(dataSize);
std::copy(std::istreambuf_iterator<char>(ifs),
std::istreambuf_iterator<char>(),
std::back_inserter(buffer));
// Place the buffer in the cache
auto result = rawFontData_.emplace(filename, std::move(buffer));
// Return the cached buffer
return result.first->second;
}
void FontManager::LoadApplicationFont(types::Font font,
const std::string& filename)
{
// If the font cache failed to create, don't attempt to cache any fonts
if (p->fontCachePath_.empty())
{
return;
}
// Make a copy of the font in the cache (if it doesn't exist)
QFile fontFile(QString::fromStdString(filename));
QFileInfo fontFileInfo(fontFile);
QFile cacheFile(QString::fromStdString(p->fontCachePath_) +
fontFileInfo.fileName());
QFileInfo cacheFileInfo(cacheFile);
std::string cacheFilename = cacheFile.fileName().toStdString();
if (fontFile.exists())
{
// If the file has not been cached, or the font file size has changed
if (!cacheFile.exists() || fontFileInfo.size() != cacheFileInfo.size())
{
logger_->info("Caching font: {}", filename);
if (!fontFile.copy(cacheFile.fileName()))
{
logger_->error("Could not cache font: {}", filename);
return;
}
}
}
else
{
logger_->error("Font does not exist: {}", filename);
return;
}
// Load the file into the Qt Font Database
int fontId =
QFontDatabase::addApplicationFont(QString::fromStdString(cacheFilename));
p->fontIds_.emplace(font, fontId);
// Load the file into fontconfig
FcBool result = FcConfigAppFontAddFile(
nullptr, reinterpret_cast<const FcChar8*>(cacheFilename.c_str()));
if (!result)
{
logger_->error("Could not load font into fontconfig database", filename);
}
}
void FontManager::Impl::InitializeEnvironment()
{
#if defined(__linux__)
// Because of the way Fontconfig is built with Conan, FONTCONFIG_PATH must be
// defined on Linux to ensure fonts can be found
static const std::string kFontconfigPathKey {"FONTCONFIG_PATH"};
std::string fontconfigPath = scwx::util::GetEnvironment(kFontconfigPathKey);
if (fontconfigPath.empty())
{
scwx::util::SetEnvironment(kFontconfigPathKey, "/etc/fonts");
}
#endif
}
void FontManager::Impl::InitializeFontCache()
{
std::string cachePath {
QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
.toStdString() +
"/fonts"};
fontCachePath_ = cachePath + "/";
if (!std::filesystem::exists(cachePath))
{
std::error_code error;
if (!std::filesystem::create_directories(cachePath, error))
{
logger_->error("Unable to create font cache directory: \"{}\" ({})",
cachePath,
error.message());
fontCachePath_.clear();
}
}
}
void FontManager::Impl::InitializeFontconfig()
{
FcConfig* fcConfig = FcInitLoadConfigAndFonts();
FcConfigSetCurrent(fcConfig);
}
void FontManager::Impl::FinalizeFontconfig()
{
FcFini();
}
FontRecord
FontManager::Impl::MatchFontFile(const std::string& family,
const std::vector<std::string>& styles)
{
const std::string styleString = fmt::format("{}", fmt::join(styles, " "));
const std::string fontString = fmt::format("{}:{}", family, styleString);
// Build fontconfig pattern
FcPattern* pattern = FcPatternCreate();
FcPatternAddString(
pattern, FC_FAMILY, reinterpret_cast<const FcChar8*>(family.c_str()));
FcPatternAddString(pattern,
FC_FONTFORMAT,
reinterpret_cast<const FcChar8*>(kFcTrueType_.c_str()));
if (!styles.empty())
{
FcPatternAddString(pattern,
FC_STYLE,
reinterpret_cast<const FcChar8*>(styleString.c_str()));
}
// Perform font pattern match substitution
FcConfigSubstitute(nullptr, pattern, FcMatchPattern);
FcDefaultSubstitute(pattern);
// Find matching font
FcResult result;
FcPattern* match = FcFontMatch(nullptr, pattern, &result);
FontRecord record {};
if (match != nullptr)
{
FcChar8* fcFamily;
FcChar8* fcStyle;
FcChar8* fcFile;
// Match was found, get properties
if (FcPatternGetString(match, FC_FAMILY, 0, &fcFamily) == FcResultMatch &&
FcPatternGetString(match, FC_STYLE, 0, &fcStyle) == FcResultMatch &&
FcPatternGetString(match, FC_FILE, 0, &fcFile) == FcResultMatch)
{
record.family_ = reinterpret_cast<char*>(fcFamily);
record.style_ = reinterpret_cast<char*>(fcStyle);
record.filename_ = reinterpret_cast<char*>(fcFile);
logger_->debug("Found matching font: {}:{} ({})",
record.family_,
record.style_,
record.filename_);
}
}
if (record.filename_.empty())
{
logger_->warn("Could not find matching font: {}", fontString);
}
// Cleanup
FcPatternDestroy(match);
FcPatternDestroy(pattern);
return record;
}
FontManager& FontManager::Instance()
{
static FontManager instance_ {};
return instance_;
}
size_t FontRecordHash<FontRecordPair>::operator()(const FontRecordPair& x) const
{
size_t seed = 0;
boost::hash_combine(seed, x.first.family_);
boost::hash_combine(seed, x.first.style_);
boost::hash_combine(seed, x.first.filename_);
boost::hash_combine(seed, x.second.value());
return seed;
}
bool operator==(const FontRecord& lhs, const FontRecord& rhs)
{
return lhs.family_ == rhs.family_ && //
lhs.style_ == rhs.style_ && //
lhs.filename_ == rhs.filename_;
}
} // namespace manager
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,53 @@
#pragma once
#include <scwx/qt/types/imgui_font.hpp>
#include <scwx/qt/types/font_types.hpp>
#include <scwx/qt/types/text_types.hpp>
#include <shared_mutex>
#include <QFont>
#include <QObject>
namespace scwx
{
namespace qt
{
namespace manager
{
class FontManager : public QObject
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(FontManager)
public:
explicit FontManager();
~FontManager();
std::shared_mutex& imgui_font_atlas_mutex();
std::uint64_t imgui_fonts_build_count() const;
int GetFontId(types::Font font) const;
std::shared_ptr<types::ImGuiFont>
GetImGuiFont(types::FontCategory fontCategory);
QFont GetQFont(types::FontCategory fontCategory);
std::shared_ptr<types::ImGuiFont>
LoadImGuiFont(const std::string& family,
const std::vector<std::string>& styles,
units::font_size::points<double> size,
bool loadIfNotFound = true);
void LoadApplicationFont(types::Font font, const std::string& filename);
void InitializeFonts();
static FontManager& Instance();
private:
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace manager
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,808 @@
#include <scwx/qt/manager/placefile_manager.hpp>
#include <scwx/qt/manager/font_manager.hpp>
#include <scwx/qt/manager/resource_manager.hpp>
#include <scwx/qt/main/application.hpp>
#include <scwx/qt/util/json.hpp>
#include <scwx/qt/util/network.hpp>
#include <scwx/gr/placefile.hpp>
#include <scwx/network/cpr.hpp>
#include <scwx/util/logger.hpp>
#include <shared_mutex>
#include <vector>
#include <QDir>
#include <QGuiApplication>
#include <QScreen>
#include <QStandardPaths>
#include <QUrl>
#include <boost/algorithm/string.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/thread_pool.hpp>
#include <boost/json.hpp>
#include <boost/tokenizer.hpp>
#include <cpr/cpr.h>
#include <fmt/chrono.h>
namespace scwx
{
namespace qt
{
namespace manager
{
static const std::string logPrefix_ = "scwx::qt::manager::placefile_manager";
static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
static const std::string kEnabledName_ = "enabled";
static const std::string kThresholdedName_ = "thresholded";
static const std::string kTitleName_ = "title";
static const std::string kNameName_ = "name";
class PlacefileManager::Impl
{
public:
class PlacefileRecord;
explicit Impl(PlacefileManager* self) : self_ {self} {}
~Impl() { threadPool_.join(); }
void InitializePlacefileSettings();
void ReadPlacefileSettings();
void WritePlacefileSettings();
static boost::unordered_flat_map<std::size_t,
std::shared_ptr<types::ImGuiFont>>
LoadFontResources(const std::shared_ptr<gr::Placefile>& placefile);
static std::vector<std::shared_ptr<boost::gil::rgba8_image_t>>
LoadImageResources(const std::shared_ptr<gr::Placefile>& placefile);
boost::asio::thread_pool threadPool_ {1u};
PlacefileManager* self_;
std::string placefileSettingsPath_ {};
std::shared_ptr<config::RadarSite> radarSite_ {};
std::vector<std::shared_ptr<PlacefileRecord>> placefileRecords_ {};
boost::unordered_flat_map<std::string, std::shared_ptr<PlacefileRecord>>
placefileRecordMap_ {};
std::shared_mutex placefileRecordLock_ {};
};
class PlacefileManager::Impl::PlacefileRecord
{
public:
explicit PlacefileRecord(Impl* impl,
const std::string& name,
std::shared_ptr<gr::Placefile> placefile,
const std::string& title = {},
bool enabled = false,
bool thresholded = false) :
p {impl},
name_ {name},
title_ {title},
placefile_ {placefile},
enabled_ {enabled},
thresholded_ {thresholded}
{
}
~PlacefileRecord()
{
std::unique_lock refreshLock(refreshMutex_);
std::unique_lock timerLock(timerMutex_);
refreshTimer_.cancel();
timerLock.unlock();
refreshLock.unlock();
threadPool_.join();
}
bool refresh_enabled() const;
std::chrono::seconds refresh_time() const;
void CancelRefresh();
void ScheduleRefresh();
void Update();
void UpdateAsync();
friend void tag_invoke(boost::json::value_from_tag,
boost::json::value& jv,
const std::shared_ptr<PlacefileRecord>& record)
{
jv = {{kEnabledName_, record->enabled_},
{kThresholdedName_, record->thresholded_},
{kTitleName_, record->title_},
{kNameName_, record->name_}};
}
friend PlacefileRecord tag_invoke(boost::json::value_to_tag<PlacefileRecord>,
const boost::json::value& jv)
{
return PlacefileRecord {
nullptr,
boost::json::value_to<std::string>(jv.at(kNameName_)),
nullptr,
boost::json::value_to<std::string>(jv.at(kTitleName_)),
jv.at(kEnabledName_).as_bool(),
jv.at(kThresholdedName_).as_bool()};
}
Impl* p;
std::string name_;
std::string title_;
std::shared_ptr<gr::Placefile> placefile_;
bool enabled_;
bool thresholded_;
boost::asio::thread_pool threadPool_ {1u};
boost::asio::steady_timer refreshTimer_ {threadPool_};
std::mutex refreshMutex_ {};
std::mutex timerMutex_ {};
boost::unordered_flat_map<std::size_t, std::shared_ptr<types::ImGuiFont>>
fonts_ {};
std::mutex fontsMutex_ {};
std::vector<std::shared_ptr<boost::gil::rgba8_image_t>> images_ {};
std::string lastRadarSite_ {};
std::chrono::system_clock::time_point lastUpdateTime_ {};
};
PlacefileManager::PlacefileManager() : p(std::make_unique<Impl>(this))
{
boost::asio::post(p->threadPool_,
[this]()
{
p->InitializePlacefileSettings();
// Read placefile settings on startup
main::Application::WaitForInitialization();
p->ReadPlacefileSettings();
Q_EMIT PlacefilesInitialized();
});
}
PlacefileManager::~PlacefileManager()
{
// Write placefile settings on shutdown
p->WritePlacefileSettings();
};
bool PlacefileManager::placefile_enabled(const std::string& name)
{
std::shared_lock lock(p->placefileRecordLock_);
auto it = p->placefileRecordMap_.find(name);
if (it != p->placefileRecordMap_.cend())
{
return it->second->enabled_;
}
return false;
}
bool PlacefileManager::placefile_thresholded(const std::string& name)
{
std::shared_lock lock(p->placefileRecordLock_);
auto it = p->placefileRecordMap_.find(name);
if (it != p->placefileRecordMap_.cend())
{
return it->second->thresholded_;
}
return false;
}
std::string PlacefileManager::placefile_title(const std::string& name)
{
std::shared_lock lock(p->placefileRecordLock_);
auto it = p->placefileRecordMap_.find(name);
if (it != p->placefileRecordMap_.cend())
{
return it->second->title_;
}
return {};
}
std::shared_ptr<gr::Placefile>
PlacefileManager::placefile(const std::string& name)
{
std::shared_lock lock(p->placefileRecordLock_);
auto it = p->placefileRecordMap_.find(name);
if (it != p->placefileRecordMap_.cend())
{
return it->second->placefile_;
}
return nullptr;
}
boost::unordered_flat_map<std::size_t, std::shared_ptr<types::ImGuiFont>>
PlacefileManager::placefile_fonts(const std::string& name)
{
std::shared_lock lock(p->placefileRecordLock_);
auto it = p->placefileRecordMap_.find(name);
if (it != p->placefileRecordMap_.cend())
{
std::unique_lock fontsLock {it->second->fontsMutex_};
return it->second->fonts_;
}
return {};
}
void PlacefileManager::set_placefile_enabled(const std::string& name,
bool enabled)
{
std::shared_lock lock(p->placefileRecordLock_);
auto it = p->placefileRecordMap_.find(name);
if (it != p->placefileRecordMap_.cend())
{
auto record = it->second;
record->enabled_ = enabled;
lock.unlock();
Q_EMIT PlacefileEnabled(name, enabled);
using namespace std::chrono_literals;
// Update the placefile
if (enabled)
{
if (p->radarSite_ != nullptr &&
record->lastRadarSite_ != p->radarSite_->id())
{
// If the radar site has changed, update now
record->UpdateAsync();
}
else
{
// Otherwise, schedule an update
record->ScheduleRefresh();
}
}
else if (!enabled)
{
record->CancelRefresh();
}
}
}
void PlacefileManager::set_placefile_thresholded(const std::string& name,
bool thresholded)
{
std::shared_lock lock(p->placefileRecordLock_);
auto it = p->placefileRecordMap_.find(name);
if (it != p->placefileRecordMap_.cend())
{
it->second->thresholded_ = thresholded;
lock.unlock();
Q_EMIT PlacefileUpdated(name);
}
}
void PlacefileManager::set_placefile_url(const std::string& name,
const std::string& newUrl)
{
std::string normalizedUrl = util::network::NormalizeUrl(newUrl);
std::unique_lock lock(p->placefileRecordLock_);
auto it = p->placefileRecordMap_.find(name);
auto itNew = p->placefileRecordMap_.find(normalizedUrl);
if (it != p->placefileRecordMap_.cend() &&
itNew == p->placefileRecordMap_.cend())
{
auto placefileRecord = it->second;
placefileRecord->name_ = normalizedUrl;
placefileRecord->placefile_ = nullptr;
placefileRecord->fonts_.clear();
placefileRecord->images_.clear();
p->placefileRecordMap_.erase(it);
p->placefileRecordMap_.insert_or_assign(normalizedUrl, placefileRecord);
lock.unlock();
Q_EMIT PlacefileRenamed(name, normalizedUrl);
// Queue a placefile update
placefileRecord->UpdateAsync();
}
}
bool PlacefileManager::Impl::PlacefileRecord::refresh_enabled() const
{
if (placefile_ != nullptr)
{
using namespace std::chrono_literals;
return placefile_->refresh() > 0s;
}
return false;
}
std::chrono::seconds
PlacefileManager::Impl::PlacefileRecord::refresh_time() const
{
using namespace std::chrono_literals;
if (refresh_enabled())
{
// Don't refresh more often than every 15 seconds
return std::max(placefile_->refresh(), 15s);
}
return -1s;
}
void PlacefileManager::Impl::InitializePlacefileSettings()
{
std::string appDataPath {
QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)
.toStdString()};
if (!std::filesystem::exists(appDataPath))
{
if (!std::filesystem::create_directories(appDataPath))
{
logger_->error("Unable to create application data directory: \"{}\"",
appDataPath);
}
}
placefileSettingsPath_ = appDataPath + "/placefiles.json";
}
void PlacefileManager::Impl::ReadPlacefileSettings()
{
logger_->info("Reading placefile settings");
boost::json::value placefileJson = nullptr;
// Determine if placefile settings exists
if (std::filesystem::exists(placefileSettingsPath_))
{
placefileJson = util::json::ReadJsonFile(placefileSettingsPath_);
}
// If placefile settings was successfully read
if (placefileJson != nullptr && placefileJson.is_array())
{
// For each placefile entry
auto& placefileArray = placefileJson.as_array();
for (auto& placefileEntry : placefileArray)
{
try
{
// Convert placefile entry to a record
PlacefileRecord record =
boost::json::value_to<PlacefileRecord>(placefileEntry);
if (!record.name_.empty())
{
self_->AddUrl(record.name_,
record.title_,
record.enabled_,
record.thresholded_);
}
}
catch (const std::exception& ex)
{
logger_->warn("Invalid placefile entry: {}", ex.what());
}
}
}
}
void PlacefileManager::Impl::WritePlacefileSettings()
{
logger_->info("Saving placefile settings");
std::shared_lock lock {placefileRecordLock_};
auto placefileJson = boost::json::value_from(placefileRecords_);
util::json::WriteJsonFile(placefileSettingsPath_, placefileJson);
}
void PlacefileManager::SetRadarSite(
std::shared_ptr<config::RadarSite> radarSite)
{
if (p->radarSite_ == radarSite || radarSite == nullptr)
{
// No action needed
return;
}
logger_->debug("SetRadarSite: {}", radarSite->id());
p->radarSite_ = radarSite;
// Update all enabled records
std::shared_lock lock(p->placefileRecordLock_);
for (auto& record : p->placefileRecords_)
{
if (record->enabled_)
{
record->UpdateAsync();
}
}
}
void PlacefileManager::AddUrl(const std::string& urlString,
const std::string& title,
bool enabled,
bool thresholded)
{
std::string normalizedUrl = util::network::NormalizeUrl(urlString);
std::unique_lock lock(p->placefileRecordLock_);
// Determine if the placefile has been loaded previously
auto it = std::find_if(p->placefileRecords_.begin(),
p->placefileRecords_.end(),
[&normalizedUrl](auto& record)
{ return record->name_ == normalizedUrl; });
if (it != p->placefileRecords_.end())
{
logger_->debug("Placefile already added: {}", normalizedUrl);
return;
}
// Placefile is new, proceed with adding
logger_->info("AddUrl: {}", normalizedUrl);
// Add an empty placefile record for the new URL
auto& record =
p->placefileRecords_.emplace_back(std::make_shared<Impl::PlacefileRecord>(
p.get(), normalizedUrl, nullptr, title, enabled, thresholded));
p->placefileRecordMap_.insert_or_assign(normalizedUrl, record);
lock.unlock();
if (enabled)
{
Q_EMIT PlacefileEnabled(normalizedUrl, record->enabled_);
}
Q_EMIT PlacefileUpdated(normalizedUrl);
// Queue a placefile update, either if enabled, or if we don't know the title
if (enabled || title.empty())
{
record->UpdateAsync();
}
}
void PlacefileManager::RemoveUrl(const std::string& urlString)
{
std::unique_lock lock(p->placefileRecordLock_);
// Determine if the placefile has been loaded previously
auto it = std::find_if(p->placefileRecords_.begin(),
p->placefileRecords_.end(),
[&urlString](auto& record)
{ return record->name_ == urlString; });
if (it == p->placefileRecords_.end())
{
logger_->debug("Placefile doesn't exist: {}", urlString);
return;
}
// Placefile exists, proceed with removing
logger_->info("RemoveUrl: {}", urlString);
// Remove record
p->placefileRecords_.erase(it);
p->placefileRecordMap_.erase(urlString);
lock.unlock();
Q_EMIT PlacefileRemoved(urlString);
}
void PlacefileManager::Refresh(const std::string& name)
{
std::shared_lock lock {p->placefileRecordLock_};
auto it = p->placefileRecordMap_.find(name);
if (it != p->placefileRecordMap_.cend())
{
it->second->UpdateAsync();
}
}
void PlacefileManager::Impl::PlacefileRecord::Update()
{
logger_->debug("Update: {}", name_);
// Take unique lock before refreshing
std::unique_lock lock {refreshMutex_};
// Make a copy of name in the event it changes.
const std::string name {name_};
std::shared_ptr<gr::Placefile> updatedPlacefile {};
QUrl url = QUrl::fromUserInput(QString::fromStdString(name));
if (url.isLocalFile())
{
updatedPlacefile = gr::Placefile::Load(name);
}
else
{
std::string decodedUrl {name};
auto queryPos = decodedUrl.find('?');
if (queryPos != std::string::npos)
{
decodedUrl.erase(queryPos);
}
if (p->radarSite_ == nullptr)
{
// Wait to process until a radar site is selected
return;
}
auto dpi = QGuiApplication::primaryScreen()->logicalDotsPerInch();
// Specify parameters
auto parameters = cpr::Parameters {
{"version", "1.5"}, // Placefile Version Supported
{"dpi", fmt::format("{:0.0f}", dpi)},
{"lat", fmt::format("{:0.3f}", p->radarSite_->latitude())},
{"lon", fmt::format("{:0.3f}", p->radarSite_->longitude())}};
// Iterate through each query parameter in the URL
if (url.hasQuery())
{
auto query = url.query(QUrl::ComponentFormattingOption::PrettyDecoded)
.toStdString();
boost::char_separator<char> delimiter("&");
boost::tokenizer tokens(query, delimiter);
for (auto& token : tokens)
{
std::vector<std::string> split {};
boost::split(split, token, boost::is_any_of("="));
if (split.size() >= 2)
{
// Token is a key=value parameter
parameters.Add({split[0], split[1]});
}
else
{
// Token is a single key with no value
parameters.Add({token, {}});
}
}
}
// Send HTTP GET request
auto response =
cpr::Get(cpr::Url {decodedUrl}, network::cpr::GetHeader(), parameters);
if (cpr::status::is_success(response.status_code))
{
std::istringstream responseBody {response.text};
updatedPlacefile = gr::Placefile::Load(name, responseBody);
}
else if (response.status_code == 0)
{
logger_->error("Error loading placefile: {}", response.error.message);
}
else
{
logger_->error("Error loading placefile: {}", response.status_line);
}
}
if (updatedPlacefile != nullptr)
{
// Load placefile resources
auto newFonts = Impl::LoadFontResources(updatedPlacefile);
auto newImages = Impl::LoadImageResources(updatedPlacefile);
// Check the name matches, in case the name updated
if (name_ == name)
{
// Update the placefile
placefile_ = updatedPlacefile;
title_ = placefile_->title();
lastUpdateTime_ = std::chrono::system_clock::now();
// Update font resources
{
std::unique_lock fontsLock {fontsMutex_};
fonts_.swap(newFonts);
newFonts.clear();
}
// Update image resources
images_.swap(newImages);
newImages.clear();
if (p->radarSite_ != nullptr)
{
lastRadarSite_ = p->radarSite_->id();
}
// Notify slots of the placefile update
Q_EMIT p->self_->PlacefileUpdated(name);
}
}
// Update refresh timer
ScheduleRefresh();
}
void PlacefileManager::Impl::PlacefileRecord::ScheduleRefresh()
{
using namespace std::chrono_literals;
if (!enabled_ || !refresh_enabled())
{
// Refresh is disabled
return;
}
std::unique_lock lock {timerMutex_};
auto nextUpdateTime = lastUpdateTime_ + refresh_time();
auto timeUntilNextUpdate = nextUpdateTime - std::chrono::system_clock::now();
logger_->debug(
"Scheduled refresh in {:%M:%S} ({})",
std::chrono::duration_cast<std::chrono::seconds>(timeUntilNextUpdate),
name_);
refreshTimer_.expires_after(timeUntilNextUpdate);
refreshTimer_.async_wait(
[this](const boost::system::error_code& e)
{
if (e == boost::asio::error::operation_aborted)
{
logger_->debug("Refresh timer cancelled");
}
else if (e != boost::system::errc::success)
{
logger_->warn("Refresh timer error: {}", e.message());
}
else
{
Update();
}
});
}
void PlacefileManager::Impl::PlacefileRecord::CancelRefresh()
{
std::unique_lock lock {timerMutex_};
refreshTimer_.cancel();
}
void PlacefileManager::Impl::PlacefileRecord::UpdateAsync()
{
boost::asio::post(threadPool_, [this]() { Update(); });
}
std::shared_ptr<PlacefileManager> PlacefileManager::Instance()
{
static std::weak_ptr<PlacefileManager> placefileManagerReference_ {};
static std::mutex instanceMutex_ {};
std::unique_lock lock(instanceMutex_);
std::shared_ptr<PlacefileManager> placefileManager =
placefileManagerReference_.lock();
if (placefileManager == nullptr)
{
placefileManager = std::make_shared<PlacefileManager>();
placefileManagerReference_ = placefileManager;
}
return placefileManager;
}
boost::unordered_flat_map<std::size_t, std::shared_ptr<types::ImGuiFont>>
PlacefileManager::Impl::LoadFontResources(
const std::shared_ptr<gr::Placefile>& placefile)
{
boost::unordered_flat_map<std::size_t, std::shared_ptr<types::ImGuiFont>>
imGuiFonts {};
auto fonts = placefile->fonts();
for (auto& font : fonts)
{
units::font_size::pixels<double> size {font.second->pixels_};
std::vector<std::string> styles {};
if (font.second->IsBold())
{
styles.push_back("bold");
}
if (font.second->IsItalic())
{
styles.push_back("italic");
}
auto imGuiFont = FontManager::Instance().LoadImGuiFont(
font.second->face_, styles, size);
imGuiFonts.emplace(font.first, std::move(imGuiFont));
}
return imGuiFonts;
}
std::vector<std::shared_ptr<boost::gil::rgba8_image_t>>
PlacefileManager::Impl::LoadImageResources(
const std::shared_ptr<gr::Placefile>& placefile)
{
const auto iconFiles = placefile->icon_files();
const auto drawItems = placefile->GetDrawItems();
const QUrl baseUrl =
QUrl::fromUserInput(QString::fromStdString(placefile->name()));
std::vector<std::string> urlStrings {};
urlStrings.reserve(iconFiles.size());
// Resolve Icon Files
std::transform(iconFiles.cbegin(),
iconFiles.cend(),
std::back_inserter(urlStrings),
[&baseUrl](auto& iconFile)
{
// Resolve target URL relative to base URL
QString filePath =
QString::fromStdString(iconFile->filename_);
QUrl fileUrl = QUrl(QDir::fromNativeSeparators(filePath));
QUrl resolvedUrl = baseUrl.resolved(fileUrl);
return resolvedUrl.toString().toStdString();
});
// Resolve Image Files
for (auto& di : drawItems)
{
switch (di->itemType_)
{
case gr::Placefile::ItemType::Image:
{
const std::string& imageFile =
std::static_pointer_cast<gr::Placefile::ImageDrawItem>(di)
->imageFile_;
QString filePath = QString::fromStdString(imageFile);
QUrl fileUrl = QUrl(QDir::fromNativeSeparators(filePath));
QUrl resolvedUrl = baseUrl.resolved(fileUrl);
std::string urlString = resolvedUrl.toString().toStdString();
if (std::find(urlStrings.cbegin(), urlStrings.cend(), urlString) ==
urlStrings.cend())
{
urlStrings.push_back(urlString);
}
break;
}
default:
break;
}
}
return ResourceManager::LoadImageResources(urlStrings);
}
} // namespace manager
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,63 @@
#pragma once
#include <scwx/gr/placefile.hpp>
#include <scwx/qt/config/radar_site.hpp>
#include <scwx/qt/types/imgui_font.hpp>
#include <QObject>
#include <boost/unordered/unordered_flat_map.hpp>
namespace scwx
{
namespace qt
{
namespace manager
{
class PlacefileManager : public QObject
{
Q_OBJECT
public:
explicit PlacefileManager();
~PlacefileManager();
bool placefile_enabled(const std::string& name);
bool placefile_thresholded(const std::string& name);
std::string placefile_title(const std::string& name);
std::shared_ptr<gr::Placefile> placefile(const std::string& name);
boost::unordered_flat_map<std::size_t, std::shared_ptr<types::ImGuiFont>>
placefile_fonts(const std::string& name);
void set_placefile_enabled(const std::string& name, bool enabled);
void set_placefile_thresholded(const std::string& name, bool thresholded);
void set_placefile_url(const std::string& name, const std::string& newUrl);
void SetRadarSite(std::shared_ptr<config::RadarSite> radarSite);
void AddUrl(const std::string& urlString,
const std::string& title = {},
bool enabled = false,
bool thresholded = false);
void RemoveUrl(const std::string& urlString);
void Refresh(const std::string& name);
static std::shared_ptr<PlacefileManager> Instance();
signals:
void PlacefilesInitialized();
void PlacefileEnabled(const std::string& name, bool enabled);
void PlacefileRemoved(const std::string& name);
void PlacefileRenamed(const std::string& oldName,
const std::string& newName);
void PlacefileUpdated(const std::string& name);
private:
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace manager
} // namespace qt
} // namespace scwx

View file

@ -1,13 +1,14 @@
#include <scwx/qt/manager/resource_manager.hpp>
#include <scwx/qt/manager/font_manager.hpp>
#include <scwx/qt/config/county_database.hpp>
#include <scwx/qt/model/imgui_context_model.hpp>
#include <scwx/qt/util/font.hpp>
#include <scwx/qt/util/texture_atlas.hpp>
#include <scwx/util/logger.hpp>
#include <imgui.h>
#include <execution>
#include <mutex>
#include <QFontDatabase>
#include <imgui.h>
namespace scwx
{
@ -24,11 +25,10 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
static void LoadFonts();
static void LoadTextures();
static const std::unordered_map<types::Font, std::string> fontNames_ {
static const std::vector<std::pair<types::Font, std::string>> fontNames_ {
{types::Font::din1451alt, ":/res/fonts/din1451alt.ttf"},
{types::Font::din1451alt_g, ":/res/fonts/din1451alt_g.ttf"}};
static std::unordered_map<types::Font, int> fontIds_ {};
{types::Font::din1451alt_g, ":/res/fonts/din1451alt_g.ttf"},
{types::Font::Inconsolata_Regular, ":/res/fonts/Inconsolata-Regular.ttf"}};
void Initialize()
{
@ -40,29 +40,52 @@ void Initialize()
void Shutdown() {}
int FontId(types::Font font)
std::shared_ptr<boost::gil::rgba8_image_t>
LoadImageResource(const std::string& urlString)
{
auto it = fontIds_.find(font);
if (it != fontIds_.cend())
{
return it->second;
util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance();
return textureAtlas.CacheTexture(urlString, urlString);
}
return -1;
std::vector<std::shared_ptr<boost::gil::rgba8_image_t>>
LoadImageResources(const std::vector<std::string>& urlStrings)
{
std::mutex m {};
std::vector<std::shared_ptr<boost::gil::rgba8_image_t>> images {};
std::for_each(std::execution::par_unseq,
urlStrings.begin(),
urlStrings.end(),
[&](auto& urlString)
{
auto image = LoadImageResource(urlString);
if (image != nullptr)
{
std::unique_lock lock {m};
images.emplace_back(std::move(image));
}
});
if (!images.empty())
{
util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance();
textureAtlas.BuildAtlas(2048, 2048);
}
return images;
}
static void LoadFonts()
{
auto& fontManager = FontManager::Instance();
for (auto& fontName : fontNames_)
{
int fontId = QFontDatabase::addApplicationFont(
QString::fromStdString(fontName.second));
fontIds_.emplace(fontName.first, fontId);
util::Font::Create(fontName.second);
fontManager.LoadApplicationFont(fontName.first, fontName.second);
}
ImFontAtlas* fontAtlas = model::ImGuiContextModel::Instance().font_atlas();
fontAtlas->AddFontDefault();
fontManager.InitializeFonts();
}
static void LoadTextures()
@ -72,7 +95,7 @@ static void LoadTextures()
":/res/textures/lines/default-1x7.png");
textureAtlas.RegisterTexture("lines/test-pattern",
":/res/textures/lines/test-pattern.png");
textureAtlas.BuildAtlas(8, 8);
textureAtlas.BuildAtlas(2048, 2048);
}
} // namespace ResourceManager

View file

@ -2,6 +2,10 @@
#include <scwx/qt/types/font_types.hpp>
#include <vector>
#include <boost/gil/typedefs.hpp>
namespace scwx
{
namespace qt
@ -14,7 +18,10 @@ namespace ResourceManager
void Initialize();
void Shutdown();
int FontId(types::Font font);
std::shared_ptr<boost::gil::rgba8_image_t>
LoadImageResource(const std::string& urlString);
std::vector<std::shared_ptr<boost::gil::rgba8_image_t>>
LoadImageResources(const std::vector<std::string>& urlStrings);
} // namespace ResourceManager
} // namespace manager

View file

@ -1,5 +1,9 @@
#include <scwx/qt/manager/settings_manager.hpp>
#include <scwx/qt/map/map_provider.hpp>
#include <scwx/qt/settings/general_settings.hpp>
#include <scwx/qt/settings/map_settings.hpp>
#include <scwx/qt/settings/palette_settings.hpp>
#include <scwx/qt/settings/text_settings.hpp>
#include <scwx/qt/settings/ui_settings.hpp>
#include <scwx/qt/util/json.hpp>
#include <scwx/util/logger.hpp>
@ -17,21 +21,33 @@ namespace qt
{
namespace manager
{
namespace SettingsManager
{
static const std::string logPrefix_ = "scwx::qt::manager::settings_manager";
static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
class SettingsManager::Impl
{
public:
explicit Impl(SettingsManager* self) : self_ {self} {}
~Impl() = default;
void ValidateSettings();
static boost::json::value ConvertSettingsToJson();
static void GenerateDefaultSettings();
static bool LoadSettings(const boost::json::object& settingsJson);
static void ValidateSettings();
static bool initialized_ {false};
static std::string settingsPath_ {};
SettingsManager* self_;
void Initialize()
bool initialized_ {false};
std::string settingsPath_ {};
};
SettingsManager::SettingsManager() : p(std::make_unique<Impl>(this)) {}
SettingsManager::~SettingsManager() {};
void SettingsManager::Initialize()
{
std::string appDataPath {
QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)
@ -46,14 +62,14 @@ void Initialize()
}
}
settingsPath_ = appDataPath + "/settings.json";
initialized_ = true;
p->settingsPath_ = appDataPath + "/settings.json";
p->initialized_ = true;
ReadSettings(settingsPath_);
ValidateSettings();
ReadSettings(p->settingsPath_);
p->ValidateSettings();
}
void ReadSettings(const std::string& settingsPath)
void SettingsManager::ReadSettings(const std::string& settingsPath)
{
boost::json::value settingsJson = nullptr;
@ -64,39 +80,41 @@ void ReadSettings(const std::string& settingsPath)
if (settingsJson == nullptr || !settingsJson.is_object())
{
GenerateDefaultSettings();
settingsJson = ConvertSettingsToJson();
Impl::GenerateDefaultSettings();
settingsJson = Impl::ConvertSettingsToJson();
util::json::WriteJsonFile(settingsPath, settingsJson);
}
else
{
bool jsonDirty = LoadSettings(settingsJson.as_object());
bool jsonDirty = Impl::LoadSettings(settingsJson.as_object());
if (jsonDirty)
{
settingsJson = ConvertSettingsToJson();
settingsJson = Impl::ConvertSettingsToJson();
util::json::WriteJsonFile(settingsPath, settingsJson);
}
};
}
void SaveSettings()
void SettingsManager::SaveSettings()
{
if (initialized_)
if (p->initialized_)
{
logger_->info("Saving settings");
boost::json::value settingsJson = ConvertSettingsToJson();
util::json::WriteJsonFile(settingsPath_, settingsJson);
boost::json::value settingsJson = Impl::ConvertSettingsToJson();
util::json::WriteJsonFile(p->settingsPath_, settingsJson);
Q_EMIT SettingsSaved();
}
}
void Shutdown()
void SettingsManager::Shutdown()
{
bool dataChanged = false;
dataChanged |= general_settings().Shutdown();
dataChanged |= map_settings().Shutdown();
dataChanged |= settings::GeneralSettings::Instance().Shutdown();
dataChanged |= settings::MapSettings::Instance().Shutdown();
dataChanged |= settings::UiSettings::Instance().Shutdown();
if (dataChanged)
@ -105,67 +123,53 @@ void Shutdown()
}
}
settings::GeneralSettings& general_settings()
{
static settings::GeneralSettings generalSettings_;
return generalSettings_;
}
settings::MapSettings& map_settings()
{
static settings::MapSettings mapSettings_;
return mapSettings_;
}
settings::PaletteSettings& palette_settings()
{
static settings::PaletteSettings paletteSettings_;
return paletteSettings_;
}
static boost::json::value ConvertSettingsToJson()
boost::json::value SettingsManager::Impl::ConvertSettingsToJson()
{
boost::json::object settingsJson;
general_settings().WriteJson(settingsJson);
map_settings().WriteJson(settingsJson);
palette_settings().WriteJson(settingsJson);
settings::GeneralSettings::Instance().WriteJson(settingsJson);
settings::MapSettings::Instance().WriteJson(settingsJson);
settings::PaletteSettings::Instance().WriteJson(settingsJson);
settings::TextSettings::Instance().WriteJson(settingsJson);
settings::UiSettings::Instance().WriteJson(settingsJson);
return settingsJson;
}
static void GenerateDefaultSettings()
void SettingsManager::Impl::GenerateDefaultSettings()
{
logger_->info("Generating default settings");
general_settings().SetDefaults();
map_settings().SetDefaults();
palette_settings().SetDefaults();
settings::GeneralSettings::Instance().SetDefaults();
settings::MapSettings::Instance().SetDefaults();
settings::PaletteSettings::Instance().SetDefaults();
settings::TextSettings::Instance().SetDefaults();
settings::UiSettings::Instance().SetDefaults();
}
static bool LoadSettings(const boost::json::object& settingsJson)
bool SettingsManager::Impl::LoadSettings(
const boost::json::object& settingsJson)
{
logger_->info("Loading settings");
bool jsonDirty = false;
jsonDirty |= !general_settings().ReadJson(settingsJson);
jsonDirty |= !map_settings().ReadJson(settingsJson);
jsonDirty |= !palette_settings().ReadJson(settingsJson);
jsonDirty |= !settings::GeneralSettings::Instance().ReadJson(settingsJson);
jsonDirty |= !settings::MapSettings::Instance().ReadJson(settingsJson);
jsonDirty |= !settings::PaletteSettings::Instance().ReadJson(settingsJson);
jsonDirty |= !settings::TextSettings::Instance().ReadJson(settingsJson);
jsonDirty |= !settings::UiSettings::Instance().ReadJson(settingsJson);
return jsonDirty;
}
static void ValidateSettings()
void SettingsManager::Impl::ValidateSettings()
{
logger_->debug("Validating settings");
bool settingsChanged = false;
auto& generalSettings = general_settings();
auto& generalSettings = settings::GeneralSettings::Instance();
// Validate map provider
std::string mapProviderName = generalSettings.map_provider().GetValue();
@ -196,11 +200,16 @@ static void ValidateSettings()
if (settingsChanged)
{
SaveSettings();
self_->SaveSettings();
}
}
} // namespace SettingsManager
SettingsManager& SettingsManager::Instance()
{
static SettingsManager instance_ {};
return instance_;
}
} // namespace manager
} // namespace qt
} // namespace scwx

View file

@ -1,8 +1,9 @@
#pragma once
#include <scwx/qt/settings/general_settings.hpp>
#include <scwx/qt/settings/map_settings.hpp>
#include <scwx/qt/settings/palette_settings.hpp>
#include <string>
#include <memory>
#include <QObject>
namespace scwx
{
@ -10,19 +11,31 @@ namespace qt
{
namespace manager
{
namespace SettingsManager
class SettingsManager : public QObject
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(SettingsManager)
public:
explicit SettingsManager();
~SettingsManager();
void Initialize();
void ReadSettings(const std::string& settingsPath);
void SaveSettings();
void Shutdown();
settings::GeneralSettings& general_settings();
settings::MapSettings& map_settings();
settings::PaletteSettings& palette_settings();
static SettingsManager& Instance();
signals:
void SettingsSaved();
private:
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace SettingsManager
} // namespace manager
} // namespace qt
} // namespace scwx

View file

@ -2,7 +2,7 @@
#include <scwx/qt/manager/timeline_manager.hpp>
#include <scwx/qt/manager/radar_product_manager.hpp>
#include <scwx/qt/manager/settings_manager.hpp>
#include <scwx/qt/settings/general_settings.hpp>
#include <scwx/util/logger.hpp>
#include <scwx/util/map.hpp>
#include <scwx/util/time.hpp>
@ -39,7 +39,7 @@ class TimelineManager::Impl
public:
explicit Impl(TimelineManager* self) : self_ {self}
{
auto& generalSettings = SettingsManager::general_settings();
auto& generalSettings = settings::GeneralSettings::Instance();
loopDelay_ =
std::chrono::milliseconds(generalSettings.loop_delay().GetValue());
@ -281,7 +281,12 @@ void TimelineManager::Impl::RadarSweepMonitorReset()
void TimelineManager::Impl::RadarSweepMonitorWait(
std::unique_lock<std::mutex>& lock)
{
std::cv_status status =
radarSweepMonitorCondition_.wait_for(lock, kRadarSweepMonitorTimeout_);
if (status == std::cv_status::timeout)
{
logger_->debug("Radar sweep monitor timed out");
}
radarSweepMonitorActive_ = false;
}

View file

@ -1,6 +1,7 @@
#include <scwx/qt/map/alert_layer.hpp>
#include <scwx/qt/manager/settings_manager.hpp>
#include <scwx/qt/manager/text_event_manager.hpp>
#include <scwx/qt/settings/palette_settings.hpp>
#include <scwx/qt/types/layer_types.hpp>
#include <scwx/qt/util/color.hpp>
#include <scwx/util/logger.hpp>
#include <scwx/util/threads.hpp>
@ -22,7 +23,8 @@ namespace map
static const std::string logPrefix_ = "scwx::qt::map::alert_layer";
static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
static void AddAlertLayer(std::shared_ptr<QMapLibreGL::Map> map,
static std::vector<std::string>
AddAlertLayer(std::shared_ptr<QMapLibreGL::Map> map,
awips::Phenomenon phenomenon,
bool alertActive,
const QString& beforeLayer);
@ -132,56 +134,36 @@ public:
};
AlertLayer::AlertLayer(std::shared_ptr<MapContext> context) :
DrawLayer(context), p(std::make_unique<AlertLayerImpl>(context))
p(std::make_unique<AlertLayerImpl>(context))
{
}
AlertLayer::~AlertLayer() = default;
void AlertLayer::Initialize()
std::vector<std::string> AlertLayer::AddLayers(awips::Phenomenon phenomenon,
const std::string& before)
{
logger_->debug("Initialize()");
logger_->debug("AddLayers(): {}", awips::GetPhenomenonCode(phenomenon));
DrawLayer::Initialize();
}
void AlertLayer::Render(const QMapLibreGL::CustomLayerRenderParameters& params)
{
gl::OpenGLFunctions& gl = context()->gl();
DrawLayer::Render(params);
SCWX_GL_CHECK_ERROR();
}
void AlertLayer::Deinitialize()
{
logger_->debug("Deinitialize()");
DrawLayer::Deinitialize();
}
void AlertLayer::AddLayers(const std::string& before)
{
logger_->debug("AddLayers()");
std::vector<std::string> layers {};
auto map = p->context_->map().lock();
if (map == nullptr)
{
return;
return layers;
}
const QString beforeLayer {QString::fromStdString(before)};
// Add/update GeoJSON sources and create layers
for (auto& phenomenon : kAlertPhenomena_)
{
for (bool alertActive : {false, true})
{
p->UpdateSource(phenomenon, alertActive);
AddAlertLayer(map, phenomenon, alertActive, beforeLayer);
}
auto newLayers = AddAlertLayer(map, phenomenon, alertActive, beforeLayer);
layers.insert(layers.end(), newLayers.cbegin(), newLayers.cend());
}
return layers;
}
std::list<QMapLibreGL::Feature>*
@ -388,21 +370,25 @@ std::shared_ptr<AlertLayerHandler> AlertLayerHandler::Instance()
return alertLayerHandler;
}
static void AddAlertLayer(std::shared_ptr<QMapLibreGL::Map> map,
static std::vector<std::string>
AddAlertLayer(std::shared_ptr<QMapLibreGL::Map> map,
awips::Phenomenon phenomenon,
bool alertActive,
const QString& beforeLayer)
{
settings::PaletteSettings& paletteSettings =
manager::SettingsManager::palette_settings();
settings::PaletteSettings::Instance();
QString layerPrefix = QString::fromStdString(
types::GetLayerName(types::LayerType::Alert, phenomenon));
QString sourceId = GetSourceId(phenomenon, alertActive);
QString idSuffix = GetSuffix(phenomenon, alertActive);
auto outlineColor = util::color::ToRgba8PixelT(
paletteSettings.alert_color(phenomenon, alertActive).GetValue());
QString bgLayerId = QString("alertPolygonLayerBg-%1").arg(idSuffix);
QString fgLayerId = QString("alertPolygonLayerFg-%1").arg(idSuffix);
QString bgLayerId = QString("%1::bg-%2").arg(layerPrefix).arg(idSuffix);
QString fgLayerId = QString("%1::fg-%2").arg(layerPrefix).arg(idSuffix);
if (map->layerExists(bgLayerId))
{
@ -436,6 +422,8 @@ static void AddAlertLayer(std::shared_ptr<QMapLibreGL::Map> map,
.arg(outlineColor[3]));
map->setPaintProperty(fgLayerId, "line-opacity", QString("%1").arg(opacity));
map->setPaintProperty(fgLayerId, "line-width", "3");
return {bgLayerId.toStdString(), fgLayerId.toStdString()};
}
static QMapLibreGL::Feature

View file

@ -1,6 +1,11 @@
#pragma once
#include <scwx/qt/map/draw_layer.hpp>
#include <scwx/awips/phenomenon.hpp>
#include <scwx/qt/map/map_context.hpp>
#include <memory>
#include <string>
#include <vector>
namespace scwx
{
@ -11,17 +16,14 @@ namespace map
class AlertLayerImpl;
class AlertLayer : public DrawLayer
class AlertLayer
{
public:
explicit AlertLayer(std::shared_ptr<MapContext> context);
~AlertLayer();
void Initialize() override final;
void Render(const QMapLibreGL::CustomLayerRenderParameters&) override final;
void Deinitialize() override final;
void AddLayers(const std::string& before = {});
std::vector<std::string> AddLayers(awips::Phenomenon phenomenon,
const std::string& before = {});
private:
std::unique_ptr<AlertLayerImpl> p;

View file

@ -24,9 +24,11 @@ public:
std::shared_ptr<MapContext> context_;
std::vector<std::shared_ptr<gl::draw::DrawItem>> drawList_;
GLuint textureAtlas_;
std::uint64_t textureAtlasBuildCount_ {};
};
DrawLayer::DrawLayer(std::shared_ptr<MapContext> context) :
DrawLayer::DrawLayer(const std::shared_ptr<MapContext>& context) :
GenericLayer(context), p(std::make_unique<DrawLayerImpl>(context))
{
}
@ -45,14 +47,23 @@ void DrawLayer::Initialize()
void DrawLayer::Render(const QMapLibreGL::CustomLayerRenderParameters& params)
{
gl::OpenGLFunctions& gl = p->context_->gl();
p->textureAtlas_ = p->context_->GetTextureAtlas();
// Determine if the texture atlas changed since last render
std::uint64_t newTextureAtlasBuildCount =
p->context_->texture_buffer_count();
bool textureAtlasChanged =
newTextureAtlasBuildCount != p->textureAtlasBuildCount_;
gl.glActiveTexture(GL_TEXTURE0);
gl.glBindTexture(GL_TEXTURE_2D, p->textureAtlas_);
gl.glBindTexture(GL_TEXTURE_2D_ARRAY, p->textureAtlas_);
for (auto& item : p->drawList_)
{
item->Render(params);
item->Render(params, textureAtlasChanged);
}
p->textureAtlasBuildCount_ = newTextureAtlasBuildCount;
}
void DrawLayer::Deinitialize()
@ -65,7 +76,31 @@ void DrawLayer::Deinitialize()
}
}
void DrawLayer::AddDrawItem(std::shared_ptr<gl::draw::DrawItem> drawItem)
bool DrawLayer::RunMousePicking(
const QMapLibreGL::CustomLayerRenderParameters& params,
const QPointF& mouseLocalPos,
const QPointF& mouseGlobalPos,
const glm::vec2& mouseCoords)
{
bool itemPicked = false;
// For each draw item in the draw list in reverse
for (auto it = p->drawList_.rbegin(); it != p->drawList_.rend(); ++it)
{
// Run mouse picking on each draw item
if ((*it)->RunMousePicking(
params, mouseLocalPos, mouseGlobalPos, mouseCoords))
{
// If a draw item was picked, don't process additional items
itemPicked = true;
break;
}
}
return itemPicked;
}
void DrawLayer::AddDrawItem(const std::shared_ptr<gl::draw::DrawItem>& drawItem)
{
p->drawList_.push_back(drawItem);
}

View file

@ -15,15 +15,22 @@ class DrawLayerImpl;
class DrawLayer : public GenericLayer
{
public:
explicit DrawLayer(std::shared_ptr<MapContext> context);
explicit DrawLayer(const std::shared_ptr<MapContext>& context);
virtual ~DrawLayer();
virtual void Initialize();
virtual void Render(const QMapLibreGL::CustomLayerRenderParameters&);
virtual void Deinitialize();
virtual void Initialize() override;
virtual void
Render(const QMapLibreGL::CustomLayerRenderParameters&) override;
virtual void Deinitialize() override;
virtual bool
RunMousePicking(const QMapLibreGL::CustomLayerRenderParameters& params,
const QPointF& mouseLocalPos,
const QPointF& mouseGlobalPos,
const glm::vec2& mouseCoords) override;
protected:
void AddDrawItem(std::shared_ptr<gl::draw::DrawItem> drawItem);
void AddDrawItem(const std::shared_ptr<gl::draw::DrawItem>& drawItem);
private:
std::unique_ptr<DrawLayerImpl> p;

View file

@ -26,6 +26,16 @@ GenericLayer::GenericLayer(std::shared_ptr<MapContext> context) :
}
GenericLayer::~GenericLayer() = default;
bool GenericLayer::RunMousePicking(
const QMapLibreGL::CustomLayerRenderParameters& /* params */,
const QPointF& /* mouseLocalPos */,
const QPointF& /* mouseGlobalPos */,
const glm::vec2& /* mousePos */)
{
// By default, the layer has nothing to pick
return false;
}
std::shared_ptr<MapContext> GenericLayer::context() const
{
return p->context_;

View file

@ -6,6 +6,7 @@
#include <QObject>
#include <QMapLibreGL/QMapLibreGL>
#include <glm/gtc/type_ptr.hpp>
namespace scwx
{
@ -28,6 +29,22 @@ public:
virtual void Render(const QMapLibreGL::CustomLayerRenderParameters&) = 0;
virtual void Deinitialize() = 0;
/**
* @brief Run mouse picking on the layer.
*
* @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 a draw item was picked, otherwise false
*/
virtual bool
RunMousePicking(const QMapLibreGL::CustomLayerRenderParameters& params,
const QPointF& mouseLocalPos,
const QPointF& mouseGlobalPos,
const glm::vec2& mouseCoords);
protected:
std::shared_ptr<MapContext> context() const;

View file

@ -30,6 +30,7 @@ public:
common::RadarProductGroup radarProductGroup_;
std::string radarProduct_;
int16_t radarProductCode_;
QMapLibreGL::CustomLayerRenderParameters renderParameters_ {};
};
MapContext::MapContext(
@ -77,6 +78,11 @@ int16_t MapContext::radar_product_code() const
return p->radarProductCode_;
}
QMapLibreGL::CustomLayerRenderParameters MapContext::render_parameters() const
{
return p->renderParameters_;
}
void MapContext::set_map(std::shared_ptr<QMapLibreGL::Map> map)
{
p->map_ = map;
@ -109,6 +115,12 @@ void MapContext::set_radar_product_code(int16_t radarProductCode)
p->radarProductCode_ = radarProductCode;
}
void MapContext::set_render_parameters(
const QMapLibreGL::CustomLayerRenderParameters& params)
{
p->renderParameters_ = params;
}
} // namespace map
} // namespace qt
} // namespace scwx

View file

@ -33,6 +33,7 @@ public:
common::RadarProductGroup radar_product_group() const;
std::string radar_product() const;
int16_t radar_product_code() const;
QMapLibreGL::CustomLayerRenderParameters render_parameters() const;
void set_map(std::shared_ptr<QMapLibreGL::Map> map);
void set_pixel_ratio(float pixelRatio);
@ -41,6 +42,8 @@ public:
void set_radar_product_group(common::RadarProductGroup radarProductGroup);
void set_radar_product(const std::string& radarProduct);
void set_radar_product_code(int16_t radarProductCode);
void set_render_parameters(
const QMapLibreGL::CustomLayerRenderParameters& params);
private:
class Impl;

View file

@ -1,5 +1,5 @@
#include <scwx/qt/map/map_provider.hpp>
#include <scwx/qt/manager/settings_manager.hpp>
#include <scwx/qt/settings/general_settings.hpp>
#include <unordered_map>
@ -128,12 +128,10 @@ std::string GetMapProviderApiKey(MapProvider mapProvider)
switch (mapProvider)
{
case MapProvider::Mapbox:
return manager::SettingsManager::general_settings()
.mapbox_api_key()
.GetValue();
return settings::GeneralSettings::Instance().mapbox_api_key().GetValue();
case MapProvider::MapTiler:
return manager::SettingsManager::general_settings()
return settings::GeneralSettings::Instance()
.maptiler_api_key()
.GetValue();

View file

@ -1,21 +1,29 @@
#include <scwx/qt/map/map_widget.hpp>
#include <scwx/qt/gl/gl.hpp>
#include <scwx/qt/manager/font_manager.hpp>
#include <scwx/qt/manager/placefile_manager.hpp>
#include <scwx/qt/manager/radar_product_manager.hpp>
#include <scwx/qt/manager/settings_manager.hpp>
#include <scwx/qt/map/alert_layer.hpp>
#include <scwx/qt/map/color_table_layer.hpp>
#include <scwx/qt/map/layer_wrapper.hpp>
#include <scwx/qt/map/map_provider.hpp>
#include <scwx/qt/map/overlay_layer.hpp>
#include <scwx/qt/map/placefile_layer.hpp>
#include <scwx/qt/map/radar_product_layer.hpp>
#include <scwx/qt/map/radar_range_layer.hpp>
#include <scwx/qt/model/imgui_context_model.hpp>
#include <scwx/qt/model/layer_model.hpp>
#include <scwx/qt/settings/general_settings.hpp>
#include <scwx/qt/settings/palette_settings.hpp>
#include <scwx/qt/util/file.hpp>
#include <scwx/qt/util/maplibre.hpp>
#include <scwx/qt/util/tooltip.hpp>
#include <scwx/qt/view/radar_product_view_factory.hpp>
#include <scwx/util/logger.hpp>
#include <scwx/util/time.hpp>
#include <regex>
#include <set>
#include <backends/imgui_impl_opengl3.h>
#include <backends/imgui_impl_qt.hpp>
@ -49,7 +57,9 @@ class MapWidgetImpl : public QObject
public:
explicit MapWidgetImpl(MapWidget* widget,
std::size_t id,
const QMapLibreGL::Settings& settings) :
id_ {id},
uuid_ {boost::uuids::random_generator()()},
context_ {std::make_shared<MapContext>()},
widget_ {widget},
@ -61,11 +71,11 @@ public:
radarProductLayer_ {nullptr},
alertLayer_ {std::make_shared<AlertLayer>(context_)},
overlayLayer_ {nullptr},
placefileLayer_ {nullptr},
colorTableLayer_ {nullptr},
autoRefreshEnabled_ {true},
autoUpdateEnabled_ {true},
selectedLevel2Product_ {common::Level2Product::Unknown},
lastPos_(),
currentStyleIndex_ {0},
currentStyle_ {nullptr},
frameDraws_(0),
@ -75,8 +85,7 @@ public:
prevBearing_ {0.0},
prevPitch_ {0.0}
{
auto& generalSettings =
scwx::qt::manager::SettingsManager::general_settings();
auto& generalSettings = settings::GeneralSettings::Instance();
SetRadarSite(generalSettings.default_radar_site().GetValue());
@ -92,6 +101,8 @@ public:
// Set Map Provider Details
mapProvider_ = GetMapProvider(generalSettings.map_provider().GetValue());
ConnectSignals();
}
~MapWidgetImpl()
@ -112,22 +123,37 @@ public:
threadPool_.join();
}
void AddLayer(types::LayerType type,
types::LayerDescription description,
const std::string& before = {});
void AddLayer(const std::string& id,
std::shared_ptr<GenericLayer> layer,
const std::string& before = {});
void AddLayers();
void AddPlacefileLayer(const std::string& placefileName,
const std::string& before);
void ConnectSignals();
void ImGuiCheckFonts();
void InitializeNewRadarProductView(const std::string& colorPalette);
void RadarProductManagerConnect();
void RadarProductManagerDisconnect();
void RadarProductViewConnect();
void RadarProductViewDisconnect();
void RunMousePicking();
void SetRadarSite(const std::string& radarSite);
void UpdateLoadedStyle();
bool UpdateStoredMapParameters();
std::string FindMapSymbologyLayer();
common::Level2Product
GetLevel2ProductOrDefault(const std::string& productName) const;
static std::string GetPlacefileLayerName(const std::string& placefileName);
boost::asio::thread_pool threadPool_ {1u};
std::size_t id_;
boost::uuids::uuid uuid_;
std::shared_ptr<MapContext> context_;
@ -138,10 +164,19 @@ public:
std::shared_ptr<QMapLibreGL::Map> map_;
std::list<std::string> layerList_;
QStringList styleLayers_;
types::LayerVector customLayers_;
ImGuiContext* imGuiContext_;
std::string imGuiContextName_;
bool imGuiRendererInitialized_;
std::uint64_t imGuiFontsBuildCount_ {};
std::shared_ptr<model::LayerModel> layerModel_ {
model::LayerModel::Instance()};
std::shared_ptr<manager::PlacefileManager> placefileManager_ {
manager::PlacefileManager::Instance()};
std::shared_ptr<manager::RadarProductManager> radarProductManager_;
std::shared_ptr<common::ColorTable> colorTable_;
@ -149,14 +184,20 @@ public:
std::shared_ptr<RadarProductLayer> radarProductLayer_;
std::shared_ptr<AlertLayer> alertLayer_;
std::shared_ptr<OverlayLayer> overlayLayer_;
std::shared_ptr<PlacefileLayer> placefileLayer_;
std::shared_ptr<ColorTableLayer> colorTableLayer_;
std::list<std::shared_ptr<PlacefileLayer>> placefileLayers_ {};
bool autoRefreshEnabled_;
bool autoUpdateEnabled_;
common::Level2Product selectedLevel2Product_;
QPointF lastPos_;
bool hasMouse_ {false};
bool lastItemPicked_ {false};
QPointF lastPos_ {};
QPointF lastGlobalPos_ {};
std::size_t currentStyleIndex_;
const MapStyle* currentStyle_;
std::string initialStyleName_ {};
@ -173,9 +214,16 @@ public slots:
void Update();
};
MapWidget::MapWidget(const QMapLibreGL::Settings& settings) :
p(std::make_unique<MapWidgetImpl>(this, settings))
MapWidget::MapWidget(std::size_t id, const QMapLibreGL::Settings& settings) :
p(std::make_unique<MapWidgetImpl>(this, id, settings))
{
if (settings::GeneralSettings::Instance().anti_aliasing_enabled().GetValue())
{
QSurfaceFormat surfaceFormat = QSurfaceFormat::defaultFormat();
surfaceFormat.setSamples(4);
setFormat(surfaceFormat);
}
setFocusPolicy(Qt::StrongFocus);
ImGui_ImplQt_RegisterWidget(this);
@ -187,6 +235,63 @@ MapWidget::~MapWidget()
makeCurrent();
}
void MapWidgetImpl::ConnectSignals()
{
connect(placefileManager_.get(),
&manager::PlacefileManager::PlacefileUpdated,
widget_,
[this]() { widget_->update(); });
// When the layer model changes, update the layers
connect(layerModel_.get(),
&QAbstractItemModel::dataChanged,
widget_,
[this](const QModelIndex& topLeft,
const QModelIndex& bottomRight,
const QList<int>& /* roles */)
{
static const int enabledColumn =
static_cast<int>(model::LayerModel::Column::Enabled);
const int displayColumn =
static_cast<int>(model::LayerModel::Column::DisplayMap1) +
static_cast<int>(id_);
// Update layers if the displayed or enabled state of the layer
// has changed
if ((topLeft.column() <= displayColumn &&
displayColumn <= bottomRight.column()) ||
(topLeft.column() <= enabledColumn &&
enabledColumn <= bottomRight.column()))
{
AddLayers();
}
});
connect(layerModel_.get(),
&QAbstractItemModel::modelReset,
widget_,
[this]() { AddLayers(); });
connect(layerModel_.get(),
&QAbstractItemModel::rowsInserted,
widget_,
[this](const QModelIndex& /* parent */, //
int /* first */,
int /* last */) { AddLayers(); });
connect(layerModel_.get(),
&QAbstractItemModel::rowsMoved,
widget_,
[this](const QModelIndex& /* sourceParent */,
int /* sourceStart */,
int /* sourceEnd */,
const QModelIndex& /* destinationParent */,
int /* destinationRow */) { AddLayers(); });
connect(layerModel_.get(),
&QAbstractItemModel::rowsRemoved,
widget_,
[this](const QModelIndex& /* parent */, //
int /* first */,
int /* last */) { AddLayers(); });
}
common::Level3ProductCategoryMap MapWidget::GetAvailableLevel3Categories()
{
if (p->radarProductManager_ != nullptr)
@ -498,7 +603,7 @@ void MapWidget::SelectRadarSite(std::shared_ptr<config::RadarSite> radarSite,
false);
}
AddLayers();
p->AddLayers();
// TODO: Disable refresh from old site
@ -646,62 +751,193 @@ void MapWidget::changeStyle()
Q_EMIT MapStyleChanged(p->currentStyle_->name_);
}
void MapWidget::AddLayers()
void MapWidget::DumpLayerList() const
{
logger_->debug("AddLayers()");
// Clear custom layers
for (const std::string& id : p->layerList_)
{
p->map_->removeLayer(id.c_str());
logger_->info("Layers: {}", p->map_->layerIds().join(", ").toStdString());
}
p->layerList_.clear();
auto radarProductView = p->context_->radar_product_view();
if (radarProductView != nullptr)
std::string MapWidgetImpl::FindMapSymbologyLayer()
{
p->radarProductLayer_ = std::make_shared<RadarProductLayer>(p->context_);
p->colorTableLayer_ = std::make_shared<ColorTableLayer>(p->context_);
std::shared_ptr<config::RadarSite> radarSite =
p->radarProductManager_->radar_site();
const auto& mapStyle = *p->currentStyle_;
std::string before = "ferry";
for (const QString& qlayer : p->map_->layerIds())
for (const QString& qlayer : styleLayers_)
{
const std::string layer = qlayer.toStdString();
// Draw below layers defined in map style
auto it = std::find_if(
mapStyle.drawBelow_.cbegin(),
mapStyle.drawBelow_.cend(),
currentStyle_->drawBelow_.cbegin(),
currentStyle_->drawBelow_.cend(),
[&layer](const std::string& styleLayer) -> bool
{
std::regex re {styleLayer, std::regex_constants::icase};
return std::regex_match(layer, re);
});
if (it != mapStyle.drawBelow_.cend())
if (it != currentStyle_->drawBelow_.cend())
{
before = layer;
break;
}
}
p->AddLayer("radar", p->radarProductLayer_, before);
RadarRangeLayer::Add(p->map_,
radarProductView->range(),
{radarSite->latitude(), radarSite->longitude()});
p->AddLayer("colorTable", p->colorTableLayer_);
return before;
}
p->alertLayer_->AddLayers("colorTable");
p->overlayLayer_ = std::make_shared<OverlayLayer>(p->context_);
p->AddLayer("overlay", p->overlayLayer_);
void MapWidgetImpl::AddLayers()
{
if (styleLayers_.isEmpty())
{
// Skip if the map has not yet been initialized
return;
}
logger_->debug("Add Layers");
// Clear custom layers
for (const std::string& id : layerList_)
{
map_->removeLayer(id.c_str());
}
layerList_.clear();
placefileLayers_.clear();
// Update custom layer list from model
customLayers_ = model::LayerModel::Instance()->GetLayers();
// Start by drawing layers before any style-defined layers
std::string before = styleLayers_.front().toStdString();
// Loop through each custom layer in reverse order
for (auto it = customLayers_.crbegin(); it != customLayers_.crend(); ++it)
{
if (it->type_ == types::LayerType::Map)
{
// Style-defined map layers
switch (std::get<types::MapLayer>(it->description_))
{
// Subsequent layers are drawn underneath the map symbology layer
case types::MapLayer::MapUnderlay:
before = FindMapSymbologyLayer();
break;
// Subsequent layers are drawn after all style-defined layers
case types::MapLayer::MapSymbology:
before = "";
break;
default:
break;
}
}
else if (it->displayed_[id_])
{
// If the layer is displayed for the current map, add it
AddLayer(it->type_, it->description_, before);
}
}
}
void MapWidgetImpl::AddLayer(types::LayerType type,
types::LayerDescription description,
const std::string& before)
{
std::string layerName = types::GetLayerName(type, description);
auto radarProductView = context_->radar_product_view();
if (type == types::LayerType::Radar)
{
// If there is a radar product view, create the radar product layer
if (radarProductView != nullptr)
{
radarProductLayer_ = std::make_shared<RadarProductLayer>(context_);
AddLayer(layerName, radarProductLayer_, before);
}
}
else if (type == types::LayerType::Alert)
{
// Add the alert layer for the phenomenon
auto newLayers = alertLayer_->AddLayers(
std::get<awips::Phenomenon>(description), before);
layerList_.insert(layerList_.end(), newLayers.cbegin(), newLayers.cend());
}
else if (type == types::LayerType::Placefile)
{
// If the placefile is enabled, add the placefile layer
std::string placefileName = std::get<std::string>(description);
if (placefileManager_->placefile_enabled(placefileName))
{
AddPlacefileLayer(placefileName, before);
}
}
else if (type == types::LayerType::Information)
{
switch (std::get<types::InformationLayer>(description))
{
// Create the map overlay layer
case types::InformationLayer::MapOverlay:
overlayLayer_ = std::make_shared<OverlayLayer>(context_);
AddLayer(layerName, overlayLayer_, before);
break;
// If there is a radar product view, create the color table layer
case types::InformationLayer::ColorTable:
if (radarProductView != nullptr)
{
colorTableLayer_ = std::make_shared<ColorTableLayer>(context_);
AddLayer(layerName, colorTableLayer_, before);
}
break;
default:
break;
}
}
else if (type == types::LayerType::Data)
{
switch (std::get<types::DataLayer>(description))
{
// If there is a radar product view, create the radar range layer
case types::DataLayer::RadarRange:
if (radarProductView != nullptr)
{
std::shared_ptr<config::RadarSite> radarSite =
radarProductManager_->radar_site();
RadarRangeLayer::Add(
map_,
radarProductView->range(),
{radarSite->latitude(), radarSite->longitude()},
QString::fromStdString(before));
layerList_.push_back(types::GetLayerName(type, description));
}
break;
default:
break;
}
}
}
void MapWidgetImpl::AddPlacefileLayer(const std::string& placefileName,
const std::string& before)
{
std::shared_ptr<PlacefileLayer> placefileLayer =
std::make_shared<PlacefileLayer>(context_, placefileName);
placefileLayers_.push_back(placefileLayer);
AddLayer(GetPlacefileLayerName(placefileName), placefileLayer, before);
// When the layer updates, trigger a map widget update
connect(placefileLayer.get(),
&PlacefileLayer::DataReloaded,
widget_,
[this]() { widget_->update(); });
}
std::string
MapWidgetImpl::GetPlacefileLayerName(const std::string& placefileName)
{
return types::GetLayerName(types::LayerType::Placefile, placefileName);
}
void MapWidgetImpl::AddLayer(const std::string& id,
@ -712,10 +948,27 @@ void MapWidgetImpl::AddLayer(const std::string& id,
std::unique_ptr<QMapLibreGL::CustomLayerHostInterface> pHost =
std::make_unique<LayerWrapper>(layer);
try
{
map_->addCustomLayer(id.c_str(), std::move(pHost), before.c_str());
layerList_.push_back(id);
}
catch (const std::exception&)
{
// When dragging and dropping, a temporary duplicate layer exists
}
}
void MapWidget::enterEvent(QEnterEvent* /* ev */)
{
p->hasMouse_ = true;
}
void MapWidget::leaveEvent(QEvent* /* ev */)
{
p->hasMouse_ = false;
}
void MapWidget::keyPressEvent(QKeyEvent* ev)
{
@ -742,6 +995,7 @@ void MapWidget::keyPressEvent(QKeyEvent* ev)
void MapWidget::mousePressEvent(QMouseEvent* ev)
{
p->lastPos_ = ev->position();
p->lastGlobalPos_ = ev->globalPosition();
if (ev->type() == QEvent::MouseButtonPress)
{
@ -788,6 +1042,7 @@ void MapWidget::mouseMoveEvent(QMouseEvent* ev)
}
p->lastPos_ = ev->position();
p->lastGlobalPos_ = ev->globalPosition();
ev->accept();
}
@ -816,9 +1071,15 @@ void MapWidget::initializeGL()
makeCurrent();
p->context_->gl().initializeOpenGLFunctions();
// Lock ImGui font atlas prior to new ImGui frame
std::shared_lock imguiFontAtlasLock {
manager::FontManager::Instance().imgui_font_atlas_mutex()};
// Initialize ImGui OpenGL3 backend
ImGui::SetCurrentContext(p->imGuiContext_);
ImGui_ImplOpenGL3_Init();
p->imGuiFontsBuildCount_ =
manager::FontManager::Instance().imgui_fonts_build_count();
p->imGuiRendererInitialized_ = true;
p->map_.reset(
@ -859,16 +1120,27 @@ void MapWidget::initializeGL()
void MapWidget::paintGL()
{
auto defaultFont = manager::FontManager::Instance().GetImGuiFont(
types::FontCategory::Default);
p->frameDraws_++;
// Setup ImGui Frame
ImGui::SetCurrentContext(p->imGuiContext_);
// Lock ImGui font atlas prior to new ImGui frame
std::shared_lock imguiFontAtlasLock {
manager::FontManager::Instance().imgui_font_atlas_mutex()};
// Start ImGui Frame
ImGui_ImplQt_NewFrame(this);
ImGui_ImplOpenGL3_NewFrame();
p->ImGuiCheckFonts();
ImGui::NewFrame();
// Set default font
ImGui::PushFont(defaultFont->font());
// Update pixel ratio
p->context_->set_pixel_ratio(pixelRatio());
@ -878,20 +1150,90 @@ void MapWidget::paintGL()
size() * pixelRatio());
p->map_->render();
// Perform mouse picking
if (p->hasMouse_)
{
p->RunMousePicking();
}
else if (p->lastItemPicked_)
{
// Hide the tooltip when losing focus
util::tooltip::Hide();
p->lastItemPicked_ = false;
}
// Pop default font
ImGui::PopFont();
// Render ImGui Frame
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
// Unlock ImGui font atlas after rendering
imguiFontAtlasLock.unlock();
// Paint complete
Q_EMIT WidgetPainted();
}
void MapWidgetImpl::ImGuiCheckFonts()
{
// Update ImGui Fonts if required
std::uint64_t currentImGuiFontsBuildCount =
manager::FontManager::Instance().imgui_fonts_build_count();
if (imGuiFontsBuildCount_ != currentImGuiFontsBuildCount ||
!model::ImGuiContextModel::Instance().font_atlas()->IsBuilt())
{
ImGui_ImplOpenGL3_DestroyFontsTexture();
ImGui_ImplOpenGL3_CreateFontsTexture();
}
imGuiFontsBuildCount_ = currentImGuiFontsBuildCount;
}
void MapWidgetImpl::RunMousePicking()
{
const QMapLibreGL::CustomLayerRenderParameters params =
context_->render_parameters();
auto coordinate = map_->coordinateForPixel(lastPos_);
auto mouseScreenCoordinate =
util::maplibre::LatLongToScreenCoordinate(coordinate);
// For each layer in reverse
// TODO: All Generic Layers, not just Placefile Layers
bool itemPicked = false;
for (auto it = placefileLayers_.rbegin(); it != placefileLayers_.rend();
++it)
{
// Run mouse picking for each layer
if ((*it)->RunMousePicking(
params, lastPos_, lastGlobalPos_, mouseScreenCoordinate))
{
// If a draw item was picked, don't process additional layers
itemPicked = true;
break;
}
}
// If no draw item was picked, hide the tooltip
if (!itemPicked)
{
util::tooltip::Hide();
}
lastItemPicked_ = itemPicked;
}
void MapWidget::mapChanged(QMapLibreGL::Map::MapChange mapChange)
{
switch (mapChange)
{
case QMapLibreGL::Map::MapChangeDidFinishLoadingStyle:
AddLayers();
p->UpdateLoadedStyle();
p->AddLayers();
break;
default:
@ -899,6 +1241,11 @@ void MapWidget::mapChanged(QMapLibreGL::Map::MapChange mapChange)
}
}
void MapWidgetImpl::UpdateLoadedStyle()
{
styleLayers_ = map_->layerIds();
}
void MapWidgetImpl::RadarProductManagerConnect()
{
if (radarProductManager_ != nullptr)
@ -992,7 +1339,7 @@ void MapWidgetImpl::InitializeNewRadarProductView(
auto radarProductView = context_->radar_product_view();
std::string colorTableFile =
manager::SettingsManager::palette_settings()
settings::PaletteSettings::Instance()
.palette(colorPalette)
.GetValue();
if (!colorTableFile.empty())
@ -1009,7 +1356,7 @@ void MapWidgetImpl::InitializeNewRadarProductView(
if (map_ != nullptr)
{
widget_->AddLayers();
AddLayers();
}
}

View file

@ -32,9 +32,11 @@ class MapWidget : public QOpenGLWidget
Q_OBJECT
public:
explicit MapWidget(const QMapLibreGL::Settings&);
explicit MapWidget(std::size_t id, const QMapLibreGL::Settings&);
~MapWidget();
void DumpLayerList() const;
common::Level3ProductCategoryMap GetAvailableLevel3Categories();
float GetElevation() const;
std::vector<float> GetElevationCuts() const;
@ -119,7 +121,9 @@ private:
qreal pixelRatio();
// QWidget implementation.
void enterEvent(QEnterEvent* ev) override final;
void keyPressEvent(QKeyEvent* ev) override final;
void leaveEvent(QEvent* ev) override final;
void mousePressEvent(QMouseEvent* ev) override final;
void mouseMoveEvent(QMouseEvent* ev) override final;
void wheelEvent(QWheelEvent* ev) override final;
@ -128,8 +132,6 @@ private:
void initializeGL() override final;
void paintGL() override final;
void AddLayers();
std::unique_ptr<MapWidgetImpl> p;
friend class MapWidgetImpl;

View file

@ -95,6 +95,8 @@ void OverlayLayer::Render(
auto& settings = context()->settings();
const float pixelRatio = context()->pixel_ratio();
context()->set_render_parameters(params);
if (p->sweepTimeNeedsUpdate_ && radarProductView != nullptr)
{
const scwx::util::time_zone* currentZone;

View file

@ -0,0 +1,262 @@
#include <scwx/qt/map/placefile_layer.hpp>
#include <scwx/qt/gl/draw/placefile_icons.hpp>
#include <scwx/qt/gl/draw/placefile_images.hpp>
#include <scwx/qt/gl/draw/placefile_lines.hpp>
#include <scwx/qt/gl/draw/placefile_polygons.hpp>
#include <scwx/qt/gl/draw/placefile_triangles.hpp>
#include <scwx/qt/gl/draw/placefile_text.hpp>
#include <scwx/qt/manager/placefile_manager.hpp>
#include <scwx/qt/manager/timeline_manager.hpp>
#include <scwx/util/logger.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/thread_pool.hpp>
namespace scwx
{
namespace qt
{
namespace map
{
static const std::string logPrefix_ = "scwx::qt::map::placefile_layer";
static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
class PlacefileLayer::Impl
{
public:
explicit Impl(PlacefileLayer* self,
const std::shared_ptr<MapContext>& context,
const std::string& placefileName) :
self_ {self},
placefileName_ {placefileName},
placefileIcons_ {std::make_shared<gl::draw::PlacefileIcons>(context)},
placefileImages_ {std::make_shared<gl::draw::PlacefileImages>(context)},
placefileLines_ {std::make_shared<gl::draw::PlacefileLines>(context)},
placefilePolygons_ {
std::make_shared<gl::draw::PlacefilePolygons>(context)},
placefileTriangles_ {
std::make_shared<gl::draw::PlacefileTriangles>(context)},
placefileText_ {
std::make_shared<gl::draw::PlacefileText>(context, placefileName)}
{
ConnectSignals();
}
~Impl() { threadPool_.join(); }
void ConnectSignals();
boost::asio::thread_pool threadPool_ {1};
PlacefileLayer* self_;
std::string placefileName_;
std::mutex dataMutex_ {};
std::shared_ptr<gl::draw::PlacefileIcons> placefileIcons_;
std::shared_ptr<gl::draw::PlacefileImages> placefileImages_;
std::shared_ptr<gl::draw::PlacefileLines> placefileLines_;
std::shared_ptr<gl::draw::PlacefilePolygons> placefilePolygons_;
std::shared_ptr<gl::draw::PlacefileTriangles> placefileTriangles_;
std::shared_ptr<gl::draw::PlacefileText> placefileText_;
std::chrono::system_clock::time_point selectedTime_ {};
};
PlacefileLayer::PlacefileLayer(const std::shared_ptr<MapContext>& context,
const std::string& placefileName) :
DrawLayer(context),
p(std::make_unique<PlacefileLayer::Impl>(this, context, placefileName))
{
AddDrawItem(p->placefileImages_);
AddDrawItem(p->placefilePolygons_);
AddDrawItem(p->placefileTriangles_);
AddDrawItem(p->placefileLines_);
AddDrawItem(p->placefileIcons_);
AddDrawItem(p->placefileText_);
ReloadData();
}
PlacefileLayer::~PlacefileLayer() = default;
void PlacefileLayer::Impl::ConnectSignals()
{
auto placefileManager = manager::PlacefileManager::Instance();
auto timelineManager = manager::TimelineManager::Instance();
QObject::connect(placefileManager.get(),
&manager::PlacefileManager::PlacefileUpdated,
self_,
[this](const std::string& name)
{
if (name == placefileName_)
{
self_->ReloadData();
}
});
QObject::connect(timelineManager.get(),
&manager::TimelineManager::SelectedTimeUpdated,
self_,
[this](std::chrono::system_clock::time_point dateTime)
{ selectedTime_ = dateTime; });
}
std::string PlacefileLayer::placefile_name() const
{
return p->placefileName_;
}
void PlacefileLayer::set_placefile_name(const std::string& placefileName)
{
p->placefileName_ = placefileName;
p->placefileText_->set_placefile_name(placefileName);
ReloadData();
}
void PlacefileLayer::Initialize()
{
logger_->debug("Initialize()");
DrawLayer::Initialize();
}
void PlacefileLayer::Render(
const QMapLibreGL::CustomLayerRenderParameters& params)
{
gl::OpenGLFunctions& gl = context()->gl();
// Set OpenGL blend mode for transparency
gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
std::shared_ptr<manager::PlacefileManager> placefileManager =
manager::PlacefileManager::Instance();
auto placefile = placefileManager->placefile(p->placefileName_);
// Render placefile
if (placefile != nullptr)
{
bool thresholded =
placefileManager->placefile_thresholded(placefile->name());
p->placefileIcons_->set_thresholded(thresholded);
p->placefileImages_->set_thresholded(thresholded);
p->placefileLines_->set_thresholded(thresholded);
p->placefilePolygons_->set_thresholded(thresholded);
p->placefileTriangles_->set_thresholded(thresholded);
p->placefileText_->set_thresholded(thresholded);
p->placefileIcons_->set_selected_time(p->selectedTime_);
p->placefileImages_->set_selected_time(p->selectedTime_);
p->placefileLines_->set_selected_time(p->selectedTime_);
p->placefilePolygons_->set_selected_time(p->selectedTime_);
p->placefileTriangles_->set_selected_time(p->selectedTime_);
p->placefileText_->set_selected_time(p->selectedTime_);
}
DrawLayer::Render(params);
SCWX_GL_CHECK_ERROR();
}
void PlacefileLayer::Deinitialize()
{
logger_->debug("Deinitialize()");
DrawLayer::Deinitialize();
}
void PlacefileLayer::ReloadData()
{
boost::asio::post(
p->threadPool_,
[this]()
{
logger_->debug("ReloadData: {}", p->placefileName_);
std::unique_lock lock {p->dataMutex_};
std::shared_ptr<manager::PlacefileManager> placefileManager =
manager::PlacefileManager::Instance();
auto placefile = placefileManager->placefile(p->placefileName_);
if (placefile == nullptr)
{
return;
}
// Start draw items
p->placefileIcons_->StartIcons();
p->placefileImages_->StartImages(placefile->name());
p->placefileLines_->StartLines();
p->placefilePolygons_->StartPolygons();
p->placefileTriangles_->StartTriangles();
p->placefileText_->StartText();
p->placefileIcons_->SetIconFiles(placefile->icon_files(),
placefile->name());
p->placefileText_->SetFonts(
placefileManager->placefile_fonts(p->placefileName_));
for (auto& drawItem : placefile->GetDrawItems())
{
switch (drawItem->itemType_)
{
case gr::Placefile::ItemType::Text:
p->placefileText_->AddText(
std::static_pointer_cast<gr::Placefile::TextDrawItem>(
drawItem));
break;
case gr::Placefile::ItemType::Icon:
p->placefileIcons_->AddIcon(
std::static_pointer_cast<gr::Placefile::IconDrawItem>(
drawItem));
break;
case gr::Placefile::ItemType::Line:
p->placefileLines_->AddLine(
std::static_pointer_cast<gr::Placefile::LineDrawItem>(
drawItem));
break;
case gr::Placefile::ItemType::Polygon:
p->placefilePolygons_->AddPolygon(
std::static_pointer_cast<gr::Placefile::PolygonDrawItem>(
drawItem));
break;
case gr::Placefile::ItemType::Image:
p->placefileImages_->AddImage(
std::static_pointer_cast<gr::Placefile::ImageDrawItem>(
drawItem));
break;
case gr::Placefile::ItemType::Triangles:
p->placefileTriangles_->AddTriangles(
std::static_pointer_cast<gr::Placefile::TrianglesDrawItem>(
drawItem));
break;
default:
break;
}
}
// Finish draw items
p->placefileIcons_->FinishIcons();
p->placefileImages_->FinishImages();
p->placefileLines_->FinishLines();
p->placefilePolygons_->FinishPolygons();
p->placefileTriangles_->FinishTriangles();
p->placefileText_->FinishText();
Q_EMIT DataReloaded();
});
}
} // namespace map
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,43 @@
#pragma once
#include <scwx/qt/map/draw_layer.hpp>
#include <string>
namespace scwx
{
namespace qt
{
namespace map
{
class PlacefileLayer : public DrawLayer
{
Q_OBJECT
public:
explicit PlacefileLayer(const std::shared_ptr<MapContext>& context,
const std::string& placefileName);
~PlacefileLayer();
std::string placefile_name() const;
void set_placefile_name(const std::string& placefileName);
void Initialize() override final;
void Render(const QMapLibreGL::CustomLayerRenderParameters&) override final;
void Deinitialize() override final;
void ReloadData();
signals:
void DataReloaded();
private:
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace map
} // namespace qt
} // namespace scwx

View file

@ -1,5 +1,6 @@
#include <scwx/qt/map/radar_product_layer.hpp>
#include <scwx/qt/gl/shader_program.hpp>
#include <scwx/qt/util/maplibre.hpp>
#include <scwx/util/logger.hpp>
#include <execution>
@ -31,9 +32,6 @@ static constexpr uint32_t MAX_DATA_MOMENT_GATES = 1840;
static const std::string logPrefix_ = "scwx::qt::map::radar_product_layer";
static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
static glm::vec2
LatLongToScreenCoordinate(const QMapLibreGL::Coordinate& coordinate);
class RadarProductLayerImpl
{
public:
@ -290,7 +288,7 @@ void RadarProductLayer::Render(
gl.glUniform2fv(p->uMapScreenCoordLocation_,
1,
glm::value_ptr(LatLongToScreenCoordinate(
glm::value_ptr(util::maplibre::LatLongToScreenCoordinate(
{params.latitude, params.longitude})));
gl.glUniformMatrix4fv(
@ -358,22 +356,6 @@ void RadarProductLayer::UpdateColorTable()
gl.glUniform1f(p->uDataMomentScaleLocation_, scale);
}
static glm::vec2
LatLongToScreenCoordinate(const QMapLibreGL::Coordinate& coordinate)
{
static constexpr double RAD2DEG_D = 180.0 / M_PI;
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;
}
} // namespace map
} // namespace qt
} // namespace scwx

View file

@ -1,4 +1,5 @@
#include <scwx/qt/map/radar_range_layer.hpp>
#include <scwx/qt/types/layer_types.hpp>
#include <scwx/qt/util/geographic_lib.hpp>
#include <scwx/util/logger.hpp>
@ -22,11 +23,14 @@ void RadarRangeLayer::Add(std::shared_ptr<QMapLibreGL::Map> map,
QMapLibreGL::Coordinate center,
const QString& before)
{
static const QString layerId = QString::fromStdString(types::GetLayerName(
types::LayerType::Data, types::DataLayer::RadarRange));
logger_->debug("Add()");
if (map->layerExists("rangeCircleLayer"))
if (map->layerExists(layerId))
{
map->removeLayer("rangeCircleLayer");
map->removeLayer(layerId);
}
if (map->sourceExists("rangeCircleSource"))
{
@ -39,12 +43,10 @@ void RadarRangeLayer::Add(std::shared_ptr<QMapLibreGL::Map> map,
map->addSource(
"rangeCircleSource",
{{"type", "geojson"}, {"data", QVariant::fromValue(*rangeCircle)}});
map->addLayer({{"id", "rangeCircleLayer"},
{"type", "line"},
{"source", "rangeCircleSource"}},
map->addLayer(
{{"id", layerId}, {"type", "line"}, {"source", "rangeCircleSource"}},
before);
map->setPaintProperty(
"rangeCircleLayer", "line-color", "rgba(128, 128, 128, 128)");
map->setPaintProperty(layerId, "line-color", "rgba(128, 128, 128, 128)");
}
void RadarRangeLayer::Update(std::shared_ptr<QMapLibreGL::Map> map,

View file

@ -1,3 +1,5 @@
#define NOMINMAX
#include <scwx/qt/model/alert_model.hpp>
#include <scwx/qt/config/county_database.hpp>
#include <scwx/qt/manager/text_event_manager.hpp>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,87 @@
#pragma once
#include <scwx/qt/types/layer_types.hpp>
#include <scwx/util/iterator.hpp>
#include <memory>
#include <QAbstractTableModel>
namespace scwx
{
namespace qt
{
namespace model
{
class LayerModel : public QAbstractTableModel
{
Q_DISABLE_COPY_MOVE(LayerModel)
public:
enum class Column : int
{
Order = 0,
DisplayMap1 = 1,
DisplayMap2 = 2,
DisplayMap3 = 3,
DisplayMap4 = 4,
Type = 5,
Enabled = 6,
Description = 7
};
typedef scwx::util::Iterator<Column, Column::Order, Column::Description>
ColumnIterator;
explicit LayerModel(QObject* parent = nullptr);
~LayerModel();
types::LayerVector GetLayers() const;
void ResetLayers();
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
int columnCount(const QModelIndex& parent = QModelIndex()) const override;
Qt::ItemFlags flags(const QModelIndex& index) const override;
Qt::DropActions supportedDropActions() const override;
bool IsMovable(int row) const;
QVariant data(const QModelIndex& index,
int role = Qt::DisplayRole) const override;
QVariant headerData(int section,
Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex& index,
const QVariant& value,
int role = Qt::EditRole) override;
QStringList mimeTypes() const override;
QMimeData* mimeData(const QModelIndexList& indexes) const override;
bool dropMimeData(const QMimeData* data,
Qt::DropAction action,
int row,
int column,
const QModelIndex& parent) override;
bool removeRows(int row,
int count,
const QModelIndex& parent = QModelIndex()) override;
bool moveRows(const QModelIndex& sourceParent,
int sourceRow,
int count,
const QModelIndex& destinationParent,
int destinationChild) override;
static std::shared_ptr<LayerModel> Instance();
private:
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace model
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,377 @@
#include <scwx/qt/model/placefile_model.hpp>
#include <scwx/qt/manager/placefile_manager.hpp>
#include <scwx/qt/types/qt_types.hpp>
#include <scwx/util/logger.hpp>
#include <QApplication>
#include <QCheckBox>
#include <QFontMetrics>
#include <QStyle>
#include <QStyleOption>
namespace scwx
{
namespace qt
{
namespace model
{
static const std::string logPrefix_ = "scwx::qt::model::placefile_model";
static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
static constexpr int kFirstColumn =
static_cast<int>(PlacefileModel::Column::Enabled);
static constexpr int kLastColumn =
static_cast<int>(PlacefileModel::Column::Placefile);
static constexpr int kNumColumns = kLastColumn - kFirstColumn + 1;
class PlacefileModelImpl
{
public:
explicit PlacefileModelImpl() {}
~PlacefileModelImpl() = default;
std::shared_ptr<manager::PlacefileManager> placefileManager_ {
manager::PlacefileManager::Instance()};
std::vector<std::string> placefileNames_ {};
};
PlacefileModel::PlacefileModel(QObject* parent) :
QAbstractTableModel(parent), p(std::make_unique<PlacefileModelImpl>())
{
connect(p->placefileManager_.get(),
&manager::PlacefileManager::PlacefileEnabled,
this,
&PlacefileModel::HandlePlacefileUpdate);
connect(p->placefileManager_.get(),
&manager::PlacefileManager::PlacefileRemoved,
this,
&PlacefileModel::HandlePlacefileRemoved);
connect(p->placefileManager_.get(),
&manager::PlacefileManager::PlacefileRenamed,
this,
&PlacefileModel::HandlePlacefileRenamed);
connect(p->placefileManager_.get(),
&manager::PlacefileManager::PlacefileUpdated,
this,
&PlacefileModel::HandlePlacefileUpdate);
}
PlacefileModel::~PlacefileModel() = default;
int PlacefileModel::rowCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : static_cast<int>(p->placefileNames_.size());
}
int PlacefileModel::columnCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : kNumColumns;
}
Qt::ItemFlags PlacefileModel::flags(const QModelIndex& index) const
{
Qt::ItemFlags flags = QAbstractTableModel::flags(index);
switch (index.column())
{
case static_cast<int>(Column::Enabled):
case static_cast<int>(Column::Thresholds):
flags |= Qt::ItemFlag::ItemIsUserCheckable | Qt::ItemFlag::ItemIsEditable;
break;
case static_cast<int>(Column::Placefile):
flags |= Qt::ItemFlag::ItemIsEditable;
break;
default:
break;
}
return flags;
}
QVariant PlacefileModel::data(const QModelIndex& index, int role) const
{
static const QString enabledString = QObject::tr("Enabled");
static const QString disabledString = QObject::tr("Disabled");
static const QString thresholdsEnabledString =
QObject::tr("Thresholds Enabled");
static const QString thresholdsDisabledString =
QObject::tr("Thresholds Disabled");
if (!index.isValid() || index.row() < 0 ||
static_cast<std::size_t>(index.row()) >= p->placefileNames_.size())
{
return QVariant();
}
const auto& placefileName = p->placefileNames_.at(index.row());
switch (index.column())
{
case static_cast<int>(Column::Enabled):
if (role == Qt::ItemDataRole::ToolTipRole)
{
return p->placefileManager_->placefile_enabled(placefileName) ?
enabledString :
disabledString;
}
else if (role == Qt::ItemDataRole::CheckStateRole)
{
return static_cast<int>(
p->placefileManager_->placefile_enabled(placefileName) ?
Qt::CheckState::Checked :
Qt::CheckState::Unchecked);
}
else if (role == types::ItemDataRole::SortRole)
{
return p->placefileManager_->placefile_enabled(placefileName);
}
break;
case static_cast<int>(Column::Thresholds):
if (role == Qt::ItemDataRole::ToolTipRole)
{
return p->placefileManager_->placefile_thresholded(placefileName) ?
thresholdsEnabledString :
thresholdsDisabledString;
}
else if (role == Qt::ItemDataRole::CheckStateRole)
{
return static_cast<int>(
p->placefileManager_->placefile_thresholded(placefileName) ?
Qt::CheckState::Checked :
Qt::CheckState::Unchecked);
}
else if (role == types::ItemDataRole::SortRole)
{
return p->placefileManager_->placefile_thresholded(placefileName);
}
break;
case static_cast<int>(Column::Placefile):
if (role == Qt::ItemDataRole::DisplayRole ||
role == Qt::ItemDataRole::ToolTipRole)
{
std::string description = placefileName;
std::string title =
p->placefileManager_->placefile_title(placefileName);
if (!title.empty())
{
description = title + '\n' + description;
}
return QString::fromStdString(description);
}
else if (role == Qt::ItemDataRole::EditRole ||
role == types::ItemDataRole::SortRole)
{
return QString::fromStdString(placefileName);
}
break;
default:
break;
}
return QVariant();
}
QVariant PlacefileModel::headerData(int section,
Qt::Orientation orientation,
int role) const
{
if (role == Qt::ItemDataRole::DisplayRole)
{
if (orientation == Qt::Horizontal)
{
switch (section)
{
case static_cast<int>(Column::Enabled):
return tr("E");
case static_cast<int>(Column::Thresholds):
return tr("T");
case static_cast<int>(Column::Placefile):
return tr("Placefile");
default:
break;
}
}
}
else if (role == Qt::ItemDataRole::ToolTipRole)
{
switch (section)
{
case static_cast<int>(Column::Enabled):
return tr("Enabled");
case static_cast<int>(Column::Thresholds):
return tr("Thresholds");
default:
break;
}
}
else if (role == Qt::ItemDataRole::SizeHintRole)
{
switch (section)
{
case static_cast<int>(Column::Enabled):
case static_cast<int>(Column::Thresholds):
{
static const QCheckBox checkBox {};
QStyleOptionButton option {};
option.initFrom(&checkBox);
// Width values from QCheckBox
return QApplication::style()->sizeFromContents(
QStyle::ContentsType::CT_CheckBox,
&option,
{option.iconSize.width() + 4, 0});
}
default:
break;
}
}
return QVariant();
}
bool PlacefileModel::setData(const QModelIndex& index,
const QVariant& value,
int role)
{
if (!index.isValid() || index.row() < 0 ||
static_cast<std::size_t>(index.row()) >= p->placefileNames_.size())
{
return false;
}
const auto& placefileName = p->placefileNames_.at(index.row());
bool result = false;
switch (index.column())
{
case static_cast<int>(Column::Enabled):
if (role == Qt::ItemDataRole::CheckStateRole)
{
p->placefileManager_->set_placefile_enabled(placefileName,
value.toBool());
result = true;
}
break;
case static_cast<int>(Column::Thresholds):
if (role == Qt::ItemDataRole::CheckStateRole)
{
p->placefileManager_->set_placefile_thresholded(placefileName,
value.toBool());
result = true;
}
break;
case static_cast<int>(Column::Placefile):
if (role == Qt::ItemDataRole::EditRole)
{
QString str = value.toString();
if (!str.isEmpty())
{
p->placefileManager_->set_placefile_url(placefileName,
str.toStdString());
result = true;
}
}
break;
default:
break;
}
if (result)
{
Q_EMIT dataChanged(index, index);
}
return result;
}
void PlacefileModel::HandlePlacefileRemoved(const std::string& name)
{
auto it =
std::find(p->placefileNames_.begin(), p->placefileNames_.end(), name);
if (it != p->placefileNames_.end())
{
// Placefile exists, delete row
const int row = std::distance(p->placefileNames_.begin(), it);
beginRemoveRows(QModelIndex(), row, row);
p->placefileNames_.erase(it);
endRemoveRows();
}
}
void PlacefileModel::HandlePlacefileRenamed(const std::string& oldName,
const std::string& newName)
{
auto it =
std::find(p->placefileNames_.begin(), p->placefileNames_.end(), oldName);
if (it != p->placefileNames_.end())
{
// Placefile exists, mark row as updated
const int row = std::distance(p->placefileNames_.begin(), it);
QModelIndex topLeft = createIndex(row, kFirstColumn);
QModelIndex bottomRight = createIndex(row, kLastColumn);
// Rename placefile
*it = newName;
Q_EMIT dataChanged(topLeft, bottomRight);
}
else
{
// Placefile is new, append row
const int newIndex = static_cast<int>(p->placefileNames_.size());
beginInsertRows(QModelIndex(), newIndex, newIndex);
p->placefileNames_.push_back(newName);
endInsertRows();
}
}
void PlacefileModel::HandlePlacefileUpdate(const std::string& name)
{
auto it =
std::find(p->placefileNames_.begin(), p->placefileNames_.end(), name);
if (it != p->placefileNames_.end())
{
// Placefile exists, mark row as updated
const int row = std::distance(p->placefileNames_.begin(), it);
QModelIndex topLeft = createIndex(row, kFirstColumn);
QModelIndex bottomRight = createIndex(row, kLastColumn);
Q_EMIT dataChanged(topLeft, bottomRight);
}
else
{
// Placefile is new, append row
const int newIndex = static_cast<int>(p->placefileNames_.size());
beginInsertRows(QModelIndex(), newIndex, newIndex);
p->placefileNames_.push_back(name);
endInsertRows();
}
}
} // namespace model
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,60 @@
#pragma once
#include <scwx/qt/types/text_event_key.hpp>
#include <scwx/common/geographic.hpp>
#include <memory>
#include <QAbstractTableModel>
namespace scwx
{
namespace qt
{
namespace model
{
class PlacefileModelImpl;
class PlacefileModel : public QAbstractTableModel
{
public:
enum class Column : int
{
Enabled = 0,
Thresholds = 1,
Placefile = 2
};
explicit PlacefileModel(QObject* parent = nullptr);
~PlacefileModel();
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
int columnCount(const QModelIndex& parent = QModelIndex()) const override;
Qt::ItemFlags flags(const QModelIndex& index) const override;
QVariant data(const QModelIndex& index,
int role = Qt::DisplayRole) const override;
QVariant headerData(int section,
Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex& index,
const QVariant& value,
int role = Qt::EditRole) override;
public slots:
void HandlePlacefileRemoved(const std::string& name);
void HandlePlacefileRenamed(const std::string& oldName,
const std::string& newName);
void HandlePlacefileUpdate(const std::string& name);
private:
friend class PlacefileModelImpl;
std::unique_ptr<PlacefileModelImpl> p;
};
} // namespace model
} // namespace qt
} // namespace scwx

View file

@ -16,10 +16,10 @@ namespace settings
static const std::string logPrefix_ = "scwx::qt::settings::general_settings";
class GeneralSettingsImpl
class GeneralSettings::Impl
{
public:
explicit GeneralSettingsImpl()
explicit Impl()
{
std::string defaultDefaultAlertActionValue =
types::GetAlertActionName(types::AlertAction::Go);
@ -29,6 +29,7 @@ public:
boost::to_lower(defaultDefaultAlertActionValue);
boost::to_lower(defaultMapProviderValue);
antiAliasingEnabled_.SetDefault(true);
debugEnabled_.SetDefault(false);
defaultAlertAction_.SetDefault(defaultDefaultAlertActionValue);
defaultRadarSite_.SetDefault("KLSX");
@ -102,8 +103,9 @@ public:
{ return !value.empty(); });
}
~GeneralSettingsImpl() {}
~Impl() {}
SettingsVariable<bool> antiAliasingEnabled_ {"anti_aliasing_enabled"};
SettingsVariable<bool> debugEnabled_ {"debug_enabled"};
SettingsVariable<std::string> defaultAlertAction_ {"default_alert_action"};
SettingsVariable<std::string> defaultRadarSite_ {"default_radar_site"};
@ -120,9 +122,10 @@ public:
};
GeneralSettings::GeneralSettings() :
SettingsCategory("general"), p(std::make_unique<GeneralSettingsImpl>())
SettingsCategory("general"), p(std::make_unique<Impl>())
{
RegisterVariables({&p->debugEnabled_,
RegisterVariables({&p->antiAliasingEnabled_,
&p->debugEnabled_,
&p->defaultAlertAction_,
&p->defaultRadarSite_,
&p->fontSizes_,
@ -143,6 +146,11 @@ GeneralSettings::GeneralSettings(GeneralSettings&&) noexcept = default;
GeneralSettings&
GeneralSettings::operator=(GeneralSettings&&) noexcept = default;
SettingsVariable<bool>& GeneralSettings::anti_aliasing_enabled() const
{
return p->antiAliasingEnabled_;
}
SettingsVariable<bool>& GeneralSettings::debug_enabled() const
{
return p->debugEnabled_;
@ -221,9 +229,16 @@ bool GeneralSettings::Shutdown()
return dataChanged;
}
GeneralSettings& GeneralSettings::Instance()
{
static GeneralSettings generalSettings_;
return generalSettings_;
}
bool operator==(const GeneralSettings& lhs, const GeneralSettings& rhs)
{
return (lhs.p->debugEnabled_ == rhs.p->debugEnabled_ &&
return (lhs.p->antiAliasingEnabled_ == rhs.p->antiAliasingEnabled_ &&
lhs.p->debugEnabled_ == rhs.p->debugEnabled_ &&
lhs.p->defaultAlertAction_ == rhs.p->defaultAlertAction_ &&
lhs.p->defaultRadarSite_ == rhs.p->defaultRadarSite_ &&
lhs.p->fontSizes_ == rhs.p->fontSizes_ &&

View file

@ -13,8 +13,6 @@ namespace qt
namespace settings
{
class GeneralSettingsImpl;
class GeneralSettings : public SettingsCategory
{
public:
@ -27,6 +25,7 @@ public:
GeneralSettings(GeneralSettings&&) noexcept;
GeneralSettings& operator=(GeneralSettings&&) noexcept;
SettingsVariable<bool>& anti_aliasing_enabled() const;
SettingsVariable<bool>& debug_enabled() const;
SettingsVariable<std::string>& default_alert_action() const;
SettingsVariable<std::string>& default_radar_site() const;
@ -41,13 +40,16 @@ public:
SettingsVariable<std::string>& maptiler_api_key() const;
SettingsVariable<bool>& update_notifications_enabled() const;
static GeneralSettings& Instance();
friend bool operator==(const GeneralSettings& lhs,
const GeneralSettings& rhs);
bool Shutdown();
private:
std::unique_ptr<GeneralSettingsImpl> p;
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace settings

View file

@ -35,7 +35,7 @@ static const std::string kDefaultRadarProductGroupString_ = "L3";
static const std::array<std::string, kCount_> kDefaultRadarProduct_ {
"N0B", "N0G", "N0C", "N0X"};
class MapSettingsImpl
class MapSettings::Impl
{
public:
struct MapData
@ -47,7 +47,7 @@ public:
SettingsVariable<std::string> radarProduct_ {kRadarProductName_};
};
explicit MapSettingsImpl()
explicit Impl()
{
for (std::size_t i = 0; i < kCount_; i++)
{
@ -101,7 +101,7 @@ public:
}
}
~MapSettingsImpl() {}
~Impl() {}
void SetDefaults(std::size_t i)
{
@ -111,12 +111,30 @@ public:
map_[i].radarProduct_.SetValueToDefault();
}
friend void tag_invoke(boost::json::value_from_tag,
boost::json::value& jv,
const MapData& data)
{
jv = {{kMapStyleName_, data.mapStyle_.GetValue()},
{kRadarSiteName_, data.radarSite_.GetValue()},
{kRadarProductGroupName_, data.radarProductGroup_.GetValue()},
{kRadarProductName_, data.radarProduct_.GetValue()}};
}
friend bool operator==(const MapData& lhs, const MapData& rhs)
{
return (lhs.mapStyle_ == rhs.mapStyle_ && //
lhs.radarSite_ == rhs.radarSite_ &&
lhs.radarProductGroup_ == rhs.radarProductGroup_ &&
lhs.radarProduct_ == rhs.radarProduct_);
}
std::array<MapData, kCount_> map_ {};
std::vector<SettingsVariableBase*> variables_ {};
};
MapSettings::MapSettings() :
SettingsCategory("maps"), p(std::make_unique<MapSettingsImpl>())
SettingsCategory("maps"), p(std::make_unique<Impl>())
{
RegisterVariables(p->variables_);
SetDefaults();
@ -161,7 +179,7 @@ bool MapSettings::Shutdown()
// Commit settings that are managed separate from the settings dialog
for (std::size_t i = 0; i < kCount_; ++i)
{
MapSettingsImpl::MapData& mapRecordSettings = p->map_[i];
Impl::MapData& mapRecordSettings = p->map_[i];
dataChanged |= mapRecordSettings.mapStyle_.Commit();
}
@ -184,7 +202,7 @@ bool MapSettings::ReadJson(const boost::json::object& json)
if (i < mapArray.size() && mapArray.at(i).is_object())
{
const boost::json::object& mapRecord = mapArray.at(i).as_object();
MapSettingsImpl::MapData& mapRecordSettings = p->map_[i];
Impl::MapData& mapRecordSettings = p->map_[i];
// Load JSON Elements
validated &= mapRecordSettings.mapStyle_.ReadValue(mapRecord);
@ -234,14 +252,10 @@ void MapSettings::WriteJson(boost::json::object& json) const
json.insert_or_assign(name(), object);
}
void tag_invoke(boost::json::value_from_tag,
boost::json::value& jv,
const MapSettingsImpl::MapData& data)
MapSettings& MapSettings::Instance()
{
jv = {{kMapStyleName_, data.mapStyle_.GetValue()},
{kRadarSiteName_, data.radarSite_.GetValue()},
{kRadarProductGroupName_, data.radarProductGroup_.GetValue()},
{kRadarProductName_, data.radarProduct_.GetValue()}};
static MapSettings mapSettings_;
return mapSettings_;
}
bool operator==(const MapSettings& lhs, const MapSettings& rhs)
@ -249,15 +263,6 @@ bool operator==(const MapSettings& lhs, const MapSettings& rhs)
return (lhs.p->map_ == rhs.p->map_);
}
bool operator==(const MapSettingsImpl::MapData& lhs,
const MapSettingsImpl::MapData& rhs)
{
return (lhs.mapStyle_ == rhs.mapStyle_ && //
lhs.radarSite_ == rhs.radarSite_ &&
lhs.radarProductGroup_ == rhs.radarProductGroup_ &&
lhs.radarProduct_ == rhs.radarProduct_);
}
} // namespace settings
} // namespace qt
} // namespace scwx

View file

@ -13,8 +13,6 @@ namespace qt
namespace settings
{
class MapSettingsImpl;
class MapSettings : public SettingsCategory
{
public:
@ -52,10 +50,13 @@ public:
*/
void WriteJson(boost::json::object& json) const override;
static MapSettings& Instance();
friend bool operator==(const MapSettings& lhs, const MapSettings& rhs);
private:
std::unique_ptr<MapSettingsImpl> p;
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace settings

View file

@ -72,10 +72,10 @@ static const std::map<
static const std::string kDefaultKey_ {"???"};
static const awips::Phenomenon kDefaultPhenomenon_ {awips::Phenomenon::Marine};
class PaletteSettingsImpl
class PaletteSettings::Impl
{
public:
explicit PaletteSettingsImpl()
explicit Impl()
{
for (const auto& name : kPaletteKeys_)
{
@ -120,7 +120,7 @@ public:
}
}
~PaletteSettingsImpl() {}
~Impl() {}
static bool ValidateColor(const std::string& value);
@ -132,14 +132,14 @@ public:
std::vector<SettingsVariableBase*> variables_ {};
};
bool PaletteSettingsImpl::ValidateColor(const std::string& value)
bool PaletteSettings::Impl::ValidateColor(const std::string& value)
{
static const std::regex re {"#[0-9A-Za-z]{8}"};
return std::regex_match(value, re);
}
PaletteSettings::PaletteSettings() :
SettingsCategory("palette"), p(std::make_unique<PaletteSettingsImpl>())
SettingsCategory("palette"), p(std::make_unique<Impl>())
{
RegisterVariables(p->variables_);
SetDefaults();
@ -200,6 +200,12 @@ const std::vector<awips::Phenomenon>& PaletteSettings::alert_phenomena()
return kAlertPhenomena_;
}
PaletteSettings& PaletteSettings::Instance()
{
static PaletteSettings paletteSettings_;
return paletteSettings_;
}
bool operator==(const PaletteSettings& lhs, const PaletteSettings& rhs)
{
return lhs.p->palette_ == rhs.p->palette_;

View file

@ -14,8 +14,6 @@ namespace qt
namespace settings
{
class PaletteSettingsImpl;
class PaletteSettings : public SettingsCategory
{
public:
@ -34,11 +32,14 @@ public:
static const std::vector<awips::Phenomenon>& alert_phenomena();
static PaletteSettings& Instance();
friend bool operator==(const PaletteSettings& lhs,
const PaletteSettings& rhs);
private:
std::unique_ptr<PaletteSettingsImpl> p;
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace settings

View file

@ -2,6 +2,8 @@
#include <scwx/qt/util/json.hpp>
#include <scwx/util/logger.hpp>
#include <algorithm>
namespace scwx
{
namespace qt
@ -21,6 +23,8 @@ public:
const std::string name_;
std::vector<std::pair<std::string, std::vector<SettingsCategory*>>>
subcategoryArrays_;
std::vector<SettingsVariableBase*> variables_;
};
@ -41,6 +45,16 @@ std::string SettingsCategory::name() const
void SettingsCategory::SetDefaults()
{
// Set subcategory array defaults
for (auto& subcategoryArray : p->subcategoryArrays_)
{
for (auto& subcategory : subcategoryArray.second)
{
subcategory->SetDefaults();
}
}
// Set variable defaults
for (auto& variable : p->variables_)
{
variable->SetValueToDefault();
@ -57,6 +71,47 @@ bool SettingsCategory::ReadJson(const boost::json::object& json)
{
const boost::json::object& object = value->as_object();
// Read subcategory arrays
for (auto& subcategoryArray : p->subcategoryArrays_)
{
const boost::json::value* arrayValue =
object.if_contains(subcategoryArray.first);
if (arrayValue != nullptr && arrayValue->is_object())
{
const boost::json::object& arrayObject = arrayValue->as_object();
for (auto& subcategory : subcategoryArray.second)
{
validated &= subcategory->ReadJson(arrayObject);
}
}
else
{
if (arrayValue == nullptr)
{
logger_->debug(
"Subcategory array key {} is not present, resetting to "
"defaults",
subcategoryArray.first);
}
else if (!arrayValue->is_object())
{
logger_->warn(
"Invalid json for subcategory array key {}, resetting to "
"defaults",
p->name_);
}
for (auto& subcategory : subcategoryArray.second)
{
subcategory->SetDefaults();
}
validated = false;
}
}
// Read variables
for (auto& variable : p->variables_)
{
validated &= variable->ReadValue(object);
@ -66,7 +121,7 @@ bool SettingsCategory::ReadJson(const boost::json::object& json)
{
if (value == nullptr)
{
logger_->warn("Key {} is not present, resetting to defaults",
logger_->debug("Key {} is not present, resetting to defaults",
p->name_);
}
else if (!value->is_object())
@ -86,6 +141,20 @@ void SettingsCategory::WriteJson(boost::json::object& json) const
{
boost::json::object object;
// Write subcategory arrays
for (auto& subcategoryArray : p->subcategoryArrays_)
{
boost::json::object arrayObject;
for (auto& subcategory : subcategoryArray.second)
{
subcategory->WriteJson(arrayObject);
}
object.insert_or_assign(subcategoryArray.first, arrayObject);
}
// Write variables
for (auto& variable : p->variables_)
{
variable->WriteValue(object);
@ -94,6 +163,18 @@ void SettingsCategory::WriteJson(boost::json::object& json) const
json.insert_or_assign(p->name_, object);
}
void SettingsCategory::RegisterSubcategoryArray(
const std::string& name, std::vector<SettingsCategory>& subcategories)
{
auto& newSubcategories = p->subcategoryArrays_.emplace_back(
name, std::vector<SettingsCategory*> {});
std::transform(subcategories.begin(),
subcategories.end(),
std::back_inserter(newSubcategories.second),
[](SettingsCategory& subcategory) { return &subcategory; });
}
void SettingsCategory::RegisterVariables(
std::initializer_list<SettingsVariableBase*> variables)
{

View file

@ -50,7 +50,8 @@ public:
*/
virtual void WriteJson(boost::json::object& json) const;
protected:
void RegisterSubcategoryArray(const std::string& name,
std::vector<SettingsCategory>& subcategories);
void
RegisterVariables(std::initializer_list<SettingsVariableBase*> variables);
void RegisterVariables(std::vector<SettingsVariableBase*> variables);

View file

@ -9,6 +9,7 @@
#include <QCheckBox>
#include <QComboBox>
#include <QCoreApplication>
#include <QLabel>
#include <QLineEdit>
#include <QSpinBox>
#include <QWidget>
@ -26,16 +27,21 @@ template<class T>
class SettingsInterface<T>::Impl
{
public:
explicit Impl()
explicit Impl(SettingsInterface* self) : self_ {self}
{
context_->moveToThread(QCoreApplication::instance()->thread());
}
~Impl() {}
template<class U>
void SetWidgetText(U* widget, const T& currentValue);
void UpdateEditWidget();
void UpdateResetButton();
SettingsInterface<T>* self_;
SettingsVariable<T>* variable_ {nullptr};
bool stagedValid_ {true};
@ -49,17 +55,27 @@ public:
template<class T>
SettingsInterface<T>::SettingsInterface() :
SettingsInterfaceBase(), p(std::make_unique<Impl>())
SettingsInterfaceBase(), p(std::make_unique<Impl>(this))
{
}
template<class T>
SettingsInterface<T>::~SettingsInterface() = default;
template<class T>
SettingsInterface<T>::SettingsInterface(SettingsInterface&&) noexcept = default;
SettingsInterface<T>::SettingsInterface(SettingsInterface&& o) noexcept :
p {std::move(o.p)}
{
p->self_ = this;
}
template<class T>
SettingsInterface<T>&
SettingsInterface<T>::operator=(SettingsInterface&&) noexcept = default;
SettingsInterface<T>::operator=(SettingsInterface&& o) noexcept
{
p = std::move(o.p);
p->self_ = this;
return *this;
}
template<class T>
void SettingsInterface<T>::SetSettingsVariable(SettingsVariable<T>& variable)
@ -73,6 +89,27 @@ SettingsVariable<T>* SettingsInterface<T>::GetSettingsVariable() const
return p->variable_;
}
template<class T>
bool SettingsInterface<T>::IsDefault()
{
bool isDefault = false;
const std::optional<T> staged = p->variable_->GetStaged();
const T defaultValue = p->variable_->GetDefault();
const T value = p->variable_->GetValue();
if (staged.has_value())
{
isDefault = (p->stagedValid_ && *staged == defaultValue);
}
else
{
isDefault = (value == defaultValue);
}
return isDefault;
}
template<class T>
bool SettingsInterface<T>::Commit()
{
@ -95,6 +132,14 @@ void SettingsInterface<T>::StageDefault()
p->UpdateResetButton();
}
template<class T>
void SettingsInterface<T>::StageValue(const T& value)
{
p->variable_->StageValue(value);
p->UpdateEditWidget();
p->UpdateResetButton();
}
template<class T>
void SettingsInterface<T>::SetEditWidget(QWidget* widget)
{
@ -105,6 +150,11 @@ void SettingsInterface<T>::SetEditWidget(QWidget* widget)
p->editWidget_ = widget;
if (widget == nullptr)
{
return;
}
if (QLineEdit* lineEdit = dynamic_cast<QLineEdit*>(widget))
{
if constexpr (std::is_same_v<T, std::string>)
@ -274,6 +324,8 @@ void SettingsInterface<T>::SetResetButton(QAbstractButton* button)
p->resetButton_ = button;
if (p->resetButton_ != nullptr)
{
QObject::connect(p->resetButton_,
&QAbstractButton::clicked,
p->context_.get(),
@ -283,8 +335,8 @@ void SettingsInterface<T>::SetResetButton(QAbstractButton* button)
if (p->variable_->GetValue() == defaultValue)
{
// If the current value is default, reset the staged
// value
// If the current value is default, reset the
// staged value
p->variable_->Reset();
p->stagedValid_ = true;
p->UpdateEditWidget();
@ -302,6 +354,7 @@ void SettingsInterface<T>::SetResetButton(QAbstractButton* button)
p->UpdateResetButton();
}
}
template<class T>
void SettingsInterface<T>::SetMapFromValueFunction(
@ -317,6 +370,39 @@ void SettingsInterface<T>::SetMapToValueFunction(
p->mapToValue_ = function;
}
template<class T>
template<class U>
void SettingsInterface<T>::Impl::SetWidgetText(U* widget, const T& currentValue)
{
if constexpr (std::is_integral_v<T>)
{
widget->setText(QString::number(currentValue));
}
else if constexpr (std::is_same_v<T, std::string>)
{
if (mapFromValue_ != nullptr)
{
widget->setText(QString::fromStdString(mapFromValue_(currentValue)));
}
else
{
widget->setText(QString::fromStdString(currentValue));
}
}
else if constexpr (std::is_same_v<T, std::vector<std::int64_t>>)
{
if (mapFromValue_ != nullptr)
{
widget->setText(QString::fromStdString(mapFromValue_(currentValue)));
}
else
{
widget->setText(QString::fromStdString(
fmt::format("{}", fmt::join(currentValue, ", "))));
}
}
}
template<class T>
void SettingsInterface<T>::Impl::UpdateEditWidget()
{
@ -327,35 +413,11 @@ void SettingsInterface<T>::Impl::UpdateEditWidget()
if (QLineEdit* lineEdit = dynamic_cast<QLineEdit*>(editWidget_))
{
if constexpr (std::is_integral_v<T>)
{
lineEdit->setText(QString::number(currentValue));
SetWidgetText(lineEdit, currentValue);
}
else if constexpr (std::is_same_v<T, std::string>)
else if (QLabel* label = dynamic_cast<QLabel*>(editWidget_))
{
if (mapFromValue_ != nullptr)
{
lineEdit->setText(
QString::fromStdString(mapFromValue_(currentValue)));
}
else
{
lineEdit->setText(QString::fromStdString(currentValue));
}
}
else if constexpr (std::is_same_v<T, std::vector<std::int64_t>>)
{
if (mapFromValue_ != nullptr)
{
lineEdit->setText(
QString::fromStdString(mapFromValue_(currentValue)));
}
else
{
lineEdit->setText(QString::fromStdString(
fmt::format("{}", fmt::join(currentValue, ", "))));
}
}
SetWidgetText(label, currentValue);
}
else if (QCheckBox* checkBox = dynamic_cast<QCheckBox*>(editWidget_))
{
@ -391,20 +453,9 @@ void SettingsInterface<T>::Impl::UpdateEditWidget()
template<class T>
void SettingsInterface<T>::Impl::UpdateResetButton()
{
const std::optional<T> staged = variable_->GetStaged();
const T defaultValue = variable_->GetDefault();
const T value = variable_->GetValue();
if (resetButton_ != nullptr)
{
if (staged.has_value())
{
resetButton_->setVisible(!stagedValid_ || *staged != defaultValue);
}
else
{
resetButton_->setVisible(value != defaultValue);
}
resetButton_->setVisible(!self_->IsDefault());
}
}

View file

@ -45,6 +45,14 @@ public:
*/
SettingsVariable<T>* GetSettingsVariable() const;
/**
* Gets whether the staged value (or current value, if none staged) is
* set to the default value.
*
* @return true if the settings variable is set to default, otherwise false.
*/
bool IsDefault() override;
/**
* Sets the current value of the associated settings variable to the staged
* value.
@ -64,6 +72,11 @@ public:
*/
void StageDefault() override;
/**
* Stages a value to the associated settings variable.
*/
void StageValue(const T& value);
/**
* Sets the edit widget from the settings dialog.
*
@ -103,6 +116,7 @@ private:
#ifdef SETTINGS_INTERFACE_IMPLEMENTATION
template class SettingsInterface<bool>;
template class SettingsInterface<double>;
template class SettingsInterface<std::int64_t>;
template class SettingsInterface<std::string>;

View file

@ -24,6 +24,14 @@ public:
SettingsInterfaceBase(SettingsInterfaceBase&&) noexcept;
SettingsInterfaceBase& operator=(SettingsInterfaceBase&&) noexcept;
/**
* Gets whether the staged value (or current value, if none staged) is
* set to the default value.
*
* @return true if the settings variable is set to default, otherwise false.
*/
virtual bool IsDefault() = 0;
/**
* Sets the current value of the associated settings variable to the staged
* value.

View file

@ -239,6 +239,12 @@ std::optional<T> SettingsVariable<T>::GetStaged() const
return p->staged_;
}
template<class T>
T SettingsVariable<T>::GetStagedOrValue() const
{
return p->staged_.value_or(GetValue());
}
template<class T>
T SettingsVariable<T>::GetDefault() const
{

View file

@ -103,6 +103,14 @@ public:
*/
std::optional<T> GetStaged() const;
/**
* Gets the staged value of the settings variable, if defined, otherwise the
* current value.
*
* @return Staged value or current value
*/
T GetStagedOrValue() const;
/**
* Validate the value against the defined parameters of the settings
* variable.

View file

@ -0,0 +1,198 @@
#include <scwx/qt/settings/text_settings.hpp>
#include <scwx/qt/types/text_types.hpp>
#include <boost/algorithm/string.hpp>
namespace scwx
{
namespace qt
{
namespace settings
{
static const std::string logPrefix_ = "scwx::qt::settings::text_settings";
static const std::string kAlteDIN1451Mittelscrhift_ {
"Alte DIN 1451 Mittelschrift"};
static const std::string kInconsolata_ {"Inconsolata"};
static const std::string kRegular_ {"Regular"};
static const std::unordered_map<types::FontCategory, std::string>
kDefaultFontFamily_ {
{types::FontCategory::Default, kAlteDIN1451Mittelscrhift_},
{types::FontCategory::Tooltip, kInconsolata_}};
static const std::unordered_map<types::FontCategory, std::string>
kDefaultFontStyle_ {{types::FontCategory::Default, kRegular_},
{types::FontCategory::Tooltip, kRegular_}};
static const std::unordered_map<types::FontCategory, double>
kDefaultFontPointSize_ {{types::FontCategory::Default, 12.0},
{types::FontCategory::Tooltip, 10.5}};
class TextSettings::Impl
{
public:
struct FontData
{
SettingsVariable<std::string> fontFamily_ {"font_family"};
SettingsVariable<std::string> fontStyle_ {"font_style"};
SettingsVariable<double> fontPointSize_ {"font_point_size"};
};
explicit Impl(TextSettings* self) : self_ {self}
{
std::string defaultTooltipMethodValue =
types::GetTooltipMethodName(types::TooltipMethod::ImGui);
boost::to_lower(defaultTooltipMethodValue);
hoverTextWrap_.SetDefault(80);
hoverTextWrap_.SetMinimum(0);
hoverTextWrap_.SetMaximum(999);
placefileTextDropShadowEnabled_.SetDefault(true);
tooltipMethod_.SetDefault(defaultTooltipMethodValue);
tooltipMethod_.SetValidator(
[](const std::string& value)
{
for (types::TooltipMethod tooltipMethod :
types::TooltipMethodIterator())
{
// If the value is equal to a lower case alert action name
std::string tooltipMethodName =
types::GetTooltipMethodName(tooltipMethod);
boost::to_lower(tooltipMethodName);
if (value == tooltipMethodName)
{
// Regard as a match, valid
return true;
}
}
// No match found, invalid
return false;
});
InitializeFontVariables();
}
~Impl() {}
void InitializeFontVariables();
friend bool operator==(const FontData& lhs, const FontData& rhs)
{
return (lhs.fontFamily_ == rhs.fontFamily_ &&
lhs.fontStyle_ == rhs.fontStyle_ &&
lhs.fontPointSize_ == rhs.fontPointSize_);
}
TextSettings* self_;
std::unordered_map<types::FontCategory, FontData> fontData_ {};
std::vector<SettingsCategory> fontSettings_ {};
SettingsVariable<std::int64_t> hoverTextWrap_ {"hover_text_wrap"};
SettingsVariable<std::string> tooltipMethod_ {"tooltip_method"};
SettingsVariable<bool> placefileTextDropShadowEnabled_ {
"placefile_text_drop_shadow_enabled"};
};
TextSettings::TextSettings() :
SettingsCategory("text"), p(std::make_unique<Impl>(this))
{
RegisterVariables({&p->hoverTextWrap_,
&p->placefileTextDropShadowEnabled_,
&p->tooltipMethod_});
SetDefaults();
}
TextSettings::~TextSettings() = default;
TextSettings::TextSettings(TextSettings&&) noexcept = default;
TextSettings& TextSettings::operator=(TextSettings&&) noexcept = default;
void TextSettings::Impl::InitializeFontVariables()
{
for (auto fontCategory : types::FontCategoryIterator())
{
auto result = fontData_.emplace(fontCategory, FontData {});
auto& pair = *result.first;
auto& font = pair.second;
font.fontFamily_.SetDefault(kDefaultFontFamily_.at(fontCategory));
font.fontStyle_.SetDefault(kDefaultFontStyle_.at(fontCategory));
font.fontPointSize_.SetDefault(kDefaultFontPointSize_.at(fontCategory));
// String values must not be empty
font.fontFamily_.SetValidator([](const std::string& value)
{ return !value.empty(); });
font.fontStyle_.SetValidator([](const std::string& value)
{ return !value.empty(); });
// Font point size must be between 6 and 72
font.fontPointSize_.SetMinimum(6.0);
font.fontPointSize_.SetMaximum(72.0);
// Variable registration
auto& settings = fontSettings_.emplace_back(
SettingsCategory {types::GetFontCategoryName(fontCategory)});
settings.RegisterVariables(
{&font.fontFamily_, &font.fontStyle_, &font.fontPointSize_});
}
self_->RegisterSubcategoryArray("fonts", fontSettings_);
}
SettingsVariable<std::string>&
TextSettings::font_family(types::FontCategory fontCategory) const
{
return p->fontData_.at(fontCategory).fontFamily_;
}
SettingsVariable<std::string>&
TextSettings::font_style(types::FontCategory fontCategory) const
{
return p->fontData_.at(fontCategory).fontStyle_;
}
SettingsVariable<double>&
TextSettings::font_point_size(types::FontCategory fontCategory) const
{
return p->fontData_.at(fontCategory).fontPointSize_;
}
SettingsVariable<std::int64_t>& TextSettings::hover_text_wrap() const
{
return p->hoverTextWrap_;
}
SettingsVariable<bool>& TextSettings::placefile_text_drop_shadow_enabled() const
{
return p->placefileTextDropShadowEnabled_;
}
SettingsVariable<std::string>& TextSettings::tooltip_method() const
{
return p->tooltipMethod_;
}
TextSettings& TextSettings::Instance()
{
static TextSettings textSettings_;
return textSettings_;
}
bool operator==(const TextSettings& lhs, const TextSettings& rhs)
{
return (lhs.p->fontData_ == rhs.p->fontData_ &&
lhs.p->hoverTextWrap_ == rhs.p->hoverTextWrap_ &&
lhs.p->placefileTextDropShadowEnabled_ ==
rhs.p->placefileTextDropShadowEnabled_ &&
lhs.p->tooltipMethod_ == rhs.p->tooltipMethod_);
}
} // namespace settings
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,52 @@
#pragma once
#include <scwx/qt/settings/settings_category.hpp>
#include <scwx/qt/settings/settings_variable.hpp>
#include <scwx/qt/types/text_types.hpp>
#include <memory>
#include <string>
namespace scwx
{
namespace qt
{
namespace settings
{
class TextSettings : public SettingsCategory
{
public:
explicit TextSettings();
~TextSettings();
TextSettings(const TextSettings&) = delete;
TextSettings& operator=(const TextSettings&) = delete;
TextSettings(TextSettings&&) noexcept;
TextSettings& operator=(TextSettings&&) noexcept;
SettingsVariable<std::string>&
font_family(types::FontCategory fontCategory) const;
SettingsVariable<std::string>&
font_style(types::FontCategory fontCategory) const;
SettingsVariable<double>&
font_point_size(types::FontCategory fontCategory) const;
SettingsVariable<std::int64_t>& hover_text_wrap() const;
SettingsVariable<bool>& placefile_text_drop_shadow_enabled() const;
SettingsVariable<std::string>& tooltip_method() const;
static TextSettings& Instance();
friend bool operator==(const TextSettings& lhs, const TextSettings& rhs);
private:
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace settings
} // namespace qt
} // namespace scwx

View file

@ -1,5 +1,31 @@
#pragma once
#include <units/core.h>
namespace units
{
namespace dimension
{
struct font_size_tag
{
static constexpr const char* const name = "font size";
static constexpr const char* const abbreviation = "px";
};
using font_size = make_dimension<font_size_tag>;
} // namespace dimension
UNIT_ADD(font_size,
pixels,
px,
conversion_factor<std::ratio<1>, dimension::font_size>)
UNIT_ADD(font_size, points, pt, conversion_factor<std::ratio<4, 3>, pixels<>>)
} // namespace units
namespace scwx
{
namespace qt
@ -10,7 +36,8 @@ namespace types
enum class Font
{
din1451alt,
din1451alt_g
din1451alt_g,
Inconsolata_Regular
};
} // namespace types

View file

@ -0,0 +1,83 @@
// Disable strncpy warning
#define _CRT_SECURE_NO_WARNINGS
#include <scwx/qt/types/imgui_font.hpp>
#include <scwx/qt/model/imgui_context_model.hpp>
#include <scwx/util/logger.hpp>
#include <algorithm>
#include <limits>
#include <imgui.h>
namespace scwx
{
namespace qt
{
namespace types
{
static const std::string logPrefix_ = "scwx::qt::types::imgui_font";
static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
class ImGuiFont::Impl
{
public:
explicit Impl(const std::string& fontName,
const std::vector<char>& fontData,
units::font_size::pixels<int> size) :
fontName_ {fontName}, size_ {size}
{
CreateImGuiFont(fontData);
}
~Impl() {}
void CreateImGuiFont(const std::vector<char>& fontData);
const std::string fontName_;
const units::font_size::pixels<int> size_;
ImFont* imFont_ {nullptr};
};
ImGuiFont::ImGuiFont(const std::string& fontName,
const std::vector<char>& fontData,
units::font_size::pixels<int> size) :
p(std::make_unique<Impl>(fontName, fontData, size))
{
}
ImGuiFont::~ImGuiFont() = default;
void ImGuiFont::Impl::CreateImGuiFont(const std::vector<char>& fontData)
{
logger_->debug("Creating Font: {}", fontName_);
ImFontAtlas* fontAtlas = model::ImGuiContextModel::Instance().font_atlas();
ImFontConfig fontConfig {};
const float sizePixels = static_cast<float>(size_.value());
// Do not transfer ownership of font data to ImGui, makes const_cast safe
fontConfig.FontDataOwnedByAtlas = false;
// Assign name to font
strncpy(fontConfig.Name, fontName_.c_str(), sizeof(fontConfig.Name) - 1);
fontConfig.Name[sizeof(fontConfig.Name) - 1] = 0;
imFont_ = fontAtlas->AddFontFromMemoryTTF(
const_cast<void*>(static_cast<const void*>(fontData.data())),
static_cast<int>(std::clamp<std::size_t>(
fontData.size(), 0, std::numeric_limits<int>::max())),
sizePixels,
&fontConfig);
}
ImFont* ImGuiFont::font()
{
return p->imFont_;
}
} // namespace types
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,41 @@
#pragma once
#include <memory>
#include <string>
#include <vector>
#include <scwx/qt/types/font_types.hpp>
struct ImFont;
namespace scwx
{
namespace qt
{
namespace types
{
class ImGuiFont
{
public:
explicit ImGuiFont(const std::string& fontName,
const std::vector<char>& fontData,
units::font_size::pixels<int> size);
~ImGuiFont();
ImGuiFont(const ImGuiFont&) = delete;
ImGuiFont& operator=(const ImGuiFont&) = delete;
ImGuiFont(ImGuiFont&&) = delete;
ImGuiFont& operator=(ImGuiFont&&) = delete;
ImFont* font();
private:
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace types
} // namespace qt
} // namespace scwx

Some files were not shown because too many files have changed in this diff Show more