Skip to content
Snippets Groups Projects
Commit 397dcf87 authored by Marco Matthies's avatar Marco Matthies
Browse files

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
parent e6ea63fb
Branches
No related tags found
No related merge requests found
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})
#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();
}
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()
// }
// }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment