diff --git a/.cppQuickFix b/.cppQuickFix new file mode 100644 index 000000000..873e6d80d --- /dev/null +++ b/.cppQuickFix @@ -0,0 +1,8 @@ +[CppEditor.QuickFix] +GettersOutsideClassFrom=-1 +ResetNameTemplateV2=\"reset_\" + name +SetterNameTemplateV2=\"set_\" + name +SetterParameterNameV2=\"new_\" + name +SettersOutsideClassFrom=-1 +SignalNameTemplateV2=name + \"_changed\" +SignalWithNewValue=true diff --git a/.gitignore b/.gitignore index 9db9daf94..076b6de7c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,14 @@ /doc/* /doc /build/* +app/icons/eaws/eaws_menu_closed.png +app/icons/eaws/eaws_report_active.png +app/icons/eaws/eaws_report_inactive.png +app/icons/eaws/risk_level_active.png +app/icons/eaws/risk_level_inactive.png +app/icons/eaws/slope_angle_active.png +app/icons/eaws/slope_angle_inactive.png +app/icons/eaws/stop_or_go_active.png +app/icons/eaws/stop_or_go_inactive.png /.qtcreator/CMakeLists.txt.user **/.qmlls.ini diff --git a/CMakeLists.txt b/CMakeLists.txt index d3dcef8b9..26056c530 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,7 @@ option(ALP_ENABLE_TRACK_OBJECT_LIFECYCLE "enables debug cmd printout of construc option(ALP_ENABLE_APP_SHUTDOWN_AFTER_60S "Shuts down the app after 60S, used for CI testing with asan." OFF) option(ALP_ENABLE_LTO "Enable link time optimisation." OFF) option(ALP_ENABLE_GL_ENGINE "Enable OpenGL/WebGL engine" ON) -option(ALP_ENABLE_AVLANCHE_WARNING_LAYER "Enables avalanche warning layer (requires Qt Gui in nucleus)" OFF) +option(ALP_ENABLE_AVLANCHE_WARNING_LAYER "Enables avalanche warning layer (requires Qt Gui in nucleus)" ON) option(ALP_ENABLE_LABELS "Enables label rendering" ON) set(ALP_EXTERN_DIR "extern" CACHE STRING "name of the directory to store external libraries, fonts etc..") diff --git a/app/About.qml b/app/About.qml index 4a1a860c9..359cc3e02 100644 --- a/app/About.qml +++ b/app/About.qml @@ -84,7 +84,7 @@ it is licensed under the Open Data Commons Open Database License (ODbL) by the O

Authors:

-Adam Celarek, Lucas Dworschak, Gerald Kimmersdorfer, Jakob Lindner, Patrick Komon, Jakob Maier, Markus Rampp +Adam Celarek, Lucas Dworschak, Gerald Kimmersdorfer, Jakob Lindner, Joerg-Christian Reiher, Patrick Komon, Jakob Maier, Markus Rampp

Impressum:

diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index d75aa6eb3..1b6501e97 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -43,6 +43,11 @@ qt_add_qml_module(alpineapp icons/menu.png icons/search.png icons/icon.png + icons/eaws/eaws_menu.png + icons/eaws/eaws_report.png + icons/eaws/risk_level.png + icons/eaws/slope_angle.png + icons/eaws/stop_or_go.png icons/material/monitoring.png icons/material/3d_rotation.png icons/material/map.png @@ -65,6 +70,10 @@ qt_add_qml_module(alpineapp icons/logo_type_horizontal.png icons/logo_type_vertical.png icons/logo_type_horizontal_short.png + eaws/banner_eaws_report.png + eaws/banner_risk_level.png + eaws/banner_slope_angle.png + eaws/banner_stop_or_go.png QML_FILES Main.qml About.qml @@ -102,7 +111,8 @@ qt_add_qml_module(alpineapp picker/Default.qml picker/PoiAlpineHut.qml picker/PoiSettlement.qml - SOURCES TileStatistics.h TileStatistics.cpp + SOURCES + TileStatistics.h TileStatistics.cpp ) qt_add_resources(alpineapp "fonts" diff --git a/app/FloatingActionButtonGroup.qml b/app/FloatingActionButtonGroup.qml index 4c5556386..f01bc04be 100644 --- a/app/FloatingActionButtonGroup.qml +++ b/app/FloatingActionButtonGroup.qml @@ -20,8 +20,10 @@ import QtQuick import QtQuick.Controls.Material import QtQuick.Layouts -import app import "components" +import app + + ColumnLayout { id: fab_group @@ -76,7 +78,13 @@ ColumnLayout { FloatingActionButton { image: _r + "icons/presets/basic.png" - onClicked: map.set_gl_preset("AAABIHjaY2BgYLL_wAAGGPRhY2EHEP303YEDIPrZPr0FQHr_EU-HBAYEwKn_5syZIPX2DxgEGLDQcP0_ILQDBwMKcHBgwAoc7KC0CJTuhyh0yGRAoeHueIBK4wAKQMwIxXAAAFQuIIw") + onClicked: { + risk_level_toggle.checked = false; + eaws_report_toggle.checked = false; + slope_angle_toggle.checked = false; + stop_or_go_toggle.checked = false; + map.set_gl_preset("AAABIHjaY2BgYLL_wAAGGPRhY2EHEP303YEDIPrZPr0FQHr_EU-HBAYEwKn_5syZIPX2DxgEGLDQcP0_ILQDBwMKcHBgwAoc7KC0CJTuhyh0yGRAoeHueIBK4wAKQMwIxXAAAFQuIIw") + } size: parent.height image_size: 42 image_opacity: 1.0 @@ -87,7 +95,13 @@ ColumnLayout { FloatingActionButton { image: _r + "icons/presets/shaded.png" - onClicked: map.set_gl_preset("AAABIHjaY2BgYLL_wAAGGPRhY2EHEP1s0rwEMG32D0TvPxS4yIEBAXDqvzlz5gIQ_YBBgAELDdf_A0I7cDCgAAcHBqzAwQ5Ki0DpfohCh0wGFBrujgeoNBAwQjEyXwFNHEwDAMaIIAM") + onClicked: { + risk_level_toggle.checked = false; + eaws_report_toggle.checked = false; + slope_angle_toggle.checked = false; + stop_or_go_toggle.checked = false; + map.set_gl_preset("AAABIHjaY2BgYLL_wAAGGPRhY2EHEP1s0rwEMG32D0TvPxS4yIEBAXDqvzlz5gIQ_YBBgAELDdf_A0I7cDCgAAcHBqzAwQ5Ki0DpfohCh0wGFBrujgeoNBAwQjEyXwFNHEwDAMaIIAM") + } size: parent.height image_size: 42 image_opacity: 1.0 @@ -98,7 +112,13 @@ ColumnLayout { FloatingActionButton { image: _r + "icons/presets/snow.png" - onClicked: map.set_gl_preset("AAABIHjaY2BgYLL_wAAGGPRhY2EHEP1s0rwEMG32D0TvPxS4yIEBAXDqvzlz5gIQ_YBBgAELDdf_A0I7cDCgAAcHVPPg4nZQWgRK90MUOmQyoNBwdzxApYGAEYqR-Qpo4mAaAFhrITI") + onClicked: { + risk_level_toggle.checked = false; + eaws_report_toggle.checked = false; + slope_angle_toggle.checked = false; + stop_or_go_toggle.checked = false; + map.set_gl_preset("AAABIHjaY2BgYLL_wAAGGPRhY2EHEP1s0rwEMG32D0TvPxS4yIEBAXDqvzlz5gIQ_YBBgAELDdf_A0I7cDCgAAcHVPPg4nZQWgRK90MUOmQyoNBwdzxApYGAEYqR-Qpo4mAaAFhrITI") + } size: parent.height image_size: 42 image_opacity: 1.0 @@ -119,7 +139,14 @@ ColumnLayout { } FloatingActionButton { image: _r + "icons/material/steepness.png" - onClicked: {map.shared_config.overlay_mode = 101; toggleSteepnessLegend();} + onClicked: { + risk_level_toggle.checked = false; + eaws_report_toggle.checked = false; + slope_angle_toggle.checked = false; + stop_or_go_toggle.checked = false; + + map.shared_config.overlay_mode = 101; + toggleSteepnessLegend();} size: parent.height image_size: 24 image_opacity: 1.0 @@ -131,6 +158,279 @@ ColumnLayout { } } + // Button for avalanche menu + FloatingActionButton { + id: avalanche_menu + image: _r + "icons/" + (checked ? "material/chevron_left.png": "eaws/eaws_menu.png") + size: parent.width + checkable: true + property bool firstClickDone: false // Tracks if the button was clicked before + onClicked:{ + if (!firstClickDone) {firstClickDone = false} + map.updateEawsReportDate(date_picker.selectedDate.getDate(), date_picker.selectedDate.getMonth()+1, date_picker.selectedDate.getFullYear()) + } + + // Textbox for warning , only shown on first click of avalanche menu button + Rectangle { + id: warning + visible: avalanche_menu.checked && !avalanche_menu.firstClickDone //parent.checked + height: 300 + width: 400 + radius: avalanche_menu.radius + anchors.bottom: parent.bottom + ColumnLayout { + anchors.fill: parent + + Label { + id: warning_text + textFormat: Text.StyledText + text: "EXPERIMENTAL FEATURE! +
These visualisation tools are experimental and should not be used as a sole basis for decision-making during tour planning. +
We cannot guarantee the correctness of the information displayed. +
Any liability for accidents and damages in connection with the use of this service is excluded. The planning and execution of your winter sports activities is at your own risk and under your sole responsibility." + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignJustify + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: parent.width - 30 // prevent overflow + Layout.margins: 15 + } + + Rectangle { + Layout.alignment: Qt.AlignCenter + Layout.fillHeight: true + color: "blue" + Layout.preferredWidth: 0 + } + RowLayout { + Layout.alignment: Qt.AlignHCenter + Layout.margins: 15 + Button { + text: "Read more" + ToolTip.visible: hovered + ToolTip.text: qsTr("Open Thesis by Johannes Eschner at TU Wien") + onClicked: { + Qt.openUrlExternally("https://repositum.tuwien.at/handle/20.500.12708/177341?mode=simple") + } + } + + Button { + text: "Accept and Continue" + ToolTip.visible: hovered + ToolTip.text: qsTr("Accept terms of use and open avalanche risk visualisation menu") + onClicked: { + avalanche_menu.firstClickDone = true + } + } + } + } + } + + // Box with all avalanche menu buttons , shown after opening avalanche menu + Rectangle { + visible: avalanche_menu.checked && avalanche_menu.firstClickDone //parent.checked + height: 64 + width: avalanche_subgroup.implicitWidth + radius: avalanche_menu.radius + anchors.left: parent.right + anchors.bottom: parent.bottom + + color: Qt.alpha(Material.backgroundColor, 0.9) + border { width: 2; color: Qt.alpha( "black", 0.5); } + + RowLayout { + anchors.fill: parent + id: avalanche_subgroup + spacing: 0 + height: parent.height + + // stop-or-go toggle button + FloatingActionButton { + id: stop_or_go_toggle + image: _r + "icons/eaws/stop_or_go.png" + onClicked:{ + eaws_report_toggle.checked = false; + risk_level_toggle.checked = false; + slope_angle_toggle.checked = false; + banner_image.source = "eaws/banner_stop_or_go.png" + map.set_stop_or_go_layer(checked); + } + size: parent.height + image_size: 42 + image_opacity: (checked? 1.0 : 0.4) + checkable: true + + ToolTip.visible: hovered + ToolTip.text: qsTr("Stop or Go") + } + + // Risk Level Toggle Button + FloatingActionButton { + id: risk_level_toggle + image: _r + "icons/eaws/risk_level.png" + onClicked:{ + eaws_report_toggle.checked = false; + slope_angle_toggle.checked = false; + stop_or_go_toggle.checked = false; + banner_image.source = "eaws/banner_risk_level.png" + map.set_risk_level_layer(checked); + } + size: parent.height + image_size: 42 + image_opacity: (checked? 1.0 : 0.4) + checkable: true + + ToolTip.visible: hovered + ToolTip.text: qsTr("Risk Level") + } + + // Slope Angle Toggle Button + FloatingActionButton { + id: slope_angle_toggle + image: _r + "icons/eaws/slope_angle.png" + + onClicked:{ + eaws_report_toggle.checked = false; + risk_level_toggle.checked = false; + stop_or_go_toggle.checked = false; + banner_image.source = "eaws/banner_slope_angle.png" + map.set_slope_angle_layer(checked); + } + size: parent.height + image_size: 42 + image_opacity: (checked? 1.0 : 0.4) + checkable: true + + ToolTip.visible: hovered + ToolTip.text: qsTr("Slope Angle") + } + + //EAWS Report Toggle Button + FloatingActionButton { + id: eaws_report_toggle + image: _r + "icons/eaws/eaws_report.png" + image_opacity: (checked? 1.0 : 0.4) + onClicked:{ + risk_level_toggle.checked = false; + slope_angle_toggle.checked = false; + stop_or_go_toggle.checked = false; + banner_image.source = "eaws/banner_eaws_report.png" + map.set_eaws_warning_layer(checked); + } + size: parent.height + image_size: 42 + checkable: true + + ToolTip.visible: hovered + ToolTip.text: qsTr("Show EAWS Report") + + } + + // Banner with color chart (only visible when an avalanche overlay is active + Image{ + id: banner_image + Layout.preferredWidth: implicitWidth * Layout.preferredHeight / implicitHeight + 20 + Layout.preferredHeight: 60 + fillMode: Image.PreserveAspectFit // Keep aspect ratio + visible: (eaws_report_toggle.checked || risk_level_toggle.checked || slope_angle_toggle.checked || stop_or_go_toggle.checked) + } + + // subrectangle with date slection functionality + RowLayout { + id: dateControls + spacing: 0 + Layout.alignment: Qt.AlignVCenter + Layout.fillHeight: true + + // Previous day button + FloatingActionButton { + text: "<" + Layout.fillHeight: true + onClicked: { + let d = new Date(date_picker.selectedDate) + d.setDate(d.getDate() - 1) + date_picker.selectedDate = d + } + ToolTip.visible: hovered + ToolTip.text: qsTr("previous day") + } + + // Date picker. Item ensures it is vertically centered in the rectangle + Item { + id: datePickerWrapper + Layout.alignment: Qt.AlignVCenter + width:80 + height:25 + + DatePicker { + id: date_picker + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: 80 + Layout.preferredHeight: 60 + selectedDate: new Date() + onSelectedDateChanged: { + map.updateEawsReportDate( + selectedDate.getDate(), + selectedDate.getMonth() + 1, + selectedDate.getFullYear() + ) + } + } + } + + // Next day button + FloatingActionButton { + text: ">" + Layout.fillHeight: true + onClicked: { + let d = new Date(date_picker.selectedDate) + d.setDate(d.getDate() + 1) + date_picker.selectedDate = d + } + + ToolTip.visible: hovered + ToolTip.text: qsTr("next day") + } + + // Today button only appears when selected date differs from today + FloatingActionButton { + text: "Today" + width: 60 + height: 20 + onClicked: { date_picker.selectedDate = new Date() } + ToolTip.visible: hovered + ToolTip.text: qsTr("Set date to today") + visible: { + var today = new Date() + var sel = date_picker.selectedDate + return !(sel.getDate() === today.getDate() && + sel.getMonth() === today.getMonth() && + sel.getFullYear() === today.getFullYear()) + } + } + + // Button that opens report of selected date on www.avalanche.report + ToolButton { + text: "avalanche.report" + onClicked: { + if (date_picker.selectedDate) { + let date = date_picker.selectedDate; + let year = date.getFullYear(); + let month = (date.getMonth() + 1).toString().padStart(2, "0"); + let day = date.getDate().toString().padStart(2, "0"); + let url = "https://avalanche.report/bulletin/" + year + "-" + month + "-" + day; + Qt.openUrlExternally(url); + } + } + + ToolTip.visible: hovered + ToolTip.text: qsTr("Open the selected date on Avalanche.report") + } + } + } + } + } + + Connections { enabled: fab_location.checked || fab_presets.checked target: map @@ -156,3 +456,5 @@ ColumnLayout { } + + diff --git a/app/RenderingContext.cpp b/app/RenderingContext.cpp index 7ff57f471..0c60b1c4c 100644 --- a/app/RenderingContext.cpp +++ b/app/RenderingContext.cpp @@ -23,11 +23,17 @@ #include #include #include +#include #include #include #include #include #include +#include +#include +#include +#include +#include #include #include #include @@ -39,7 +45,6 @@ #include #include #include - using namespace nucleus::tile; using namespace nucleus::map_label; using namespace nucleus::picker; @@ -54,6 +59,8 @@ struct RenderingContext::Data { // the ones below are on the scheduler thread. nucleus::tile::setup::GeometrySchedulerHolder geometry; nucleus::tile::setup::TextureSchedulerHolder ortho_texture; + nucleus::tile::setup::TextureSchedulerHolder surfaceshaded_texture; + nucleus::avalanche::setup::EawsTextureSchedulerHolder eaws_texture; nucleus::map_label::setup::SchedulerHolder map_label; std::shared_ptr data_querier; std::unique_ptr camera_controller; @@ -61,6 +68,7 @@ struct RenderingContext::Data { std::shared_ptr picker_manager; std::shared_ptr aabb_decorator; std::unique_ptr scheduler_director; + std::shared_ptr eaws_report_load_service; }; RenderingContext::RenderingContext(QObject* parent) @@ -90,29 +98,44 @@ RenderingContext::RenderingContext(QObject* parent) m->geometry = nucleus::tile::setup::geometry_scheduler(std::move(geometry_service), m->aabb_decorator, m->scheduler_thread.get()); m->scheduler_director->check_in("geometry", m->geometry.scheduler); m->data_querier = std::make_shared(&m->geometry.scheduler->ram_cache()); + // auto ortho_service = std::make_unique("https://gataki.cg.tuwien.ac.at/raw/basemap/tiles/", TilePattern::ZYX_yPointingSouth, ".jpeg"); auto ortho_service = std::make_unique("https://mapsneu.wien.gv.at/basemap/bmaporthofoto30cm/normal/google3857/", TilePattern::ZYX_yPointingSouth, ".jpeg"); m->ortho_texture = nucleus::tile::setup::texture_scheduler(std::move(ortho_service), m->aabb_decorator, m->scheduler_thread.get()); m->scheduler_director->check_in("ortho", m->ortho_texture.scheduler); + + auto surfaceshaded_service = std::make_unique("https://mapsneu.wien.gv.at/basemap/bmapoberflaeche/grau/google3857/", TilePattern::ZYX_yPointingSouth, ".jpeg"); + m->surfaceshaded_texture = nucleus::tile::setup::texture_scheduler(std::move(surfaceshaded_service), m->aabb_decorator, m->scheduler_thread.get()); + m->scheduler_director->check_in("surfaceshading", m->surfaceshaded_texture.scheduler); + auto map_label_service = std::make_unique("https://osm.cg.tuwien.ac.at/vector_tiles/poi_v1/", TilePattern::ZXY_yPointingSouth, ""); m->map_label = nucleus::map_label::setup::scheduler(std::move(map_label_service), m->aabb_decorator, m->data_querier, m->scheduler_thread.get()); m->scheduler_director->check_in("map_label", m->map_label.scheduler); + + auto eaws_regions_service = std::make_unique("https://osm.cg.tuwien.ac.at/vector_tiles/eaws-regions/", TilePattern::ZXY_yPointingSouth, ""); + m->eaws_texture = nucleus::avalanche::setup::eaws_texture_scheduler(std::move(eaws_regions_service), m->aabb_decorator, m->scheduler_thread.get()); + m->scheduler_director->check_in("eaws_regions", m->eaws_texture.scheduler); // clang-format on m->scheduler_director->visit([](nucleus::tile::Scheduler* sch) { nucleus::utils::thread::async_call(sch, [sch]() { sch->read_disk_cache(); }); }); } + m->map_label.scheduler->set_geometry_ram_cache(&m->geometry.scheduler->ram_cache()); m->geometry.scheduler->set_dataquerier(m->data_querier); - + m->eaws_report_load_service = std::make_shared(m->eaws_texture.scheduler->get_uint_id_manager()); m->picker_manager = std::make_shared(); m->label_filter = std::make_shared(); if (m->scheduler_thread) { m->picker_manager->moveToThread(m->scheduler_thread.get()); m->label_filter->moveToThread(m->scheduler_thread.get()); + m->eaws_report_load_service->moveToThread(m->scheduler_thread.get()); } + // clang-format off connect(m->geometry.scheduler.get(), &nucleus::tile::GeometryScheduler::gpu_tiles_updated, RenderThreadNotifier::instance(), &RenderThreadNotifier::notify); connect(m->ortho_texture.scheduler.get(), &nucleus::tile::TextureScheduler::gpu_tiles_updated, RenderThreadNotifier::instance(), &RenderThreadNotifier::notify); + connect(m->surfaceshaded_texture.scheduler.get(), &nucleus::tile::TextureScheduler::gpu_tiles_updated, RenderThreadNotifier::instance(), &RenderThreadNotifier::notify); + connect(m->eaws_texture.scheduler.get(), &nucleus::avalanche::Scheduler::gpu_tiles_updated, RenderThreadNotifier::instance(), &RenderThreadNotifier::notify); connect(m->map_label.scheduler.get(), &nucleus::map_label::Scheduler::gpu_tiles_updated, RenderThreadNotifier::instance(), &RenderThreadNotifier::notify); connect(m->map_label.scheduler.get(), &nucleus::map_label::Scheduler::gpu_tiles_updated, m->picker_manager.get(), &PickerManager::update_quads); connect(m->map_label.scheduler.get(), &nucleus::map_label::Scheduler::gpu_tiles_updated, m->label_filter.get(), &Filter::update_quads); @@ -120,14 +143,10 @@ RenderingContext::RenderingContext(QObject* parent) if (QNetworkInformation::loadDefaultBackend() && QNetworkInformation::instance()) { QNetworkInformation* n = QNetworkInformation::instance(); - m->geometry.scheduler->set_network_reachability(n->reachability()); - m->ortho_texture.scheduler->set_network_reachability(n->reachability()); - m->map_label.scheduler->set_network_reachability(n->reachability()); - // clang-format off - connect(n, &QNetworkInformation::reachabilityChanged, m->geometry.scheduler.get(), &nucleus::tile::Scheduler::set_network_reachability); - connect(n, &QNetworkInformation::reachabilityChanged, m->ortho_texture.scheduler.get(), &nucleus::tile::Scheduler::set_network_reachability); - connect(n, &QNetworkInformation::reachabilityChanged, m->map_label.scheduler.get(), &nucleus::tile::Scheduler::set_network_reachability); - // clang-format on + m->scheduler_director->visit([n](nucleus::tile::Scheduler* sch) { + sch->set_network_reachability(n->reachability()); + connect(n, &QNetworkInformation::reachabilityChanged, sch, &nucleus::tile::Scheduler::set_network_reachability); + }); } #ifdef ALP_ENABLE_THREADING qDebug() << "Scheduler thread: " << m->scheduler_thread.get(); @@ -142,7 +161,6 @@ RenderingContext::~RenderingContext() RenderingContext* RenderingContext::instance() { - static RenderingContext s_instance; return &s_instance; } @@ -157,10 +175,14 @@ void RenderingContext::initialise() // standard tiles m->engine_context->set_tile_geometry(std::make_shared(65)); m->engine_context->set_ortho_layer(std::make_shared(512)); + m->engine_context->set_surfaceshaded_layer(std::make_shared(512)); m->engine_context->tile_geometry()->set_tile_limit(2048); + m->engine_context->set_eaws_layer(std::make_shared()); m->engine_context->tile_geometry()->set_aabb_decorator(m->aabb_decorator); m->engine_context->set_aabb_decorator(m->aabb_decorator); m->engine_context->ortho_layer()->set_tile_limit(1024); + m->engine_context->surfaceshaded_layer()->set_tile_limit(1024); + m->engine_context->eaws_layer()->set_tile_limit(1024); nucleus::utils::thread::async_call(m->geometry.scheduler.get(), [this]() { m->geometry.scheduler->set_enabled(true); }); const auto texture_compression = gl_engine::Texture::compression_algorithm(); @@ -168,6 +190,11 @@ void RenderingContext::initialise() m->ortho_texture.scheduler->set_texture_compression_algorithm(texture_compression); m->ortho_texture.scheduler->set_enabled(true); }); + nucleus::utils::thread::async_call(m->surfaceshaded_texture.scheduler.get(), [this, texture_compression]() { + m->surfaceshaded_texture.scheduler->set_texture_compression_algorithm(texture_compression); + m->surfaceshaded_texture.scheduler->set_enabled(true); + }); + nucleus::utils::thread::async_call(m->eaws_texture.scheduler.get(), [this]() { m->eaws_texture.scheduler->set_enabled(true); }); // labels m->engine_context->set_map_label_manager(std::make_unique(m->aabb_decorator)); @@ -175,8 +202,10 @@ void RenderingContext::initialise() nucleus::utils::thread::async_call(m->map_label.scheduler.get(), [this]() { m->map_label.scheduler->set_enabled(true); }); // clang-format off - connect(m->geometry.scheduler.get(), &nucleus::tile::GeometryScheduler::gpu_tiles_updated, m->engine_context->tile_geometry(), &gl_engine::TileGeometry::update_gpu_tiles); - connect(m->ortho_texture.scheduler.get(), &nucleus::tile::TextureScheduler::gpu_tiles_updated, m->engine_context->ortho_layer(), &gl_engine::TextureLayer::update_gpu_tiles); + connect(m->geometry.scheduler.get(), &nucleus::tile::GeometryScheduler::gpu_tiles_updated, m->engine_context->tile_geometry(), &gl_engine::TileGeometry::update_gpu_tiles); + connect(m->ortho_texture.scheduler.get(), &nucleus::tile::TextureScheduler::gpu_tiles_updated, m->engine_context->ortho_layer(), &gl_engine::TextureLayer::update_gpu_tiles); + connect(m->surfaceshaded_texture.scheduler.get(), &nucleus::tile::TextureScheduler::gpu_tiles_updated, m->engine_context->surfaceshaded_layer(), &gl_engine::TextureLayer::update_gpu_tiles); + connect(m->eaws_texture.scheduler.get(), &nucleus::avalanche::Scheduler::gpu_tiles_updated, m->engine_context->eaws_layer(), &gl_engine::AvalancheWarningLayer::update_gpu_tiles); connect(QOpenGLContext::currentContext(), &QOpenGLContext::aboutToBeDestroyed, m->engine_context.get(), &nucleus::EngineContext::destroy); connect(QOpenGLContext::currentContext(), &QOpenGLContext::aboutToBeDestroyed, this, &RenderingContext::destroy); @@ -200,12 +229,14 @@ void RenderingContext::destroy() m->picker_manager.reset(); m->map_label.scheduler.reset(); m->ortho_texture.scheduler.reset(); + m->eaws_texture.scheduler.reset(); m->scheduler_director.reset(); }); nucleus::utils::thread::sync_call(m->geometry.tile_service.get(), [this]() { m->geometry.tile_service.reset(); m->map_label.tile_service.reset(); m->ortho_texture.tile_service.reset(); + m->eaws_texture.tile_service.reset(); }); m->scheduler_thread->quit(); m->scheduler_thread->wait(500); // msec @@ -261,8 +292,26 @@ nucleus::tile::TextureScheduler* RenderingContext::ortho_scheduler() const return m->ortho_texture.scheduler.get(); } +nucleus::tile::TextureScheduler* RenderingContext::surfaceshaded_scheduler() const +{ + QMutexLocker locker(&m->shared_ptr_mutex); + return m->surfaceshaded_texture.scheduler.get(); +} + SchedulerDirector* RenderingContext::scheduler_director() const { QMutexLocker locker(&m->shared_ptr_mutex); return m->scheduler_director.get(); } + +nucleus::avalanche::Scheduler* RenderingContext::eaws_scheduler() const +{ + QMutexLocker locker(&m->shared_ptr_mutex); + return m->eaws_texture.scheduler.get(); +} + +std::shared_ptr RenderingContext::eaws_report_load_service() const +{ + QMutexLocker locker(&m->shared_ptr_mutex); + return m->eaws_report_load_service; +} diff --git a/app/RenderingContext.h b/app/RenderingContext.h index e49b66610..37d722c97 100644 --- a/app/RenderingContext.h +++ b/app/RenderingContext.h @@ -19,7 +19,6 @@ #pragma once #include - // move to pimpl to avoid including all the stuff in the header. namespace gl_engine { @@ -46,6 +45,12 @@ namespace nucleus::tile::utils { class AabbDecorator; } +namespace nucleus::avalanche { +class Scheduler; +class UIntIdManager; +class ReportLoadService; +} // namespace nucleus::avalanche + class RenderingContext : public QObject { Q_OBJECT QML_ELEMENT @@ -77,7 +82,10 @@ class RenderingContext : public QObject { [[nodiscard]] std::shared_ptr label_filter() const; [[nodiscard]] nucleus::map_label::Scheduler* map_label_scheduler() const; [[nodiscard]] nucleus::tile::TextureScheduler* ortho_scheduler() const; + [[nodiscard]] nucleus::tile::TextureScheduler* surfaceshaded_scheduler() const; [[nodiscard]] nucleus::tile::SchedulerDirector* scheduler_director() const; + [[nodiscard]] nucleus::avalanche::Scheduler* eaws_scheduler() const; + [[nodiscard]] std::shared_ptr eaws_report_load_service() const; signals: void initialised(); diff --git a/app/StatsWindow.qml b/app/StatsWindow.qml index 4df740dff..ead967e96 100644 --- a/app/StatsWindow.qml +++ b/app/StatsWindow.qml @@ -544,7 +544,7 @@ Rectangle { // LABEL //-------------------------- Label { - text: qsTr("Map Label requested: ") + text: qsTr("Label requested: ") } ProgressBar { id: map_label_n_quads_requested @@ -570,6 +570,37 @@ Rectangle { Label { text: "(" + map_label_n_quads_ram.value + ")" } + + //-------------------------- + // Eaws regions + //-------------------------- + Label { + text: qsTr("EAWS requested: ") + } + ProgressBar { + id: eaws_n_quads_requested + Layout.fillWidth: true + from: 0 + to: 500 + value: map.tile_statistics.scheduler.eaws_n_quads_requested * 4 + } + Label { + text: "(" + eaws_n_quads_requested.value + ")" + } + + Label { + text: qsTr("EAWS ram: ") + } + ProgressBar { + id: eaws_n_quads_ram + Layout.fillWidth: true + from: 0 + to: map.tile_statistics.scheduler.eaws_n_quads_ram_max * 4 + value: map.tile_statistics.scheduler.eaws_n_quads_ram * 4 + } + Label { + text: "(" + eaws_n_quads_ram.value + ")" + } } CheckGroup { diff --git a/app/TerrainRenderer.cpp b/app/TerrainRenderer.cpp index 6201b843b..52de76f1d 100644 --- a/app/TerrainRenderer.cpp +++ b/app/TerrainRenderer.cpp @@ -30,6 +30,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -38,7 +41,6 @@ #include #include #include - TerrainRenderer::TerrainRenderer() { using nucleus::map_label::Filter; @@ -59,14 +61,17 @@ TerrainRenderer::TerrainRenderer() // In Qt/QML the rendering thread goes to sleep (at least until Qt 6.5, See RenderThreadNotifier). // At the time of writing, an additional connection from tile_ready and tile_expired to the notifier is made. // this only works if ALP_ENABLE_THREADING is on, i.e., the tile scheduler is on an extra thread. -> potential issue on webassembly - connect(m_camera_controller.get(), &CameraController::definition_changed, ctx->geometry_scheduler(), &Scheduler::update_camera); - connect(m_camera_controller.get(), &CameraController::definition_changed, ctx->map_label_scheduler(), &Scheduler::update_camera); - connect(m_camera_controller.get(), &CameraController::definition_changed, ctx->ortho_scheduler(), &Scheduler::update_camera); - connect(m_camera_controller.get(), &CameraController::definition_changed, m_glWindow.get(), &gl_engine::Window::update_camera); - - connect(ctx->geometry_scheduler(), &nucleus::tile::GeometryScheduler::gpu_tiles_updated, gl_window_ptr, &gl_engine::Window::update_requested); - connect(ctx->ortho_scheduler(), &nucleus::tile::TextureScheduler::gpu_tiles_updated, gl_window_ptr, &gl_engine::Window::update_requested); - connect(ctx->label_filter().get(), &Filter::filter_finished, gl_window_ptr, &gl_engine::Window::update_requested); + connect(m_camera_controller.get(), &CameraController::definition_changed, ctx->geometry_scheduler(), &Scheduler::update_camera); + connect(m_camera_controller.get(), &CameraController::definition_changed, ctx->map_label_scheduler(), &Scheduler::update_camera); + connect(m_camera_controller.get(), &CameraController::definition_changed, ctx->ortho_scheduler(), &Scheduler::update_camera); + connect(m_camera_controller.get(), &CameraController::definition_changed, ctx->surfaceshaded_scheduler(), &Scheduler::update_camera); + connect(m_camera_controller.get(), &CameraController::definition_changed, ctx->eaws_scheduler(), &Scheduler::update_camera); + connect(m_camera_controller.get(), &CameraController::definition_changed, m_glWindow.get(), &gl_engine::Window::update_camera); + + connect(ctx->geometry_scheduler(), &nucleus::tile::GeometryScheduler::gpu_tiles_updated, gl_window_ptr, &gl_engine::Window::update_requested); + connect(ctx->ortho_scheduler(), &nucleus::tile::TextureScheduler::gpu_tiles_updated, gl_window_ptr, &gl_engine::Window::update_requested); + connect(ctx->eaws_scheduler(), &nucleus::avalanche::Scheduler::gpu_tiles_updated, gl_window_ptr, &gl_engine::Window::update_requested); + connect(ctx->label_filter().get(), &Filter::filter_finished, gl_window_ptr, &gl_engine::Window::update_requested); connect(ctx->picker_manager().get(), &PickerManager::pick_requested, gl_window_ptr, &gl_engine::Window::pick_value); connect(gl_window_ptr, &gl_engine::Window::value_picked, ctx->picker_manager().get(), &PickerManager::eval_pick); @@ -74,6 +79,12 @@ TerrainRenderer::TerrainRenderer() m_glWindow->initialise_gpu(); // ctx->scheduler()->set_enabled(true); // after tile manager moves to ctx. + + m_eaws_report_load_service = ctx->eaws_report_load_service(); + connect(ctx->eaws_report_load_service().get(), + &nucleus::avalanche::ReportLoadService::load_from_TU_Wien_finished, + gl_window_ptr, + &gl_engine::Window::update_eaws_reports); } TerrainRenderer::~TerrainRenderer() = default; @@ -147,3 +158,9 @@ gl_engine::Window *TerrainRenderer::glWindow() const } nucleus::camera::Controller* TerrainRenderer::controller() const { return m_camera_controller.get(); } + +std::shared_ptr TerrainRenderer::eaws_report_load_service() +{ + assert(m_eaws_report_load_service); + return m_eaws_report_load_service; +} diff --git a/app/TerrainRenderer.h b/app/TerrainRenderer.h index ca6e9ca17..8f9d013f0 100644 --- a/app/TerrainRenderer.h +++ b/app/TerrainRenderer.h @@ -32,6 +32,9 @@ class Controller; namespace nucleus::camera { class Controller; } +namespace nucleus::avalanche { +class ReportLoadService; +} class TerrainRenderer : public QObject, public QQuickFramebufferObject::Renderer { Q_OBJECT @@ -49,8 +52,11 @@ class TerrainRenderer : public QObject, public QQuickFramebufferObject::Renderer [[nodiscard]] nucleus::camera::Controller* controller() const; + [[nodiscard]] std::shared_ptr eaws_report_load_service(); + private: QQuickWindow* m_window = nullptr; std::unique_ptr m_glWindow; std::unique_ptr m_camera_controller; + std::shared_ptr m_eaws_report_load_service; }; diff --git a/app/TerrainRendererItem.cpp b/app/TerrainRendererItem.cpp index fa369b991..555b653e4 100644 --- a/app/TerrainRendererItem.cpp +++ b/app/TerrainRendererItem.cpp @@ -38,6 +38,8 @@ #include #include #include +#include +#include #include #include #include @@ -118,6 +120,7 @@ QQuickFramebufferObject::Renderer* TerrainRendererItem::createRenderer() const connect(ctx->geometry_scheduler(), &nucleus::tile::Scheduler::stats_ready, this->m_tile_statistics, &TileStatistics::update_scheduler_stats); connect(ctx->map_label_scheduler(), &nucleus::tile::Scheduler::stats_ready, this->m_tile_statistics, &TileStatistics::update_scheduler_stats); connect(ctx->ortho_scheduler(), &nucleus::tile::Scheduler::stats_ready, this->m_tile_statistics, &TileStatistics::update_scheduler_stats); + connect(ctx->eaws_scheduler(), &nucleus::tile::Scheduler::stats_ready, this->m_tile_statistics, &TileStatistics::update_scheduler_stats); connect(m_update_timer, &QTimer::timeout, this, &QQuickFramebufferObject::update); connect(this, &TerrainRendererItem::touch_made, r->controller(), &nucleus::camera::Controller::touch); @@ -153,6 +156,9 @@ QQuickFramebufferObject::Renderer* TerrainRendererItem::createRenderer() const connect(this, &TerrainRendererItem::label_filter_changed, ctx->label_filter().get(), &nucleus::map_label::Filter::update_filter); connect(ctx->picker_manager().get(), &nucleus::picker::PickerManager::pick_evaluated, this, &TerrainRendererItem::set_picked_feature); + connect( + this, &TerrainRendererItem::eaws_report_date_changed, ctx->eaws_report_load_service().get(), &nucleus::avalanche::ReportLoadService::load_from_tu_wien); + #ifdef ALP_ENABLE_DEV_TOOLS connect(r->glWindow(), &gl_engine::Window::timer_measurements_ready, TimerFrontendManager::instance(), &TimerFrontendManager::receive_measurements); #endif @@ -160,7 +166,6 @@ QQuickFramebufferObject::Renderer* TerrainRendererItem::createRenderer() const // We now have to initialize everything based on the url, but we need to do this on the thread this instance // belongs to. (gui thread?) Therefore we use the following signal to signal the init process emit init_after_creation(); - return r; } @@ -515,3 +520,36 @@ void TerrainRendererItem::gl_sundir_date_link_changed(bool) { recalculate_sun_angles(); } + +void TerrainRendererItem::set_eaws_warning_layer(bool value) +{ + gl_engine::uboSharedConfig tmp; + tmp.m_eaws_danger_rating_enabled = value; + set_shared_config(tmp); +} + +void TerrainRendererItem::set_risk_level_layer(bool value) +{ + gl_engine::uboSharedConfig tmp; + tmp.m_eaws_risk_level_enabled = value; + set_shared_config(tmp); +} + +void TerrainRendererItem::set_slope_angle_layer(bool value) +{ + gl_engine::uboSharedConfig tmp; + tmp.m_eaws_slope_angle_enabled = value; + set_shared_config(tmp); +} + +void TerrainRendererItem::set_stop_or_go_layer(bool value) +{ + gl_engine::uboSharedConfig tmp; + tmp.m_eaws_stop_or_go_enabled = value; + set_shared_config(tmp); +} + +void TerrainRendererItem::updateEawsReportDate(int day, int month, int year) +{ + emit eaws_report_date_changed(QDate(year, month, day)); +} diff --git a/app/TerrainRendererItem.h b/app/TerrainRendererItem.h index b3d3120b1..9c2947100 100644 --- a/app/TerrainRendererItem.h +++ b/app/TerrainRendererItem.h @@ -85,7 +85,6 @@ class TerrainRendererItem : public QQuickFramebufferObject { void shared_config_changed(gl_engine::uboSharedConfig new_shared_config) const; void label_filter_changed(const nucleus::map_label::FilterDefinitions label_filter) const; void hud_visible_changed(bool new_hud_visible); - void rotation_north_requested(); void camera_changed(); void camera_width_changed(); @@ -113,6 +112,8 @@ class TerrainRendererItem : public QQuickFramebufferObject { void world_space_cursor_position_changed(const QVector3D& world_space_cursor_position); + void eaws_report_date_changed(QDate newDate) const; // This is emitted after user picked a date for eaws avalanche report in the gui + protected: void touchEvent(QTouchEvent*) override; void mousePressEvent(QMouseEvent*) override; @@ -127,7 +128,11 @@ public slots: void rotate_north(); void set_gl_preset(const QString& preset_b64_string); void camera_definition_changed(const nucleus::camera::Definition& new_definition); // gets called whenever camera changes - + void set_eaws_warning_layer(bool value); + void set_risk_level_layer(bool value); + void set_slope_angle_layer(bool value); + void set_stop_or_go_layer(bool value); + void updateEawsReportDate(int day, int month, int year); private slots: void schedule_update(); void init_after_creation_slot(); diff --git a/app/eaws/banner_eaws_report.png b/app/eaws/banner_eaws_report.png new file mode 100644 index 000000000..e3197550d Binary files /dev/null and b/app/eaws/banner_eaws_report.png differ diff --git a/app/eaws/banner_risk_level.png b/app/eaws/banner_risk_level.png new file mode 100644 index 000000000..04d6ae5ed Binary files /dev/null and b/app/eaws/banner_risk_level.png differ diff --git a/app/eaws/banner_slope_angle.png b/app/eaws/banner_slope_angle.png new file mode 100644 index 000000000..455e2991e Binary files /dev/null and b/app/eaws/banner_slope_angle.png differ diff --git a/app/eaws/banner_stop_or_go.png b/app/eaws/banner_stop_or_go.png new file mode 100644 index 000000000..38935cf57 Binary files /dev/null and b/app/eaws/banner_stop_or_go.png differ diff --git a/app/icons/eaws/eaws_menu.png b/app/icons/eaws/eaws_menu.png new file mode 100644 index 000000000..242c16f3e Binary files /dev/null and b/app/icons/eaws/eaws_menu.png differ diff --git a/app/icons/eaws/eaws_report.png b/app/icons/eaws/eaws_report.png new file mode 100644 index 000000000..2d5721046 Binary files /dev/null and b/app/icons/eaws/eaws_report.png differ diff --git a/app/icons/eaws/risk_level.png b/app/icons/eaws/risk_level.png new file mode 100644 index 000000000..39e05e767 Binary files /dev/null and b/app/icons/eaws/risk_level.png differ diff --git a/app/icons/eaws/slope_angle.png b/app/icons/eaws/slope_angle.png new file mode 100644 index 000000000..9be0c413c Binary files /dev/null and b/app/icons/eaws/slope_angle.png differ diff --git a/app/icons/eaws/stop_or_go.png b/app/icons/eaws/stop_or_go.png new file mode 100644 index 000000000..8d5bf3b75 Binary files /dev/null and b/app/icons/eaws/stop_or_go.png differ diff --git a/gl_engine/AvalancheWarningLayer.cpp b/gl_engine/AvalancheWarningLayer.cpp new file mode 100644 index 000000000..1b0fb20d2 --- /dev/null +++ b/gl_engine/AvalancheWarningLayer.cpp @@ -0,0 +1,119 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Adam Celarek + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "AvalancheWarningLayer.h" + +#include "ShaderProgram.h" +#include "ShaderRegistry.h" +#include "TileGeometry.h" +#include +#include + +namespace gl_engine { + +AvalancheWarningLayer::AvalancheWarningLayer(QObject* parent) + : QObject { parent } +{ +} + +void gl_engine::AvalancheWarningLayer::init(ShaderRegistry* shader_registry, std::shared_ptr surfaceshaded_layer) +{ + m_shader = std::make_shared("tile.vert", "eaws.frag"); + shader_registry->add_shader(m_shader); + + m_texture_array = std::make_unique(Texture::Target::_2dArray, Texture::Format::R16UI); + m_texture_array->setParams(Texture::Filter::Nearest, Texture::Filter::Nearest, false); + m_texture_array->allocate_array(m_resolution, m_resolution, unsigned(m_gpu_array_helper.size())); + + m_instanced_zoom = std::make_unique(Texture::Target::_2d, Texture::Format::R8UI); + m_instanced_zoom->setParams(Texture::Filter::Nearest, Texture::Filter::Nearest); + + m_instanced_array_index = std::make_unique(Texture::Target::_2d, Texture::Format::R16UI); + m_instanced_array_index->setParams(Texture::Filter::Nearest, Texture::Filter::Nearest); + m_surfshaded_layer = surfaceshaded_layer; +} + +void AvalancheWarningLayer::draw( + const TileGeometry& tile_geometry, const nucleus::camera::Definition& camera, const std::vector& draw_list) const +{ + m_shader->bind(); + m_texture_array->bind(2); + m_shader->set_uniform("texture_sampler", 2); + + nucleus::Raster zoom_level_raster = { glm::uvec2 { 1024, 1 } }; + nucleus::Raster array_index_raster = { glm::uvec2 { 1024, 1 } }; + for (unsigned i = 0; i < std::min(unsigned(draw_list.size()), 1024u); ++i) { + const auto layer = m_gpu_array_helper.layer(draw_list[i].id); + zoom_level_raster.pixel({ i, 0 }) = layer.id.zoom_level; + array_index_raster.pixel({ i, 0 }) = layer.index; + } + + m_instanced_array_index->bind(7); + m_shader->set_uniform("instanced_texture_array_index_sampler", 7); + m_instanced_array_index->upload(array_index_raster); + + m_instanced_zoom->bind(8); + m_shader->set_uniform("instanced_texture_zoom_sampler", 8); + m_instanced_zoom->upload(zoom_level_raster); + + m_surfshaded_layer->m_texture_array->bind(9); + m_shader->set_uniform("texture_sampler2", 9); + for (unsigned i = 0; i < std::min(unsigned(draw_list.size()), 1024u); ++i) { + const auto layer = m_surfshaded_layer->m_gpu_array_helper.layer(draw_list[i].id); + zoom_level_raster.pixel({ i, 0 }) = layer.id.zoom_level; + array_index_raster.pixel({ i, 0 }) = layer.index; + } + + m_surfshaded_layer->m_instanced_array_index->bind(10); + m_shader->set_uniform("instanced_texture_array_index_sampler2", 10); + m_surfshaded_layer->m_instanced_array_index->upload(array_index_raster); + + m_surfshaded_layer->m_instanced_zoom->bind(11); + m_shader->set_uniform("instanced_texture_zoom_sampler2", 11); + m_surfshaded_layer->m_instanced_zoom->upload(zoom_level_raster); + + tile_geometry.draw(m_shader.get(), camera, draw_list); +} + +void AvalancheWarningLayer::update_gpu_tiles(const std::vector& deleted_tiles, const std::vector& new_tiles) +{ + if (!QOpenGLContext::currentContext()) // can happen during shutdown. + return; + + for (const auto& tile_id : deleted_tiles) { + m_gpu_array_helper.remove_tile(tile_id); + } + for (const auto& tile : new_tiles) { + // test for validity + assert(tile.id.zoom_level < 100); + assert(tile.texture); + + // find empty spot and upload texture + const auto layer_index = m_gpu_array_helper.add_tile(tile.id); + m_texture_array->upload(*tile.texture, layer_index); + } +} + +void AvalancheWarningLayer::set_tile_limit(unsigned int new_limit) +{ + assert(new_limit < 2048); // array textures with size > 2048 are not supported on all devices + assert(!m_texture_array); + m_gpu_array_helper.set_tile_limit(new_limit); +} + +} // namespace gl_engine diff --git a/gl_engine/AvalancheWarningLayer.h b/gl_engine/AvalancheWarningLayer.h new file mode 100644 index 000000000..e87117504 --- /dev/null +++ b/gl_engine/AvalancheWarningLayer.h @@ -0,0 +1,65 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Adam Celarek + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Texture.h" +#include +#include +#include +#include +#include + +namespace camera { +class Definition; +} + +class QOpenGLShaderProgram; +class QOpenGLBuffer; +class QOpenGLVertexArrayObject; + +namespace gl_engine { +class ShaderRegistry; +class ShaderProgram; +class TileGeometry; +class TextureLayer; + +class AvalancheWarningLayer : public QObject { + Q_OBJECT +public: + explicit AvalancheWarningLayer(QObject* parent = nullptr); + void init(ShaderRegistry* shader_registry, std::shared_ptr surfaceshaded_layer); // needs OpenGL context + void draw(const TileGeometry& tile_geometry, const nucleus::camera::Definition& camera, const std::vector& draw_list) const; + + unsigned int tile_count() const; + +public slots: + void update_gpu_tiles(const std::vector& deleted_tiles, const std::vector& new_tiles); + void set_tile_limit(unsigned new_limit); + +private: + const unsigned m_resolution = 512u; + + std::shared_ptr m_shader; + std::unique_ptr m_texture_array; + std::unique_ptr m_instanced_zoom; + std::unique_ptr m_instanced_array_index; + nucleus::tile::GpuArrayHelper m_gpu_array_helper; + std::shared_ptr m_surfshaded_layer = nullptr; +}; +} // namespace gl_engine diff --git a/gl_engine/CMakeLists.txt b/gl_engine/CMakeLists.txt index 81934f378..885a3a6f3 100644 --- a/gl_engine/CMakeLists.txt +++ b/gl_engine/CMakeLists.txt @@ -44,6 +44,13 @@ qt_add_library(gl_engine STATIC types.h ) + +if (ALP_ENABLE_AVLANCHE_WARNING_LAYER) + target_sources(gl_engine PRIVATE + AvalancheWarningLayer.h AvalancheWarningLayer.cpp + ) +endif() + if(ALP_ENABLE_LABELS) target_sources(gl_engine PRIVATE MapLabels.h MapLabels.cpp @@ -84,6 +91,8 @@ qt_add_resources(gl_engine "shaders" shaders/track.vert shaders/turbo_colormap.glsl shaders/intersection.glsl + shaders/eaws.glsl + shaders/eaws.frag ) target_compile_definitions(gl_engine PUBLIC ALP_RESOURCES_PREFIX="${CMAKE_CURRENT_SOURCE_DIR}/shaders/") diff --git a/gl_engine/Context.cpp b/gl_engine/Context.cpp index efdd3ddce..04eb5ae65 100644 --- a/gl_engine/Context.cpp +++ b/gl_engine/Context.cpp @@ -17,6 +17,7 @@ *****************************************************************************/ #include "Context.h" +#include "AvalancheWarningLayer.h" #include "MapLabels.h" #include "ShaderRegistry.h" #include "TextureLayer.h" @@ -59,12 +60,20 @@ void Context::internal_initialise() if (m_ortho_layer) m_ortho_layer->init(m_shader_registry.get()); + + if (m_surfaceshaded_layer) + m_surfaceshaded_layer->init(m_shader_registry.get()); + + if (m_eaws_layer && m_surfaceshaded_layer) + m_eaws_layer->init(m_shader_registry.get(), m_surfaceshaded_layer); } void Context::internal_destroy() { // this is necessary for a clean shutdown (and we want a clean shutdown for the ci integration test). m_ortho_layer.reset(); + m_surfaceshaded_layer.reset(); + m_eaws_layer.reset(); m_tile_geometry.reset(); m_track_manager.reset(); m_shader_registry.reset(); @@ -73,10 +82,26 @@ void Context::internal_destroy() TextureLayer* Context::ortho_layer() const { return m_ortho_layer.get(); } -void Context::set_ortho_layer(std::shared_ptr new_ortho_layer) +AvalancheWarningLayer* Context::eaws_layer() const { return m_eaws_layer.get(); } + +void Context::set_ortho_layer(std::shared_ptr new_layer) +{ + assert(!is_alive()); // only set before init is called. + m_ortho_layer = std::move(new_layer); +} + +TextureLayer* Context::surfaceshaded_layer() const { return m_surfaceshaded_layer.get(); } + +void Context::set_surfaceshaded_layer(std::shared_ptr new_layer) +{ + assert(!is_alive()); // only set before init is called. + m_surfaceshaded_layer = std::move(new_layer); +} + +void Context::set_eaws_layer(std::shared_ptr new_layer) { assert(!is_alive()); // only set before init is called. - m_ortho_layer = std::move(new_ortho_layer); + m_eaws_layer = std::move(new_layer); } TileGeometry* Context::tile_geometry() const { return m_tile_geometry.get(); } diff --git a/gl_engine/Context.h b/gl_engine/Context.h index e9dde0e39..c478f8506 100644 --- a/gl_engine/Context.h +++ b/gl_engine/Context.h @@ -18,15 +18,19 @@ #pragma once +#include "TrackManager.h" #include -#include "TrackManager.h" +namespace nucleus::avalanche { +class UIntIdManager; +} namespace gl_engine { class MapLabels; class ShaderRegistry; class TileGeometry; class TextureLayer; +class AvalancheWarningLayer; class Context : public nucleus::EngineContext { private: @@ -48,13 +52,21 @@ class Context : public nucleus::EngineContext { [[nodiscard]] TextureLayer* ortho_layer() const; void set_ortho_layer(std::shared_ptr new_ortho_layer); + [[nodiscard]] TextureLayer* surfaceshaded_layer() const; + void set_surfaceshaded_layer(std::shared_ptr new_layer); + + [[nodiscard]] AvalancheWarningLayer* eaws_layer() const; + void set_eaws_layer(std::shared_ptr new_ortho_layer); + protected: void internal_initialise() override; void internal_destroy() override; private: std::shared_ptr m_tile_geometry; + std::shared_ptr m_surfaceshaded_layer; std::shared_ptr m_ortho_layer; + std::shared_ptr m_eaws_layer; std::shared_ptr m_map_label_manager; std::shared_ptr m_track_manager; std::shared_ptr m_shader_registry; diff --git a/gl_engine/TextureLayer.h b/gl_engine/TextureLayer.h index 516600d79..1190ac1b9 100644 --- a/gl_engine/TextureLayer.h +++ b/gl_engine/TextureLayer.h @@ -38,9 +38,12 @@ class ShaderRegistry; class ShaderProgram; class Texture; class TileGeometry; +class AvalancheWarningLayer; class TextureLayer : public QObject { Q_OBJECT + friend class AvalancheWarningLayer; + public: explicit TextureLayer(unsigned resolution = 256, QObject* parent = nullptr); void init(ShaderRegistry* shader_registry); // needs OpenGL context diff --git a/gl_engine/UniformBuffer.cpp b/gl_engine/UniformBuffer.cpp index 6e759eb1e..660d9a64f 100644 --- a/gl_engine/UniformBuffer.cpp +++ b/gl_engine/UniformBuffer.cpp @@ -16,13 +16,14 @@ * along with this program. If not, see . *****************************************************************************/ #include "UniformBuffer.h" -#include #include "ShaderProgram.h" #include "UniformBufferObjects.h" -#include #include #include +#include #include +#include +#include #if defined(__ANDROID__) #include // for GL_UNIFORM_BUFFER! DONT EXACTLY KNOW WHY I NEED THIS HERE! (on other platforms it works without) #endif @@ -89,6 +90,7 @@ template class gl_engine::UniformBuffer; template class gl_engine::UniformBuffer; template class gl_engine::UniformBuffer; template class gl_engine::UniformBuffer; +template class gl_engine::UniformBuffer; template class gl_engine::UniformBuffer>; template class gl_engine::UniformBuffer>; template class gl_engine::UniformBuffer>; diff --git a/gl_engine/UniformBufferObjects.h b/gl_engine/UniformBufferObjects.h index 578b10883..7e8261a1f 100644 --- a/gl_engine/UniformBufferObjects.h +++ b/gl_engine/UniformBufferObjects.h @@ -83,6 +83,11 @@ struct uboSharedConfig { GLuint m_overlay_shadowmaps_enabled = false; GLuint m_padi1 = 0; + GLuint m_eaws_danger_rating_enabled = false; + GLuint m_eaws_risk_level_enabled = false; + GLuint m_eaws_slope_angle_enabled = false; + GLuint m_eaws_stop_or_go_enabled = false; + // WARNING: Don't move the following Q_PROPERTIES to the top, otherwise the MOC // will do weird things with the data alignment!! Q_PROPERTY(QVector4D sun_light MEMBER m_sun_light) @@ -143,7 +148,6 @@ struct uboShadowConfig { glm::vec2 buff; }; - // This struct is only used for unit tests struct uboTestConfig { Q_GADGET diff --git a/gl_engine/Window.cpp b/gl_engine/Window.cpp index ec37477d0..9a91a6f67 100644 --- a/gl_engine/Window.cpp +++ b/gl_engine/Window.cpp @@ -21,6 +21,7 @@ * along with this program. If not, see . *****************************************************************************/ #include "Window.h" +#include "AvalancheWarningLayer.h" #include "Context.h" #include "Framebuffer.h" #include "SSAO.h" @@ -44,6 +45,7 @@ #include #include #include +#include #include #include #include @@ -211,6 +213,10 @@ void Window::initialise_gpu() m_shadow_config_ubo->init(); m_shadow_config_ubo->bind_to_shader(shader_registry->all()); + m_eaws_reports_ubo = std::make_shared>(5, "eaws_reports"); + m_eaws_reports_ubo->init(); + m_eaws_reports_ubo->bind_to_shader(shader_registry->all()); + { // INITIALIZE CPU AND GPU TIMER using namespace std; using nucleus::timing::CpuTimer; @@ -218,17 +224,17 @@ void Window::initialise_gpu() // GPU Timing Queries not supported on Web GL #if (defined(__linux)) || defined(_WIN32) || defined(_WIN64) - m_timer->add_timer(make_shared("ssao", "GPU", 240, 1.0f/60.0f)); + m_timer->add_timer(make_shared("ssao", "GPU", 240, 1.0f / 60.0f)); m_timer->add_timer(make_shared("atmosphere", "GPU", 240, 1.0f / 60.0f)); - m_timer->add_timer(make_shared("tiles", "GPU", 240, 1.0f/60.0f)); - m_timer->add_timer(make_shared("tracks", "GPU", 240, 1.0f/60.0f)); - m_timer->add_timer(make_shared("shadowmap", "GPU", 240, 1.0f/60.0f)); - m_timer->add_timer(make_shared("compose", "GPU", 240, 1.0f/60.0f)); + m_timer->add_timer(make_shared("tiles", "GPU", 240, 1.0f / 60.0f)); + m_timer->add_timer(make_shared("tracks", "GPU", 240, 1.0f / 60.0f)); + m_timer->add_timer(make_shared("shadowmap", "GPU", 240, 1.0f / 60.0f)); + m_timer->add_timer(make_shared("compose", "GPU", 240, 1.0f / 60.0f)); m_timer->add_timer(make_shared("labels", "GPU", 240, 1.0f / 60.0f)); m_timer->add_timer(make_shared("picker", "GPU", 240, 1.0f / 60.0f)); - m_timer->add_timer(make_shared("gpu_total", "TOTAL", 240, 1.0f/60.0f)); + m_timer->add_timer(make_shared("gpu_total", "TOTAL", 240, 1.0f / 60.0f)); #endif - m_timer->add_timer(make_shared("cpu_total", "TOTAL", 240, 1.0f/60.0f)); + m_timer->add_timer(make_shared("cpu_total", "TOTAL", 240, 1.0f / 60.0f)); m_timer->add_timer(make_shared("cpu_b2b", "TOTAL", 240, 1.0f / 60.0f)); m_timer->add_timer(make_shared("draw_list", "TOTAL", 240, 1.0f / 60.0f)); } @@ -240,7 +246,8 @@ void Window::resize_framebuffer(int width, int height) return; QOpenGLFunctions* f = QOpenGLContext::currentContext()->functions(); - if (!f) return; + if (!f) + return; m_gbuffer->resize({ width, height }); { m_decoration_buffer->resize({ width, height }); @@ -260,7 +267,7 @@ void Window::paint(QOpenGLFramebufferObject* framebuffer) m_timer->start_timer("cpu_total"); m_timer->start_timer("gpu_total"); - QOpenGLExtraFunctions *f = QOpenGLContext::currentContext()->extraFunctions(); + QOpenGLExtraFunctions* f = QOpenGLContext::currentContext()->extraFunctions(); f->glEnable(GL_CULL_FACE); f->glCullFace(GL_BACK); @@ -340,18 +347,25 @@ void Window::paint(QOpenGLFramebufferObject* framebuffer) } f->glEnable(GL_DEPTH_TEST); - f->glDepthFunc(GL_GREATER); // reverse z + f->glDepthFunc(GL_GEQUAL); // reverse z, reuse z buffer for sucessive passes m_timer->start_timer("tiles"); - m_context->ortho_layer()->draw(*m_context->tile_geometry(), m_camera, culled_draw_list); + + if (m_shared_config_ubo->data.m_eaws_danger_rating_enabled || m_shared_config_ubo->data.m_eaws_risk_level_enabled + || m_shared_config_ubo->data.m_eaws_slope_angle_enabled || m_shared_config_ubo->data.m_eaws_stop_or_go_enabled) { + m_context->surfaceshaded_layer()->draw(*m_context->tile_geometry(), m_camera, culled_draw_list); + m_context->eaws_layer()->draw(*m_context->tile_geometry(), m_camera, culled_draw_list); + } else { + m_context->ortho_layer()->draw(*m_context->tile_geometry(), m_camera, culled_draw_list); + } m_timer->stop_timer("tiles"); m_gbuffer->unbind(); - if (m_shared_config_ubo->data.m_ssao_enabled) { m_timer->start_timer("ssao"); - m_ssao->draw(m_gbuffer.get(), &m_screen_quad_geometry, m_camera, m_shared_config_ubo->data.m_ssao_kernel, m_shared_config_ubo->data.m_ssao_blur_kernel_size); + m_ssao->draw( + m_gbuffer.get(), &m_screen_quad_geometry, m_camera, m_shared_config_ubo->data.m_ssao_kernel, m_shared_config_ubo->data.m_ssao_blur_kernel_size); m_timer->stop_timer("ssao"); } @@ -382,15 +396,14 @@ void Window::paint(QOpenGLFramebufferObject* framebuffer) m_gbuffer->bind_colour_texture(1, 1); m_compose_shader->set_uniform("texin_normal", 2); m_gbuffer->bind_colour_texture(2, 2); + m_compose_shader->set_uniform("texin_atmosphere", 4); + m_atmospherebuffer->bind_colour_texture(0, 4); - m_compose_shader->set_uniform("texin_atmosphere", 3); - m_atmospherebuffer->bind_colour_texture(0, 3); - - m_compose_shader->set_uniform("texin_ssao", 4); - m_ssao->bind_ssao_texture(4); + m_compose_shader->set_uniform("texin_ssao", 5); + m_ssao->bind_ssao_texture(5); /* texture units 5 - 8 */ - m_shadowmapping->bind_shadow_maps(m_compose_shader.get(), 5); + m_shadowmapping->bind_shadow_maps(m_compose_shader.get(), 6); m_timer->start_timer("compose"); m_screen_quad_geometry.draw(); @@ -459,13 +472,15 @@ void Window::paint(QOpenGLFramebufferObject* framebuffer) emit tile_stats_ready(tile_stats); } -void Window::shared_config_changed(gl_engine::uboSharedConfig ubo) { +void Window::shared_config_changed(gl_engine::uboSharedConfig ubo) +{ m_shared_config_ubo->data = ubo; m_shared_config_ubo->update_gpu_data(); emit update_requested(); } -void Window::reload_shader() { +void Window::reload_shader() +{ auto do_reload = [this]() { auto* shader_manager = m_context->shader_registry(); shader_manager->reload_shaders(); @@ -473,6 +488,7 @@ void Window::reload_shader() { m_shared_config_ubo->bind_to_shader(shader_manager->all()); m_camera_config_ubo->bind_to_shader(shader_manager->all()); m_shadow_config_ubo->bind_to_shader(shader_manager->all()); + m_eaws_reports_ubo->bind_to_shader(shader_manager->all()); qDebug("all shaders reloaded"); emit update_requested(); }; @@ -480,7 +496,7 @@ void Window::reload_shader() { // Reload shaders from the web and afterwards do the reload ShaderProgram::web_download_shader_files_and_put_in_cache(do_reload); #else - // Reset shader cache. The shaders will then be reload from file + // Reset shader cache. The shaders will then be reload from file ShaderProgram::reset_shader_cache(); do_reload(); #endif @@ -513,6 +529,14 @@ void Window::pick_value(const glm::dvec2& screen_space_coordinates) emit value_picked(value); } +void Window::update_eaws_reports(const nucleus::avalanche::UboEawsReports& newUboEawsReports) +{ + assert(m_eaws_reports_ubo); + m_eaws_reports_ubo->data = newUboEawsReports; + m_eaws_reports_ubo->update_gpu_data(); + emit update_requested(); +} + glm::dvec3 Window::position(const glm::dvec2& normalised_device_coordinates) { return m_camera.position() + m_camera.ray_direction(normalised_device_coordinates) * (double)depth(normalised_device_coordinates); diff --git a/gl_engine/Window.h b/gl_engine/Window.h index f9c0bb3c6..16a38aaf8 100644 --- a/gl_engine/Window.h +++ b/gl_engine/Window.h @@ -44,6 +44,11 @@ class QOpenGLTexture; class QOpenGLShaderProgram; class QOpenGLVertexArrayObject; +namespace nucleus::avalanche { +class UIntIdManager; +struct UboEawsReports; +} // namespace nucleus::avalanche + namespace gl_engine { class MapLabels; @@ -52,7 +57,6 @@ class Framebuffer; class SSAO; class ShadowMapping; class Context; - class Window : public nucleus::AbstractRenderWindow, public nucleus::camera::AbstractDepthTester { Q_OBJECT public: @@ -75,6 +79,7 @@ public slots: void shared_config_changed(gl_engine::uboSharedConfig ubo); void reload_shader(); void pick_value(const glm::dvec2& screen_space_coordinates) override; + void update_eaws_reports(const nucleus::avalanche::UboEawsReports& uboEawsReports); signals: void timer_measurements_ready(QList values); @@ -98,7 +103,7 @@ public slots: std::shared_ptr> m_shared_config_ubo; // needs opengl context std::shared_ptr> m_camera_config_ubo; std::shared_ptr> m_shadow_config_ubo; - + std::shared_ptr> m_eaws_reports_ubo; helpers::ScreenQuadGeometry m_screen_quad_geometry; nucleus::camera::Definition m_camera; @@ -110,7 +115,6 @@ public slots: QString m_debug_scheduler_stats; std::unique_ptr m_timer; - }; -} // namespace +} // namespace gl_engine diff --git a/gl_engine/shaders/compose.frag b/gl_engine/shaders/compose.frag index 851a3d937..234803aff 100644 --- a/gl_engine/shaders/compose.frag +++ b/gl_engine/shaders/compose.frag @@ -156,7 +156,6 @@ highp float csm_shadow_term(highp vec4 pos_cws, highp vec3 normal_ws, out lowp i void main() { lowp vec3 albedo = texture(texin_albedo, texcoords).rgb; - highp vec4 pos_dist = texture(texin_position, texcoords); highp vec3 pos_cws = pos_dist.xyz; highp float dist = pos_dist.w; // negative if sky diff --git a/gl_engine/shaders/eaws.frag b/gl_engine/shaders/eaws.frag new file mode 100644 index 000000000..2342b1e9c --- /dev/null +++ b/gl_engine/shaders/eaws.frag @@ -0,0 +1,196 @@ +/***************************************************************************** +* AlpineMaps.org +* Copyright (C) 2022 Adam Celarek +* Copyright (C) 2023 Gerald Kimmersdorfer +* Copyright (C) 2024 Jörg Christian Reiher +* Copyright (C) 2024 Johannes Eschner +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*****************************************************************************/ +#ifdef GL_ES +precision highp float; +#endif + +#include "shared_config.glsl" +#include "camera_config.glsl" +#include "encoder.glsl" +#include "tile_id.glsl" +#include "eaws.glsl" + +uniform highp usampler2DArray texture_sampler; +uniform highp usampler2D instanced_texture_array_index_sampler; +uniform highp usampler2D instanced_texture_zoom_sampler; +uniform highp sampler2DArray texture_sampler2; +uniform highp usampler2D instanced_texture_array_index_sampler2; +uniform highp usampler2D instanced_texture_zoom_sampler2; + +layout (location = 0) out lowp vec3 texout_albedo; +layout (location = 1) out highp vec4 texout_position; +layout (location = 2) out highp uvec2 texout_normal; +layout (location = 3) out lowp vec4 texout_depth; + +flat in highp uvec3 var_tile_id; +in highp vec2 var_uv; +in highp vec3 var_pos_cws; +in highp vec3 var_normal; +flat in highp uint instance_id; + +#if CURTAIN_DEBUG_MODE > 0 +in lowp float is_curtain; +#endif +flat in lowp vec3 vertex_color; +in highp float var_altitude; + +highp vec3 normal_by_fragment_position_interpolation() { + highp vec3 dFdxPos = dFdx(var_pos_cws); + highp vec3 dFdyPos = dFdy(var_pos_cws); + return normalize(cross(dFdxPos, dFdyPos)); +} + +void main() { +#if CURTAIN_DEBUG_MODE == 2 + if (is_curtain == 0.0) { + discard; + } +#endif + highp uvec3 tile_id = var_tile_id; + highp vec2 uv = var_uv; + + // photo texture drawing + decrease_zoom_level_until(tile_id, uv, texelFetch(instanced_texture_zoom_sampler2, ivec2(instance_id, 0), 0).x); + highp float texture_layer2_f = float(texelFetch(instanced_texture_array_index_sampler2, ivec2(instance_id, 0), 0).x); + + lowp vec3 terrain_color = texture(texture_sampler2, vec3(uv, texture_layer2_f)).rgb; + terrain_color = mix(terrain_color, conf.material_color.rgb, conf.material_color.a); + + // Write Position (and distance) in gbuffer + highp float dist = length(var_pos_cws); + texout_position = vec4(var_pos_cws, dist); + + // Write and encode normal in gbuffer + highp vec3 normal = vec3(0.0); + if (conf.normal_mode == 0u) normal = normal_by_fragment_position_interpolation(); + else normal = var_normal; + texout_normal = octNormalEncode2u16(normal); + + // Write and encode distance for readback + texout_depth = vec4(depthWSEncode2n8(dist), 0.0, 0.0); + + // HANDLE OVERLAYS (and mix it with the albedo color) THAT CAN JUST BE DONE IN THIS STAGE + // (because of DATA thats not forwarded) + // NOTE: Performancewise its generally better to handle overlays in the compose step! (screenspace effect) + if (conf.overlay_mode > 0u && conf.overlay_mode < 100u) { + lowp vec3 overlay_color = vec3(0.0); + switch(conf.overlay_mode) { + case 1u: overlay_color = normal * 0.5 + 0.5; break; + default: overlay_color = vertex_color; + } + terrain_color = mix(terrain_color, overlay_color, conf.overlay_strength); + } + +#if CURTAIN_DEBUG_MODE == 1 + if (is_curtain > 0.0) { + texout_albedo = vec3(1.0, 0.0, 0.0); + return; + } +#endif + + + //From here on: EAWS Layer Drawing + decrease_zoom_level_until(tile_id, uv, texelFetch(instanced_texture_zoom_sampler, ivec2(instance_id, 0), 0).x); + highp float texture_layer_f = float(texelFetch(instanced_texture_array_index_sampler, ivec2(instance_id, 0), 0).x); + + highp uint eawsRegionId = texelFetch(texture_sampler, ivec3(int(uv.x * float(512)), int(uv.y * float(512)) , texture_layer_f), 0).r; + ivec4 report = eaws.reports[eawsRegionId]; + vec3 eaws_color = color_no_report_available; + + // // debug output regions + //eaws_color = vec3(float((eawsRegionId >> 8u) & 255u) / 256.0, float(eawsRegionId & 255u) / 256.0, float((eawsRegionId >> 16u) & 255u) / 256.0); + //return; + + // Get altitude and slope normal + highp float frag_height = var_altitude; + vec3 fragNormal = var_normal; // just to clarify naming + + // calculate frag color according to selected overlay type + if(bool(conf.eaws_slope_angle_enabled)) // Slope Angle overlay is activated (does not require avalanche report) + { + // assign a color to slope angle obtained from (not normalized) normal + eaws_color = slopeAngleColorFromNormal(fragNormal); + } + else if(report.a > 0 || report.z > 0 ) // avalanche report is available. report.x = < 0 would mean no report available since .x stores the exposition as described in the Masters THesis of Joey which must be >0 + { + // get avalanche ratings for eaws region of current fragment + int bound = report.y; // bound dividing moutain in Hi region and low region + int ratingHi = report.a; // rating should be value in {0,1,2,3,4} + int ratingLo = report.z; // rating should be value in {0,1,2,3,4} + int rating = ratingLo; + + // if eaws report overlay activated: calculate color for danger level(blend at borders) + if(bool(conf.eaws_danger_rating_enabled)) + { + // color fragment according to danger level + float margin = 200.0; // margin within which colorblending between hi and lo happens + if(frag_height > float(bound)) + eaws_color = color_from_eaws_danger_rating(ratingHi); + else if (frag_height < float(bound) - margin) + eaws_color = color_from_eaws_danger_rating(ratingLo); + else + { + // around border: blend colors between upper and lower danger rating + float a = (frag_height - (float(bound) - margin)) / margin; // This is a value between 0 and 1 + eaws_color = mix(color_from_eaws_danger_rating(ratingLo), color_from_eaws_danger_rating(ratingHi), a); + } + } + + // If risk Level Overlay is activated, read unfavorable expositions information and check if fragment has unfavorable exposition + else if(bool(conf.eaws_risk_level_enabled)) + { + // report.x encodes dangerous directions bitwise as 1000000000 = North is unfavorable, 01000000 = NE is unfavorable etc. + // direction() returns the direction of the fragment + // the bitwise & comparison checks if direction bit is marked in report.x as unfavorable direction + bool unfavorable = (0 != (report.x & direction(fragNormal))); + + // color the fragment according to danger level + float margin = 200.f; // margin within which colorblending between hi and lo happens + if(frag_height > float(bound)) + eaws_color = color_from_snowCard_risk_parameters(ratingHi, fragNormal, unfavorable); + else if (frag_height < float(bound) - margin) + eaws_color = color_from_snowCard_risk_parameters(ratingLo, fragNormal, unfavorable); + else + { + // around border: blend colors between upper and lower danger rating + float a = (frag_height - (float(bound) - margin)) / margin; // This is a value between 0 and 1 + vec3 colorLo = color_from_snowCard_risk_parameters(ratingLo, fragNormal, unfavorable); + vec3 colorHi = color_from_snowCard_risk_parameters(ratingHi, fragNormal, unfavorable); + eaws_color = mix(colorLo, colorHi, a); // color_from_snowCard_risk_parameters(int eaws_danger_rating, int slope_angle_in_deg, bool unfavorable) + } + } + + //if Stop or GO Layer activated + else if(bool(conf.eaws_stop_or_go_enabled)) + { + // Get eaws danger rating from fragment altitude + int eaws_danger_rating = frag_height >= float(bound)? ratingHi: ratingLo; + eaws_color = color_from_stop_or_go(fragNormal, eaws_danger_rating); + } + } + + // merge photo texture with eaws color + if(eaws_color.r > 0.0 || eaws_color.g > 0.0 || eaws_color.b > 0.0) + texout_albedo = mix(terrain_color, eaws_color, 0.5); + else if(eaws_color.r < 0.0 ) // no report available or danger rating = 0: grey + texout_albedo = mix(terrain_color, vec3(0.5,0.5,0.5), 0.9); + else + texout_albedo = terrain_color; +} diff --git a/gl_engine/shaders/eaws.glsl b/gl_engine/shaders/eaws.glsl new file mode 100644 index 000000000..e9f322b5d --- /dev/null +++ b/gl_engine/shaders/eaws.glsl @@ -0,0 +1,184 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Joerg Christian Reiher + * Copyright (C) 2024 Johannes Eschner + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#ifdef GL_ES +precision highp float; +#endif + +layout (std140) uniform eaws_reports { + // length of array must be the same as in nucleus::avalanche::uboEawsReports on host side + ivec4 reports[1000]; +} eaws; + +// Color for areas where no report is available +vec3 color_no_report_available = vec3(-1.0,-1.0,-1.0); + +vec3 color_from_eaws_danger_rating(int rating) +{ + if(1 == rating) return vec3(0.0,1.0,0.0); // green for 1 = low + if(2 == rating) return vec3(1.0,1.0,0.0); // yellow for 2 = moderate + if(3 == rating) return vec3(1.0,0.53f,0.0); // orange for 3 = considerable + if(4 == rating) return vec3(1.0,0.0,0.0); // red for 4 = high + if(5 == rating) return vec3(0.5333,0.0,0.0); // dark red for 5 = extreme + return(color_no_report_available); // grey for undefined cases +} + +vec3 snowCardLevel[6] = vec3[6]( + vec3(1.0 , 1.0 , 1.0 ), // level 0 = white + vec3(0.9961 , 0.8000, 0.3608), // level 1 = yellow + vec3(0.9922 , 0.5530, 0.2353), // level 2 = orange + vec3(0.9412 , 0.2314, 0.1255), // level 3 = red + vec3(0.4588 , 0.0510, 0.1333), // level 4 = dark red + vec3(0.0 ,0.0 ,0.0 ) // level 5 = black +); + + +vec3 slopeAngleColorFromNormal(vec3 notNormalizedNormal) +{ + // Calculte slope angle + vec3 normal = normalize(notNormalizedNormal); + float slope_in_rad = acos(normal.z); + float slope_in_deg = degrees(slope_in_rad); + + // Get color for slope angle + vec3 slopeColor; + if(slope_in_deg < 30.0) // white + slopeColor= vec3(1.0,1.0,1.0); + else if (30.0 <= slope_in_deg && slope_in_deg < 35.0) //yellow + slopeColor = vec3(0.9490196078431372, 0.8980392156862745, 0.0392156862745098); + else if (35.0 <= slope_in_deg && slope_in_deg < 40.0) //orange + slopeColor = vec3(0.95686274, 0.43529411764705883,0.1411764705882353); + else if (40.0 <= slope_in_deg && slope_in_deg < 45.0) //red + slopeColor = vec3(0.8705882352941177, 0.0196078431372549, 0.3568627450980392); + else // purple if > 45 + slopeColor = vec3(0.7843137254901961, 0.5372549019607843, 0.7333333333333333); + + // Return color + return slopeColor; +} + +// SnowCard Risk overlay: +// adapts eaws rating according to slope angle and favorable/unfavorable position +// E.g. favorable1[0] contains snowcard rating for ewas rating level 1 at favorable position for 27deg slope angle, +// favorable1[1]for eaws rating 1 at 28deg etc. up to 45deg +// -------------------------------------------- +// 27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45 +int favorable1[19] = int[19]( 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5); // 1 = danger rating, favorable +int favorable2[19] = int[19]( 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 5); // 2 = danger rating, favorable +int favorable3[19] = int[19]( 0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 4, 5); // 3 = danger rating, favorable +int favorable4[19] = int[19]( 1, 2, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5); // 4 = danger rating, favorable +int favorable5[19] = int[19]( 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5); // 5 = danger rating, favorable +int unfavorable1[19] = int[19]( 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 5); // 1 = danger rating, unfavorable +int unfavorable2[19] = int[19]( 0, 0, 0, 1, 1, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5); // 2 = danger rating, unfavorable +int unfavorable3[19] = int[19]( 0, 0, 0, 1, 2, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5); // 3 = danger rating, unfavorable +int unfavorable4[19] = int[19]( 1, 2, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5); // 4 = danger rating, unfavorable +int unfavorable5[19] = int[19]( 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5); // 5 = danger rating, unfavorable + +// returns warning level according to SnowCard for a given eaws reprot on a slope angle at favorable/unfavorable exposition +// returns 0 if exposition contains something else than 1(favorable) or -1(unfavorable) +vec3 color_from_snowCard_risk_parameters(int eaws_danger_rating, vec3 notNormalizedNormal, bool unfavorable) +{ + // Calculate slope angle and return black if steeper than 45 deg + vec3 normal = normalize(notNormalizedNormal); + float slope_in_rad = acos(normal.z); + int slopeAngleAsInt = int(degrees(slope_in_rad)); + + // Truncate slope angle and calculate corresponding index for accessing array with danger ratings + int angle = min(max(slopeAngleAsInt,27), 45); // angle below 27deg is treated like 27deg (same for above 45deg) + int idx = angle-27; // array[0] contains rating for 27 degree, see above + + // Calculate mixing factor for smooth transition over borders + float a = degrees(slope_in_rad) - float(slopeAngleAsInt); + + //Avoid out index overflow + int nextIdx = min(idx+1,18); + if(true == unfavorable) + { + // pick unfavorable array according to eaws danger rating + switch(eaws_danger_rating) + { + case 0: return color_no_report_available; + case 1: return (1.0-a) * snowCardLevel[unfavorable1[idx]] + a * snowCardLevel[unfavorable1[nextIdx]]; + case 2: return (1.0-a) * snowCardLevel[unfavorable2[idx]] + a * snowCardLevel[unfavorable2[nextIdx]]; + case 3: return (1.0-a) * snowCardLevel[unfavorable3[idx]] + a * snowCardLevel[unfavorable3[nextIdx]]; + case 4: return (1.0-a) * snowCardLevel[unfavorable4[idx]] + a * snowCardLevel[unfavorable4[nextIdx]]; + case 5: return (1.0-a) * snowCardLevel[unfavorable5[idx]] + a * snowCardLevel[unfavorable5[nextIdx]]; + } + } + else // favorable direction + { + // pick favorable array according to eaws danger rating + switch(eaws_danger_rating) + { + case 0: return color_no_report_available; + case 1: return (1.0-a) * snowCardLevel[favorable1[idx]] + a * snowCardLevel[favorable1[nextIdx]]; + case 2: return (1.0-a) * snowCardLevel[favorable2[idx]] + a * snowCardLevel[favorable2[nextIdx]]; + case 3: return (1.0-a) * snowCardLevel[favorable3[idx]] + a * snowCardLevel[favorable3[nextIdx]]; + case 4: return (1.0-a) * snowCardLevel[favorable4[idx]] + a * snowCardLevel[favorable4[nextIdx]]; + case 5: return (1.0-a) * snowCardLevel[favorable5[idx]] + a * snowCardLevel[favorable5[nextIdx]]; + } + } +} + + +// Colors for stop or go map +bool go0to30[5] = bool[5](true, true, true, true, false); +bool go30to35[5] = bool[5](true, true, true, false, false); +bool go35to40[5] = bool[5](true, true, false, false, false); +bool goOver40[5] = bool[5](true, false, false, false, false); +vec3 color_from_stop_or_go(vec3 notNormalizedNormal, int eaws_danger_rating) +{ + // danger rating must be in [1,5] + if(eaws_danger_rating < 1 || 5 < eaws_danger_rating) return color_no_report_available; + int idx = eaws_danger_rating -1; + + // Calculte slope angle + vec3 normal = normalize(notNormalizedNormal); + float slope_in_rad = acos(normal.z); + float slope_in_deg = degrees(slope_in_rad); + bool go = false; + if(slope_in_deg <= 30.0) go = go0to30[idx]; + else if (slope_in_deg <= 35.0) go = go30to35[idx]; + else if (slope_in_deg <= 40.0) go = go35to40[idx]; + else if (slope_in_deg <= 45.0) go = goOver40[idx]; + + if(go) return vec3(0.0,0.0,0.0); // GO : return 0 0 0 so overlay is transparent + return vec3(1.0,0.0,0.0); // STOP: return red; +} + +// converts a 3d normal vector into a bit encoded direction N, NE, E etc +// n must be a unitvector !!! +int direction(vec3 n) +{ + //Ensure n has length = 1 + n = normalize(n); + + // calculate direction of fragment (North, South etc + float angle = sign(n.y)*degrees(acos(n.x)); + if(112.5 <= angle && angle < 157.5) return 1; // Encodes NW = 00000001 + else if(157.5 <= abs(angle) && abs(angle) <=180.0) return (1<<1); // Encodes W = 00000010 + else if(-157.5 <= angle && angle < -112.5) return (1<<2); // Encodes SW = 00000100 + else if(-112.5 <= angle && angle < -67.5) return (1<<3); // Encodes S = 00001000 + else if(-112.5 <= angle && angle < -67.5) return (1<<4); // Encodes SE = 00010000 + else if(0.0 <= abs(angle) && abs(angle) < 22.5) return (1<<5); // Encodes E = 00100000 + else if(22.5 <= angle && angle < 67.5) return (1<<6); // Encode NE = 01000000 + else return (1<<7); // Enncodes N = 10000000 +} + + + diff --git a/gl_engine/shaders/shared_config.glsl b/gl_engine/shaders/shared_config.glsl index 2f71e2023..e1186eca3 100644 --- a/gl_engine/shaders/shared_config.glsl +++ b/gl_engine/shaders/shared_config.glsl @@ -52,4 +52,9 @@ layout (std140) uniform shared_config { highp uint csm_enabled; highp uint overlay_shadowmaps_enabled; highp uint padi1; + + highp uint eaws_danger_rating_enabled; + highp uint eaws_risk_level_enabled; + highp uint eaws_slope_angle_enabled; + highp uint eaws_stop_or_go_enabled; } conf; diff --git a/gl_engine/shaders/tile.frag b/gl_engine/shaders/tile.frag index abb8493bd..5f61d5fe4 100644 --- a/gl_engine/shaders/tile.frag +++ b/gl_engine/shaders/tile.frag @@ -33,6 +33,7 @@ layout (location = 0) out lowp vec3 texout_albedo; layout (location = 1) out highp vec4 texout_position; layout (location = 2) out highp uvec2 texout_normal; layout (location = 3) out lowp vec4 texout_depth; +layout (location = 4) out lowp vec4 texout_eaws; flat in highp uvec3 var_tile_id; in highp vec2 var_uv; @@ -44,10 +45,6 @@ in lowp float is_curtain; flat in lowp vec3 vertex_color; flat in highp uint instance_id; -highp float calculate_falloff(highp float dist, highp float from, highp float to) { - return clamp(1.0 - (dist - from) / (to - from), 0.0, 1.0); -} - highp vec3 normal_by_fragment_position_interpolation() { highp vec3 dFdxPos = dFdx(var_pos_cws); highp vec3 dFdyPos = dFdy(var_pos_cws); diff --git a/gl_engine/shaders/tile.glsl b/gl_engine/shaders/tile.glsl index 02753b7f7..78db05273 100644 --- a/gl_engine/shaders/tile.glsl +++ b/gl_engine/shaders/tile.glsl @@ -37,8 +37,8 @@ highp float y_to_lat(highp float y) { return latRad; } - -void compute_vertex(out vec3 position, out vec2 uv, out uvec3 tile_id, bool compute_normal, out vec3 normal) { +// Note: position contains a corrected z value for normal calculation, altitude is the height above sealevel +void compute_vertex(out vec3 position, out vec2 uv, out uvec3 tile_id, bool compute_normal, out vec3 normal, out float altitude) { tile_id = unpack_tile_id(instance_tile_id_packed); highp uvec3 dtm_tile_id = tile_id; @@ -90,7 +90,7 @@ void compute_vertex(out vec3 position, out vec2 uv, out uvec3 tile_id, bool comp // highp float dtm_texture_layer_f = float(texelFetch(instance_2_array_index_sampler, ivec2(uint(gl_InstanceID), 0), 0).x); highp float dtm_texture_layer_f = float(dtm_array_index); float altitude_tex = float(texture(height_tex_sampler, vec3(dtm_uv, dtm_texture_layer_f)).r); - + altitude = 0.125 * altitude_tex; // Note: for higher zoom levels it would be enough to calculate the altitude_correction_factor on cpu // for lower zoom levels we could bake it into the texture. // there was no measurable difference despite a cos and a atan, so leaving as is for now. @@ -142,5 +142,6 @@ void compute_vertex(out vec3 position) { highp vec2 uv; highp uvec3 tile_id; vec3 normal; - compute_vertex(position, uv, tile_id, false, normal); + highp float altitude; + compute_vertex(position, uv, tile_id, false, normal, altitude); } diff --git a/gl_engine/shaders/tile.vert b/gl_engine/shaders/tile.vert index f43c95f00..c29d648e7 100644 --- a/gl_engine/shaders/tile.vert +++ b/gl_engine/shaders/tile.vert @@ -31,10 +31,10 @@ out lowp float is_curtain; #endif flat out lowp vec3 vertex_color; flat out highp uint instance_id; - +out highp float var_altitude; void main() { - compute_vertex(var_pos_cws, var_uv, var_tile_id, conf.normal_mode == 1u, var_normal); + compute_vertex(var_pos_cws, var_uv, var_tile_id, conf.normal_mode == 1u, var_normal, var_altitude); gl_Position = camera.view_proj_matrix * vec4(var_pos_cws, 1); instance_id = uint(gl_InstanceID); diff --git a/misc/build_custom_qt_for_webassembly b/misc/build_custom_qt_for_webassembly new file mode 100755 index 000000000..a24a6b138 --- /dev/null +++ b/misc/build_custom_qt_for_webassembly @@ -0,0 +1,22 @@ +#!/bin/bash +qt_version="6.8.0" + +# dest_dir="wasm_lite_lto" +# build_dir="wasm_lite_lto_build" +# rm -rf ./${dest_dir}/* +# rm -rf ./${build_dir}/* +# mkdir -p ${build_dir} +# cd ${build_dir} +# /home/madam/bin/Qt/${qt_version}/Src/configure -qt-host-path /home/madam/bin/Qt/${qt_version}/gcc_64/ -release -ltcg $(cat /home/madam/bin/Qt/${qt_version}/qt_lite_alpine_maps.txt) -prefix /home/madam/bin/Qt/${qt_version}/${dest_dir}/ +# sed -i 's/-flto=thin/-flto/g' ./build.ninja +# cmake --build . --parallel && cmake --install . +# cd .. + +dest_dir="wasm_lite" +build_dir="wasm_lite_build" +rm -rf ./${dest_dir}/* +rm -rf ./${build_dir}/* +mkdir -p ${build_dir} +cd ${build_dir} +/home/madam/bin/Qt/${qt_version}/Src/configure -qt-host-path /home/madam/bin/Qt/${qt_version}/gcc_64/ -release $(cat /home/madam/bin/Qt/${qt_version}/qt_lite.txt) -prefix /home/madam/bin/Qt/${qt_version}/${dest_dir}/ && cmake --build . --parallel && cmake --install . +cd .. diff --git a/nucleus/CMakeLists.txt b/nucleus/CMakeLists.txt index 059a8fb27..0080cc216 100644 --- a/nucleus/CMakeLists.txt +++ b/nucleus/CMakeLists.txt @@ -125,7 +125,13 @@ qt_add_library(nucleus STATIC if (ALP_ENABLE_AVLANCHE_WARNING_LAYER) target_sources(nucleus - PUBLIC avalanche/eaws.h avalanche/eaws.cpp + PRIVATE + avalanche/eaws.h avalanche/eaws.cpp + avalanche/Scheduler.h avalanche/Scheduler.cpp + avalanche/ReportLoadService.h avalanche/ReportLoadService.cpp + avalanche/UIntIdManager.h avalanche/UIntIdManager.cpp + avalanche/eaws.h avalanche/eaws.cpp + avalanche/setup.h ) target_link_libraries(nucleus PUBLIC Qt::Gui) endif() diff --git a/nucleus/avalanche/ReportLoadService.cpp b/nucleus/avalanche/ReportLoadService.cpp new file mode 100644 index 000000000..0d1ff958c --- /dev/null +++ b/nucleus/avalanche/ReportLoadService.cpp @@ -0,0 +1,191 @@ +#include "nucleus/avalanche/ReportLoadService.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nucleus::avalanche { + +// helper function that encodes reports in an uint vector +nucleus::avalanche::UboEawsReports convertReportsToUbo( + UboEawsReports& ubo, const std::vector& reports, std::shared_ptr m_uint_id_manager) +{ + // Fill array with initial vectors + std::fill(ubo.reports, ubo.reports + 1000, glm::ivec4(-1, 0, 0, 0)); + + // if reports arrived as expected write them to ubo object + for (const nucleus::avalanche::ReportTUWien& report : reports) { + + // Correct region id if it has invalid format: That means create new sub regions with same forecast + std::vector new_region_ids; + if (report.region_id.endsWith("-00")) { + // remove last three digits + QString parent_region_id = report.region_id; + parent_region_id = parent_region_id.left(parent_region_id.length() - 3); + + // check if our map contains subregions of current report region and save these as vector + uint i = 1; + QString new_region_id = parent_region_id + QString("-01"); + while (m_uint_id_manager->contains(new_region_id)) { + new_region_ids.push_back(new_region_id); + i++; + new_region_id = i < 10 ? parent_region_id + QString("-0") + QString::number(i) : parent_region_id + QString("-") + QString::number(i); + } + + // If no subregions of report region were found, use report region + if (new_region_ids.empty()) { + new_region_ids.push_back(parent_region_id); + } + } else { + new_region_ids.push_back(report.region_id); + } + + // Write (corrected) region(s) to ubo + for (QString new_region_id : new_region_ids) { + uint idx = m_uint_id_manager->convert_region_id_to_internal_id(new_region_id); + ubo.reports[idx] = glm::ivec4(report.unfavorable, report.border, report.rating_lo, report.rating_hi); + } + } + return ubo; +} + +// Constructor: only creates network manager that lives the whole runtime. Ideally the whole app would only use one Manager ! +ReportLoadService::ReportLoadService(std::shared_ptr uint_id_manager) + : m_network_manager(new QNetworkAccessManager(this)) + , m_uint_id_manager(uint_id_manager) +{ +} + +void ReportLoadService::load_from_tu_wien(const QDate& date) const +{ + + // Prepare ubo to be returned + UboEawsReports ubo; + std::fill(ubo.reports, ubo.reports + 1000, glm::ivec4(-1, 0, 0, 0)); + + QString date_string = date.toString("yyyy-MM-dd"); + QUrl qurl(QString("https://alpinemaps.cg.tuwien.ac.at/avalanche-reports-v2/get-current-report?date=" + date_string)); + QNetworkRequest request(qurl); + request.setTransferTimeout(int(8000)); + request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache); +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + request.setAttribute(QNetworkRequest::UseCredentialsAttribute, false); +#endif + + // Make a GET request to the provided url + QNetworkReply* reply = m_network_manager->get(request); + + // Process the reply + QObject::connect(reply, &QNetworkReply::finished, [this, ubo, reply]() { + // Error message is emitted in case something goes wrong + QString error_message("\nERROR: "); + + // Check if Network Error occured + if (reply->error() != QNetworkReply::NoError) { + std::cout << "\nERROR: Eaws Report Load Service has network error."; + emit this->load_from_TU_Wien_finished(ubo); + reply->deleteLater(); + return; + } + + // Read the response data + QByteArray data = reply->readAll(); + + // Convert data to Json + QJsonParseError parse_error; + QJsonDocument json_document = QJsonDocument::fromJson(data, &parse_error); + + // Check for parsing error + if (parse_error.error != QJsonParseError::NoError) { + error_message.append("Parse error = ").append(parse_error.errorString()); + std::cout << error_message.toStdString(); + emit this->load_from_TU_Wien_finished(ubo); + reply->deleteLater(); + return; + } + + // Check for empty json + if (json_document.isEmpty() || json_document.isNull()) { + error_message.append("Empty or Null json."); + std::cout << error_message.toStdString(); + emit this->load_from_TU_Wien_finished(ubo); + reply->deleteLater(); + return; + } + + // Check if json doc is array + if (!json_document.isArray()) { + error_message.append("jsonDocument does not contain array."); + std::cout << error_message.toStdString(); + emit this->load_from_TU_Wien_finished(ubo); + reply->deleteLater(); + return; + } + + // Create json Array and check if empty + QJsonArray jsonArray = json_document.array(); + if (jsonArray.isEmpty()) { + error_message.append("Array empty!"); + std::cout << error_message.toStdString(); + emit this->load_from_TU_Wien_finished(ubo); + reply->deleteLater(); + return; + } + + // parse array containing report for each region + std::vector region_ratings; + for (const QJsonValue& jsonValue_region_rating : jsonArray) { + // prepare an item that goes into the bulletin + ReportTUWien region_rating; + + // Check if Json array contains json objects + if (!jsonValue_region_rating.isObject()) { + error_message.append("json object is array of other type than json object"); + std::cout << error_message.toStdString(); + emit this->load_from_TU_Wien_finished(ubo); + reply->deleteLater(); + return; + } + + // Write regional report to struct + QJsonObject jsonObject_region_rating = jsonValue_region_rating.toObject(); + if (jsonObject_region_rating.contains("regionCode")) + region_rating.region_id = jsonObject_region_rating["regionCode"].toString(); + if (jsonObject_region_rating.contains("dangerBorder")) { + // dangerBorder = null is interpreted as dangerBorder = treeLine = 1600, see Joey's thesis p.44 + QJsonValue val = jsonObject_region_rating["dangerBorder"]; + region_rating.border = ((val.isNull() || val.isUndefined()) ? 1600 : val.toInt()); + } + if (jsonObject_region_rating.contains("dangerRatingHi")) + region_rating.rating_hi = jsonObject_region_rating["dangerRatingHi"].toInt(); + if (jsonObject_region_rating.contains("dangerRatingLo")) + region_rating.rating_lo = jsonObject_region_rating["dangerRatingLo"].toInt(); + if (jsonObject_region_rating.contains("startTime")) + region_rating.start_time = jsonObject_region_rating["startTime"].toString(); + if (jsonObject_region_rating.contains("endTime")) + region_rating.end_time = jsonObject_region_rating["endTime"].toString(); + if (jsonObject_region_rating.contains("unfavorable")) + region_rating.unfavorable = jsonObject_region_rating["unfavorable"].toInt(); + + // Write struct to vector to be returned + region_ratings.push_back(region_rating); + } + + // Convert reports to ubo + nucleus::avalanche::UboEawsReports ubo = convertReportsToUbo(ubo, region_ratings, m_uint_id_manager); + + // Emit ratings + emit this->load_from_TU_Wien_finished(ubo); + reply->deleteLater(); + }); +} + +} // namespace nucleus::avalanche diff --git a/nucleus/avalanche/ReportLoadService.h b/nucleus/avalanche/ReportLoadService.h new file mode 100644 index 000000000..2c9f926d9 --- /dev/null +++ b/nucleus/avalanche/ReportLoadService.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +class QNetworkAccessManager; + +namespace nucleus::avalanche { +class UIntIdManager; + +// Relevant data from a CAAML json provided by avalanche services +struct DangerRatingCAAML { +public: + QString main_value = ""; + int lower_bound = INT_MAX; + int upper_bound = INT_MIN; + QString valid_time_period = ""; + bool operator==(const DangerRatingCAAML& rhs) const = default; +}; + +// Contains a list of regions and the altitude dependent ratings valid for these regions +struct BulletinItemCAAML { +public: + std::unordered_set regions_ids; + std::vector danger_ratings; + bool operator==(const BulletinItemCAAML& rhs) const = default; +}; + +// Contains rating for one region as obtained from TU Wien server +struct ReportTUWien { + QString region_id = ""; + QString start_time = ""; + QString end_time = ""; + int border = INT_MAX; + int rating_hi = -1; + int rating_lo = -1; + int unfavorable = -1; + bool operator==(const ReportTUWien& rhs) const = default; +}; + +// Loads a Bulletinn from the server and converts it to custom struct +class ReportLoadService : public QObject { + Q_OBJECT +private: + std::shared_ptr m_network_manager; + std::shared_ptr m_uint_id_manager; + +public: + ReportLoadService(std::shared_ptr m_uint_id_manager); // Constructor creates a new NetworkManager with id manager it obtains from context +public slots: + void load_from_tu_wien(const QDate& date) const; + +signals: + void load_from_TU_Wien_finished(const nucleus::avalanche::UboEawsReports& ubo) const; + +public: + // QNetworkAccessManager m_network_manager; + const QString m_url_latest_report = "https://static.avalanche.report/bulletins/latest/EUREGIO_de_CAAMLv6.json"; + const QString m_url_custom_report = "https://static.avalanche.report/eaws_bulletins/${date}/${date}-${region}.json"; + + bool operator==(const ReportLoadService& rhs) { return this->m_url_latest_report == rhs.m_url_latest_report; } +}; +} // namespace nucleus::avalanche diff --git a/nucleus/avalanche/Scheduler.cpp b/nucleus/avalanche/Scheduler.cpp new file mode 100644 index 000000000..957a0edc6 --- /dev/null +++ b/nucleus/avalanche/Scheduler.cpp @@ -0,0 +1,106 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2025 Joerg-Christian Reiher + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "Scheduler.h" +#include "eaws.h" +#include +#include + +namespace nucleus::avalanche { + +Scheduler::Scheduler(const Settings& settings) + : nucleus::tile::Scheduler(settings) + , m_default_raster(glm::uvec2(settings.tile_resolution), 0) +{ + m_uint_id_manager = std::make_shared(QDate(2025, 7, 1)); +} + +Scheduler::~Scheduler() = default; + +void Scheduler::transform_and_emit(const std::vector& new_quads, const std::vector& deleted_quads) +{ + std::vector new_gpu_tiles; + new_gpu_tiles.reserve(new_quads.size()); + + Q_UNUSED(deleted_quads) + for (const auto& quad : new_quads) { + nucleus::tile::GpuEawsTile gpu_tile_from_quad; + gpu_tile_from_quad.id = quad.id; + nucleus::Raster quad_as_raster = to_raster(quad, m_default_raster, m_uint_id_manager); + gpu_tile_from_quad.texture = std::make_shared>(quad_as_raster); + new_gpu_tiles.push_back(gpu_tile_from_quad); + } + + emit gpu_tiles_updated(deleted_quads, new_gpu_tiles); +} + +nucleus::Raster Scheduler::to_raster( + const nucleus::tile::DataQuad& quad, const nucleus::Raster& default_raster, std::shared_ptr uint_id_manager) +{ + std::array, 4> quad_rasters; + std::array quad_ids; + for (const auto& tile : quad.tiles) { + const auto quad_index = unsigned(quad_position(tile.id)); + quad_ids[quad_index] = tile.id; + if (!tile.data->size()) { + // Data not available use default raster + quad_rasters[quad_index] = default_raster; + continue; + } + + // Read vector tile from data + tl::expected result = vector_tile_reader(*tile.data, tile.id); + + // could not read vector tile from data, use default raster + if (!result.has_value()) { + quad_rasters[quad_index] = default_raster; + continue; + } + + // Reading tile worked. Create qimage with color coded eaws regions for current tile + RegionTile eaws_region_tile = result.value(); + QImage eawsImage = draw_regions(eaws_region_tile, uint_id_manager, 256, 256, tile.id); + + // Convert Qimage to raster with a 16bit uint region id + nucleus::Raster eaws_raster_16bit(glm::uvec2(256, 256), 0); + for (int i = 0; i < 256; i++) { + for (int j = 0; j < 256; j++) { + glm::u8vec4 color_vector_8bit(0, 0, 0, 0); + QColor color = eawsImage.pixelColor(QPoint(i, j)); + color_vector_8bit.x = static_cast(color.red()); + color_vector_8bit.y = static_cast(color.green()); + eaws_raster_16bit.pixel(glm::uvec2(i, j)) = 256 * color_vector_8bit.x + color_vector_8bit.y; + } + } + + // Collect raster of current tile in quad + quad_rasters[quad_index] = nucleus::Raster(eaws_raster_16bit); + } + + // Merge 4 tiles from quad into one raster representing the quad + nucleus::Raster quad_as_raster + = nucleus::concatenate_horizontally(quad_rasters[unsigned(tile::QuadPosition::TopLeft)], quad_rasters[unsigned(tile::QuadPosition::TopRight)]); + quad_as_raster.append_vertically( + nucleus::concatenate_horizontally(quad_rasters[unsigned(tile::QuadPosition::BottomLeft)], quad_rasters[unsigned(tile::QuadPosition::BottomRight)])); + + // return raster represntation of provided quad + return quad_as_raster; +} + +} // namespace nucleus::avalanche diff --git a/nucleus/avalanche/Scheduler.h b/nucleus/avalanche/Scheduler.h new file mode 100644 index 000000000..260bc3b00 --- /dev/null +++ b/nucleus/avalanche/Scheduler.h @@ -0,0 +1,46 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Adam Celarek + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include +#include +namespace nucleus::avalanche { +class UIntIdManager; +class Scheduler : public nucleus::tile::Scheduler { + Q_OBJECT +public: + Scheduler(const Scheduler::Settings& settings); + ~Scheduler(); + static nucleus::Raster to_raster( + const nucleus::tile::DataQuad& quad, const nucleus::Raster& default_raster, std::shared_ptr uint_id_manager); + std::shared_ptr get_uint_id_manager() { return m_uint_id_manager; } + +signals: + void gpu_tiles_updated(const std::vector& deleted_quads, const std::vector& new_tiles); + +protected: + void transform_and_emit(const std::vector& new_quads, const std::vector& deleted_quads) override; + +private: + nucleus::Raster m_default_raster; + std::shared_ptr m_uint_id_manager; +}; + +} // namespace nucleus::avalanche diff --git a/nucleus/avalanche/UIntIdManager.cpp b/nucleus/avalanche/UIntIdManager.cpp new file mode 100644 index 000000000..8d6d9801f --- /dev/null +++ b/nucleus/avalanche/UIntIdManager.cpp @@ -0,0 +1,100 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Adam Celarek + * Copyright (C) 2025 Joerg-Christian Reiher + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "UIntIdManager.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nucleus::avalanche { +UIntIdManager::UIntIdManager(const QDate& reference_date) + : m_reference_date(reference_date) +{ + // intern_id = 0 means "no region" + m_region_id_to_internal_id[QString("")] = 0; + m_internal_id_to_region_id[0] = QString(""); + assert(m_max_internal_id == 0); +} + +uint UIntIdManager::convert_region_id_to_internal_id(const QString& region_id) +{ + // If Key exists return its value, else add it and return its newly created value + auto entry = m_region_id_to_internal_id.find(region_id); + if (entry == m_region_id_to_internal_id.end()) { + m_region_id_to_internal_id[region_id] = ++m_max_internal_id; + return m_max_internal_id; + } else + return entry->second; +} + +QString UIntIdManager::convert_internal_id_to_region_id(const uint& internal_id) const +{ + auto entry = m_internal_id_to_region_id.find(internal_id); + if (entry == m_internal_id_to_region_id.end()) + return QString(""); + else + return entry->second; +} + +QColor UIntIdManager::convert_region_id_to_color(const QString& region_id) +{ + const uint& internal_id = this->convert_region_id_to_internal_id(region_id); + uint red = internal_id / 256; + uint green = internal_id % 256; + return QColor::fromRgb(red, green, 0); +} + +QString UIntIdManager::convert_color_to_region_id(const QColor& color) const +{ + uint internal_id = color.red() * 256 + color.green(); + auto entry = m_internal_id_to_region_id.find(internal_id); + if (entry == m_internal_id_to_region_id.end()) + return QString(""); + return m_internal_id_to_region_id.at(internal_id); +} + +uint UIntIdManager::convert_color_to_internal_id(const QColor& color) const +{ + QString region_id = this->convert_color_to_region_id(color); + auto entry = m_region_id_to_internal_id.find(region_id); + if (entry == m_region_id_to_internal_id.end()) + return 0; + else + return entry->second; +} + +std::vector UIntIdManager::get_all_registered_region_ids() const +{ + std::vector region_ids(m_internal_id_to_region_id.size()); + for (const auto& [internal_id, region_id] : m_internal_id_to_region_id) + region_ids[internal_id] = region_id; + return region_ids; +} + +bool UIntIdManager::contains(const QString& region_id) const { return m_region_id_to_internal_id.contains(region_id); } + +} // namespace nucleus::avalanche diff --git a/nucleus/avalanche/UIntIdManager.h b/nucleus/avalanche/UIntIdManager.h new file mode 100644 index 000000000..5be115b72 --- /dev/null +++ b/nucleus/avalanche/UIntIdManager.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include + +class QNetworkAccessManager; +namespace nucleus::avalanche { +// This class handles conversion from region-id strings to internal ids as uint and as color +// querying a region, that the manager does not know, returns 0 +// querying an int that is 0 or unknown, returns empty string, +class UIntIdManager : public QObject { + Q_OBJECT + +public: + const std::vector supported_image_formats { QImage::Format_ARGB32 }; + UIntIdManager(const QDate& reference_date); + QColor convert_region_id_to_color(const QString& region_id); + QString convert_color_to_region_id(const QColor& color) const; + uint convert_region_id_to_internal_id(const QString& color); + QString convert_internal_id_to_region_id(const uint& internal_id) const; + uint convert_color_to_internal_id(const QColor& color) const; + bool contains(const QString& region_id) const; + std::vector get_all_registered_region_ids() const; + QDate get_reference_date() const { return m_reference_date; } + +private: + std::unordered_map m_region_id_to_internal_id; + std::unordered_map m_internal_id_to_region_id; + uint m_max_internal_id = 0; + QDate m_reference_date; +}; +} // namespace nucleus::avalanche diff --git a/nucleus/avalanche/eaws.cpp b/nucleus/avalanche/eaws.cpp index 9afff0c6e..1bea72a17 100644 --- a/nucleus/avalanche/eaws.cpp +++ b/nucleus/avalanche/eaws.cpp @@ -17,10 +17,22 @@ *****************************************************************************/ #include "eaws.h" -#include +#include "UIntIdManager.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include -tl::expected avalanche::eaws::vector_tile_reader(const QByteArray& input_data, const tile::Id& tile_id) +namespace nucleus::avalanche { + +tl::expected vector_tile_reader(const QByteArray& input_data, const radix::tile::Id& tile_id) { // This name could theoretically be changed by the EAWS (very unlikely though) const QString& name_of_layer_with_eaws_regions = "micro-regions"; @@ -51,9 +63,9 @@ tl::expected avalanche::eaws::vector_tile_ uint extent = layer.getExtent(); // Loop through features = micro-regions of the layer - std::vector regions_to_be_returned; + std::vector regions_to_be_returned; for (std::size_t feature_index = 0; feature_index < layer.featureCount(); feature_index++) { - avalanche::eaws::Region region; + Region region; region.resolution = glm::ivec2(extent, extent); // Parse properties of the region (name, start date, end date) const protozero::data_view& feature_data_view = layer.getFeature(feature_index); mapbox::vector_tile::feature current_feature(feature_data_view, layer); @@ -83,69 +95,11 @@ tl::expected avalanche::eaws::vector_tile_ } // Combine all regions with their tile id and return this pair - return tl::expected(RegionTile(tile_id, regions_to_be_returned)); -} - -avalanche::eaws::UIntIdManager::UIntIdManager() -{ - // intern_id = 0 means "no region" - region_id_to_internal_id[QString("")] = 0; - internal_id_to_region_id[0] = QString(""); - assert(max_internal_id == 0); -} - -uint avalanche::eaws::UIntIdManager::convert_region_id_to_internal_id(const QString& region_id) -{ - // If Key exists returns its values otherwise create it and return created value - auto entry = region_id_to_internal_id.find(region_id); - if (entry == region_id_to_internal_id.end()) { - max_internal_id++; - region_id_to_internal_id[region_id] = max_internal_id; - return max_internal_id; - } else - return entry->second; -} - -QString avalanche::eaws::UIntIdManager::convert_internal_id_to_region_id(const uint& internal_id) const { return internal_id_to_region_id.at(internal_id); } - -QColor avalanche::eaws::UIntIdManager::convert_region_id_to_color(const QString& region_id, QImage::Format color_format) -{ - assert(this->checkIfImageFormatSupported(color_format)); - const uint& internal_id = this->convert_region_id_to_internal_id(region_id); - assert(internal_id != 0); - uint red = internal_id / 256; - uint green = internal_id % 256; - return QColor::fromRgb(red, green, 0); -} - -QString avalanche::eaws::UIntIdManager::convert_color_to_region_id(const QColor& color, const QImage::Format& color_format) const -{ - assert(QImage::Format_ARGB32 == color_format); - uint internal_id = color.red() * 256 + color.green(); - return internal_id_to_region_id.at(internal_id); -} - -uint avalanche::eaws::UIntIdManager::convert_color_to_internal_id(const QColor& color, const QImage::Format& color_format) -{ - return this->convert_region_id_to_internal_id(this->convert_color_to_region_id(color, color_format)); -} - -QColor avalanche::eaws::UIntIdManager::convert_internal_id_to_color(const uint& internal_id, const QImage::Format& color_format) -{ - return this->convert_region_id_to_color(this->convert_internal_id_to_region_id(internal_id), color_format); -} - -bool avalanche::eaws::UIntIdManager::checkIfImageFormatSupported(const QImage::Format& color_format) const -{ - for (const auto& supported_format : this->supported_image_formats) { - if (color_format == supported_format) - return true; - } - return false; + return tl::expected(RegionTile(tile_id, regions_to_be_returned)); } // Auxillary function: Calculates new coordinates of a region boundary after zoom in / out -std::vector transform_vertices(const avalanche::eaws::Region& region, const tile::Id& tile_id_in, const tile::Id& tile_id_out, QImage* img) +std::vector transform_vertices(const Region& region, const radix::tile::Id& tile_id_in, const radix::tile::Id& tile_id_out, QImage* img) { // Check if input is consistent assert(img->devicePixelRatio() == 1.0); @@ -157,16 +111,16 @@ std::vector transform_vertices(const avalanche::eaws::Region& region, c assert(tile_id_out.coords.y < qPow(2, tile_id_out.zoom_level)); // Check whether we are zooming in or out for the output raster - uint zoom_in = tile_id_in.zoom_level; - uint zoom_out = tile_id_out.zoom_level; - float tile_size_in = qPow(0.5f, zoom_in); - float tile_size_out = qPow(0.5f, zoom_out); + uint zoom_level_in = tile_id_in.zoom_level; + uint zoom_level_out = tile_id_out.zoom_level; + float tile_size_in = qPow(0.5f, zoom_level_in); + float tile_size_out = qPow(0.5f, zoom_level_out); glm::vec2 origin_in = tile_size_in * glm::vec2((float)tile_id_in.coords.x, (float)tile_id_in.coords.y); glm::vec2 origin_out = tile_size_out * glm::vec2((float)tile_id_out.coords.x, (float)tile_id_out.coords.y); float relative_zoom = 1.f; glm::vec2 relative_origin(0.f, 0.f); - if (zoom_in < zoom_out) { + if (zoom_level_in < zoom_level_out) { // Output tile origin must lie within input tile assert(origin_in.x <= origin_out.x && origin_in.y <= origin_out.y); @@ -176,10 +130,14 @@ std::vector transform_vertices(const avalanche::eaws::Region& region, c relative_origin = glm::vec2((origin_out.x - origin_in.x) / tile_size_in, (origin_out.y - origin_in.y) / tile_size_in); // Calculate scale factor - float n = zoom_out - zoom_in; // n is the differnc ein zoom steps between in and out + float n = zoom_level_out - zoom_level_in; // n is the differnc ein zoom steps between in and out relative_zoom = qPow(2, (float)n); + } - } else if (zoom_out < zoom_in) { + // This case does not work and it is not clear at this point if this case is necessary + assert(zoom_level_in <= zoom_level_out); + /* + else if (zoom_level_out < zoom_level_in) { // zoom_in > zoom_out => Output tile origin must lie within input tile assert(origin_out.x <= origin_in.x && origin_out.y <= origin_in.y); assert(origin_in.x + tile_size_in <= origin_out.x + tile_size_out && origin_in.y + tile_size_in <= origin_out.y + tile_size_out); @@ -188,23 +146,28 @@ std::vector transform_vertices(const avalanche::eaws::Region& region, c relative_origin = glm::vec2((origin_in.x - origin_out.x) / tile_size_out, (origin_in.y - origin_out.y) / tile_size_out); // Set logical coordinates to the resolution of the region. These are the coordinates within which we provide vertices of region boundaries - float n = zoom_in - zoom_out; // n is the differnc ein zoom steps between in and out + float n = zoom_level_in - zoom_level_out; // n is the differnc ein zoom steps between in and out relative_zoom = qPow(0.5, (float)n); } + */ // Transform boundary according to input/output tile parameters std::vector transformed_vertices_as_QPointFs; - QTransform trafo - = QTransform::fromTranslate(relative_origin.x, relative_origin.y) * QTransform::fromScale(img->width() * relative_zoom, img->height() * relative_zoom); - for (const glm::vec2& vec : region.vertices_in_local_coordinates) - transformed_vertices_as_QPointFs.push_back(trafo.map(QPointF((float)vec.x, (float)vec.y))); + for (const glm::vec2& vec_in : region.vertices_in_local_coordinates) { + glm::vec2 vec_out = (vec_in - relative_origin) * relative_zoom; + transformed_vertices_as_QPointFs.push_back(QPointF((float)vec_out.x * img->width(), (float)vec_out.y * img->height())); + } // Return transformed vertices return transformed_vertices_as_QPointFs; } -QImage avalanche::eaws::draw_regions(const RegionTile& region_tile, avalanche::eaws::UIntIdManager* internal_id_manager, const uint& image_width, - const uint& image_height, const tile::Id& tile_id_out, const QImage::Format& image_format) +QImage draw_regions(const RegionTile& region_tile, + std::shared_ptr internal_id_manager, + const uint& image_width, + const uint& image_height, + const radix::tile::Id& tile_id_out, + const QImage::Format& image_format) { // Create correctly formatted image to draw to QImage img(image_width, image_height, image_format); @@ -216,13 +179,18 @@ QImage avalanche::eaws::draw_regions(const RegionTile& region_tile, avalanche::e // Draw all regions to the image assert(region_tile.second.size() > 0); - tile::Id tile_id_in = region_tile.first; + radix::tile::Id tile_id_in = region_tile.first; for (const auto& region : region_tile.second) { + // Only draw regions as of July 1st 2025 + QDate refDate(2025, 7, 1); + if ((region.start_date.has_value() && region.start_date > refDate) || (region.end_date.has_value() && region.end_date < refDate)) + continue; + // Calculate vertex coordinates of region w.r.t. output tile at output resolution std::vector transformed_vertices_as_QPointFs = transform_vertices(region, tile_id_in, tile_id_out, &img); // Convert region id to color, for debugging use color_of_region = QColor::fromRgb(255, 255, 255); - QColor color_of_region = internal_id_manager->convert_region_id_to_color(region.id, img.format()); + QColor color_of_region = internal_id_manager->convert_region_id_to_color(region.id); painter.setBrush(QBrush(color_of_region)); painter.setPen(QPen(color_of_region)); // we also have to set the pen if we want to draw boundaries @@ -235,21 +203,21 @@ QImage avalanche::eaws::draw_regions(const RegionTile& region_tile, avalanche::e return img; } -nucleus::Raster avalanche::eaws::rasterize_regions(const RegionTile& region_tile, avalanche::eaws::UIntIdManager* internal_id_manager, - const uint raster_width, const uint raster_height, const tile::Id& tile_id_out) +nucleus::Raster rasterize_regions(const RegionTile& region_tile, + std::shared_ptr internal_id_manager, + const uint raster_width, + const uint raster_height, + const radix::tile::Id& tile_id_out) { // Draw region ids to image, if all pixel have same value return one pixel with this value const QImage img = draw_regions(region_tile, internal_id_manager, raster_width, raster_height, tile_id_out); - const auto raster = nucleus::utils::tile_conversion::qimage_to_u16raster(img); - const auto first_pixel = raster.pixel({ 0, 0 }); - for (auto p : raster) { - if (p != first_pixel) - return raster; - } - return nucleus::Raster({ 1, 1 }, first_pixel); + const auto raster = nucleus::tile::conversion::qimage_to_u16raster(img); + return raster; } -nucleus::Raster avalanche::eaws::rasterize_regions(const RegionTile& region_tile, avalanche::eaws::UIntIdManager* internal_id_manager) +nucleus::Raster rasterize_regions(const RegionTile& region_tile, std::shared_ptr internal_id_manager) { return rasterize_regions(region_tile, internal_id_manager, region_tile.second[0].resolution.x, region_tile.second[0].resolution.y, region_tile.first); } + +} // namespace nucleus::avalanche diff --git a/nucleus/avalanche/eaws.h b/nucleus/avalanche/eaws.h index e954a6f6c..0a6ba661d 100644 --- a/nucleus/avalanche/eaws.h +++ b/nucleus/avalanche/eaws.h @@ -15,17 +15,21 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . *****************************************************************************/ +#pragma once -#ifndef EAWS_H -#define EAWS_H #include #include #include #include #include -#include -namespace avalanche::eaws { +namespace radix::tile { +struct Id; +} + +namespace nucleus::avalanche { +class UIntIdManager; // comes from nucleus/avalanche/UIntIdManager.h + struct Region { public: QString id = ""; // The id is the name of the region e.g. "AT-05-18" @@ -36,7 +40,7 @@ struct Region { = std::vector(); // The vertices of the region's bounding polygon with respect to tile resolution, must be in range [0,1] glm::uvec2 resolution = glm::vec2(4096, 4096); // Tile resolution }; -using RegionTile = std::pair>; +using RegionTile = std::pair>; /* Reads all EAWS regions stored in a provided vector tile * Returns a vector of structs, each containing the name, geometry and "alt-id", "start_date", "end_date" if applicable. @@ -52,36 +56,30 @@ using RegionTile = std::pair>; * @param input_data: An array holding the data read froma vector tile (usually obtained by reading a from a mvt file). * @param tile_id: The zoom, x-y-cordinates and tile-scheme belonging to the input data */ -tl::expected vector_tile_reader(const QByteArray& input_data, const tile::Id& tile_id); - -// This class handles conversion from region-id strings to internal ids as uint and as color -class UIntIdManager { -public: - const std::vector supported_image_formats { QImage::Format_ARGB32 }; - UIntIdManager(); - QColor convert_region_id_to_color(const QString& region_id, QImage::Format color_format = QImage::Format_ARGB32); - QString convert_color_to_region_id(const QColor& color, const QImage::Format& color_format) const; - uint convert_region_id_to_internal_id(const QString& color); - QString convert_internal_id_to_region_id(const uint& internal_id) const; - uint convert_color_to_internal_id(const QColor& color, const QImage::Format& color_format); - QColor convert_internal_id_to_color(const uint& internal_id, const QImage::Format& color_format); - bool checkIfImageFormatSupported(const QImage::Format& color_format) const; +tl::expected vector_tile_reader(const QByteArray& input_data, const radix::tile::Id& tile_id); -private: - std::unordered_map region_id_to_internal_id; - std::unordered_map internal_id_to_region_id; - uint max_internal_id = 0; +// This struct contains report data written to ubo on gpu +struct UboEawsReports { + glm::ivec4 reports[1000]; // ~600 regions where each region has a forecast of the form: .x: unfavorable .y: border .z: rating below border .a: rating above }; // Creates a new QImage and draws all regions to it where color encodes the region id. Throws error when no regions are provided -QImage draw_regions(const RegionTile& region_tile, avalanche::eaws::UIntIdManager* internal_id_manager, const uint& image_width, const uint& image_height, - const tile::Id& tile_id_out, const QImage::Format& image_format = QImage::Format_ARGB32); +// Note: tile_id_out must have greater or equal zoomlevel than tile_id_in +QImage draw_regions(const RegionTile& region_tile, + std::shared_ptr internal_id_manager, + const uint& image_width, + const uint& image_height, + const radix::tile::Id& tile_id_out, + const QImage::Format& image_format = QImage::Format_ARGB32); // Creates a raster from a QImage with regions in it. Throws error when raster_width or raster_height is 0. -nucleus::Raster rasterize_regions(const RegionTile& region_tile, avalanche::eaws::UIntIdManager* internal_id_manager, const uint raster_width, - const uint raster_height, const tile::Id& tile_id_out); +// Note: tile_id_out must have greater or equal zoomlevel than tile_id_in +nucleus::Raster rasterize_regions(const RegionTile& region_tile, + std::shared_ptr internal_id_manager, + const uint raster_width, + const uint raster_height, + const radix::tile::Id& tile_id_out); // Overload: Output has same resolution as EAWS regions, throws error when regions.size() == 0 -nucleus::Raster rasterize_regions(const RegionTile& region_tile, avalanche::eaws::UIntIdManager* internal_id_manager); -} // namespace avalanche::eaws -#endif // EAWS_H +nucleus::Raster rasterize_regions(const RegionTile& region_tile, std::shared_ptr internal_id_manager); +} // namespace nucleus::avalanche diff --git a/nucleus/avalanche/setup.h b/nucleus/avalanche/setup.h new file mode 100644 index 000000000..4bd9a2508 --- /dev/null +++ b/nucleus/avalanche/setup.h @@ -0,0 +1,96 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Adam Celarek + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include "Scheduler.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nucleus::avalanche::setup { + +using TileLoadServicePtr = std::unique_ptr; + +struct EawsTextureSchedulerHolder { + std::shared_ptr scheduler; + TileLoadServicePtr tile_service; +}; + +inline EawsTextureSchedulerHolder eaws_texture_scheduler(TileLoadServicePtr tile_service, + const tile::utils::AabbDecoratorPtr& aabb_decorator, + QThread* thread = nullptr) +{ + Scheduler::Settings settings; + settings.max_zoom_level = 18; + settings.tile_resolution = 256; + settings.gpu_quad_limit = 512; + settings.ram_quad_limit = 12000; + + std::shared_ptr scheduler = std::make_unique(settings); + scheduler->set_aabb_decorator(aabb_decorator); + + { + using nucleus::tile::QuadAssembler; + using nucleus::tile::RateLimiter; + using nucleus::tile::SlotLimiter; + using nucleus::tile::TileLoadService; + + auto* sch = scheduler.get(); + auto* sl = new SlotLimiter(sch); + auto* rl = new RateLimiter(sch); + auto* qa = new QuadAssembler(sch); + + QObject::connect(sch, &Scheduler::quads_requested, sl, &SlotLimiter::request_quads); + QObject::connect(sl, &SlotLimiter::quad_requested, rl, &RateLimiter::request_quad); + QObject::connect(rl, &RateLimiter::quad_requested, qa, &QuadAssembler::load); + QObject::connect(qa, &QuadAssembler::tile_requested, tile_service.get(), &TileLoadService::load); + QObject::connect(tile_service.get(), &TileLoadService::load_finished, qa, &QuadAssembler::deliver_tile); + + QObject::connect(qa, &QuadAssembler::quad_loaded, sl, &SlotLimiter::deliver_quad); + QObject::connect(sl, &SlotLimiter::quad_delivered, sch, &Scheduler::receive_quad); + } + if (QNetworkInformation::loadDefaultBackend() && QNetworkInformation::instance()) { + QNetworkInformation* n = QNetworkInformation::instance(); + scheduler->set_network_reachability(n->reachability()); + QObject::connect(n, &QNetworkInformation::reachabilityChanged, scheduler.get(), &Scheduler::set_network_reachability); + } + + Q_UNUSED(thread); +#ifdef ALP_ENABLE_THREADING +#ifdef __EMSCRIPTEN__ // make request from main thread on webassembly due to QTBUG-109396 + tile_service->moveToThread(QCoreApplication::instance()->thread()); +#else + if (thread) + tile_service->moveToThread(thread); +#endif + if (thread) + scheduler->moveToThread(thread); +#endif + + return { std::move(scheduler), std::move(tile_service) }; +} + + +} // namespace nucleus::tile::setup diff --git a/nucleus/tile/Scheduler.cpp b/nucleus/tile/Scheduler.cpp index c2ce0826a..ef6a41eb0 100644 --- a/nucleus/tile/Scheduler.cpp +++ b/nucleus/tile/Scheduler.cpp @@ -124,7 +124,7 @@ void Scheduler::update_gpu_quads() return false; if (!is_ready_to_ship(quad)) return false; - if (quad.id.zoom_level > 10 && quad.network_info().status != NetworkInfo::Status::Good) + if (quad.id.zoom_level > 8 && quad.network_info().status != NetworkInfo::Status::Good) return false; if (m_gpu_cached.contains(quad.id)) return true; diff --git a/nucleus/tile/Scheduler.h b/nucleus/tile/Scheduler.h index 1d918a14e..a65a4b545 100644 --- a/nucleus/tile/Scheduler.h +++ b/nucleus/tile/Scheduler.h @@ -133,6 +133,5 @@ public slots: utils::AabbDecoratorPtr m_aabb_decorator; Cache m_ram_cache; Cache m_gpu_cached; - }; } diff --git a/nucleus/tile/TextureScheduler.h b/nucleus/tile/TextureScheduler.h index 2cb0fcd7e..3fe17a24c 100644 --- a/nucleus/tile/TextureScheduler.h +++ b/nucleus/tile/TextureScheduler.h @@ -33,7 +33,7 @@ class TextureScheduler : public Scheduler { static Raster to_raster(const tile::DataQuad& data_quad, const Raster& default_raster); signals: - void gpu_tiles_updated(const std::vector& deleted_tiles, const std::vector& new_tiles); + void gpu_tiles_updated(const std::vector& deleted_tiles, const std::vector& new_tiles); protected: void transform_and_emit(const std::vector& new_quads, const std::vector& deleted_quads) override; diff --git a/nucleus/tile/types.h b/nucleus/tile/types.h index a8beaae9e..e1bbdd8ca 100644 --- a/nucleus/tile/types.h +++ b/nucleus/tile/types.h @@ -92,11 +92,26 @@ struct GpuTextureTile { }; static_assert(NamedTile); +struct GpuEawsTile { + tile::Id id; + std::shared_ptr> texture; +}; +static_assert(NamedTile); + struct TileBounds { tile::Id id; tile::SrsAndHeightBounds bounds = {}; }; + +struct GpuEawsQuad { + tile::Id id; + std::array tiles; +}; + +static_assert(NamedTile); + + struct GpuGeometryTile { tile::Id id; tile::SrsAndHeightBounds bounds = {}; diff --git a/plain_renderer/Window.cpp b/plain_renderer/Window.cpp index 7fcc7584e..44c4860b6 100644 --- a/plain_renderer/Window.cpp +++ b/plain_renderer/Window.cpp @@ -50,10 +50,7 @@ void Window::paintGL() p.drawRect(8, 8, 16, 16); } -gl_engine::Window* Window::render_window() -{ - return m_gl_window; -} +gl_engine::Window* Window::render_window() { return m_gl_window; } void Window::closeEvent(QCloseEvent*) { @@ -65,20 +62,11 @@ void Window::closeEvent(QCloseEvent*) m_gl_window = nullptr; } -void Window::mousePressEvent(QMouseEvent* e) -{ - emit mouse_pressed(nucleus::event_parameter::make(e)); -} +void Window::mousePressEvent(QMouseEvent* e) { emit mouse_pressed(nucleus::event_parameter::make(e)); } -void Window::mouseMoveEvent(QMouseEvent* e) -{ - emit mouse_moved(nucleus::event_parameter::make(e)); -} +void Window::mouseMoveEvent(QMouseEvent* e) { emit mouse_moved(nucleus::event_parameter::make(e)); } -void Window::wheelEvent(QWheelEvent* e) -{ - emit wheel_turned(nucleus::event_parameter::make(e)); -} +void Window::wheelEvent(QWheelEvent* e) { emit wheel_turned(nucleus::event_parameter::make(e)); } void Window::keyPressEvent(QKeyEvent* e) { @@ -96,10 +84,7 @@ void Window::keyReleaseEvent(QKeyEvent* e) emit key_released(e->keyCombination()); } -void Window::touchEvent(QTouchEvent* e) -{ - emit touch_made(nucleus::event_parameter::make(e)); -} +void Window::touchEvent(QTouchEvent* e) { emit touch_made(nucleus::event_parameter::make(e)); } void Window::key_timer() { diff --git a/plain_renderer/Window.h b/plain_renderer/Window.h index b037b09c9..b94f72895 100644 --- a/plain_renderer/Window.h +++ b/plain_renderer/Window.h @@ -25,8 +25,7 @@ #include "gl_engine/Window.h" #include "nucleus/event_parameter.h" -class Window : public QOpenGLWindow -{ +class Window : public QOpenGLWindow { Q_OBJECT public: Window(std::shared_ptr context); @@ -65,4 +64,3 @@ private slots: int m_keys_pressed = 0; bool m_closing = false; }; - diff --git a/unittests/gl_engine/uniformbuffer.cpp b/unittests/gl_engine/uniformbuffer.cpp index e929adea5..758e4d83a 100644 --- a/unittests/gl_engine/uniformbuffer.cpp +++ b/unittests/gl_engine/uniformbuffer.cpp @@ -39,6 +39,8 @@ #include "UnittestGLContext.h" +#include + using Catch::Approx; using gl_engine::Framebuffer; using gl_engine::ShaderProgram; @@ -213,4 +215,30 @@ TEST_CASE("gl uniformbuffer") CHECK((ubo1 != ubo2) == false); CHECK((ubo1 != ubo3) == true); } + SECTION("test eaws ubo") + { + // NOTE: If theres an error here, check proper alignment first!!! + Framebuffer b(Framebuffer::DepthFormat::None, { Framebuffer::ColourFormat::RGBA8 }); + ShaderProgram shader = create_debug_shader2(R"( + #include "eaws.glsl" + out lowp vec4 out_Number; + void main() { + out_Number = vec4(0, 0, 0, 0); + if (eaws.reports[0].x == -1) + out_Number = vec4(1, 1, 1, 1); + } + )"); + auto ubo = std::make_unique>(0, "eaws_reports"); + ubo->init(); + ubo->bind_to_shader(&shader); + + ubo->data.reports[0] = glm::vec4(-1, 0, 0, 0); + ubo->update_gpu_data(); + + b.bind(); + shader.bind(); + gl_engine::helpers::create_screen_quad_geometry().draw(); + const auto value_at_0_0 = b.read_colour_attachment_pixel(0, glm::dvec2(-1.0, -1.0)); + CHECK(value_at_0_0.x == 255u); + } } diff --git a/unittests/nucleus/CMakeLists.txt b/unittests/nucleus/CMakeLists.txt index 0ee5ece32..7c37320e2 100644 --- a/unittests/nucleus/CMakeLists.txt +++ b/unittests/nucleus/CMakeLists.txt @@ -44,9 +44,9 @@ alp_add_unittest(unittests_nucleus tile_drawing.cpp ) + if (ALP_ENABLE_AVLANCHE_WARNING_LAYER) - target_sources(unittests_nucleus - PRIVATE + target_sources(unittests_nucleus PRIVATE avalanche_warning_layer.cpp ) endif() @@ -67,9 +67,6 @@ qt_add_resources(unittests_nucleus "test_data" data/test-tile.png data/example.gpx data/vectortile.mvt - data/eaws_0-0-0.mvt - data/eaws_2-2-0.mvt - data/eaws_10-236-299.mvt data/rasterizer_simple_triangle.png data/rasterizer_output_random_triangle.png data/quad/7_68_82.jpg @@ -77,6 +74,14 @@ qt_add_resources(unittests_nucleus "test_data" data/quad/7_69_82.jpg data/quad/7_69_83.jpg data/quad/merged.jpg + data/eaws_0-0-0.mvt + data/eaws_2-2-0.mvt + data/eaws_10-236-299.mvt + data/eaws_6-33-22.mvt + data/eaws_7-66-44.mvt + data/eaws_7-66-45.mvt + data/eaws_7-67-44.mvt + data/eaws_7-67-45.mvt ) target_link_libraries(unittests_nucleus PUBLIC nucleus Catch2::Catch2 Qt::Test Qt::Gui) target_compile_definitions(unittests_nucleus PUBLIC "ALP_TEST_DATA_DIR=\":/test_data/\"") diff --git a/unittests/nucleus/avalanche_warning_layer.cpp b/unittests/nucleus/avalanche_warning_layer.cpp index 3280fa5e4..711434025 100644 --- a/unittests/nucleus/avalanche_warning_layer.cpp +++ b/unittests/nucleus/avalanche_warning_layer.cpp @@ -17,10 +17,20 @@ * along with this program. If not, see . *****************************************************************************/ +#include "test_helpers.h" #include +#include #include - +#include +#include +#include +#include #include +#include +#include +#include +#include +#include TEST_CASE("nucleus/EAWS Vector Tiles") { @@ -57,12 +67,12 @@ TEST_CASE("nucleus/EAWS Vector Tiles") CHECK(layer.getExtent() > 0); // Check if reader returns a std::vector with EAWS regions when reading mvt file - tile::Id tile_id_0_0_0({ 0, glm::uvec2(0, 0), tile::Scheme::SlippyMap }); - tl::expected result = avalanche::eaws::vector_tile_reader(test_data, tile_id_0_0_0); + radix::tile::Id tile_id_0_0_0({ 0, glm::uvec2(0, 0), radix::tile::Scheme::SlippyMap }); + tl::expected result = nucleus::avalanche::vector_tile_reader(test_data, tile_id_0_0_0); CHECK(result.has_value()); // Check if EAWS region struct is initialized with empty attributes - const avalanche::eaws::Region empty_eaws_region; + const nucleus::avalanche::Region empty_eaws_region; CHECK("" == empty_eaws_region.id); CHECK(std::nullopt == empty_eaws_region.id_alt); CHECK(std::nullopt == empty_eaws_region.start_date); @@ -70,16 +80,16 @@ TEST_CASE("nucleus/EAWS Vector Tiles") CHECK(empty_eaws_region.vertices_in_local_coordinates.empty()); // Check for some samples of the returned regions if they have the correct properties - avalanche::eaws::RegionTile region_tile_0_0_0; - std::vector eaws_regions_0_0_0; + nucleus::avalanche::RegionTile region_tile_0_0_0; + std::vector eaws_regions_0_0_0; if (result.has_value()) { // Retrieve vector of all eaws regions region_tile_0_0_0 = result.value(); eaws_regions_0_0_0 = region_tile_0_0_0.second; // Retrieve samples that should have certain properties - avalanche::eaws::Region region_with_start_date, region_with_end_date, region_with_id_alt; - for (const avalanche::eaws::Region& region : eaws_regions_0_0_0) { + nucleus::avalanche::Region region_with_start_date, region_with_end_date, region_with_id_alt; + for (const nucleus::avalanche::Region& region : eaws_regions_0_0_0) { if ("DE-BY-10" == region.id) { region_with_id_alt = region; region_with_end_date = region; @@ -118,16 +128,37 @@ TEST_CASE("nucleus/EAWS Vector Tiles") } // Create internal id manager that is later needed to write region ids to image pixels - avalanche::eaws::UIntIdManager internal_id_manager; - CHECK(internal_id_manager.convert_internal_id_to_region_id(0) == ""); - CHECK(internal_id_manager.convert_region_id_to_internal_id("") == 0); + std::shared_ptr internal_id_manager = std::make_shared(QDate(2025, 7, 1)); + internal_id_manager->convert_region_id_to_internal_id(QString("TestRegion1")); + internal_id_manager->convert_region_id_to_internal_id(QString("TestRegion2")); + CHECK(internal_id_manager->convert_region_id_to_internal_id("") == 0); + std::vector all_region_Ids = internal_id_manager->get_all_registered_region_ids(); + bool internal_maps_match = true; + for (uint i = 0; i < all_region_Ids.size(); i++) { + if (internal_id_manager->convert_region_id_to_internal_id(all_region_Ids[i]) != i) { + internal_maps_match = false; + break; + } + } + CHECK(internal_maps_match); + + // Check if conversion color << id << color works consistentenly + bool wrong_conversion = false; + for (QString region_id : all_region_Ids) { + QColor color = internal_id_manager->convert_region_id_to_color(region_id); + QString region_id_from_color = internal_id_manager->convert_color_to_region_id(color); + wrong_conversion = (region_id != region_id_from_color); + if (wrong_conversion) + break; + } + CHECK((!wrong_conversion)); // Load tiles at higher zoom level for testing std::vector file_names({ "eaws_2-2-0.mvt", "eaws_10-236-299.mvt" }); - tile::Id tile_id_2_2_0 = tile::Id(tile::Id(2, glm::vec2(2, 0), tile::Scheme::SlippyMap)); - tile::Id tile_id_10_236_299 = tile::Id(tile::Id(10, glm::vec2(236, 299), tile::Scheme::SlippyMap)); - std::vector tile_ids_at_zoom_Level_2({ tile_id_2_2_0, tile_id_10_236_299 }); - std::vector region_tiles_at_zoom_level_2; + radix::tile::Id tile_id_2_2_0 = radix::tile::Id(radix::tile::Id(2, glm::vec2(2, 0), radix::tile::Scheme::SlippyMap)); + radix::tile::Id tile_id_10_236_299 = radix::tile::Id(radix::tile::Id(10, glm::vec2(236, 299), radix::tile::Scheme::SlippyMap)); + std::vector tile_ids_at_zoom_Level_2({ tile_id_2_2_0, tile_id_10_236_299 }); + std::vector region_tiles_at_zoom_level_2; for (uint i = 0; i < file_names.size(); i++) { std::string test_file_name2 = file_names[i]; filepath = QString("%1%2").arg(ALP_TEST_DATA_DIR, test_file_name2.c_str()); @@ -144,45 +175,141 @@ TEST_CASE("nucleus/EAWS Vector Tiles") layer = tileBuffer2.getLayer("micro-regions"); CHECK(layer.featureCount() > 0); CHECK(layer.getExtent() > 0); - auto result = avalanche::eaws::vector_tile_reader(test_data2, tile_ids_at_zoom_Level_2[i]); + auto result = nucleus::avalanche::vector_tile_reader(test_data2, tile_ids_at_zoom_Level_2[i]); CHECK(result.has_value()); if (result.has_value()) region_tiles_at_zoom_level_2.push_back(result.value()); } CHECK(region_tiles_at_zoom_level_2.size() == file_names.size()); - avalanche::eaws::RegionTile region_tile_2_2_0; - avalanche::eaws::RegionTile region_tile_10_236_299; + nucleus::avalanche::RegionTile region_tile_2_2_0; + nucleus::avalanche::RegionTile region_tile_10_236_299; if (2 <= region_tiles_at_zoom_level_2.size()) { region_tile_2_2_0 = region_tiles_at_zoom_level_2[0]; region_tile_10_236_299 = region_tiles_at_zoom_level_2[1]; } // Rasterize all regions at same raster reslution as input regions - const auto raster = avalanche::eaws::rasterize_regions( - region_tile_0_0_0, &internal_id_manager, region_with_start_date.resolution.x, region_with_start_date.resolution.y, tile_id_0_0_0); + const auto raster = nucleus::avalanche::rasterize_regions( + region_tile_0_0_0, internal_id_manager, region_with_start_date.resolution.x, region_with_start_date.resolution.y, tile_id_0_0_0); // Check if raster has correct size CHECK((raster.width() == region_with_start_date.resolution.x && raster.height() == region_with_start_date.resolution.y)); // Check if raster contains correct internal region-ids at certain pixels CHECK(0 == raster.pixel(glm::uvec2(0, 0))); - CHECK(internal_id_manager.convert_region_id_to_internal_id(region_with_start_date.id) == raster.pixel(glm::vec2(2128, 1459))); + CHECK(internal_id_manager->convert_region_id_to_internal_id(region_with_start_date.id) == raster.pixel(glm::vec2(2128, 1459))); // Check if raster and image have same values when drawn with same resolution - QImage img_small = avalanche::eaws::draw_regions(region_tile_2_2_0, &internal_id_manager, 20, 20, tile_id_2_2_0); - const auto raster_small = avalanche::eaws::rasterize_regions(region_tile_2_2_0, &internal_id_manager, 20, 20, tile_id_2_2_0); + QImage img_small = nucleus::avalanche::draw_regions(region_tile_2_2_0, internal_id_manager, 20, 20, tile_id_2_2_0); + const auto raster_small = nucleus::avalanche::rasterize_regions(region_tile_2_2_0, internal_id_manager, 20, 20, tile_id_2_2_0); for (uint i = 0; i < 10; i++) { for (uint j = 0; j < 10; j++) { - uint id_from_img = internal_id_manager.convert_color_to_internal_id(img_small.pixel(i, j), QImage::Format_ARGB32); + uint id_from_img = internal_id_manager->convert_color_to_internal_id(img_small.pixel(i, j)); uint id_from_raster = raster_small.pixel(glm::uvec2(i, j)); CHECK(id_from_img == id_from_raster); } } // Check if tile that has only region NO-3035 in it produces a 1x1 raster with the corresponding internal region id - const auto raster_with_one_pixel = avalanche::eaws::rasterize_regions(region_tile_10_236_299, &internal_id_manager); - CHECK((1 == raster_with_one_pixel.width() && 1 == raster_with_one_pixel.width())); - CHECK((1 == raster_with_one_pixel.width() && 1 == raster_with_one_pixel.height())); - CHECK(internal_id_manager.convert_region_id_to_internal_id("NO-3035") == raster_with_one_pixel.pixel(glm::uvec2(0, 0))); + const auto raster_NO3035 = nucleus::avalanche::rasterize_regions(region_tile_10_236_299, internal_id_manager); + CHECK(internal_id_manager->convert_region_id_to_internal_id("NO-3035") == raster_NO3035.pixel(glm::uvec2(0, 0))); + } +} + +TEST_CASE("nucleus/avalanche/ReportLoadService") +{ + // Loads this report item and tests correct processing: + //{"regionCode":"AT-02-02-00","dangerBorder":2400,"dangerRatingHi":2,"dangerRatingLo":1,"startTime":"2025-01-05T16:00:00.000Z","endTime":"2025-01-06T16:00:00.000Z","unfavorable":225} + + // Load id of region we will test for correct avalanche report + std::shared_ptr id_manager = std::make_shared(QDate(2025, 7, 1)); + uint testId = id_manager->convert_region_id_to_internal_id(QString("AT-02-02")); + // Create Report Load Service and let it load a reference report + nucleus::avalanche::ReportLoadService reportLoadService(id_manager); + QSignalSpy spy(&reportLoadService, &nucleus::avalanche::ReportLoadService::load_from_TU_Wien_finished); + reportLoadService.load_from_tu_wien(QDate(2025, 1, 6)); + spy.wait(10000); + REQUIRE(spy.count() == 1); + QList arguments = spy.takeFirst(); + REQUIRE(arguments.size() == 1); + tl::expected, QString> result + = qvariant_cast, QString>>(arguments.at(0)); + CHECK(result.has_value()); + if (result.has_value()) { + nucleus::avalanche::UboEawsReports ubo = arguments.at(0).value(); + CHECK(ubo.reports[0].x == -1); + CHECK(ubo.reports[testId].x == 225); + CHECK(ubo.reports[testId].y == 2400); + CHECK(ubo.reports[testId].z == 1); + CHECK(ubo.reports[testId].w == 2); + } +} + +#include +#include +QByteArray load_raw_data_from_file(const std::string& test_file_name) +{ + QString filepath = QString("%1%2").arg(ALP_TEST_DATA_DIR, test_file_name.c_str()); + QFile file(filepath); + CHECK(file.exists()); + CHECK(file.size() > 0); + REQUIRE(file.open(QIODevice::ReadOnly | QIODevice::Unbuffered)); + QByteArray raw_data = file.readAll(); + file.close(); + return raw_data; +} + +std::pair load_tile_from_file(const std::string& test_file_name, const radix::tile::Id& tile_id) +{ + QByteArray test_data = load_raw_data_from_file(test_file_name); + CHECK(test_data.size() > 0); + tl::expected result = nucleus::avalanche::vector_tile_reader(test_data, tile_id); + CHECK(result.has_value()); + nucleus::avalanche::RegionTile region_tile = result.value(); + return std::pair(test_data, region_tile); +} + +TEST_CASE("nucleus/avalanche/Scheduler") +{ + SECTION("to_raster") + { + // Build Quad and save its tiles as raster + QDate refDate(2025, 7, 1); + std::shared_ptr id_manager = std::make_shared(refDate); + nucleus::tile::DataQuad quad; + quad.id = radix::tile::Id { 6, { 33, 22 }, radix::tile::Scheme::SlippyMap }; + std::vector tiles; + std::vector> rasters; + rasters.reserve(4); + unsigned int idx = 1; + for (radix::tile::Id tile_id : quad.id.children()) { + quad.tiles[idx].id = tile_id; + QString file_name = QString("eaws_%1-%2-%3.mvt").arg(tile_id.zoom_level).arg(tile_id.coords.x).arg(tile_id.coords.y); + std::pair data_and_tile = load_tile_from_file(file_name.toStdString(), tile_id); + rasters.push_back(rasterize_regions(data_and_tile.second, id_manager, 256, 256, data_and_tile.second.first)); + + quad.tiles[idx].data = std::make_shared(std::move(data_and_tile.first)); + quad.tiles[idx].network_info = { nucleus::tile::NetworkInfo::Status::Good, 12345 }; + idx = (idx + 1) % 4; + } + quad.n_tiles = 4; + + // use "to_raster" on quad and compare result to previously loaded tile rasters + nucleus::Raster default_raster(glm::uvec2(256, 256), glm::uint16 { 255 }); + const auto joined = nucleus::avalanche::Scheduler::to_raster(quad, default_raster, id_manager); + REQUIRE(joined.width() == 512); + REQUIRE(joined.height() == 512); + for (int i = 0; i < 4; i++) { + REQUIRE(rasters[i].width() == 256); + REQUIRE(rasters[i].height() == 256); + } + CHECK(joined.pixel(glm::uvec2(0, 0)) == rasters[0].pixel(glm::uvec2(0, 0))); + CHECK(joined.pixel(glm::uvec2(255, 0)) == rasters[0].pixel(glm::uvec2(255, 0))); + CHECK(joined.pixel(glm::uvec2(256, 0)) == rasters[1].pixel(glm::uvec2(0, 0))); + CHECK(joined.pixel(glm::uvec2(511, 0)) == rasters[1].pixel(glm::uvec2(255, 0))); + CHECK(joined.pixel(glm::uvec2(0, 255)) == rasters[2].pixel(glm::uvec2(0, 0))); + CHECK(joined.pixel(glm::uvec2(255, 255)) == rasters[2].pixel(glm::uvec2(255, 0))); + CHECK(joined.pixel(glm::uvec2(256, 255)) == rasters[3].pixel(glm::uvec2(0, 0))); + CHECK(joined.pixel(glm::uvec2(511, 511)) == rasters[3].pixel(glm::uvec2(255, 255))); } } diff --git a/unittests/nucleus/data/eaws_6-33-22.mvt b/unittests/nucleus/data/eaws_6-33-22.mvt new file mode 100644 index 000000000..44fc6c038 Binary files /dev/null and b/unittests/nucleus/data/eaws_6-33-22.mvt differ diff --git a/unittests/nucleus/data/eaws_7-66-44.mvt b/unittests/nucleus/data/eaws_7-66-44.mvt new file mode 100644 index 000000000..c25a18f21 Binary files /dev/null and b/unittests/nucleus/data/eaws_7-66-44.mvt differ diff --git a/unittests/nucleus/data/eaws_7-66-45.mvt b/unittests/nucleus/data/eaws_7-66-45.mvt new file mode 100644 index 000000000..66c9304e7 Binary files /dev/null and b/unittests/nucleus/data/eaws_7-66-45.mvt differ diff --git a/unittests/nucleus/data/eaws_7-67-44.mvt b/unittests/nucleus/data/eaws_7-67-44.mvt new file mode 100644 index 000000000..3aca2e53c Binary files /dev/null and b/unittests/nucleus/data/eaws_7-67-44.mvt differ diff --git a/unittests/nucleus/data/eaws_7-67-45.mvt b/unittests/nucleus/data/eaws_7-67-45.mvt new file mode 100644 index 000000000..37fc49518 Binary files /dev/null and b/unittests/nucleus/data/eaws_7-67-45.mvt differ diff --git a/unittests/nucleus/rasterizer.cpp b/unittests/nucleus/rasterizer.cpp index 492e7e3ea..d2d1b7191 100644 --- a/unittests/nucleus/rasterizer.cpp +++ b/unittests/nucleus/rasterizer.cpp @@ -31,6 +31,7 @@ #include +namespace { /* * calculates how far away the given position is from a triangle * uses distance to shift the current triangle distance @@ -158,7 +159,7 @@ std::pair>> triangulate(std::vect return std::make_pair(cdt.triangles, cdt.vertices); } -QImage example_rasterizer_image(QString filename) +QImage example_rasterizer_image(const QString& filename) { auto image_file = QFile(QString("%1%2").arg(ALP_TEST_DATA_DIR, filename)); REQUIRE(image_file.open(QFile::ReadOnly)); @@ -166,6 +167,7 @@ QImage example_rasterizer_image(QString filename) REQUIRE(!QImage::fromData(image_bytes).isNull()); return QImage::fromData(image_bytes); } +} // namespace TEST_CASE("nucleus/rasterizer") {