From 397dcf87e6d54dd065032925895ef1469fa4b62f Mon Sep 17 00:00:00 2001
From: Marco Matthies <71844+marcom@users.noreply.github.com>
Date: Tue, 25 Jun 2024 16:47:29 +0200
Subject: [PATCH] Initial static mockup of GUI

Startup time < 1s.

Not hooked up to the simulation data yet.

- shows the map

- tries to place a canvas over the map to plot the animals, doesn't
  work for now

- Play/Pause buttons that don't work yet

- some menu bar entries are still missing
---
 c++/CMakeLists.txt |  55 +++++++
 c++/main.cpp       |  56 ++++++++
 c++/main.qml       | 348 +++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 459 insertions(+)
 create mode 100644 c++/CMakeLists.txt
 create mode 100644 c++/main.cpp
 create mode 100644 c++/main.qml

diff --git a/c++/CMakeLists.txt b/c++/CMakeLists.txt
new file mode 100644
index 0000000..692cc05
--- /dev/null
+++ b/c++/CMakeLists.txt
@@ -0,0 +1,55 @@
+cmake_minimum_required(VERSION 3.16)
+project(PersefoneGUI LANGUAGES CXX)
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED True)
+set(CMAKE_CXX_EXTENSIONS OFF)
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17")
+
+#set(CMAKE_PREFIX_PATH "/usr/lib/x86_64-linux-gnu/cmake/Qt6" CACHE STRING "Qt6 installation prefix")
+#set(HOME_DIR $ENV{HOME})
+#set(CMAKE_PREFIX_PATH "${HOME_DIR}/app/qt6/6.7.2/gcc_64/lib/cmake/Qt6" CACHE STRING "Qt6 installation prefix")
+
+set(CMAKE_AUTOMOC ON)
+
+find_package(Qt6 REQUIRED COMPONENTS Core Gui Quick)
+
+qt_add_executable(PersefoneGUI
+    main.cpp
+)
+
+set_target_properties(PersefoneGUI PROPERTIES
+    WIN32_EXECUTABLE TRUE
+    MACOSX_BUNDLE TRUE
+)
+
+target_link_libraries(PersefoneGUI PRIVATE
+    Qt6::Core
+    Qt6::Gui
+    Qt6::Quick
+)
+
+# Resources:
+set(qml_resource_files
+    "main.qml"
+)
+
+qt_add_resources(PersefoneGUI "qml"
+    PREFIX "/"
+    FILES ${qml_resource_files}
+)
+
+install(TARGETS PersefoneGUI
+    BUNDLE  DESTINATION .
+    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+)
+
+# qt_generate_deploy_qml_app_script(
+#     TARGET PersefoneGUI
+#     OUTPUT_SCRIPT deploy_script
+#     MACOS_BUNDLE_POST_BUILD
+#     NO_UNSUPPORTED_PLATFORM_ERROR
+#     DEPLOY_USER_QML_MODULES_ON_UNSUPPORTED_PLATFORM
+# )
+# install(SCRIPT ${deploy_script})
diff --git a/c++/main.cpp b/c++/main.cpp
new file mode 100644
index 0000000..9421885
--- /dev/null
+++ b/c++/main.cpp
@@ -0,0 +1,56 @@
+#include <QGuiApplication>
+#include <QQmlApplicationEngine>
+#include <QQmlContext>
+#include <QVariantList>
+#include <QImageReader>
+#include <QColor>
+#include <QDir>
+#include <QUrl>
+
+QString getFileUrlRelativeToExecutable(const QString &relativeFilePath) {
+    // Get the directory path of the executable
+    QString basePath = QCoreApplication::applicationDirPath();
+    // Combine the base path with the relative file path
+    QDir dir(basePath);
+    QString fullPath = dir.filePath(relativeFilePath);  // Ensures the path is correctly formed
+    // Convert the file path to a URL
+    QUrl fileUrl = QUrl::fromLocalFile(fullPath);
+    return fileUrl.toString();
+}
+
+
+int main(int argc, char *argv[])
+{
+    QGuiApplication gui_app(argc, argv);
+
+    // Check if TIFF format is supported
+    QStringList supportedFormats;
+    for (const QByteArray &format : QImageReader::supportedImageFormats()) {
+        supportedFormats.append(QString::fromLatin1(format));
+    }
+    if (!supportedFormats.contains("tiff", Qt::CaseInsensitive)) {
+        qWarning() << "TIFF format is not supported!";
+        return -1;
+    }
+
+    QQmlApplicationEngine engine;
+
+    QString mapImagePath = getFileUrlRelativeToExecutable("../../data/regions/jena-small/landcover.tif");
+    engine.rootContext()->setContextProperty("mapImagePath", mapImagePath);
+
+    QVariantList colors;
+    colors.append(QVariant::fromValue(QColor(255, 0, 0)));   // Red
+    colors.append(QVariant::fromValue(QColor(0, 255, 0)));   // Green
+    colors.append(QVariant::fromValue(QColor(0, 0, 255)));   // Blue
+    engine.rootContext()->setContextProperty("colorList", colors);
+
+    const QUrl qml_url(u"qrc:/main.qml"_qs);
+    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, &gui_app,
+                     [qml_url](QObject *obj, const QUrl &obj_url) {
+                         if (!obj && qml_url == obj_url)
+                             QCoreApplication::exit(-1);
+                     },
+                     Qt::QueuedConnection);
+    engine.load(qml_url);
+    return gui_app.exec();
+}
diff --git a/c++/main.qml b/c++/main.qml
new file mode 100644
index 0000000..27afbbb
--- /dev/null
+++ b/c++/main.qml
@@ -0,0 +1,348 @@
+import QtQuick 6.2
+import QtQuick.Controls 6.2
+import QtQuick.Dialogs  6.2
+import QtQuick.Layouts 6.2
+
+ApplicationWindow {
+    id: mainWindow
+    title: "Persefone.jl GUI"
+    width: 1024
+    height: 768
+    visibility: "Maximized"
+    visible: true
+
+    // these are the coordinates of the map image
+    property var mapCoords: ({ x: 0, y: 0, width: 0, height: 0 })
+
+    // calculate imgCoords of an Image
+    function calculateVisibleGeometry(img) {
+        var containerWidth = img.width;
+        var containerHeight = img.height;
+        var imgRatio = img.sourceSize.width / img.sourceSize.height;
+        var containerRatio = containerWidth / containerHeight;
+        var visibleWidth, visibleHeight, visibleX, visibleY;
+
+        if (imgRatio > containerRatio) {
+            visibleWidth = containerWidth;
+            visibleHeight = containerWidth / imgRatio;
+            visibleX = 0;
+            visibleY = (containerHeight - visibleHeight) / 2;
+        } else {
+            visibleHeight = containerHeight;
+            visibleWidth = containerHeight * imgRatio;
+            visibleX = (containerWidth - visibleWidth) / 2;
+            visibleY = 0;
+        }
+        return {x: visibleX, y: visibleY, width: visibleWidth, height: visibleHeight};
+    }
+
+    menuBar: MenuBar {
+        Menu {
+            title: "&Simulation"
+            // Action {
+            //     text: "&New Simulation"
+            //     onTriggered: { vars.launching = true } //TODO select config file
+            // }
+            // Action {
+            //     text: "&Configure Simulation"
+            //     onTriggered: { Julia.configwindow() }
+            // }
+            // Action {
+            //     text: "&Load Saved State"
+            //     onTriggered: { loadFileChooser.open() }
+            // }
+            // Action {
+            //     text: "&Save Current State"
+            //     onTriggered: { saveFileChooser.open() }
+            // }
+            MenuSeparator { }
+            Action {
+                text: "&Quit"
+                onTriggered: { mainWindow.close() }
+            }
+        }
+        //Menu {
+            //title: "&Data"
+            //     Action {
+            //         text: "Show &Population Graph"
+            //         onTriggered: { populationGraph.visible = true }
+            //     }
+            //     Action {
+            //         text: "Save &Simulation Output"
+            //         onTriggered: { Julia.saveoutput() }
+            //     }
+        //}
+        Menu {
+            title: "&Help"
+            Action {
+                text: "&Documentation"
+                onTriggered: { Qt.openUrlExternally("https://persefone-model.eu/documentation") }
+            }
+            Action {
+                text: "&Website"
+                onTriggered: { Qt.openUrlExternally("https://persefone-model.eu/") }
+            }
+            // Action {
+            //     text: "&About"
+            //     onTriggered: { aboutDialog.open() }
+            // }
+        }
+    }
+
+    ColumnLayout {
+        anchors.fill: parent
+        //anchors.horizontalCenter: parent.horizontalCenter
+        
+        Image {
+            id: mapImage
+            source: mapImagePath
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            height: parent.height - 50  // Leave some space for the row with play/pause
+            //anchors.fill: parent
+            //fillMode: Image.PreserveAspectFit
+            //anchors.top: parent.top
+            //anchors.bottom: buttonRow.top
+            //anchors.horizontalCenter: parent.horizontalCenter
+            fillMode: Image.PreserveAspectFit
+
+            onStatusChanged: {
+                if (status == Image.Error)
+                    console.log("Error loading image: ", source);
+                if (status == Image.Ready) {
+                    mapCoords = calculateVisibleGeometry(mapImage);
+                    //console.log("Image loaded successfully.");
+                }
+            }
+
+            Canvas {
+                id: canvas
+                //anchors.fill: parent
+                //height: parent.height
+                //width: parent.width
+                // anchors.horizontalCenter: parent.horizontalCenter
+                width: mapImage.width
+                height: mapImage.height
+                x: mapImage.x
+                y: mapImage.y
+                z: 1
+                onPaint: {
+                    var ctx = canvas.getContext("2d");
+                    ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+                    // Adjust based on the image's scaling and aspect ratio
+                    // var imageScaleX = mapImage.width;    //    / mapImage.width;
+                    // var imageScaleY = mapImage.height;   //    / mapImage.height;
+
+                    // Define circles (example positions and radii)
+                    var circles = [
+                        { x: 0.4, y: 0.5, radius: 5 },
+                        { x: 0.8, y: 0.3, radius: 10 },
+                        { x: 1.0, y: 0.5, radius: 20 }
+                    ];
+
+                    for (var i = 0; i < circles.length; i++) {
+                        var circle = circles[i];
+                        var color = colorList[i % colorList.length];  // Use colors from colorList
+                        ctx.fillStyle = Qt.rgba(color.r / 255, color.g / 255, color.b / 255, 1);
+                        ctx.beginPath();
+                        ctx.arc(mapCoords.x + circle.x * mapCoords.width,
+                                mapCoords.y + circle.y * mapCoords.height,
+                                circle.radius, 0, 2 * Math.PI);
+                        ctx.fill();
+                    }
+                }
+
+                Timer {
+                    id: moveTimer
+                    interval: 100  // TODO: what is the unit, maybe milliseconds?
+                    repeat: true
+                    onTriggered: {
+                        // TODO: circle movement is random for now, should come
+                        // from simulation
+                        for (var i = 0; i < parent.circles.length; i++) {
+                            parent.circles[i].x += 2 * Math.random() - 1;
+                            parent.circles[i].y += 2 * Math.random() - 1;
+                        }
+                        parent.requestPaint();
+                    }
+                }
+            }
+        }
+
+        RowLayout {
+            Layout.alignment: Qt.AlignCenter
+            Layout.fillWidth: true
+            height: 50
+            //anchors.bottom: parent.bottom
+            //anchors.horizontalCenter: parent.horizontalCenter
+
+            Button {
+                text: "Play"
+                onClicked: moveTimer.start()
+            }
+
+            Button {
+                text: "Stop"
+                onClicked: moveTimer.stop()
+            }
+        }
+    }
+
+
+    // Redraw when the window is resized
+    Component.onCompleted: canvas.requestPaint()
+    onWidthChanged: canvas.requestPaint()
+    onHeightChanged: canvas.requestPaint()
+}
+
+    // visualise the model map and the locations of animals
+    // MakieViewport {
+    //     id: mapviewport
+    //     anchors.fill: parent
+    //     renderFunction: render_map_callback
+    // 
+
+    // // the main control bar, with pause/step/run buttons, the progress
+    // // bar and a speed slider
+    // footer: ToolBar {
+    //     RowLayout {
+    //         //TODO change button texts to icons
+    //         // (https://doc.qt.io/qt-6/qtquickcontrols-icons.html)
+    //         id: controlBar
+    //         anchors.fill: parent
+    //         Layout.alignment: Qt.AlignVCenter
+    //         Layout.fillWidth: true
+    //         // anchors.topMargin: 5 //FIXME
+    //         // anchors.bottomMargin: 5
+    //         Button {
+    //             id: backButton
+    //             text: "<"
+    //             ToolTip.text: "Back"
+    //             ToolTip.visible: hovered
+    //             onClicked: { Julia.previousstep() }
+    //         }
+    //         Button {
+    //             id: stepButton
+    //             text: ">"
+    //             ToolTip.text: "Step"
+    //             ToolTip.visible: hovered
+    //             onClicked: { Julia.nextstep() }
+    //         }
+    //         Button {
+    //             id: runButton
+    //             text: vars.runbuttontext
+    //             ToolTip.text: vars.runbuttontip
+    //             ToolTip.visible: hovered
+    //             onClicked: { vars.running = !vars.running }
+    //         }
+    //         ProgressBar {
+    //             id: progressBar
+    //             value: vars.progress
+    //             Layout.fillWidth: true
+    //             ToolTip.text: "Simulation progress"
+    //             ToolTip.visible: hovered
+    //         }
+    //         Slider {
+    //             id: speedSlider
+    //             from: 0.0
+    //             to: 2.0
+    //             value: vars.delay
+    //             stepSize: 0.1
+    //             snapMode: Slider.SnapAlways
+    //             ToolTip.text: "Time delay between updates"
+    //             ToolTip.visible: hovered
+    //             onValueChanged: vars.delay = value
+    //         }
+    //         Text {
+    //             id: dateText
+    //             text: Julia.datestring()
+    //             //width:  //TODO
+    //         }
+    //     }
+    // }
+
+
+    // TODO: MessageDialog is available since Qt 6.7
+    // extra windows
+//     MessageDialog {
+//         id: aboutDialog
+//         text: "Persefone.jl GUI"
+//         informativeText: "A mechanistic model of agricultural landscapes \
+// and ecosystems in Europe.\n\n\
+// © 2023 Daniel Vedder, Lea Kolb, Guy Pe'er\n\
+// Distributed under the MIT license."
+//     }
+
+    // FileDialog {
+    //     id: loadFileChooser
+    //     defaultSuffix: "dat"
+    //     nameFilters: ["Save files (*.dat)"]
+    //     onAccepted: { Julia.loadsimulation(selectedFile.toString()) }
+    // }
+
+    // FileDialog {
+    //     id: saveFileChooser
+    //     defaultSuffix: "dat"
+    //     fileMode: FileDialog.SaveFile
+    //     nameFilters: ["Save files (*.dat)"]
+    //     onAccepted: { Julia.savesimulation(selectedFile.toString()) }
+    // }
+
+    // Window {
+    //     id: populationGraph
+    //     title: "Population Graph"
+    //     width: 512
+    //     height: 512
+    //     visible: false
+
+    //     MakieViewport {
+    //         id: plotviewport
+    //         anchors.fill: parent
+    //         renderFunction: render_plot_callback
+    //     }
+    // }
+
+    // Popup {
+    //     id: splashPopup
+    //     parent: Overlay.overlay
+    //     closePolicy: Popup.NoAutoClose
+    //     modal: true
+    //     padding: 0
+
+    //     width: 600
+    //     height: 250
+    //     x: Math.round((parent.width - width) / 2)
+    //     y: Math.round((parent.height - height) / 2)
+        
+    //     Image {
+    //         anchors.fill: parent
+    //         source: "persefonejl_logo_v3_splash.png"
+    //     }
+    // }
+
+    // set up connections and signals to update the simulation and the display
+    // Connections {
+    //     target: timer
+    //     function onTimeout() { vars.ticks += 1 }
+    // }
+
+    // JuliaSignals {
+    //     signal updateMakie()
+    //     onUpdateMakie: {
+    //         dateText.text = Julia.datestring();
+    //         mapviewport.update();
+    //         plotviewport.update();
+    //     }
+    //     signal showSplash()
+    //     onShowSplash: {
+    //         splashPopup.open()
+    //     }
+    //     signal closeSplash()
+    //     onCloseSplash: {
+    //         splashPopup.close()
+    //     }
+    // }
+
+
+
-- 
GitLab