6
									
								
								.gitmodules
									
										
									
									
										vendored
									
									
								
							
							
						
						|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
							
								
								
									
										6
									
								
								external/CMakeLists.txt
									
										
									
									
										vendored
									
									
								
							
							
						
						|  | @ -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) | ||||
|  |  | |||
							
								
								
									
										2
									
								
								external/mapbox-gl-native
									
										
									
									
										vendored
									
									
								
							
							
						
						|  | @ -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
									
								
							
							
						
						|  | @ -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
									
								
							
							
						
						|  | @ -0,0 +1,4 @@ | |||
| cmake_minimum_required(VERSION 3.20) | ||||
| set(PROJECT_NAME scwx-units) | ||||
| 
 | ||||
| add_subdirectory(units) | ||||
|  | @ -1,5 +1,5 @@ | |||
| #version 330 core | ||||
| in vec4 color; | ||||
| smooth in vec4 color; | ||||
| 
 | ||||
| layout (location = 0) out vec4 fragColor; | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ layout (location = 1) in vec4 aColor; | |||
| 
 | ||||
| uniform mat4 uMVPMatrix; | ||||
| 
 | ||||
| out vec4 color; | ||||
| smooth out vec4 color; | ||||
| 
 | ||||
| void main() | ||||
| { | ||||
|  |  | |||
|  | @ -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; | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										65
									
								
								scwx-qt/gl/geo_texture2d.vert
									
										
									
									
									
										Normal 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
									
								
							
							
						
						|  | @ -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); | ||||
| } | ||||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										16
									
								
								scwx-qt/gl/texture2d_array.frag
									
										
									
									
									
										Normal 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
									
								
							
							
						
						|  | @ -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(); | ||||
|    } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								scwx-qt/res/fonts/Inconsolata-Regular.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										1
									
								
								scwx-qt/res/icons/font-awesome-6/angle-down-solid.svg
									
										
									
									
									
										Normal 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 | 
							
								
								
									
										1
									
								
								scwx-qt/res/icons/font-awesome-6/angle-up-solid.svg
									
										
									
									
									
										Normal 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 | 
							
								
								
									
										1
									
								
								scwx-qt/res/icons/font-awesome-6/angles-down-solid.svg
									
										
									
									
									
										Normal 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 | 
							
								
								
									
										1
									
								
								scwx-qt/res/icons/font-awesome-6/angles-up-solid.svg
									
										
									
									
									
										Normal 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 | 
|  | @ -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 | 
							
								
								
									
										1
									
								
								scwx-qt/res/icons/font-awesome-6/font-solid.svg
									
										
									
									
									
										Normal 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 | 
							
								
								
									
										1
									
								
								scwx-qt/res/icons/font-awesome-6/layer-group-solid.svg
									
										
									
									
									
										Normal 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 | 
|  | @ -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 | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
							
								
								
									
										22
									
								
								scwx-qt/source/scwx/qt/external/stb_image.cpp
									
										
									
									
										vendored
									
									
										Normal 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 | ||||
|  | @ -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( | ||||
|  |  | |||
|  | @ -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 Deinitialize()                                    = 0; | ||||
|    virtual void Render(const QMapLibreGL::CustomLayerRenderParameters& params); | ||||
|    virtual void Render(const QMapLibreGL::CustomLayerRenderParameters& params, | ||||
|                        bool textureAtlasChanged); | ||||
|    virtual void Deinitialize() = 0; | ||||
| 
 | ||||
|    /**
 | ||||
|     * @brief Run mouse picking on the draw item. | ||||
|     * | ||||
|     * @param [in] params Custom layer render parameters | ||||
|     * @param [in] mouseLocalPos Mouse cursor widget position | ||||
|     * @param [in] mouseGlobalPos Mouse cursor screen position | ||||
|     * @param [in] mouseCoords Mouse cursor location in map screen coordinates | ||||
|     * | ||||
|     * @return true if the draw item was picked, otherwise false | ||||
|     */ | ||||
|    virtual bool | ||||
|    RunMousePicking(const QMapLibreGL::CustomLayerRenderParameters& params, | ||||
|                    const QPointF&   mouseLocalPos, | ||||
|                    const QPointF&   mouseGlobalPos, | ||||
|                    const glm::vec2& mouseCoords); | ||||
| 
 | ||||
| protected: | ||||
|    void | ||||
|    UseDefaultProjection(const QMapLibreGL::CustomLayerRenderParameters& params, | ||||
|                         GLint uMVPMatrixLocation); | ||||
|    void | ||||
|    UseRotationProjection(const QMapLibreGL::CustomLayerRenderParameters& params, | ||||
|                          GLint uMVPMatrixLocation); | ||||
|    void UseMapProjection(const QMapLibreGL::CustomLayerRenderParameters& params, | ||||
|                          GLint uMVPMatrixLocation, | ||||
|                          GLint uMapScreenCoordLocation); | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
							
								
								
									
										788
									
								
								scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										80
									
								
								scwx-qt/source/scwx/qt/gl/draw/placefile_icons.hpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										487
									
								
								scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										62
									
								
								scwx-qt/source/scwx/qt/gl/draw/placefile_images.hpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										601
									
								
								scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										66
									
								
								scwx-qt/source/scwx/qt/gl/draw/placefile_lines.hpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										486
									
								
								scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										63
									
								
								scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.hpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										311
									
								
								scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										81
									
								
								scwx-qt/source/scwx/qt/gl/draw/placefile_text.hpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										351
									
								
								scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.cpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										62
									
								
								scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.hpp
									
										
									
									
									
										Normal 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
 | ||||
|  | @ -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,12 +56,36 @@ 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}; | ||||
|    std::shared_ptr<gl::ShaderProgram>        shaderProgram; | ||||
|    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
 | ||||
|  |  | |||
|  | @ -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(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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); | ||||
| 
 | ||||
|    if (!fragmentFile.isOpen()) | ||||
|    { | ||||
|       logger_->error("Could not load fragment shader: {}", fragmentPath); | ||||
|       return false; | ||||
|    } | ||||
|       QFile file(shader.second.c_str()); | ||||
|       file.open(QIODevice::ReadOnly | QIODevice::Text); | ||||
| 
 | ||||
|    QTextStream vertexShaderStream(&vertexFile); | ||||
|    QTextStream fragmentShaderStream(&fragmentFile); | ||||
|       if (!file.isOpen()) | ||||
|       { | ||||
|          logger_->error("Could not load shader"); | ||||
|          success = false; | ||||
|          break; | ||||
|       } | ||||
| 
 | ||||
|    vertexShaderStream.setEncoding(QStringConverter::Utf8); | ||||
|    fragmentShaderStream.setEncoding(QStringConverter::Utf8); | ||||
|       QTextStream shaderStream(&file); | ||||
|       shaderStream.setEncoding(QStringConverter::Utf8); | ||||
| 
 | ||||
|    std::string vertexShaderSource = vertexShaderStream.readAll().toStdString(); | ||||
|    std::string fragmentShaderSource = | ||||
|       fragmentShaderStream.readAll().toStdString(); | ||||
|       std::string shaderSource  = shaderStream.readAll().toStdString(); | ||||
|       const char* shaderSourceC = shaderSource.c_str(); | ||||
| 
 | ||||
|    const char* vertexShaderSourceC   = vertexShaderSource.c_str(); | ||||
|    const char* fragmentShaderSourceC = fragmentShaderSource.c_str(); | ||||
|       // Create a shader
 | ||||
|       GLuint shaderId = gl.glCreateShader(shader.first); | ||||
|       shaderIds.push_back(shaderId); | ||||
| 
 | ||||
|    // Create a vertex shader
 | ||||
|    GLuint vertexShader = gl.glCreateShader(GL_VERTEX_SHADER); | ||||
|       // Attach the shader source code and compile the shader
 | ||||
|       gl.glShaderSource(shaderId, 1, &shaderSourceC, NULL); | ||||
|       gl.glCompileShader(shaderId); | ||||
| 
 | ||||
|    // Attach the shader source code and compile the shader
 | ||||
|    gl.glShaderSource(vertexShader, 1, &vertexShaderSourceC, NULL); | ||||
|    gl.glCompileShader(vertexShader); | ||||
| 
 | ||||
|    // Check for errors
 | ||||
