diff --git a/.gitmodules b/.gitmodules index 880712a3..bfaddd87 100644 --- a/.gitmodules +++ b/.gitmodules @@ -31,3 +31,9 @@ [submodule "external/date"] path = external/date url = https://github.com/HowardHinnant/date.git +[submodule "external/units"] + path = external/units + url = https://github.com/nholthaus/units.git +[submodule "external/textflowcpp"] + path = external/textflowcpp + url = https://github.com/catchorg/textflowcpp.git diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index b5a5b93a..4e6fffa0 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -18,6 +18,7 @@ Supercell Wx uses code from the following dependencies: | [CSS Color Parser](https://github.com/deanm/css-color-parser-js) | [MIT License](https://spdx.org/licenses/MIT.html) | Ported to C++ for MapLibre Native | | [Date](https://github.com/HowardHinnant/date) | [MIT License](https://spdx.org/licenses/MIT.html) | | [Dear ImGui](https://github.com/ocornut/imgui) | [MIT License](https://spdx.org/licenses/MIT.html) | +| [fontconfig](http://fontconfig.org/) | [MIT License](https://spdx.org/licenses/MIT.html) | | [FreeType](https://freetype.org/) | [Freetype Project License](https://spdx.org/licenses/FTL.html) | | [FreeType GL](https://github.com/rougier/freetype-gl) | [BSD 2-Clause with views sentence](https://spdx.org/licenses/BSD-2-Clause-Views.html) | | [GeographicLib](https://geographiclib.sourceforge.io/) | [MIT License](https://spdx.org/licenses/MIT.html) | @@ -36,6 +37,8 @@ Supercell Wx uses code from the following dependencies: | [spdlog](https://github.com/gabime/spdlog) | [MIT License](https://spdx.org/licenses/MIT.html) | | [SQLite](https://www.sqlite.org/) | Public Domain | | [stb](https://github.com/nothings/stb) | Public Domain | +| [TextFlowCpp](https://github.com/catchorg/textflowcpp) | [Boost Software License 1.0](https://spdx.org/licenses/BSL-1.0.html) | +| [Units](https://github.com/nholthaus/units) | [MIT License](https://spdx.org/licenses/MIT.html) | | [Vulkan SDK](https://www.vulkan.org/) | [Apache License 2.0](https://spdx.org/licenses/Apache-2.0.html) | | [zlib](https://zlib.net/) | [zlib License](https://spdx.org/licenses/Zlib.html) | @@ -55,7 +58,9 @@ Supercell Wx uses assets from the following sources: | Source | License | Notes | | ------ | ------- | ----- | +| Alte DIN 1451 Mittelschrift | SIL Open Font License | | [Font Awesome Free](https://fontawesome.com/) | CC BY 4.0 License | +| [Inconsolata](https://fonts.google.com/specimen/Inconsolata) | SIL Open Font License | | [NOAA's Weather and Climate Toolkit](https://www.ncdc.noaa.gov/wct/) | Public Domain | Default Color Tables | | [Supercell thunderstorm with dramatic clouds](https://www.shutterstock.com/image-photo/supercell-thunderstorm-dramatic-clouds-1354353521) | Shutterstock Standard License | Photo by John Sirlin diff --git a/conanfile.py b/conanfile.py index 4a803580..af9b5b90 100644 --- a/conanfile.py +++ b/conanfile.py @@ -4,6 +4,7 @@ class SupercellWxConan(ConanFile): settings = ("os", "compiler", "build_type", "arch") requires = ("boost/1.81.0", "cpr/1.9.3", + "fontconfig/2.14.2", "freetype/2.12.1", "geographiclib/1.52", "glew/2.2.0", diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 65b2ef20..3c0be1cf 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -10,7 +10,9 @@ set_property(DIRECTORY hsluv-c.cmake imgui.cmake mapbox-gl-native.cmake - stb.cmake) + stb.cmake + textflowcpp.cmake + units.cmake) include(aws-sdk-cpp.cmake) include(date.cmake) @@ -19,3 +21,5 @@ include(hsluv-c.cmake) include(imgui.cmake) include(mapbox-gl-native.cmake) include(stb.cmake) +include(textflowcpp.cmake) +include(units.cmake) diff --git a/external/mapbox-gl-native b/external/mapbox-gl-native index fbb06ff5..3e85454f 160000 --- a/external/mapbox-gl-native +++ b/external/mapbox-gl-native @@ -1 +1 @@ -Subproject commit fbb06ff53e74d3a81b434b84fff1a5dfe4b2d3c7 +Subproject commit 3e85454fe5e571e7b235131912bb867ef9d75c3c diff --git a/external/stb b/external/stb index 8b5f1f37..5736b15f 160000 --- a/external/stb +++ b/external/stb @@ -1 +1 @@ -Subproject commit 8b5f1f37b5b75829fc72d38e7b5d4bcbf8a26d55 +Subproject commit 5736b15f7ea0ffb08dd38af21067c314d6a3aae9 diff --git a/external/textflowcpp b/external/textflowcpp new file mode 160000 index 00000000..12010ddc --- /dev/null +++ b/external/textflowcpp @@ -0,0 +1 @@ +Subproject commit 12010ddc8d15538ceea20622d22977e7c5a25da5 diff --git a/external/textflowcpp.cmake b/external/textflowcpp.cmake new file mode 100644 index 00000000..1e36da18 --- /dev/null +++ b/external/textflowcpp.cmake @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 3.20) +set(PROJECT_NAME scwx-textflowcpp) + +set(TEXTFLOWCPP_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/textflowcpp PARENT_SCOPE) diff --git a/external/units b/external/units new file mode 160000 index 00000000..da6dd917 --- /dev/null +++ b/external/units @@ -0,0 +1 @@ +Subproject commit da6dd9176e8515323c75030d5e51ee19cf6c9afd diff --git a/external/units.cmake b/external/units.cmake new file mode 100644 index 00000000..d037ae54 --- /dev/null +++ b/external/units.cmake @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 3.20) +set(PROJECT_NAME scwx-units) + +add_subdirectory(units) diff --git a/scwx-qt/gl/color.frag b/scwx-qt/gl/color.frag index 003c6c8d..1266306b 100644 --- a/scwx-qt/gl/color.frag +++ b/scwx-qt/gl/color.frag @@ -1,5 +1,5 @@ #version 330 core -in vec4 color; +smooth in vec4 color; layout (location = 0) out vec4 fragColor; diff --git a/scwx-qt/gl/color.vert b/scwx-qt/gl/color.vert index 5c8d1ff7..f849710c 100644 --- a/scwx-qt/gl/color.vert +++ b/scwx-qt/gl/color.vert @@ -4,7 +4,7 @@ layout (location = 1) in vec4 aColor; uniform mat4 uMVPMatrix; -out vec4 color; +smooth out vec4 color; void main() { diff --git a/scwx-qt/gl/geo_line.vert b/scwx-qt/gl/geo_line.vert index ea7d6351..487b8041 100644 --- a/scwx-qt/gl/geo_line.vert +++ b/scwx-qt/gl/geo_line.vert @@ -8,15 +8,15 @@ layout (location = 0) in vec2 aLatLong; layout (location = 1) in vec2 aXYOffset; -layout (location = 2) in vec2 aTexCoord; +layout (location = 2) in vec3 aTexCoord; layout (location = 3) in vec4 aModulate; uniform mat4 uMVPMatrix; uniform mat4 uMapMatrix; uniform vec2 uMapScreenCoord; -smooth out vec2 texCoord; -flat out vec4 modulate; +smooth out vec3 texCoord; +smooth out vec4 color; vec2 latLngToScreenCoordinate(in vec2 latLng) { @@ -31,7 +31,7 @@ void main() { // Pass the texture coordinate and color modulate to the fragment shader texCoord = aTexCoord; - modulate = aModulate; + color = aModulate; vec2 p = latLngToScreenCoordinate(aLatLong) - uMapScreenCoord; diff --git a/scwx-qt/gl/geo_texture2d.vert b/scwx-qt/gl/geo_texture2d.vert new file mode 100644 index 00000000..7977af95 --- /dev/null +++ b/scwx-qt/gl/geo_texture2d.vert @@ -0,0 +1,65 @@ +#version 330 core + +#define DEGREES_MAX 360.0f +#define LATITUDE_MAX 85.051128779806604f +#define LONGITUDE_MAX 180.0f +#define PI 3.1415926535897932384626433f +#define RAD2DEG 57.295779513082320876798156332941f +#define DEG2RAD 0.0174532925199432957692369055556f + +layout (location = 0) in vec2 aLatLong; +layout (location = 1) in vec2 aXYOffset; +layout (location = 2) in vec3 aTexCoord; +layout (location = 3) in vec4 aModulate; +layout (location = 4) in float aAngleDeg; +layout (location = 5) in int aThreshold; +layout (location = 6) in ivec2 aTimeRange; + +uniform mat4 uMVPMatrix; +uniform mat4 uMapMatrix; +uniform vec2 uMapScreenCoord; + +out VertexData +{ + int threshold; + vec3 texCoord; + vec4 color; + ivec2 timeRange; +} vsOut; + +smooth out vec3 texCoord; +smooth out vec4 color; + +vec2 latLngToScreenCoordinate(in vec2 latLng) +{ + vec2 p; + latLng.x = clamp(latLng.x, -LATITUDE_MAX, LATITUDE_MAX); + p.xy = vec2(LONGITUDE_MAX + latLng.y, + -(LONGITUDE_MAX - RAD2DEG * log(tan(PI / 4 + latLng.x * PI / DEGREES_MAX)))); + return p; +} + +void main() +{ + // Pass the threshold and time range to the geometry shader + vsOut.threshold = aThreshold; + vsOut.timeRange = aTimeRange; + + // Pass the texture coordinate and color modulate to the geometry and + // fragment shaders + vsOut.texCoord = aTexCoord; + vsOut.color = aModulate; + texCoord = aTexCoord; + color = aModulate; + + vec2 p = latLngToScreenCoordinate(aLatLong) - uMapScreenCoord; + + // Rotate clockwise + float angle = aAngleDeg * DEG2RAD; + mat2 rotate = mat2(cos(angle), -sin(angle), + sin(angle), cos(angle)); + + // Transform the position to screen coordinates + gl_Position = uMapMatrix * vec4(p, 0.0f, 1.0f) + + uMVPMatrix * vec4(rotate * aXYOffset, 0.0f, 0.0f); +} diff --git a/scwx-qt/gl/map_color.vert b/scwx-qt/gl/map_color.vert new file mode 100644 index 00000000..4319310f --- /dev/null +++ b/scwx-qt/gl/map_color.vert @@ -0,0 +1,38 @@ +#version 330 core + +layout (location = 0) in vec2 aScreenCoord; +layout (location = 1) in vec2 aXYOffset; +layout (location = 2) in vec4 aColor; +layout (location = 3) in int aThreshold; +layout (location = 4) in ivec2 aTimeRange; + +uniform mat4 uMVPMatrix; +uniform mat4 uMapMatrix; +uniform vec2 uMapScreenCoord; + +out VertexData +{ + int threshold; + vec3 texCoord; + vec4 color; + ivec2 timeRange; +} vsOut; + +smooth out vec4 color; + +void main() +{ + // Pass the threshold and time range to the geometry shader + vsOut.threshold = aThreshold; + vsOut.timeRange = aTimeRange; + + // Pass the color to the geometry and fragment shaders + vsOut.color = aColor; + color = aColor; + + vec2 p = aScreenCoord - uMapScreenCoord; + + // Transform the position to screen coordinates + gl_Position = uMapMatrix * vec4(p, 0.0f, 1.0f) + + uMVPMatrix * vec4(aXYOffset, 0.0f, 0.0f); +} diff --git a/scwx-qt/gl/texture2d.frag b/scwx-qt/gl/texture2d.frag index 16ab8960..725d66ad 100644 --- a/scwx-qt/gl/texture2d.frag +++ b/scwx-qt/gl/texture2d.frag @@ -6,11 +6,11 @@ precision mediump float; uniform sampler2D uTexture; smooth in vec2 texCoord; -flat in vec4 modulate; +smooth in vec4 color; layout (location = 0) out vec4 fragColor; void main() { - fragColor = texture(uTexture, texCoord) * modulate; + fragColor = texture(uTexture, texCoord) * color; } diff --git a/scwx-qt/gl/texture2d_array.frag b/scwx-qt/gl/texture2d_array.frag new file mode 100644 index 00000000..b73bfda8 --- /dev/null +++ b/scwx-qt/gl/texture2d_array.frag @@ -0,0 +1,16 @@ +#version 330 core + +// Lower the default precision to medium +precision mediump float; + +uniform sampler2DArray uTexture; + +smooth in vec3 texCoord; +smooth in vec4 color; + +layout (location = 0) out vec4 fragColor; + +void main() +{ + fragColor = texture(uTexture, texCoord) * color; +} diff --git a/scwx-qt/gl/threshold.geom b/scwx-qt/gl/threshold.geom new file mode 100644 index 00000000..dec09b01 --- /dev/null +++ b/scwx-qt/gl/threshold.geom @@ -0,0 +1,46 @@ +#version 330 core + +layout (triangles) in; +layout (triangle_strip, max_vertices = 3) out; + +uniform float uMapDistance; +uniform int uSelectedTime; + +in VertexData +{ + int threshold; + vec3 texCoord; + vec4 color; + ivec2 timeRange; +} gsIn[]; + +smooth out vec3 texCoord; +smooth out vec4 color; + +void main() +{ + if ((gsIn[0].threshold <= 0 || // If Threshold: 0 was specified, no threshold + gsIn[0].threshold >= uMapDistance || // If Threshold is above current map distance + gsIn[0].threshold >= 999) && // If Threshold: 999 was specified (or greater), no threshold + (gsIn[0].timeRange[0] == 0 || // If there is no start time specified + (gsIn[0].timeRange[0] <= uSelectedTime && // If the selected time is after the start time + uSelectedTime < gsIn[0].timeRange[1]))) // If the selected time is before the end time + { + gl_Position = gl_in[0].gl_Position; + texCoord = gsIn[0].texCoord; + color = gsIn[0].color; + EmitVertex(); + + gl_Position = gl_in[1].gl_Position; + texCoord = gsIn[1].texCoord; + color = gsIn[1].color; + + EmitVertex(); + gl_Position = gl_in[2].gl_Position; + texCoord = gsIn[2].texCoord; + color = gsIn[2].color; + + EmitVertex(); + EndPrimitive(); + } +} diff --git a/scwx-qt/res/fonts/Inconsolata-Regular.ttf b/scwx-qt/res/fonts/Inconsolata-Regular.ttf new file mode 100644 index 00000000..0d879bf3 Binary files /dev/null and b/scwx-qt/res/fonts/Inconsolata-Regular.ttf differ diff --git a/scwx-qt/res/icons/font-awesome-6/angle-down-solid.svg b/scwx-qt/res/icons/font-awesome-6/angle-down-solid.svg new file mode 100644 index 00000000..c877d491 --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/angle-down-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scwx-qt/res/icons/font-awesome-6/angle-up-solid.svg b/scwx-qt/res/icons/font-awesome-6/angle-up-solid.svg new file mode 100644 index 00000000..da43225c --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/angle-up-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scwx-qt/res/icons/font-awesome-6/angles-down-solid.svg b/scwx-qt/res/icons/font-awesome-6/angles-down-solid.svg new file mode 100644 index 00000000..68671b02 --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/angles-down-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scwx-qt/res/icons/font-awesome-6/angles-up-solid.svg b/scwx-qt/res/icons/font-awesome-6/angles-up-solid.svg new file mode 100644 index 00000000..74692e85 --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/angles-up-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scwx-qt/res/icons/font-awesome-6/earth-americas-solid.svg b/scwx-qt/res/icons/font-awesome-6/earth-americas-solid.svg new file mode 100644 index 00000000..f427a71e --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/earth-americas-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scwx-qt/res/icons/font-awesome-6/font-solid.svg b/scwx-qt/res/icons/font-awesome-6/font-solid.svg new file mode 100644 index 00000000..2fa4599c --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/font-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scwx-qt/res/icons/font-awesome-6/layer-group-solid.svg b/scwx-qt/res/icons/font-awesome-6/layer-group-solid.svg new file mode 100644 index 00000000..a6366610 --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/layer-group-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 3cae5e6c..50767c34 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -12,6 +12,7 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Boost) +find_package(Fontconfig) find_package(Freetype) find_package(geographiclib) find_package(glm) @@ -46,7 +47,8 @@ set(HDR_CONFIG source/scwx/qt/config/county_database.hpp source/scwx/qt/config/radar_site.hpp) set(SRC_CONFIG source/scwx/qt/config/county_database.cpp source/scwx/qt/config/radar_site.cpp) -set(SRC_EXTERNAL source/scwx/qt/external/stb_rect_pack.cpp) +set(SRC_EXTERNAL source/scwx/qt/external/stb_image.cpp + source/scwx/qt/external/stb_rect_pack.cpp) set(HDR_GL source/scwx/qt/gl/gl.hpp source/scwx/qt/gl/gl_context.hpp source/scwx/qt/gl/shader_program.hpp @@ -56,18 +58,34 @@ set(SRC_GL source/scwx/qt/gl/gl_context.cpp source/scwx/qt/gl/text_shader.cpp) set(HDR_GL_DRAW source/scwx/qt/gl/draw/draw_item.hpp source/scwx/qt/gl/draw/geo_line.hpp + source/scwx/qt/gl/draw/placefile_icons.hpp + source/scwx/qt/gl/draw/placefile_images.hpp + source/scwx/qt/gl/draw/placefile_lines.hpp + source/scwx/qt/gl/draw/placefile_polygons.hpp + source/scwx/qt/gl/draw/placefile_text.hpp + source/scwx/qt/gl/draw/placefile_triangles.hpp source/scwx/qt/gl/draw/rectangle.hpp) set(SRC_GL_DRAW source/scwx/qt/gl/draw/draw_item.cpp source/scwx/qt/gl/draw/geo_line.cpp + source/scwx/qt/gl/draw/placefile_icons.cpp + source/scwx/qt/gl/draw/placefile_images.cpp + source/scwx/qt/gl/draw/placefile_lines.cpp + source/scwx/qt/gl/draw/placefile_polygons.cpp + source/scwx/qt/gl/draw/placefile_text.cpp + source/scwx/qt/gl/draw/placefile_triangles.cpp source/scwx/qt/gl/draw/rectangle.cpp) -set(HDR_MANAGER source/scwx/qt/manager/radar_product_manager.hpp +set(HDR_MANAGER source/scwx/qt/manager/font_manager.hpp + source/scwx/qt/manager/placefile_manager.hpp + source/scwx/qt/manager/radar_product_manager.hpp source/scwx/qt/manager/radar_product_manager_notifier.hpp source/scwx/qt/manager/resource_manager.hpp source/scwx/qt/manager/settings_manager.hpp source/scwx/qt/manager/text_event_manager.hpp source/scwx/qt/manager/timeline_manager.hpp source/scwx/qt/manager/update_manager.hpp) -set(SRC_MANAGER source/scwx/qt/manager/radar_product_manager.cpp +set(SRC_MANAGER source/scwx/qt/manager/font_manager.cpp + source/scwx/qt/manager/placefile_manager.cpp + source/scwx/qt/manager/radar_product_manager.cpp source/scwx/qt/manager/radar_product_manager_notifier.cpp source/scwx/qt/manager/resource_manager.cpp source/scwx/qt/manager/settings_manager.cpp @@ -84,6 +102,7 @@ set(HDR_MAP source/scwx/qt/map/alert_layer.hpp source/scwx/qt/map/map_settings.hpp source/scwx/qt/map/map_widget.hpp source/scwx/qt/map/overlay_layer.hpp + source/scwx/qt/map/placefile_layer.hpp source/scwx/qt/map/radar_product_layer.hpp source/scwx/qt/map/radar_range_layer.hpp) set(SRC_MAP source/scwx/qt/map/alert_layer.cpp @@ -95,11 +114,14 @@ set(SRC_MAP source/scwx/qt/map/alert_layer.cpp source/scwx/qt/map/map_provider.cpp source/scwx/qt/map/map_widget.cpp source/scwx/qt/map/overlay_layer.cpp + source/scwx/qt/map/placefile_layer.cpp source/scwx/qt/map/radar_product_layer.cpp source/scwx/qt/map/radar_range_layer.cpp) set(HDR_MODEL source/scwx/qt/model/alert_model.hpp source/scwx/qt/model/alert_proxy_model.hpp source/scwx/qt/model/imgui_context_model.hpp + source/scwx/qt/model/layer_model.hpp + source/scwx/qt/model/placefile_model.hpp source/scwx/qt/model/radar_product_model.hpp source/scwx/qt/model/radar_site_model.hpp source/scwx/qt/model/tree_item.hpp @@ -107,6 +129,8 @@ set(HDR_MODEL source/scwx/qt/model/alert_model.hpp set(SRC_MODEL source/scwx/qt/model/alert_model.cpp source/scwx/qt/model/alert_proxy_model.cpp source/scwx/qt/model/imgui_context_model.cpp + source/scwx/qt/model/layer_model.cpp + source/scwx/qt/model/placefile_model.cpp source/scwx/qt/model/radar_product_model.cpp source/scwx/qt/model/radar_site_model.cpp source/scwx/qt/model/tree_item.cpp @@ -122,6 +146,7 @@ set(HDR_SETTINGS source/scwx/qt/settings/general_settings.hpp source/scwx/qt/settings/settings_interface_base.hpp source/scwx/qt/settings/settings_variable.hpp source/scwx/qt/settings/settings_variable_base.hpp + source/scwx/qt/settings/text_settings.hpp source/scwx/qt/settings/ui_settings.hpp) set(SRC_SETTINGS source/scwx/qt/settings/general_settings.cpp source/scwx/qt/settings/map_settings.cpp @@ -132,19 +157,26 @@ set(SRC_SETTINGS source/scwx/qt/settings/general_settings.cpp source/scwx/qt/settings/settings_interface_base.cpp source/scwx/qt/settings/settings_variable.cpp source/scwx/qt/settings/settings_variable_base.cpp + source/scwx/qt/settings/text_settings.cpp source/scwx/qt/settings/ui_settings.cpp) set(HDR_TYPES source/scwx/qt/types/alert_types.hpp source/scwx/qt/types/font_types.hpp source/scwx/qt/types/github_types.hpp + source/scwx/qt/types/imgui_font.hpp + source/scwx/qt/types/layer_types.hpp source/scwx/qt/types/map_types.hpp source/scwx/qt/types/qt_types.hpp source/scwx/qt/types/radar_product_record.hpp - source/scwx/qt/types/text_event_key.hpp) + source/scwx/qt/types/text_event_key.hpp + source/scwx/qt/types/text_types.hpp) set(SRC_TYPES source/scwx/qt/types/alert_types.cpp source/scwx/qt/types/github_types.cpp + source/scwx/qt/types/imgui_font.cpp + source/scwx/qt/types/layer_types.cpp source/scwx/qt/types/map_types.cpp source/scwx/qt/types/radar_product_record.cpp - source/scwx/qt/types/text_event_key.cpp) + source/scwx/qt/types/text_event_key.cpp + source/scwx/qt/types/text_types.cpp) set(HDR_UI source/scwx/qt/ui/about_dialog.hpp source/scwx/qt/ui/alert_dialog.hpp source/scwx/qt/ui/alert_dock_widget.hpp @@ -153,9 +185,14 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp source/scwx/qt/ui/flow_layout.hpp source/scwx/qt/ui/imgui_debug_dialog.hpp source/scwx/qt/ui/imgui_debug_widget.hpp + source/scwx/qt/ui/layer_dialog.hpp + source/scwx/qt/ui/left_elided_item_delegate.hpp source/scwx/qt/ui/level2_products_widget.hpp source/scwx/qt/ui/level2_settings_widget.hpp source/scwx/qt/ui/level3_products_widget.hpp + source/scwx/qt/ui/open_url_dialog.hpp + source/scwx/qt/ui/placefile_dialog.hpp + source/scwx/qt/ui/placefile_settings_widget.hpp source/scwx/qt/ui/radar_site_dialog.hpp source/scwx/qt/ui/settings_dialog.hpp source/scwx/qt/ui/update_dialog.hpp) @@ -167,9 +204,14 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp source/scwx/qt/ui/flow_layout.cpp source/scwx/qt/ui/imgui_debug_dialog.cpp source/scwx/qt/ui/imgui_debug_widget.cpp + source/scwx/qt/ui/layer_dialog.cpp + source/scwx/qt/ui/left_elided_item_delegate.cpp source/scwx/qt/ui/level2_products_widget.cpp source/scwx/qt/ui/level2_settings_widget.cpp source/scwx/qt/ui/level3_products_widget.cpp + source/scwx/qt/ui/open_url_dialog.cpp + source/scwx/qt/ui/placefile_dialog.cpp + source/scwx/qt/ui/placefile_settings_widget.cpp source/scwx/qt/ui/radar_site_dialog.cpp source/scwx/qt/ui/settings_dialog.cpp source/scwx/qt/ui/update_dialog.cpp) @@ -179,6 +221,10 @@ set(UI_UI source/scwx/qt/ui/about_dialog.ui source/scwx/qt/ui/animation_dock_widget.ui source/scwx/qt/ui/collapsible_group.ui source/scwx/qt/ui/imgui_debug_dialog.ui + source/scwx/qt/ui/layer_dialog.ui + source/scwx/qt/ui/open_url_dialog.ui + source/scwx/qt/ui/placefile_dialog.ui + source/scwx/qt/ui/placefile_settings_widget.ui source/scwx/qt/ui/radar_site_dialog.ui source/scwx/qt/ui/settings_dialog.ui source/scwx/qt/ui/update_dialog.ui) @@ -187,22 +233,30 @@ set(HDR_UTIL source/scwx/qt/util/color.hpp source/scwx/qt/util/font.hpp source/scwx/qt/util/font_buffer.hpp source/scwx/qt/util/geographic_lib.hpp + source/scwx/qt/util/imgui.hpp source/scwx/qt/util/json.hpp + source/scwx/qt/util/maplibre.hpp + source/scwx/qt/util/network.hpp source/scwx/qt/util/streams.hpp source/scwx/qt/util/texture_atlas.hpp source/scwx/qt/util/q_file_buffer.hpp source/scwx/qt/util/q_file_input_stream.hpp - source/scwx/qt/util/time.hpp) + source/scwx/qt/util/time.hpp + source/scwx/qt/util/tooltip.hpp) set(SRC_UTIL source/scwx/qt/util/color.cpp source/scwx/qt/util/file.cpp source/scwx/qt/util/font.cpp source/scwx/qt/util/font_buffer.cpp source/scwx/qt/util/geographic_lib.cpp + source/scwx/qt/util/imgui.cpp source/scwx/qt/util/json.cpp + source/scwx/qt/util/maplibre.cpp + source/scwx/qt/util/network.cpp source/scwx/qt/util/texture_atlas.cpp source/scwx/qt/util/q_file_buffer.cpp source/scwx/qt/util/q_file_input_stream.cpp - source/scwx/qt/util/time.cpp) + source/scwx/qt/util/time.cpp + source/scwx/qt/util/tooltip.cpp) set(HDR_VIEW source/scwx/qt/view/level2_product_view.hpp source/scwx/qt/view/level3_product_view.hpp source/scwx/qt/view/level3_radial_view.hpp @@ -221,13 +275,17 @@ set(RESOURCE_FILES scwx-qt.qrc) set(SHADER_FILES gl/color.frag gl/color.vert gl/geo_line.vert + gl/geo_texture2d.vert + gl/map_color.vert gl/radar.frag gl/radar.vert gl/text.frag gl/text.vert gl/texture1d.frag gl/texture1d.vert - gl/texture2d.frag) + gl/texture2d.frag + gl/texture2d_array.frag + gl/threshold.geom) set(CMAKE_FILES scwx-qt.cmake) @@ -386,7 +444,8 @@ target_include_directories(scwx-qt PUBLIC ${scwx-qt_SOURCE_DIR}/source ${FTGL_INCLUDE_DIR} ${IMGUI_INCLUDE_DIRS} ${MBGL_INCLUDE_DIR} - ${STB_INCLUDE_DIR}) + ${STB_INCLUDE_DIR} + ${TEXTFLOWCPP_INCLUDE_DIR}) target_include_directories(supercell-wx PUBLIC ${scwx-qt_SOURCE_DIR}/source) @@ -432,6 +491,7 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets Boost::timer qmaplibregl $<$:opengl32> + Fontconfig::Fontconfig freetype-gl GeographicLib::GeographicLib glm::glm diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc index 3939ddf4..10d8b0a2 100644 --- a/scwx-qt/scwx-qt.qrc +++ b/scwx-qt/scwx-qt.qrc @@ -3,6 +3,8 @@ gl/color.frag gl/color.vert gl/geo_line.vert + gl/geo_texture2d.vert + gl/map_color.vert gl/radar.frag gl/radar.vert gl/text.frag @@ -10,19 +12,29 @@ gl/texture1d.frag gl/texture1d.vert gl/texture2d.frag + gl/texture2d_array.frag + gl/threshold.geom res/config/radar_sites.json res/fonts/din1451alt.ttf res/fonts/din1451alt_g.ttf + res/fonts/Inconsolata-Regular.ttf res/icons/scwx-256.ico res/icons/scwx-256.png + res/icons/font-awesome-6/angle-down-solid.svg res/icons/font-awesome-6/angle-left-solid.svg res/icons/font-awesome-6/angle-right-solid.svg + res/icons/font-awesome-6/angle-up-solid.svg + res/icons/font-awesome-6/angles-down-solid.svg + res/icons/font-awesome-6/angles-up-solid.svg res/icons/font-awesome-6/backward-step-solid.svg res/icons/font-awesome-6/book-solid.svg res/icons/font-awesome-6/discord.svg + res/icons/font-awesome-6/earth-americas-solid.svg + res/icons/font-awesome-6/font-solid.svg res/icons/font-awesome-6/forward-step-solid.svg res/icons/font-awesome-6/gears-solid.svg res/icons/font-awesome-6/github.svg + res/icons/font-awesome-6/layer-group-solid.svg res/icons/font-awesome-6/palette-solid.svg res/icons/font-awesome-6/pause-solid.svg res/icons/font-awesome-6/play-solid.svg diff --git a/scwx-qt/source/scwx/qt/external/stb_image.cpp b/scwx-qt/source/scwx/qt/external/stb_image.cpp new file mode 100644 index 00000000..f611b488 --- /dev/null +++ b/scwx-qt/source/scwx/qt/external/stb_image.cpp @@ -0,0 +1,22 @@ +#define STB_IMAGE_IMPLEMENTATION +#define STBI_ASSERT(x) +#define STBI_FAILURE_USERMSG + +#if defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#endif + +#if defined(_MSC_VER) +# pragma warning(push, 0) +#endif + +#include + +#if defined(__GNUC__) +# pragma GCC diagnostic pop +#endif + +#if defined(_MSC_VER) +# pragma warning(pop) +#endif diff --git a/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp b/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp index b2c92279..21dab7b0 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp @@ -1,4 +1,5 @@ #include +#include #include @@ -41,6 +42,27 @@ DrawItem::~DrawItem() = default; DrawItem::DrawItem(DrawItem&&) noexcept = default; DrawItem& DrawItem::operator=(DrawItem&&) noexcept = default; +void DrawItem::Render( + const QMapLibreGL::CustomLayerRenderParameters& /* params */) +{ +} + +void DrawItem::Render(const QMapLibreGL::CustomLayerRenderParameters& params, + bool /* textureAtlasChanged */) +{ + Render(params); +} + +bool DrawItem::RunMousePicking( + const QMapLibreGL::CustomLayerRenderParameters& /* params */, + const QPointF& /* mouseLocalPos */, + const QPointF& /* mouseGlobalPos */, + const glm::vec2& /* mouseCoords */) +{ + // By default, the draw item is not picked + return false; +} + void DrawItem::UseDefaultProjection( const QMapLibreGL::CustomLayerRenderParameters& params, GLint uMVPMatrixLocation) @@ -54,21 +76,21 @@ void DrawItem::UseDefaultProjection( uMVPMatrixLocation, 1, GL_FALSE, glm::value_ptr(projection)); } -// TODO: Refactor to utility class -static glm::vec2 -LatLongToScreenCoordinate(const QMapLibreGL::Coordinate& coordinate) +void DrawItem::UseRotationProjection( + const QMapLibreGL::CustomLayerRenderParameters& params, + GLint uMVPMatrixLocation) { - static constexpr double RAD2DEG_D = 180.0 / M_PI; + glm::mat4 projection = glm::ortho(0.0f, + static_cast(params.width), + 0.0f, + static_cast(params.height)); - double latitude = std::clamp( - coordinate.first, -mbgl::util::LATITUDE_MAX, mbgl::util::LATITUDE_MAX); - glm::vec2 screen { - mbgl::util::LONGITUDE_MAX + coordinate.second, - -(mbgl::util::LONGITUDE_MAX - - RAD2DEG_D * - std::log(std::tan(M_PI / 4.0 + - latitude * M_PI / mbgl::util::DEGREES_MAX)))}; - return screen; + projection = glm::rotate(projection, + glm::radians(params.bearing), + glm::vec3(0.0f, 0.0f, 1.0f)); + + p->gl_.glUniformMatrix4fv( + uMVPMatrixLocation, 1, GL_FALSE, glm::value_ptr(projection)); } void DrawItem::UseMapProjection( @@ -78,21 +100,11 @@ void DrawItem::UseMapProjection( { OpenGLFunctions& gl = p->gl_; - // TODO: Refactor to utility class - const float scale = std::pow(2.0, params.zoom) * 2.0f * - mbgl::util::tileSize_D / mbgl::util::DEGREES_MAX; - const float xScale = scale / params.width; - const float yScale = scale / params.height; - - glm::mat4 uMVPMatrix(1.0f); - uMVPMatrix = glm::scale(uMVPMatrix, glm::vec3(xScale, yScale, 1.0f)); - uMVPMatrix = glm::rotate(uMVPMatrix, - glm::radians(params.bearing), - glm::vec3(0.0f, 0.0f, 1.0f)); + const glm::mat4 uMVPMatrix = util::maplibre::GetMapMatrix(params); gl.glUniform2fv(uMapScreenCoordLocation, 1, - glm::value_ptr(LatLongToScreenCoordinate( + glm::value_ptr(util::maplibre::LatLongToScreenCoordinate( {params.latitude, params.longitude}))); gl.glUniformMatrix4fv( diff --git a/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp b/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp index de94ebe7..b4d7d4ce 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp +++ b/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp @@ -5,6 +5,7 @@ #include #include +#include namespace scwx { @@ -28,14 +29,34 @@ public: DrawItem& operator=(DrawItem&&) noexcept; virtual void Initialize() = 0; - virtual void - Render(const QMapLibreGL::CustomLayerRenderParameters& params) = 0; - virtual void Deinitialize() = 0; + virtual void Render(const QMapLibreGL::CustomLayerRenderParameters& params); + virtual void Render(const QMapLibreGL::CustomLayerRenderParameters& params, + bool textureAtlasChanged); + virtual void Deinitialize() = 0; + + /** + * @brief Run mouse picking on the draw item. + * + * @param [in] params Custom layer render parameters + * @param [in] mouseLocalPos Mouse cursor widget position + * @param [in] mouseGlobalPos Mouse cursor screen position + * @param [in] mouseCoords Mouse cursor location in map screen coordinates + * + * @return true if the draw item was picked, otherwise false + */ + virtual bool + RunMousePicking(const QMapLibreGL::CustomLayerRenderParameters& params, + const QPointF& mouseLocalPos, + const QPointF& mouseGlobalPos, + const glm::vec2& mouseCoords); protected: void UseDefaultProjection(const QMapLibreGL::CustomLayerRenderParameters& params, GLint uMVPMatrixLocation); + void + UseRotationProjection(const QMapLibreGL::CustomLayerRenderParameters& params, + GLint uMVPMatrixLocation); void UseMapProjection(const QMapLibreGL::CustomLayerRenderParameters& params, GLint uMVPMatrixLocation, GLint uMapScreenCoordLocation); diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_line.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_line.cpp index a009f6bb..17d84a79 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_line.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_line.cpp @@ -23,10 +23,12 @@ static constexpr size_t kNumRectangles = 1; static constexpr size_t kNumTriangles = kNumRectangles * 2; static constexpr size_t kVerticesPerTriangle = 3; static constexpr size_t kVerticesPerRectangle = kVerticesPerTriangle * 2; -static constexpr size_t kPointsPerVertex = 10; +static constexpr size_t kPointsPerVertex = 11; static constexpr size_t kBufferLength = kNumTriangles * kVerticesPerTriangle * kPointsPerVertex; +static const std::string kTextureName = "lines/default-1x7"; + class GeoLine::Impl { public: @@ -90,8 +92,8 @@ void GeoLine::Initialize() { gl::OpenGLFunctions& gl = p->context_->gl(); - p->shaderProgram_ = p->context_->GetShaderProgram(":/gl/geo_line.vert", - ":/gl/texture2d.frag"); + p->shaderProgram_ = p->context_->GetShaderProgram( + ":/gl/geo_line.vert", ":/gl/texture2d_array.frag"); p->uMVPMatrixLocation_ = gl.glGetUniformLocation(p->shaderProgram_->id(), "uMVPMatrix"); @@ -115,7 +117,7 @@ void GeoLine::Initialize() } p->texture_ = - util::TextureAtlas::Instance().GetTextureAttributes("lines/default-1x7"); + util::TextureAtlas::Instance().GetTextureAttributes(kTextureName); gl.glGenVertexArrays(1, &p->vao_); gl.glGenBuffers(1, &p->vbo_); @@ -145,7 +147,7 @@ void GeoLine::Initialize() // aTexCoord gl.glVertexAttribPointer(2, - 2, + 3, GL_FLOAT, GL_FALSE, kPointsPerVertex * sizeof(float), @@ -158,7 +160,7 @@ void GeoLine::Initialize() GL_FLOAT, GL_FALSE, kPointsPerVertex * sizeof(float), - reinterpret_cast(6 * sizeof(float))); + reinterpret_cast(7 * sizeof(float))); gl.glEnableVertexAttribArray(3); p->dirty_ = true; @@ -248,6 +250,9 @@ void GeoLine::Impl::Update() { gl::OpenGLFunctions& gl = context_->gl(); + texture_ = + util::TextureAtlas::Instance().GetTextureAttributes(kTextureName); + // Latitude and longitude coordinates in degrees const float lx = points_[0].latitude_; const float rx = points_[1].latitude_; @@ -259,6 +264,8 @@ void GeoLine::Impl::Update() const float oy = width_ * 0.5f * sinf(angle_); // Texture coordinates + static constexpr float r = 0.0f; + const float ls = texture_.sLeft_; const float rs = texture_.sRight_; const float tt = texture_.tTop_; @@ -284,12 +291,12 @@ void GeoLine::Impl::Update() { // // Line { - {lx, by, -ox, -oy, ls, bt, mc0, mc1, mc2, mc3}, // BL - {lx, by, +ox, +oy, ls, tt, mc0, mc1, mc2, mc3}, // TL - {rx, ty, -ox, -oy, rs, bt, mc0, mc1, mc2, mc3}, // BR - {rx, ty, -ox, -oy, rs, bt, mc0, mc1, mc2, mc3}, // BR - {rx, ty, +ox, +oy, rs, tt, mc0, mc1, mc2, mc3}, // TR - {lx, by, +ox, +oy, ls, tt, mc0, mc1, mc2, mc3} // TL + {lx, by, -ox, -oy, ls, bt, r, mc0, mc1, mc2, mc3}, // BL + {lx, by, +ox, +oy, ls, tt, r, mc0, mc1, mc2, mc3}, // TL + {rx, ty, -ox, -oy, rs, bt, r, mc0, mc1, mc2, mc3}, // BR + {rx, ty, -ox, -oy, rs, bt, r, mc0, mc1, mc2, mc3}, // BR + {rx, ty, +ox, +oy, rs, tt, r, mc0, mc1, mc2, mc3}, // TR + {lx, by, +ox, +oy, ls, tt, r, mc0, mc1, mc2, mc3} // TL }}; gl.glBufferData(GL_ARRAY_BUFFER, diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp new file mode 100644 index 00000000..36c792db --- /dev/null +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp @@ -0,0 +1,788 @@ +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace gl +{ +namespace draw +{ + +static const std::string logPrefix_ = "scwx::qt::gl::draw::placefile_icons"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +static constexpr std::size_t kNumRectangles = 1; +static constexpr std::size_t kNumTriangles = kNumRectangles * 2; +static constexpr std::size_t kVerticesPerTriangle = 3; +static constexpr std::size_t kVerticesPerRectangle = kVerticesPerTriangle * 2; +static constexpr std::size_t kPointsPerVertex = 9; +static constexpr std::size_t kPointsPerTexCoord = 3; +static constexpr std::size_t kIconBufferLength = + kNumTriangles * kVerticesPerTriangle * kPointsPerVertex; +static constexpr std::size_t kTextureBufferLength = + kNumTriangles * kVerticesPerTriangle * kPointsPerTexCoord; + +// Threshold, start time, end time +static constexpr std::size_t kIntegersPerVertex_ = 3; + +struct PlacefileIconInfo +{ + PlacefileIconInfo( + const std::shared_ptr& iconFile, + const std::string& baseUrlString) : + iconFile_ {iconFile} + { + // Resolve using base URL + auto baseUrl = QUrl::fromUserInput(QString::fromStdString(baseUrlString)); + auto relativeUrl = QUrl(QDir::fromNativeSeparators( + QString::fromStdString(iconFile->filename_))); + resolvedUrl_ = baseUrl.resolved(relativeUrl).toString().toStdString(); + } + + void UpdateTextureInfo(); + + std::string resolvedUrl_; + std::shared_ptr iconFile_; + util::TextureAttributes texture_ {}; + std::size_t rows_ {}; + std::size_t columns_ {}; + std::size_t numIcons_ {}; + float scaledWidth_ {}; + float scaledHeight_ {}; +}; + +class PlacefileIcons::Impl +{ +public: + struct IconHoverEntry + { + std::shared_ptr di_; + + glm::vec2 p_; + glm::vec2 otl_; + glm::vec2 otr_; + glm::vec2 obl_; + glm::vec2 obr_; + }; + + explicit Impl(const std::shared_ptr& context) : + context_ {context}, + shaderProgram_ {nullptr}, + uMVPMatrixLocation_(GL_INVALID_INDEX), + uMapMatrixLocation_(GL_INVALID_INDEX), + uMapScreenCoordLocation_(GL_INVALID_INDEX), + uMapDistanceLocation_(GL_INVALID_INDEX), + uSelectedTimeLocation_(GL_INVALID_INDEX), + vao_ {GL_INVALID_INDEX}, + vbo_ {GL_INVALID_INDEX}, + numVertices_ {0} + { + } + + ~Impl() {} + + void UpdateBuffers(); + void UpdateTextureBuffer(); + void Update(bool textureAtlasChanged); + + std::shared_ptr context_; + + bool dirty_ {false}; + bool thresholded_ {false}; + + std::chrono::system_clock::time_point selectedTime_ {}; + + std::mutex iconMutex_; + + boost::unordered_flat_map + currentIconFiles_ {}; + boost::unordered_flat_map newIconFiles_ {}; + + std::vector> + currentIconList_ {}; + std::vector> + newIconList_ {}; + std::vector> + newValidIconList_ {}; + + std::vector currentIconBuffer_ {}; + std::vector currentIntegerBuffer_ {}; + std::vector newIconBuffer_ {}; + std::vector newIntegerBuffer_ {}; + + std::vector textureBuffer_ {}; + + std::vector currentHoverIcons_ {}; + std::vector newHoverIcons_ {}; + + std::shared_ptr shaderProgram_; + GLint uMVPMatrixLocation_; + GLint uMapMatrixLocation_; + GLint uMapScreenCoordLocation_; + GLint uMapDistanceLocation_; + GLint uSelectedTimeLocation_; + + GLuint vao_; + std::array vbo_; + + GLsizei numVertices_; +}; + +PlacefileIcons::PlacefileIcons(const std::shared_ptr& context) : + DrawItem(context->gl()), p(std::make_unique(context)) +{ +} +PlacefileIcons::~PlacefileIcons() = default; + +PlacefileIcons::PlacefileIcons(PlacefileIcons&&) noexcept = default; +PlacefileIcons& PlacefileIcons::operator=(PlacefileIcons&&) noexcept = default; + +void PlacefileIcons::set_selected_time( + std::chrono::system_clock::time_point selectedTime) +{ + p->selectedTime_ = selectedTime; +} + +void PlacefileIcons::set_thresholded(bool thresholded) +{ + p->thresholded_ = thresholded; +} + +void PlacefileIcons::Initialize() +{ + gl::OpenGLFunctions& gl = p->context_->gl(); + + p->shaderProgram_ = p->context_->GetShaderProgram( + {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"}, + {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"}, + {GL_FRAGMENT_SHADER, ":/gl/texture2d_array.frag"}}); + + p->uMVPMatrixLocation_ = p->shaderProgram_->GetUniformLocation("uMVPMatrix"); + p->uMapMatrixLocation_ = p->shaderProgram_->GetUniformLocation("uMapMatrix"); + p->uMapScreenCoordLocation_ = + p->shaderProgram_->GetUniformLocation("uMapScreenCoord"); + p->uMapDistanceLocation_ = + p->shaderProgram_->GetUniformLocation("uMapDistance"); + p->uSelectedTimeLocation_ = + p->shaderProgram_->GetUniformLocation("uSelectedTime"); + + gl.glGenVertexArrays(1, &p->vao_); + gl.glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); + + gl.glBindVertexArray(p->vao_); + gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); + gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + + // aLatLong + gl.glVertexAttribPointer(0, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + static_cast(0)); + gl.glEnableVertexAttribArray(0); + + // aXYOffset + gl.glVertexAttribPointer(1, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(2 * sizeof(float))); + gl.glEnableVertexAttribArray(1); + + // aModulate + gl.glVertexAttribPointer(3, + 4, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(4 * sizeof(float))); + gl.glEnableVertexAttribArray(3); + + // aAngle + gl.glVertexAttribPointer(4, + 1, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(8 * sizeof(float))); + gl.glEnableVertexAttribArray(4); + + gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); + gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + + // aTexCoord + gl.glVertexAttribPointer(2, + 3, + GL_FLOAT, + GL_FALSE, + kPointsPerTexCoord * sizeof(float), + static_cast(0)); + gl.glEnableVertexAttribArray(2); + + gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[2]); + gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + + // aThreshold + gl.glVertexAttribIPointer(5, // + 1, + GL_INT, + 0, + static_cast(0)); + gl.glEnableVertexAttribArray(5); + + // aTimeRange + gl.glVertexAttribIPointer(6, // + 2, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(1 * sizeof(GLint))); + gl.glEnableVertexAttribArray(6); + + p->dirty_ = true; +} + +void PlacefileIcons::Render( + const QMapLibreGL::CustomLayerRenderParameters& params, + bool textureAtlasChanged) +{ + std::unique_lock lock {p->iconMutex_}; + + if (!p->currentIconList_.empty()) + { + gl::OpenGLFunctions& gl = p->context_->gl(); + + gl.glBindVertexArray(p->vao_); + + p->Update(textureAtlasChanged); + p->shaderProgram_->Use(); + UseRotationProjection(params, p->uMVPMatrixLocation_); + UseMapProjection( + params, p->uMapMatrixLocation_, p->uMapScreenCoordLocation_); + + if (p->thresholded_) + { + // If thresholding is enabled, set the map distance + units::length::nautical_miles mapDistance = + util::maplibre::GetMapDistance(params); + gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); + } + else + { + // If thresholding is disabled, set the map distance to 0 + gl.glUniform1f(p->uMapDistanceLocation_, 0.0f); + } + + // Selected time + std::chrono::system_clock::time_point selectedTime = + (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? + std::chrono::system_clock::now() : + p->selectedTime_; + gl.glUniform1i( + p->uSelectedTimeLocation_, + static_cast(std::chrono::duration_cast( + selectedTime.time_since_epoch()) + .count())); + + // Interpolate texture coordinates + gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + // Draw icons + gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); + } +} + +void PlacefileIcons::Deinitialize() +{ + gl::OpenGLFunctions& gl = p->context_->gl(); + + gl.glDeleteVertexArrays(1, &p->vao_); + gl.glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); + + std::unique_lock lock {p->iconMutex_}; + + p->currentIconList_.clear(); + p->currentIconFiles_.clear(); + p->currentHoverIcons_.clear(); + p->currentIconBuffer_.clear(); + p->currentIntegerBuffer_.clear(); + p->textureBuffer_.clear(); +} + +void PlacefileIconInfo::UpdateTextureInfo() +{ + texture_ = util::TextureAtlas::Instance().GetTextureAttributes(resolvedUrl_); + + if (iconFile_->iconWidth_ > 0 && iconFile_->iconHeight_ > 0) + { + columns_ = texture_.size_.x / iconFile_->iconWidth_; + rows_ = texture_.size_.y / iconFile_->iconHeight_; + } + else + { + columns_ = 0u; + rows_ = 0u; + } + + numIcons_ = columns_ * rows_; + + // Pixel size + float xFactor = 0.0f; + float yFactor = 0.0f; + + if (texture_.size_.x > 0 && texture_.size_.y > 0) + { + xFactor = (texture_.sRight_ - texture_.sLeft_) / texture_.size_.x; + yFactor = (texture_.tBottom_ - texture_.tTop_) / texture_.size_.y; + } + + scaledWidth_ = iconFile_->iconWidth_ * xFactor; + scaledHeight_ = iconFile_->iconHeight_ * yFactor; +} + +void PlacefileIcons::StartIcons() +{ + // Clear the new buffer + p->newIconList_.clear(); + p->newValidIconList_.clear(); + p->newIconFiles_.clear(); + p->newIconBuffer_.clear(); + p->newIntegerBuffer_.clear(); + p->newHoverIcons_.clear(); +} + +void PlacefileIcons::SetIconFiles( + const std::vector>& iconFiles, + const std::string& baseUrl) +{ + // Populate icon file map + for (auto& file : iconFiles) + { + p->newIconFiles_.emplace( + std::piecewise_construct, + std::tuple {file->fileNumber_}, + std::forward_as_tuple(PlacefileIconInfo {file, baseUrl})); + } +} + +void PlacefileIcons::AddIcon( + const std::shared_ptr& di) +{ + if (di != nullptr) + { + p->newIconList_.emplace_back(di); + } +} + +void PlacefileIcons::FinishIcons() +{ + // Update icon files + for (auto& iconFile : p->newIconFiles_) + { + iconFile.second.UpdateTextureInfo(); + } + + // Update buffers + p->UpdateBuffers(); + + std::unique_lock lock {p->iconMutex_}; + + // Swap buffers + p->currentIconList_.swap(p->newValidIconList_); + p->currentIconFiles_.swap(p->newIconFiles_); + p->currentIconBuffer_.swap(p->newIconBuffer_); + p->currentIntegerBuffer_.swap(p->newIntegerBuffer_); + p->currentHoverIcons_.swap(p->newHoverIcons_); + + // Clear the new buffers + p->newIconList_.clear(); + p->newValidIconList_.clear(); + p->newIconFiles_.clear(); + p->newIconBuffer_.clear(); + p->newIntegerBuffer_.clear(); + p->newHoverIcons_.clear(); + + // Mark the draw item dirty + p->dirty_ = true; +} + +void PlacefileIcons::Impl::UpdateBuffers() +{ + newIconBuffer_.clear(); + newIconBuffer_.reserve(newIconList_.size() * kIconBufferLength); + newIntegerBuffer_.clear(); + newIntegerBuffer_.reserve(newIconList_.size() * kVerticesPerRectangle * + kIntegersPerVertex_); + + for (auto& di : newIconList_) + { + auto it = newIconFiles_.find(di->fileNumber_); + if (it == newIconFiles_.cend()) + { + // No file found + logger_->trace("Could not find file number: {}", di->fileNumber_); + continue; + } + + auto& icon = it->second; + + // Validate icon + if (di->iconNumber_ == 0 || di->iconNumber_ > icon.numIcons_) + { + // No icon found + logger_->trace("Invalid icon number: {}", di->iconNumber_); + continue; + } + + // Icon is valid, add to valid icon list + newValidIconList_.push_back(di); + + // Threshold value + units::length::nautical_miles threshold = di->threshold_; + GLint thresholdValue = static_cast(std::round(threshold.value())); + + // Start and end time + GLint startTime = + static_cast(std::chrono::duration_cast( + di->startTime_.time_since_epoch()) + .count()); + GLint endTime = + static_cast(std::chrono::duration_cast( + di->endTime_.time_since_epoch()) + .count()); + + // Latitude and longitude coordinates in degrees + const float lat = static_cast(di->latitude_); + const float lon = static_cast(di->longitude_); + + // Base X/Y offsets in pixels + const float x = static_cast(di->x_); + const float y = static_cast(di->y_); + + // Icon size + const float iw = static_cast(icon.iconFile_->iconWidth_); + const float ih = static_cast(icon.iconFile_->iconHeight_); + + // Hot X/Y (zero-based icon center) + const float hx = static_cast(icon.iconFile_->hotX_); + const float hy = static_cast(icon.iconFile_->hotY_); + + // Final X/Y offsets in pixels + const float lx = std::roundf(x - hx); + const float rx = std::roundf(lx + iw); + const float ty = std::roundf(y + hy); + const float by = std::roundf(ty - ih); + + // Angle in degrees + units::angle::degrees angle = di->angle_; + const float a = angle.value(); + + // Modulate color + const float mc0 = di->modulate_[0] / 255.0f; + const float mc1 = di->modulate_[1] / 255.0f; + const float mc2 = di->modulate_[2] / 255.0f; + const float mc3 = di->modulate_[3] / 255.0f; + + newIconBuffer_.insert(newIconBuffer_.end(), + { + // Icon + lat, lon, lx, by, mc0, mc1, mc2, mc3, a, // BL + lat, lon, lx, ty, mc0, mc1, mc2, mc3, a, // TL + lat, lon, rx, by, mc0, mc1, mc2, mc3, a, // BR + lat, lon, rx, by, mc0, mc1, mc2, mc3, a, // BR + lat, lon, rx, ty, mc0, mc1, mc2, mc3, a, // TR + lat, lon, lx, ty, mc0, mc1, mc2, mc3, a // TL + }); + newIntegerBuffer_.insert(newIntegerBuffer_.end(), + {thresholdValue, + startTime, + endTime, + thresholdValue, + startTime, + endTime, + thresholdValue, + startTime, + endTime, + thresholdValue, + startTime, + endTime, + thresholdValue, + startTime, + endTime, + thresholdValue, + startTime, + endTime}); + + if (!di->hoverText_.empty()) + { + const units::angle::radians radians = angle; + + const auto sc = util::maplibre::LatLongToScreenCoordinate({lat, lon}); + + const float cosAngle = cosf(static_cast(radians.value())); + const float sinAngle = sinf(static_cast(radians.value())); + + const glm::mat2 rotate {cosAngle, -sinAngle, sinAngle, cosAngle}; + + const glm::vec2 otl = rotate * glm::vec2 {lx, ty}; + const glm::vec2 otr = rotate * glm::vec2 {rx, ty}; + const glm::vec2 obl = rotate * glm::vec2 {lx, by}; + const glm::vec2 obr = rotate * glm::vec2 {rx, by}; + + newHoverIcons_.emplace_back( + IconHoverEntry {di, sc, otl, otr, obl, obr}); + } + } +} + +void PlacefileIcons::Impl::UpdateTextureBuffer() +{ + textureBuffer_.clear(); + textureBuffer_.reserve(currentIconList_.size() * kTextureBufferLength); + + for (auto& di : currentIconList_) + { + auto it = currentIconFiles_.find(di->fileNumber_); + if (it == currentIconFiles_.cend()) + { + // No file found. Should not get here, but insert empty data to match + // up with data already buffered + logger_->error("Could not find file number: {}", di->fileNumber_); + + // clang-format off + textureBuffer_.insert( + textureBuffer_.end(), + { + // Icon + 0.0f, 0.0f, 0.0f, // BL + 0.0f, 0.0f, 0.0f, // TL + 0.0f, 0.0f, 0.0f, // BR + 0.0f, 0.0f, 0.0f, // BR + 0.0f, 0.0f, 0.0f, // TR + 0.0f, 0.0f, 0.0f // TL + }); + // clang-format on + + continue; + } + + auto& icon = it->second; + + // Validate icon + if (di->iconNumber_ == 0 || di->iconNumber_ > icon.numIcons_) + { + // No icon found + logger_->trace("Invalid icon number: {}", di->iconNumber_); + + // Will get here if a texture changes, and the texture shrunk such that + // the icon is no longer found + + // clang-format off + textureBuffer_.insert( + textureBuffer_.end(), + { + // Icon + 0.0f, 0.0f, 0.0f, // BL + 0.0f, 0.0f, 0.0f, // TL + 0.0f, 0.0f, 0.0f, // BR + 0.0f, 0.0f, 0.0f, // BR + 0.0f, 0.0f, 0.0f, // TR + 0.0f, 0.0f, 0.0f // TL + }); + // clang-format on + + continue; + } + + // Texture coordinates + const std::size_t iconRow = (di->iconNumber_ - 1) / icon.columns_; + const std::size_t iconColumn = (di->iconNumber_ - 1) % icon.columns_; + + const float iconX = iconColumn * icon.scaledWidth_; + const float iconY = iconRow * icon.scaledHeight_; + + const float ls = icon.texture_.sLeft_ + iconX; + const float rs = ls + icon.scaledWidth_; + const float tt = icon.texture_.tTop_ + iconY; + const float bt = tt + icon.scaledHeight_; + const float r = static_cast(icon.texture_.layerId_); + + // clang-format off + textureBuffer_.insert( + textureBuffer_.end(), + { + // Icon + ls, bt, r, // BL + ls, tt, r, // TL + rs, bt, r, // BR + rs, bt, r, // BR + rs, tt, r, // TR + ls, tt, r // TL + }); + // clang-format on + } +} + +void PlacefileIcons::Impl::Update(bool textureAtlasChanged) +{ + gl::OpenGLFunctions& gl = context_->gl(); + + // If the texture atlas has changed + if (dirty_ || textureAtlasChanged) + { + // Update texture coordinates + for (auto& iconFile : currentIconFiles_) + { + iconFile.second.UpdateTextureInfo(); + } + + // Update OpenGL texture buffer data + UpdateTextureBuffer(); + + // Buffer texture data + gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); + gl.glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * textureBuffer_.size(), + textureBuffer_.data(), + GL_DYNAMIC_DRAW); + } + + // If buffers need updating + if (dirty_) + { + // Buffer vertex data + gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); + gl.glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * currentIconBuffer_.size(), + currentIconBuffer_.data(), + GL_DYNAMIC_DRAW); + + // Buffer threshold data + gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]); + gl.glBufferData(GL_ARRAY_BUFFER, + sizeof(GLint) * currentIntegerBuffer_.size(), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); + + numVertices_ = + static_cast(currentIconBuffer_.size() / kPointsPerVertex); + } + + dirty_ = false; +} + +bool PlacefileIcons::RunMousePicking( + const QMapLibreGL::CustomLayerRenderParameters& params, + const QPointF& /* mouseLocalPos */, + const QPointF& mouseGlobalPos, + const glm::vec2& mouseCoords) +{ + std::unique_lock lock {p->iconMutex_}; + + bool itemPicked = false; + + // Calculate map scale, remove width and height from original calculation + glm::vec2 scale = util::maplibre::GetMapScale(params); + scale = 2.0f / glm::vec2 {scale.x * params.width, scale.y * params.height}; + + // Scale and rotate the identity matrix to create the map matrix + glm::mat4 mapMatrix {1.0f}; + mapMatrix = glm::scale(mapMatrix, glm::vec3 {scale, 1.0f}); + mapMatrix = glm::rotate(mapMatrix, + glm::radians(params.bearing), + glm::vec3(0.0f, 0.0f, 1.0f)); + + units::length::meters mapDistance = + (p->thresholded_) ? util::maplibre::GetMapDistance(params) : + units::length::meters {0.0}; + + // If no time has been selected, use the current time + std::chrono::system_clock::time_point selectedTime = + (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? + std::chrono::system_clock::now() : + p->selectedTime_; + + // For each pickable icon + auto it = std::find_if( + std::execution::par_unseq, + p->currentHoverIcons_.crbegin(), + p->currentHoverIcons_.crend(), + [&mapDistance, &selectedTime, &mapMatrix, &mouseCoords](const auto& icon) + { + if (( + // Placefile is thresholded + mapDistance > units::length::meters {0.0} && + + // Placefile threshold is < 999 nmi + static_cast(std::round( + units::length::nautical_miles {icon.di_->threshold_} + .value())) < 999 && + + // Map distance is beyond the threshold + icon.di_->threshold_ < mapDistance) || + + ( + // Line has a start time + icon.di_->startTime_ != + std::chrono::system_clock::time_point {} && + + // The time range has not yet started + (selectedTime < icon.di_->startTime_ || + + // The time range has ended + icon.di_->endTime_ <= selectedTime))) + { + // Icon is not pickable + return false; + } + + // Initialize vertices + glm::vec2 bl = icon.p_; + glm::vec2 br = bl; + glm::vec2 tl = br; + glm::vec2 tr = tl; + + // Calculate offsets + // - Rotated offset is based on final X/Y offsets (pixels) + // - Multiply the offset by the scaled and rotated map matrix + const glm::vec2 otl = mapMatrix * glm::vec4 {icon.otl_, 0.0f, 1.0f}; + const glm::vec2 obl = mapMatrix * glm::vec4 {icon.obl_, 0.0f, 1.0f}; + const glm::vec2 obr = mapMatrix * glm::vec4 {icon.obr_, 0.0f, 1.0f}; + const glm::vec2 otr = mapMatrix * glm::vec4 {icon.otr_, 0.0f, 1.0f}; + + // Offset vertices + tl += otl; + bl += obl; + br += obr; + tr += otr; + + // Test point against polygon bounds + return util::maplibre::IsPointInPolygon({tl, bl, br, tr}, mouseCoords); + }); + + if (it != p->currentHoverIcons_.crend()) + { + itemPicked = true; + util::tooltip::Show(it->di_->hoverText_, mouseGlobalPos); + } + + return itemPicked; +} + +} // namespace draw +} // namespace gl +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.hpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.hpp new file mode 100644 index 00000000..16ac08cd --- /dev/null +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace gl +{ +namespace draw +{ + +class PlacefileIcons : public DrawItem +{ +public: + explicit PlacefileIcons(const std::shared_ptr& context); + ~PlacefileIcons(); + + PlacefileIcons(const PlacefileIcons&) = delete; + PlacefileIcons& operator=(const PlacefileIcons&) = delete; + + PlacefileIcons(PlacefileIcons&&) noexcept; + PlacefileIcons& operator=(PlacefileIcons&&) noexcept; + + void set_selected_time(std::chrono::system_clock::time_point selectedTime); + void set_thresholded(bool thresholded); + + void Initialize() override; + void Render(const QMapLibreGL::CustomLayerRenderParameters& params, + bool textureAtlasChanged) override; + void Deinitialize() override; + + bool RunMousePicking(const QMapLibreGL::CustomLayerRenderParameters& params, + const QPointF& mouseLocalPos, + const QPointF& mouseGlobalPos, + const glm::vec2& mouseCoords) override; + + /** + * Resets and prepares the draw item for adding a new set of icons. + */ + void StartIcons(); + + /** + * Configures the textures for drawing the placefile icons. + * + * @param [in] iconFiles A list of icon files + * @param [in] baseUrl The base URL of the placefile + */ + void SetIconFiles( + const std::vector>& + iconFiles, + const std::string& baseUrl); + + /** + * Adds a placefile icon to the internal draw list. + * + * @param [in] di Placefile icon + */ + void AddIcon(const std::shared_ptr& di); + + /** + * Finalizes the draw item after adding new icons. + */ + void FinishIcons(); + +private: + class Impl; + + std::unique_ptr p; +}; + +} // namespace draw +} // namespace gl +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp new file mode 100644 index 00000000..1b46bd99 --- /dev/null +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp @@ -0,0 +1,487 @@ +#include +#include +#include +#include + +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace gl +{ +namespace draw +{ + +static const std::string logPrefix_ = "scwx::qt::gl::draw::placefile_images"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +static constexpr std::size_t kNumRectangles = 1; +static constexpr std::size_t kNumTriangles = kNumRectangles * 2; +static constexpr std::size_t kVerticesPerTriangle = 3; +static constexpr std::size_t kVerticesPerRectangle = kVerticesPerTriangle * 2; +static constexpr std::size_t kPointsPerVertex = 8; +static constexpr std::size_t kPointsPerTexCoord = 3; +static constexpr std::size_t kImageBufferLength = + kNumTriangles * kVerticesPerTriangle * kPointsPerVertex; +static constexpr std::size_t kTextureBufferLength = + kNumTriangles * kVerticesPerTriangle * kPointsPerTexCoord; + +// Threshold, start time, end time +static constexpr std::size_t kIntegersPerVertex_ = 3; + +struct PlacefileImageInfo +{ + PlacefileImageInfo(const std::string& imageFile, + const std::string& baseUrlString) + { + // Resolve using base URL + auto baseUrl = QUrl::fromUserInput(QString::fromStdString(baseUrlString)); + auto relativeUrl = + QUrl(QDir::fromNativeSeparators(QString::fromStdString(imageFile))); + resolvedUrl_ = baseUrl.resolved(relativeUrl).toString().toStdString(); + } + + void UpdateTextureInfo(); + + std::string resolvedUrl_; + util::TextureAttributes texture_ {}; + float scaledWidth_ {}; + float scaledHeight_ {}; +}; + +class PlacefileImages::Impl +{ +public: + explicit Impl(const std::shared_ptr& context) : + context_ {context}, + shaderProgram_ {nullptr}, + uMVPMatrixLocation_(GL_INVALID_INDEX), + uMapMatrixLocation_(GL_INVALID_INDEX), + uMapScreenCoordLocation_(GL_INVALID_INDEX), + uMapDistanceLocation_(GL_INVALID_INDEX), + uSelectedTimeLocation_(GL_INVALID_INDEX), + vao_ {GL_INVALID_INDEX}, + vbo_ {GL_INVALID_INDEX}, + numVertices_ {0} + { + } + + ~Impl() {} + + void UpdateBuffers(); + void UpdateTextureBuffer(); + void Update(bool textureAtlasChanged); + + std::shared_ptr context_; + + std::string baseUrl_ {}; + + bool dirty_ {false}; + bool thresholded_ {false}; + + std::chrono::system_clock::time_point selectedTime_ {}; + + std::mutex imageMutex_; + + boost::unordered_flat_map + currentImageFiles_ {}; + boost::unordered_flat_map newImageFiles_ {}; + + std::vector> + currentImageList_ {}; + std::vector> + newImageList_ {}; + + std::vector currentImageBuffer_ {}; + std::vector currentIntegerBuffer_ {}; + std::vector newImageBuffer_ {}; + std::vector newIntegerBuffer_ {}; + + std::vector textureBuffer_ {}; + + std::shared_ptr shaderProgram_; + GLint uMVPMatrixLocation_; + GLint uMapMatrixLocation_; + GLint uMapScreenCoordLocation_; + GLint uMapDistanceLocation_; + GLint uSelectedTimeLocation_; + + GLuint vao_; + std::array vbo_; + + GLsizei numVertices_; +}; + +PlacefileImages::PlacefileImages(const std::shared_ptr& context) : + DrawItem(context->gl()), p(std::make_unique(context)) +{ +} +PlacefileImages::~PlacefileImages() = default; + +PlacefileImages::PlacefileImages(PlacefileImages&&) noexcept = default; +PlacefileImages& +PlacefileImages::operator=(PlacefileImages&&) noexcept = default; + +void PlacefileImages::set_selected_time( + std::chrono::system_clock::time_point selectedTime) +{ + p->selectedTime_ = selectedTime; +} + +void PlacefileImages::set_thresholded(bool thresholded) +{ + p->thresholded_ = thresholded; +} + +void PlacefileImages::Initialize() +{ + gl::OpenGLFunctions& gl = p->context_->gl(); + + p->shaderProgram_ = p->context_->GetShaderProgram( + {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"}, + {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"}, + {GL_FRAGMENT_SHADER, ":/gl/texture2d_array.frag"}}); + + p->uMVPMatrixLocation_ = p->shaderProgram_->GetUniformLocation("uMVPMatrix"); + p->uMapMatrixLocation_ = p->shaderProgram_->GetUniformLocation("uMapMatrix"); + p->uMapScreenCoordLocation_ = + p->shaderProgram_->GetUniformLocation("uMapScreenCoord"); + p->uMapDistanceLocation_ = + p->shaderProgram_->GetUniformLocation("uMapDistance"); + p->uSelectedTimeLocation_ = + p->shaderProgram_->GetUniformLocation("uSelectedTime"); + + gl.glGenVertexArrays(1, &p->vao_); + gl.glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); + + gl.glBindVertexArray(p->vao_); + gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); + gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + + // aLatLong + gl.glVertexAttribPointer(0, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + static_cast(0)); + gl.glEnableVertexAttribArray(0); + + // aXYOffset + gl.glVertexAttribPointer(1, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(2 * sizeof(float))); + gl.glEnableVertexAttribArray(1); + + // aModulate + gl.glVertexAttribPointer(3, + 4, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(4 * sizeof(float))); + gl.glEnableVertexAttribArray(3); + + gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); + gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + + // aTexCoord + gl.glVertexAttribPointer(2, + 3, + GL_FLOAT, + GL_FALSE, + kPointsPerTexCoord * sizeof(float), + static_cast(0)); + gl.glEnableVertexAttribArray(2); + + gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[2]); + gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + + // aThreshold + gl.glVertexAttribIPointer(5, // + 1, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + static_cast(0)); + gl.glEnableVertexAttribArray(5); + + // aTimeRange + gl.glVertexAttribIPointer(6, // + 2, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(1 * sizeof(GLint))); + gl.glEnableVertexAttribArray(6); + + p->dirty_ = true; +} + +void PlacefileImages::Render( + const QMapLibreGL::CustomLayerRenderParameters& params, + bool textureAtlasChanged) +{ + std::unique_lock lock {p->imageMutex_}; + + if (!p->currentImageList_.empty()) + { + gl::OpenGLFunctions& gl = p->context_->gl(); + + gl.glBindVertexArray(p->vao_); + + p->Update(textureAtlasChanged); + p->shaderProgram_->Use(); + UseRotationProjection(params, p->uMVPMatrixLocation_); + UseMapProjection( + params, p->uMapMatrixLocation_, p->uMapScreenCoordLocation_); + + if (p->thresholded_) + { + // If thresholding is enabled, set the map distance + units::length::nautical_miles mapDistance = + util::maplibre::GetMapDistance(params); + gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); + } + else + { + // If thresholding is disabled, set the map distance to 0 + gl.glUniform1f(p->uMapDistanceLocation_, 0.0f); + } + + // Selected time + std::chrono::system_clock::time_point selectedTime = + (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? + std::chrono::system_clock::now() : + p->selectedTime_; + gl.glUniform1i( + p->uSelectedTimeLocation_, + static_cast(std::chrono::duration_cast( + selectedTime.time_since_epoch()) + .count())); + + // Interpolate texture coordinates + gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + // Draw images + gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); + } +} + +void PlacefileImages::Deinitialize() +{ + gl::OpenGLFunctions& gl = p->context_->gl(); + + gl.glDeleteVertexArrays(1, &p->vao_); + gl.glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); + + std::unique_lock lock {p->imageMutex_}; + + p->currentImageList_.clear(); + p->currentImageFiles_.clear(); + p->currentImageBuffer_.clear(); + p->currentIntegerBuffer_.clear(); + p->textureBuffer_.clear(); +} + +void PlacefileImageInfo::UpdateTextureInfo() +{ + texture_ = util::TextureAtlas::Instance().GetTextureAttributes(resolvedUrl_); + + scaledWidth_ = texture_.sRight_ - texture_.sLeft_; + scaledHeight_ = texture_.tBottom_ - texture_.tTop_; +} + +void PlacefileImages::StartImages(const std::string& baseUrl) +{ + p->baseUrl_ = baseUrl; + + // Clear the new buffer + p->newImageList_.clear(); + p->newImageFiles_.clear(); + p->newImageBuffer_.clear(); + p->newIntegerBuffer_.clear(); +} + +void PlacefileImages::AddImage( + const std::shared_ptr& di) +{ + if (di != nullptr) + { + p->newImageList_.emplace_back(di); + } +} + +void PlacefileImages::FinishImages() +{ + // Update buffers + p->UpdateBuffers(); + + std::unique_lock lock {p->imageMutex_}; + + // Swap buffers + p->currentImageList_.swap(p->newImageList_); + p->currentImageFiles_.swap(p->newImageFiles_); + p->currentImageBuffer_.swap(p->newImageBuffer_); + p->currentIntegerBuffer_.swap(p->newIntegerBuffer_); + + // Clear the new buffers + p->newImageList_.clear(); + p->newImageFiles_.clear(); + p->newImageBuffer_.clear(); + p->newIntegerBuffer_.clear(); + + // Mark the draw item dirty + p->dirty_ = true; +} + +void PlacefileImages::Impl::UpdateBuffers() +{ + newImageBuffer_.clear(); + newImageBuffer_.reserve(newImageList_.size() * kImageBufferLength); + newIntegerBuffer_.clear(); + newIntegerBuffer_.reserve(newImageList_.size() * kVerticesPerRectangle * + kIntegersPerVertex_); + newImageFiles_.clear(); + + // Fixed modulate color + static const float mc0 = 1.0f; + static const float mc1 = 1.0f; + static const float mc2 = 1.0f; + static const float mc3 = 1.0f; + + for (auto& di : newImageList_) + { + // Populate image file map + newImageFiles_.emplace( + std::piecewise_construct, + std::tuple {di->imageFile_}, + std::forward_as_tuple(PlacefileImageInfo {di->imageFile_, baseUrl_})); + + // Threshold value + units::length::nautical_miles threshold = di->threshold_; + GLint thresholdValue = static_cast(std::round(threshold.value())); + + // Start and end time + GLint startTime = + static_cast(std::chrono::duration_cast( + di->startTime_.time_since_epoch()) + .count()); + GLint endTime = + static_cast(std::chrono::duration_cast( + di->endTime_.time_since_epoch()) + .count()); + + // Limit processing to groups of 3 (triangles) + std::size_t numElements = di->elements_.size() - di->elements_.size() % 3; + for (std::size_t i = 0; i < numElements; ++i) + { + auto& element = di->elements_[i]; + + // Latitude and longitude coordinates in degrees + const float lat = static_cast(element.latitude_); + const float lon = static_cast(element.longitude_); + + // Base X/Y offsets in pixels + const float x = static_cast(element.x_); + const float y = static_cast(element.y_); + + newImageBuffer_.insert(newImageBuffer_.end(), + {lat, lon, x, y, mc0, mc1, mc2, mc3}); + newIntegerBuffer_.insert(newIntegerBuffer_.end(), + {thresholdValue, startTime, endTime}); + } + } +} + +void PlacefileImages::Impl::UpdateTextureBuffer() +{ + textureBuffer_.clear(); + textureBuffer_.reserve(currentImageList_.size() * kTextureBufferLength); + + for (auto& di : currentImageList_) + { + // Get placefile image info. The key should always be found in the map, as + // it is populated when the placefile is updated. + auto it = currentImageFiles_.find(di->imageFile_); + const PlacefileImageInfo& image = (it == currentImageFiles_.cend()) ? + currentImageFiles_.cbegin()->second : + it->second; + + const float r = static_cast(image.texture_.layerId_); + + // Limit processing to groups of 3 (triangles) + std::size_t numElements = di->elements_.size() - di->elements_.size() % 3; + for (std::size_t i = 0; i < numElements; ++i) + { + auto& element = di->elements_[i]; + + // Texture coordinates + const float s = + image.texture_.sLeft_ + (image.scaledWidth_ * element.tu_); + const float t = + image.texture_.tTop_ + (image.scaledHeight_ * element.tv_); + + textureBuffer_.insert(textureBuffer_.end(), {s, t, r}); + } + } +} + +void PlacefileImages::Impl::Update(bool textureAtlasChanged) +{ + gl::OpenGLFunctions& gl = context_->gl(); + + // If the texture atlas has changed + if (dirty_ || textureAtlasChanged) + { + // Update texture coordinates + for (auto& imageFile : currentImageFiles_) + { + imageFile.second.UpdateTextureInfo(); + } + + // Update OpenGL texture buffer data + UpdateTextureBuffer(); + + // Buffer texture data + gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); + gl.glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * textureBuffer_.size(), + textureBuffer_.data(), + GL_DYNAMIC_DRAW); + } + + // If buffers need updating + if (dirty_) + { + // Buffer vertex data + gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); + gl.glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * currentImageBuffer_.size(), + currentImageBuffer_.data(), + GL_DYNAMIC_DRAW); + + // Buffer threshold data + gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]); + gl.glBufferData(GL_ARRAY_BUFFER, + sizeof(GLint) * currentIntegerBuffer_.size(), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); + + numVertices_ = + static_cast(currentImageBuffer_.size() / kPointsPerVertex); + } + + dirty_ = false; +} + +} // namespace draw +} // namespace gl +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_images.hpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_images.hpp new file mode 100644 index 00000000..d99c43a0 --- /dev/null +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_images.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace gl +{ +namespace draw +{ + +class PlacefileImages : public DrawItem +{ +public: + explicit PlacefileImages(const std::shared_ptr& context); + ~PlacefileImages(); + + PlacefileImages(const PlacefileImages&) = delete; + PlacefileImages& operator=(const PlacefileImages&) = delete; + + PlacefileImages(PlacefileImages&&) noexcept; + PlacefileImages& operator=(PlacefileImages&&) noexcept; + + void set_selected_time(std::chrono::system_clock::time_point selectedTime); + void set_thresholded(bool thresholded); + + void Initialize() override; + void Render(const QMapLibreGL::CustomLayerRenderParameters& params, + bool textureAtlasChanged) override; + void Deinitialize() override; + + /** + * Resets and prepares the draw item for adding a new set of images. + */ + void StartImages(const std::string& baseUrl); + + /** + * Adds a placefile image to the internal draw list. + * + * @param [in] di Placefile image + */ + void AddImage(const std::shared_ptr& di); + + /** + * Finalizes the draw item after adding new images. + */ + void FinishImages(); + +private: + class Impl; + + std::unique_ptr p; +}; + +} // namespace draw +} // namespace gl +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp new file mode 100644 index 00000000..df0eff48 --- /dev/null +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp @@ -0,0 +1,601 @@ +#include +#include +#include +#include +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace gl +{ +namespace draw +{ + +static const std::string logPrefix_ = "scwx::qt::gl::draw::placefile_lines"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +static constexpr std::size_t kNumRectangles = 1; +static constexpr std::size_t kNumTriangles = kNumRectangles * 2; +static constexpr std::size_t kVerticesPerTriangle = 3; +static constexpr std::size_t kVerticesPerRectangle = kVerticesPerTriangle * 2; +static constexpr std::size_t kPointsPerVertex = 9; +static constexpr std::size_t kBufferLength = + kNumTriangles * kVerticesPerTriangle * kPointsPerVertex; + +// Threshold, start time, end time +static constexpr std::size_t kIntegersPerVertex_ = 3; + +static const boost::gil::rgba8_pixel_t kBlack_ {0, 0, 0, 255}; + +class PlacefileLines::Impl +{ +public: + struct LineHoverEntry + { + std::shared_ptr di_; + + glm::vec2 p1_; + glm::vec2 p2_; + glm::vec2 otl_; + glm::vec2 otr_; + glm::vec2 obl_; + glm::vec2 obr_; + }; + + explicit Impl(const std::shared_ptr& context) : + context_ {context}, + shaderProgram_ {nullptr}, + uMVPMatrixLocation_(GL_INVALID_INDEX), + uMapMatrixLocation_(GL_INVALID_INDEX), + uMapScreenCoordLocation_(GL_INVALID_INDEX), + uMapDistanceLocation_(GL_INVALID_INDEX), + uSelectedTimeLocation_(GL_INVALID_INDEX), + vao_ {GL_INVALID_INDEX}, + vbo_ {GL_INVALID_INDEX}, + numVertices_ {0} + { + } + + ~Impl() {} + + void BufferLine(const std::shared_ptr& di, + const gr::Placefile::LineDrawItem::Element& e1, + const gr::Placefile::LineDrawItem::Element& e2, + const float width, + const units::angle::degrees angle, + const boost::gil::rgba8_pixel_t color, + const GLint threshold, + const GLint startTime, + const GLint endTime, + bool bufferHover = false); + void + UpdateBuffers(const std::shared_ptr& di); + void Update(); + + std::shared_ptr context_; + + bool dirty_ {false}; + bool thresholded_ {false}; + + std::chrono::system_clock::time_point selectedTime_ {}; + + std::mutex lineMutex_ {}; + + std::size_t currentNumLines_ {}; + std::size_t newNumLines_ {}; + + std::vector currentLinesBuffer_ {}; + std::vector currentIntegerBuffer_ {}; + std::vector newLinesBuffer_ {}; + std::vector newIntegerBuffer_ {}; + + std::vector currentHoverLines_ {}; + std::vector newHoverLines_ {}; + + std::shared_ptr shaderProgram_; + GLint uMVPMatrixLocation_; + GLint uMapMatrixLocation_; + GLint uMapScreenCoordLocation_; + GLint uMapDistanceLocation_; + GLint uSelectedTimeLocation_; + + GLuint vao_; + std::array vbo_; + + GLsizei numVertices_; +}; + +PlacefileLines::PlacefileLines(const std::shared_ptr& context) : + DrawItem(context->gl()), p(std::make_unique(context)) +{ +} +PlacefileLines::~PlacefileLines() = default; + +PlacefileLines::PlacefileLines(PlacefileLines&&) noexcept = default; +PlacefileLines& PlacefileLines::operator=(PlacefileLines&&) noexcept = default; + +void PlacefileLines::set_selected_time( + std::chrono::system_clock::time_point selectedTime) +{ + p->selectedTime_ = selectedTime; +} + +void PlacefileLines::set_thresholded(bool thresholded) +{ + p->thresholded_ = thresholded; +} + +void PlacefileLines::Initialize() +{ + gl::OpenGLFunctions& gl = p->context_->gl(); + + p->shaderProgram_ = p->context_->GetShaderProgram( + {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"}, + {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"}, + {GL_FRAGMENT_SHADER, ":/gl/color.frag"}}); + + p->uMVPMatrixLocation_ = p->shaderProgram_->GetUniformLocation("uMVPMatrix"); + p->uMapMatrixLocation_ = p->shaderProgram_->GetUniformLocation("uMapMatrix"); + p->uMapScreenCoordLocation_ = + p->shaderProgram_->GetUniformLocation("uMapScreenCoord"); + p->uMapDistanceLocation_ = + p->shaderProgram_->GetUniformLocation("uMapDistance"); + p->uSelectedTimeLocation_ = + p->shaderProgram_->GetUniformLocation("uSelectedTime"); + + gl.glGenVertexArrays(1, &p->vao_); + gl.glGenBuffers(2, p->vbo_.data()); + + gl.glBindVertexArray(p->vao_); + gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); + gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + + // aLatLong + gl.glVertexAttribPointer(0, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + static_cast(0)); + gl.glEnableVertexAttribArray(0); + + // aXYOffset + gl.glVertexAttribPointer(1, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(2 * sizeof(float))); + gl.glEnableVertexAttribArray(1); + + // aModulate + gl.glVertexAttribPointer(3, + 4, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(4 * sizeof(float))); + gl.glEnableVertexAttribArray(3); + + // aAngle + gl.glVertexAttribPointer(4, + 1, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(8 * sizeof(float))); + gl.glEnableVertexAttribArray(4); + + gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); + gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + + // aThreshold + gl.glVertexAttribIPointer(5, // + 1, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + static_cast(0)); + gl.glEnableVertexAttribArray(5); + + // aTimeRange + gl.glVertexAttribIPointer(6, // + 2, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(1 * sizeof(GLint))); + gl.glEnableVertexAttribArray(6); + + p->dirty_ = true; +} + +void PlacefileLines::Render( + const QMapLibreGL::CustomLayerRenderParameters& params) +{ + std::unique_lock lock {p->lineMutex_}; + + if (p->currentNumLines_ > 0) + { + gl::OpenGLFunctions& gl = p->context_->gl(); + + gl.glBindVertexArray(p->vao_); + + p->Update(); + p->shaderProgram_->Use(); + UseRotationProjection(params, p->uMVPMatrixLocation_); + UseMapProjection( + params, p->uMapMatrixLocation_, p->uMapScreenCoordLocation_); + + if (p->thresholded_) + { + // If thresholding is enabled, set the map distance + units::length::nautical_miles mapDistance = + util::maplibre::GetMapDistance(params); + gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); + } + else + { + // If thresholding is disabled, set the map distance to 0 + gl.glUniform1f(p->uMapDistanceLocation_, 0.0f); + } + + // Selected time + std::chrono::system_clock::time_point selectedTime = + (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? + std::chrono::system_clock::now() : + p->selectedTime_; + gl.glUniform1i( + p->uSelectedTimeLocation_, + static_cast(std::chrono::duration_cast( + selectedTime.time_since_epoch()) + .count())); + + // Draw icons + gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); + } +} + +void PlacefileLines::Deinitialize() +{ + gl::OpenGLFunctions& gl = p->context_->gl(); + + gl.glDeleteVertexArrays(1, &p->vao_); + gl.glDeleteBuffers(2, p->vbo_.data()); + + std::unique_lock lock {p->lineMutex_}; + + p->currentLinesBuffer_.clear(); + p->currentIntegerBuffer_.clear(); + p->currentHoverLines_.clear(); +} + +void PlacefileLines::StartLines() +{ + // Clear the new buffers + p->newLinesBuffer_.clear(); + p->newIntegerBuffer_.clear(); + p->newHoverLines_.clear(); + + p->newNumLines_ = 0u; +} + +void PlacefileLines::AddLine( + const std::shared_ptr& di) +{ + if (di != nullptr && !di->elements_.empty()) + { + p->UpdateBuffers(di); + p->newNumLines_ += (di->elements_.size() - 1) * 2; + } +} + +void PlacefileLines::FinishLines() +{ + std::unique_lock lock {p->lineMutex_}; + + // Swap buffers + p->currentLinesBuffer_.swap(p->newLinesBuffer_); + p->currentIntegerBuffer_.swap(p->newIntegerBuffer_); + p->currentHoverLines_.swap(p->newHoverLines_); + + // Clear the new buffers + p->newLinesBuffer_.clear(); + p->newIntegerBuffer_.clear(); + p->newHoverLines_.clear(); + + // Update the number of lines + p->currentNumLines_ = p->newNumLines_; + p->numVertices_ = + static_cast(p->currentNumLines_ * kVerticesPerRectangle); + + // Mark the draw item dirty + p->dirty_ = true; +} + +void PlacefileLines::Impl::UpdateBuffers( + const std::shared_ptr& di) +{ + // Threshold value + units::length::nautical_miles threshold = di->threshold_; + GLint thresholdValue = static_cast(std::round(threshold.value())); + + // Start and end time + GLint startTime = + static_cast(std::chrono::duration_cast( + di->startTime_.time_since_epoch()) + .count()); + GLint endTime = + static_cast(std::chrono::duration_cast( + di->endTime_.time_since_epoch()) + .count()); + + std::vector> angles {}; + angles.reserve(di->elements_.size() - 1); + + // For each element pair inside a Line statement, render a black line + for (std::size_t i = 0; i < di->elements_.size() - 1; ++i) + { + // Latitude and longitude coordinates in degrees + const float lat1 = static_cast(di->elements_[i].latitude_); + const float lon1 = static_cast(di->elements_[i].longitude_); + const float lat2 = static_cast(di->elements_[i + 1].latitude_); + const float lon2 = static_cast(di->elements_[i + 1].longitude_); + + // Calculate angle + const units::angle::degrees angle = + util::GeographicLib::GetAngle(lat1, lon1, lat2, lon2); + angles.push_back(angle); + + // Buffer line with hover text + BufferLine(di, + di->elements_[i], + di->elements_[i + 1], + di->width_ + 2, + angle, + kBlack_, + thresholdValue, + startTime, + endTime, + true); + } + + // For each element pair inside a Line statement, render a colored line + for (std::size_t i = 0; i < di->elements_.size() - 1; ++i) + { + BufferLine(di, + di->elements_[i], + di->elements_[i + 1], + di->width_, + angles[i], + di->color_, + thresholdValue, + startTime, + endTime); + } +} + +void PlacefileLines::Impl::BufferLine( + const std::shared_ptr& di, + const gr::Placefile::LineDrawItem::Element& e1, + const gr::Placefile::LineDrawItem::Element& e2, + const float width, + const units::angle::degrees angle, + const boost::gil::rgba8_pixel_t color, + const GLint threshold, + const GLint startTime, + const GLint endTime, + bool bufferHover) +{ + // Latitude and longitude coordinates in degrees + const float lat1 = static_cast(e1.latitude_); + const float lon1 = static_cast(e1.longitude_); + const float lat2 = static_cast(e2.latitude_); + const float lon2 = static_cast(e2.longitude_); + + // TODO: Base X/Y offsets in pixels + // const float x1 = static_cast(e1.x_); + // const float y1 = static_cast(e1.y_); + // const float x2 = static_cast(e2.x_); + // const float y2 = static_cast(e2.y_); + + // Angle + const float a = static_cast(angle.value()); + + // Final X/Y offsets in pixels + const float hw = width * 0.5f; + const float lx = -hw; + const float rx = +hw; + const float ty = +hw; + const float by = -hw; + + // Modulate color + const float mc0 = color[0] / 255.0f; + const float mc1 = color[1] / 255.0f; + const float mc2 = color[2] / 255.0f; + const float mc3 = color[3] / 255.0f; + + // Update buffers + newLinesBuffer_.insert(newLinesBuffer_.end(), + { + // Line + lat1, lon1, lx, by, mc0, mc1, mc2, mc3, a, // BL + lat2, lon2, lx, ty, mc0, mc1, mc2, mc3, a, // TL + lat1, lon1, rx, by, mc0, mc1, mc2, mc3, a, // BR + lat1, lon1, rx, by, mc0, mc1, mc2, mc3, a, // BR + lat2, lon2, rx, ty, mc0, mc1, mc2, mc3, a, // TR + lat2, lon2, lx, ty, mc0, mc1, mc2, mc3, a // TL + }); + newIntegerBuffer_.insert(newIntegerBuffer_.end(), + {threshold, + startTime, + endTime, + threshold, + startTime, + endTime, + threshold, + startTime, + endTime, + threshold, + startTime, + endTime, + threshold, + startTime, + endTime, + threshold, + startTime, + endTime}); + + if (bufferHover && !di->hoverText_.empty()) + { + const units::angle::radians radians = angle; + + const auto sc1 = util::maplibre::LatLongToScreenCoordinate({lat1, lon1}); + const auto sc2 = util::maplibre::LatLongToScreenCoordinate({lat2, lon2}); + + const float cosAngle = cosf(static_cast(radians.value())); + const float sinAngle = sinf(static_cast(radians.value())); + + const glm::mat2 rotate {cosAngle, -sinAngle, sinAngle, cosAngle}; + + const glm::vec2 otl = rotate * glm::vec2 {-hw, +hw}; + const glm::vec2 otr = rotate * glm::vec2 {+hw, +hw}; + const glm::vec2 obl = rotate * glm::vec2 {-hw, -hw}; + const glm::vec2 obr = rotate * glm::vec2 {+hw, -hw}; + + newHoverLines_.emplace_back( + LineHoverEntry {di, sc1, sc2, otl, otr, obl, obr}); + } +} + +void PlacefileLines::Impl::Update() +{ + // If the placefile has been updated + if (dirty_) + { + gl::OpenGLFunctions& gl = context_->gl(); + + // Buffer lines data + gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); + gl.glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * currentLinesBuffer_.size(), + currentLinesBuffer_.data(), + GL_DYNAMIC_DRAW); + + // Buffer threshold data + gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); + gl.glBufferData(GL_ARRAY_BUFFER, + sizeof(GLint) * currentIntegerBuffer_.size(), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); + } + + dirty_ = false; +} + +bool PlacefileLines::RunMousePicking( + const QMapLibreGL::CustomLayerRenderParameters& params, + const QPointF& /* mouseLocalPos */, + const QPointF& mouseGlobalPos, + const glm::vec2& mouseCoords) +{ + std::unique_lock lock {p->lineMutex_}; + + bool itemPicked = false; + + // Calculate map scale, remove width and height from original calculation + glm::vec2 scale = util::maplibre::GetMapScale(params); + scale = 2.0f / glm::vec2 {scale.x * params.width, scale.y * params.height}; + + // Scale and rotate the identity matrix to create the map matrix + glm::mat4 mapMatrix {1.0f}; + mapMatrix = glm::scale(mapMatrix, glm::vec3 {scale, 1.0f}); + mapMatrix = glm::rotate(mapMatrix, + glm::radians(params.bearing), + glm::vec3(0.0f, 0.0f, 1.0f)); + + units::length::meters mapDistance = + (p->thresholded_) ? util::maplibre::GetMapDistance(params) : + units::length::meters {0.0}; + + // If no time has been selected, use the current time + std::chrono::system_clock::time_point selectedTime = + (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? + std::chrono::system_clock::now() : + p->selectedTime_; + + // For each pickable line + auto it = std::find_if( + std::execution::par_unseq, + p->currentHoverLines_.crbegin(), + p->currentHoverLines_.crend(), + [&mapDistance, &selectedTime, &mapMatrix, &mouseCoords](const auto& line) + { + if (( + // Placefile is thresholded + mapDistance > units::length::meters {0.0} && + + // Placefile threshold is < 999 nmi + static_cast(std::round( + units::length::nautical_miles {line.di_->threshold_} + .value())) < 999 && + + // Map distance is beyond the threshold + line.di_->threshold_ < mapDistance) || + + ( + // Line has a start time + line.di_->startTime_ != + std::chrono::system_clock::time_point {} && + + // The time range has not yet started + (selectedTime < line.di_->startTime_ || + + // The time range has ended + line.di_->endTime_ <= selectedTime))) + { + // Line is not pickable + return false; + } + + // Initialize vertices + glm::vec2 bl = line.p1_; + glm::vec2 br = bl; + glm::vec2 tl = line.p2_; + glm::vec2 tr = tl; + + // Calculate offsets + // - Rotated offset is half the line width (pixels) in each direction + // - Multiply the offset by the scaled and rotated map matrix + const glm::vec2 otl = mapMatrix * glm::vec4 {line.otl_, 0.0f, 1.0f}; + const glm::vec2 obl = mapMatrix * glm::vec4 {line.obl_, 0.0f, 1.0f}; + const glm::vec2 obr = mapMatrix * glm::vec4 {line.obr_, 0.0f, 1.0f}; + const glm::vec2 otr = mapMatrix * glm::vec4 {line.otr_, 0.0f, 1.0f}; + + // Offset vertices + tl += otl; + bl += obl; + br += obr; + tr += otr; + + // TODO: X/Y offsets + + // Test point against polygon bounds + return util::maplibre::IsPointInPolygon({tl, bl, br, tr}, mouseCoords); + }); + + if (it != p->currentHoverLines_.crend()) + { + itemPicked = true; + util::tooltip::Show(it->di_->hoverText_, mouseGlobalPos); + } + + return itemPicked; +} + +} // namespace draw +} // namespace gl +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.hpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.hpp new file mode 100644 index 00000000..c4f9c7ad --- /dev/null +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace gl +{ +namespace draw +{ + +class PlacefileLines : public DrawItem +{ +public: + explicit PlacefileLines(const std::shared_ptr& context); + ~PlacefileLines(); + + PlacefileLines(const PlacefileLines&) = delete; + PlacefileLines& operator=(const PlacefileLines&) = delete; + + PlacefileLines(PlacefileLines&&) noexcept; + PlacefileLines& operator=(PlacefileLines&&) noexcept; + + void set_selected_time(std::chrono::system_clock::time_point selectedTime); + void set_thresholded(bool thresholded); + + void Initialize() override; + void Render(const QMapLibreGL::CustomLayerRenderParameters& params) override; + void Deinitialize() override; + + bool RunMousePicking(const QMapLibreGL::CustomLayerRenderParameters& params, + const QPointF& mouseLocalPos, + const QPointF& mouseGlobalPos, + const glm::vec2& mouseCoords) override; + + /** + * Resets and prepares the draw item for adding a new set of lines. + */ + void StartLines(); + + /** + * Adds a placefile line to the internal draw list. + * + * @param [in] di Placefile line + */ + void AddLine(const std::shared_ptr& di); + + /** + * Finalizes the draw item after adding new lines. + */ + void FinishLines(); + +private: + class Impl; + + std::unique_ptr p; +}; + +} // namespace draw +} // namespace gl +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp new file mode 100644 index 00000000..24a96fe6 --- /dev/null +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp @@ -0,0 +1,486 @@ +#include +#include +#include + +#include + +#include +#include + +#if defined(_WIN32) +typedef void (*_GLUfuncptr)(void); +#endif + +namespace scwx +{ +namespace qt +{ +namespace gl +{ +namespace draw +{ + +static const std::string logPrefix_ = "scwx::qt::gl::draw::placefile_polygons"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +static constexpr std::size_t kVerticesPerTriangle = 3; +static constexpr std::size_t kPointsPerVertex = 8; + +// Threshold, start time, end time +static constexpr std::size_t kIntegersPerVertex_ = 3; + +static constexpr std::size_t kTessVertexScreenX_ = 0; +static constexpr std::size_t kTessVertexScreenY_ = 1; +static constexpr std::size_t kTessVertexScreenZ_ = 2; +static constexpr std::size_t kTessVertexXOffset_ = 3; +static constexpr std::size_t kTessVertexYOffset_ = 4; +static constexpr std::size_t kTessVertexR_ = 5; +static constexpr std::size_t kTessVertexG_ = 6; +static constexpr std::size_t kTessVertexB_ = 7; +static constexpr std::size_t kTessVertexA_ = 8; +static constexpr std::size_t kTessVertexSize_ = kTessVertexA_ + 1; + +typedef std::array TessVertexArray; + +class PlacefilePolygons::Impl +{ +public: + explicit Impl(const std::shared_ptr& context) : + context_ {context}, + shaderProgram_ {nullptr}, + uMVPMatrixLocation_(GL_INVALID_INDEX), + uMapMatrixLocation_(GL_INVALID_INDEX), + uMapScreenCoordLocation_(GL_INVALID_INDEX), + uMapDistanceLocation_(GL_INVALID_INDEX), + uSelectedTimeLocation_(GL_INVALID_INDEX), + vao_ {GL_INVALID_INDEX}, + vbo_ {GL_INVALID_INDEX}, + numVertices_ {0} + { + tessellator_ = gluNewTess(); + + gluTessCallback(tessellator_, // + GLU_TESS_COMBINE_DATA, + (_GLUfuncptr) &TessellateCombineCallback); + gluTessCallback(tessellator_, // + GLU_TESS_VERTEX_DATA, + (_GLUfuncptr) &TessellateVertexCallback); + + // Force GLU_TRIANGLES + gluTessCallback(tessellator_, // + GLU_TESS_EDGE_FLAG, + []() {}); + + gluTessCallback(tessellator_, // + GLU_TESS_ERROR, + (_GLUfuncptr) &TessellateErrorCallback); + } + + ~Impl() { gluDeleteTess(tessellator_); } + + void Update(); + + void Tessellate(const std::shared_ptr& di); + + static void TessellateCombineCallback(GLdouble coords[3], + void* vertexData[4], + GLfloat weight[4], + void** outData, + void* polygonData); + static void TessellateVertexCallback(void* vertexData, void* polygonData); + static void TessellateErrorCallback(GLenum errorCode); + + std::shared_ptr context_; + + bool dirty_ {false}; + bool thresholded_ {false}; + + std::chrono::system_clock::time_point selectedTime_ {}; + + boost::container::stable_vector tessCombineBuffer_ {}; + + std::mutex bufferMutex_ {}; + std::vector currentBuffer_ {}; + std::vector currentIntegerBuffer_ {}; + std::vector newBuffer_ {}; + std::vector newIntegerBuffer_ {}; + + GLUtesselator* tessellator_; + + std::shared_ptr shaderProgram_; + GLint uMVPMatrixLocation_; + GLint uMapMatrixLocation_; + GLint uMapScreenCoordLocation_; + GLint uMapDistanceLocation_; + GLint uSelectedTimeLocation_; + + GLuint vao_; + std::array vbo_; + + GLsizei numVertices_; + + GLint currentThreshold_ {}; + GLint currentStartTime_ {}; + GLint currentEndTime_ {}; +}; + +PlacefilePolygons::PlacefilePolygons( + const std::shared_ptr& context) : + DrawItem(context->gl()), p(std::make_unique(context)) +{ +} +PlacefilePolygons::~PlacefilePolygons() = default; + +PlacefilePolygons::PlacefilePolygons(PlacefilePolygons&&) noexcept = default; +PlacefilePolygons& +PlacefilePolygons::operator=(PlacefilePolygons&&) noexcept = default; + +void PlacefilePolygons::set_selected_time( + std::chrono::system_clock::time_point selectedTime) +{ + p->selectedTime_ = selectedTime; +} + +void PlacefilePolygons::set_thresholded(bool thresholded) +{ + p->thresholded_ = thresholded; +} + +void PlacefilePolygons::Initialize() +{ + gl::OpenGLFunctions& gl = p->context_->gl(); + + p->shaderProgram_ = p->context_->GetShaderProgram( + {{GL_VERTEX_SHADER, ":/gl/map_color.vert"}, + {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"}, + {GL_FRAGMENT_SHADER, ":/gl/color.frag"}}); + + p->uMVPMatrixLocation_ = p->shaderProgram_->GetUniformLocation("uMVPMatrix"); + p->uMapMatrixLocation_ = p->shaderProgram_->GetUniformLocation("uMapMatrix"); + p->uMapScreenCoordLocation_ = + p->shaderProgram_->GetUniformLocation("uMapScreenCoord"); + p->uMapDistanceLocation_ = + p->shaderProgram_->GetUniformLocation("uMapDistance"); + p->uSelectedTimeLocation_ = + p->shaderProgram_->GetUniformLocation("uSelectedTime"); + + gl.glGenVertexArrays(1, &p->vao_); + gl.glGenBuffers(2, p->vbo_.data()); + + gl.glBindVertexArray(p->vao_); + gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); + gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + + // aScreenCoord + gl.glVertexAttribPointer(0, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + static_cast(0)); + gl.glEnableVertexAttribArray(0); + + // aXYOffset + gl.glVertexAttribPointer(1, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(2 * sizeof(float))); + gl.glEnableVertexAttribArray(1); + + // aColor + gl.glVertexAttribPointer(2, + 4, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(4 * sizeof(float))); + gl.glEnableVertexAttribArray(2); + + gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); + gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + + // aThreshold + gl.glVertexAttribIPointer(3, // + 1, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + static_cast(0)); + gl.glEnableVertexAttribArray(3); + + // aTimeRange + gl.glVertexAttribIPointer(4, // + 2, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(1 * sizeof(GLint))); + gl.glEnableVertexAttribArray(4); + + p->dirty_ = true; +} + +void PlacefilePolygons::Render( + const QMapLibreGL::CustomLayerRenderParameters& params) +{ + if (!p->currentBuffer_.empty()) + { + gl::OpenGLFunctions& gl = p->context_->gl(); + + gl.glBindVertexArray(p->vao_); + + p->Update(); + p->shaderProgram_->Use(); + UseRotationProjection(params, p->uMVPMatrixLocation_); + UseMapProjection( + params, p->uMapMatrixLocation_, p->uMapScreenCoordLocation_); + + if (p->thresholded_) + { + // If thresholding is enabled, set the map distance + units::length::nautical_miles mapDistance = + util::maplibre::GetMapDistance(params); + gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); + } + else + { + // If thresholding is disabled, set the map distance to 0 + gl.glUniform1f(p->uMapDistanceLocation_, 0.0f); + } + + // Selected time + std::chrono::system_clock::time_point selectedTime = + (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? + std::chrono::system_clock::now() : + p->selectedTime_; + gl.glUniform1i( + p->uSelectedTimeLocation_, + static_cast(std::chrono::duration_cast( + selectedTime.time_since_epoch()) + .count())); + + // Draw icons + gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); + } +} + +void PlacefilePolygons::Deinitialize() +{ + gl::OpenGLFunctions& gl = p->context_->gl(); + + gl.glDeleteVertexArrays(1, &p->vao_); + gl.glDeleteBuffers(2, p->vbo_.data()); + + std::unique_lock lock {p->bufferMutex_}; + + // Clear the current buffers + p->currentBuffer_.clear(); + p->currentIntegerBuffer_.clear(); +} + +void PlacefilePolygons::StartPolygons() +{ + // Clear the new buffers + p->newBuffer_.clear(); + p->newIntegerBuffer_.clear(); +} + +void PlacefilePolygons::AddPolygon( + const std::shared_ptr& di) +{ + if (di != nullptr) + { + p->Tessellate(di); + } +} + +void PlacefilePolygons::FinishPolygons() +{ + std::unique_lock lock {p->bufferMutex_}; + + // Swap buffers + p->currentBuffer_.swap(p->newBuffer_); + p->currentIntegerBuffer_.swap(p->newIntegerBuffer_); + + // Clear the new buffers + p->newBuffer_.clear(); + p->newIntegerBuffer_.clear(); + + // Mark the draw item dirty + p->dirty_ = true; +} + +void PlacefilePolygons::Impl::Update() +{ + if (dirty_) + { + gl::OpenGLFunctions& gl = context_->gl(); + + std::unique_lock lock {bufferMutex_}; + + // Buffer vertex data + gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); + gl.glBufferData(GL_ARRAY_BUFFER, + sizeof(GLfloat) * currentBuffer_.size(), + currentBuffer_.data(), + GL_DYNAMIC_DRAW); + + // Buffer threshold data + gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); + gl.glBufferData(GL_ARRAY_BUFFER, + sizeof(GLint) * currentIntegerBuffer_.size(), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); + + numVertices_ = + static_cast(currentBuffer_.size() / kPointsPerVertex); + + dirty_ = false; + } +} + +void PlacefilePolygons::Impl::Tessellate( + const std::shared_ptr& di) +{ + // Vertex storage + boost::container::stable_vector vertices {}; + + // Default color to "Color" statement + boost::gil::rgba8_pixel_t lastColor = di->color_; + + // Current threshold + units::length::nautical_miles threshold = di->threshold_; + currentThreshold_ = static_cast(std::round(threshold.value())); + + // Start and end time + currentStartTime_ = + static_cast(std::chrono::duration_cast( + di->startTime_.time_since_epoch()) + .count()); + currentEndTime_ = + static_cast(std::chrono::duration_cast( + di->endTime_.time_since_epoch()) + .count()); + + gluTessBeginPolygon(tessellator_, this); + + for (auto& contour : di->contours_) + { + gluTessBeginContour(tessellator_); + + for (auto& element : contour) + { + // Calculate screen coordinate + auto screenCoordinate = util::maplibre::LatLongToScreenCoordinate( + {element.latitude_, element.longitude_}); + + // Update the most recent color if specified + if (element.color_.has_value()) + { + lastColor = element.color_.value(); + } + + // Add vertex to temporary storage + auto& vertex = + vertices.emplace_back(TessVertexArray {screenCoordinate.x, + screenCoordinate.y, + 0.0, // z + element.x_, + element.y_, + lastColor[0] / 255.0, + lastColor[1] / 255.0, + lastColor[2] / 255.0, + lastColor[3] / 255.0}); + + // Tessellate vertex + gluTessVertex(tessellator_, vertex.data(), vertex.data()); + } + + gluTessEndContour(tessellator_); + } + + gluTessEndPolygon(tessellator_); + + // Clear temporary storage + tessCombineBuffer_.clear(); + + // Remove extra vertices that don't correspond to a full triangle + while (newBuffer_.size() % kVerticesPerTriangle != 0) + { + newBuffer_.pop_back(); + newIntegerBuffer_.pop_back(); + } +} + +void PlacefilePolygons::Impl::TessellateCombineCallback(GLdouble coords[3], + void* vertexData[4], + GLfloat w[4], + void** outData, + void* polygonData) +{ + static constexpr std::size_t r = kTessVertexR_; + static constexpr std::size_t a = kTessVertexA_; + + Impl* self = static_cast(polygonData); + + // Create new vertex data with given coordinates and interpolated color + auto& newVertexData = self->tessCombineBuffer_.emplace_back( // + TessVertexArray { + coords[0], + coords[1], + coords[2], + 0.0, // offsetX + 0.0, // offsetY + 0.0, // r + 0.0, // g + 0.0, // b + 0.0 // a + }); + + for (std::size_t i = 0; i < 4; ++i) + { + GLdouble* d = static_cast(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(polygonData); + GLdouble* data = static_cast(vertexData); + + // Buffer vertex + self->newBuffer_.insert(self->newBuffer_.end(), + {static_cast(data[kTessVertexScreenX_]), + static_cast(data[kTessVertexScreenY_]), + static_cast(data[kTessVertexXOffset_]), + static_cast(data[kTessVertexYOffset_]), + static_cast(data[kTessVertexR_]), + static_cast(data[kTessVertexG_]), + static_cast(data[kTessVertexB_]), + static_cast(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 diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.hpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.hpp new file mode 100644 index 00000000..3c607b72 --- /dev/null +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace gl +{ +namespace draw +{ + +class PlacefilePolygons : public DrawItem +{ +public: + explicit PlacefilePolygons(const std::shared_ptr& 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& di); + + /** + * Finalizes the draw item after adding new polygons. + */ + void FinishPolygons(); + +private: + class Impl; + + std::unique_ptr p; +}; + +} // namespace draw +} // namespace gl +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp new file mode 100644 index 00000000..44851fa3 --- /dev/null +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp @@ -0,0 +1,311 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace gl +{ +namespace draw +{ + +static const std::string logPrefix_ = "scwx::qt::gl::draw::placefile_text"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class PlacefileText::Impl +{ +public: + explicit Impl(const std::shared_ptr& context, + const std::string& placefileName) : + context_ {context}, placefileName_ {placefileName} + { + } + + ~Impl() {} + + void RenderTextDrawItem( + const QMapLibreGL::CustomLayerRenderParameters& params, + const std::shared_ptr& 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 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 mapDistance_ {}; + + std::mutex listMutex_ {}; + std::vector> textList_ {}; + std::vector> newList_ {}; + + std::vector> fonts_ {}; + std::vector> newFonts_ {}; +}; + +PlacefileText::PlacefileText(const std::shared_ptr& context, + const std::string& placefileName) : + DrawItem(context->gl()), p(std::make_unique(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& 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(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>& 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& 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 diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.hpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.hpp new file mode 100644 index 00000000..a8d23bc3 --- /dev/null +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace gl +{ +namespace draw +{ + +class PlacefileText : public DrawItem +{ +public: + explicit PlacefileText(const std::shared_ptr& 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>& + fonts); + + /** + * Adds placefile text to the internal draw list. + * + * @param [in] di Placefile icon + */ + void AddText(const std::shared_ptr& di); + + /** + * Finalizes the draw item after adding new text. + */ + void FinishText(); + +private: + class Impl; + + std::unique_ptr p; +}; + +} // namespace draw +} // namespace gl +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.cpp new file mode 100644 index 00000000..dc0bc781 --- /dev/null +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.cpp @@ -0,0 +1,351 @@ +#include +#include +#include + +#include + +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& 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& di); + void Update(); + + std::shared_ptr context_; + + bool dirty_ {false}; + bool thresholded_ {false}; + + std::chrono::system_clock::time_point selectedTime_ {}; + + std::mutex bufferMutex_ {}; + + std::vector currentBuffer_ {}; + std::vector currentIntegerBuffer_ {}; + std::vector newBuffer_ {}; + std::vector newIntegerBuffer_ {}; + + std::shared_ptr shaderProgram_; + GLint uMVPMatrixLocation_; + GLint uMapMatrixLocation_; + GLint uMapScreenCoordLocation_; + GLint uMapDistanceLocation_; + GLint uSelectedTimeLocation_; + + GLuint vao_; + std::array vbo_; + + GLsizei numVertices_; +}; + +PlacefileTriangles::PlacefileTriangles( + const std::shared_ptr& context) : + DrawItem(context->gl()), p(std::make_unique(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(0)); + gl.glEnableVertexAttribArray(0); + + // aXYOffset + gl.glVertexAttribPointer(1, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(2 * sizeof(float))); + gl.glEnableVertexAttribArray(1); + + // aColor + gl.glVertexAttribPointer(2, + 4, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(4 * sizeof(float))); + gl.glEnableVertexAttribArray(2); + + gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); + gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + + // aThreshold + gl.glVertexAttribIPointer(3, // + 1, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + static_cast(0)); + gl.glEnableVertexAttribArray(3); + + // aTimeRange + gl.glVertexAttribIPointer(4, // + 2, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(1 * sizeof(GLint))); + gl.glEnableVertexAttribArray(4); + + p->dirty_ = true; +} + +void 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 mapDistance = + util::maplibre::GetMapDistance(params); + gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); + } + else + { + // If thresholding is disabled, set the map distance to 0 + gl.glUniform1f(p->uMapDistanceLocation_, 0.0f); + } + + // Selected time + std::chrono::system_clock::time_point selectedTime = + (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? + std::chrono::system_clock::now() : + p->selectedTime_; + gl.glUniform1i( + p->uSelectedTimeLocation_, + static_cast(std::chrono::duration_cast( + selectedTime.time_since_epoch()) + .count())); + + // Draw icons + gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); + } +} + +void 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& 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& di) +{ + // Threshold value + units::length::nautical_miles threshold = di->threshold_; + GLint thresholdValue = static_cast(std::round(threshold.value())); + + // Start and end time + GLint startTime = + static_cast(std::chrono::duration_cast( + di->startTime_.time_since_epoch()) + .count()); + GLint endTime = + static_cast(std::chrono::duration_cast( + di->endTime_.time_since_epoch()) + .count()); + + // 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(element.x_); + const float y = static_cast(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(currentBuffer_.size() / kPointsPerVertex); + + dirty_ = false; + } +} + +} // namespace draw +} // namespace gl +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.hpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.hpp new file mode 100644 index 00000000..daacc120 --- /dev/null +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace gl +{ +namespace draw +{ + +class PlacefileTriangles : public DrawItem +{ +public: + explicit PlacefileTriangles(const std::shared_ptr& 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& di); + + /** + * Finalizes the draw item after adding new triangles. + */ + void FinishTriangles(); + +private: + class Impl; + + std::unique_ptr p; +}; + +} // namespace draw +} // namespace gl +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/gl/gl_context.cpp b/scwx-qt/source/scwx/qt/gl/gl_context.cpp index d0e0fd80..592d12cb 100644 --- a/scwx-qt/source/scwx/qt/gl/gl_context.cpp +++ b/scwx-qt/source/scwx/qt/gl/gl_context.cpp @@ -1,8 +1,9 @@ #include #include -#include #include +#include + namespace scwx { namespace qt @@ -25,16 +26,23 @@ public: } ~Impl() {} + void InitializeGL(); + + static std::size_t + GetShaderKey(std::initializer_list> shaders); + gl::OpenGLFunctions gl_; - std::unordered_map, - std::shared_ptr, - scwx::util::hash>> + bool glInitialized_ {false}; + + std::unordered_map> shaderProgramMap_; std::mutex shaderProgramMutex_; GLuint textureAtlas_; std::mutex textureMutex_; + + std::uint64_t textureBufferCount_ {}; }; GlContext::GlContext() : p(std::make_unique()) {} @@ -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 GlContext::GetShaderProgram(const std::string& vertexPath, const std::string& fragmentPath) { - const std::pair key {vertexPath, fragmentPath}; - std::shared_ptr shaderProgram; + return GetShaderProgram( + {{GL_VERTEX_SHADER, vertexPath}, {GL_FRAGMENT_SHADER, fragmentPath}}); +} + +std::shared_ptr GlContext::GetShaderProgram( + std::initializer_list> shaders) +{ + const auto key = Impl::GetShaderKey(shaders); + std::shared_ptr 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(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> 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 diff --git a/scwx-qt/source/scwx/qt/gl/gl_context.hpp b/scwx-qt/source/scwx/qt/gl/gl_context.hpp index 623b5855..b09ff403 100644 --- a/scwx-qt/source/scwx/qt/gl/gl_context.hpp +++ b/scwx-qt/source/scwx/qt/gl/gl_context.hpp @@ -24,9 +24,13 @@ public: gl::OpenGLFunctions& gl(); + std::uint64_t texture_buffer_count() const; + std::shared_ptr GetShaderProgram(const std::string& vertexPath, const std::string& fragmentPath); + std::shared_ptr GetShaderProgram( + std::initializer_list> shaders); GLuint GetTextureAtlas(); diff --git a/scwx-qt/source/scwx/qt/gl/shader_program.cpp b/scwx-qt/source/scwx/qt/gl/shader_program.cpp index 1d9e2143..4da07a32 100644 --- a/scwx-qt/source/scwx/qt/gl/shader_program.cpp +++ b/scwx-qt/source/scwx/qt/gl/shader_program.cpp @@ -15,6 +15,11 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static constexpr GLsizei kInfoLogBufSize = 512; +static const std::unordered_map 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> 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 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; } diff --git a/scwx-qt/source/scwx/qt/gl/shader_program.hpp b/scwx-qt/source/scwx/qt/gl/shader_program.hpp index 9f84053e..a2b887d8 100644 --- a/scwx-qt/source/scwx/qt/gl/shader_program.hpp +++ b/scwx-qt/source/scwx/qt/gl/shader_program.hpp @@ -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> shaderPaths); void Use() const; diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 400909d7..4c0bdac4 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -1,13 +1,18 @@ +#define NOMINMAX + #include #include +#include #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -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); diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 725bb2c0..5c2a94fc 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -5,14 +5,16 @@ #include #include +#include #include -#include #include #include #include #include #include #include +#include +#include #include #include #include @@ -20,9 +22,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -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 radarProductModel_; + std::shared_ptr placefileManager_; std::shared_ptr textEventManager_; std::shared_ptr timelineManager_; std::shared_ptr 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() diff --git a/scwx-qt/source/scwx/qt/main/main_window.hpp b/scwx-qt/source/scwx/qt/main/main_window.hpp index 6ed96aba..f4532f44 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.hpp +++ b/scwx-qt/source/scwx/qt/main/main_window.hpp @@ -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(); diff --git a/scwx-qt/source/scwx/qt/main/main_window.ui b/scwx-qt/source/scwx/qt/main/main_window.ui index 369c8d28..628d6ddc 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.ui +++ b/scwx-qt/source/scwx/qt/main/main_window.ui @@ -85,10 +85,19 @@ + + + + &Tools + + + + + @@ -415,6 +424,29 @@ &Check for Updates + + + + :/res/icons/font-awesome-6/earth-americas-solid.svg:/res/icons/font-awesome-6/earth-americas-solid.svg + + + &Placefile Manager + + + + + + :/res/icons/font-awesome-6/layer-group-solid.svg:/res/icons/font-awesome-6/layer-group-solid.svg + + + &Layer Manager + + + + + Dump &Layer List + + diff --git a/scwx-qt/source/scwx/qt/manager/font_manager.cpp b/scwx-qt/source/scwx/qt/manager/font_manager.cpp new file mode 100644 index 00000000..20f5f9a6 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/font_manager.cpp @@ -0,0 +1,550 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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> FontRecordPair; + +template +struct FontRecordHash; + +template<> +struct FontRecordHash +{ + 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& GetRawFontData(const std::string& filename); + + static FontRecord MatchFontFile(const std::string& family, + const std::vector& styles); + + FontManager* self_; + + std::string fontCachePath_ {}; + + std::shared_mutex imguiFontAtlasMutex_ {}; + + std::uint64_t imguiFontsBuildCount_ {}; + + boost::unordered_flat_map, + FontRecordHash> + imguiFonts_ {}; + std::shared_mutex imguiFontsMutex_ {}; + + boost::unordered_flat_map> rawFontData_ {}; + std::mutex rawFontDataMutex_ {}; + + std::shared_ptr defaultFont_ {}; + boost::unordered_flat_map> + fontCategoryImguiFontMap_ {}; + boost::unordered_flat_map + fontCategoryQFontMap_ {}; + std::mutex fontCategoryMutex_ {}; + + boost::unordered_flat_set dirtyFonts_ {}; + std::mutex dirtyFontsMutex_ {}; + + boost::unordered_flat_map fontIds_ {}; +}; + +FontManager::FontManager() : p(std::make_unique(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 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 size { + textSettings.font_point_size(fontCategory).GetValue()}; + + QFont font = QFontDatabase::font(QString::fromStdString(family), + QString::fromStdString(styles), + static_cast(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 +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 +FontManager::LoadImGuiFont(const std::string& family, + const std::vector& styles, + units::font_size::points 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 pixels {size}; + units::font_size::pixels imFontSize { + std::clamp(static_cast(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 imguiFont = + std::make_shared(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& +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 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 buffer {}; + buffer.reserve(dataSize); + std::copy(std::istreambuf_iterator(ifs), + std::istreambuf_iterator(), + 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(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& 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(family.c_str())); + FcPatternAddString(pattern, + FC_FONTFORMAT, + reinterpret_cast(kFcTrueType_.c_str())); + + if (!styles.empty()) + { + FcPatternAddString(pattern, + FC_STYLE, + reinterpret_cast(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(fcFamily); + record.style_ = reinterpret_cast(fcStyle); + record.filename_ = reinterpret_cast(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::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 diff --git a/scwx-qt/source/scwx/qt/manager/font_manager.hpp b/scwx-qt/source/scwx/qt/manager/font_manager.hpp new file mode 100644 index 00000000..e52d0d16 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/font_manager.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include + +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 + GetImGuiFont(types::FontCategory fontCategory); + QFont GetQFont(types::FontCategory fontCategory); + std::shared_ptr + LoadImGuiFont(const std::string& family, + const std::vector& styles, + units::font_size::points size, + bool loadIfNotFound = true); + + void LoadApplicationFont(types::Font font, const std::string& filename); + void InitializeFonts(); + + static FontManager& Instance(); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp b/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp new file mode 100644 index 00000000..4b1b0550 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp @@ -0,0 +1,808 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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> + LoadFontResources(const std::shared_ptr& placefile); + static std::vector> + LoadImageResources(const std::shared_ptr& placefile); + + boost::asio::thread_pool threadPool_ {1u}; + + PlacefileManager* self_; + + std::string placefileSettingsPath_ {}; + + std::shared_ptr radarSite_ {}; + + std::vector> placefileRecords_ {}; + boost::unordered_flat_map> + placefileRecordMap_ {}; + std::shared_mutex placefileRecordLock_ {}; +}; + +class PlacefileManager::Impl::PlacefileRecord +{ +public: + explicit PlacefileRecord(Impl* impl, + const std::string& name, + std::shared_ptr 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& record) + { + jv = {{kEnabledName_, record->enabled_}, + {kThresholdedName_, record->thresholded_}, + {kTitleName_, record->title_}, + {kNameName_, record->name_}}; + } + + friend PlacefileRecord tag_invoke(boost::json::value_to_tag, + const boost::json::value& jv) + { + return PlacefileRecord { + nullptr, + boost::json::value_to(jv.at(kNameName_)), + nullptr, + boost::json::value_to(jv.at(kTitleName_)), + jv.at(kEnabledName_).as_bool(), + jv.at(kThresholdedName_).as_bool()}; + } + + Impl* p; + + std::string name_; + std::string title_; + std::shared_ptr 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> + fonts_ {}; + std::mutex fontsMutex_ {}; + + std::vector> images_ {}; + + std::string lastRadarSite_ {}; + std::chrono::system_clock::time_point lastUpdateTime_ {}; +}; + +PlacefileManager::PlacefileManager() : p(std::make_unique(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 +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> +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(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 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( + 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 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 delimiter("&"); + boost::tokenizer tokens(query, delimiter); + + for (auto& token : tokens) + { + std::vector 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(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::Instance() +{ + static std::weak_ptr placefileManagerReference_ {}; + static std::mutex instanceMutex_ {}; + + std::unique_lock lock(instanceMutex_); + + std::shared_ptr placefileManager = + placefileManagerReference_.lock(); + + if (placefileManager == nullptr) + { + placefileManager = std::make_shared(); + placefileManagerReference_ = placefileManager; + } + + return placefileManager; +} + +boost::unordered_flat_map> +PlacefileManager::Impl::LoadFontResources( + const std::shared_ptr& placefile) +{ + boost::unordered_flat_map> + imGuiFonts {}; + auto fonts = placefile->fonts(); + + for (auto& font : fonts) + { + units::font_size::pixels size {font.second->pixels_}; + std::vector 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> +PlacefileManager::Impl::LoadImageResources( + const std::shared_ptr& placefile) +{ + const auto iconFiles = placefile->icon_files(); + const auto drawItems = placefile->GetDrawItems(); + + const QUrl baseUrl = + QUrl::fromUserInput(QString::fromStdString(placefile->name())); + + std::vector 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(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 diff --git a/scwx-qt/source/scwx/qt/manager/placefile_manager.hpp b/scwx-qt/source/scwx/qt/manager/placefile_manager.hpp new file mode 100644 index 00000000..17b39b2f --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/placefile_manager.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include + +#include +#include + +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 placefile(const std::string& name); + boost::unordered_flat_map> + 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 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 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 p; +}; + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp index c6f8106e..3048fc6c 100644 --- a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp @@ -1,13 +1,14 @@ #include +#include #include #include -#include #include #include -#include +#include +#include -#include +#include 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 fontNames_ { +static const std::vector> fontNames_ { {types::Font::din1451alt, ":/res/fonts/din1451alt.ttf"}, - {types::Font::din1451alt_g, ":/res/fonts/din1451alt_g.ttf"}}; - -static std::unordered_map 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 +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> +LoadImageResources(const std::vector& urlStrings) +{ + std::mutex m {}; + std::vector> 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 diff --git a/scwx-qt/source/scwx/qt/manager/resource_manager.hpp b/scwx-qt/source/scwx/qt/manager/resource_manager.hpp index a6ff13e1..00658891 100644 --- a/scwx-qt/source/scwx/qt/manager/resource_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/resource_manager.hpp @@ -2,6 +2,10 @@ #include +#include + +#include + namespace scwx { namespace qt @@ -14,7 +18,10 @@ namespace ResourceManager void Initialize(); void Shutdown(); -int FontId(types::Font font); +std::shared_ptr +LoadImageResource(const std::string& urlString); +std::vector> +LoadImageResources(const std::vector& urlStrings); } // namespace ResourceManager } // namespace manager diff --git a/scwx-qt/source/scwx/qt/manager/settings_manager.cpp b/scwx-qt/source/scwx/qt/manager/settings_manager.cpp index c504e15e..a4c0f4a3 100644 --- a/scwx-qt/source/scwx/qt/manager/settings_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/settings_manager.cpp @@ -1,5 +1,9 @@ #include #include +#include +#include +#include +#include #include #include #include @@ -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(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 diff --git a/scwx-qt/source/scwx/qt/manager/settings_manager.hpp b/scwx-qt/source/scwx/qt/manager/settings_manager.hpp index ed05ca1e..254ea4c8 100644 --- a/scwx-qt/source/scwx/qt/manager/settings_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/settings_manager.hpp @@ -1,8 +1,9 @@ #pragma once -#include -#include -#include +#include +#include + +#include 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 p; +}; -} // namespace SettingsManager } // namespace manager } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp index 667fa4dd..f7fc3f48 100644 --- a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include #include @@ -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& 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; } diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index f769231c..d1676d7a 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -1,6 +1,7 @@ #include -#include #include +#include +#include #include #include #include @@ -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 map, - awips::Phenomenon phenomenon, - bool alertActive, - const QString& beforeLayer); +static std::vector +AddAlertLayer(std::shared_ptr 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 context) : - DrawLayer(context), p(std::make_unique(context)) + p(std::make_unique(context)) { } AlertLayer::~AlertLayer() = default; -void AlertLayer::Initialize() +std::vector 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 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* @@ -388,21 +370,25 @@ std::shared_ptr AlertLayerHandler::Instance() return alertLayerHandler; } -static void AddAlertLayer(std::shared_ptr map, - awips::Phenomenon phenomenon, - bool alertActive, - const QString& beforeLayer) +static std::vector +AddAlertLayer(std::shared_ptr 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 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 diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.hpp b/scwx-qt/source/scwx/qt/map/alert_layer.hpp index 8b081c64..6ce681a9 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.hpp @@ -1,6 +1,11 @@ #pragma once -#include +#include +#include + +#include +#include +#include namespace scwx { @@ -11,17 +16,14 @@ namespace map class AlertLayerImpl; -class AlertLayer : public DrawLayer +class AlertLayer { public: explicit AlertLayer(std::shared_ptr 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 AddLayers(awips::Phenomenon phenomenon, + const std::string& before = {}); private: std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.cpp b/scwx-qt/source/scwx/qt/map/draw_layer.cpp index 2b055454..1cbcd849 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.cpp @@ -24,9 +24,11 @@ public: std::shared_ptr context_; std::vector> drawList_; GLuint textureAtlas_; + + std::uint64_t textureAtlasBuildCount_ {}; }; -DrawLayer::DrawLayer(std::shared_ptr context) : +DrawLayer::DrawLayer(const std::shared_ptr& context) : GenericLayer(context), p(std::make_unique(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 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& drawItem) { p->drawList_.push_back(drawItem); } diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.hpp b/scwx-qt/source/scwx/qt/map/draw_layer.hpp index e2835f02..912657fa 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.hpp @@ -15,15 +15,22 @@ class DrawLayerImpl; class DrawLayer : public GenericLayer { public: - explicit DrawLayer(std::shared_ptr context); + explicit DrawLayer(const std::shared_ptr& 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 drawItem); + void AddDrawItem(const std::shared_ptr& drawItem); private: std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/map/generic_layer.cpp b/scwx-qt/source/scwx/qt/map/generic_layer.cpp index 7c5f6e34..2c7ae6f2 100644 --- a/scwx-qt/source/scwx/qt/map/generic_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/generic_layer.cpp @@ -26,6 +26,16 @@ GenericLayer::GenericLayer(std::shared_ptr 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 GenericLayer::context() const { return p->context_; diff --git a/scwx-qt/source/scwx/qt/map/generic_layer.hpp b/scwx-qt/source/scwx/qt/map/generic_layer.hpp index 86bf7a20..239d271b 100644 --- a/scwx-qt/source/scwx/qt/map/generic_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/generic_layer.hpp @@ -6,6 +6,7 @@ #include #include +#include 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 context() const; diff --git a/scwx-qt/source/scwx/qt/map/map_context.cpp b/scwx-qt/source/scwx/qt/map/map_context.cpp index b141b838..79d9c719 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.cpp +++ b/scwx-qt/source/scwx/qt/map/map_context.cpp @@ -23,13 +23,14 @@ public: ~Impl() {} - std::weak_ptr map_; - MapSettings settings_; - float pixelRatio_; - std::shared_ptr radarProductView_; - common::RadarProductGroup radarProductGroup_; - std::string radarProduct_; - int16_t radarProductCode_; + std::weak_ptr map_; + MapSettings settings_; + float pixelRatio_; + std::shared_ptr 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 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 diff --git a/scwx-qt/source/scwx/qt/map/map_context.hpp b/scwx-qt/source/scwx/qt/map/map_context.hpp index b9659ad2..fc2b893e 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.hpp +++ b/scwx-qt/source/scwx/qt/map/map_context.hpp @@ -26,13 +26,14 @@ public: MapContext(MapContext&&) noexcept; MapContext& operator=(MapContext&&) noexcept; - std::weak_ptr map() const; - MapSettings& settings(); - float pixel_ratio() const; - std::shared_ptr radar_product_view() const; - common::RadarProductGroup radar_product_group() const; - std::string radar_product() const; - int16_t radar_product_code() const; + std::weak_ptr map() const; + MapSettings& settings(); + float pixel_ratio() const; + std::shared_ptr 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 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; diff --git a/scwx-qt/source/scwx/qt/map/map_provider.cpp b/scwx-qt/source/scwx/qt/map/map_provider.cpp index 2a431614..df4bba08 100644 --- a/scwx-qt/source/scwx/qt/map/map_provider.cpp +++ b/scwx-qt/source/scwx/qt/map/map_provider.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include @@ -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(); diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index b42b4e3b..52c177bd 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1,21 +1,29 @@ #include #include +#include +#include #include -#include #include #include #include #include #include +#include #include #include #include +#include +#include +#include #include +#include +#include #include #include #include #include +#include #include #include @@ -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()}, widget_ {widget}, @@ -61,11 +71,11 @@ public: radarProductLayer_ {nullptr}, alertLayer_ {std::make_shared(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 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 context_; @@ -138,10 +164,19 @@ public: std::shared_ptr map_; std::list layerList_; + QStringList styleLayers_; + types::LayerVector customLayers_; + ImGuiContext* imGuiContext_; std::string imGuiContextName_; bool imGuiRendererInitialized_; + std::uint64_t imGuiFontsBuildCount_ {}; + std::shared_ptr layerModel_ { + model::LayerModel::Instance()}; + + std::shared_ptr placefileManager_ { + manager::PlacefileManager::Instance()}; std::shared_ptr radarProductManager_; std::shared_ptr colorTable_; @@ -149,14 +184,20 @@ public: std::shared_ptr radarProductLayer_; std::shared_ptr alertLayer_; std::shared_ptr overlayLayer_; + std::shared_ptr placefileLayer_; std::shared_ptr colorTableLayer_; + std::list> 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(this, settings)) +MapWidget::MapWidget(std::size_t id, const QMapLibreGL::Settings& settings) : + p(std::make_unique(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& /* roles */) + { + static const int enabledColumn = + static_cast(model::LayerModel::Column::Enabled); + const int displayColumn = + static_cast(model::LayerModel::Column::DisplayMap1) + + static_cast(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 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(p->context_); - p->colorTableLayer_ = std::make_shared(p->context_); - - std::shared_ptr 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(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(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(context_); + AddLayer(layerName, radarProductLayer_, before); + } + } + else if (type == types::LayerType::Alert) + { + // Add the alert layer for the phenomenon + auto newLayers = alertLayer_->AddLayers( + std::get(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(description); + if (placefileManager_->placefile_enabled(placefileName)) + { + AddPlacefileLayer(placefileName, before); + } + } + else if (type == types::LayerType::Information) + { + switch (std::get(description)) + { + // Create the map overlay layer + case types::InformationLayer::MapOverlay: + overlayLayer_ = std::make_shared(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(context_); + AddLayer(layerName, colorTableLayer_, before); + } + break; + + default: + break; + } + } + else if (type == types::LayerType::Data) + { + switch (std::get(description)) + { + // If there is a radar product view, create the radar range layer + case types::DataLayer::RadarRange: + if (radarProductView != nullptr) + { + std::shared_ptr 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 = + std::make_shared(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 pHost = std::make_unique(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(); } } diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp index e1c75a79..c130c3e6 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.hpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp @@ -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 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 p; friend class MapWidgetImpl; diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index e7f4b4ae..d114573d 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -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; diff --git a/scwx-qt/source/scwx/qt/map/placefile_layer.cpp b/scwx-qt/source/scwx/qt/map/placefile_layer.cpp new file mode 100644 index 00000000..3f6e5961 --- /dev/null +++ b/scwx-qt/source/scwx/qt/map/placefile_layer.cpp @@ -0,0 +1,262 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +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& context, + const std::string& placefileName) : + self_ {self}, + placefileName_ {placefileName}, + placefileIcons_ {std::make_shared(context)}, + placefileImages_ {std::make_shared(context)}, + placefileLines_ {std::make_shared(context)}, + placefilePolygons_ { + std::make_shared(context)}, + placefileTriangles_ { + std::make_shared(context)}, + placefileText_ { + std::make_shared(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 placefileIcons_; + std::shared_ptr placefileImages_; + std::shared_ptr placefileLines_; + std::shared_ptr placefilePolygons_; + std::shared_ptr placefileTriangles_; + std::shared_ptr placefileText_; + + std::chrono::system_clock::time_point selectedTime_ {}; +}; + +PlacefileLayer::PlacefileLayer(const std::shared_ptr& context, + const std::string& placefileName) : + DrawLayer(context), + p(std::make_unique(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 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 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( + drawItem)); + break; + + case gr::Placefile::ItemType::Icon: + p->placefileIcons_->AddIcon( + std::static_pointer_cast( + drawItem)); + break; + + case gr::Placefile::ItemType::Line: + p->placefileLines_->AddLine( + std::static_pointer_cast( + drawItem)); + break; + + case gr::Placefile::ItemType::Polygon: + p->placefilePolygons_->AddPolygon( + std::static_pointer_cast( + drawItem)); + break; + + case gr::Placefile::ItemType::Image: + p->placefileImages_->AddImage( + std::static_pointer_cast( + drawItem)); + break; + + case gr::Placefile::ItemType::Triangles: + p->placefileTriangles_->AddTriangles( + std::static_pointer_cast( + 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 diff --git a/scwx-qt/source/scwx/qt/map/placefile_layer.hpp b/scwx-qt/source/scwx/qt/map/placefile_layer.hpp new file mode 100644 index 00000000..ec351510 --- /dev/null +++ b/scwx-qt/source/scwx/qt/map/placefile_layer.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace map +{ + +class PlacefileLayer : public DrawLayer +{ + Q_OBJECT + +public: + explicit PlacefileLayer(const std::shared_ptr& 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 p; +}; + +} // namespace map +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index 8fb2bbde..c495e23a 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -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 diff --git a/scwx-qt/source/scwx/qt/map/radar_range_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_range_layer.cpp index ea4fa55b..a4738404 100644 --- a/scwx-qt/source/scwx/qt/map/radar_range_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_range_layer.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -22,11 +23,14 @@ void RadarRangeLayer::Add(std::shared_ptr 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 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 map, diff --git a/scwx-qt/source/scwx/qt/model/alert_model.cpp b/scwx-qt/source/scwx/qt/model/alert_model.cpp index 4cadff42..e44e2a17 100644 --- a/scwx-qt/source/scwx/qt/model/alert_model.cpp +++ b/scwx-qt/source/scwx/qt/model/alert_model.cpp @@ -1,3 +1,5 @@ +#define NOMINMAX + #include #include #include diff --git a/scwx-qt/source/scwx/qt/model/layer_model.cpp b/scwx-qt/source/scwx/qt/model/layer_model.cpp new file mode 100644 index 00000000..a153f4f2 --- /dev/null +++ b/scwx-qt/source/scwx/qt/model/layer_model.cpp @@ -0,0 +1,1067 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace model +{ + +static const std::string logPrefix_ = "scwx::qt::model::layer_model"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +static constexpr int kFirstColumn = static_cast(LayerModel::Column::Order); +static constexpr int kLastColumn = + static_cast(LayerModel::Column::Description); +static constexpr int kNumColumns = kLastColumn - kFirstColumn + 1; + +static constexpr std::size_t kMapCount_ = 4u; + +static const QString kMimeFormat {"application/x.scwx-layer-model"}; + +static const std::vector kDefaultLayers_ { + {types::LayerType::Information, types::InformationLayer::MapOverlay, false}, + {types::LayerType::Information, types::InformationLayer::ColorTable, false}, + {types::LayerType::Data, types::DataLayer::RadarRange, true}, + {types::LayerType::Alert, awips::Phenomenon::Tornado, true}, + {types::LayerType::Alert, awips::Phenomenon::SnowSquall, true}, + {types::LayerType::Alert, awips::Phenomenon::SevereThunderstorm, true}, + {types::LayerType::Alert, awips::Phenomenon::FlashFlood, true}, + {types::LayerType::Alert, awips::Phenomenon::Marine, true}, + {types::LayerType::Map, types::MapLayer::MapSymbology, false}, + {types::LayerType::Radar, std::monostate {}, true}, + {types::LayerType::Map, types::MapLayer::MapUnderlay, false}, +}; + +static const std::vector kImmovableLayers_ { + {types::LayerType::Information, types::InformationLayer::MapOverlay, false}, + {types::LayerType::Information, types::InformationLayer::ColorTable, false}, + {types::LayerType::Map, types::MapLayer::MapSymbology, false}, + {types::LayerType::Map, types::MapLayer::MapUnderlay, false}, +}; + +static const std::array kAlertPhenomena_ { + awips::Phenomenon::Tornado, + awips::Phenomenon::SnowSquall, + awips::Phenomenon::SevereThunderstorm, + awips::Phenomenon::FlashFlood, + awips::Phenomenon::Marine}; + +class LayerModel::Impl +{ +public: + explicit Impl(LayerModel* self) : self_ {self} {} + ~Impl() = default; + + void AddPlacefile(const std::string& name); + void HandlePlacefileRemoved(const std::string& name); + void HandlePlacefileRenamed(const std::string& oldName, + const std::string& newName); + void HandlePlacefileUpdate(const std::string& name, Column column); + void InitializeLayerSettings(); + void ReadLayerSettings(); + void SynchronizePlacefileLayers(); + void WriteLayerSettings(); + + static void ValidateLayerSettings(types::LayerVector& layers); + + LayerModel* self_; + + std::string layerSettingsPath_ {}; + + bool placefilesInitialized_ {false}; + std::vector initialPlacefiles_ {}; + + std::shared_ptr placefileManager_ { + manager::PlacefileManager::Instance()}; + + types::LayerVector layers_ {}; +}; + +LayerModel::LayerModel(QObject* parent) : + QAbstractTableModel(parent), p(std::make_unique(this)) +{ + connect(p->placefileManager_.get(), + &manager::PlacefileManager::PlacefilesInitialized, + this, + [this]() { p->SynchronizePlacefileLayers(); }); + + connect(p->placefileManager_.get(), + &manager::PlacefileManager::PlacefileEnabled, + this, + [this](const std::string& name, bool /* enabled */) + { p->HandlePlacefileUpdate(name, Column::Enabled); }); + + connect(p->placefileManager_.get(), + &manager::PlacefileManager::PlacefileRemoved, + this, + [this](const std::string& name) + { p->HandlePlacefileRemoved(name); }); + + connect(p->placefileManager_.get(), + &manager::PlacefileManager::PlacefileRenamed, + this, + [this](const std::string& oldName, const std::string& newName) + { p->HandlePlacefileRenamed(oldName, newName); }); + + connect(p->placefileManager_.get(), + &manager::PlacefileManager::PlacefileUpdated, + this, + [this](const std::string& name) + { p->HandlePlacefileUpdate(name, Column::Description); }); + + p->InitializeLayerSettings(); + p->ReadLayerSettings(); + + if (p->layers_.empty()) + { + p->layers_.assign(kDefaultLayers_.cbegin(), kDefaultLayers_.cend()); + } +} + +LayerModel::~LayerModel() +{ + // Write layer settings on shutdown + p->WriteLayerSettings(); +}; + +void LayerModel::Impl::InitializeLayerSettings() +{ + 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); + } + } + + layerSettingsPath_ = appDataPath + "/layers.json"; +} + +void LayerModel::Impl::ReadLayerSettings() +{ + logger_->info("Reading layer settings"); + + boost::json::value layerJson = nullptr; + types::LayerVector newLayers {}; + + // Determine if layer settings exists + if (std::filesystem::exists(layerSettingsPath_)) + { + layerJson = util::json::ReadJsonFile(layerSettingsPath_); + } + + // If layer settings was successfully read + if (layerJson != nullptr && layerJson.is_array()) + { + // For each layer entry + auto& layerArray = layerJson.as_array(); + for (auto& layerEntry : layerArray) + { + try + { + // Convert layer entry to a LayerInfo record, and add to new layers + newLayers.emplace_back( + boost::json::value_to(layerEntry)); + } + catch (const std::exception& ex) + { + logger_->warn("Invalid layer entry: {}", ex.what()); + } + } + + // Validate and correct read layers + ValidateLayerSettings(newLayers); + + // Assign read layers + layers_.swap(newLayers); + } +} + +void LayerModel::Impl::ValidateLayerSettings(types::LayerVector& layers) +{ + // Validate layer properties + for (auto it = layers.begin(); it != layers.end();) + { + // If the layer is invalid, remove it + if (it->type_ == types::LayerType::Unknown || + (std::holds_alternative(it->description_) && + std::get(it->description_) == + types::DataLayer::Unknown) || + (std::holds_alternative(it->description_) && + std::get(it->description_) == + types::InformationLayer::Unknown) || + (std::holds_alternative(it->description_) && + std::get(it->description_) == + types::MapLayer::Unknown) || + (std::holds_alternative(it->description_) && + std::get(it->description_) == + awips::Phenomenon::Unknown)) + { + // Erase the current layer and continue + it = layers.erase(it); + continue; + } + + // Ensure layers are appropriately marked movable + it->movable_ = (it->type_ != types::LayerType::Information && + it->type_ != types::LayerType::Map); + + // Continue to the next layer + ++it; + } + + // Validate immovable layers + std::vector immovableIterators {}; + types::LayerVector::iterator colorTableIterator {}; + types::LayerVector::iterator mapSymbologyIterator {}; + types::LayerVector::iterator mapUnderlayIterator {}; + for (auto& immovableLayer : kImmovableLayers_) + { + // Set the default displayed state for a layer that is not found + std::array displayed {true, true, true, true}; + + // Find the immovable layer + auto it = std::find_if(layers.begin(), + layers.end(), + [&immovableLayer](const types::LayerInfo& layer) + { + return layer.type_ == immovableLayer.type_ && + layer.description_ == + immovableLayer.description_; + }); + + // If the immovable layer is out of order + if (!immovableIterators.empty() && immovableIterators.back() > it) + { + // Save the displayed state of the immovable layer + displayed = it->displayed_; + + // Remove the layer from the list, to re-add it later + layers.erase(it); + + // Treat the layer as not found + it = layers.end(); + } + + // If the immovable layer is not found + if (it == layers.end()) + { + // If this is the first immovable layer, insert at the beginning, + // otherwise, insert after the previous immovable layer + types::LayerVector::iterator insertPosition = + immovableIterators.empty() ? layers.begin() : + immovableIterators.back() + 1; + it = layers.insert(insertPosition, immovableLayer); + + // Restore the displayed state of the immovable layer + it->displayed_ = displayed; + } + + // Store positional iterators + if (it->type_ == types::LayerType::Information) + { + switch (std::get(it->description_)) + { + case types::InformationLayer::ColorTable: + colorTableIterator = it; + break; + + default: + break; + } + } + else if (it->type_ == types::LayerType::Map) + { + switch (std::get(it->description_)) + { + case types::MapLayer::MapSymbology: + mapSymbologyIterator = it; + break; + + case types::MapLayer::MapUnderlay: + mapUnderlayIterator = it; + break; + + default: + break; + } + } + + // Add the immovable iterator to the list + immovableIterators.push_back(it); + } + + // Validate data layers + std::vector dataIterators {}; + for (const auto& dataLayer : types::DataLayerIterator()) + { + // Find the data layer + auto it = std::find_if(layers.begin(), + layers.end(), + [&dataLayer](const types::LayerInfo& layer) + { + return layer.type_ == types::LayerType::Data && + std::get( + layer.description_) == dataLayer; + }); + + if (it == layers.end()) + { + // If this is the first data layer, insert after the color table layer, + // otherwise, insert after the previous data layer + types::LayerVector::iterator insertPosition = + dataIterators.empty() ? colorTableIterator + 1 : + dataIterators.back() + 1; + it = + layers.insert(insertPosition, {types::LayerType::Data, dataLayer}); + } + + dataIterators.push_back(it); + } + + // Validate alert layers + std::vector alertIterators {}; + for (auto& phenomenon : kAlertPhenomena_) + { + // Find the alert layer + auto it = std::find_if(layers.begin(), + layers.end(), + [&phenomenon](const types::LayerInfo& layer) + { + return layer.type_ == types::LayerType::Alert && + std::get( + layer.description_) == phenomenon; + }); + + if (it == layers.end()) + { + // Insert before the map symbology layer + it = layers.insert(mapSymbologyIterator, + {types::LayerType::Alert, phenomenon}); + } + + alertIterators.push_back(it); + } + + // Validate the radar layer + auto it = std::find_if(layers.begin(), + layers.end(), + [](const types::LayerInfo& layer) + { return layer.type_ == types::LayerType::Radar; }); + if (it == layers.end()) + { + // Insert before the map underlay layer + it = layers.insert(mapUnderlayIterator, + {types::LayerType::Radar, std::monostate {}}); + } +} + +void LayerModel::Impl::WriteLayerSettings() +{ + logger_->info("Saving layer settings"); + + auto layerJson = boost::json::value_from(layers_); + util::json::WriteJsonFile(layerSettingsPath_, layerJson); +} + +types::LayerVector LayerModel::GetLayers() const +{ + return p->layers_; +} + +void LayerModel::ResetLayers() +{ + // Initialize a new layer vector from the default + types::LayerVector newLayers {}; + newLayers.assign(kDefaultLayers_.cbegin(), kDefaultLayers_.cend()); + + auto colorTableIterator = std::find_if( + newLayers.begin(), + newLayers.end(), + [](const types::LayerInfo& layerInfo) + { + return std::holds_alternative( + layerInfo.description_) && + std::get(layerInfo.description_) == + types::InformationLayer::ColorTable; + }); + + // Add all existing placefile layers + for (auto it = p->layers_.rbegin(); it != p->layers_.rend(); ++it) + { + if (it->type_ == types::LayerType::Placefile) + { + newLayers.insert( + colorTableIterator + 1, + {it->type_, it->description_, it->movable_, it->displayed_}); + } + } + + // Swap the model + beginResetModel(); + p->layers_.swap(newLayers); + endResetModel(); +} + +void LayerModel::Impl::SynchronizePlacefileLayers() +{ + placefilesInitialized_ = true; + + int row = 0; + for (auto it = layers_.begin(); it != layers_.end();) + { + if (it->type_ == types::LayerType::Placefile && + std::find(initialPlacefiles_.begin(), + initialPlacefiles_.end(), + std::get(it->description_)) == + initialPlacefiles_.end()) + { + // If the placefile layer was not loaded by the placefile manager, + // erase it + self_->beginRemoveRows(QModelIndex(), row, row); + it = layers_.erase(it); + self_->endRemoveRows(); + continue; + } + + ++it; + ++row; + } + + initialPlacefiles_.clear(); +} + +int LayerModel::rowCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : static_cast(p->layers_.size()); +} + +int LayerModel::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : kNumColumns; +} + +Qt::ItemFlags LayerModel::flags(const QModelIndex& index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + if (!index.isValid() || index.row() < 0 || + static_cast(index.row()) >= p->layers_.size()) + { + return flags; + } + + const auto& layer = p->layers_.at(index.row()); + + switch (index.column()) + { + case static_cast(Column::DisplayMap1): + case static_cast(Column::DisplayMap2): + case static_cast(Column::DisplayMap3): + case static_cast(Column::DisplayMap4): + if (layer.type_ != types::LayerType::Map) + { + flags |= + Qt::ItemFlag::ItemIsUserCheckable | Qt::ItemFlag::ItemIsEditable; + } + break; + + default: + break; + } + + if (layer.movable_) + { + flags |= Qt::ItemFlag::ItemIsDragEnabled; + } + + flags |= Qt::ItemFlag::ItemIsDropEnabled; + + return flags; +} + +Qt::DropActions LayerModel::supportedDropActions() const +{ + return Qt::DropAction::MoveAction; +} + +bool LayerModel::IsMovable(int row) const +{ + bool movable = false; + + if (0 <= row && static_cast(row) < p->layers_.size()) + { + movable = p->layers_.at(row).movable_; + } + + return movable; +} + +QVariant LayerModel::data(const QModelIndex& index, int role) const +{ + static const QString enabledString = QObject::tr("Enabled"); + static const QString disabledString = QObject::tr("Disabled"); + + static const QString displayedString = QObject::tr("Displayed"); + static const QString hiddenString = QObject::tr("Hidden"); + + if (!index.isValid() || index.row() < 0 || + static_cast(index.row()) >= p->layers_.size()) + { + return QVariant(); + } + + const auto& layer = p->layers_.at(index.row()); + + switch (index.column()) + { + case static_cast(Column::Order): + if (role == Qt::ItemDataRole::DisplayRole) + { + return index.row() + 1; + } + break; + + case static_cast(Column::DisplayMap1): + case static_cast(Column::DisplayMap2): + case static_cast(Column::DisplayMap3): + case static_cast(Column::DisplayMap4): + if (layer.type_ != types::LayerType::Map) + { + bool displayed = + layer.displayed_[index.column() - + static_cast(Column::DisplayMap1)]; + + if (role == Qt::ItemDataRole::ToolTipRole) + { + return displayed ? displayedString : hiddenString; + } + else if (role == Qt::ItemDataRole::CheckStateRole) + { + return static_cast(displayed ? Qt::CheckState::Checked : + Qt::CheckState::Unchecked); + } + } + break; + + case static_cast(Column::Type): + if (role == Qt::ItemDataRole::DisplayRole || + role == Qt::ItemDataRole::ToolTipRole) + { + return QString::fromStdString(types::GetLayerTypeName(layer.type_)); + } + break; + + case static_cast(Column::Enabled): + if (role == Qt::ItemDataRole::DisplayRole || + role == Qt::ItemDataRole::ToolTipRole) + { + if (layer.type_ == types::LayerType::Placefile) + { + return p->placefileManager_->placefile_enabled( + std::get(layer.description_)) ? + enabledString : + disabledString; + } + } + break; + + case static_cast(Column::Description): + if (role == Qt::ItemDataRole::DisplayRole || + role == Qt::ItemDataRole::ToolTipRole) + { + if (layer.type_ == types::LayerType::Placefile) + { + std::string placefileName = + std::get(layer.description_); + std::string description = placefileName; + std::string title = + p->placefileManager_->placefile_title(placefileName); + if (!title.empty()) + { + description = title + '\n' + description; + } + + return QString::fromStdString(description); + } + else + { + return QString::fromStdString( + types::GetLayerDescriptionName(layer.description_)); + } + } + break; + + default: + break; + } + + return QVariant(); +} + +QVariant +LayerModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role == Qt::ItemDataRole::DisplayRole) + { + if (orientation == Qt::Horizontal) + { + switch (section) + { + case static_cast(Column::DisplayMap1): + return tr("1"); + + case static_cast(Column::DisplayMap2): + return tr("2"); + + case static_cast(Column::DisplayMap3): + return tr("3"); + + case static_cast(Column::DisplayMap4): + return tr("4"); + + case static_cast(Column::Type): + return tr("Type"); + + case static_cast(Column::Enabled): + return tr("Enabled"); + + case static_cast(Column::Description): + return tr("Description"); + + default: + break; + } + } + } + else if (role == Qt::ItemDataRole::ToolTipRole) + { + switch (section) + { + case static_cast(Column::Order): + return tr("Order"); + + case static_cast(Column::DisplayMap1): + return tr("Display on Map 1"); + + case static_cast(Column::DisplayMap2): + return tr("Display on Map 2"); + + case static_cast(Column::DisplayMap3): + return tr("Display on Map 3"); + + case static_cast(Column::DisplayMap4): + return tr("Display on Map 4"); + + default: + break; + } + } + else if (role == Qt::ItemDataRole::SizeHintRole) + { + switch (section) + { + case static_cast(Column::DisplayMap1): + case static_cast(Column::DisplayMap2): + case static_cast(Column::DisplayMap3): + case static_cast(Column::DisplayMap4): + { + 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 LayerModel::setData(const QModelIndex& index, + const QVariant& value, + int role) +{ + if (!index.isValid() || index.row() < 0 || + static_cast(index.row()) >= p->layers_.size()) + { + return false; + } + + auto& layer = p->layers_.at(index.row()); + bool result = false; + + switch (index.column()) + { + case static_cast(Column::DisplayMap1): + case static_cast(Column::DisplayMap2): + case static_cast(Column::DisplayMap3): + case static_cast(Column::DisplayMap4): + if (role == Qt::ItemDataRole::CheckStateRole) + { + layer.displayed_[index.column() - + static_cast(Column::DisplayMap1)] = + value.toBool(); + result = true; + } + break; + + default: + break; + } + + if (result) + { + Q_EMIT dataChanged(index, index); + } + + return result; +} + +QStringList LayerModel::mimeTypes() const +{ + return {kMimeFormat}; +} + +QMimeData* LayerModel::mimeData(const QModelIndexList& indexes) const +{ + // Get parent QMimeData + QMimeData* mimeData = QAbstractTableModel::mimeData(indexes); + + // Generate LayerModel data + QByteArray data {}; + QDataStream stream(&data, QIODevice::WriteOnly); + std::set rows {}; + + for (auto& index : indexes) + { + if (!rows.contains(index.row())) + { + rows.insert(index.row()); + stream << index.row(); + } + } + + // Set LayerModel data in QMimeData + mimeData->setData(kMimeFormat, data); + + return mimeData; +} + +bool LayerModel::dropMimeData(const QMimeData* data, + Qt::DropAction /* action */, + int /* row */, + int /* column */, + const QModelIndex& parent) +{ + QByteArray mimeData = data->data(kMimeFormat); + QDataStream stream(&mimeData, QIODevice::ReadOnly); + std::vector sourceRows {}; + + // Read source rows from QMimeData + while (!stream.atEnd()) + { + int sourceRow; + stream >> sourceRow; + sourceRows.push_back(sourceRow); + } + + // Ensure rows are in numerical order + std::sort(sourceRows.begin(), sourceRows.end()); + + if (sourceRows.back() >= static_cast(p->layers_.size())) + { + logger_->error("Cannot perform drop action, invalid source rows"); + return false; + } + + // Nothing to insert + if (sourceRows.empty()) + { + return false; + } + + // Create a copy of the layers to insert (don't insert in-place) + std::vector newLayers {}; + for (auto& sourceRow : sourceRows) + { + newLayers.push_back(p->layers_.at(sourceRow)); + } + + // Insert the copied layers + auto insertPosition = p->layers_.begin() + parent.row(); + beginInsertRows(QModelIndex(), + parent.row(), + parent.row() + static_cast(sourceRows.size()) - 1); + p->layers_.insert(insertPosition, newLayers.begin(), newLayers.end()); + endInsertRows(); + + return true; +} + +bool LayerModel::removeRows(int row, int count, const QModelIndex& parent) +{ + // Validate count + if (count <= 0) + { + return false; + } + + // Remove rows + auto erasePosition = p->layers_.begin() + row; + for (int i = 0; i < count; ++i) + { + if (erasePosition->movable_) + { + // Remove the current row if movable + beginRemoveRows(parent, row, row); + erasePosition = p->layers_.erase(erasePosition); + endRemoveRows(); + } + else + { + // Don't remove immovable rows + ++erasePosition; + ++row; + } + } + + return true; +} + +bool LayerModel::moveRows(const QModelIndex& sourceParent, + int sourceRow, + int count, + const QModelIndex& destinationParent, + int destinationChild) +{ + bool moved = false; + + if (sourceParent != destinationParent || // Only accept internal moves + count < 1 || // Minimum selection size of 1 + sourceRow < 0 || // Valid source row (start) + sourceRow + count > + static_cast(p->layers_.size()) || // Valid source row (end) + destinationChild < 0 || // Valid destination row + destinationChild > static_cast(p->layers_.size())) + { + return false; + } + + if (destinationChild < sourceRow) + { + // Move up + auto first = p->layers_.begin() + destinationChild; + auto middle = p->layers_.begin() + sourceRow; + auto last = middle + count; + + beginMoveRows(sourceParent, + sourceRow, + sourceRow + count - 1, + destinationParent, + destinationChild); + std::rotate(first, middle, last); + endMoveRows(); + + moved = true; + } + else if (sourceRow + count < destinationChild) + { + // Move down + auto first = p->layers_.begin() + sourceRow; + auto middle = first + count; + auto last = p->layers_.begin() + destinationChild; + + beginMoveRows(sourceParent, + sourceRow, + sourceRow + count - 1, + destinationParent, + destinationChild); + std::rotate(first, middle, last); + endMoveRows(); + + moved = true; + } + + return moved; +} + +void LayerModel::Impl::HandlePlacefileRemoved(const std::string& name) +{ + auto it = + std::find_if(layers_.begin(), + layers_.end(), + [&name](const auto& layer) + { + return layer.type_ == types::LayerType::Placefile && + std::get(layer.description_) == name; + }); + + if (it != layers_.end()) + { + // Placefile exists, delete row + const int row = std::distance(layers_.begin(), it); + + self_->beginRemoveRows(QModelIndex(), row, row); + layers_.erase(it); + self_->endRemoveRows(); + } +} + +void LayerModel::Impl::HandlePlacefileRenamed(const std::string& oldName, + const std::string& newName) +{ + auto it = std::find_if( + layers_.begin(), + layers_.end(), + [&oldName](const auto& layer) + { + return layer.type_ == types::LayerType::Placefile && + std::get(layer.description_) == oldName; + }); + + if (it != layers_.end()) + { + // Placefile exists, mark row as updated + const int row = std::distance(layers_.begin(), it); + QModelIndex topLeft = + self_->createIndex(row, static_cast(Column::Description)); + QModelIndex bottomRight = + self_->createIndex(row, static_cast(Column::Description)); + + // Rename placefile + it->description_ = newName; + + Q_EMIT self_->dataChanged(topLeft, bottomRight); + } + else + { + // Placefile doesn't exist, add row + AddPlacefile(newName); + } +} + +void LayerModel::Impl::HandlePlacefileUpdate(const std::string& name, + Column column) +{ + if (!placefilesInitialized_) + { + initialPlacefiles_.push_back(name); + } + + auto it = + std::find_if(layers_.begin(), + layers_.end(), + [&name](const auto& layer) + { + return layer.type_ == types::LayerType::Placefile && + std::get(layer.description_) == name; + }); + + if (it != layers_.end()) + { + // Placefile exists, mark row as updated + const int row = std::distance(layers_.begin(), it); + QModelIndex topLeft = self_->createIndex(row, static_cast(column)); + QModelIndex bottomRight = + self_->createIndex(row, static_cast(column)); + + Q_EMIT self_->dataChanged(topLeft, bottomRight); + } + else + { + // Placefile doesn't exist, add row + AddPlacefile(name); + } +} + +void LayerModel::Impl::AddPlacefile(const std::string& name) +{ + // Insert after color table + auto insertPosition = std::find_if( + layers_.begin(), + layers_.end(), + [](const types::LayerInfo& layerInfo) + { + return std::holds_alternative( + layerInfo.description_) && + std::get(layerInfo.description_) == + types::InformationLayer::ColorTable; + }); + if (insertPosition != layers_.end()) + { + ++insertPosition; + } + + // Placefile is new, add row + self_->beginInsertRows(QModelIndex(), 0, 0); + layers_.insert(insertPosition, {types::LayerType::Placefile, name, true}); + self_->endInsertRows(); +} + +template +std::array tag_invoke(boost::json::value_to_tag>, + const boost::json::value& jv) +{ + std::array array {}; + boost::json::array jsonArray = jv.as_array(); + + for (std::size_t i = 0; i < n && i < jsonArray.size(); ++i) + { + array[i] = jsonArray[i]; + } + + return array; +} + +std::shared_ptr LayerModel::Instance() +{ + static std::weak_ptr layerModelReference_ {}; + static std::mutex instanceMutex_ {}; + + std::unique_lock lock(instanceMutex_); + + std::shared_ptr layerModel = layerModelReference_.lock(); + + if (layerModel == nullptr) + { + layerModel = std::make_shared(); + layerModelReference_ = layerModel; + } + + return layerModel; +} + +} // namespace model +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/model/layer_model.hpp b/scwx-qt/source/scwx/qt/model/layer_model.hpp new file mode 100644 index 00000000..426615c7 --- /dev/null +++ b/scwx-qt/source/scwx/qt/model/layer_model.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include +#include + +#include + +#include + +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 + 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 Instance(); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace model +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/model/placefile_model.cpp b/scwx-qt/source/scwx/qt/model/placefile_model.cpp new file mode 100644 index 00000000..9f08e0ec --- /dev/null +++ b/scwx-qt/source/scwx/qt/model/placefile_model.cpp @@ -0,0 +1,377 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +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(PlacefileModel::Column::Enabled); +static constexpr int kLastColumn = + static_cast(PlacefileModel::Column::Placefile); +static constexpr int kNumColumns = kLastColumn - kFirstColumn + 1; + +class PlacefileModelImpl +{ +public: + explicit PlacefileModelImpl() {} + ~PlacefileModelImpl() = default; + + std::shared_ptr placefileManager_ { + manager::PlacefileManager::Instance()}; + + std::vector placefileNames_ {}; +}; + +PlacefileModel::PlacefileModel(QObject* parent) : + QAbstractTableModel(parent), p(std::make_unique()) +{ + 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(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(Column::Enabled): + case static_cast(Column::Thresholds): + flags |= Qt::ItemFlag::ItemIsUserCheckable | Qt::ItemFlag::ItemIsEditable; + break; + + case static_cast(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(index.row()) >= p->placefileNames_.size()) + { + return QVariant(); + } + + const auto& placefileName = p->placefileNames_.at(index.row()); + + switch (index.column()) + { + case static_cast(Column::Enabled): + if (role == Qt::ItemDataRole::ToolTipRole) + { + return p->placefileManager_->placefile_enabled(placefileName) ? + enabledString : + disabledString; + } + else if (role == Qt::ItemDataRole::CheckStateRole) + { + return static_cast( + 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(Column::Thresholds): + if (role == Qt::ItemDataRole::ToolTipRole) + { + return p->placefileManager_->placefile_thresholded(placefileName) ? + thresholdsEnabledString : + thresholdsDisabledString; + } + else if (role == Qt::ItemDataRole::CheckStateRole) + { + return static_cast( + 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(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(Column::Enabled): + return tr("E"); + + case static_cast(Column::Thresholds): + return tr("T"); + + case static_cast(Column::Placefile): + return tr("Placefile"); + + default: + break; + } + } + } + else if (role == Qt::ItemDataRole::ToolTipRole) + { + switch (section) + { + case static_cast(Column::Enabled): + return tr("Enabled"); + + case static_cast(Column::Thresholds): + return tr("Thresholds"); + + default: + break; + } + } + else if (role == Qt::ItemDataRole::SizeHintRole) + { + switch (section) + { + case static_cast(Column::Enabled): + case static_cast(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(index.row()) >= p->placefileNames_.size()) + { + return false; + } + + const auto& placefileName = p->placefileNames_.at(index.row()); + bool result = false; + + switch (index.column()) + { + case static_cast(Column::Enabled): + if (role == Qt::ItemDataRole::CheckStateRole) + { + p->placefileManager_->set_placefile_enabled(placefileName, + value.toBool()); + result = true; + } + break; + + case static_cast(Column::Thresholds): + if (role == Qt::ItemDataRole::CheckStateRole) + { + p->placefileManager_->set_placefile_thresholded(placefileName, + value.toBool()); + result = true; + } + break; + + case static_cast(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(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(p->placefileNames_.size()); + beginInsertRows(QModelIndex(), newIndex, newIndex); + p->placefileNames_.push_back(name); + endInsertRows(); + } +} + +} // namespace model +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/model/placefile_model.hpp b/scwx-qt/source/scwx/qt/model/placefile_model.hpp new file mode 100644 index 00000000..5dc4fa14 --- /dev/null +++ b/scwx-qt/source/scwx/qt/model/placefile_model.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include +#include + +#include + +#include + +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 p; +}; + +} // namespace model +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index 8f2d3e74..997d4e06 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -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 antiAliasingEnabled_ {"anti_aliasing_enabled"}; SettingsVariable debugEnabled_ {"debug_enabled"}; SettingsVariable defaultAlertAction_ {"default_alert_action"}; SettingsVariable defaultRadarSite_ {"default_radar_site"}; @@ -120,9 +122,10 @@ public: }; GeneralSettings::GeneralSettings() : - SettingsCategory("general"), p(std::make_unique()) + SettingsCategory("general"), p(std::make_unique()) { - 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& GeneralSettings::anti_aliasing_enabled() const +{ + return p->antiAliasingEnabled_; +} + SettingsVariable& 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_ && diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.hpp b/scwx-qt/source/scwx/qt/settings/general_settings.hpp index 375d887e..57045ac6 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.hpp @@ -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& anti_aliasing_enabled() const; SettingsVariable& debug_enabled() const; SettingsVariable& default_alert_action() const; SettingsVariable& default_radar_site() const; @@ -41,13 +40,16 @@ public: SettingsVariable& maptiler_api_key() const; SettingsVariable& update_notifications_enabled() const; + static GeneralSettings& Instance(); + friend bool operator==(const GeneralSettings& lhs, const GeneralSettings& rhs); bool Shutdown(); private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; } // namespace settings diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.cpp b/scwx-qt/source/scwx/qt/settings/map_settings.cpp index 8edd1c31..2d8e93b0 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.cpp @@ -35,7 +35,7 @@ static const std::string kDefaultRadarProductGroupString_ = "L3"; static const std::array kDefaultRadarProduct_ { "N0B", "N0G", "N0C", "N0X"}; -class MapSettingsImpl +class MapSettings::Impl { public: struct MapData @@ -47,7 +47,7 @@ public: SettingsVariable 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 map_ {}; std::vector variables_ {}; }; MapSettings::MapSettings() : - SettingsCategory("maps"), p(std::make_unique()) + SettingsCategory("maps"), p(std::make_unique()) { 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 diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.hpp b/scwx-qt/source/scwx/qt/settings/map_settings.hpp index fd0c74dd..c8726491 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.hpp @@ -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 p; + class Impl; + std::unique_ptr p; }; } // namespace settings diff --git a/scwx-qt/source/scwx/qt/settings/palette_settings.cpp b/scwx-qt/source/scwx/qt/settings/palette_settings.cpp index 8edf2cf2..f041c078 100644 --- a/scwx-qt/source/scwx/qt/settings/palette_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/palette_settings.cpp @@ -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 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()) + SettingsCategory("palette"), p(std::make_unique()) { RegisterVariables(p->variables_); SetDefaults(); @@ -200,6 +200,12 @@ const std::vector& 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_; diff --git a/scwx-qt/source/scwx/qt/settings/palette_settings.hpp b/scwx-qt/source/scwx/qt/settings/palette_settings.hpp index 948decf5..c0f7985a 100644 --- a/scwx-qt/source/scwx/qt/settings/palette_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/palette_settings.hpp @@ -14,8 +14,6 @@ namespace qt namespace settings { -class PaletteSettingsImpl; - class PaletteSettings : public SettingsCategory { public: @@ -34,11 +32,14 @@ public: static const std::vector& alert_phenomena(); + static PaletteSettings& Instance(); + friend bool operator==(const PaletteSettings& lhs, const PaletteSettings& rhs); private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; } // namespace settings diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.cpp b/scwx-qt/source/scwx/qt/settings/settings_category.cpp index 75daf41d..e6c929f8 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.cpp @@ -2,6 +2,8 @@ #include #include +#include + namespace scwx { namespace qt @@ -21,6 +23,8 @@ public: const std::string name_; + std::vector>> + subcategoryArrays_; std::vector 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& subcategories) +{ + auto& newSubcategories = p->subcategoryArrays_.emplace_back( + name, std::vector {}); + + std::transform(subcategories.begin(), + subcategories.end(), + std::back_inserter(newSubcategories.second), + [](SettingsCategory& subcategory) { return &subcategory; }); +} + void SettingsCategory::RegisterVariables( std::initializer_list variables) { diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.hpp b/scwx-qt/source/scwx/qt/settings/settings_category.hpp index cc07a7d7..d7c86abd 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.hpp @@ -50,7 +50,8 @@ public: */ virtual void WriteJson(boost::json::object& json) const; -protected: + void RegisterSubcategoryArray(const std::string& name, + std::vector& subcategories); void RegisterVariables(std::initializer_list variables); void RegisterVariables(std::vector variables); diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp index dd729da0..b7133537 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -26,16 +27,21 @@ template class SettingsInterface::Impl { public: - explicit Impl() + explicit Impl(SettingsInterface* self) : self_ {self} { context_->moveToThread(QCoreApplication::instance()->thread()); } ~Impl() {} + template + void SetWidgetText(U* widget, const T& currentValue); + void UpdateEditWidget(); void UpdateResetButton(); + SettingsInterface* self_; + SettingsVariable* variable_ {nullptr}; bool stagedValid_ {true}; @@ -49,17 +55,27 @@ public: template SettingsInterface::SettingsInterface() : - SettingsInterfaceBase(), p(std::make_unique()) + SettingsInterfaceBase(), p(std::make_unique(this)) { } template SettingsInterface::~SettingsInterface() = default; template -SettingsInterface::SettingsInterface(SettingsInterface&&) noexcept = default; +SettingsInterface::SettingsInterface(SettingsInterface&& o) noexcept : + p {std::move(o.p)} +{ + p->self_ = this; +} + template SettingsInterface& -SettingsInterface::operator=(SettingsInterface&&) noexcept = default; +SettingsInterface::operator=(SettingsInterface&& o) noexcept +{ + p = std::move(o.p); + p->self_ = this; + return *this; +} template void SettingsInterface::SetSettingsVariable(SettingsVariable& variable) @@ -73,6 +89,27 @@ SettingsVariable* SettingsInterface::GetSettingsVariable() const return p->variable_; } +template +bool SettingsInterface::IsDefault() +{ + bool isDefault = false; + + const std::optional 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 bool SettingsInterface::Commit() { @@ -95,6 +132,14 @@ void SettingsInterface::StageDefault() p->UpdateResetButton(); } +template +void SettingsInterface::StageValue(const T& value) +{ + p->variable_->StageValue(value); + p->UpdateEditWidget(); + p->UpdateResetButton(); +} + template void SettingsInterface::SetEditWidget(QWidget* widget) { @@ -105,6 +150,11 @@ void SettingsInterface::SetEditWidget(QWidget* widget) p->editWidget_ = widget; + if (widget == nullptr) + { + return; + } + if (QLineEdit* lineEdit = dynamic_cast(widget)) { if constexpr (std::is_same_v) @@ -274,33 +324,36 @@ void SettingsInterface::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 @@ -317,6 +370,39 @@ void SettingsInterface::SetMapToValueFunction( p->mapToValue_ = function; } +template +template +void SettingsInterface::Impl::SetWidgetText(U* widget, const T& currentValue) +{ + if constexpr (std::is_integral_v) + { + widget->setText(QString::number(currentValue)); + } + else if constexpr (std::is_same_v) + { + if (mapFromValue_ != nullptr) + { + widget->setText(QString::fromStdString(mapFromValue_(currentValue))); + } + else + { + widget->setText(QString::fromStdString(currentValue)); + } + } + else if constexpr (std::is_same_v>) + { + if (mapFromValue_ != nullptr) + { + widget->setText(QString::fromStdString(mapFromValue_(currentValue))); + } + else + { + widget->setText(QString::fromStdString( + fmt::format("{}", fmt::join(currentValue, ", ")))); + } + } +} + template void SettingsInterface::Impl::UpdateEditWidget() { @@ -327,35 +413,11 @@ void SettingsInterface::Impl::UpdateEditWidget() if (QLineEdit* lineEdit = dynamic_cast(editWidget_)) { - if constexpr (std::is_integral_v) - { - lineEdit->setText(QString::number(currentValue)); - } - else if constexpr (std::is_same_v) - { - if (mapFromValue_ != nullptr) - { - lineEdit->setText( - QString::fromStdString(mapFromValue_(currentValue))); - } - else - { - lineEdit->setText(QString::fromStdString(currentValue)); - } - } - else if constexpr (std::is_same_v>) - { - 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(editWidget_)) + { + SetWidgetText(label, currentValue); } else if (QCheckBox* checkBox = dynamic_cast(editWidget_)) { @@ -391,20 +453,9 @@ void SettingsInterface::Impl::UpdateEditWidget() template void SettingsInterface::Impl::UpdateResetButton() { - const std::optional 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()); } } diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp index 030b8996..f5f5bb4a 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp @@ -45,6 +45,14 @@ public: */ SettingsVariable* 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; +template class SettingsInterface; template class SettingsInterface; template class SettingsInterface; diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface_base.hpp b/scwx-qt/source/scwx/qt/settings/settings_interface_base.hpp index 97ae442d..d0dc2ff2 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface_base.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface_base.hpp @@ -24,10 +24,18 @@ 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. - * + * * @return true if the staged value was committed, false if no staged value * is present. */ diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable.cpp b/scwx-qt/source/scwx/qt/settings/settings_variable.cpp index 270a2eed..1f7661c0 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable.cpp @@ -239,6 +239,12 @@ std::optional SettingsVariable::GetStaged() const return p->staged_; } +template +T SettingsVariable::GetStagedOrValue() const +{ + return p->staged_.value_or(GetValue()); +} + template T SettingsVariable::GetDefault() const { diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable.hpp b/scwx-qt/source/scwx/qt/settings/settings_variable.hpp index c7999c0d..c05c543f 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable.hpp @@ -103,6 +103,14 @@ public: */ std::optional 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. diff --git a/scwx-qt/source/scwx/qt/settings/text_settings.cpp b/scwx-qt/source/scwx/qt/settings/text_settings.cpp new file mode 100644 index 00000000..c6c221d4 --- /dev/null +++ b/scwx-qt/source/scwx/qt/settings/text_settings.cpp @@ -0,0 +1,198 @@ +#include +#include + +#include + +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 + kDefaultFontFamily_ { + {types::FontCategory::Default, kAlteDIN1451Mittelscrhift_}, + {types::FontCategory::Tooltip, kInconsolata_}}; +static const std::unordered_map + kDefaultFontStyle_ {{types::FontCategory::Default, kRegular_}, + {types::FontCategory::Tooltip, kRegular_}}; +static const std::unordered_map + kDefaultFontPointSize_ {{types::FontCategory::Default, 12.0}, + {types::FontCategory::Tooltip, 10.5}}; + +class TextSettings::Impl +{ +public: + struct FontData + { + SettingsVariable fontFamily_ {"font_family"}; + SettingsVariable fontStyle_ {"font_style"}; + SettingsVariable 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 fontData_ {}; + std::vector fontSettings_ {}; + + SettingsVariable hoverTextWrap_ {"hover_text_wrap"}; + SettingsVariable tooltipMethod_ {"tooltip_method"}; + + SettingsVariable placefileTextDropShadowEnabled_ { + "placefile_text_drop_shadow_enabled"}; +}; + +TextSettings::TextSettings() : + SettingsCategory("text"), p(std::make_unique(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& +TextSettings::font_family(types::FontCategory fontCategory) const +{ + return p->fontData_.at(fontCategory).fontFamily_; +} + +SettingsVariable& +TextSettings::font_style(types::FontCategory fontCategory) const +{ + return p->fontData_.at(fontCategory).fontStyle_; +} + +SettingsVariable& +TextSettings::font_point_size(types::FontCategory fontCategory) const +{ + return p->fontData_.at(fontCategory).fontPointSize_; +} + +SettingsVariable& TextSettings::hover_text_wrap() const +{ + return p->hoverTextWrap_; +} + +SettingsVariable& TextSettings::placefile_text_drop_shadow_enabled() const +{ + return p->placefileTextDropShadowEnabled_; +} + +SettingsVariable& 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 diff --git a/scwx-qt/source/scwx/qt/settings/text_settings.hpp b/scwx-qt/source/scwx/qt/settings/text_settings.hpp new file mode 100644 index 00000000..ac300abf --- /dev/null +++ b/scwx-qt/source/scwx/qt/settings/text_settings.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include + +#include +#include + +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& + font_family(types::FontCategory fontCategory) const; + SettingsVariable& + font_style(types::FontCategory fontCategory) const; + SettingsVariable& + font_point_size(types::FontCategory fontCategory) const; + + SettingsVariable& hover_text_wrap() const; + SettingsVariable& placefile_text_drop_shadow_enabled() const; + SettingsVariable& tooltip_method() const; + + static TextSettings& Instance(); + + friend bool operator==(const TextSettings& lhs, const TextSettings& rhs); + +private: + class Impl; + + std::unique_ptr p; +}; + +} // namespace settings +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/font_types.hpp b/scwx-qt/source/scwx/qt/types/font_types.hpp index 95a62469..6fc8c506 100644 --- a/scwx-qt/source/scwx/qt/types/font_types.hpp +++ b/scwx-qt/source/scwx/qt/types/font_types.hpp @@ -1,5 +1,31 @@ #pragma once +#include + +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; + +} // namespace dimension + +UNIT_ADD(font_size, + pixels, + px, + conversion_factor, dimension::font_size>) +UNIT_ADD(font_size, points, pt, conversion_factor, 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 diff --git a/scwx-qt/source/scwx/qt/types/imgui_font.cpp b/scwx-qt/source/scwx/qt/types/imgui_font.cpp new file mode 100644 index 00000000..e6f22ad1 --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/imgui_font.cpp @@ -0,0 +1,83 @@ +// Disable strncpy warning +#define _CRT_SECURE_NO_WARNINGS + +#include +#include +#include + +#include +#include + +#include + +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& fontData, + units::font_size::pixels size) : + fontName_ {fontName}, size_ {size} + { + CreateImGuiFont(fontData); + } + + ~Impl() {} + + void CreateImGuiFont(const std::vector& fontData); + + const std::string fontName_; + const units::font_size::pixels size_; + + ImFont* imFont_ {nullptr}; +}; + +ImGuiFont::ImGuiFont(const std::string& fontName, + const std::vector& fontData, + units::font_size::pixels size) : + p(std::make_unique(fontName, fontData, size)) +{ +} +ImGuiFont::~ImGuiFont() = default; + +void ImGuiFont::Impl::CreateImGuiFont(const std::vector& fontData) +{ + logger_->debug("Creating Font: {}", fontName_); + + ImFontAtlas* fontAtlas = model::ImGuiContextModel::Instance().font_atlas(); + ImFontConfig fontConfig {}; + + const float sizePixels = static_cast(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(static_cast(fontData.data())), + static_cast(std::clamp( + fontData.size(), 0, std::numeric_limits::max())), + sizePixels, + &fontConfig); +} + +ImFont* ImGuiFont::font() +{ + return p->imFont_; +} + +} // namespace types +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/imgui_font.hpp b/scwx-qt/source/scwx/qt/types/imgui_font.hpp new file mode 100644 index 00000000..ace8ba09 --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/imgui_font.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +#include + +struct ImFont; + +namespace scwx +{ +namespace qt +{ +namespace types +{ + +class ImGuiFont +{ +public: + explicit ImGuiFont(const std::string& fontName, + const std::vector& fontData, + units::font_size::pixels 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 p; +}; + +} // namespace types +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/layer_types.cpp b/scwx-qt/source/scwx/qt/types/layer_types.cpp new file mode 100644 index 00000000..c25beb12 --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/layer_types.cpp @@ -0,0 +1,254 @@ +#include + +#include + +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace types +{ + +static const std::unordered_map layerTypeName_ { + {LayerType::Map, "Map"}, + {LayerType::Radar, "Radar"}, + {LayerType::Alert, "Alert"}, + {LayerType::Placefile, "Placefile"}, + {LayerType::Information, "Information"}, + {LayerType::Data, "Data"}, + {LayerType::Unknown, "?"}}; + +static const std::unordered_map dataLayerName_ { + {DataLayer::RadarRange, "Radar Range"}, {DataLayer::Unknown, "?"}}; + +static const std::unordered_map + informationLayerName_ {{InformationLayer::MapOverlay, "Map Overlay"}, + {InformationLayer::ColorTable, "Color Table"}, + {InformationLayer::Unknown, "?"}}; + +static const std::unordered_map mapLayerName_ { + {MapLayer::MapSymbology, "Map Symbology"}, + {MapLayer::MapUnderlay, "Map Underlay"}, + {MapLayer::Unknown, "?"}}; + +static const std::string kTypeName_ {"type"}; +static const std::string kDescriptionName_ {"description"}; +static const std::string kMovableName_ {"movable"}; +static const std::string kDisplayedName_ {"displayed"}; + +LayerType GetLayerType(const std::string& name) +{ + auto result = + std::find_if(layerTypeName_.cbegin(), + layerTypeName_.cend(), + [&](const std::pair& pair) -> bool + { return boost::iequals(pair.second, name); }); + + if (result != layerTypeName_.cend()) + { + return result->first; + } + else + { + return LayerType::Unknown; + } +} + +std::string GetLayerTypeName(LayerType layerType) +{ + return layerTypeName_.at(layerType); +} + +DataLayer GetDataLayer(const std::string& name) +{ + auto result = + std::find_if(dataLayerName_.cbegin(), + dataLayerName_.cend(), + [&](const std::pair& pair) -> bool + { return boost::iequals(pair.second, name); }); + + if (result != dataLayerName_.cend()) + { + return result->first; + } + else + { + return DataLayer::Unknown; + } +} + +std::string GetDataLayerName(DataLayer layer) +{ + return dataLayerName_.at(layer); +} + +InformationLayer GetInformationLayer(const std::string& name) +{ + auto result = std::find_if( + informationLayerName_.cbegin(), + informationLayerName_.cend(), + [&](const std::pair& pair) -> bool + { return boost::iequals(pair.second, name); }); + + if (result != informationLayerName_.cend()) + { + return result->first; + } + else + { + return InformationLayer::Unknown; + } +} + +std::string GetInformationLayerName(InformationLayer layer) +{ + return informationLayerName_.at(layer); +} + +MapLayer GetMapLayer(const std::string& name) +{ + auto result = + std::find_if(mapLayerName_.cbegin(), + mapLayerName_.cend(), + [&](const std::pair& pair) -> bool + { return boost::iequals(pair.second, name); }); + + if (result != mapLayerName_.cend()) + { + return result->first; + } + else + { + return MapLayer::Unknown; + } +} + +std::string GetMapLayerName(MapLayer layer) +{ + return mapLayerName_.at(layer); +} + +std::string GetLayerDescriptionName(LayerDescription description) +{ + if (std::holds_alternative(description)) + { + return std::get(description); + } + else if (std::holds_alternative(description)) + { + return GetDataLayerName(std::get(description)); + } + else if (std::holds_alternative(description)) + { + return GetInformationLayerName(std::get(description)); + } + else if (std::holds_alternative(description)) + { + return GetMapLayerName(std::get(description)); + } + else if (std::holds_alternative(description)) + { + return awips::GetPhenomenonCode(std::get(description)); + } + else if (std::holds_alternative(description)) + { + return ""; + } + else + { + return "?"; + } +} + +std::string GetLayerName(types::LayerType type, + types::LayerDescription description) +{ + return fmt::format("scwx.{}.{}", + types::GetLayerTypeName(type), + types::GetLayerDescriptionName(description)); +} + +void tag_invoke(boost::json::value_from_tag, + boost::json::value& jv, + const LayerInfo& record) +{ + std::string description {}; + + if (std::holds_alternative(record.description_)) + { + description = awips::GetPhenomenonCode( + std::get(record.description_)); + } + else if (std::holds_alternative(record.description_)) + { + description = GetDataLayerName(std::get(record.description_)); + } + else if (std::holds_alternative(record.description_)) + { + description = GetInformationLayerName( + std::get(record.description_)); + } + else if (std::holds_alternative(record.description_)) + { + description = GetMapLayerName(std::get(record.description_)); + } + else if (std::holds_alternative(record.description_)) + { + description = std::get(record.description_); + } + + jv = {{kTypeName_, GetLayerTypeName(record.type_)}, + {kDescriptionName_, description}, + {kMovableName_, record.movable_}, + {kDisplayedName_, boost::json::value_from(record.displayed_)}}; +} + +LayerInfo tag_invoke(boost::json::value_to_tag, + const boost::json::value& jv) +{ + const LayerType layerType = + GetLayerType(boost::json::value_to(jv.at(kTypeName_))); + const std::string descriptionName = + boost::json::value_to(jv.at(kDescriptionName_)); + + LayerDescription description {}; + + if (layerType == LayerType::Map) + { + description = GetMapLayer(descriptionName); + } + else if (layerType == LayerType::Information) + { + description = GetInformationLayer(descriptionName); + } + else if (layerType == LayerType::Data) + { + description = GetDataLayer(descriptionName); + } + else if (layerType == LayerType::Radar) + { + description = std::monostate {}; + } + else if (layerType == LayerType::Alert) + { + description = awips::GetPhenomenon(descriptionName); + } + else + { + description = descriptionName; + } + + return LayerInfo { + layerType, + description, + jv.at(kMovableName_).as_bool(), + boost::json::value_to>(jv.at(kDisplayedName_))}; +} + +} // namespace types +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/layer_types.hpp b/scwx-qt/source/scwx/qt/types/layer_types.hpp new file mode 100644 index 00000000..6a00d03b --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/layer_types.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace types +{ + +enum class LayerType +{ + Map, + Radar, + Alert, + Placefile, + Information, + Data, + Unknown +}; + +enum class DataLayer +{ + RadarRange, + Unknown +}; +typedef scwx::util:: + Iterator + DataLayerIterator; + +enum class InformationLayer +{ + MapOverlay, + ColorTable, + Unknown +}; + +enum class MapLayer +{ + MapSymbology, + MapUnderlay, + Unknown +}; + +typedef std::variant + LayerDescription; + +struct LayerInfo +{ + LayerType type_; + LayerDescription description_; + bool movable_ {true}; + std::array displayed_ {true, true, true, true}; +}; + +typedef boost::container::stable_vector LayerVector; + +LayerType GetLayerType(const std::string& name); +std::string GetLayerTypeName(LayerType layerType); + +DataLayer GetDataLayer(const std::string& name); +std::string GetDataLayerName(DataLayer layer); + +InformationLayer GetInformationLayer(const std::string& name); +std::string GetInformationLayerName(InformationLayer layer); + +MapLayer GetMapLayer(const std::string& name); +std::string GetMapLayerName(MapLayer layer); + +std::string GetLayerDescriptionName(LayerDescription description); + +std::string GetLayerName(LayerType type, LayerDescription description); + +void tag_invoke(boost::json::value_from_tag, + boost::json::value& jv, + const LayerInfo& record); +LayerInfo tag_invoke(boost::json::value_to_tag, + const boost::json::value& jv); + +} // namespace types +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/text_types.cpp b/scwx-qt/source/scwx/qt/types/text_types.cpp new file mode 100644 index 00000000..f994dab9 --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/text_types.cpp @@ -0,0 +1,73 @@ +#include + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace types +{ + +static const std::unordered_map fontCategoryName_ { + {FontCategory::Default, "Default"}, + {FontCategory::Tooltip, "Tooltip"}, + {FontCategory::Unknown, "?"}}; + +static const std::unordered_map tooltipMethodName_ { + {TooltipMethod::ImGui, "ImGui"}, + {TooltipMethod::QToolTip, "Native Tooltip"}, + {TooltipMethod::QLabel, "Floating Label"}, + {TooltipMethod::Unknown, "?"}}; + +FontCategory GetFontCategory(const std::string& name) +{ + auto result = + std::find_if(fontCategoryName_.cbegin(), + fontCategoryName_.cend(), + [&](const std::pair& pair) -> bool + { return boost::iequals(pair.second, name); }); + + if (result != fontCategoryName_.cend()) + { + return result->first; + } + else + { + return FontCategory::Unknown; + } +} + +std::string GetFontCategoryName(FontCategory fontCategory) +{ + return fontCategoryName_.at(fontCategory); +} + +TooltipMethod GetTooltipMethod(const std::string& name) +{ + auto result = std::find_if( + tooltipMethodName_.cbegin(), + tooltipMethodName_.cend(), + [&](const std::pair& pair) -> bool + { return boost::iequals(pair.second, name); }); + + if (result != tooltipMethodName_.cend()) + { + return result->first; + } + else + { + return TooltipMethod::Unknown; + } +} + +std::string GetTooltipMethodName(TooltipMethod tooltipMethod) +{ + return tooltipMethodName_.at(tooltipMethod); +} + +} // namespace types +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/text_types.hpp b/scwx-qt/source/scwx/qt/types/text_types.hpp new file mode 100644 index 00000000..07c1ea00 --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/text_types.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace types +{ + +enum class FontCategory +{ + Default, + Tooltip, + Unknown +}; +typedef scwx::util:: + Iterator + FontCategoryIterator; + +enum class TooltipMethod +{ + ImGui, + QToolTip, + QLabel, + Unknown +}; +typedef scwx::util:: + Iterator + TooltipMethodIterator; + +FontCategory GetFontCategory(const std::string& name); +std::string GetFontCategoryName(FontCategory fontCategory); +TooltipMethod GetTooltipMethod(const std::string& name); +std::string GetTooltipMethodName(TooltipMethod tooltipMethod); + +} // namespace types +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/about_dialog.cpp b/scwx-qt/source/scwx/qt/ui/about_dialog.cpp index a42be06f..42ec4e32 100644 --- a/scwx-qt/source/scwx/qt/ui/about_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/about_dialog.cpp @@ -1,7 +1,7 @@ #include "about_dialog.hpp" #include "ui_about_dialog.h" #include -#include +#include #include @@ -27,7 +27,7 @@ AboutDialog::AboutDialog(QWidget* parent) : ui->setupUi(this); int titleFontId = - manager::ResourceManager::FontId(types::Font::din1451alt_g); + manager::FontManager::Instance().GetFontId(types::Font::din1451alt_g); QString titleFontFamily = QFontDatabase::applicationFontFamilies(titleFontId).at(0); QFont titleFont(titleFontFamily, 14); diff --git a/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp b/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp index 5efca78a..e827469c 100644 --- a/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp @@ -1,10 +1,10 @@ #include "alert_dock_widget.hpp" #include "ui_alert_dock_widget.h" -#include #include #include #include +#include #include #include #include @@ -175,10 +175,10 @@ void AlertDockWidgetImpl::ConnectSignals() // If an item is selected if (selectedAlertKey_ != types::TextEventKey {}) { - types::AlertAction alertAction = types::GetAlertAction( - manager::SettingsManager::general_settings() - .default_alert_action() - .GetValue()); + types::AlertAction alertAction = + types::GetAlertAction(settings::GeneralSettings::Instance() + .default_alert_action() + .GetValue()); switch (alertAction) { diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp index 34b1001a..f19be01f 100644 --- a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp @@ -1,7 +1,7 @@ #include "animation_dock_widget.hpp" #include "ui_animation_dock_widget.h" -#include +#include #include #include @@ -101,7 +101,7 @@ AnimationDockWidget::AnimationDockWidget(QWidget* parent) : maxDateTimer->start(15000); // Set loop defaults - auto& generalSettings = manager::SettingsManager::general_settings(); + auto& generalSettings = settings::GeneralSettings::Instance(); ui->loopTimeSpinBox->setValue(generalSettings.loop_time().GetValue()); ui->loopSpeedSpinBox->setValue(generalSettings.loop_speed().GetValue()); ui->loopDelaySpinBox->setValue(generalSettings.loop_delay().GetValue() * @@ -175,7 +175,7 @@ void AnimationDockWidgetImpl::ConnectSignals() self_, [this](int i) { - manager::SettingsManager::general_settings().loop_time().StageValue(i); + settings::GeneralSettings::Instance().loop_time().StageValue(i); Q_EMIT self_->LoopTimeChanged(std::chrono::minutes(i)); }); QObject::connect( @@ -184,8 +184,7 @@ void AnimationDockWidgetImpl::ConnectSignals() self_, [this](double d) { - manager::SettingsManager::general_settings().loop_speed().StageValue( - d); + settings::GeneralSettings::Instance().loop_speed().StageValue(d); Q_EMIT self_->LoopSpeedChanged(d); }); QObject::connect( @@ -194,7 +193,7 @@ void AnimationDockWidgetImpl::ConnectSignals() self_, [this](double d) { - manager::SettingsManager::general_settings().loop_delay().StageValue( + settings::GeneralSettings::Instance().loop_delay().StageValue( static_cast(d * 1000.0)); Q_EMIT self_->LoopDelayChanged(std::chrono::milliseconds( static_cast(d * 1000.0))); diff --git a/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.cpp b/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.cpp index 18011b7f..fcbed5d6 100644 --- a/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -50,6 +51,8 @@ public: model::ImGuiContextModel::Instance().DestroyContext(contextName_); } + void ImGuiCheckFonts(); + ImGuiDebugWidget* self_; ImGuiContext* context_; std::string contextName_; @@ -58,6 +61,7 @@ public: std::set renderedSet_ {}; bool imGuiRendererInitialized_ {false}; + std::uint64_t imGuiFontsBuildCount_ {}; }; ImGuiDebugWidget::ImGuiDebugWidget(QWidget* parent) : @@ -102,6 +106,8 @@ void ImGuiDebugWidget::initializeGL() // Initialize ImGui OpenGL3 backend ImGui::SetCurrentContext(p->context_); ImGui_ImplOpenGL3_Init(); + p->imGuiFontsBuildCount_ = + manager::FontManager::Instance().imgui_fonts_build_count(); p->imGuiRendererInitialized_ = true; } @@ -109,9 +115,13 @@ void ImGuiDebugWidget::paintGL() { ImGui::SetCurrentContext(p->currentContext_); + // Lock ImGui font atlas prior to new ImGui frame + std::shared_lock imguiFontAtlasLock { + manager::FontManager::Instance().imgui_font_atlas_mutex()}; + ImGui_ImplQt_NewFrame(this); ImGui_ImplOpenGL3_NewFrame(); - + p->ImGuiCheckFonts(); ImGui::NewFrame(); if (!p->renderedSet_.contains(p->currentContext_)) @@ -131,6 +141,29 @@ void ImGuiDebugWidget::paintGL() ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + + // Unlock ImGui font atlas after rendering + imguiFontAtlasLock.unlock(); +} + +void ImGuiDebugWidgetImpl::ImGuiCheckFonts() +{ + // Update ImGui Fonts if required + std::uint64_t currentImGuiFontsBuildCount = + manager::FontManager::Instance().imgui_fonts_build_count(); + + if ((context_ == currentContext_ && + imGuiFontsBuildCount_ != currentImGuiFontsBuildCount) || + !model::ImGuiContextModel::Instance().font_atlas()->IsBuilt()) + { + ImGui_ImplOpenGL3_DestroyFontsTexture(); + ImGui_ImplOpenGL3_CreateFontsTexture(); + } + + if (context_ == currentContext_) + { + imGuiFontsBuildCount_ = currentImGuiFontsBuildCount; + } } } // namespace ui diff --git a/scwx-qt/source/scwx/qt/ui/layer_dialog.cpp b/scwx-qt/source/scwx/qt/ui/layer_dialog.cpp new file mode 100644 index 00000000..828a6b48 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/layer_dialog.cpp @@ -0,0 +1,315 @@ +#include "layer_dialog.hpp" +#include "ui_layer_dialog.h" + +#include +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +static const std::string logPrefix_ = "scwx::qt::ui::layer_dialog"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class LayerDialogImpl +{ +public: + explicit LayerDialogImpl(LayerDialog* self) : + self_ {self}, + layerModel_ {model::LayerModel::Instance()}, + layerProxyModel_ {new QSortFilterProxyModel(self_)} + { + layerProxyModel_->setSourceModel(layerModel_.get()); + layerProxyModel_->setFilterCaseSensitivity( + Qt::CaseSensitivity::CaseInsensitive); + layerProxyModel_->setFilterKeyColumn(-1); + } + ~LayerDialogImpl() = default; + + void ConnectSignals(); + void UpdateMoveButtonsEnabled(); + + std::vector GetSelectedRows(); + std::vector> GetContiguousRows(); + + LayerDialog* self_; + std::shared_ptr layerModel_; + QSortFilterProxyModel* layerProxyModel_; +}; + +LayerDialog::LayerDialog(QWidget* parent) : + QDialog(parent), + p {std::make_unique(this)}, + ui(new Ui::LayerDialog) +{ + ui->setupUi(this); + + ui->layerTreeView->setModel(p->layerProxyModel_); + + auto layerViewHeader = ui->layerTreeView->header(); + + layerViewHeader->setMinimumSectionSize(10); + + // Give small columns a fixed size + for (auto column : model::LayerModel::ColumnIterator()) + { + if (column != model::LayerModel::Column::Description) + { + layerViewHeader->setSectionResizeMode( + static_cast(column), + QHeaderView::ResizeMode::ResizeToContents); + } + } + + // Disable move buttons + ui->moveTopButton->setEnabled(false); + ui->moveUpButton->setEnabled(false); + ui->moveDownButton->setEnabled(false); + ui->moveBottomButton->setEnabled(false); + + p->ConnectSignals(); +} + +LayerDialog::~LayerDialog() +{ + delete ui; +} + +void LayerDialogImpl::ConnectSignals() +{ + QObject::connect( + self_->ui->buttonBox->button(QDialogButtonBox::StandardButton::Reset), + &QAbstractButton::clicked, + self_, + [this]() { layerModel_->ResetLayers(); }); + + QObject::connect(self_->ui->layerFilter, + &QLineEdit::textChanged, + layerProxyModel_, + &QSortFilterProxyModel::setFilterWildcard); + + QObject::connect(self_->ui->layerTreeView->selectionModel(), + &QItemSelectionModel::selectionChanged, + self_, + [this](const QItemSelection& /* selected */, + const QItemSelection& /* deselected */) + { UpdateMoveButtonsEnabled(); }); + + QObject::connect(layerModel_.get(), + &QAbstractItemModel::rowsMoved, + self_, + [this]() + { + UpdateMoveButtonsEnabled(); + + auto selectedRows = GetSelectedRows(); + if (!selectedRows.empty()) + { + self_->ui->layerTreeView->scrollTo( + layerModel_->index(selectedRows.front(), 0)); + } + }); + + QObject::connect( // + self_->ui->moveTopButton, + &QAbstractButton::clicked, + self_, + [this]() + { + auto contiguousRows = GetContiguousRows(); + int destinationChild = 0; + + for (auto& selectedRows : contiguousRows) + { + int sourceRow = selectedRows.front(); + int count = static_cast(selectedRows.size()); + + layerModel_->moveRows(QModelIndex(), + sourceRow, + count, + QModelIndex(), + destinationChild); + + // Next set of rows should follow rows just added + destinationChild += count; + } + }); + QObject::connect( // + self_->ui->moveUpButton, + &QAbstractButton::clicked, + self_, + [this]() + { + auto contiguousRows = GetContiguousRows(); + int destinationChild = -1; + + for (auto& selectedRows : contiguousRows) + { + int sourceRow = selectedRows.front(); + int count = static_cast(selectedRows.size()); + if (destinationChild == -1) + { + destinationChild = sourceRow - 1; + } + + layerModel_->moveRows(QModelIndex(), + sourceRow, + count, + QModelIndex(), + destinationChild); + + // Next set of rows should follow rows just added + destinationChild += count; + } + }); + QObject::connect( // + self_->ui->moveDownButton, + &QAbstractButton::clicked, + self_, + [this]() + { + auto contiguousRows = GetContiguousRows(); + int destinationChild = 0; + int offset = 0; + if (!contiguousRows.empty()) + { + destinationChild = contiguousRows.back().back() + 2; + } + + for (auto& selectedRows : contiguousRows) + { + int sourceRow = selectedRows.front() - offset; + int count = static_cast(selectedRows.size()); + + layerModel_->moveRows(QModelIndex(), + sourceRow, + count, + QModelIndex(), + destinationChild); + + // Next set of rows should be offset + offset += count; + } + }); + QObject::connect( // + self_->ui->moveBottomButton, + &QAbstractButton::clicked, + self_, + [this]() + { + auto contiguousRows = GetContiguousRows(); + int destinationChild = layerModel_->rowCount(); + int offset = 0; + + for (auto& selectedRows : contiguousRows) + { + int sourceRow = selectedRows.front() - offset; + int count = static_cast(selectedRows.size()); + + layerModel_->moveRows(QModelIndex(), + sourceRow, + count, + QModelIndex(), + destinationChild); + + // Next set of rows should be offset + offset += count; + } + }); +} + +std::vector LayerDialogImpl::GetSelectedRows() +{ + QModelIndexList selectedRows = + self_->ui->layerTreeView->selectionModel()->selectedRows(); + std::vector rows {}; + for (auto& selectedRow : selectedRows) + { + rows.push_back(layerProxyModel_->mapToSource(selectedRow).row()); + } + std::sort(rows.begin(), rows.end()); + return rows; +} + +std::vector> LayerDialogImpl::GetContiguousRows() +{ + std::vector> contiguousRows {}; + std::vector currentContiguousRows {}; + auto rows = GetSelectedRows(); + + for (auto& row : rows) + { + // Next row is not contiguous with current row set + if (!currentContiguousRows.empty() && + currentContiguousRows.back() + 1 < row) + { + // Add current row set to contiguous rows, and reset current set + contiguousRows.emplace_back(std::move(currentContiguousRows)); + currentContiguousRows.clear(); + } + + // Add row to current row set + currentContiguousRows.push_back(row); + } + + if (!currentContiguousRows.empty()) + { + // Add remaining rows to contiguous rows + contiguousRows.emplace_back(currentContiguousRows); + } + + return contiguousRows; +} + +void LayerDialogImpl::UpdateMoveButtonsEnabled() +{ + QModelIndexList selectedRows = + self_->ui->layerTreeView->selectionModel()->selectedRows(); + + bool itemsSelected = selectedRows.size() > 0; + bool itemsMovableUp = itemsSelected; + bool itemsMovableDown = itemsSelected; + int rowCount = layerModel_->rowCount(); + + for (auto& rowIndex : selectedRows) + { + int row = layerProxyModel_->mapToSource(rowIndex).row(); + if (!layerModel_->IsMovable(row)) + { + // If an item in the selection is not movable, disable all moves + itemsMovableUp = false; + itemsMovableDown = false; + break; + } + else + { + // If the first row is selected, items cannot be moved up + if (row == 0) + { + itemsMovableUp = false; + } + + // If the last row is selected, items cannot be moved down + if (row == rowCount - 1) + { + itemsMovableDown = false; + } + } + } + + // Enable move buttons according to selection + self_->ui->moveTopButton->setEnabled(itemsMovableUp); + self_->ui->moveUpButton->setEnabled(itemsMovableUp); + self_->ui->moveDownButton->setEnabled(itemsMovableDown); + self_->ui->moveBottomButton->setEnabled(itemsMovableDown); +} + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/layer_dialog.hpp b/scwx-qt/source/scwx/qt/ui/layer_dialog.hpp new file mode 100644 index 00000000..a8101d0d --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/layer_dialog.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +namespace Ui +{ +class LayerDialog; +} + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +class LayerDialogImpl; + +class LayerDialog : public QDialog +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(LayerDialog) + +public: + explicit LayerDialog(QWidget* parent = nullptr); + ~LayerDialog(); + +private: + friend class LayerDialogImpl; + std::unique_ptr p; + Ui::LayerDialog* ui; +}; + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/layer_dialog.ui b/scwx-qt/source/scwx/qt/ui/layer_dialog.ui new file mode 100644 index 00000000..f9b2a076 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/layer_dialog.ui @@ -0,0 +1,259 @@ + + + LayerDialog + + + + 0 + 0 + 700 + 600 + + + + Layer Manager + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + QAbstractItemView::InternalMove + + + Qt::MoveAction + + + true + + + QAbstractItemView::ExtendedSelection + + + 0 + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + ... + + + + :/res/icons/font-awesome-6/angles-up-solid.svg:/res/icons/font-awesome-6/angles-up-solid.svg + + + + + + + ... + + + + :/res/icons/font-awesome-6/angle-up-solid.svg:/res/icons/font-awesome-6/angle-up-solid.svg + + + + + + + ... + + + + :/res/icons/font-awesome-6/angle-down-solid.svg:/res/icons/font-awesome-6/angle-down-solid.svg + + + + + + + ... + + + + :/res/icons/font-awesome-6/angles-down-solid.svg:/res/icons/font-awesome-6/angles-down-solid.svg + + + + + + + Qt::Vertical + + + + 20 + 209 + + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Filter + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + QDialogButtonBox::Close|QDialogButtonBox::Reset + + + + + + + + + + + + + + buttonBox + accepted() + LayerDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + LayerDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/scwx-qt/source/scwx/qt/ui/left_elided_item_delegate.cpp b/scwx-qt/source/scwx/qt/ui/left_elided_item_delegate.cpp new file mode 100644 index 00000000..a102e052 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/left_elided_item_delegate.cpp @@ -0,0 +1,28 @@ +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +LeftElidedItemDelegate::LeftElidedItemDelegate(QObject* parent) : + QStyledItemDelegate(parent) +{ +} + +LeftElidedItemDelegate::~LeftElidedItemDelegate() {} + +void LeftElidedItemDelegate::paint(QPainter* painter, + const QStyleOptionViewItem& option, + const QModelIndex& index) const +{ + QStyleOptionViewItem newOption = option; + newOption.textElideMode = Qt::TextElideMode::ElideLeft; + QStyledItemDelegate::paint(painter, newOption, index); +} + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/left_elided_item_delegate.hpp b/scwx-qt/source/scwx/qt/ui/left_elided_item_delegate.hpp new file mode 100644 index 00000000..9b8223a1 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/left_elided_item_delegate.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +class LeftElidedItemDelegate : public QStyledItemDelegate +{ +public: + explicit LeftElidedItemDelegate(QObject* parent = nullptr); + ~LeftElidedItemDelegate(); + + void paint(QPainter* painter, + const QStyleOptionViewItem& option, + const QModelIndex& index) const override; +}; + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/open_url_dialog.cpp b/scwx-qt/source/scwx/qt/ui/open_url_dialog.cpp new file mode 100644 index 00000000..0019a472 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/open_url_dialog.cpp @@ -0,0 +1,103 @@ +#include "open_url_dialog.hpp" +#include "ui_open_url_dialog.h" + +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +static const std::string logPrefix_ = "scwx::qt::ui::open_url_dialog"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class OpenUrlDialogImpl +{ +public: + explicit OpenUrlDialogImpl(OpenUrlDialog* self) : self_ {self} {} + ~OpenUrlDialogImpl() = default; + + void ConnectSignals(); + void SelectFile(); + + OpenUrlDialog* self_; +}; + +OpenUrlDialog::OpenUrlDialog(const QString& title, QWidget* parent) : + QDialog(parent), + p {std::make_unique(this)}, + ui(new Ui::OpenUrlDialog) +{ + ui->setupUi(this); + + setWindowTitle(title); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + + p->ConnectSignals(); +} + +OpenUrlDialog::~OpenUrlDialog() +{ + delete ui; +} + +void OpenUrlDialogImpl::ConnectSignals() +{ + QObject::connect(self_->ui->urlEdit, + &QLineEdit::textChanged, + self_, + [this](const QString& text) + { + QUrl url(text); + self_->ui->buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled(url.isValid()); + }); + + QObject::connect(self_->ui->fileButton, + &QToolButton::clicked, + self_, + [this]() { SelectFile(); }); +} + +void OpenUrlDialog::showEvent(QShowEvent* event) +{ + ui->urlEdit->setText(QString()); + QDialog::showEvent(event); +} + +QString OpenUrlDialog::url() const +{ + return ui->urlEdit->text(); +} + +void OpenUrlDialogImpl::SelectFile() +{ + static const std::string placefileFilter = "Placefiles (*)"; + + QFileDialog* dialog = new QFileDialog(self_); + + dialog->setFileMode(QFileDialog::ExistingFile); + dialog->setNameFilter(QObject::tr(placefileFilter.c_str())); + dialog->setAttribute(Qt::WA_DeleteOnClose); + + QObject::connect(dialog, + &QFileDialog::fileSelected, + self_, + [this](const QString& file) + { + logger_->debug("Selected: {}", file.toStdString()); + self_->ui->urlEdit->setText(file); + }); + + dialog->open(); +} + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/open_url_dialog.hpp b/scwx-qt/source/scwx/qt/ui/open_url_dialog.hpp new file mode 100644 index 00000000..ef7666e7 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/open_url_dialog.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include + +namespace Ui +{ +class OpenUrlDialog; +} + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +class OpenUrlDialogImpl; + +class OpenUrlDialog : public QDialog +{ + Q_OBJECT + +public: + explicit OpenUrlDialog(const QString& title, QWidget* parent = nullptr); + ~OpenUrlDialog(); + + QString url() const; + +protected: + void showEvent(QShowEvent* event); + +private: + friend class OpenUrlDialogImpl; + std::unique_ptr p; + Ui::OpenUrlDialog* ui; +}; + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/open_url_dialog.ui b/scwx-qt/source/scwx/qt/ui/open_url_dialog.ui new file mode 100644 index 00000000..a796d20d --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/open_url_dialog.ui @@ -0,0 +1,118 @@ + + + OpenUrlDialog + + + + 0 + 0 + 400 + 80 + + + + Dialog + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + URL: + + + + + + + + + + ... + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + OpenUrlDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + OpenUrlDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/scwx-qt/source/scwx/qt/ui/placefile_dialog.cpp b/scwx-qt/source/scwx/qt/ui/placefile_dialog.cpp new file mode 100644 index 00000000..6adb96ba --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/placefile_dialog.cpp @@ -0,0 +1,45 @@ +#include "placefile_dialog.hpp" +#include "ui_placefile_dialog.h" + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +static const std::string logPrefix_ = "scwx::qt::ui::placefile_dialog"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class PlacefileDialogImpl +{ +public: + explicit PlacefileDialogImpl() {} + ~PlacefileDialogImpl() = default; + + PlacefileSettingsWidget* placefileSettingsWidget_ {nullptr}; +}; + +PlacefileDialog::PlacefileDialog(QWidget* parent) : + QDialog(parent), + p {std::make_unique()}, + ui(new Ui::PlacefileDialog) +{ + ui->setupUi(this); + + p->placefileSettingsWidget_ = new PlacefileSettingsWidget(this); + p->placefileSettingsWidget_->layout()->setContentsMargins(0, 0, 0, 0); + ui->contentsFrame->layout()->addWidget(p->placefileSettingsWidget_); +} + +PlacefileDialog::~PlacefileDialog() +{ + delete ui; +} + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/placefile_dialog.hpp b/scwx-qt/source/scwx/qt/ui/placefile_dialog.hpp new file mode 100644 index 00000000..eb93cc44 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/placefile_dialog.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +namespace Ui +{ +class PlacefileDialog; +} + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +class PlacefileDialogImpl; + +class PlacefileDialog : public QDialog +{ + Q_OBJECT + +public: + explicit PlacefileDialog(QWidget* parent = nullptr); + ~PlacefileDialog(); + +private: + friend class PlacefileDialogImpl; + std::unique_ptr p; + Ui::PlacefileDialog* ui; +}; + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/placefile_dialog.ui b/scwx-qt/source/scwx/qt/ui/placefile_dialog.ui new file mode 100644 index 00000000..8ff045a6 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/placefile_dialog.ui @@ -0,0 +1,88 @@ + + + PlacefileDialog + + + + 0 + 0 + 700 + 600 + + + + Placefile Manager + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + + buttonBox + accepted() + PlacefileDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + PlacefileDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/scwx-qt/source/scwx/qt/ui/placefile_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/placefile_settings_widget.cpp new file mode 100644 index 00000000..a23a3f0c --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/placefile_settings_widget.cpp @@ -0,0 +1,178 @@ +#include "placefile_settings_widget.hpp" +#include "ui_placefile_settings_widget.h" + +#include +#include +#include +#include +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +static const std::string logPrefix_ = "scwx::qt::ui::placefile_settings_widget"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class PlacefileSettingsWidgetImpl +{ +public: + explicit PlacefileSettingsWidgetImpl(PlacefileSettingsWidget* self) : + self_ {self}, + openUrlDialog_ {new OpenUrlDialog(QObject::tr("Add Placefile"), self_)}, + placefileModel_ {new model::PlacefileModel(self_)}, + placefileProxyModel_ {new QSortFilterProxyModel(self_)} + { + placefileProxyModel_->setSourceModel(placefileModel_); + placefileProxyModel_->setSortRole(types::ItemDataRole::SortRole); + placefileProxyModel_->setFilterCaseSensitivity( + Qt::CaseSensitivity::CaseInsensitive); + placefileProxyModel_->setFilterKeyColumn(-1); + } + ~PlacefileSettingsWidgetImpl() = default; + + void ConnectSignals(); + + PlacefileSettingsWidget* self_; + OpenUrlDialog* openUrlDialog_; + + std::shared_ptr placefileManager_ { + manager::PlacefileManager::Instance()}; + + model::PlacefileModel* placefileModel_; + QSortFilterProxyModel* placefileProxyModel_; +}; + +PlacefileSettingsWidget::PlacefileSettingsWidget(QWidget* parent) : + QFrame(parent), + p {std::make_unique(this)}, + ui(new Ui::PlacefileSettingsWidget) +{ + ui->setupUi(this); + + ui->removeButton->setEnabled(false); + ui->refreshButton->setEnabled(false); + + ui->placefileView->setModel(p->placefileProxyModel_); + + auto placefileViewHeader = ui->placefileView->header(); + + placefileViewHeader->setMinimumSectionSize(10); + placefileViewHeader->setSortIndicator( + static_cast(model::PlacefileModel::Column::Placefile), + Qt::AscendingOrder); + + // Enabled and Thresholds columns have a fixed size (checkbox) + placefileViewHeader->setSectionResizeMode( + static_cast(model::PlacefileModel::Column::Enabled), + QHeaderView::ResizeMode::ResizeToContents); + placefileViewHeader->setSectionResizeMode( + static_cast(model::PlacefileModel::Column::Thresholds), + QHeaderView::ResizeMode::ResizeToContents); + + p->ConnectSignals(); +} + +PlacefileSettingsWidget::~PlacefileSettingsWidget() +{ + delete ui; +} + +void PlacefileSettingsWidgetImpl::ConnectSignals() +{ + QObject::connect(self_->ui->addButton, + &QPushButton::clicked, + self_, + [this]() { openUrlDialog_->open(); }); + + QObject::connect(self_->ui->removeButton, + &QPushButton::clicked, + self_, + [this]() + { + auto selectionModel = + self_->ui->placefileView->selectionModel(); + + // Get selected URL string + QModelIndex selected = + selectionModel + ->selectedRows(static_cast( + model::PlacefileModel::Column::Placefile)) + .first(); + QVariant data = self_->ui->placefileView->model()->data( + selected, types::ItemDataRole::SortRole); + std::string urlString = data.toString().toStdString(); + + // Remove Placefile + if (!urlString.empty()) + { + placefileManager_->RemoveUrl(urlString); + } + }); + + QObject::connect(self_->ui->refreshButton, + &QPushButton::clicked, + self_, + [this]() + { + auto selectionModel = + self_->ui->placefileView->selectionModel(); + + // Get selected URL string + QModelIndex selected = + selectionModel + ->selectedRows(static_cast( + model::PlacefileModel::Column::Placefile)) + .first(); + QVariant data = self_->ui->placefileView->model()->data( + selected, types::ItemDataRole::SortRole); + std::string urlString = data.toString().toStdString(); + + // Refresh placefile + if (!urlString.empty()) + { + placefileManager_->Refresh(urlString); + } + }); + + QObject::connect( + openUrlDialog_, + &OpenUrlDialog::accepted, + self_, + [this]() + { placefileManager_->AddUrl(openUrlDialog_->url().toStdString()); }); + + QObject::connect(self_->ui->placefileFilter, + &QLineEdit::textChanged, + placefileProxyModel_, + &QSortFilterProxyModel::setFilterWildcard); + + QObject::connect( + self_->ui->placefileView->selectionModel(), + &QItemSelectionModel::selectionChanged, + self_, + [this](const QItemSelection& selected, const QItemSelection& deselected) + { + if (selected.size() == 0 && deselected.size() == 0) + { + // Items which stay selected but change their index are not + // included in selected and deselected. Thus, this signal might + // be emitted with both selected and deselected empty, if only + // the indices of selected items change. + return; + } + + bool itemSelected = selected.size() > 0; + self_->ui->removeButton->setEnabled(itemSelected); + self_->ui->refreshButton->setEnabled(itemSelected); + }); +} + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/placefile_settings_widget.hpp b/scwx-qt/source/scwx/qt/ui/placefile_settings_widget.hpp new file mode 100644 index 00000000..69628446 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/placefile_settings_widget.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +namespace Ui +{ +class PlacefileSettingsWidget; +} + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +class PlacefileSettingsWidgetImpl; + +class PlacefileSettingsWidget : public QFrame +{ + Q_OBJECT + +public: + explicit PlacefileSettingsWidget(QWidget* parent = nullptr); + ~PlacefileSettingsWidget(); + +private: + friend class PlacefileSettingsWidgetImpl; + std::unique_ptr p; + Ui::PlacefileSettingsWidget* ui; +}; + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/placefile_settings_widget.ui b/scwx-qt/source/scwx/qt/ui/placefile_settings_widget.ui new file mode 100644 index 00000000..b95ab784 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/placefile_settings_widget.ui @@ -0,0 +1,108 @@ + + + PlacefileSettingsWidget + + + + 0 + 0 + 400 + 300 + + + + Frame + + + + + + true + + + 0 + + + true + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Filter + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + &Add + + + + + + + false + + + R&emove + + + + + + + false + + + &Refresh + + + + + + + + + + + diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index ee962794..ee216db0 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -6,8 +6,13 @@ #include #include #include +#include +#include #include +#include #include +#include +#include #include #include #include @@ -18,6 +23,9 @@ #include #include #include +#include +#include +#include #include namespace scwx @@ -81,20 +89,25 @@ public: explicit SettingsDialogImpl(SettingsDialog* self) : self_ {self}, radarSiteDialog_ {new RadarSiteDialog(self)}, + fontDialog_ {new QFontDialog(self)}, + fontCategoryModel_ {new QStandardItemModel(self)}, settings_ {std::initializer_list { &defaultRadarSite_, - &fontSizes_, &gridWidth_, &gridHeight_, &mapProvider_, &mapboxApiKey_, &mapTilerApiKey_, &defaultAlertAction_, + &antiAliasingEnabled_, &updateNotificationsEnabled_, - &debugEnabled_}} + &debugEnabled_, + &hoverTextWrap_, + &tooltipMethod_, + &placefileTextDropShadowEnabled_}} { // Configure default alert phenomena colors - auto& paletteSettings = manager::SettingsManager::palette_settings(); + auto& paletteSettings = settings::PaletteSettings::Instance(); int index = 0; for (auto& phenomenon : settings::PaletteSettings::alert_phenomena()) @@ -108,6 +121,12 @@ public: QColor(QString::fromStdString( paletteSettings.alert_color(phenomenon, false).GetDefault()))); } + + // Configure font dialog + fontDialog_->setOptions( + QFontDialog::FontDialogOption::DontUseNativeDialog | + QFontDialog::FontDialogOption::ScalableFonts); + fontDialog_->setWindowModality(Qt::WindowModality::WindowModal); } ~SettingsDialogImpl() = default; @@ -115,10 +134,15 @@ public: void SetupGeneralTab(); void SetupPalettesColorTablesTab(); void SetupPalettesAlertsTab(); + void SetupTextTab(); void ShowColorDialog(QLineEdit* lineEdit, QFrame* frame = nullptr); void UpdateRadarDialogLocation(const std::string& id); + QFont GetSelectedFont(); + void SelectFontCategory(types::FontCategory fontCategory); + void UpdateFontDisplayData(); + void ApplyChanges(); void DiscardChanges(); void ResetToDefault(); @@ -138,17 +162,22 @@ public: SettingsDialog* self_; RadarSiteDialog* radarSiteDialog_; + QFontDialog* fontDialog_; - settings::SettingsInterface defaultRadarSite_ {}; - settings::SettingsInterface> fontSizes_ {}; - settings::SettingsInterface gridWidth_ {}; - settings::SettingsInterface gridHeight_ {}; - settings::SettingsInterface mapProvider_ {}; - settings::SettingsInterface mapboxApiKey_ {}; - settings::SettingsInterface mapTilerApiKey_ {}; - settings::SettingsInterface defaultAlertAction_ {}; - settings::SettingsInterface updateNotificationsEnabled_ {}; - settings::SettingsInterface debugEnabled_ {}; + QStandardItemModel* fontCategoryModel_; + + types::FontCategory selectedFontCategory_ {types::FontCategory::Unknown}; + + settings::SettingsInterface defaultRadarSite_ {}; + settings::SettingsInterface gridWidth_ {}; + settings::SettingsInterface gridHeight_ {}; + settings::SettingsInterface mapProvider_ {}; + settings::SettingsInterface mapboxApiKey_ {}; + settings::SettingsInterface mapTilerApiKey_ {}; + settings::SettingsInterface defaultAlertAction_ {}; + settings::SettingsInterface antiAliasingEnabled_ {}; + settings::SettingsInterface updateNotificationsEnabled_ {}; + settings::SettingsInterface debugEnabled_ {}; std::unordered_map> colorTables_ {}; @@ -159,6 +188,19 @@ public: settings::SettingsInterface> inactiveAlertColors_ {}; + std::unordered_map> + fontFamilies_ {}; + std::unordered_map> + fontStyles_ {}; + std::unordered_map> + fontPointSizes_ {}; + + settings::SettingsInterface hoverTextWrap_ {}; + settings::SettingsInterface tooltipMethod_ {}; + settings::SettingsInterface placefileTextDropShadowEnabled_ {}; + std::vector settings_; }; @@ -178,6 +220,9 @@ SettingsDialog::SettingsDialog(QWidget* parent) : // Palettes > Alerts p->SetupPalettesAlertsTab(); + // Text + p->SetupTextTab(); + p->ConnectSignals(); } @@ -222,6 +267,72 @@ void SettingsDialogImpl::ConnectSignals() [this](const std::string& newValue) { UpdateRadarDialogLocation(newValue); }); + QObject::connect( + self_->ui->fontListView->selectionModel(), + &QItemSelectionModel::selectionChanged, + self_, + [this](const QItemSelection& selected, const QItemSelection& deselected) + { + if (selected.size() == 0 && deselected.size() == 0) + { + // Items which stay selected but change their index are not + // included in selected and deselected. Thus, this signal might + // be emitted with both selected and deselected empty, if only + // the indices of selected items change. + return; + } + + if (selected.size() > 0) + { + QModelIndex selectedIndex = selected[0].indexes()[0]; + QVariant variantData = + self_->ui->fontListView->model()->data(selectedIndex); + if (variantData.typeId() == QMetaType::QString) + { + types::FontCategory fontCategory = + types::GetFontCategory(variantData.toString().toStdString()); + SelectFontCategory(fontCategory); + UpdateFontDisplayData(); + } + } + }); + + QObject::connect(self_->ui->fontSelectButton, + &QAbstractButton::clicked, + self_, + [this]() + { + fontDialog_->setCurrentFont(GetSelectedFont()); + fontDialog_->show(); + }); + + QObject::connect(fontDialog_, + &QFontDialog::fontSelected, + self_, + [this](const QFont& font) + { + fontFamilies_.at(selectedFontCategory_) + .StageValue(font.family().toStdString()); + fontStyles_.at(selectedFontCategory_) + .StageValue(font.styleName().toStdString()); + fontPointSizes_.at(selectedFontCategory_) + .StageValue(font.pointSizeF()); + + UpdateFontDisplayData(); + }); + + QObject::connect(self_->ui->resetFontButton, + &QAbstractButton::clicked, + self_, + [this]() + { + fontFamilies_.at(selectedFontCategory_).StageDefault(); + fontStyles_.at(selectedFontCategory_).StageDefault(); + fontPointSizes_.at(selectedFontCategory_).StageDefault(); + + UpdateFontDisplayData(); + }); + QObject::connect( self_->ui->buttonBox, &QDialogButtonBox::clicked, @@ -272,7 +383,7 @@ void SettingsDialogImpl::SetupGeneralTab() } settings::GeneralSettings& generalSettings = - manager::SettingsManager::general_settings(); + settings::GeneralSettings::Instance(); defaultRadarSite_.SetSettingsVariable(generalSettings.default_radar_site()); defaultRadarSite_.SetMapFromValueFunction( @@ -310,10 +421,6 @@ void SettingsDialogImpl::SetupGeneralTab() defaultRadarSite_.SetResetButton(self_->ui->resetRadarSiteButton); UpdateRadarDialogLocation(generalSettings.default_radar_site().GetValue()); - fontSizes_.SetSettingsVariable(generalSettings.font_sizes()); - fontSizes_.SetEditWidget(self_->ui->fontSizesLineEdit); - fontSizes_.SetResetButton(self_->ui->resetFontSizesButton); - gridWidth_.SetSettingsVariable(generalSettings.grid_width()); gridWidth_.SetEditWidget(self_->ui->gridWidthSpinBox); gridWidth_.SetResetButton(self_->ui->resetGridWidthButton); @@ -401,6 +508,10 @@ void SettingsDialogImpl::SetupGeneralTab() defaultAlertAction_.SetEditWidget(self_->ui->defaultAlertActionComboBox); defaultAlertAction_.SetResetButton(self_->ui->resetDefaultAlertActionButton); + antiAliasingEnabled_.SetSettingsVariable( + generalSettings.anti_aliasing_enabled()); + antiAliasingEnabled_.SetEditWidget(self_->ui->antiAliasingEnabledCheckBox); + updateNotificationsEnabled_.SetSettingsVariable( generalSettings.update_notifications_enabled()); updateNotificationsEnabled_.SetEditWidget( @@ -413,7 +524,7 @@ void SettingsDialogImpl::SetupGeneralTab() void SettingsDialogImpl::SetupPalettesColorTablesTab() { settings::PaletteSettings& paletteSettings = - manager::SettingsManager::palette_settings(); + settings::PaletteSettings::Instance(); // Palettes > Color Tables QGridLayout* colorTableLayout = @@ -505,7 +616,7 @@ void SettingsDialogImpl::SetupPalettesColorTablesTab() void SettingsDialogImpl::SetupPalettesAlertsTab() { settings::PaletteSettings& paletteSettings = - manager::SettingsManager::palette_settings(); + settings::PaletteSettings::Instance(); // Palettes > Alerts QGridLayout* alertsLayout = @@ -618,6 +729,89 @@ void SettingsDialogImpl::SetupPalettesAlertsTab() } } +void SettingsDialogImpl::SetupTextTab() +{ + settings::TextSettings& textSettings = settings::TextSettings::Instance(); + + self_->ui->fontListView->setModel(fontCategoryModel_); + for (const auto& fontCategory : types::FontCategoryIterator()) + { + // Add font category to list view + fontCategoryModel_->appendRow(new QStandardItem( + QString::fromStdString(types::GetFontCategoryName(fontCategory)))); + + // Create settings interface + auto fontFamilyResult = fontFamilies_.emplace( + fontCategory, settings::SettingsInterface {}); + auto fontStyleResult = fontStyles_.emplace( + fontCategory, settings::SettingsInterface {}); + auto fontSizeResult = fontPointSizes_.emplace( + fontCategory, settings::SettingsInterface {}); + + auto& fontFamily = (*fontFamilyResult.first).second; + auto& fontStyle = (*fontStyleResult.first).second; + auto& fontSize = (*fontSizeResult.first).second; + + // Add to settings list + settings_.push_back(&fontFamily); + settings_.push_back(&fontStyle); + settings_.push_back(&fontSize); + + // Set settings variables + fontFamily.SetSettingsVariable(textSettings.font_family(fontCategory)); + fontStyle.SetSettingsVariable(textSettings.font_style(fontCategory)); + fontSize.SetSettingsVariable(textSettings.font_point_size(fontCategory)); + } + self_->ui->fontListView->setCurrentIndex(fontCategoryModel_->index(0, 0)); + SelectFontCategory(*types::FontCategoryIterator().begin()); + UpdateFontDisplayData(); + + hoverTextWrap_.SetSettingsVariable(textSettings.hover_text_wrap()); + hoverTextWrap_.SetEditWidget(self_->ui->hoverTextWrapSpinBox); + hoverTextWrap_.SetResetButton(self_->ui->resetHoverTextWrapButton); + + for (const auto& tooltipMethod : types::TooltipMethodIterator()) + { + self_->ui->tooltipMethodComboBox->addItem( + QString::fromStdString(types::GetTooltipMethodName(tooltipMethod))); + } + + tooltipMethod_.SetSettingsVariable(textSettings.tooltip_method()); + tooltipMethod_.SetMapFromValueFunction( + [](const std::string& text) -> std::string + { + for (types::TooltipMethod tooltipMethod : + types::TooltipMethodIterator()) + { + const std::string tooltipMethodName = + types::GetTooltipMethodName(tooltipMethod); + + if (boost::iequals(text, tooltipMethodName)) + { + // Return tooltip method label + return tooltipMethodName; + } + } + + // Tooltip method label not found, return unknown + return "?"; + }); + tooltipMethod_.SetMapToValueFunction( + [](std::string text) -> std::string + { + // Convert label to lower case and return + boost::to_lower(text); + return text; + }); + tooltipMethod_.SetEditWidget(self_->ui->tooltipMethodComboBox); + tooltipMethod_.SetResetButton(self_->ui->resetTooltipMethodButton); + + placefileTextDropShadowEnabled_.SetSettingsVariable( + textSettings.placefile_text_drop_shadow_enabled()); + placefileTextDropShadowEnabled_.SetEditWidget( + self_->ui->placefileTextDropShadowCheckBox); +} + QImage SettingsDialogImpl::GenerateColorTableImage( std::shared_ptr colorTable, std::uint16_t min, @@ -731,6 +925,54 @@ void SettingsDialogImpl::UpdateRadarDialogLocation(const std::string& id) } } +QFont SettingsDialogImpl::GetSelectedFont() +{ + std::string fontFamily = fontFamilies_.at(selectedFontCategory_) + .GetSettingsVariable() + ->GetStagedOrValue(); + std::string fontStyle = fontStyles_.at(selectedFontCategory_) + .GetSettingsVariable() + ->GetStagedOrValue(); + units::font_size::points fontSize { + fontPointSizes_.at(selectedFontCategory_) + .GetSettingsVariable() + ->GetStagedOrValue()}; + + QFont font = QFontDatabase::font(QString::fromStdString(fontFamily), + QString::fromStdString(fontStyle), + static_cast(fontSize.value())); + font.setPointSizeF(fontSize.value()); + + return font; +} + +void SettingsDialogImpl::SelectFontCategory(types::FontCategory fontCategory) +{ + selectedFontCategory_ = fontCategory; +} + +void SettingsDialogImpl::UpdateFontDisplayData() +{ + QFont font = GetSelectedFont(); + + self_->ui->fontNameLabel->setText(font.family()); + self_->ui->fontStyleLabel->setText(font.styleName()); + self_->ui->fontSizeLabel->setText(QString::number(font.pointSizeF())); + + self_->ui->fontPreviewLabel->setFont(font); + + if (selectedFontCategory_ != types::FontCategory::Unknown) + { + auto& fontFamily = fontFamilies_.at(selectedFontCategory_); + auto& fontStyle = fontStyles_.at(selectedFontCategory_); + auto& fontSize = fontPointSizes_.at(selectedFontCategory_); + + self_->ui->resetFontButton->setVisible(!fontFamily.IsDefault() || + !fontStyle.IsDefault() || + !fontSize.IsDefault()); + } +} + void SettingsDialogImpl::ApplyChanges() { logger_->info("Applying settings changes"); @@ -744,7 +986,7 @@ void SettingsDialogImpl::ApplyChanges() if (committed) { - manager::SettingsManager::SaveSettings(); + manager::SettingsManager::Instance().SaveSettings(); } } diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index a4e39411..8a065168 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -73,6 +73,15 @@ :/res/icons/font-awesome-6/palette-solid.svg:/res/icons/font-awesome-6/palette-solid.svg + + + Text + + + + :/res/icons/font-awesome-6/font-solid.svg:/res/icons/font-awesome-6/font-solid.svg + + @@ -109,27 +118,7 @@ 0 - - - - - - - Font Sizes - - - - - - - - - - Default Radar Site - - - - + ... @@ -140,45 +129,14 @@ - - - - - + + - Mapbox API Key + Grid Width - - - - MapTiler API Key - - - - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - ... - - - - + ... @@ -189,16 +147,6 @@ - - - - QLineEdit::Password - - - - - - @@ -210,32 +158,14 @@ - - + + - Grid Height + MapTiler API Key - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - QLineEdit::Password - - - - + ... @@ -246,42 +176,55 @@ + + + + + + - + - Grid Width + Grid Height - - + + - Map Provider + Default Radar Site - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + QLineEdit::Password - + + + + QLineEdit::Password + + + + Default Alert Action - - + + + + Mapbox API Key + + - + ... @@ -292,9 +235,61 @@ + + + + + + + ... + + + + + + + Map Provider + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + + + + Anti-Aliasing Enabled + + + @@ -346,8 +341,8 @@ 0 0 - 66 - 18 + 512 + 382 @@ -418,6 +413,288 @@ + + + + + + QFrame::StyledPanel + + + QFrame::Plain + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Display Item: + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + QFrame::Panel + + + QFrame::Plain + + + + + + Tornado Warning expires in 15 minutes + + + Qt::AlignCenter + + + true + + + + + + + + + + ... + + + + + + + [Style] + + + + + + + [Font Name] + + + + + + + [Size] + + + + + + + Font: + + + + + + + Style: + + + + + + + Size: + + + + + + + Preview: + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 999 + + + + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + Tooltip Method + + + + + + + Hover text character wrap (0 to disable) + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + Placefile Text Drop Shadow + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + diff --git a/scwx-qt/source/scwx/qt/ui/update_dialog.cpp b/scwx-qt/source/scwx/qt/ui/update_dialog.cpp index 93b3ac0a..4029fa9a 100644 --- a/scwx-qt/source/scwx/qt/ui/update_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/update_dialog.cpp @@ -1,7 +1,7 @@ #include "update_dialog.hpp" #include "ui_update_dialog.h" #include -#include +#include #include #include @@ -30,7 +30,7 @@ UpdateDialog::UpdateDialog(QWidget* parent) : ui->setupUi(this); int titleFontId = - manager::ResourceManager::FontId(types::Font::din1451alt_g); + manager::FontManager::Instance().GetFontId(types::Font::din1451alt_g); QString titleFontFamily = QFontDatabase::applicationFontFamilies(titleFontId).at(0); QFont titleFont(titleFontFamily, 12); diff --git a/scwx-qt/source/scwx/qt/util/font.cpp b/scwx-qt/source/scwx/qt/util/font.cpp index ce87714c..f2397bd2 100644 --- a/scwx-qt/source/scwx/qt/util/font.cpp +++ b/scwx-qt/source/scwx/qt/util/font.cpp @@ -5,8 +5,8 @@ #define _CRT_SECURE_NO_WARNINGS #include -#include #include +#include #include #include @@ -126,9 +126,6 @@ public: } } - void CreateImGuiFont(QFile& fontFile, - QByteArray& fontData, - const std::vector& fontSizes); void ParseNames(FT_Face face); const std::string resource_; @@ -266,37 +263,14 @@ GLuint Font::GenerateTexture(gl::OpenGLFunctions& gl) return p->atlas_->id; } -void FontImpl::CreateImGuiFont(QFile& fontFile, - QByteArray& fontData, - const std::vector& fontSizes) +ImFont* Font::ImGuiFont(std::size_t fontPixelSize) { - QFileInfo fileInfo(fontFile); - ImFontAtlas* fontAtlas = model::ImGuiContextModel::Instance().font_atlas(); - ImFontConfig fontConfig {}; - - // Do not transfer ownership of font data to ImGui, makes const_cast safe - fontConfig.FontDataOwnedByAtlas = false; - - for (int64_t fontSize : fontSizes) + auto it = p->imGuiFonts_.find(fontPixelSize); + if (it != p->imGuiFonts_.cend()) { - const float sizePixels = static_cast(fontSize); - - // Assign name to font - strncpy(fontConfig.Name, - fmt::format("{}:{}", fileInfo.fileName().toStdString(), fontSize) - .c_str(), - sizeof(fontConfig.Name) - 1); - fontConfig.Name[sizeof(fontConfig.Name) - 1] = 0; - - // Add font to atlas - imGuiFonts_.emplace( - fontSize, - fontAtlas->AddFontFromMemoryTTF( - const_cast(static_cast(fontData.constData())), - fontData.size(), - sizePixels, - &fontConfig)); + return it->second; } + return nullptr; } std::shared_ptr Font::Create(const std::string& resource) @@ -324,11 +298,6 @@ std::shared_ptr Font::Create(const std::string& resource) font = std::make_shared(resource); QByteArray fontData = fontFile.readAll(); - font->p->CreateImGuiFont( - fontFile, - fontData, - manager::SettingsManager::general_settings().font_sizes().GetValue()); - font->p->atlas_ = ftgl::texture_atlas_new(512, 512, 1); ftgl::texture_font_t* textureFont = ftgl::texture_font_new_from_memory( font->p->atlas_, BASE_POINT_SIZE, fontData.constData(), fontData.size()); diff --git a/scwx-qt/source/scwx/qt/util/font.hpp b/scwx-qt/source/scwx/qt/util/font.hpp index dc87570a..f545a93b 100644 --- a/scwx-qt/source/scwx/qt/util/font.hpp +++ b/scwx-qt/source/scwx/qt/util/font.hpp @@ -6,7 +6,9 @@ #include #include -#include +#include + +struct ImFont; namespace scwx { @@ -23,10 +25,10 @@ public: explicit Font(const std::string& resource); ~Font(); - Font(const Font&) = delete; + Font(const Font&) = delete; Font& operator=(const Font&) = delete; - Font(Font&&) = delete; + Font(Font&&) = delete; Font& operator=(Font&&) = delete; float BufferText(std::shared_ptr buffer, @@ -38,6 +40,8 @@ public: float Kerning(char c1, char c2) const; float TextLength(const std::string& text, float pointSize) const; + ImFont* ImGuiFont(std::size_t fontPixelSize); + GLuint GenerateTexture(gl::OpenGLFunctions& gl); static std::shared_ptr Create(const std::string& resource); diff --git a/scwx-qt/source/scwx/qt/util/geographic_lib.cpp b/scwx-qt/source/scwx/qt/util/geographic_lib.cpp index 5c55b9e2..6718715a 100644 --- a/scwx-qt/source/scwx/qt/util/geographic_lib.cpp +++ b/scwx-qt/source/scwx/qt/util/geographic_lib.cpp @@ -18,6 +18,25 @@ const ::GeographicLib::Geodesic& DefaultGeodesic() return geodesic_; } +units::angle::degrees +GetAngle(double lat1, double lon1, double lat2, double lon2) +{ + double azi1; + double azi2; + DefaultGeodesic().Inverse(lat1, lon1, lat2, lon2, azi1, azi2); + + return units::angle::degrees {azi1}; +} + +units::length::meters +GetDistance(double lat1, double lon1, double lat2, double lon2) +{ + double distance; + DefaultGeodesic().Inverse(lat1, lon1, lat2, lon2, distance); + + return units::length::meters {distance}; +} + } // namespace GeographicLib } // namespace util } // namespace qt diff --git a/scwx-qt/source/scwx/qt/util/geographic_lib.hpp b/scwx-qt/source/scwx/qt/util/geographic_lib.hpp index 3fe3c187..d03aac04 100644 --- a/scwx-qt/source/scwx/qt/util/geographic_lib.hpp +++ b/scwx-qt/source/scwx/qt/util/geographic_lib.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include +#include namespace scwx { @@ -14,10 +16,36 @@ namespace GeographicLib /** * Get the default geodesic for the WGS84 ellipsoid. * - * return WGS84 ellipsoid geodesic + * @return WGS84 ellipsoid geodesic */ const ::GeographicLib::Geodesic& DefaultGeodesic(); +/** + * Get the angle between two points. + * + * @param [in] lat1 latitude of point 1 (degrees) + * @param [in] lon1 longitude of point 1 (degrees) + * @param [in] lat2 latitude of point 2 (degrees) + * @param [in] lon2 longitude of point 2 (degrees) + * + * @return angle between point 1 and point 2 + */ +units::angle::degrees +GetAngle(double lat1, double lon1, double lat2, double lon2); + +/** + * Get the distance between two points. + * + * @param [in] lat1 latitude of point 1 (degrees) + * @param [in] lon1 longitude of point 1 (degrees) + * @param [in] lat2 latitude of point 2 (degrees) + * @param [in] lon2 longitude of point 2 (degrees) + * + * @return distance between point 1 and point 2 + */ +units::length::meters +GetDistance(double lat1, double lon1, double lat2, double lon2); + } // namespace GeographicLib } // namespace util } // namespace qt diff --git a/scwx-qt/source/scwx/qt/util/imgui.cpp b/scwx-qt/source/scwx/qt/util/imgui.cpp new file mode 100644 index 00000000..46bc859e --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/imgui.cpp @@ -0,0 +1,50 @@ +#include +#include +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace util +{ + +static const std::string logPrefix_ = "scwx::qt::util::imgui"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class ImGui::Impl +{ +public: + explicit Impl() {} + ~Impl() {} +}; + +ImGui::ImGui() : p(std::make_unique()) {} +ImGui::~ImGui() = default; + +ImGui::ImGui(ImGui&&) noexcept = default; +ImGui& ImGui::operator=(ImGui&&) noexcept = default; + +void ImGui::DrawTooltip(const std::string& hoverText) +{ + auto tooltipFont = manager::FontManager::Instance().GetImGuiFont( + types::FontCategory::Tooltip); + + ::ImGui::BeginTooltip(); + ::ImGui::PushFont(tooltipFont->font()); + ::ImGui::TextUnformatted(hoverText.c_str()); + ::ImGui::PopFont(); + ::ImGui::EndTooltip(); +} + +ImGui& ImGui::Instance() +{ + static ImGui instance_ {}; + return instance_; +} + +} // namespace util +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/util/imgui.hpp b/scwx-qt/source/scwx/qt/util/imgui.hpp new file mode 100644 index 00000000..fce09a1e --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/imgui.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace util +{ + +class ImGui +{ +public: + explicit ImGui(); + ~ImGui(); + + ImGui(const ImGui&) = delete; + ImGui& operator=(const ImGui&) = delete; + + ImGui(ImGui&&) noexcept; + ImGui& operator=(ImGui&&) noexcept; + + void DrawTooltip(const std::string& hoverText); + + static ImGui& Instance(); + +private: + class Impl; + + std::unique_ptr p; +}; + +} // namespace util +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/util/maplibre.cpp b/scwx-qt/source/scwx/qt/util/maplibre.cpp new file mode 100644 index 00000000..9cc092bb --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/maplibre.cpp @@ -0,0 +1,94 @@ +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace util +{ +namespace maplibre +{ + +units::length::meters +GetMapDistance(const QMapLibreGL::CustomLayerRenderParameters& params) +{ + return units::length::meters( + QMapLibreGL::metersPerPixelAtLatitude(params.latitude, params.zoom) * + (params.width + params.height) / 2.0); +} + +glm::mat4 GetMapMatrix(const QMapLibreGL::CustomLayerRenderParameters& params) +{ + glm::vec2 scale = GetMapScale(params); + + glm::mat4 mapMatrix(1.0f); + mapMatrix = glm::scale(mapMatrix, glm::vec3(scale, 1.0f)); + mapMatrix = glm::rotate(mapMatrix, + glm::radians(params.bearing), + glm::vec3(0.0f, 0.0f, 1.0f)); + + return mapMatrix; +} + +glm::vec2 GetMapScale(const QMapLibreGL::CustomLayerRenderParameters& params) +{ + 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; + + return glm::vec2 {xScale, yScale}; +} + +bool IsPointInPolygon(const std::vector& vertices, + const glm::vec2& point) +{ + bool inPolygon = true; + + // For each vertex, assume counterclockwise order + for (std::size_t i = 0; i < vertices.size(); ++i) + { + const auto& p1 = vertices[i]; + const auto& p2 = + (i == vertices.size() - 1) ? vertices[0] : vertices[i + 1]; + + // Test which side of edge point lies on + const float a = -(p2.y - p1.y); + const float b = p2.x - p1.x; + const float c = -(a * p1.x + b * p1.y); + const float d = a * point.x + b * point.y + c; + + // If d < 0, the point is on the right-hand side, and outside of the + // polygon + if (d < 0) + { + inPolygon = false; + break; + } + } + + return inPolygon; +} + +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 maplibre +} // namespace util +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/util/maplibre.hpp b/scwx-qt/source/scwx/qt/util/maplibre.hpp new file mode 100644 index 00000000..35989cbf --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/maplibre.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace util +{ +namespace maplibre +{ + +units::length::meters +GetMapDistance(const QMapLibreGL::CustomLayerRenderParameters& params); +glm::mat4 GetMapMatrix(const QMapLibreGL::CustomLayerRenderParameters& params); +glm::vec2 GetMapScale(const QMapLibreGL::CustomLayerRenderParameters& params); + +/** + * @brief Determine whether a point lies within a polygon + * + * @param [in] vertices Counterclockwise vertices + * @param [in] point Point to test + * + * @return Whether the point lies within the polygon + */ +bool IsPointInPolygon(const std::vector& vertices, + const glm::vec2& point); + +glm::vec2 LatLongToScreenCoordinate(const QMapLibreGL::Coordinate& coordinate); + +} // namespace maplibre +} // namespace util +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/util/network.cpp b/scwx-qt/source/scwx/qt/util/network.cpp new file mode 100644 index 00000000..2e8d442f --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/network.cpp @@ -0,0 +1,36 @@ +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace util +{ +namespace network +{ + +std::string NormalizeUrl(const std::string& urlString) +{ + std::string normalizedUrl; + + // Normalize URL string + QUrl url = QUrl::fromUserInput(QString::fromStdString(urlString)); + if (url.isLocalFile()) + { + normalizedUrl = QDir::toNativeSeparators(url.toLocalFile()).toStdString(); + } + else + { + normalizedUrl = urlString; + } + + return normalizedUrl; +} + +} // namespace network +} // namespace util +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/util/network.hpp b/scwx-qt/source/scwx/qt/util/network.hpp new file mode 100644 index 00000000..23142b3a --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/network.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace scwx +{ +namespace qt +{ +namespace util +{ +namespace network +{ + +/** + * @brief Converts a local or remote URL to a consistent format. + * + * @param [in] urlString URL to normalize + * + * @return Normalized URL string + */ +std::string NormalizeUrl(const std::string& urlString); + +} // namespace network +} // namespace util +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/util/texture_atlas.cpp b/scwx-qt/source/scwx/qt/util/texture_atlas.cpp index 9c5dcc04..fbb4f187 100644 --- a/scwx-qt/source/scwx/qt/util/texture_atlas.cpp +++ b/scwx-qt/source/scwx/qt/util/texture_atlas.cpp @@ -1,7 +1,9 @@ #include #include +#include #include +#include #include #include @@ -13,13 +15,21 @@ #include #include +#include +#include +#include #include #include +#include #if defined(_MSC_VER) # pragma warning(pop) #endif +#if defined(LoadImage) +# undef LoadImage +#endif + namespace scwx { namespace qt @@ -33,24 +43,25 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class TextureAtlas::Impl { public: - explicit Impl() : - texturePathMap_ {}, - texturePathMutex_ {}, - atlas_ {}, - atlasMap_ {}, - atlasMutex_ {} - { - } + explicit Impl() {} ~Impl() {} - static boost::gil::rgba8_image_t LoadImage(const std::string& imagePath); + static std::shared_ptr + LoadImage(const std::string& imagePath); - std::unordered_map texturePathMap_; - std::shared_mutex texturePathMutex_; + std::vector> + registeredTextures_ {}; + std::shared_mutex registeredTextureMutex_ {}; - boost::gil::rgba8_image_t atlas_; - std::unordered_map atlasMap_; - std::shared_mutex atlasMutex_; + std::shared_mutex textureCacheMutex_ {}; + std::unordered_map> + textureCache_ {}; + + std::vector atlasArray_ {}; + std::unordered_map atlasMap_ {}; + std::shared_mutex atlasMutex_ {}; + + std::uint64_t buildCount_ {0u}; }; TextureAtlas::TextureAtlas() : p(std::make_unique()) {} @@ -59,184 +70,291 @@ TextureAtlas::~TextureAtlas() = default; TextureAtlas::TextureAtlas(TextureAtlas&&) noexcept = default; TextureAtlas& TextureAtlas::operator=(TextureAtlas&&) noexcept = default; +std::uint64_t TextureAtlas::BuildCount() const +{ + return p->buildCount_; +} + void TextureAtlas::RegisterTexture(const std::string& name, const std::string& path) { - std::unique_lock lock(p->texturePathMutex_); - p->texturePathMap_.insert_or_assign(name, path); + std::unique_lock lock(p->registeredTextureMutex_); + + std::shared_ptr image = CacheTexture(name, path); + p->registeredTextures_.emplace_back(std::move(image)); } -void TextureAtlas::BuildAtlas(size_t width, size_t height) +std::shared_ptr +TextureAtlas::CacheTexture(const std::string& name, const std::string& path) +{ + // Attempt to load the image + std::shared_ptr image = + TextureAtlas::Impl::LoadImage(path); + + // If the image is valid + if (image != nullptr && image->width() > 0 && image->height() > 0) + { + // Store it in the texture cache + std::unique_lock lock(p->textureCacheMutex_); + + p->textureCache_.insert_or_assign(name, image); + + return image; + } + + return nullptr; +} + +void TextureAtlas::BuildAtlas(std::size_t width, std::size_t height) { logger_->debug("Building {}x{} texture atlas", width, height); + boost::timer::cpu_timer timer {}; + timer.start(); + if (width > INT_MAX || height > INT_MAX) { logger_->error("Cannot build texture atlas of size {}x{}", width, height); return; } - std::vector> images; - std::vector stbrpRects; + typedef std::vector< + std::pair>> + ImageVector; - // Load images + ImageVector images {}; + std::vector stbrpRects {}; + + // Cached images { - // Take a read lock on the texture path map - std::shared_lock lock(p->texturePathMutex_); + // Take a lock on the texture cache map while adding textures images to + // the atlas vector. + std::unique_lock textureCacheLock(p->textureCacheMutex_); - // For each registered texture - std::for_each(p->texturePathMap_.cbegin(), - p->texturePathMap_.cend(), - [&](const auto& pair) - { - // Load texture image - boost::gil::rgba8_image_t image = - Impl::LoadImage(pair.second); + // For each cached texture + for (auto it = p->textureCache_.begin(); it != p->textureCache_.end();) + { + auto& texture = *it; + auto image = texture.second.lock(); - if (image.width() > 0u && image.height() > 0u) - { - // Store STB rectangle pack data in a vector - stbrpRects.push_back(stbrp_rect { - 0, - static_cast(image.width()), - static_cast(image.height()), - 0, - 0, - 0}); + if (image == nullptr) + { + logger_->trace("Removing texture from the cache: {}", + texture.first); - // Store image data in a vector - images.emplace_back(pair.first, std::move(image)); - } - }); + // If the image is no longer cached, erase the iterator and continue + it = p->textureCache_.erase(it); + continue; + } + else if (image->width() > 0u && image->height() > 0u) + { + // Store STB rectangle pack data in a vector + stbrpRects.push_back( + stbrp_rect {0, + static_cast(image->width()), + static_cast(image->height()), + 0, + 0, + 0}); + + // Store image data in a vector + images.push_back({texture.first, image}); + } + + // Increment iterator + ++it; + } } - // Pack images - { - logger_->trace("Packing {} images", images.size()); - - // Optimal number of nodes = width - stbrp_context stbrpContext; - std::vector stbrpNodes(width); - - stbrp_init_target(&stbrpContext, - static_cast(width), - static_cast(height), - stbrpNodes.data(), - static_cast(stbrpNodes.size())); - - // Pack loaded textures - stbrp_pack_rects( - &stbrpContext, stbrpRects.data(), static_cast(stbrpRects.size())); - } - - // Lock atlas - std::unique_lock lock(p->atlasMutex_); - - // Clear index - p->atlasMap_.clear(); - - // Clear atlas - p->atlas_.recreate(width, height); - boost::gil::rgba8_view_t atlasView = boost::gil::view(p->atlas_); - boost::gil::fill_pixels(atlasView, - boost::gil::rgba8_pixel_t {255, 0, 255, 255}); - - // Populate atlas - logger_->trace("Populating atlas"); + // GL_MAX_ARRAY_TEXTURE_LAYERS is guaranteed to be at least 256 in OpenGL 3.3 + constexpr std::size_t kMaxLayers = 256u; const float xStep = 1.0f / width; const float yStep = 1.0f / height; const float xMin = xStep * 0.5f; const float yMin = yStep * 0.5f; - for (size_t i = 0; i < images.size(); i++) + // Optimal number of nodes = width + stbrp_context stbrpContext; + std::vector stbrpNodes(width); + ImageVector unpackedImages {}; + std::vector unpackedRects {}; + + std::vector newAtlasArray {}; + std::unordered_map newAtlasMap {}; + + for (std::size_t layer = 0; layer < kMaxLayers; ++layer) { - // If the image was packed successfully - if (stbrpRects[i].was_packed != 0) + logger_->trace("Processing layer {}", layer); + + // Pack images { - // Populate the atlas - boost::gil::rgba8c_view_t imageView = - boost::gil::const_view(images[i].second); - boost::gil::rgba8_view_t atlasSubView = - boost::gil::subimage_view(atlasView, - stbrpRects[i].x, - stbrpRects[i].y, - imageView.width(), - imageView.height()); + logger_->trace("Packing {} images", images.size()); - boost::gil::copy_pixels(imageView, atlasSubView); + stbrp_init_target(&stbrpContext, + static_cast(width), + static_cast(height), + stbrpNodes.data(), + static_cast(stbrpNodes.size())); - // Add texture image to the index - const stbrp_coord x = stbrpRects[i].x; - const stbrp_coord y = stbrpRects[i].y; + // Pack loaded textures + stbrp_pack_rects(&stbrpContext, + stbrpRects.data(), + static_cast(stbrpRects.size())); + } - const float sLeft = x * xStep + xMin; - const float sRight = - sLeft + static_cast(imageView.width() - 1) / width; - const float tTop = y * yStep + yMin; - const float tBottom = - tTop + static_cast(imageView.height() - 1) / height; + // Clear atlas + boost::gil::rgba8_image_t atlas(width, height); + boost::gil::rgba8_view_t atlasView = boost::gil::view(atlas); + boost::gil::fill_pixels(atlasView, + boost::gil::rgba8_pixel_t {255, 0, 255, 255}); - p->atlasMap_.emplace( - std::piecewise_construct, - std::forward_as_tuple(images[i].first), - std::forward_as_tuple( - boost::gil::point_t {x, y}, - boost::gil::point_t {imageView.width(), imageView.height()}, - sLeft, - sRight, - tTop, - tBottom)); + // Populate atlas + logger_->trace("Populating atlas"); + + std::size_t numPackedImages = 0u; + + for (std::size_t i = 0; i < images.size(); ++i) + { + // If the image was packed successfully + if (stbrpRects[i].was_packed != 0) + { + // Populate the atlas + boost::gil::rgba8c_view_t imageView = + boost::gil::const_view(*images[i].second); + + boost::gil::rgba8_view_t atlasSubView = + boost::gil::subimage_view(atlasView, + stbrpRects[i].x, + stbrpRects[i].y, + imageView.width(), + imageView.height()); + + boost::gil::copy_pixels(imageView, atlasSubView); + + // Add texture image to the index + const stbrp_coord x = stbrpRects[i].x; + const stbrp_coord y = stbrpRects[i].y; + + const float sLeft = x * xStep + xMin; + const float sRight = + sLeft + static_cast(imageView.width() - 1) / width; + const float tTop = y * yStep + yMin; + const float tBottom = + tTop + static_cast(imageView.height() - 1) / height; + + newAtlasMap.emplace( + std::piecewise_construct, + std::forward_as_tuple(images[i].first), + std::forward_as_tuple( + layer, + boost::gil::point_t {x, y}, + boost::gil::point_t {imageView.width(), imageView.height()}, + sLeft, + sRight, + tTop, + tBottom)); + + numPackedImages++; + } + else + { + unpackedImages.push_back(std::move(images[i])); + unpackedRects.push_back(stbrpRects[i]); + } + } + + if (numPackedImages > 0u) + { + // The new atlas layer has images that were able to be packed + newAtlasArray.emplace_back(std::move(atlas)); + } + + if (unpackedImages.empty()) + { + // All images have been packed into the texture atlas + break; + } + else if (layer == kMaxLayers - 1u || numPackedImages == 0u) + { + // Some images were unable to be packed into the texture atlas + for (auto& image : unpackedImages) + { + logger_->warn("Unable to pack texture: {}", image.first); + } } else { - logger_->warn("Unable to pack texture: {}", images[i].first); + // Swap in unpacked images for processing the next atlas layer + images.swap(unpackedImages); + stbrpRects.swap(unpackedRects); + unpackedImages.clear(); + unpackedRects.clear(); } } + + // Lock atlas + std::unique_lock lock(p->atlasMutex_); + + p->atlasArray_.swap(newAtlasArray); + p->atlasMap_.swap(newAtlasMap); + + // Mark the need to buffer the atlas + ++p->buildCount_; + + timer.stop(); + logger_->debug("Texture atlas built in {}", timer.format(6, "%ws")); } -GLuint TextureAtlas::BufferAtlas(gl::OpenGLFunctions& gl) +void TextureAtlas::BufferAtlas(gl::OpenGLFunctions& gl, GLuint texture) { - GLuint texture = GL_INVALID_INDEX; - std::shared_lock lock(p->atlasMutex_); - if (p->atlas_.width() > 0u && p->atlas_.height() > 0u) + if (p->atlasArray_.size() > 0u && p->atlasArray_[0].width() > 0 && + p->atlasArray_[0].height() > 0) { - boost::gil::rgba8_view_t view = boost::gil::view(p->atlas_); - std::vector pixelData(view.width() * - view.height()); + const std::size_t numLayers = p->atlasArray_.size(); + const std::size_t width = p->atlasArray_[0].width(); + const std::size_t height = p->atlasArray_[0].height(); + const std::size_t layerSize = width * height; - boost::gil::copy_pixels( - view, - boost::gil::interleaved_view(view.width(), - view.height(), - pixelData.data(), - view.width() * - sizeof(boost::gil::rgba8_pixel_t))); + std::vector pixelData {layerSize * numLayers}; + + for (std::size_t i = 0; i < numLayers; ++i) + { + boost::gil::rgba8_view_t view = boost::gil::view(p->atlasArray_[i]); + + boost::gil::copy_pixels( + view, + boost::gil::interleaved_view(view.width(), + view.height(), + pixelData.data() + (i * layerSize), + view.width() * + sizeof(boost::gil::rgba8_pixel_t))); + } lock.unlock(); - gl.glGenTextures(1, &texture); - gl.glBindTexture(GL_TEXTURE_2D, texture); + gl.glBindTexture(GL_TEXTURE_2D_ARRAY, texture); - gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + gl.glTexParameteri( + GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + gl.glTexParameteri( + GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - gl.glTexImage2D(GL_TEXTURE_2D, + gl.glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, - view.width(), - view.height(), + static_cast(width), + static_cast(height), + static_cast(numLayers), 0, GL_RGBA, GL_UNSIGNED_BYTE, pixelData.data()); } - - return texture; } TextureAttributes TextureAtlas::GetTextureAttributes(const std::string& name) @@ -253,36 +371,107 @@ TextureAttributes TextureAtlas::GetTextureAttributes(const std::string& name) return attr; } -boost::gil::rgba8_image_t +std::shared_ptr TextureAtlas::Impl::LoadImage(const std::string& imagePath) { logger_->debug("Loading image: {}", imagePath); - boost::gil::rgba8_image_t image; + std::shared_ptr image = + std::make_shared(); - QFile imageFile(imagePath.c_str()); + QUrl url = QUrl::fromUserInput(QString::fromStdString(imagePath)); - imageFile.open(QIODevice::ReadOnly); - - if (!imageFile.isOpen()) + if (url.isLocalFile()) { - logger_->error("Could not open image: {}", imagePath); - return image; + QFile imageFile(imagePath.c_str()); + + imageFile.open(QIODevice::ReadOnly); + + if (!imageFile.isOpen()) + { + logger_->error("Could not open image: {}", imagePath); + return nullptr; + } + + boost::iostreams::stream dataStream(imageFile); + + try + { + boost::gil::read_and_convert_image( + dataStream, *image, boost::gil::png_tag()); + } + catch (const std::exception& ex) + { + logger_->error("Error reading image: {}", ex.what()); + return nullptr; + } } - - boost::iostreams::stream dataStream(imageFile); - - boost::gil::image x; - - try + else { - boost::gil::read_and_convert_image( - dataStream, image, boost::gil::png_tag()); - } - catch (const std::exception& ex) - { - logger_->error("Error reading image: {}", ex.what()); - return image; + auto response = cpr::Get(cpr::Url {imagePath}, network::cpr::GetHeader()); + + if (cpr::status::is_success(response.status_code)) + { + // Use stbi, since we can only guess the image format + static constexpr int desiredChannels = 4; + + int width; + int height; + int numChannels; + + unsigned char* pixelData = stbi_load_from_memory( + reinterpret_cast(response.text.data()), + static_cast( + std::clamp(response.text.size(), 0, INT32_MAX)), + &width, + &height, + &numChannels, + desiredChannels); + + if (pixelData == nullptr) + { + logger_->error("Error loading image: {}", stbi_failure_reason()); + return nullptr; + } + + // Create a view pointing to the STB image data + auto stbView = boost::gil::interleaved_view( + width, + height, + reinterpret_cast(pixelData), + width * desiredChannels); + + // Copy the view to the destination image + *image = boost::gil::rgba8_image_t(stbView); + auto& view = boost::gil::view(*image); + + // If no alpha channel, replace black with transparent + if (numChannels == 3) + { + std::for_each( + std::execution::par_unseq, + view.begin(), + view.end(), + [](boost::gil::rgba8_pixel_t& pixel) + { + static const boost::gil::rgba8_pixel_t kBlack {0, 0, 0, 255}; + if (pixel == kBlack) + { + pixel[3] = 0; + } + }); + } + + stbi_image_free(pixelData); + } + else if (response.status_code == 0) + { + logger_->error("Error loading image: {}", response.error.message); + } + else + { + logger_->error("Error loading image: {}", response.status_line); + } } return image; diff --git a/scwx-qt/source/scwx/qt/util/texture_atlas.hpp b/scwx-qt/source/scwx/qt/util/texture_atlas.hpp index bf904e6c..64c5a2d7 100644 --- a/scwx-qt/source/scwx/qt/util/texture_atlas.hpp +++ b/scwx-qt/source/scwx/qt/util/texture_atlas.hpp @@ -6,6 +6,7 @@ #include #include +#include namespace scwx { @@ -18,6 +19,7 @@ struct TextureAttributes { TextureAttributes() : valid_ {false}, + layerId_ {}, position_ {}, size_ {}, sLeft_ {}, @@ -27,13 +29,15 @@ struct TextureAttributes { } - TextureAttributes(boost::gil::point_t position, + TextureAttributes(std::size_t layerId, + boost::gil::point_t position, boost::gil::point_t size, float sLeft, float sRight, float tTop, float tBottom) : valid_ {true}, + layerId_ {layerId}, position_ {position}, size_ {size}, sLeft_ {sLeft}, @@ -44,6 +48,7 @@ struct TextureAttributes } bool valid_; + std::size_t layerId_; boost::gil::point_t position_; boost::gil::point_t size_; float sLeft_; @@ -66,9 +71,13 @@ public: static TextureAtlas& Instance(); - void RegisterTexture(const std::string& name, const std::string& path); - void BuildAtlas(size_t width, size_t height); - GLuint BufferAtlas(gl::OpenGLFunctions& gl); + std::uint64_t BuildCount() const; + + void RegisterTexture(const std::string& name, const std::string& path); + std::shared_ptr + CacheTexture(const std::string& name, const std::string& path); + void BuildAtlas(std::size_t width, std::size_t height); + void BufferAtlas(gl::OpenGLFunctions& gl, GLuint texture); TextureAttributes GetTextureAttributes(const std::string& name); diff --git a/scwx-qt/source/scwx/qt/util/tooltip.cpp b/scwx-qt/source/scwx/qt/util/tooltip.cpp new file mode 100644 index 00000000..42fcaa4c --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/tooltip.cpp @@ -0,0 +1,185 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace util +{ +namespace tooltip +{ + +static const std::string logPrefix_ = "scwx::qt::util::tooltip"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +static std::unique_ptr tooltipLabel_ = nullptr; +static std::unique_ptr tooltipParent_ = nullptr; + +void Initialize() +{ + static bool initialized = false; + + if (initialized) + { + return; + } + + tooltipParent_ = std::make_unique(); + tooltipParent_->setStyleSheet( + "QToolTip" + "{" + "background-color: rgba(15, 15, 15, 191);" + "border: 1px solid rgba(110, 110, 128, 128);" + "color: rgba(255, 255, 255, 204);" + "}"); + + tooltipLabel_ = std::make_unique(); + tooltipLabel_->setWindowFlag(Qt::ToolTip); + tooltipLabel_->setContentsMargins(6, 4, 6, 4); + tooltipLabel_->setStyleSheet( + "background-color: rgba(15, 15, 15, 191);" + "border: 1px solid rgba(110, 110, 128, 128);" + "color: rgba(255, 255, 255, 204);"); + + initialized = true; +} + +void Show(const std::string& text, const QPointF& mouseGlobalPos) +{ + Initialize(); + + auto& textSettings = settings::TextSettings::Instance(); + + std::size_t textWidth = + static_cast(textSettings.hover_text_wrap().GetValue()); + types::TooltipMethod tooltipMethod = + types::GetTooltipMethod(textSettings.tooltip_method().GetValue()); + + // Wrap text if enabled + std::string wrappedText {}; + if (textWidth > 0) + { + wrappedText = TextFlow::Column(text).width(textWidth).toString(); + } + + // Display text is either wrapped or unwrapped text (do this to avoid copy + // when not wrapping) + const std::string& displayText = (textWidth > 0) ? wrappedText : text; + + if (tooltipMethod == types::TooltipMethod::ImGui) + { + util::ImGui::Instance().DrawTooltip(displayText); + } + else if (tooltipMethod == types::TooltipMethod::QToolTip) + { + QString fontFamily = QString::fromStdString( + textSettings.font_family(types::FontCategory::Tooltip).GetValue()); + QString fontStyle = QString::fromStdString( + textSettings.font_style(types::FontCategory::Tooltip).GetValue()); + double fontPointSize = + textSettings.font_point_size(types::FontCategory::Tooltip).GetValue(); + + static std::size_t id = 0; + QToolTip::showText( + mouseGlobalPos.toPoint(), + QString("%5") + .arg(++id) + .arg(fontFamily) + .arg(fontStyle) + .arg(fontPointSize) + .arg(QString::fromStdString(displayText).replace("\n", "
")), + tooltipParent_.get(), + {}, + std::numeric_limits::max()); + } + else if (tooltipMethod == types::TooltipMethod::QLabel) + { + // Configure the label + QFont font = manager::FontManager::Instance().GetQFont( + types::FontCategory::Tooltip); + tooltipLabel_->setFont(font); + tooltipLabel_->setText(QString::fromStdString(displayText)); + tooltipLabel_->resize(tooltipLabel_->sizeHint()); + + // Get the screen the label will be displayed on + QScreen* screen = QGuiApplication::screenAt(mouseGlobalPos.toPoint()); + if (screen == nullptr) + { + screen = QGuiApplication::primaryScreen(); + } + + // Default offset for label + const QPoint offset {25, 0}; + + // Get starting label position (below and to the right) + QPoint p = mouseGlobalPos.toPoint() + offset; + + // Adjust position if necessary + const QRect r = screen->geometry(); + if (p.x() + tooltipLabel_->width() > r.x() + r.width()) + { + // If the label extends beyond the right of the screen, move it left + p.rx() -= offset.x() * 2 + tooltipLabel_->width(); + } + if (p.y() + tooltipLabel_->height() > r.y() + r.height()) + { + // If the label extends beyond the bottom of the screen, move it up + // p.ry() -= offset.y() * 2 + tooltipLabel_->height(); + // Don't, let it fall through and clamp instead + } + + // Clamp the label within the screen + if (p.y() < r.y()) + { + p.setY(r.y()); + } + if (p.x() + tooltipLabel_->width() > r.x() + r.width()) + { + p.setX(r.x() + r.width() - tooltipLabel_->width()); + } + if (p.x() < r.x()) + { + p.setX(r.x()); + } + if (p.y() + tooltipLabel_->height() > r.y() + r.height()) + { + p.setY(r.y() + r.height() - tooltipLabel_->height()); + } + + // Move the tooltip to the calculated offset + tooltipLabel_->move(p); + + // Show the tooltip + if (tooltipLabel_->isHidden()) + { + tooltipLabel_->show(); + } + } +} + +void Hide() +{ + Initialize(); + + // TooltipMethod::QToolTip + QToolTip::hideText(); + + // TooltipMethod::QLabel + tooltipLabel_->hide(); +} + +} // namespace tooltip +} // namespace util +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/util/tooltip.hpp b/scwx-qt/source/scwx/qt/util/tooltip.hpp new file mode 100644 index 00000000..83c5b146 --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/tooltip.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace util +{ +namespace tooltip +{ + +void Show(const std::string& text, const QPointF& mouseGlobalPos); +void Hide(); + +} // namespace tooltip +} // namespace util +} // namespace qt +} // namespace scwx diff --git a/test/data b/test/data index b2fa7d86..fc428fa1 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit b2fa7d866800902f4c092c567017c832bcdbe702 +Subproject commit fc428fa1460b3d5ce04646727e379e2f4f90f5ec diff --git a/test/source/scwx/gr/placefile.test.cpp b/test/source/scwx/gr/placefile.test.cpp new file mode 100644 index 00000000..98115ba4 --- /dev/null +++ b/test/source/scwx/gr/placefile.test.cpp @@ -0,0 +1,21 @@ +#include + +#include + +namespace scwx +{ +namespace gr +{ + +TEST(PlacefileTest, OldExample) +{ + std::string filename(std::string(SCWX_TEST_DATA_DIR) + + "/gr/placefiles/placefile-old-example.txt"); + + std::shared_ptr ct = Placefile::Load(filename); + + EXPECT_EQ(true, true); +} + +} // namespace gr +} // namespace scwx diff --git a/test/source/scwx/qt/manager/settings_manager.test.cpp b/test/source/scwx/qt/manager/settings_manager.test.cpp index be576069..54df04df 100644 --- a/test/source/scwx/qt/manager/settings_manager.test.cpp +++ b/test/source/scwx/qt/manager/settings_manager.test.cpp @@ -1,5 +1,10 @@ #include #include +#include +#include +#include +#include +#include #include #include @@ -39,10 +44,14 @@ void VerifyDefaults() settings::GeneralSettings defaultGeneralSettings {}; settings::MapSettings defaultMapSettings {}; settings::PaletteSettings defaultPaletteSettings {}; + settings::TextSettings defaultTextSettings {}; + settings::UiSettings defaultUiSettings {}; - EXPECT_EQ(defaultGeneralSettings, SettingsManager::general_settings()); - EXPECT_EQ(defaultMapSettings, SettingsManager::map_settings()); - EXPECT_EQ(defaultPaletteSettings, SettingsManager::palette_settings()); + EXPECT_EQ(defaultGeneralSettings, settings::GeneralSettings::Instance()); + EXPECT_EQ(defaultMapSettings, settings::MapSettings::Instance()); + EXPECT_EQ(defaultPaletteSettings, settings::PaletteSettings::Instance()); + EXPECT_EQ(defaultTextSettings, settings::TextSettings::Instance()); + EXPECT_EQ(defaultUiSettings, settings::UiSettings::Instance()); } void CompareFiles(const std::string& file1, const std::string& file2) @@ -67,7 +76,7 @@ TEST_F(SettingsManagerTest, CreateJson) // Verify file doesn't exist prior to test start EXPECT_EQ(std::filesystem::exists(filename), false); - SettingsManager::ReadSettings(filename); + SettingsManager::Instance().ReadSettings(filename); EXPECT_EQ(std::filesystem::exists(filename), true); @@ -83,14 +92,14 @@ TEST_F(SettingsManagerTest, SettingsKeax) std::string filename(std::string(SCWX_TEST_DATA_DIR) + "/json/settings/settings-keax.json"); - SettingsManager::ReadSettings(filename); + SettingsManager::Instance().ReadSettings(filename); EXPECT_EQ( - SettingsManager::general_settings().default_radar_site().GetValue(), + settings::GeneralSettings::Instance().default_radar_site().GetValue(), "KEAX"); - for (size_t i = 0; i < SettingsManager::map_settings().count(); ++i) + for (size_t i = 0; i < settings::MapSettings::Instance().count(); ++i) { - EXPECT_EQ(SettingsManager::map_settings().radar_site(i).GetValue(), + EXPECT_EQ(settings::MapSettings::Instance().radar_site(i).GetValue(), "KEAX"); } } @@ -103,7 +112,7 @@ TEST_P(DefaultSettingsTest, DefaultSettings) std::filesystem::copy_file(sourceFile, filename); - SettingsManager::ReadSettings(filename); + SettingsManager::Instance().ReadSettings(filename); VerifyDefaults(); CompareFiles(filename, DEFAULT_SETTINGS_FILE); @@ -131,7 +140,7 @@ TEST_P(BadSettingsTest, BadSettings) std::filesystem::copy_file(sourceFile, filename); - SettingsManager::ReadSettings(filename); + SettingsManager::Instance().ReadSettings(filename); CompareFiles(filename, goodFile); diff --git a/test/source/scwx/util/strings.test.cpp b/test/source/scwx/util/strings.test.cpp new file mode 100644 index 00000000..e91c95d9 --- /dev/null +++ b/test/source/scwx/util/strings.test.cpp @@ -0,0 +1,62 @@ +#include + +#include + +namespace scwx +{ +namespace util +{ + +TEST(StringsTest, ParseTokensColor) +{ + static const std::string line {"Color: red green blue alpha discarded"}; + static const std::vector delimiters {":", " ", " ", " ", " "}; + + std::vector tokens = ParseTokens(line, delimiters); + + ASSERT_EQ(tokens.size(), 6); + EXPECT_EQ(tokens[0], "Color"); + EXPECT_EQ(tokens[1], "red"); + EXPECT_EQ(tokens[2], "green"); + EXPECT_EQ(tokens[3], "blue"); + EXPECT_EQ(tokens[4], "alpha"); + EXPECT_EQ(tokens[5], "discarded"); +} + +TEST(StringsTest, ParseTokensColorOffset) +{ + static const std::string line {"Color: red green blue alpha"}; + static const std::vector delimiters {" ", " ", " ", " "}; + static const std::size_t offset = std::string {"Color:"}.size(); + + std::vector tokens = ParseTokens(line, delimiters, offset); + + ASSERT_EQ(tokens.size(), 4); + EXPECT_EQ(tokens[0], "red"); + EXPECT_EQ(tokens[1], "green"); + EXPECT_EQ(tokens[2], "blue"); + EXPECT_EQ(tokens[3], "alpha"); +} + +TEST(StringsTest, ParseTokensText) +{ + static const std::string line { + "Text: lat, lon, fontNumber, \"string, string\", \"hover, hover\", " + "discarded"}; + static const std::vector delimiters { + ":", ",", ",", ",", ",", ","}; + + std::vector tokens = ParseTokens(line, delimiters); + + ASSERT_EQ(tokens.size(), 7); + EXPECT_EQ(tokens[0], "Text"); + EXPECT_EQ(tokens[1], "lat"); + EXPECT_EQ(tokens[2], "lon"); + EXPECT_EQ(tokens[3], "fontNumber"); + EXPECT_EQ(tokens[4], "\"string, string\""); + EXPECT_EQ(tokens[5], "\"hover, hover\""); + EXPECT_EQ(tokens[6], "discarded"); +} + +} // namespace util +} // namespace scwx diff --git a/test/test.cmake b/test/test.cmake index b0731ddc..b7b5fffa 100644 --- a/test/test.cmake +++ b/test/test.cmake @@ -15,6 +15,7 @@ set(SRC_AWIPS_TESTS source/scwx/awips/coded_location.test.cpp source/scwx/awips/ugc.test.cpp) set(SRC_COMMON_TESTS source/scwx/common/color_table.test.cpp source/scwx/common/products.test.cpp) +set(SRC_GR_TESTS source/scwx/gr/placefile.test.cpp) set(SRC_NETWORK_TESTS source/scwx/network/dir_list.test.cpp) set(SRC_PROVIDER_TESTS source/scwx/provider/aws_level2_data_provider.test.cpp source/scwx/provider/aws_level3_data_provider.test.cpp @@ -31,6 +32,7 @@ set(SRC_QT_UTIL_TESTS source/scwx/qt/util/q_file_input_stream.test.cpp) set(SRC_UTIL_TESTS source/scwx/util/float.test.cpp source/scwx/util/rangebuf.test.cpp source/scwx/util/streams.test.cpp + source/scwx/util/strings.test.cpp source/scwx/util/vectorbuf.test.cpp) set(SRC_WSR88D_TESTS source/scwx/wsr88d/ar2v_file.test.cpp source/scwx/wsr88d/level3_file.test.cpp @@ -41,6 +43,7 @@ set(CMAKE_FILES test.cmake) add_executable(wxtest ${SRC_MAIN} ${SRC_AWIPS_TESTS} ${SRC_COMMON_TESTS} + ${SRC_GR_TESTS} ${SRC_NETWORK_TESTS} ${SRC_PROVIDER_TESTS} ${SRC_QT_CONFIG_TESTS} @@ -56,6 +59,7 @@ add_executable(wxtest ${SRC_MAIN} source_group("Source Files\\main" FILES ${SRC_MAIN}) source_group("Source Files\\awips" FILES ${SRC_AWIPS_TESTS}) source_group("Source Files\\common" FILES ${SRC_COMMON_TESTS}) +source_group("Source Files\\gr" FILES ${SRC_GR_TESTS}) source_group("Source Files\\network" FILES ${SRC_NETWORK_TESTS}) source_group("Source Files\\provider" FILES ${SRC_PROVIDER_TESTS}) source_group("Source Files\\qt\\config" FILES ${SRC_QT_CONFIG_TESTS}) diff --git a/wxdata/include/scwx/common/geographic.hpp b/wxdata/include/scwx/common/geographic.hpp index 1318f43b..8945db17 100644 --- a/wxdata/include/scwx/common/geographic.hpp +++ b/wxdata/include/scwx/common/geographic.hpp @@ -11,6 +11,8 @@ namespace common constexpr double kMilesPerMeter = 0.00062137119; constexpr double kKilometersPerMeter = 0.001; +constexpr double kDegreesToRadians = 0.0174532925199432957692369055556; + /** * @brief Coordinate type to hold latitude and longitude of a location. */ diff --git a/wxdata/include/scwx/gr/color.hpp b/wxdata/include/scwx/gr/color.hpp new file mode 100644 index 00000000..787a7649 --- /dev/null +++ b/wxdata/include/scwx/gr/color.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include +#include + +#include + +namespace scwx +{ +namespace gr +{ + +boost::gil::rgba8_pixel_t ParseColor(const std::vector& tokenList, + std::size_t startIndex, + ColorMode colorMode, + bool hasAlpha = true); + +} // namespace gr +} // namespace scwx diff --git a/wxdata/include/scwx/gr/gr_types.hpp b/wxdata/include/scwx/gr/gr_types.hpp new file mode 100644 index 00000000..e90c1042 --- /dev/null +++ b/wxdata/include/scwx/gr/gr_types.hpp @@ -0,0 +1,15 @@ +#pragma once + +namespace scwx +{ +namespace gr +{ + +enum class ColorMode +{ + RGBA, + HSLuv +}; + +} // namespace gr +} // namespace scwx diff --git a/wxdata/include/scwx/gr/placefile.hpp b/wxdata/include/scwx/gr/placefile.hpp new file mode 100644 index 00000000..e9b381fe --- /dev/null +++ b/wxdata/include/scwx/gr/placefile.hpp @@ -0,0 +1,217 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace scwx +{ +namespace gr +{ + +/** + * @brief Place File + * + * Implementation based on: + * Place File Specification + * Mike Gibson + * Gibson Ridge Software, LLC. Used with permission. + * http://www.grlevelx.com/manuals/gis/files_places.htm + */ +class Placefile +{ +public: + explicit Placefile(); + ~Placefile(); + + Placefile(const Placefile&) = delete; + Placefile& operator=(const Placefile&) = delete; + + Placefile(Placefile&&) noexcept; + Placefile& operator=(Placefile&&) noexcept; + + enum class ItemType + { + Icon, + Font, + Text, + Line, + Triangles, + Image, + Polygon, + Unknown + }; + + struct IconFile + { + std::size_t fileNumber_ {}; + std::size_t iconWidth_ {}; + std::size_t iconHeight_ {}; + std::size_t hotX_ {}; + std::size_t hotY_ {}; + std::string filename_ {}; + }; + + struct Font + { + std::size_t fontNumber_ {}; + std::size_t pixels_ {}; + std::int32_t flags_ {}; + std::string face_ {}; + + bool IsBold() { return flags_ & 1; } + bool IsItalic() { return flags_ & 2; } + }; + + struct DrawItem + { + ItemType itemType_ {ItemType::Unknown}; + units::length::nautical_miles threshold_ {}; + std::chrono::sys_time startTime_ {}; + std::chrono::sys_time endTime_ {}; + }; + + struct IconDrawItem : DrawItem + { + IconDrawItem() { itemType_ = ItemType::Icon; } + + boost::gil::rgba8_pixel_t modulate_ {}; + double latitude_ {}; + double longitude_ {}; + double x_ {}; + double y_ {}; + units::degrees angle_ {}; + std::size_t fileNumber_ {0u}; + std::size_t iconNumber_ {0u}; + std::string hoverText_ {}; + }; + + struct TextDrawItem : DrawItem + { + TextDrawItem() { itemType_ = ItemType::Text; } + + boost::gil::rgba8_pixel_t color_ {}; + double latitude_ {}; + double longitude_ {}; + double x_ {}; + double y_ {}; + std::size_t fontNumber_ {0u}; + std::string text_ {}; + std::string hoverText_ {}; + }; + + struct LineDrawItem : DrawItem + { + LineDrawItem() { itemType_ = ItemType::Line; } + + boost::gil::rgba8_pixel_t color_ {}; + double width_ {}; + std::int32_t flags_ {}; + std::string hoverText_ {}; + + struct Element + { + double latitude_ {}; + double longitude_ {}; + double x_ {}; + double y_ {}; + }; + + std::vector elements_ {}; + }; + + struct TrianglesDrawItem : DrawItem + { + TrianglesDrawItem() { itemType_ = ItemType::Triangles; } + + boost::gil::rgba8_pixel_t color_ {}; + + struct Element + { + double latitude_ {}; + double longitude_ {}; + double x_ {}; + double y_ {}; + + std::optional color_ {}; + }; + + std::vector elements_ {}; + }; + + struct ImageDrawItem : DrawItem + { + ImageDrawItem() { itemType_ = ItemType::Image; } + + std::string imageFile_ {}; + + struct Element + { + double latitude_ {}; + double longitude_ {}; + double x_ {}; + double y_ {}; + double tu_ {}; + double tv_ {}; + }; + + std::vector elements_ {}; + }; + + struct PolygonDrawItem : DrawItem + { + PolygonDrawItem() { itemType_ = ItemType::Polygon; } + + boost::gil::rgba8_pixel_t color_ {}; + + struct Element + { + double latitude_ {}; + double longitude_ {}; + double x_ {}; + double y_ {}; + + std::optional color_ {}; + }; + + std::vector> contours_ {}; + scwx::common::Coordinate center_ {}; + }; + + bool IsValid() const; + + /** + * @brief Gets the list of draw items defined in the placefile + * + * @return vector of draw item pointers + */ + std::vector> GetDrawItems(); + + std::vector> icon_files(); + + std::string name() const; + std::string title() const; + std::chrono::seconds refresh() const; + std::unordered_map> fonts(); + std::shared_ptr font(std::size_t i); + + static std::shared_ptr Load(const std::string& filename); + static std::shared_ptr Load(const std::string& name, + std::istream& is); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace gr +} // namespace scwx diff --git a/wxdata/include/scwx/network/cpr.hpp b/wxdata/include/scwx/network/cpr.hpp new file mode 100644 index 00000000..f05c093a --- /dev/null +++ b/wxdata/include/scwx/network/cpr.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace scwx +{ +namespace network +{ +namespace cpr +{ + +::cpr::Header GetHeader(); +void SetUserAgent(const std::string& userAgent); + +} // namespace cpr +} // namespace network +} // namespace scwx diff --git a/wxdata/include/scwx/util/strings.hpp b/wxdata/include/scwx/util/strings.hpp index 0a5cc43f..f3088c9d 100644 --- a/wxdata/include/scwx/util/strings.hpp +++ b/wxdata/include/scwx/util/strings.hpp @@ -8,6 +8,25 @@ namespace scwx namespace util { +/** + * @brief Parse a list of tokens from a string + * + * This function will take an input string, and apply the delimiters vector in + * order to tokenize the string. Each set of delimiters in the delimiters vector + * will be used once. A set of delimiters will be used to match any character, + * rather than a sequence of characters. Tokens are automatically trimmed of any + * whitespace. + * + * @param [in] s Input string to tokenize + * @param [in] delimiters A vector of delimiters to use for each token. + * @param [in] pos Search begin position. Default is 0. + * + * @return Tokenized string + */ +std::vector ParseTokens(const std::string& s, + std::vector delimiters, + std::size_t pos = 0); + std::string ToString(const std::vector& v); } // namespace util diff --git a/wxdata/source/scwx/gr/color.cpp b/wxdata/source/scwx/gr/color.cpp new file mode 100644 index 00000000..5f815e6d --- /dev/null +++ b/wxdata/source/scwx/gr/color.cpp @@ -0,0 +1,86 @@ +#include + +#include + +#include + +namespace scwx +{ +namespace gr +{ + +template +T RoundChannel(double value); +template +T StringToDecimal(const std::string& str); + +boost::gil::rgba8_pixel_t ParseColor(const std::vector& tokenList, + std::size_t startIndex, + ColorMode colorMode, + bool hasAlpha) +{ + + std::uint8_t r {}; + std::uint8_t g {}; + std::uint8_t b {}; + std::uint8_t a = 255; + + if (colorMode == ColorMode::RGBA) + { + if (tokenList.size() >= startIndex + 3) + { + r = StringToDecimal(tokenList[startIndex + 0]); + g = StringToDecimal(tokenList[startIndex + 1]); + b = StringToDecimal(tokenList[startIndex + 2]); + } + + if (hasAlpha && tokenList.size() >= startIndex + 4) + { + a = StringToDecimal(tokenList[startIndex + 3]); + } + } + else // if (colorMode == ColorMode::HSLuv) + { + double h {}; + double s {}; + double l {}; + + if (tokenList.size() >= startIndex + 3) + { + h = std::stod(tokenList[startIndex + 0]); + s = std::stod(tokenList[startIndex + 1]); + l = std::stod(tokenList[startIndex + 2]); + } + + double dr; + double dg; + double db; + + hsluv2rgb(h, s, l, &dr, &dg, &db); + + r = RoundChannel(dr * 255.0); + g = RoundChannel(dg * 255.0); + b = RoundChannel(db * 255.0); + } + + return boost::gil::rgba8_pixel_t {r, g, b, a}; +} + +template +T RoundChannel(double value) +{ + return static_cast(std::clamp(std::lround(value), + std::numeric_limits::min(), + std::numeric_limits::max())); +} + +template +T StringToDecimal(const std::string& str) +{ + return static_cast(std::clamp(std::stoi(str), + std::numeric_limits::min(), + std::numeric_limits::max())); +} + +} // namespace gr +} // namespace scwx diff --git a/wxdata/source/scwx/gr/placefile.cpp b/wxdata/source/scwx/gr/placefile.cpp new file mode 100644 index 00000000..a7584fc3 --- /dev/null +++ b/wxdata/source/scwx/gr/placefile.cpp @@ -0,0 +1,954 @@ +// Enable chrono formatters +#ifndef __cpp_lib_format +# define __cpp_lib_format 202110L +#endif + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#if !defined(_MSC_VER) +# include +#endif + +using namespace units::literals; + +namespace scwx +{ +namespace gr +{ + +static const std::string logPrefix_ {"scwx::gr::placefile"}; +static const auto logger_ = util::Logger::Create(logPrefix_); + +enum class DrawingStatement +{ + Standard, + Line, + Triangles, + Image, + Polygon +}; + +class Placefile::Impl +{ +public: + explicit Impl() = default; + ~Impl() = default; + + struct Object + { + double x_ {}; + double y_ {}; + }; + + void ParseLocation(const std::string& latitudeToken, + const std::string& longitudeToken, + double& latitude, + double& longitude, + double& x, + double& y); + void ProcessElement(const std::string& line); + void ProcessElementEnd(); + void ProcessLine(const std::string& line); + + static void ProcessEscapeCharacters(std::string& s); + static void TrimQuotes(std::string& s); + + std::string name_ {}; + std::string title_ {}; + std::chrono::seconds refresh_ {-1}; + + // Parsing state + units::length::nautical_miles threshold_ {999.0_nmi}; + boost::gil::rgba8_pixel_t color_ {255, 255, 255, 255}; + boost::gil::rgba8_pixel_t iconModulate_ {255, 255, 255, 255}; + ColorMode colorMode_ {ColorMode::RGBA}; + std::chrono::sys_time startTime_ {}; + std::chrono::sys_time endTime_ {}; + + std::vector objectStack_ {}; + DrawingStatement currentStatement_ {DrawingStatement::Standard}; + std::shared_ptr currentDrawItem_ {nullptr}; + std::vector currentPolygonContour_ {}; + + // References + std::unordered_map> iconFiles_ {}; + std::unordered_map> fonts_ {}; + + std::vector> drawItems_ {}; +}; + +Placefile::Placefile() : p(std::make_unique()) {} +Placefile::~Placefile() = default; + +Placefile::Placefile(Placefile&&) noexcept = default; +Placefile& Placefile::operator=(Placefile&&) noexcept = default; + +bool Placefile::IsValid() const +{ + return p->drawItems_.size() > 0; +} + +std::vector> Placefile::GetDrawItems() +{ + return p->drawItems_; +} + +std::vector> Placefile::icon_files() +{ + std::vector> iconFiles {}; + iconFiles.reserve(p->iconFiles_.size()); + + std::transform(p->iconFiles_.begin(), + p->iconFiles_.end(), + std::back_inserter(iconFiles), + [](auto& iconFile) { return iconFile.second; }); + + return iconFiles; +} + +std::string Placefile::name() const +{ + return p->name_; +} + +std::string Placefile::title() const +{ + return p->title_; +} + +std::chrono::seconds Placefile::refresh() const +{ + return p->refresh_; +} + +std::unordered_map> +Placefile::fonts() +{ + return p->fonts_; +} + +std::shared_ptr Placefile::font(std::size_t i) +{ + auto it = p->fonts_.find(i); + if (it != p->fonts_.cend()) + { + return it->second; + } + return nullptr; +} + +std::shared_ptr Placefile::Load(const std::string& filename) +{ + logger_->debug("Loading placefile: {}", filename); + std::ifstream f(filename, std::ios_base::in); + return Load(filename, f); +} + +std::shared_ptr Placefile::Load(const std::string& name, + std::istream& is) +{ + std::shared_ptr placefile = std::make_shared(); + + placefile->p->name_ = name; + + std::string line; + while (scwx::util::getline(is, line)) + { + // Find position of comment (;) + bool inQuotes = false; + for (std::size_t i = 0; i < line.size(); ++i) + { + if (!inQuotes && line[i] == ';') + { + // Remove comment + line.erase(i); + break; + } + else if (line[i] == '"') + { + // Toggle quote state + inQuotes = !inQuotes; + } + } + + // Remove extra spacing from line + boost::trim(line); + + if (line.size() >= 1) + { + try + { + switch (placefile->p->currentStatement_) + { + case DrawingStatement::Standard: + placefile->p->ProcessLine(line); + break; + + case DrawingStatement::Line: + case DrawingStatement::Triangles: + case DrawingStatement::Image: + case DrawingStatement::Polygon: + if (boost::istarts_with(line, "End:")) + { + placefile->p->ProcessElementEnd(); + + placefile->p->currentStatement_ = DrawingStatement::Standard; + placefile->p->currentDrawItem_ = nullptr; + } + else if (placefile->p->currentDrawItem_ != nullptr) + { + placefile->p->ProcessElement(line); + } + break; + } + } + catch (const std::exception&) + { + logger_->warn("Could not parse line: {}", line); + } + } + } + + return placefile; +} + +void Placefile::Impl::ProcessLine(const std::string& line) +{ + static const std::string titleKey_ {"Title:"}; + static const std::string thresholdKey_ {"Threshold:"}; + static const std::string timeRangeKey_ {"TimeRange:"}; + static const std::string hsluvKey_ {"HSLuv:"}; + static const std::string colorKey_ {"Color:"}; + static const std::string refreshKey_ {"Refresh:"}; + static const std::string refreshSecondsKey_ {"RefreshSeconds:"}; + static const std::string placeKey_ {"Place:"}; + static const std::string iconFileKey_ {"IconFile:"}; + static const std::string iconKey_ {"Icon:"}; + static const std::string fontKey_ {"Font:"}; + static const std::string textKey_ {"Text:"}; + static const std::string objectKey_ {"Object:"}; + static const std::string endKey_ {"End:"}; + static const std::string lineKey_ {"Line:"}; + static const std::string trianglesKey_ {"Triangles:"}; + static const std::string imageKey_ {"Image:"}; + static const std::string polygonKey_ {"Polygon:"}; + + static const std::string scwxModulateIconKey_ {"scwx-ModulateIcon:"}; + + currentStatement_ = DrawingStatement::Standard; + + // When tokenizing, add one additional delimiter to discard unexpected + // parameters (where appropriate) + + if (boost::istarts_with(line, titleKey_)) + { + // Title: title + title_ = line.substr(titleKey_.size()); + boost::trim(title_); + } + else if (boost::istarts_with(line, thresholdKey_)) + { + // Threshold: nautical_miles + std::vector tokenList = + util::ParseTokens(line, {" "}, thresholdKey_.size()); + + if (tokenList.size() >= 1) + { + threshold_ = + units::length::nautical_miles(std::stod(tokenList[0])); + } + } + else if (boost::istarts_with(line, timeRangeKey_)) + { + // TimeRange: start_time end_time + // (YYYY-MM-DDThh:mm:ss) + std::vector tokenList = + util::ParseTokens(line, {" ", " "}, timeRangeKey_.size()); + + if (tokenList.size() >= 2) + { + using namespace std::chrono; + +#if !defined(_MSC_VER) + using namespace date; +#endif + + static const std::string dateTimeFormat {"%Y-%m-%dT%H:%M:%S"}; + + std::istringstream ssStartTime {tokenList[0]}; + std::istringstream ssEndTime {tokenList[1]}; + + std::chrono::sys_time startTime; + std::chrono::sys_time endTime; + + ssStartTime >> parse(dateTimeFormat, startTime); + ssEndTime >> parse(dateTimeFormat, endTime); + + if (!ssStartTime.fail() && !ssEndTime.fail()) + { + startTime_ = startTime; + endTime_ = endTime; + } + else + { + startTime_ = {}; + endTime_ = {}; + + logger_->warn("TimeRange statement parse error: {}", line); + } + } + else + { + logger_->warn("TimeRange statement malformed: {}", line); + } + } + else if (boost::istarts_with(line, hsluvKey_)) + { + // HSLuv: value + std::vector tokenList = + util::ParseTokens(line, {" "}, hsluvKey_.size()); + + if (tokenList.size() >= 1) + { + if (boost::iequals(tokenList[0], "true")) + { + colorMode_ = ColorMode::HSLuv; + } + else + { + colorMode_ = ColorMode::RGBA; + } + } + } + else if (boost::istarts_with(line, colorKey_)) + { + // Color: red green blue [alpha] + std::vector tokenList = + util::ParseTokens(line, {" ", " ", " ", " "}, colorKey_.size()); + + if (tokenList.size() >= 3) + { + color_ = ParseColor(tokenList, 0, colorMode_); + } + } + else if (boost::istarts_with(line, scwxModulateIconKey_)) + { + // Supercell Wx Extension + // scwx-ModulateIcon: red green blue [alpha] + std::vector tokenList = util::ParseTokens( + line, {" ", " ", " ", " "}, scwxModulateIconKey_.size()); + + if (tokenList.size() >= 3) + { + iconModulate_ = ParseColor(tokenList, 0, colorMode_); + } + } + else if (boost::istarts_with(line, refreshKey_)) + { + // Refresh: minutes + std::vector tokenList = + util::ParseTokens(line, {" "}, refreshKey_.size()); + + if (tokenList.size() >= 1) + { + refresh_ = std::chrono::minutes {std::stoi(tokenList[0])}; + } + } + else if (boost::istarts_with(line, refreshSecondsKey_)) + { + // RefreshSeconds: seconds + std::vector tokenList = + util::ParseTokens(line, {" "}, refreshSecondsKey_.size()); + + if (tokenList.size() >= 1) + { + refresh_ = std::chrono::seconds {std::stoi(tokenList[0])}; + } + } + else if (boost::istarts_with(line, placeKey_)) + { + // Place: latitude, longitude, string with spaces + std::vector tokenList = + util::ParseTokens(line, {",", ","}, placeKey_.size()); + + if (tokenList.size() >= 3) + { + std::shared_ptr di = std::make_shared(); + + di->threshold_ = threshold_; + di->color_ = color_; + di->startTime_ = startTime_; + di->endTime_ = endTime_; + + ParseLocation(tokenList[0], + tokenList[1], + di->latitude_, + di->longitude_, + di->x_, + di->y_); + + ProcessEscapeCharacters(tokenList[2]); + di->text_.swap(tokenList[2]); + + drawItems_.emplace_back(std::move(di)); + } + else + { + logger_->warn("Place statement malformed: {}", line); + } + } + else if (boost::istarts_with(line, iconFileKey_)) + { + // IconFile: fileNumber, iconWidth, iconHeight, hotX, hotY, fileName + std::vector tokenList = util::ParseTokens( + line, {",", ",", ",", ",", ","}, iconFileKey_.size()); + + if (tokenList.size() >= 6) + { + std::shared_ptr iconFile = std::make_shared(); + + iconFile->fileNumber_ = std::stoul(tokenList[0]); + iconFile->iconWidth_ = std::stoul(tokenList[1]); + iconFile->iconHeight_ = std::stoul(tokenList[2]); + iconFile->hotX_ = std::stoul(tokenList[3]); + iconFile->hotY_ = std::stoul(tokenList[4]); + + TrimQuotes(tokenList[5]); + iconFile->filename_.swap(tokenList[5]); + + iconFiles_.insert_or_assign(iconFile->fileNumber_, iconFile); + } + else + { + logger_->warn("IconFile statement malformed: {}", line); + } + } + else if (boost::istarts_with(line, iconKey_)) + { + // Icon: lat, lon, angle, fileNumber, iconNumber, hoverText + std::vector tokenList = + util::ParseTokens(line, {",", ",", ",", ",", ","}, iconKey_.size()); + + std::shared_ptr di = nullptr; + + if (tokenList.size() >= 5) + { + di = std::make_shared(); + + di->threshold_ = threshold_; + di->startTime_ = startTime_; + di->endTime_ = endTime_; + di->modulate_ = iconModulate_; + + ParseLocation(tokenList[0], + tokenList[1], + di->latitude_, + di->longitude_, + di->x_, + di->y_); + + di->angle_ = units::angle::degrees(std::stod(tokenList[2])); + + di->fileNumber_ = std::stoul(tokenList[3]); + di->iconNumber_ = std::stoul(tokenList[4]); + } + if (tokenList.size() >= 6) + { + ProcessEscapeCharacters(tokenList[5]); + TrimQuotes(tokenList[5]); + di->hoverText_.swap(tokenList[5]); + } + + if (di != nullptr) + { + drawItems_.emplace_back(std::move(di)); + } + else + { + logger_->warn("Icon statement malformed: {}", line); + } + } + else if (boost::istarts_with(line, fontKey_)) + { + // Font: fontNumber, pixels, flags, "face" + std::vector tokenList = + util::ParseTokens(line, {",", ",", ",", ","}, fontKey_.size()); + + if (tokenList.size() >= 4) + { + std::shared_ptr font = std::make_shared(); + + font->fontNumber_ = std::stoul(tokenList[0]); + font->pixels_ = std::stoul(tokenList[1]); + font->flags_ = std::stoi(tokenList[2]); + + TrimQuotes(tokenList[3]); + font->face_.swap(tokenList[3]); + + fonts_.insert_or_assign(font->fontNumber_, font); + } + else + { + logger_->warn("Font statement malformed: {}", line); + } + } + else if (boost::istarts_with(line, textKey_)) + { + // Text: lat, lon, fontNumber, "string", "hover" + std::vector tokenList = + util::ParseTokens(line, {",", ",", ",", ",", ","}, textKey_.size()); + + std::shared_ptr di = nullptr; + + if (tokenList.size() >= 4) + { + di = std::make_shared(); + + di->threshold_ = threshold_; + di->color_ = color_; + di->startTime_ = startTime_; + di->endTime_ = endTime_; + + ParseLocation(tokenList[0], + tokenList[1], + di->latitude_, + di->longitude_, + di->x_, + di->y_); + + di->fontNumber_ = std::stoul(tokenList[2]); + + ProcessEscapeCharacters(tokenList[3]); + TrimQuotes(tokenList[3]); + di->text_.swap(tokenList[3]); + } + if (tokenList.size() >= 5) + { + ProcessEscapeCharacters(tokenList[4]); + TrimQuotes(tokenList[4]); + di->hoverText_.swap(tokenList[4]); + } + + if (di != nullptr) + { + drawItems_.emplace_back(std::move(di)); + } + else + { + logger_->warn("Text statement malformed: {}", line); + } + } + else if (boost::istarts_with(line, objectKey_)) + { + // Object: lat, lon + // ... + // End: + std::vector tokenList = + util::ParseTokens(line, {",", ","}, objectKey_.size()); + + double latitude {}; + double longitude {}; + + if (tokenList.size() >= 2) + { + latitude = std::stod(tokenList[0]); + longitude = std::stod(tokenList[1]); + } + else + { + logger_->warn("Object statement malformed: {}", line); + } + + objectStack_.emplace_back(Object {latitude, longitude}); + } + else if (boost::istarts_with(line, endKey_)) + { + // Object End + if (!objectStack_.empty()) + { + objectStack_.pop_back(); + } + } + else if (boost::istarts_with(line, lineKey_)) + { + // Line: width, flags [, hover_text] + // lat, lon + // ... + // End: + std::vector tokenList = + util::ParseTokens(line, {",", ","}, lineKey_.size()); + + currentStatement_ = DrawingStatement::Line; + + std::shared_ptr di = nullptr; + + if (tokenList.size() >= 2) + { + di = std::make_shared(); + + di->threshold_ = threshold_; + di->color_ = color_; + di->startTime_ = startTime_; + di->endTime_ = endTime_; + + di->width_ = std::stoul(tokenList[0]); + + if (!tokenList[1].empty()) + { + di->flags_ = std::stoul(tokenList[1]); + } + } + if (tokenList.size() >= 3) + { + ProcessEscapeCharacters(tokenList[2]); + TrimQuotes(tokenList[2]); + di->hoverText_.swap(tokenList[2]); + } + + if (di != nullptr) + { + currentDrawItem_ = di; + drawItems_.emplace_back(std::move(di)); + } + else + { + logger_->warn("Line statement malformed: {}", line); + } + } + else if (boost::istarts_with(line, trianglesKey_)) + { + // Triangles: + // lat, lon [, r, g, b [,a]] + // ... + // End: + currentStatement_ = DrawingStatement::Triangles; + + std::shared_ptr di = + std::make_shared(); + + di->threshold_ = threshold_; + di->color_ = color_; + di->startTime_ = startTime_; + di->endTime_ = endTime_; + + currentDrawItem_ = di; + drawItems_.emplace_back(std::move(di)); + } + else if (boost::istarts_with(line, imageKey_)) + { + // Image: image_file + // lat, lon, Tu [, Tv ] + // ... + // End: + std::vector tokenList = + util::ParseTokens(line, {" "}, imageKey_.size()); + + currentStatement_ = DrawingStatement::Image; + + std::shared_ptr di = nullptr; + + if (tokenList.size() >= 1) + { + di = std::make_shared(); + + di->threshold_ = threshold_; + di->startTime_ = startTime_; + di->endTime_ = endTime_; + + TrimQuotes(tokenList[0]); + di->imageFile_.swap(tokenList[0]); + + currentDrawItem_ = di; + drawItems_.emplace_back(std::move(di)); + } + else + { + logger_->warn("Image statement malformed: {}", line); + } + } + else if (boost::istarts_with(line, polygonKey_)) + { + // Polygon: + // lat1, lon1 [, r, g, b [,a]] ; start of the first contour + // ... + // lat1, lon1 ; repeating the first point closes the + // ; contour + // + // lat2, lon2 ; next point starts a new contour + // ... + // lat2, lon2 ; and repeating it ends the contour + // End: + currentStatement_ = DrawingStatement::Polygon; + + std::shared_ptr di = std::make_shared(); + + di->threshold_ = threshold_; + di->color_ = color_; + di->startTime_ = startTime_; + di->endTime_ = endTime_; + + currentDrawItem_ = di; + drawItems_.emplace_back(std::move(di)); + } + else + { + logger_->trace("Unknown statement: {}", line); + } +} + +void Placefile::Impl::ProcessElement(const std::string& line) +{ + if (currentStatement_ == DrawingStatement::Line) + { + // Line: width, flags [, hover_text] + // lat, lon + // ... + // End: + std::vector tokenList = util::ParseTokens(line, {",", ","}); + + if (tokenList.size() >= 2) + { + LineDrawItem::Element element; + + ParseLocation(tokenList[0], + tokenList[1], + element.latitude_, + element.longitude_, + element.x_, + element.y_); + + std::static_pointer_cast(currentDrawItem_) + ->elements_.emplace_back(std::move(element)); + } + else + { + logger_->warn("Line sub-statement malformed: {}", line); + } + } + else if (currentStatement_ == DrawingStatement::Triangles) + { + // Triangles: + // lat, lon [, r, g, b [,a]] + // ... + // End: + std::vector tokenList = + util::ParseTokens(line, {",", ",", ",", ",", ",", ","}); + + TrianglesDrawItem::Element element; + + if (tokenList.size() >= 5) + { + element.color_ = ParseColor(tokenList, 2, colorMode_); + } + + if (tokenList.size() >= 2) + { + ParseLocation(tokenList[0], + tokenList[1], + element.latitude_, + element.longitude_, + element.x_, + element.y_); + + std::static_pointer_cast(currentDrawItem_) + ->elements_.emplace_back(std::move(element)); + } + else + { + logger_->warn("Triangles sub-statement malformed: {}", line); + } + } + else if (currentStatement_ == DrawingStatement::Image) + { + // Image: image_file + // lat, lon, Tu [, Tv ] + // ... + // End: + std::vector tokenList = + util::ParseTokens(line, {",", ",", ",", ","}); + + ImageDrawItem::Element element; + + if (tokenList.size() >= 3) + { + ParseLocation(tokenList[0], + tokenList[1], + element.latitude_, + element.longitude_, + element.x_, + element.y_); + + element.tu_ = std::stod(tokenList[2]); + } + + if (tokenList.size() >= 4) + { + element.tv_ = std::stod(tokenList[3]); + } + else + { + element.tv_ = element.tu_; + } + + if (tokenList.size() >= 3) + { + std::static_pointer_cast(currentDrawItem_) + ->elements_.emplace_back(std::move(element)); + } + else + { + logger_->warn("Image sub-statement malformed: {}", line); + } + } + else if (currentStatement_ == DrawingStatement::Polygon) + { + // Polygon: + // lat1, lon1 [, r, g, b [,a]] ; start of the first contour + // ... + // lat1, lon1 ; repeating the first point closes the + // ; contour + // + // lat2, lon2 ; next point starts a new contour + // ... + // lat2, lon2 ; and repeating it ends the contour + // End: + std::vector tokenList = + util::ParseTokens(line, {",", ",", ",", ",", ",", ","}); + + PolygonDrawItem::Element element; + + if (tokenList.size() >= 5) + { + element.color_ = ParseColor(tokenList, 2, colorMode_); + } + + if (tokenList.size() >= 2) + { + ParseLocation(tokenList[0], + tokenList[1], + element.latitude_, + element.longitude_, + element.x_, + element.y_); + + currentPolygonContour_.emplace_back(std::move(element)); + + if (currentPolygonContour_.size() >= 2) + { + auto& first = currentPolygonContour_.front(); + auto& last = currentPolygonContour_.back(); + + // Repeating the first point closes the contour + if (first.latitude_ == last.latitude_ && + first.longitude_ == last.longitude_ && // + first.x_ == last.x_ && // + first.y_ == last.y_) + { + auto& contours = + std::static_pointer_cast(currentDrawItem_) + ->contours_; + + auto& newContour = contours.emplace_back( + std::vector {}); + newContour.swap(currentPolygonContour_); + } + } + } + else + { + logger_->warn("Polygon sub-statement malformed: {}", line); + } + } +} + +void Placefile::Impl::ProcessElementEnd() +{ + if (currentStatement_ == DrawingStatement::Polygon) + { + auto di = std::static_pointer_cast(currentDrawItem_); + + // Complete the current contour when ending the Polygon statement + if (!currentPolygonContour_.empty()) + { + auto& contours = di->contours_; + + auto& newContour = + contours.emplace_back(std::vector {}); + newContour.swap(currentPolygonContour_); + } + + if (!di->contours_.empty()) + { + std::vector coordinates {}; + std::transform(di->contours_[0].cbegin(), + di->contours_[0].cend(), + std::back_inserter(coordinates), + [](auto& element) { + return common::Coordinate {element.latitude_, + element.longitude_}; + }); + di->center_ = GetCentroid(coordinates); + } + } +} + +void Placefile::Impl::ParseLocation(const std::string& latitudeToken, + const std::string& longitudeToken, + double& latitude, + double& longitude, + double& x, + double& y) +{ + if (objectStack_.empty()) + { + // If an Object statement is not currently open, parse latitude and + // longitude tokens as-is + latitude = std::stod(latitudeToken); + longitude = std::stod(longitudeToken); + } + else + { + // If an Object statement is open, the latitude and longitude are from the + // outermost Object + latitude = objectStack_[0].x_; + longitude = objectStack_[0].y_; + + // The latitude and longitude tokens are interpreted as x, y offsets + x = std::stod(latitudeToken); + y = std::stod(longitudeToken); + + // If there are inner Object statements open, treat these as x, y offsets + for (std::size_t i = 1; i < objectStack_.size(); i++) + { + x += objectStack_[i].x_; + y += objectStack_[i].y_; + } + } +} + +void Placefile::Impl::ProcessEscapeCharacters(std::string& s) +{ + boost::replace_all(s, "\\n", "\n"); +} + +void Placefile::Impl::TrimQuotes(std::string& s) +{ + if (s.size() >= 2 && s.front() == '"' && s.back() == '"') + { + s.erase(s.size() - 1); + s.erase(0, 1); + } +} + +} // namespace gr +} // namespace scwx diff --git a/wxdata/source/scwx/network/cpr.cpp b/wxdata/source/scwx/network/cpr.cpp new file mode 100644 index 00000000..81dea5ad --- /dev/null +++ b/wxdata/source/scwx/network/cpr.cpp @@ -0,0 +1,26 @@ +#include + +namespace scwx +{ +namespace network +{ +namespace cpr +{ + +static const std::string logPrefix_ = "scwx::network::cpr"; + +static ::cpr::Header header_ {}; + +::cpr::Header GetHeader() +{ + return header_; +} + +void SetUserAgent(const std::string& userAgent) +{ + header_.insert_or_assign("User-Agent", userAgent); +} + +} // namespace cpr +} // namespace network +} // namespace scwx diff --git a/wxdata/source/scwx/util/strings.cpp b/wxdata/source/scwx/util/strings.cpp index 3a72c993..e6b46a48 100644 --- a/wxdata/source/scwx/util/strings.cpp +++ b/wxdata/source/scwx/util/strings.cpp @@ -1,10 +1,76 @@ #include +#include + namespace scwx { namespace util { +std::vector ParseTokens(const std::string& s, + std::vector delimiters, + std::size_t pos) +{ + std::vector tokens {}; + std::size_t findPos {}; + + // Iterate through each delimiter + for (std::size_t i = 0; i < delimiters.size() && pos != std::string::npos; + ++i) + { + // Skip leading spaces + while (pos < s.size() && std::isspace(s[pos])) + { + ++pos; + } + + if (pos < s.size() && s[pos] == '"') + { + // Do not search for a delimeter within a quoted string + findPos = s.find('"', pos + 1); + + // Increment search start to one after quotation mark + if (findPos != std::string::npos) + { + ++findPos; + } + } + else + { + // Search starting at the current position + findPos = pos; + } + + // Search for delimiter + std::size_t nextPos = s.find_first_of(delimiters[i], findPos); + + // If the delimiter was not found, stop processing tokens + if (nextPos == std::string::npos) + { + break; + } + + // Add the current substring as a token + auto& newToken = tokens.emplace_back(s.substr(pos, nextPos - pos)); + boost::trim(newToken); + + // Increment nextPos until the next non-space character + while (++nextPos < s.size() && std::isspace(s[nextPos])) {} + + // Store new position value + pos = nextPos; + } + + // Add the remainder of the string as a token + if (pos < s.size()) + { + auto& newToken = tokens.emplace_back(s.substr(pos)); + boost::trim(newToken); + } + + return tokens; +} + std::string ToString(const std::vector& v) { std::string value {}; diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 578f522a..9e99d2d2 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -45,8 +45,15 @@ set(SRC_COMMON source/scwx/common/characters.cpp source/scwx/common/products.cpp source/scwx/common/sites.cpp source/scwx/common/vcp.cpp) -set(HDR_NETWORK include/scwx/network/dir_list.hpp) -set(SRC_NETWORK source/scwx/network/dir_list.cpp) +set(HDR_GR include/scwx/gr/color.hpp + include/scwx/gr/gr_types.hpp + include/scwx/gr/placefile.hpp) +set(SRC_GR source/scwx/gr/color.cpp + source/scwx/gr/placefile.cpp) +set(HDR_NETWORK include/scwx/network/cpr.hpp + include/scwx/network/dir_list.hpp) +set(SRC_NETWORK source/scwx/network/cpr.cpp + source/scwx/network/dir_list.cpp) set(HDR_PROVIDER include/scwx/provider/aws_level2_data_provider.hpp include/scwx/provider/aws_level3_data_provider.hpp include/scwx/provider/aws_nexrad_data_provider.hpp @@ -195,6 +202,8 @@ add_library(wxdata OBJECT ${HDR_AWIPS} ${SRC_AWIPS} ${HDR_COMMON} ${SRC_COMMON} + ${HDR_GR} + ${SRC_GR} ${HDR_NETWORK} ${SRC_NETWORK} ${HDR_PROVIDER} @@ -213,6 +222,8 @@ source_group("Header Files\\awips" FILES ${HDR_AWIPS}) source_group("Source Files\\awips" FILES ${SRC_AWIPS}) source_group("Header Files\\common" FILES ${HDR_COMMON}) source_group("Source Files\\common" FILES ${SRC_COMMON}) +source_group("Header Files\\gr" FILES ${HDR_GR}) +source_group("Source Files\\gr" FILES ${SRC_GR}) source_group("Header Files\\network" FILES ${HDR_NETWORK}) source_group("Source Files\\network" FILES ${SRC_NETWORK}) source_group("Header Files\\provider" FILES ${HDR_PROVIDER}) @@ -248,7 +259,8 @@ target_link_libraries(wxdata PUBLIC aws-cpp-sdk-core aws-cpp-sdk-s3 cpr::cpr LibXml2::LibXml2 - spdlog::spdlog) + spdlog::spdlog + units::units) target_link_libraries(wxdata INTERFACE Boost::iostreams BZip2::BZip2 hsluv-c)