diff --git a/.cppQuickFix b/.cppQuickFix new file mode 100644 index 00000000..873e6d80 --- /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/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 451e62bf..6e7b3f9e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -39,7 +39,7 @@ jobs: fetch-depth: 0 fetch-tags: true - - name: Install Qt native version (the one provided by aqt doesn't seem to work) + - name: Install Qt native version (the one provided by aqt does not seem to work) uses: jurplel/install-qt-action@v4 with: aqtversion: '==3.1.*' diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 10a298f8..975b723d 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -65,16 +65,6 @@ jobs: modules: 'qtcharts qtpositioning' cache: true - - name: Debug output - shell: bash - run: | - echo "${{github.workspace}}/qt/Qt/6.8.1": - ls ${{github.workspace}}/qt/Qt/6.8.1 - echo "===" - echo "${QT_ROOT_DIR}/lib/cmake/Qt6Linguist:" - ls ${QT_ROOT_DIR}/lib/cmake/Qt6Linguist - echo "===" - - name: Configure env: CC: ${{ matrix.CC }} diff --git a/.gitignore b/.gitignore index 6cd6bbc5..45eeb0a4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,12 @@ /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 diff --git a/CMakeLists.txt b/CMakeLists.txt index fdf56fc0..37993fc7 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 4a1a860c..359cc3e0 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 5403ac8e..fe7741c6 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 4c555638..f01bc04b 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 7ff57f47..0c60b1c4 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 e49b6661..37d722c9 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 3438d73f..568b665f 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 6201b843..52de76f1 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 ca6e9ca1..8f9d013f 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 fa369b99..555b653e 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 b3d3120b..9c294710 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/TrackModel.cpp b/app/TrackModel.cpp index 7717c1f9..b9341acf 100644 --- a/app/TrackModel.cpp +++ b/app/TrackModel.cpp @@ -134,8 +134,10 @@ void TrackModel::upload_track() #else const auto path = QFileDialog::getOpenFileName(nullptr, tr("Open GPX track"), "", "GPX (*.gpx *.xml)"); auto file = QFile(path); - file.open(QFile::ReadOnly); - fileContentReady(file.fileName(), file.readAll()); + if (file.open(QFile::ReadOnly)) + fileContentReady(file.fileName(), file.readAll()); + else + qDebug() << "Failed to open " << file.fileName(); #endif } diff --git a/app/eaws/banner_eaws_report.png b/app/eaws/banner_eaws_report.png new file mode 100644 index 00000000..e3197550 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 00000000..04d6ae5e 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 00000000..455e2991 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 00000000..38935cf5 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 00000000..242c16f3 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 00000000..2d572104 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 00000000..39e05e76 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 00000000..9be0c413 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 00000000..8d5bf3b7 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 00000000..1b0fb20d --- /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 00000000..e8711750 --- /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 81934f37..885a3a6f 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 efdd3ddc..04eb5ae6 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 e9dde0e3..c478f850 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/Texture.cpp b/gl_engine/Texture.cpp index 50819172..8230bb71 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -56,6 +56,8 @@ GlParams gl_tex_params(gl_engine::Texture::Format format) return { GL_RG8, GL_RG, GL_UNSIGNED_BYTE, 2, 1, true }; case F::RG32UI: return { GL_RG32UI, GL_RG_INTEGER, GL_UNSIGNED_INT, 2, 4 }; + case F::RGB32UI: + return { GL_RGB32UI, GL_RGB_INTEGER, GL_UNSIGNED_INT, 3, 4 }; case F::R8UI: return { GL_R8UI, GL_RED_INTEGER, GL_UNSIGNED_BYTE, 1, 1 }; case F::R16UI: @@ -227,8 +229,9 @@ template void gl_engine::Texture::upload(const nucleus::Raster& template void gl_engine::Texture::upload(const nucleus::Raster&, unsigned); template void gl_engine::Texture::upload(const nucleus::Raster&, unsigned); template void gl_engine::Texture::upload(const nucleus::Raster&, unsigned); -template void gl_engine::Texture::upload>(const nucleus::Raster>&, unsigned); template void gl_engine::Texture::upload>(const nucleus::Raster>&, unsigned); +template void gl_engine::Texture::upload>(const nucleus::Raster>&, unsigned); +template void gl_engine::Texture::upload>(const nucleus::Raster>&, unsigned); template void gl_engine::Texture::upload>(const nucleus::Raster>&, unsigned); template void gl_engine::Texture::upload>(const nucleus::Raster>&, unsigned); @@ -257,6 +260,7 @@ template void gl_engine::Texture::upload(const nucleus::Raster template void gl_engine::Texture::upload(const nucleus::Raster&); template void gl_engine::Texture::upload(const nucleus::Raster&); template void gl_engine::Texture::upload>(const nucleus::Raster>&); +template void gl_engine::Texture::upload>(const nucleus::Raster>&); template void gl_engine::Texture::upload>(const nucleus::Raster>&); template void gl_engine::Texture::upload>(const nucleus::Raster>&); template void gl_engine::Texture::upload>(const nucleus::Raster>&); diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index 697d1820..e8cb32e3 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -38,6 +38,7 @@ class Texture { RGBA32F, RG8, // normalised on gpu RG32UI, + RGB32UI, R8UI, R16UI, R32UI, @@ -83,7 +84,12 @@ class Texture { extern template void gl_engine::Texture::upload(const nucleus::Raster&); extern template void gl_engine::Texture::upload(const nucleus::Raster&); extern template void gl_engine::Texture::upload>(const nucleus::Raster>&); +extern template void gl_engine::Texture::upload>(const nucleus::Raster>&); extern template void gl_engine::Texture::upload>(const nucleus::Raster>&); extern template void gl_engine::Texture::upload>(const nucleus::Raster>&); +extern template void gl_engine::Texture::upload(const nucleus::Raster&, unsigned int); +extern template void gl_engine::Texture::upload>(const nucleus::Raster>&, unsigned int); +extern template void gl_engine::Texture::upload>(const nucleus::Raster>&, unsigned int); + } // namespace gl_engine diff --git a/gl_engine/TextureLayer.h b/gl_engine/TextureLayer.h index 516600d7..1190ac1b 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 6e759eb1..660d9a64 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 578b1088..7e8261a1 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 ec37477d..9a91a6f6 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 f9c0bb3c..16a38aaf 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 fb886f34..2d6259ca 100644 --- a/gl_engine/shaders/compose.frag +++ b/gl_engine/shaders/compose.frag @@ -151,7 +151,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 00000000..2342b1e9 --- /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 00000000..e9f322b5 --- /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 2f71e202..e1186eca 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 abb8493b..5f61d5fe 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 02753b7f..78db0527 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 f43c95f0..c29d648e 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 00000000..a24a6b13 --- /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 a50455b7..0080cc21 100644 --- a/nucleus/CMakeLists.txt +++ b/nucleus/CMakeLists.txt @@ -29,6 +29,7 @@ if(ALP_ENABLE_LABELS) alp_add_git_repository(vector_tiles URL https://github.com/AlpineMapsOrg/vector-tile.git COMMITISH faba88257716c4bc01ebd44d8b8b98f711ecb78c) endif() alp_add_git_repository(goofy_tc URL https://github.com/AlpineMapsOrgDependencies/Goofy_slim.git COMMITISH 13b228784960a6227bb6ca704ff34161bbac1b91 DO_NOT_ADD_SUBPROJECT) +alp_add_git_repository(cdt URL https://github.com/artem-ogre/CDT.git COMMITISH 46f1ce1f495a97617d90e8c833d0d29406335fdf DO_NOT_ADD_SUBPROJECT) add_library(zppbits INTERFACE) target_include_directories(zppbits SYSTEM INTERFACE ${zppbits_SOURCE_DIR}) @@ -40,6 +41,9 @@ set_target_properties(goofy_tc PROPERTIES SYSTEM true) add_library(tl_expected INTERFACE) target_include_directories(tl_expected INTERFACE ${tl_expected_SOURCE_DIR}/include) +add_library(cdt INTERFACE) +target_include_directories(cdt INTERFACE ${cdt_SOURCE_DIR}/CDT/include) + set(alp_version_out ${CMAKE_BINARY_DIR}/alp_version/nucleus/version.cpp) # cmake tests for existance of ${alp_version_out}.do_always_run. since it's always missing, cmake tries to generate it using this command. @@ -113,6 +117,7 @@ qt_add_library(nucleus STATIC tile/GeometryScheduler.h tile/GeometryScheduler.cpp utils/error.h utils/lang.h + utils/rasterizer.h utils/rasterizer.cpp tile/SchedulerDirector.h tile/SchedulerDirector.cpp tile/drawing.h tile/drawing.cpp camera/gesture.h @@ -120,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() @@ -147,7 +158,7 @@ endif() target_include_directories(nucleus PUBLIC ${CMAKE_SOURCE_DIR}) # Please keep Qt::Gui outside the nucleus. If you need it optional via a cmake based switch -target_link_libraries(nucleus PUBLIC radix Qt::Core Qt::Network zppbits tl_expected nucleus_version stb_slim goofy_tc) +target_link_libraries(nucleus PUBLIC radix Qt::Core Qt::Network zppbits tl_expected nucleus_version stb_slim goofy_tc cdt) qt_add_resources(nucleus "icons" PREFIX "/map_icons" diff --git a/nucleus/avalanche/ReportLoadService.cpp b/nucleus/avalanche/ReportLoadService.cpp new file mode 100644 index 00000000..0d1ff958 --- /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 00000000..2c9f926d --- /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 00000000..957a0edc --- /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 00000000..260bc3b0 --- /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 00000000..8d6d9801 --- /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 00000000..5be115b7 --- /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 9afff0c6..1bea72a1 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 e954a6f6..0a6ba661 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 00000000..4bd9a250 --- /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/camera/gesture.h b/nucleus/camera/gesture.h index d3f898e9..1dd03294 100644 --- a/nucleus/camera/gesture.h +++ b/nucleus/camera/gesture.h @@ -19,6 +19,7 @@ #pragma once #include +#include #include #include #include diff --git a/nucleus/tile/Scheduler.cpp b/nucleus/tile/Scheduler.cpp index c2ce0826..ef6a41eb 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 1d918a14..a65a4b54 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 2cb0fcd7..3fe17a24 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/conversion.h b/nucleus/tile/conversion.h index 91a9cd1f..403a9a72 100644 --- a/nucleus/tile/conversion.h +++ b/nucleus/tile/conversion.h @@ -115,4 +115,38 @@ inline glm::u8vec4 uint162alpineRGBA(uint16_t v) { return { v >> 8, v & 255, 0, 255 }; } + +inline QImage u8raster_to_qimage(const nucleus::Raster& raster) +{ + size_t width = raster.width(); + size_t height = raster.height(); + + QImage image(width, height, QImage::Format_Grayscale8); + + for (size_t y = 0; y < height; ++y) { + uchar* line = image.scanLine(y); + for (size_t x = 0; x < width; ++x) { + line[x] = raster.pixel({ x, y }); + } + } + + return image; +} + +inline QImage u8raster_2_to_qimage(const nucleus::Raster& raster1, const nucleus::Raster& raster2) +{ + size_t width = raster1.width(); + size_t height = raster1.height(); + + QImage image(width, height, QImage::Format_RGB888); + + for (size_t y = 0; y < height; ++y) { + for (size_t x = 0; x < width; ++x) { + image.setPixel(QPoint(x, y), (raster1.pixel({ x, y }) << 16) + (raster2.pixel({ x, y }) << 8)); + } + } + + return image; +} + } // namespace nucleus::tile::conversion diff --git a/nucleus/tile/types.h b/nucleus/tile/types.h index a8beaae9..e1bbdd8c 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/nucleus/utils/rasterizer.cpp b/nucleus/utils/rasterizer.cpp new file mode 100644 index 00000000..cc621b93 --- /dev/null +++ b/nucleus/utils/rasterizer.cpp @@ -0,0 +1,103 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Lucas Dworschak + * + * 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 "rasterizer.h" + +#include + +namespace nucleus::utils::rasterizer { + +std::vector generate_neighbour_edges(std::vector polygon_points, const size_t start_offset) +{ + std::vector edges; + { // create the edges + edges.reserve(polygon_points.size()); + for (size_t i = 0; i < polygon_points.size() - 1; i++) { + edges.push_back(glm::ivec2(start_offset + int(i), start_offset + int(i + 1))); + } + + // last edge between start and end vertex + edges.push_back(glm::ivec2(start_offset + polygon_points.size() - 1, start_offset)); + } + + return edges; +} + +std::vector triangulize(std::vector polygon_points, std::vector edges, bool remove_duplicate_vertices) +{ + std::vector processed_triangles; + + // triangulation + CDT::Triangulation cdt; + + if (remove_duplicate_vertices) { + CDT::RemoveDuplicatesAndRemapEdges( + polygon_points, + [](const glm::vec2& p) { return p.x; }, + [](const glm::vec2& p) { return p.y; }, + edges.begin(), + edges.end(), + [](const glm::ivec2& p) { return p.x; }, + [](const glm::ivec2& p) { return p.y; }, + [](CDT::VertInd start, CDT::VertInd end) { return glm::ivec2 { start, end }; }); + } + + cdt.insertVertices(polygon_points.begin(), polygon_points.end(), [](const glm::vec2& p) { return p.x; }, [](const glm::vec2& p) { return p.y; }); + cdt.insertEdges(edges.begin(), edges.end(), [](const glm::ivec2& p) { return p.x; }, [](const glm::ivec2& p) { return p.y; }); + cdt.eraseOuterTrianglesAndHoles(); + + // fill our own data structures + for (size_t i = 0; i < cdt.triangles.size(); ++i) { + auto tri = cdt.triangles[i]; + + std::vector tri_indices = { tri.vertices[0], tri.vertices[1], tri.vertices[2] }; + + int top_index = (cdt.vertices[tri.vertices[0]].y < cdt.vertices[tri.vertices[1]].y) ? ((cdt.vertices[tri.vertices[0]].y < cdt.vertices[tri.vertices[2]].y) ? 0 : 2) + : ((cdt.vertices[tri.vertices[1]].y < cdt.vertices[tri.vertices[2]].y) ? 1 : 2); + // for middle and bottom index we first initialize them randomly with the values that still need to be tested + int middle_index; + int bottom_index; + if (top_index == 0) { + middle_index = 1; + bottom_index = 2; + } else if (top_index == 1) { + middle_index = 2; + bottom_index = 0; + } else { + middle_index = 0; + bottom_index = 1; + } + + // and now we test if we assigned them correctly + if (cdt.vertices[tri.vertices[middle_index]].y > cdt.vertices[tri.vertices[bottom_index]].y) { + // if not we have to interchange them + int tmp = middle_index; + middle_index = bottom_index; + bottom_index = tmp; + } + + // lastly add the vertices to the vector in the correct order + processed_triangles.push_back({ cdt.vertices[tri.vertices[top_index]].x, cdt.vertices[tri.vertices[top_index]].y }); + processed_triangles.push_back({ cdt.vertices[tri.vertices[middle_index]].x, cdt.vertices[tri.vertices[middle_index]].y }); + processed_triangles.push_back({ cdt.vertices[tri.vertices[bottom_index]].x, cdt.vertices[tri.vertices[bottom_index]].y }); + } + + return processed_triangles; +} + +} // namespace nucleus::utils::rasterizer diff --git a/nucleus/utils/rasterizer.h b/nucleus/utils/rasterizer.h new file mode 100644 index 00000000..7168d795 --- /dev/null +++ b/nucleus/utils/rasterizer.h @@ -0,0 +1,558 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Lucas Dworschak + * + * 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 +#include + +#include + +#include + +namespace nucleus::utils::rasterizer { +/* + * Possible future improvements: + * - check how often each pixel is written to -> maybe we can improve performance here + */ + +/* PixelWriterFunctionConcept + * The functions use a PixelWriterFunction as a parameter. + * The pixelwriter function is a lambda with one glm::ivec2 position parameter and one optional unsigned int data argument + * glm::ivec2 position + * the position where a pixel should be rendered + * unsigned int data + * the triangle/line-segment index + */ +template +concept PixelWriterFunctionConcept = requires(Func f) { + { f(glm::ivec2(11, 22)) }; +} || requires(Func f) { + { f(glm::ivec2(11, 22), 123456u) }; +}; + +namespace details { + + /* + * This function calls either the 1 parameter or the 2 parameter pixelwriter (determined at runtime) + */ + template inline void invokePixelWriter(const PixelWriterFunction& pixel_writer, glm::ivec2 position, unsigned int data_index) + { + if constexpr (std::is_invocable_v) { + pixel_writer(position, data_index); + } else if constexpr (std::is_invocable_v) { + pixel_writer(position); + } + } + + /* + * Calculates the x value for a given y. + * The resulting x value will be on the line. + */ + inline float get_x_for_y_on_line(const glm::vec2& point_on_line, const glm::vec2& line, float y) + { + // pos_on_line = point_on_line + t * line + float t = (y - point_on_line.y) / line.y; + + return point_on_line.x + t * line.x; + } + + /* + * calculate the x value of a line + * uses fallback lines if the y value is outside of the line segment + */ + inline int get_x_with_fallback(const glm::vec2& point, + const glm::vec2& line, + float y, + bool is_enlarged, + const glm::vec2& fallback_point_bottom, + const glm::vec2& fallback_line_bottom, + const glm::vec2& fallback_point_top, + const glm::vec2& fallback_line_top) + { + // decide whether or not to fill only to the fallback line (if outside of other line segment) + if (is_enlarged && y < point.y) + return get_x_for_y_on_line(fallback_point_top, fallback_line_top, y); + else if (is_enlarged && y > point.y + line.y) + return get_x_for_y_on_line(fallback_point_bottom, fallback_line_bottom, y); + else + return get_x_for_y_on_line(point, line, y); + } + + /* + * fills a scanline in the intveral [x1,x2] (inclusive) + */ + template inline void fill_between(const PixelWriterFunction& pixel_writer, int y, int x1, int x2, unsigned int data_index) + { + int fill_direction = (x1 < x2) ? 1 : -1; + + for (int j = x1; j != x2 + fill_direction; j += fill_direction) { + invokePixelWriter(pixel_writer, glm::ivec2(j, y), data_index); + } + } + + /* + * renders the given line + * if other line is given fills everything in between the current and the other line + * if the other line is not located on the current scan line -> we fill to the given fall back lines + * the given line should always go from top to bottom (only exception horizontal lines) + */ + template + void render_line(const PixelWriterFunction& pixel_writer, + unsigned int data_index, + const glm::vec2& line_point, + const glm::vec2& line, + const int fill_direction = 0, + const glm::vec2& other_point = { 0, 0 }, + const glm::vec2& other_line = { 0, 0 }, + const glm::vec2& fallback_point_bottom = { 0, 0 }, + const glm::vec2& fallback_line_bottom = { 0, 0 }, + const glm::vec2& fallback_point_top = { 0, 0 }, + const glm::vec2& fallback_line_top = { 0, 0 }) + { + // if a line would go directly through integer points, we would falsely draw a whole pixel -> we want to prevent this + constexpr float epsilon = 0.00001; + + // find out how many y steps lie between origin and line end + const int y_steps = floor(line_point.y + line.y) - floor(line_point.y); + + // const bool is_enlarged_triangle = normal_line != glm::vec2 { 0, 0 }; + const bool fill = other_line != glm::vec2 { 0, 0 }; // no other line given? -> no fill needed + const bool is_enlarged = fallback_line_bottom != glm::vec2 { 0, 0 }; // no fallback lines given -> not enlarged + + // create variables to remember where to check for x values for the fill_between fill + // -> in the current scan line do we check the top of the pixel or the bottom of the pixel to fill a line to completion + // this assumes that line is left of other line (if it is the other way around this will be fixed further down) + bool fill_current_from_top_x = (line.x > 0) ? true : false; + bool fill_other_from_top_x = (other_line.x > 0) ? false : true; + + if (fill && fill_direction < 0) { + // we have to switch where to look for the fill line to stop + fill_current_from_top_x = !fill_current_from_top_x; + fill_other_from_top_x = !fill_other_from_top_x; + } + + // special cases for line start/end + if line is smaller than 1px + if (y_steps == 0) { + // line is smaller than one scan line + // only draw from line start to end + if (!fill) + fill_between(pixel_writer, line_point.y, line_point.x, line.x + line_point.x, data_index); + else { + // go from top or bottom of line (depending on where the other line is located) + const auto x1 = (fill_current_from_top_x) ? line_point.x : line_point.x + line.x; + + const float test_y_other = (fill_other_from_top_x) ? line_point.y : line_point.y + line.y; + const auto x2 = get_x_with_fallback(other_point, other_line, test_y_other, is_enlarged, fallback_point_bottom, fallback_line_bottom, fallback_point_top, fallback_line_top); + + fill_between(pixel_writer, line_point.y, x1, x2, data_index); + } + } else { + // draw first and last stretches of the line + if (!fill) { + // start of the line (start to bottom of pixel) + int x_start_bottom = get_x_for_y_on_line(line_point, line, ceil(line_point.y) - epsilon); + fill_between(pixel_writer, line_point.y, line_point.x, x_start_bottom, data_index); + + // end of the line (top of pixel to end of line) + int x_end_top = get_x_for_y_on_line(line_point, line, floor(line_point.y + line.y) + epsilon); + fill_between(pixel_writer, line_point.y + line.y, line_point.x + line.x, x_end_top, data_index); + } else { + { // start of the line + // const float line_top_y = line_point.y; + // in case that the current point is exactly on an integer value, we first have to increase the value by a small amount then ceil it. + const float bottom_y = ceil(line_point.y + epsilon) - epsilon; // bottom of the y step + + // either use line start or calculate the x at the bottom of the pixel + const auto x1 = (fill_current_from_top_x) ? line_point.x : get_x_for_y_on_line(line_point, line, bottom_y); + + // decide whether or not to fill only to the fallback line (if outside of other line segment) + // although this is the end of the line -> we have to test both upper and lower of the other line for large enlarged triangles and small other lines + const float test_y_other = (fill_other_from_top_x) ? line_point.y : bottom_y; + const auto x2 = get_x_with_fallback(other_point, other_line, test_y_other, is_enlarged, fallback_point_bottom, fallback_line_bottom, fallback_point_top, fallback_line_top); + + fill_between(pixel_writer, line_point.y, x1, x2, data_index); + } + + { // end of the line + const float line_bottom_y = line_point.y + line.y; + const float top_y = floor(line_bottom_y) + epsilon; // top of the y step + + // either calculate the y value at top of pixel or use the end point of line + const auto x1 = (fill_current_from_top_x) ? get_x_for_y_on_line(line_point, line, top_y) : line_point.x + line.x; + + // decide whether or not to fill only to the fallback line (if outside of other line segment) + // although this is the end of the line -> we still have to test both upper and lower of the other line for large enlarged triangles and small other lines + const float test_y_other = (fill_other_from_top_x) ? top_y : line_bottom_y; + const auto x2 = get_x_with_fallback(other_point, other_line, test_y_other, is_enlarged, fallback_point_bottom, fallback_line_bottom, fallback_point_top, fallback_line_top); + + fill_between(pixel_writer, line_bottom_y, x1, x2, data_index); + } + } + } + + // draw all the steps in between + for (int y = line_point.y + 1; y < ceil(line_point.y + line.y + epsilon) - 1; y++) { + // at a distinct y step draw the current line from floor to ceil + + if (!fill) { + // draw between line start and line end of current y level + const auto x1 = get_x_for_y_on_line(line_point, line, y + epsilon); + const auto x2 = get_x_for_y_on_line(line_point, line, y + 1 - epsilon); + + fill_between(pixel_writer, y, x1, x2, data_index); + } else { + // fill to other line + const float test_y_current = (fill_current_from_top_x) ? y + epsilon : y + 1 - epsilon; + const auto x1 = get_x_for_y_on_line(line_point, line, test_y_current); + + const float test_y_other = (fill_other_from_top_x) ? y + epsilon : y + 1 - epsilon; + const auto x2 = get_x_with_fallback(other_point, other_line, test_y_other, is_enlarged, fallback_point_bottom, fallback_line_bottom, fallback_point_top, fallback_line_top); + + fill_between(pixel_writer, y, x1, x2, data_index); + } + } + } + + /* + * renders a circle at the given position + */ + template void add_circle_end_cap(const PixelWriterFunction& pixel_writer, const glm::vec2& position, unsigned int data_index, float distance) + { + distance += sqrt(0.5); + float distance_test = ceil(distance); + + for (int i = -distance_test; i < distance_test; i++) { + for (int j = -distance_test; j < distance_test; j++) { + const auto offset = glm::vec2(i + 0.0, j + 0.0); + if (glm::length(offset) < distance) + invokePixelWriter(pixel_writer, position + offset, data_index); + } + } + } + + /* + * In order to process enlarged triangles, the triangle is separated into 3 parts: top, middle and bottom + * we first generate the normals for each edge and offset the triangle origins by the normal multiplied by the supplied distance to get 6 "enlarged" points that define the bigger triangle + * + * top / bottom part: + * the top and bottom part of the triangle are rendered by going through each y step of the line with the shorter y value and filling the inner triangle until it reaches the other side of the + * triangle for the very top and very bottom of the triangle special considerations have to be done, otherwise we would render too much if the line on the other side of the triangle is not hit + * during a fill operation (= we want to fill above or below the line segment), we use the normal of the current line as the bound for the fill operation + * + * for the middle part: + * if we enlarge the triangle by moving the edge along an edge normal we generate a gap between the top-middle and middle-bottom edge (since those lines are only translated) + * we have to somehow rasterize this gap between the bottom vertice of the translated top-middle edge and the top vertice of the middle-bottom edge. + * in order to do this we draw a new edge between those vertices and rasterize everything between this new edge and the top-bottom edge. + * + * lastly we have to add endcaps on each original vertice position with the given distance + */ + template + void render_triangle(const PixelWriterFunction& pixel_writer, const std::array triangle, unsigned int triangle_index, float distance) + { + assert(triangle[0].y <= triangle[1].y); + assert(triangle[1].y <= triangle[2].y); + + auto edge_top_bottom = triangle[2] - triangle[0]; + auto edge_top_middle = triangle[1] - triangle[0]; + auto edge_middle_bottom = triangle[2] - triangle[1]; + + // by comparing the middle vertex with the middle position of the longest line, we can determine the direction of our fill algorithm + float x_middle_of_top_bottom_line = get_x_for_y_on_line(triangle[0], edge_top_bottom, triangle[1].y); + int fill_direction = (triangle[1].x < x_middle_of_top_bottom_line) ? 1 : -1; + + if (distance == 0.0) { + // top middle + render_line(pixel_writer, triangle_index, triangle[0], edge_top_middle, fill_direction, triangle[0], edge_top_bottom); + // middle bottom + render_line(pixel_writer, triangle_index, triangle[1], edge_middle_bottom, fill_direction, triangle[0], edge_top_bottom); + + return; // finished + } + + // we need to render an enlarged triangle + auto normal_top_bottom = glm::normalize(glm::vec2(-edge_top_bottom.y, edge_top_bottom.x)); + auto normal_top_middle = glm::normalize(glm::vec2(-edge_top_middle.y, edge_top_middle.x)); + auto normal_middle_bottom = glm::normalize(glm::vec2(-edge_middle_bottom.y, edge_middle_bottom.x)); + + { // swap normal direction if they are incorrect (pointing to center) + glm::vec2 centroid = (triangle[0] + triangle[1] + triangle[2]) / 3.0f; + if (glm::dot(normal_top_bottom, centroid - triangle[0]) > 0) { + normal_top_bottom *= -1; + } + if (glm::dot(normal_top_middle, centroid - triangle[0]) > 0) { + normal_top_middle *= -1; + } + if (glm::dot(normal_middle_bottom, centroid - triangle[1]) > 0) { + normal_middle_bottom *= -1; + } + } + + auto enlarged_top_bottom_origin = triangle[0] + normal_top_bottom * distance; + // auto enlarged_top_bottom_end = triangle[2] + normal_top_bottom * distance; // not needed + + auto enlarged_top_middle_origin = triangle[0] + normal_top_middle * distance; + auto enlarged_top_middle_end = triangle[1] + normal_top_middle * distance; + + auto enlarged_middle_bottom_origin = triangle[1] + normal_middle_bottom * distance; + auto enlarged_middle_bottom_end = triangle[2] + normal_middle_bottom * distance; + + // top middle + render_line(pixel_writer, + triangle_index, + enlarged_top_middle_origin, + edge_top_middle, + fill_direction, + enlarged_top_bottom_origin, + edge_top_bottom, + enlarged_middle_bottom_end, + normal_middle_bottom, + enlarged_top_middle_origin, + normal_top_middle); + + // // middle_top_part to middle_bottom_part + auto half_middle_line = (enlarged_middle_bottom_origin - enlarged_top_middle_end) / glm::vec2(2.0); + + render_line(pixel_writer, + triangle_index, + enlarged_top_middle_end, + half_middle_line, + fill_direction, + enlarged_top_bottom_origin, + edge_top_bottom, + enlarged_middle_bottom_end, + normal_middle_bottom, + enlarged_top_middle_origin, + normal_top_middle); + render_line(pixel_writer, + triangle_index, + enlarged_top_middle_end + half_middle_line, + half_middle_line, + fill_direction, + enlarged_top_bottom_origin, + edge_top_bottom, + enlarged_middle_bottom_end, + normal_middle_bottom, + enlarged_top_middle_origin, + normal_top_middle); + + // middle bottom + render_line(pixel_writer, + triangle_index, + enlarged_middle_bottom_origin, + edge_middle_bottom, + fill_direction, + enlarged_top_bottom_origin, + edge_top_bottom, + enlarged_middle_bottom_end, + normal_middle_bottom, + enlarged_top_middle_origin, + normal_top_middle); + + // endcaps + add_circle_end_cap(pixel_writer, triangle[0], triangle_index, distance); + add_circle_end_cap(pixel_writer, triangle[1], triangle_index, distance); + add_circle_end_cap(pixel_writer, triangle[2], triangle_index, distance); + + // { // DEBUG visualize enlarged points + // auto enlarged_top_bottom_end = triangle[2] + normal_top_bottom * distance; + // invokePixelWriter(pixel_writer, triangle[0], 50); + // invokePixelWriter(pixel_writer, enlarged_top_bottom_origin, 50); + // invokePixelWriter(pixel_writer, enlarged_top_middle_origin, 50); + + // invokePixelWriter(pixel_writer, triangle[1], 50); + // invokePixelWriter(pixel_writer, enlarged_middle_bottom_origin, 50); + // invokePixelWriter(pixel_writer, enlarged_top_middle_end, 50); + + // invokePixelWriter(pixel_writer, triangle[2], 50); + // invokePixelWriter(pixel_writer, enlarged_middle_bottom_end, 50); + // invokePixelWriter(pixel_writer, enlarged_top_bottom_end, 50); + // } + } + + template + void render_line_preprocess(const PixelWriterFunction& pixel_writer, const std::array line_points, unsigned int line_index, float distance) + { + // edge from top to bottom + glm::vec2 origin; + glm::vec2 edge; + if (line_points[0].y > line_points[1].y) { + edge = line_points[0] - line_points[1]; + origin = line_points[1]; + } else { + edge = line_points[1] - line_points[0]; + origin = line_points[0]; + } + + // only draw lines with 1 pixel width + if (distance == 0.0) { + render_line(pixel_writer, line_index, origin, edge); + + // debug output for points + // invokePixelWriter(pixel_writer, line_points[0], 50); + // invokePixelWriter(pixel_writer, line_points[1], 50); + + return; // finished + } + + // we want to draw lines with a thickness + + // end caps + add_circle_end_cap(pixel_writer, line_points[0], line_index, distance); + add_circle_end_cap(pixel_writer, line_points[1], line_index, distance); + + // distance += sqrt(0.5); + + // create normal + // make sure that the normal points downwards + glm::vec2 normal; + if (edge.x < 0) + normal = glm::normalize(glm::vec2(edge.y, -edge.x)) * distance; + else + normal = glm::normalize(glm::vec2(-edge.y, edge.x)) * distance; + + auto enlarged_top_origin = origin + normal * -1.0f; + auto enlarged_middle_origin = origin + normal; + auto enlarged_middle_end = origin + edge + normal * -1.0f; + // auto enlarged_bottom_origin = origin + edge + normal; + + // double the normal so that we construct a rectangle with the edge + normal *= 2.0f; + + // determine if the doubled normal or the edge has a larger y difference + // and switch if normal is larger in y direction + if (edge.y < normal.y) { + auto tmp = normal; + normal = edge; + edge = tmp; + + // we also need to change two enlarged points + tmp = enlarged_middle_origin; + enlarged_middle_origin = enlarged_middle_end; + enlarged_middle_end = tmp; + } + + // special case: one side is horizontal + // -> only one pass from top to bottom necessary + if (normal.y == 0) { + render_line(pixel_writer, line_index, enlarged_top_origin, edge, (enlarged_top_origin.x > enlarged_middle_origin.x) ? -1 : 1, enlarged_middle_origin, edge); + + return; + } + + // we have to fill once the edge side and once the normal side. + // one side should not need fallbacks to render everything + + int fill_direction = (edge.x > 0) ? -1 : 1; + + // go from top of the line to bottom of the line + // first fill everything between edge and 2xnormal, after this fill everything betweent edge and other edge + render_line(pixel_writer, line_index, enlarged_top_origin, edge, fill_direction, enlarged_middle_origin, edge, enlarged_top_origin, normal, enlarged_top_origin, normal); + + // render the last bit of the line (from line end top to line end bottom) -> no need for a fallback line since both lines meet at the bottom + render_line(pixel_writer, line_index, enlarged_middle_end, normal, fill_direction, enlarged_middle_origin, edge * 2.0f); + + // { // DEBUG visualize enlarged points + // // const auto enlarged_bottom_origin = origin + edge + normal * distance; + // invokePixelWriter(pixel_writer, enlarged_top_origin, 50); + // invokePixelWriter(pixel_writer, enlarged_middle_origin, 50); + + // invokePixelWriter(pixel_writer, enlarged_middle_end, 50); + // invokePixelWriter(pixel_writer, enlarged_bottom_origin, 50); + + // invokePixelWriter(pixel_writer, line_points[0], 50); + // invokePixelWriter(pixel_writer, line_points[1], 50); + // } + } + +} // namespace details + +/* + * generates edges of a polygon + * this assumes that polygons neighbouring in the vector should form an edge and the first and last edge are also connected + * start offset if you want to only create an edge ring list of a part of the bigger polygon you can only provide the part and an start_offset, + * that indicates by how much the vertice index has to be offset + */ +std::vector generate_neighbour_edges(std::vector polygon_points, const size_t start_offset = 0); + +/* + * triangulizes polygons and orders the vertices by y position per triangle + * output: top, middle, bottom, top, middle,... + */ +std::vector triangulize(std::vector polygon_points, std::vector edges, bool remove_duplicate_vertices = false); + +/* + * Rasterize a triangle + * in this method every triangle is traversed only once, and it only accesses pixels it needs for itself. + * this function determines the position where pixels should be set and calls the given pixel_writer lambda to actually draw the pixels + * + * NOTE: the triangle points have to be ordered correctly by y position + * -> input triangles: ordered by y position within each triangle (top_ypos_triangle_1, middle_ypos_triangle_1, bottom_ypos_triangle_1, top_ypos_triangle_2, middle_ypos_triangle_2, ...) + * + * example usage: + * const std::vector triangle_points = { glm::vec2(30.5, 10.5), glm::vec2(10.5, 30.5), glm::vec2(50.5, 50.5) }; + * nucleus::Raster output({ 64, 64 }, 0u); + * const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + * nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangle_points); + */ +template void rasterize_triangle(const PixelWriterFunction& pixel_writer, const std::vector& triangles, float distance = 0.0) +{ + for (size_t i = 0; i < triangles.size() / 3; ++i) { + details::render_triangle(pixel_writer, { triangles[i * 3 + 0], triangles[i * 3 + 1], triangles[i * 3 + 2] }, i, distance); + } +} + +/* + * Rasterize a line + * + * this function determines the position where pixels should be set and calls the given pixel_writer lambda to actually draw the pixels + * + * input line_points: a list of points that should form a line (line_start, line_point1, line_point2, ..., line_end) + * -> generates line segments between line_start-line_point1, line_point1 to line_point2, ... + * + * example usage: + * const std::vector line = { glm::vec2(30.5, 10.5), glm::vec2(50.5, 30.5), glm::vec2(30.5, 50.5), glm::vec2(10.5, 30.5), glm::vec2(30.5, 10.5) }; + * nucleus::Raster output({ 64, 64 }, 0u); + * const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + * nucleus::utils::rasterizer::rasterize_line(pixel_writer, line); + */ +template void rasterize_line(const PixelWriterFunction& pixel_writer, const std::vector& line_points, float distance = 0.0) +{ + for (size_t i = 0; i < line_points.size() - 1; ++i) { + details::render_line_preprocess(pixel_writer, { line_points[i + 0], line_points[i + 1] }, i, distance); + } +} + +/* + * Rasterize a polygon + * convenience function that really just calls rasterize_triangle after triangulizing the polygon + * this function determines the position where pixels should be set and calls the given pixel_writer lambda to actually draw the pixels + * + * example usage: + * const std::vector polygon_points = { glm::vec2(30.5, 10.5), glm::vec2(10.5, 30.5), glm::vec2(50.5, 50.5) }; + * nucleus::Raster output({ 64, 64 }, 0u); + * const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + * nucleus::utils::rasterizer::rasterize_polygon(pixel_writer, polygon_points); + */ +template void rasterize_polygon(const PixelWriterFunction& pixel_writer, const std::vector& polygon_points, float distance = 0.0) +{ + const auto edges = generate_neighbour_edges(polygon_points); + const auto triangles = triangulize(polygon_points, edges); + + rasterize_triangle(pixel_writer, triangles, distance); +} + +} // namespace nucleus::utils::rasterizer diff --git a/plain_renderer/Window.cpp b/plain_renderer/Window.cpp index 7fcc7584..44c4860b 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 b037b09c..b94f7289 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/texture.cpp b/unittests/gl_engine/texture.cpp index b923c7cf..8924244b 100644 --- a/unittests/gl_engine/texture.cpp +++ b/unittests/gl_engine/texture.cpp @@ -174,6 +174,85 @@ void test_float_texture_with(const TexelType& texel_value, gl_engine::Texture::F CHECK(qAlpha(render_result.pixel(0, 0)) == 126); } +template > +void test_unsigned_texture_array_with(const std::array& texel_value, gl_engine::Texture::Format format) +{ + Framebuffer b(Framebuffer::DepthFormat::None, { Framebuffer::ColourFormat::RGBA8, Framebuffer::ColourFormat::RGBA8 }, { 1, 1 }); + + b.bind(); + + gl_engine::Texture opengl_texture(gl_engine::Texture::Target::_2dArray, format); + opengl_texture.setParams(gl_engine::Texture::Filter::Nearest, gl_engine::Texture::Filter::Nearest); + opengl_texture.allocate_array(1, 1, 2); + + const auto tex0 = nucleus::Raster({ 1, 1 }, texel_value[0]); + const auto tex1 = nucleus::Raster({ 1, 1 }, texel_value[1]); + opengl_texture.upload(tex0, 0); + opengl_texture.upload(tex1, 1); + + const auto precision = []() -> QString { + if (sizeof(Type) == 1) + return "lowp"; + if (sizeof(Type) == 2) + return "mediump"; + if (sizeof(Type) == 4) + return "highp"; + assert(false); + return "Type has unexpected size"; + }; + + ShaderProgram shader = create_debug_shader(QString(R"( + uniform %1 usampler2DArray texture_sampler; + layout (location = 0) out lowp vec4 out_color0; + layout (location = 1) out lowp vec4 out_color1; + void main() { + %1 uvec4 v0 = texelFetch(texture_sampler, ivec3(0, 0, 0), 0); + out_color0 = vec4((v0.r == %2) ? 123.0 / 255.0 : 9.0 / 255.0, + (%10 < 2 || v0.g == %3) ? 124.0 / 255.0 : 9.0 / 255.0, + (%10 < 3 || v0.b == %4) ? 125.0 / 255.0 : 9.0 / 255.0, + (%10 < 4 || v0.a == %5) ? 126.0 / 255.0 : 9.0 / 255.0); + + %1 uvec4 v1 = texelFetch(texture_sampler, ivec3(0, 0, 1), 0); + out_color1 = vec4((v1.r == %6) ? 127.0 / 255.0 : 9.0 / 255.0, + (%10 < 2 || v1.g == %7) ? 128.0 / 255.0 : 9.0 / 255.0, + (%10 < 3 || v1.b == %8) ? 129.0 / 255.0 : 9.0 / 255.0, + (%10 < 4 || v1.a == %9) ? 130.0 / 255.0 : 9.0 / 255.0); + } + )") + .arg(precision()) + .arg(texel_component(texel_value[0], 0)) + .arg(texel_component(texel_value[0], 1)) + .arg(texel_component(texel_value[0], 2)) + .arg(texel_component(texel_value[0], 3)) + .arg(texel_component(texel_value[1], 0)) + .arg(texel_component(texel_value[1], 1)) + .arg(texel_component(texel_value[1], 2)) + .arg(texel_component(texel_value[1], 3)) + .arg(length)); + shader.bind(); + opengl_texture.bind(0); + shader.set_uniform("texture_sampler", 0); + gl_engine::helpers::create_screen_quad_geometry().draw(); + + // render_result.save("render_result.png"); + { + const QImage render_result = b.read_colour_attachment(0); + CHECK(qRed(render_result.pixel(0, 0)) == 123); + CHECK(qGreen(render_result.pixel(0, 0)) == 124); + CHECK(qBlue(render_result.pixel(0, 0)) == 125); + CHECK(qAlpha(render_result.pixel(0, 0)) == 126); + } + { + const QImage render_result = b.read_colour_attachment(1); + CHECK(qRed(render_result.pixel(0, 0)) == 127); + CHECK(qGreen(render_result.pixel(0, 0)) == 128); + CHECK(qBlue(render_result.pixel(0, 0)) == 129); + CHECK(qAlpha(render_result.pixel(0, 0)) == 130); + } + + Framebuffer::unbind(); +} + QImage create_test_rgba_qimage(unsigned width, unsigned height) { QImage test_texture(width, height, QImage::Format_RGBA8888); @@ -360,8 +439,15 @@ TEST_CASE("gl texture") SECTION("rgba8ui") { test_unsigned_texture_with<4, unsigned char>({ 1, 2, 255, 140 }, gl_engine::Texture::Format::RGBA8UI); } SECTION("rg32ui") { test_unsigned_texture_with<2, uint32_t>({ 3000111222, 4000111222 }, gl_engine::Texture::Format::RG32UI); } SECTION("red8ui") { test_unsigned_texture_with<1, uint8_t, uint8_t>(uint8_t(178), gl_engine::Texture::Format::R8UI); } + SECTION("rgb32ui") { test_unsigned_texture_with<3, uint32_t>({ 3000111222, 4000111222, 2500111222 }, gl_engine::Texture::Format::RGB32UI); } SECTION("red16ui") { test_unsigned_texture_with<1, uint16_t, uint16_t>(uint16_t(60123), gl_engine::Texture::Format::R16UI); } SECTION("red32ui") { test_unsigned_texture_with<1, uint32_t, uint32_t>(uint32_t(4000111222), gl_engine::Texture::Format::R32UI); } + SECTION("r32ui_array") { test_unsigned_texture_array_with<1, uint32_t, uint32_t>({ uint32_t { 3000111222 }, uint32_t { 3000114422 } }, gl_engine::Texture::Format::R32UI); } + SECTION("rg32ui_array") { test_unsigned_texture_array_with<2, uint32_t>({ glm::uvec2 { 3000111222, 4000111222 }, glm::uvec2 { 3000114422, 4000114422 } }, gl_engine::Texture::Format::RG32UI); } + SECTION("rgb32ui_array") + { + test_unsigned_texture_array_with<3, uint32_t>({ glm::uvec3 { 3000111222, 4000111222, 2500111222 }, glm::uvec3 { 3000114422, 4000114422, 2500114422 } }, gl_engine::Texture::Format::RGB32UI); + } SECTION("rgba32f") { test_float_texture_with<4, float, glm::vec4>(glm::vec4(2.0, 0.0, 234012.0, -239093.0), gl_engine::Texture::Format::RGBA32F); } diff --git a/unittests/gl_engine/uniformbuffer.cpp b/unittests/gl_engine/uniformbuffer.cpp index e929adea..758e4d83 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 717145af..7c37320e 100644 --- a/unittests/nucleus/CMakeLists.txt +++ b/unittests/nucleus/CMakeLists.txt @@ -25,6 +25,7 @@ alp_add_unittest(unittests_nucleus DrawListGenerator.cpp test_helpers.h test_helpers.cpp raster.cpp + rasterizer.cpp terrain_mesh_index_generator.cpp srs.cpp track.cpp @@ -43,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() @@ -66,14 +67,21 @@ 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 data/quad/7_68_83.jpg 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 0de2d3b4..71143402 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") { @@ -32,7 +42,7 @@ TEST_CASE("nucleus/EAWS Vector Tiles") CHECK(test_file.size() > 0); // Check if testfile can be opened and read - test_file.open(QIODevice::ReadOnly | QIODevice::Unbuffered); + REQUIRE(test_file.open(QIODevice::ReadOnly | QIODevice::Unbuffered)); QByteArray test_data = test_file.readAll(); test_file.close(); CHECK(test_data.size() > 0); @@ -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,22 +128,43 @@ 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()); QFile test_file2(filepath); CHECK(test_file2.exists()); - test_file2.open(QIODevice::ReadOnly | QIODevice::Unbuffered); + REQUIRE(test_file2.open(QIODevice::ReadOnly | QIODevice::Unbuffered)); QByteArray test_data2 = test_file2.readAll(); test_file2.close(); CHECK(test_data2.size() > 0); @@ -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 00000000..44fc6c03 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 00000000..c25a18f2 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 00000000..66c9304e 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 00000000..3aca2e53 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 00000000..37fc4951 Binary files /dev/null and b/unittests/nucleus/data/eaws_7-67-45.mvt differ diff --git a/unittests/nucleus/data/rasterizer_output_random_triangle.png b/unittests/nucleus/data/rasterizer_output_random_triangle.png new file mode 100644 index 00000000..b39875a1 Binary files /dev/null and b/unittests/nucleus/data/rasterizer_output_random_triangle.png differ diff --git a/unittests/nucleus/data/rasterizer_simple_triangle.png b/unittests/nucleus/data/rasterizer_simple_triangle.png new file mode 100644 index 00000000..b9f3ce34 Binary files /dev/null and b/unittests/nucleus/data/rasterizer_simple_triangle.png differ diff --git a/unittests/nucleus/rasterizer.cpp b/unittests/nucleus/rasterizer.cpp new file mode 100644 index 00000000..d2d1b719 --- /dev/null +++ b/unittests/nucleus/rasterizer.cpp @@ -0,0 +1,1052 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Lucas Dworschak + * 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 . + *****************************************************************************/ + +// #define WRITE_RASTERIZER_DEBUG_IMAGE + +#include +#include +#include + +#include + +#include "nucleus/Raster.h" +#include "nucleus/tile/conversion.h" +#include "nucleus/utils/rasterizer.h" + +#include + +namespace { +/* + * calculates how far away the given position is from a triangle + * uses distance to shift the current triangle distance + * if it is within the triangle, the pixel writer function is called + * * uses triangle distance calculation from: https://iquilezles.org/articles/distfunctions2d/ + */ +template +void triangle_sdf(const PixelWriterFunction& pixel_writer, + const glm::vec2 current_position, + const std::array points, + const std::array edges, + float s, + unsigned int data_index, + float distance) +{ + glm::vec2 v0 = current_position - points[0], v1 = current_position - points[1], v2 = current_position - points[2]; + + // pq# = distance from edge # + glm::vec2 pq0 = v0 - edges[0] * glm::clamp(glm::dot(v0, edges[0]) / glm::dot(edges[0], edges[0]), 0.0f, 1.0f); + glm::vec2 pq1 = v1 - edges[1] * glm::clamp(glm::dot(v1, edges[1]) / glm::dot(edges[1], edges[1]), 0.0f, 1.0f); + glm::vec2 pq2 = v2 - edges[2] * glm::clamp(glm::dot(v2, edges[2]) / glm::dot(edges[2], edges[2]), 0.0f, 1.0f); + // d.x == squared distance to triangle edge/vertice + // d.y == are we inside or outside triangle + glm::vec2 d = min(min(glm::vec2(dot(pq0, pq0), s * (v0.x * edges[0].y - v0.y * edges[0].x)), glm::vec2(dot(pq1, pq1), s * (v1.x * edges[1].y - v1.y * edges[1].x))), + glm::vec2(dot(pq2, pq2), s * (v2.x * edges[2].y - v2.y * edges[2].x))); + float dist_from_tri = -sqrt(d.x) * glm::sign(d.y); + + if (dist_from_tri < distance) { + nucleus::utils::rasterizer::details::invokePixelWriter(pixel_writer, current_position, data_index); + } +} + +/* + * calculates how far away the given position is from a line segment + * uses distance to shift the current triangle distance + * if it is within the triangle, the pixel writer function is called + * * uses line segment distance calculation from: https://iquilezles.org/articles/distfunctions2d/ + */ +template +void line_sdf(const PixelWriterFunction& pixel_writer, const glm::vec2 current_position, const glm::vec2 origin, glm::vec2 edge, unsigned int data_index, float distance) +{ + glm::vec2 v0 = current_position - origin; + + float dist_from_line = length(v0 - edge * glm::clamp(glm::dot(v0, edge) / glm::dot(edge, edge), 0.0f, 1.0f)); + + if (dist_from_line < distance) { + nucleus::utils::rasterizer::details::invokePixelWriter(pixel_writer, current_position, data_index); + } +} + +/* + * ideal if you want to rasterize only a few triangles, where every triangle covers a large part of the raster size + * in this method we traverse through every triangle and generate the bounding box and traverse the bounding box + */ +template void rasterize_triangle_sdf(const PixelWriterFunction& pixel_writer, const std::vector& triangles, float distance) +{ + // we sample from the center + // and have a radius of a half pixel diagonal + // NOTICE: this still leads to false positives if the edge of a triangle is slighly in another pixel without every comming in this pixel!! + distance += sqrt(0.5); + + for (size_t i = 0; i < triangles.size() / 3; ++i) { + const std::array points = { triangles[i * 3 + 0], triangles[i * 3 + 1], triangles[i * 3 + 2] }; + const std::array edges = { points[1] - points[0], points[2] - points[1], points[0] - points[2] }; + + auto min_bound = min(min(points[0], points[1]), points[2]) - glm::vec2(distance); + auto max_bound = max(max(points[0], points[1]), points[2]) + glm::vec2(distance); + + float s = glm::sign(edges[0].x * edges[2].y - edges[0].y * edges[2].x); + + for (size_t x = min_bound.x; x < max_bound.x; x++) { + for (size_t y = min_bound.y; y < max_bound.y; y++) { + auto current_position = glm::vec2 { x + 0.5, y + 0.5 }; + + triangle_sdf(pixel_writer, current_position, points, edges, s, i, distance); + } + } + } +} + +template void rasterize_line_sdf(const PixelWriterFunction& pixel_writer, const std::vector& line_points, float distance) +{ + // we sample from the center + // and have a radius of a half pixel diagonal + // NOTICE: this still leads to false positives if the edge of a triangle is slighly in another pixel without every comming in this pixel!! + distance += sqrt(0.5); + + for (size_t i = 0; i < line_points.size() - 1; ++i) { + const std::array points = { line_points[i + 0], line_points[i + 1] }; + auto edge = points[1] - points[0]; + + auto min_bound = min(points[0], points[1]) - glm::vec2(distance); + auto max_bound = max(points[0], points[1]) + glm::vec2(distance); + + for (size_t x = min_bound.x; x < max_bound.x; x++) { + for (size_t y = min_bound.y; y < max_bound.y; y++) { + auto current_position = glm::vec2 { x + 0.5, y + 0.5 }; + + line_sdf(pixel_writer, current_position, points[0], edge, i, distance); + } + } + } +} +std::pair>> triangulate(std::vector points, std::vector edges, bool remove_duplicate_vertices) +{ + + CDT::Triangulation cdt; + + if (remove_duplicate_vertices) { + CDT::RemoveDuplicatesAndRemapEdges( + points, + [](const glm::vec2& p) { return p.x; }, + [](const glm::vec2& p) { return p.y; }, + edges.begin(), + edges.end(), + [](const glm::ivec2& p) { return p.x; }, + [](const glm::ivec2& p) { return p.y; }, + [](CDT::VertInd start, CDT::VertInd end) { return glm::ivec2 { start, end }; }); + } + + cdt.insertVertices(points.begin(), points.end(), [](const glm::vec2& p) { return p.x; }, [](const glm::vec2& p) { return p.y; }); + cdt.insertEdges(edges.begin(), edges.end(), [](const glm::ivec2& p) { return p.x; }, [](const glm::ivec2& p) { return p.y; }); + cdt.eraseOuterTrianglesAndHoles(); + + return std::make_pair(cdt.triangles, cdt.vertices); +} + +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)); + const auto image_bytes = image_file.readAll(); + REQUIRE(!QImage::fromData(image_bytes).isNull()); + return QImage::fromData(image_bytes); +} +} // namespace + +TEST_CASE("nucleus/rasterizer") +{ + + // NOTE: most tests compare the dda renderer with the sdf renderer + // unfortunatly the sdf renderer can produce false positives (writing to cell that should be empty if near edge) + // those errors have all been subverted by choosing parameters that work with both renderers + // but future changes might cause problems, when there shouldn't be problems + // so if a test fails check the debug images to see what exactly went wrong. + + SECTION("Triangulation") + { + // 5 point polygon + // basically a square where one side contains an inward facing triangle + // a triangulation algorithm should be able to discern that 3 triangles are needed to construct this shape + const std::vector points = { glm::vec2(0, 0), glm::vec2(1, 1), glm::vec2(0, 2), glm::vec2(2, 2), glm::vec2(2, 0) }; + const std::vector edges = { glm::ivec2(0, 1), glm::ivec2(1, 2), glm::ivec2(2, 3), glm::ivec2(3, 4), glm::ivec2(4, 0) }; + + auto pair = triangulate(points, edges, false); + + auto tri = pair.first; + auto vert = pair.second; + + // check if only 3 triangles have been found + CHECK(tri.size() == 3); + + // 1st triangle + CHECK(vert[tri[0].vertices[0]].x == 0.0); + CHECK(vert[tri[0].vertices[0]].y == 2.0); + CHECK(vert[tri[0].vertices[1]].x == 1.0); + CHECK(vert[tri[0].vertices[1]].y == 1.0); + CHECK(vert[tri[0].vertices[2]].x == 2.0); + CHECK(vert[tri[0].vertices[2]].y == 2.0); + + // 2nd triangle + CHECK(vert[tri[1].vertices[0]].x == 1.0); + CHECK(vert[tri[1].vertices[0]].y == 1.0); + CHECK(vert[tri[1].vertices[1]].x == 2.0); + CHECK(vert[tri[1].vertices[1]].y == 0.0); + CHECK(vert[tri[1].vertices[2]].x == 2.0); + CHECK(vert[tri[1].vertices[2]].y == 2.0); + + // 3rd triangle + CHECK(vert[tri[2].vertices[0]].x == 1.0); + CHECK(vert[tri[2].vertices[0]].y == 1.0); + CHECK(vert[tri[2].vertices[1]].x == 0.0); + CHECK(vert[tri[2].vertices[1]].y == 0.0); + CHECK(vert[tri[2].vertices[2]].x == 2.0); + CHECK(vert[tri[2].vertices[2]].y == 0.0); + + // DEBUG print out all the points of the triangles (to check what might have went wrong) + // for (std::size_t i = 0; i < tri.size(); i++) { + // printf("Triangle points: [[%f, %f], [%f, %f], [%f, %f]]\n", + // vert[tri[i].vertices[0]].x, // x0 + // vert[tri[i].vertices[0]].y, // y0 + // vert[tri[i].vertices[1]].x, // x1 + // vert[tri[i].vertices[1]].y, // y1 + // vert[tri[i].vertices[2]].x, // x2 + // vert[tri[i].vertices[2]].y // y2 + // ); + // } + } + + SECTION("Triangulation - duplicate vertices") + { + // 6 point polygon + // basically a square where two sides contains an inward facing triangle that meets at the same middle vertice (so esentially two triangles that mirror at top of bottom triangle) + // a triangulation algorithm should be able to discern that 3 triangles are needed to construct this shape + std::vector points = { glm::vec2(0, 0), glm::vec2(1, 1), glm::vec2(0, 2), glm::vec2(2, 2), glm::vec2(1, 1), glm::vec2(2, 0) }; + std::vector edges = { glm::ivec2(0, 1), glm::ivec2(1, 2), glm::ivec2(2, 3), glm::ivec2(3, 4), glm::ivec2(4, 5), glm::ivec2(5, 0) }; + + auto pair = triangulate(points, edges, true); + + auto tri = pair.first; + auto vert = pair.second; + + // check if only 3 triangles have been found + CHECK(tri.size() == 2); + + // 1st triangle + CHECK(vert[tri[0].vertices[0]].x == 0.0); + CHECK(vert[tri[0].vertices[0]].y == 2.0); + CHECK(vert[tri[0].vertices[1]].x == 1.0); + CHECK(vert[tri[0].vertices[1]].y == 1.0); + CHECK(vert[tri[0].vertices[2]].x == 2.0); + CHECK(vert[tri[0].vertices[2]].y == 2.0); + + // 2nd triangle + CHECK(vert[tri[1].vertices[0]].x == 1.0); + CHECK(vert[tri[1].vertices[0]].y == 1.0); + CHECK(vert[tri[1].vertices[1]].x == 0.0); + CHECK(vert[tri[1].vertices[1]].y == 0.0); + CHECK(vert[tri[1].vertices[2]].x == 2.0); + CHECK(vert[tri[1].vertices[2]].y == 0.0); + + // // DEBUG print out all the points of the triangles(to check what might have went wrong) for (std::size_t i = 0; i < tri.size(); i++) + // { + // for (std::size_t i = 0; i < tri.size(); i++) { + // printf("Triangle points: [[%f, %f], [%f, %f], [%f, %f]]\n", + // vert[tri[i].vertices[0]].x, // x0 + // vert[tri[i].vertices[0]].y, // y0 + // vert[tri[i].vertices[1]].x, // x1 + // vert[tri[i].vertices[1]].y, // y1 + // vert[tri[i].vertices[2]].x, // x2 + // vert[tri[i].vertices[2]].y // y2 + // ); + // } + // } + } + + SECTION("Triangle y ordering") + { + // make sure that the triangle_order function correctly orders the triangle points from lowest y to highest y value + const std::vector triangle_points_012 = { { glm::vec2(30, 10), glm::vec2(10, 30), glm::vec2(50, 50) } }; + const std::vector triangle_points_021 = { glm::vec2(30, 10), glm::vec2(50, 50), glm::vec2(10, 30) }; + const std::vector triangle_points_102 = { glm::vec2(10, 30), glm::vec2(30, 10), glm::vec2(50, 50) }; + const std::vector triangle_points_201 = { glm::vec2(10, 30), glm::vec2(50, 50), glm::vec2(30, 10) }; + const std::vector triangle_points_120 = { glm::vec2(50, 50), glm::vec2(30, 10), glm::vec2(10, 30) }; + const std::vector triangle_points_210 = { glm::vec2(50, 50), glm::vec2(10, 30), glm::vec2(30, 10) }; + + const std::vector correct = { { glm::vec2(30, 10), glm::vec2(10, 30), glm::vec2(50, 50) } }; + + const auto edges_012 = nucleus::utils::rasterizer::generate_neighbour_edges(triangle_points_012); + const auto edges_021 = nucleus::utils::rasterizer::generate_neighbour_edges(triangle_points_021); + const auto edges_102 = nucleus::utils::rasterizer::generate_neighbour_edges(triangle_points_102); + const auto edges_201 = nucleus::utils::rasterizer::generate_neighbour_edges(triangle_points_201); + const auto edges_120 = nucleus::utils::rasterizer::generate_neighbour_edges(triangle_points_120); + const auto edges_210 = nucleus::utils::rasterizer::generate_neighbour_edges(triangle_points_210); + + CHECK(correct == nucleus::utils::rasterizer::triangulize(triangle_points_012, edges_012)); + CHECK(correct == nucleus::utils::rasterizer::triangulize(triangle_points_021, edges_021)); + CHECK(correct == nucleus::utils::rasterizer::triangulize(triangle_points_102, edges_102)); + CHECK(correct == nucleus::utils::rasterizer::triangulize(triangle_points_201, edges_201)); + CHECK(correct == nucleus::utils::rasterizer::triangulize(triangle_points_120, edges_120)); + CHECK(correct == nucleus::utils::rasterizer::triangulize(triangle_points_210, edges_210)); + } + + SECTION("rasterize simple triangle - integer values") + { + // simple triangle with integer vertice values + // this tests if the visualization works with integer values + // test was added, since there was a case where the second to last row was never rendered + const std::vector polygon_points = { glm::vec2(5, 1), glm::vec2(5, 5), glm::vec2(1, 5) }; + + nucleus::Raster output({ 7, 7 }, 0u); + const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + nucleus::utils::rasterizer::rasterize_polygon(pixel_writer, polygon_points); + + auto image = nucleus::tile::conversion::u8raster_to_qimage(output); + CHECK(image == example_rasterizer_image("rasterizer_simple_triangle.png")); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + image.save(QString("rasterizer_output_simple_triangle.png")); +#endif + } + + SECTION("rasterize Polygon") + { + const std::vector polygon_points = { + glm::vec2(10.5, 10.5), + glm::vec2(30.5, 10.5), + glm::vec2(50.5, 50.5), + glm::vec2(10.5, 30.5), + }; + + nucleus::Raster output({ 64, 64 }, 0u); + const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + nucleus::utils::rasterizer::rasterize_polygon(pixel_writer, polygon_points); + + const auto edges = nucleus::utils::rasterizer::generate_neighbour_edges(polygon_points); + const auto triangles = nucleus::utils::rasterizer::triangulize(polygon_points, edges); + nucleus::Raster output2({ 64, 64 }, 0u); + auto pixel_writer2 = [&output2](glm::ivec2 pos) { output2.pixel(pos) = 255; }; + rasterize_triangle_sdf(pixel_writer2, triangles, 0); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_polygon.png")); +#endif + } + + SECTION("rasterize triangle") + { + // two triangles + const std::vector triangles = { glm::vec2(30.5, 10.5), glm::vec2(10.5, 30.5), glm::vec2(50.5, 50.5), glm::vec2(5.5, 5.5), glm::vec2(15.5, 10.5), glm::vec2(5.5, 15.5) }; + + nucleus::Raster output({ 64, 64 }, 0u); + const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles); + + nucleus::Raster output2({ 64, 64 }, 0u); + auto pixel_writer2 = [&output2](glm::ivec2 pos) { output2.pixel(pos) = 255; }; + rasterize_triangle_sdf(pixel_writer2, triangles, 0); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output.png")); +#endif + } + + SECTION("rasterize triangle small") + { + const std::vector triangles_small = { glm::vec2(2, 2), glm::vec2(1, 3), glm::vec2(3.8, 3.8) }; + nucleus::Raster output({ 6, 6 }, 0u); + const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles_small); + + nucleus::Raster output2({ 6, 6 }, 0u); + const auto pixel_writer2 = [&output2](glm::ivec2 pos) { output2.pixel(pos) = 255; }; + rasterize_triangle_sdf(pixel_writer2, triangles_small, 0); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_small.png")); +#endif + } + + SECTION("rasterize triangle smallest") + { + // less than one pixel + const std::vector triangles_smallest = { glm::vec2(30.4, 30.4), glm::vec2(30.8, 30.6), glm::vec2(30.8, 30.8) }; + nucleus::Raster output({ 64, 64 }, 0u); + const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles_smallest); + + nucleus::Raster output2({ 64, 64 }, 0u); + const auto pixel_writer2 = [&output2](glm::ivec2 pos) { output2.pixel(pos) = 255; }; + rasterize_triangle_sdf(pixel_writer2, triangles_smallest, 0); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_smallest.png")); +#endif + } + + SECTION("rasterize triangle enlarged endcaps only") + { + // less than one pixel + const std::vector triangles = { glm::vec2(5.35, 5.35), glm::vec2(5.40, 5.40), glm::vec2(5.45, 5.35) }; + auto size = glm::vec2(10, 10); + nucleus::Raster output(size, 0u); + float distance = 4.0; + radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output.pixel(pos) = 255; + }; + nucleus::utils::rasterizer::details::add_circle_end_cap(pixel_writer, triangles[0], 1, distance); + nucleus::utils::rasterizer::details::add_circle_end_cap(pixel_writer, triangles[1], 1, distance); + nucleus::utils::rasterizer::details::add_circle_end_cap(pixel_writer, triangles[2], 1, distance); + + nucleus::Raster output2(size, 0u); + const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output2.pixel(pos) = 255; + }; + rasterize_triangle_sdf(pixel_writer2, triangles, distance); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_enlarged_endcap.png")); +#endif + } + + SECTION("rasterize triangle enlarged") + { + const std::vector triangles = { glm::vec2(30.5, 10.5), + glm::vec2(10.5, 30.5), + glm::vec2(50.5, 50.5), + + glm::vec2(5.5, 5.5), + glm::vec2(15.5, 10.5), + glm::vec2(5.5, 15.5) }; + // const std::vector triangles = { glm::vec2(5.35, 5.35), glm::vec2(5.40, 5.40), glm::vec2(5.45, 5.35) }; + auto size = glm::vec2(64, 64); + nucleus::Raster output(size, 0u); + float distance = 5.0; + radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output.pixel(pos) = 255; + }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles, distance); + + nucleus::Raster output2(size, 0u); + const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output2.pixel(pos) = 255; + }; + rasterize_triangle_sdf(pixel_writer2, triangles, distance); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_enlarged.png")); +#endif + } + + SECTION("rasterize triangle horizontal") + { + const std::vector triangles = { glm::vec2(30.5, 10.5), glm::vec2(45.5, 45.5), glm::vec2(10.5, 45.5) }; + auto size = glm::vec2(64, 64); + nucleus::Raster output(size, 0u); + float distance = 4.0; + radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output.pixel(pos) = 255; + }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles, distance); + + nucleus::Raster output2(size, 0u); + const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output2.pixel(pos) = 255; + }; + rasterize_triangle_sdf(pixel_writer2, triangles, distance); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_enlarged_horizontal.png")); +#endif + } + + // // can't be verified exactly, since SDF is currently slightly inaccurate + // SECTION("rasterize triangle narrow") + // { + // const std::vector triangles = { glm::vec2(6.5, 16.5), glm::vec2(55.5, 18.5), glm::vec2(6.5, 20.5), glm::vec2(55.5, 46.5), glm::vec2(6.5, 48.5), glm::vec2(55.5, 50.5) + // } + // ; + // auto size = glm::vec2(64, 64); + // nucleus::Raster output(size, 0u); + // float distance = 5.0; + // radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + // const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + // if (bounds.contains(pos)) + // output.pixel(pos) = 255; + // }; + // nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles, distance); + + // nucleus::Raster output2(size, 0u); + // const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + // if (bounds.contains(pos)) + // output2.pixel(pos) = 255; + // }; + // rasterize_triangle_sdf(pixel_writer2, triangles, distance); + + // CHECK(output.buffer() == output2.buffer()); + + // #ifdef WRITE_DEBUG_IMAGE + // // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + // auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + // image.save(QString("rasterizer_output_enlarged_narrow.png")); + // #endif + // } + + SECTION("validate small raster with large raster") + { + // Note: this test is necessary to validate the rasterizer for small rasterizations + // while larger rasterizations also encounter this problem it is more noticeable if you rasterize on a small raster and try to rasterize the actual triangle afterwards + // specific bug: we rasterized a triangle on a 16x16 raster and saw that on some pixels that should be filled, no triangle was rendered + // the problem was that we compared an integer with a float value, and for certain circumstances this caused the rasterizer to switch fill direction and render less than it should have + + // first render on the original 16x16 raster + // note orig_scale makes sure that we can easily test other scales without having to change the triangle coords + constexpr int orig_size = 16; + constexpr double orig_scale = orig_size / 16.0; + + const std::vector triangles_grid + = { glm::vec2(12.4023 * orig_scale, 0.2754 * orig_scale), glm::vec2(7.0312 * orig_scale, 10.8027 * orig_scale), glm::vec2(7.3633 * orig_scale, 11.0684 * orig_scale) }; + + nucleus::Raster output({ orig_size, orig_size }, 0u); + const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles_grid); + + // next write the same triangle to larger raster + constexpr int enlarged_size = 2048; + nucleus::Raster output_enlarged({ enlarged_size, enlarged_size }, 0u); + + { + constexpr double enlarged_scale = enlarged_size / 16.0; + const std::vector triangles = { + glm::vec2(12.4023 * enlarged_scale, 0.2754 * enlarged_scale), glm::vec2(7.0312 * enlarged_scale, 10.8027 * enlarged_scale), glm::vec2(7.3633 * enlarged_scale, 11.0684 * enlarged_scale) + }; + + const auto pixel_writer_enlarged = [&output_enlarged](glm::ivec2 pos) { output_enlarged.pixel(pos) = 255; }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer_enlarged, triangles); + } + + // finally compare smaller with larger output -> everytime the enlarged output has a value, the smaller output should also have a value + constexpr double enlarged_to_orig = double(orig_size) / double(enlarged_size); + for (size_t i = 0; i < enlarged_size; i++) { + for (size_t j = 0; j < enlarged_size; j++) { + + const auto orig_value = output.pixel({ i * enlarged_to_orig, j * enlarged_to_orig }); + const auto enlarged_value = output_enlarged.pixel({ i, j }); + + if (enlarged_value != 0) { + CHECK(orig_value != 0); + } + + // debug output to visualize both values in one graphic at the end +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + if (orig_value != 0 && enlarged_value == 0) + output_enlarged.pixel({ i, j }) = 125; +#endif + } + } +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + auto image = nucleus::tile::conversion::u8raster_to_qimage(output_enlarged); + image.save(QString("rasterizer_problem.png")); +#endif + } + + SECTION("rasterize donut") + { + const std::vector> polygon_points = { { glm::vec2(10.5, 10.5), glm::vec2(50.5, 10.5), glm::vec2(50.5, 50.5), glm::vec2(10.5, 50.5) }, + { glm::vec2(20.5, 20.5), glm::vec2(40.5, 20.5), glm::vec2(40.5, 40.5), glm::vec2(20.5, 40.5) } }; + + const std::vector correct_edges + = { glm::ivec2(0, 1), glm::ivec2(1, 2), glm::ivec2(2, 3), glm::ivec2(3, 0), glm::ivec2(4, 5), glm::ivec2(5, 6), glm::ivec2(6, 7), glm::ivec2(7, 4) }; + + std::vector vertices; + std::vector edges; + + for (size_t i = 0; i < polygon_points.size(); i++) { + auto current_edges = nucleus::utils::rasterizer::generate_neighbour_edges(polygon_points[i], vertices.size()); + edges.insert(edges.end(), current_edges.begin(), current_edges.end()); + vertices.insert(vertices.end(), polygon_points[i].begin(), polygon_points[i].end()); + } + + CHECK(edges.size() == correct_edges.size()); + + CHECK(edges == correct_edges); + + auto triangles = nucleus::utils::rasterizer::triangulize(vertices, edges); + + nucleus::Raster output({ 64, 64 }, 0u); + const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles); + + nucleus::Raster output2({ 64, 64 }, 0u); + auto pixel_writer2 = [&output2](glm::ivec2 pos) { output2.pixel(pos) = 255; }; + rasterize_triangle_sdf(pixel_writer2, triangles, 0); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_donut.png")); +#endif + } + + SECTION("rasterize random triangles") + { + constexpr auto cells = 8u; + constexpr auto cell_size = 32u; + + nucleus::Raster output({ cells * cell_size, cells * cell_size }, 0u); + const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + + const std::vector> polygon_points = { { { 11.5, 16 }, { 12.9, 3.9 }, { 4.8, 5.5 } }, + { { 59.3, 11.9 }, { 58.9, 6.9 }, { 38.5, 24.7 } }, + { { 79.1, 14.3 }, { 82, 2.4 }, { 72.7, 31.9 } }, + { { 125.2, 24.7 }, { 105.2, 30.8 }, { 125.5, 12.1 } }, + { { 145.9, 17.1 }, { 128, 15.5 }, { 141.6, 29.5 } }, + { { 160.7, 23.1 }, { 174.3, 12.4 }, { 165.1, 0.8 } }, + { { 223.9, 19.1 }, { 205.2, 0.4 }, { 217.1, 26.8 } }, + { { 252.4, 6.9 }, { 233.3, 27.4 }, { 251.3, 14.4 } }, + { { 7.1, 37.2 }, { 4, 43.7 }, { 11.1, 35.5 } }, + { { 39.9, 52.7 }, { 55.5, 48.2 }, { 45.7, 39.9 } }, + { { 82.3, 37.1 }, { 68.7, 46.5 }, { 66.5, 32.6 } }, + { { 102.9, 41.8 }, { 125.5, 34.5 }, { 112.5, 52.2 } }, + { { 155.1, 41.8 }, { 133.3, 44.9 }, { 155.3, 55.7 } }, + { { 188.9, 51.8 }, { 178.8, 53.6 }, { 172.4, 52.2 } }, + { { 220.1, 61.9 }, { 206.1, 39.5 }, { 194.6, 51.6 } }, + { { 236, 59.8 }, { 253.5, 52.9 }, { 232.7, 35.9 } }, + { { 13.8, 83.3 }, { 21.8, 79.7 }, { 23.1, 94.5 } }, + { { 40.3, 70.3 }, { 51.3, 69.4 }, { 61.1, 77.6 } }, + { { 84.7, 65.8 }, { 87.5, 77.3 }, { 84.7, 71.5 } }, + { { 101.4, 87.1 }, { 126.6, 80.8 }, { 119.7, 70.7 } }, + { { 149.7, 65.2 }, { 137.4, 67.7 }, { 135.6, 70.5 } }, + { { 185.8, 82.1 }, { 181, 72.6 }, { 167.1, 79.6 } }, + { { 213.9, 76.1 }, { 209.6, 79.5 }, { 208.3, 92.4 } }, + { { 254.3, 65.9 }, { 226.4, 88.2 }, { 255.7, 73 } }, + { { 19.3, 119.1 }, { 27.2, 120.3 }, { 18.2, 113.9 } }, + { { 38.3, 114.9 }, { 54.6, 104 }, { 46.5, 98.9 } }, + { { 85.1, 113.4 }, { 90.1, 123.5 }, { 75.1, 125.3 } }, + { { 105.5, 97.2 }, { 99.9, 116.2 }, { 99.9, 110.3 } }, + { { 131.4, 116.3 }, { 159.7, 98.2 }, { 158.4, 101.9 } }, + { { 176.2, 127.5 }, { 171, 113.7 }, { 182.8, 107.4 } }, + { { 222.3, 125.3 }, { 216.5, 125.1 }, { 192.1, 104.1 } }, + { { 249.6, 122.2 }, { 245.7, 104.4 }, { 229.7, 115.7 } }, + { { 8.1, 148 }, { 27.4, 130.4 }, { 3.9, 140.1 } }, + { { 51.6, 159.3 }, { 52.7, 135.4 }, { 45.3, 147.3 } }, + { { 70, 147.1 }, { 68.8, 144.7 }, { 92.2, 132.3 } }, + { { 116.8, 142.8 }, { 127.2, 141.7 }, { 117.9, 141.3 } }, + { { 154.7, 146.5 }, { 154.9, 130.7 }, { 137.6, 144.4 } }, + { { 183.6, 147.8 }, { 169.5, 132.9 }, { 177, 147 } }, + { { 210.4, 141.4 }, { 200, 151.8 }, { 222.4, 135.1 } }, + { { 237.5, 136.5 }, { 249.3, 151.6 }, { 251.9, 133.5 } }, + { { 20.4, 168.7 }, { 22.4, 163.9 }, { 4.4, 179.6 } }, + { { 38.1, 191.5 }, { 53.4, 191.1 }, { 34.7, 180.2 } }, + { { 72, 191 }, { 68, 186.3 }, { 77.4, 164.7 } }, + { { 101.4, 168.2 }, { 115.3, 179 }, { 122.1, 173.4 } }, + { { 137.2, 177.7 }, { 137.3, 186.8 }, { 128.4, 181.7 } }, + { { 190.4, 184.5 }, { 188.4, 180.8 }, { 168.4, 180.8 } }, + { { 199.6, 160.9 }, { 211.2, 161.7 }, { 213.9, 189 } }, + { { 252.1, 182 }, { 240.3, 189.9 }, { 245.9, 179.3 } }, + { { 14.7, 208.9 }, { 23.1, 219.3 }, { 27.9, 194 } }, + { { 51.7, 196.5 }, { 63.3, 197.1 }, { 58.8, 208.3 } }, + { { 92, 223.7 }, { 84.5, 204.4 }, { 65.3, 216.4 } }, + { { 108.5, 208.1 }, { 113.8, 200.9 }, { 121.1, 223.8 } }, + { { 137, 199.7 }, { 152.9, 213.2 }, { 155.7, 204.5 } }, + { { 189.5, 206.8 }, { 189.3, 202.5 }, { 172.5, 212.6 } }, + { { 217.1, 217.2 }, { 209.6, 211.5 }, { 215, 203.7 } }, + { { 235.4, 223.6 }, { 236.1, 211 }, { 254.6, 192 } }, + { { 3.3, 224.6 }, { 28.7, 254.4 }, { 17.4, 245.2 } }, + { { 48.1, 245.8 }, { 62.3, 250.4 }, { 38.9, 233 } }, + { { 75, 250 }, { 87.7, 247.6 }, { 95.5, 232.3 } }, + { { 116.8, 247.5 }, { 107, 235.8 }, { 119.5, 230.4 } }, + { { 153.2, 245.2 }, { 136.2, 246.2 }, { 137.4, 229.3 } }, + { { 185.1, 250 }, { 184.4, 236.7 }, { 161.9, 245.8 } }, + { { 222.9, 233.8 }, { 215.4, 242.1 }, { 206.3, 253.2 } }, + { { 234.4, 241.6 }, { 233.2, 249.4 }, { 225.7, 245.4 } } }; + + for (const auto& tri : polygon_points) { + nucleus::utils::rasterizer::rasterize_polygon(pixel_writer, tri); + + // DEBUG view the vertices in the image(uses different pixel intensity) + // const auto pixel_writer_points = [&output](glm::ivec2 pos) { output.pixel(pos) = 125; }; + // pixel_writer_points(tri[0]); + // pixel_writer_points(tri[1]); + // pixel_writer_points(tri[2]); + } + + // DEBUG how the random points were generated + // const auto rand_pos = []() { return std::rand() % (cell_size * 10u) / 10.0f; }; + // std::srand(123458); // initialize rand -> we need to create consistent triangles + + // for (size_t i = 0; i < cells; i++) { + // for (size_t j = 0; j < cells; j++) { + // const auto cell_offset = glm::vec2(j * cell_size, i * cell_size); + + // const std::vector polygon_points + // = { glm::vec2(rand_pos(), rand_pos()) + cell_offset, glm::vec2(rand_pos(), rand_pos()) + cell_offset, glm::vec2(rand_pos(), rand_pos()) + cell_offset }; + + // nucleus::utils::rasterizer::rasterize_polygon(pixel_writer, polygon_points); + + // qDebug() << "{{" << polygon_points[0].x << "," << polygon_points[0].y << "}, " << "{" << polygon_points[1].x << "," << polygon_points[1].y << "}, " << "{" << polygon_points[2].x << + // "," + // << polygon_points[2].y << "}}, "; + // } + // } + + auto image = nucleus::tile::conversion::u8raster_to_qimage(output); + CHECK(image == example_rasterizer_image("rasterizer_output_random_triangle.png")); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + image.save(QString("rasterizer_output_random_triangle.png")); +#endif + } + + SECTION("rasterize triangle start/end y") + { + const std::vector triangles = { // down to right + glm::vec2(2.7, 1.5), + glm::vec2(3.7, 2.5), + glm::vec2(3.7, 2.7), + // down to left + glm::vec2(8.7, 1.5), + glm::vec2(7.7, 2.5), + glm::vec2(7.7, 2.7), + + // 1 lines + // down to right + glm::vec2(3.5, 7.3), + glm::vec2(26.5, 7.5), + glm::vec2(24.5, 7.6), + // down to left + glm::vec2(26.5, 12.3), + glm::vec2(3.5, 12.5), + glm::vec2(5.5, 12.6), + + // 2 lines + // down to right + glm::vec2(3.5, 16.7), + glm::vec2(6.5, 17.5), + glm::vec2(4.5, 17.6), + // down to left + glm::vec2(6.5, 21.7), + glm::vec2(3.5, 22.5), + glm::vec2(5.5, 22.6) + }; + + auto size = glm::vec2(30, 30); + nucleus::Raster output(size, 0u); + float distance = 0.0; + radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output.pixel(pos) = 255; + }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles, distance); + + nucleus::Raster output2(size, 0u); + const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output2.pixel(pos) = 255; + }; + rasterize_triangle_sdf(pixel_writer2, triangles, distance); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_triangle_start_end.png")); +#endif + } + + SECTION("rasterize line start/end y") + { + const std::vector line_lower_min = { glm::vec2(2.7, 2.5), glm::vec2(3.7, 1.5) }; + const std::vector line_upper_min = { glm::vec2(7.3, 2.5), glm::vec2(8.5, 1.3) }; + const std::vector line_left_to_right_1pixel = { glm::vec2(2.3, 7.3), glm::vec2(28.5, 7.7) }; + const std::vector line_right_to_left_1pixel = { glm::vec2(2.3, 12.7), glm::vec2(28.5, 12.3) }; + const std::vector line_left_to_right_2pixel = { glm::vec2(2.3, 17.3), glm::vec2(6.5, 18.7) }; + const std::vector line_right_to_left_2pixel = { glm::vec2(2.3, 23.7), glm::vec2(6.5, 22.3) }; + + auto size = glm::vec2(30, 30); + nucleus::Raster output(size, 0u); + float distance = 0.0; + radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output.pixel(pos) = 255; + }; + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line_lower_min, distance); + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line_upper_min, distance); + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line_left_to_right_1pixel, distance); + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line_right_to_left_1pixel, distance); + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line_left_to_right_2pixel, distance); + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line_right_to_left_2pixel, distance); + + nucleus::Raster output2(size, 0u); + const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output2.pixel(pos) = 255; + }; + rasterize_line_sdf(pixel_writer2, line_lower_min, distance); + rasterize_line_sdf(pixel_writer2, line_upper_min, distance); + rasterize_line_sdf(pixel_writer2, line_left_to_right_1pixel, distance); + rasterize_line_sdf(pixel_writer2, line_right_to_left_1pixel, distance); + rasterize_line_sdf(pixel_writer2, line_left_to_right_2pixel, distance); + rasterize_line_sdf(pixel_writer2, line_right_to_left_2pixel, distance); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_line_start_end.png")); +#endif + } + + SECTION("rasterize line straight") + { + const std::vector line = { glm::vec2(10.5, 10.5), glm::vec2(10.5, 50.5), glm::vec2(50.5, 50.5), glm::vec2(50.5, 10.5), glm::vec2(10.5, 10.5) }; + auto size = glm::vec2(64, 64); + nucleus::Raster output(size, 0u); + float distance = 0.0; + radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output.pixel(pos) = 255; + }; + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line, distance); + + nucleus::Raster output2(size, 0u); + const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output2.pixel(pos) = 255; + }; + rasterize_line_sdf(pixel_writer2, line, distance); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_line_straight.png")); +#endif + } + + SECTION("rasterize line diagonal") + { + const std::vector line = { glm::vec2(30.5, 10.5), glm::vec2(50.5, 30.5), glm::vec2(30.5, 50.5), glm::vec2(10.5, 30.5), glm::vec2(30.5, 10.5) }; + auto size = glm::vec2(64, 64); + nucleus::Raster output(size, 0u); + float distance = 0.0; + radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output.pixel(pos) = 255; + }; + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line, distance); + + nucleus::Raster output2(size, 0u); + const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output2.pixel(pos) = 255; + }; + rasterize_line_sdf(pixel_writer2, line, distance); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_line_diagonal.png")); +#endif + } + + SECTION("rasterize line straight enlarged") + { + const std::vector line = { glm::vec2(10.5, 10.5), glm::vec2(10.5, 50.5), glm::vec2(50.5, 50.5), glm::vec2(50.5, 10.5), glm::vec2(10.5, 10.5) }; + auto size = glm::vec2(64, 64); + nucleus::Raster output(size, 0u); + float distance = 2.0; + radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output.pixel(pos) = 255; + }; + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line, distance); + + nucleus::Raster output2(size, 0u); + const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output2.pixel(pos) = 255; + }; + rasterize_line_sdf(pixel_writer2, line, distance); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_line_straight_enlarged.png")); +#endif + } + + SECTION("rasterize line diagonal enlarged") + { + const std::vector line = { glm::vec2(30.5, 10.5), glm::vec2(50.5, 30.5), glm::vec2(30.5, 50.5), glm::vec2(10.5, 30.5), glm::vec2(30.5, 10.5) }; + // const std::vector line = { glm::vec2(30.5, 10.5), glm::vec2(50.5, 30.5) }; + auto size = glm::vec2(64, 64); + nucleus::Raster output(size, 0u); + float distance = 2.0; + radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output.pixel(pos) = 255; + }; + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line, distance); + + nucleus::Raster output2(size, 0u); + const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output2.pixel(pos) = 255; + }; + rasterize_line_sdf(pixel_writer2, line, distance); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_line_diagonal_enlarged.png")); +#endif + } +} +TEST_CASE("nucleus/utils/rasterizer benchmarks") +{ + + BENCHMARK("triangulize polygons") + { + const std::vector polygon_points = { glm::vec2(10.5, 10.5), glm::vec2(30.5, 10.5), glm::vec2(50.5, 50.5), glm::vec2(10.5, 30.5) }; + const auto edges = nucleus::utils::rasterizer::generate_neighbour_edges(polygon_points); + nucleus::utils::rasterizer::triangulize(polygon_points, edges); + }; + + BENCHMARK("triangulize polygons 2") + { + std::vector points = { glm::vec2(0, 0), glm::vec2(1, 1), glm::vec2(0, 2), glm::vec2(2, 2), glm::vec2(2, 0) }; + std::vector edges = { glm::ivec2(0, 1), glm::ivec2(1, 2), glm::ivec2(2, 3), glm::ivec2(3, 4), glm::ivec2(4, 0) }; + + nucleus::utils::rasterizer::triangulize(points, edges); + }; + + BENCHMARK("triangulize polygons + remove duplicates (no duplicates)") + { + std::vector points = { glm::vec2(0, 0), glm::vec2(1, 1), glm::vec2(0, 2), glm::vec2(2, 2), glm::vec2(2, 0) }; + std::vector edges = { glm::ivec2(0, 1), glm::ivec2(1, 2), glm::ivec2(2, 3), glm::ivec2(3, 4), glm::ivec2(4, 0) }; + + nucleus::utils::rasterizer::triangulize(points, edges, true); + }; + BENCHMARK("triangulize polygons + remove duplicates (with duplicates)") + { + std::vector points = { glm::vec2(0, 0), glm::vec2(1, 1), glm::vec2(0, 2), glm::vec2(2, 2), glm::vec2(1, 1), glm::vec2(2, 0) }; + std::vector edges = { glm::ivec2(0, 1), glm::ivec2(1, 2), glm::ivec2(2, 3), glm::ivec2(3, 4), glm::ivec2(4, 5), glm::ivec2(5, 0) }; + + nucleus::utils::rasterizer::triangulize(points, edges, true); + }; + + BENCHMARK("Rasterize triangle") + { + const std::vector triangles = { glm::vec2(30.5, 10.5), glm::vec2(10.5, 30.5), glm::vec2(50.5, 50.5), glm::vec2(5.5, 5.5), glm::vec2(15.5, 10.5), glm::vec2(5.5, 15.5) }; + + // we dont particular care how long it takes to write to raster -> so do nothing + const auto pixel_writer = [](glm::ivec2) { /*do nothing*/ }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles); + }; + + BENCHMARK("Rasterize triangle distance") + { + const std::vector triangles = { glm::vec2(30.5, 10.5), glm::vec2(10.5, 30.5), glm::vec2(50.5, 50.5), glm::vec2(5.5, 5.5), glm::vec2(15.5, 10.5), glm::vec2(5.5, 15.5) }; + + // we dont particular care how long it takes to write to raster -> so do nothing + const auto pixel_writer = [](glm::ivec2) { /*do nothing*/ }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles, 5.0); + }; + + BENCHMARK("Rasterize triangle SDF") + { + const std::vector triangles = { glm::vec2(30.5, 10.5), glm::vec2(10.5, 30.5), glm::vec2(50.5, 50.5), glm::vec2(5.5, 5.5), glm::vec2(15.5, 10.5), glm::vec2(5.5, 15.5) }; + + // we dont particular care how long it takes to write to raster -> so do nothing + const auto pixel_writer = [](glm::ivec2) { /*do nothing*/ }; + rasterize_triangle_sdf(pixel_writer, triangles, 0); + }; + + BENCHMARK("Rasterize triangle SDF distance") + { + const std::vector triangles = { glm::vec2(30.5, 10.5), glm::vec2(10.5, 30.5), glm::vec2(50.5, 50.5), glm::vec2(5.5, 5.5), glm::vec2(15.5, 10.5), glm::vec2(5.5, 15.5) }; + + // we dont particular care how long it takes to write to raster -> so do nothing + const auto pixel_writer = [](glm::ivec2) { /*do nothing*/ }; + rasterize_triangle_sdf(pixel_writer, triangles, 5.0); + }; +} diff --git a/unittests/nucleus/tile_scheduler.cpp b/unittests/nucleus/tile_scheduler.cpp index 986ad041..1801dffe 100644 --- a/unittests/nucleus/tile_scheduler.cpp +++ b/unittests/nucleus/tile_scheduler.cpp @@ -107,7 +107,7 @@ std::unique_ptr scheduler_with_aabb() QByteArray example_tile_data() { auto height_file = QFile(QString("%1%2").arg(ALP_TEST_DATA_DIR, "test-tile_ortho.jpeg")); - height_file.open(QFile::ReadOnly); + REQUIRE(height_file.open(QFile::ReadOnly)); const auto height_bytes = height_file.readAll(); REQUIRE(!QImage::fromData(height_bytes).isNull()); return height_bytes; diff --git a/unittests/nucleus/vector_tile.cpp b/unittests/nucleus/vector_tile.cpp index 1781c76b..e319baac 100644 --- a/unittests/nucleus/vector_tile.cpp +++ b/unittests/nucleus/vector_tile.cpp @@ -60,7 +60,7 @@ TEST_CASE("nucleus/vector_tiles") { QString filepath = QString("%1%2").arg(ALP_TEST_DATA_DIR, "vectortile.mvt"); QFile file(filepath); - file.open(QIODevice::ReadOnly | QIODevice::Unbuffered); + REQUIRE(file.open(QIODevice::ReadOnly | QIODevice::Unbuffered)); QByteArray data = file.readAll(); CHECK(data.size() > 0);