|    gl.glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &glSuccess); | ||||
|    gl.glGetShaderInfoLog(vertexShader, kInfoLogBufSize, &logLength, infoLog); | ||||
|    if (!glSuccess) | ||||
|    { | ||||
|       logger_->error("Vertex shader compilation failed: {}", infoLog); | ||||
|       success = false; | ||||
|    } | ||||
|    else if (logLength > 0) | ||||
|    { | ||||
|       logger_->error("Vertex 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); | ||||
|       // Check for errors
 | ||||
|       gl.glGetShaderiv(shaderId, GL_COMPILE_STATUS, &glSuccess); | ||||
|       gl.glGetShaderInfoLog(shaderId, kInfoLogBufSize, &logLength, infoLog); | ||||
|       if (!glSuccess) | ||||
|       { | ||||
|          logger_->error("Shader compilation failed: {}", infoLog); | ||||
|          success = false; | ||||
|          break; | ||||
|       } | ||||
|       else if (logLength > 0) | ||||
|       { | ||||
|          logger_->error("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; | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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>&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>&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>&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>&Layer Manager</string> | ||||
|    </property> | ||||
|   </action> | ||||
|   <action name="actionDumpLayerList"> | ||||
|    <property name="text"> | ||||
|     <string>Dump &Layer List</string> | ||||
|    </property> | ||||
|   </action> | ||||
|  </widget> | ||||
|  <resources> | ||||
|   <include location="../../../../scwx-qt.qrc"/> | ||||
|  |  | |||
							
								
								
									
										550
									
								
								scwx-qt/source/scwx/qt/manager/font_manager.cpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										53
									
								
								scwx-qt/source/scwx/qt/manager/font_manager.hpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										808
									
								
								scwx-qt/source/scwx/qt/manager/placefile_manager.cpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										63
									
								
								scwx-qt/source/scwx/qt/manager/placefile_manager.hpp
									
										
									
									
									
										Normal 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
 | ||||
|  | @ -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()) | ||||
|    util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); | ||||
|    return textureAtlas.CacheTexture(urlString, urlString); | ||||
| } | ||||
| 
 | ||||
| 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()) | ||||
|    { | ||||
|       return it->second; | ||||
|       util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); | ||||
|       textureAtlas.BuildAtlas(2048, 2048); | ||||
|    } | ||||
|    return -1; | ||||
| 
 | ||||
|    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
 | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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_); | ||||
| 
 | ||||
| static boost::json::value ConvertSettingsToJson(); | ||||
| static void               GenerateDefaultSettings(); | ||||
| static bool               LoadSettings(const boost::json::object& settingsJson); | ||||
| static void               ValidateSettings(); | ||||
| class SettingsManager::Impl | ||||
| { | ||||
| public: | ||||
|    explicit Impl(SettingsManager* self) : self_ {self} {} | ||||
|    ~Impl() = default; | ||||
| 
 | ||||
| static bool        initialized_ {false}; | ||||
| static std::string settingsPath_ {}; | ||||
|    void ValidateSettings(); | ||||
| 
 | ||||
| void Initialize() | ||||
|    static boost::json::value ConvertSettingsToJson(); | ||||
|    static void               GenerateDefaultSettings(); | ||||
|    static bool LoadSettings(const boost::json::object& settingsJson); | ||||
| 
 | ||||
|    SettingsManager* self_; | ||||
| 
 | ||||
|    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
 | ||||
|  |  | |||
|  | @ -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) | ||||
| 
 | ||||
| void Initialize(); | ||||
| void ReadSettings(const std::string& settingsPath); | ||||
| void SaveSettings(); | ||||
| void Shutdown(); | ||||
| public: | ||||
|    explicit SettingsManager(); | ||||
|    ~SettingsManager(); | ||||
| 
 | ||||
| settings::GeneralSettings& general_settings(); | ||||
| settings::MapSettings&     map_settings(); | ||||
| settings::PaletteSettings& palette_settings(); | ||||
|    void Initialize(); | ||||
|    void ReadSettings(const std::string& settingsPath); | ||||
|    void SaveSettings(); | ||||
|    void Shutdown(); | ||||
| 
 | ||||
|    static SettingsManager& Instance(); | ||||
| 
 | ||||
| signals: | ||||
|    void SettingsSaved(); | ||||
| 
 | ||||
| private: | ||||
|    class Impl; | ||||
|    std::unique_ptr<Impl> p; | ||||
| }; | ||||
| 
 | ||||
| } // namespace SettingsManager
 | ||||
