diff --git a/c++/CMakeLists.txt b/c++/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..692cc0519dc57b64917c3240e37debb764c9e71c --- /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 0000000000000000000000000000000000000000..9421885f8a1f1f914a55bc8da5488b39dd01ddea --- /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 0000000000000000000000000000000000000000..27afbbb8da3c1740eab29950fcf0f4ec5c8a72a6 --- /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() + // } + // } + + +