| } // namespace manager
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
|  |  | |||
|  | @ -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) | ||||
| { | ||||
|    radarSweepMonitorCondition_.wait_for(lock, kRadarSweepMonitorTimeout_); | ||||
|    std::cv_status status = | ||||
|       radarSweepMonitorCondition_.wait_for(lock, kRadarSweepMonitorTimeout_); | ||||
|    if (status == std::cv_status::timeout) | ||||
|    { | ||||
|       logger_->debug("Radar sweep monitor timed out"); | ||||
|    } | ||||
|    radarSweepMonitorActive_ = false; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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,10 +23,11 @@ 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, | ||||
|                           awips::Phenomenon                 phenomenon, | ||||
|                           bool                              alertActive, | ||||
|                           const QString&                    beforeLayer); | ||||
| static std::vector<std::string> | ||||
| AddAlertLayer(std::shared_ptr<QMapLibreGL::Map> map, | ||||
|               awips::Phenomenon                 phenomenon, | ||||
|               bool                              alertActive, | ||||
|               const QString&                    beforeLayer); | ||||
| static QMapLibreGL::Feature | ||||
| CreateFeature(const awips::CodedLocation& codedLocation); | ||||
| static QMapLibreGL::Coordinate | ||||
|  | @ -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}) | ||||
|    { | ||||
|       for (bool alertActive : {false, true}) | ||||
|       { | ||||
|          p->UpdateSource(phenomenon, alertActive); | ||||
|          AddAlertLayer(map, phenomenon, alertActive, beforeLayer); | ||||
|       } | ||||
|       p->UpdateSource(phenomenon, alertActive); | ||||
|       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, | ||||
|                           awips::Phenomenon                 phenomenon, | ||||
|                           bool                              alertActive, | ||||
|                           const QString&                    beforeLayer) | ||||
| 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 | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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); | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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_; | ||||
|  |  | |||
|  | @ -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; | ||||
| 
 | ||||
|  |  | |||
|  | @ -23,13 +23,14 @@ public: | |||
| 
 | ||||
|    ~Impl() {} | ||||
| 
 | ||||
|    std::weak_ptr<QMapLibreGL::Map>         map_; | ||||
|    MapSettings                             settings_; | ||||
|    float                                   pixelRatio_; | ||||
|    std::shared_ptr<view::RadarProductView> radarProductView_; | ||||
|    common::RadarProductGroup               radarProductGroup_; | ||||
|    std::string                             radarProduct_; | ||||
|    int16_t                                 radarProductCode_; | ||||
|    std::weak_ptr<QMapLibreGL::Map>          map_; | ||||
|    MapSettings                              settings_; | ||||
|    float                                    pixelRatio_; | ||||
|    std::shared_ptr<view::RadarProductView>  radarProductView_; | ||||
|    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
 | ||||
|  |  | |||
|  | @ -26,13 +26,14 @@ public: | |||
|    MapContext(MapContext&&) noexcept; | ||||
|    MapContext& operator=(MapContext&&) noexcept; | ||||
| 
 | ||||
|    std::weak_ptr<QMapLibreGL::Map>         map() const; | ||||
|    MapSettings&                            settings(); | ||||
|    float                                   pixel_ratio() const; | ||||
|    std::shared_ptr<view::RadarProductView> radar_product_view() const; | ||||
|    common::RadarProductGroup               radar_product_group() const; | ||||
|    std::string                             radar_product() const; | ||||
|    int16_t                                 radar_product_code() const; | ||||
|    std::weak_ptr<QMapLibreGL::Map>          map() const; | ||||
|    MapSettings&                             settings(); | ||||
|    float                                    pixel_ratio() const; | ||||
|    std::shared_ptr<view::RadarProductView>  radar_product_view() const; | ||||
|    common::RadarProductGroup                radar_product_group() const; | ||||
|    std::string                              radar_product() const; | ||||
|    int16_t                                  radar_product_code() const; | ||||
|    QMapLibreGL::CustomLayerRenderParameters render_parameters() const; | ||||
| 
 | ||||
|    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; | ||||
|  |  | |||
|  | @ -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(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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()"); | ||||
|    logger_->info("Layers: {}", p->map_->layerIds().join(", ").toStdString()); | ||||
| } | ||||
| 
 | ||||
| std::string MapWidgetImpl::FindMapSymbologyLayer() | ||||
| { | ||||
|    std::string before = "ferry"; | ||||
| 
 | ||||
|    for (const QString& qlayer : styleLayers_) | ||||
|    { | ||||
|       const std::string layer = qlayer.toStdString(); | ||||
| 
 | ||||
|       // Draw below layers defined in map style
 | ||||
|       auto it = std::find_if( | ||||
|          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 != currentStyle_->drawBelow_.cend()) | ||||
|       { | ||||
|          before = layer; | ||||
|          break; | ||||
|       } | ||||
|    } | ||||
| 
 | ||||
|    return before; | ||||
| } | ||||
| 
 | ||||
| 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 : p->layerList_) | ||||
|    for (const std::string& id : layerList_) | ||||
|    { | ||||
|       p->map_->removeLayer(id.c_str()); | ||||
|       map_->removeLayer(id.c_str()); | ||||
|    } | ||||
|    p->layerList_.clear(); | ||||
|    layerList_.clear(); | ||||
|    placefileLayers_.clear(); | ||||
| 
 | ||||
|    auto radarProductView = p->context_->radar_product_view(); | ||||
|    // Update custom layer list from model
 | ||||
|    customLayers_ = model::LayerModel::Instance()->GetLayers(); | ||||
| 
 | ||||
|    if (radarProductView != nullptr) | ||||
|    // 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) | ||||
|    { | ||||
|       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()) | ||||
|       if (it->type_ == types::LayerType::Map) | ||||
|       { | ||||
|          const std::string layer = qlayer.toStdString(); | ||||
| 
 | ||||
|          // Draw below layers defined in map style
 | ||||
|          auto it = std::find_if( | ||||
|             mapStyle.drawBelow_.cbegin(), | ||||
|             mapStyle.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()) | ||||
|          // Style-defined map layers
 | ||||
|          switch (std::get<types::MapLayer>(it->description_)) | ||||
|          { | ||||
|             before = layer; | ||||
|          // 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; | ||||
|          } | ||||
|       } | ||||
| 
 | ||||
|       p->AddLayer("radar", p->radarProductLayer_, before); | ||||
|       RadarRangeLayer::Add(p->map_, | ||||
|                            radarProductView->range(), | ||||
|                            {radarSite->latitude(), radarSite->longitude()}); | ||||
|       p->AddLayer("colorTable", p->colorTableLayer_); | ||||
|       else if (it->displayed_[id_]) | ||||
|       { | ||||
|          // If the layer is displayed for the current map, add it
 | ||||
|          AddLayer(it->type_, it->description_, before); | ||||
|       } | ||||
|    } | ||||
| } | ||||
| 
 | ||||
|    p->alertLayer_->AddLayers("colorTable"); | ||||
|    p->overlayLayer_ = std::make_shared<OverlayLayer>(p->context_); | ||||
|    p->AddLayer("overlay", p->overlayLayer_); | ||||
| 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,9 +948,26 @@ void MapWidgetImpl::AddLayer(const std::string&            id, | |||
|    std::unique_ptr<QMapLibreGL::CustomLayerHostInterface> pHost = | ||||
|       std::make_unique<LayerWrapper>(layer); | ||||
| 
 | ||||
|    map_->addCustomLayer(id.c_str(), std::move(pHost), before.c_str()); | ||||
|    try | ||||
|    { | ||||
|       map_->addCustomLayer(id.c_str(), std::move(pHost), before.c_str()); | ||||
| 
 | ||||
|    layerList_.push_back(id); | ||||
|       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) | ||||
|  | @ -741,7 +994,8 @@ void MapWidget::keyPressEvent(QKeyEvent* ev) | |||
| 
 | ||||
| void MapWidget::mousePressEvent(QMouseEvent* ev) | ||||
| { | ||||
|    p->lastPos_ = ev->position(); | ||||
|    p->lastPos_       = ev->position(); | ||||
|    p->lastGlobalPos_ = ev->globalPosition(); | ||||
| 
 | ||||
|    if (ev->type() == QEvent::MouseButtonPress) | ||||
|    { | ||||
|  | @ -787,7 +1041,8 @@ void MapWidget::mouseMoveEvent(QMouseEvent* ev) | |||
|       } | ||||
|    } | ||||
| 
 | ||||
|    p->lastPos_ = ev->position(); | ||||
|    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(); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
							
								
								
									
										262
									
								
								scwx-qt/source/scwx/qt/map/placefile_layer.cpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										43
									
								
								scwx-qt/source/scwx/qt/map/placefile_layer.hpp
									
										
									
									
									
										Normal 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
 | ||||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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"}}, | ||||
|                  before); | ||||
|    map->setPaintProperty( | ||||
|       "rangeCircleLayer", "line-color", "rgba(128, 128, 128, 128)"); | ||||
|    map->addLayer( | ||||
|       {{"id", layerId}, {"type", "line"}, {"source", "rangeCircleSource"}}, | ||||
|       before); | ||||
|    map->setPaintProperty(layerId, "line-color", "rgba(128, 128, 128, 128)"); | ||||
| } | ||||
| 
 | ||||
| void RadarRangeLayer::Update(std::shared_ptr<QMapLibreGL::Map> map, | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
							
								
								
									
										1067
									
								
								scwx-qt/source/scwx/qt/model/layer_model.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										87
									
								
								scwx-qt/source/scwx/qt/model/layer_model.hpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										377
									
								
								scwx-qt/source/scwx/qt/model/placefile_model.cpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										60
									
								
								scwx-qt/source/scwx/qt/model/placefile_model.hpp
									
										
									
									
									
										Normal 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
 | ||||
|  | @ -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_ && | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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_; | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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,8 +121,8 @@ bool SettingsCategory::ReadJson(const boost::json::object& json) | |||
|    { | ||||
|       if (value == nullptr) | ||||
|       { | ||||
|          logger_->warn("Key {} is not present, resetting to defaults", | ||||
|                        p->name_); | ||||
|          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) | ||||
| { | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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,33 +324,36 @@ void SettingsInterface<T>::SetResetButton(QAbstractButton* button) | |||
| 
 | ||||
|    p->resetButton_ = button; | ||||
| 
 | ||||
|    QObject::connect(p->resetButton_, | ||||
|                     &QAbstractButton::clicked, | ||||
|                     p->context_.get(), | ||||
|                     [this]() | ||||
|                     { | ||||
|                        T defaultValue = p->variable_->GetDefault(); | ||||
| 
 | ||||
|                        if (p->variable_->GetValue() == defaultValue) | ||||
|    if (p->resetButton_ != nullptr) | ||||
|    { | ||||
|       QObject::connect(p->resetButton_, | ||||
|                        &QAbstractButton::clicked, | ||||
|                        p->context_.get(), | ||||
|                        [this]() | ||||
|                        { | ||||
|                           // If the current value is default, reset the staged
 | ||||
|                           // value
 | ||||
|                           p->variable_->Reset(); | ||||
|                           p->stagedValid_ = true; | ||||
|                           p->UpdateEditWidget(); | ||||
|                           p->UpdateResetButton(); | ||||
|                        } | ||||
|                        else | ||||
|                        { | ||||
|                           // Stage the default value
 | ||||
|                           p->stagedValid_ = | ||||
|                              p->variable_->StageValue(defaultValue); | ||||
|                           p->UpdateEditWidget(); | ||||
|                           p->UpdateResetButton(); | ||||
|                        } | ||||
|                     }); | ||||
|                           T defaultValue = p->variable_->GetDefault(); | ||||
| 
 | ||||
|    p->UpdateResetButton(); | ||||
|                           if (p->variable_->GetValue() == defaultValue) | ||||
|                           { | ||||
|                              // If the current value is default, reset the
 | ||||
|                              // staged value
 | ||||
|                              p->variable_->Reset(); | ||||
|                              p->stagedValid_ = true; | ||||
|                              p->UpdateEditWidget(); | ||||
|                              p->UpdateResetButton(); | ||||
|                           } | ||||
|                           else | ||||
|                           { | ||||
|                              // Stage the default value
 | ||||
|                              p->stagedValid_ = | ||||
|                                 p->variable_->StageValue(defaultValue); | ||||
|                              p->UpdateEditWidget(); | ||||
|                              p->UpdateResetButton(); | ||||
|                           } | ||||
|                        }); | ||||
| 
 | ||||
|       p->UpdateResetButton(); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
| template<class T> | ||||
|  | @ -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)); | ||||
|       } | ||||
|       else if constexpr (std::is_same_v<T, std::string>) | ||||
|       { | ||||
|          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(lineEdit, currentValue); | ||||
|    } | ||||
|    else if (QLabel* label = dynamic_cast<QLabel*>(editWidget_)) | ||||
|    { | ||||
|       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()); | ||||
|    } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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>; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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 | ||||
| { | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
							
								
								
									
										198
									
								
								scwx-qt/source/scwx/qt/settings/text_settings.cpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										52
									
								
								scwx-qt/source/scwx/qt/settings/text_settings.hpp
									
										
									
									
									
										Normal 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
 | ||||
|  | @ -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
 | ||||
|  |  | |||
							
								
								
									
										83
									
								
								scwx-qt/source/scwx/qt/types/imgui_font.cpp
									
										
									
									
									
										Normal 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
 | ||||
							
								
								
									
										41
									
								
								scwx-qt/source/scwx/qt/types/imgui_font.hpp
									
										
									
									
									
										Normal 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
 | ||||
 Dan Paulat
						Dan Paulat