diff --git a/Manifest.toml b/Manifest.toml index 4a51b1a671cd9e3bce5f2ddd7a1534935d09d2c4..75d18cc04331ae3013e00c4610336700ae88b1a2 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -2,7 +2,7 @@ julia_version = "1.9.3" manifest_format = "2.0" -project_hash = "bcd13ee640573e4651fc680662e595f5fd73216d" +project_hash = "9170528eea0c39d465f2660dd18c961dc469bd3c" [[deps.AbstractFFTs]] deps = ["LinearAlgebra"] diff --git a/Project.toml b/Project.toml index bedbfecce453f485343a7eff4dbf31647572d048..e91dfd7dca058ddbd51bb737cfb6916b68f3d61e 100644 --- a/Project.toml +++ b/Project.toml @@ -6,6 +6,7 @@ version = "0.1.0" [deps] Agents = "46ada45e-f475-11e8-01d0-f70cc89e6671" CxxWrap = "1f15a43c-97ca-5a2a-ae31-89f07a497df4" +DataFramesMeta = "1313f7d8-7da2-5740-9ea0-a2ca25f37964" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" diff --git a/guiparams.toml b/guiparams.toml index 04d95ec40fc90b423ad544c9adddbaa49310dfdd..093580941ffc148b7da4835a19abcd7f4413c668 100644 --- a/guiparams.toml +++ b/guiparams.toml @@ -21,9 +21,10 @@ startdate = 2022-01-01 enddate = 2022-12-31 [world] -landcovermap = "data/landcover_jena.tif" # location of the landcover map -farmfieldsmap = "data/fields_jena.tif" # location of the field geometry map -weatherfile = "data/weather_jena.csv" # location of the weather data file +#TODO point to files in the main data directory? (avoid file duplication) +landcovermap = "data/regions/jena/landcover.tif" # location of the landcover map +farmfieldsmap = "data/regions/jena/fields.tif" # location of the field geometry map +weatherfile = "data/regions/jena/weather.csv" # location of the weather data file [farm] farmmodel = "FieldManager" # which version of the farm model to use (not yet implemented) @@ -36,6 +37,5 @@ insectmodel = ["season", "habitat", "pesticides", "weather"] # factors affecting [crop] cropmodel = "almass" # crop growth model to use, "almass" or "aquacrop" -cropfile = "data/crop_data_general.csv" # file with general crop parameters -growthfile = "data/almass_crop_growth_curves.csv" # file with crop growth parameters - +cropfile = "data/crops/almass/crop_data_general.csv" # file with general crop parameters +growthfile = "data/crops/almass/almass_crop_growth_curves.csv" # file with crop growth param diff --git a/src/GUI.jl b/src/GUI.jl index 271edf47fe5580638fabdb83f57959fdce4adafd..530ace8157629d13c5764ace3dd602adc72fabab 100644 --- a/src/GUI.jl +++ b/src/GUI.jl @@ -3,18 +3,11 @@ ### This file links the QML UI designs to the underlying model. ### -## For references, see: -## - https://github.com/barche/QML.jl -## - https://github.com/barche/QmlJuliaExamples -## - https://doc.qt.io/qt-6/qtquick-index.html - -#FIXME we either have a memory leak, and/or too much memory consumed -# -> leads to SIGKILL after 3 months - global model = nothing global landcovermap = nothing global runsimulation = nothing global timer = nothing +global launching = false const configfile = Observable("guiparams.toml") const running = Observable(false) @@ -24,18 +17,55 @@ const progress = Observable(0.0) const delay = Observable(0.5) const runbuttontext = Observable(">>") const runbuttontip = Observable("Run") +const mapimage = Observable(Figure().scene) + +global guiproperties = + ["running" => running, + "ticks" => ticks, + "date" => date, + "delay" => delay, + "progress" => progress, + "runbuttontext" => runbuttontext, + "runbuttontip" => runbuttontip] function newsimulation() global model, landcovermap running[] = false progress[] = 0.0 model = initialise(configfile[]) - landcovermap = load(@param(world.landcovermap)) + landcovermap = rotr90(load(@param(world.landcovermap))) date[] = model.date println("Model initialised.") end -#TODO loadsimulation() +function loadsimulation(filename) + global model + fn = convert(filename, String) + startswith(fn, "file://") && (fn = fn[8:end]) + model = loadmodelobject(fn) + running[] = false + landcovermap = rotr90(load(@param(world.landcovermap))) + date[] = model.date + progress[] = (date[]-@param(core.startdate)) / + (@param(core.enddate)-@param(core.startdate)) + @emit updateMakie() +end + +function savesimulation(filename) + global model + fn = convert(filename, String) + startswith(fn, "file://") && (fn = fn[8:end]) + savemodelobject(model, fn) +end + +function Base.convert(s::QString, ::Type{<:AbstractString}) + s2 = "" + for l in s + s2 *= l + end + s2 +end + #TODO saveoutput() function nextstep() @@ -106,18 +136,50 @@ function togglerunning() runsimulation = createrunfunction() end end - + function render_map(screen) - #FIXME individuals not plotted - global model, landcovermap + global model, mapimage, landcovermap, launching + launching && display(screen, Figure().scene) # blank screen at launch println("Updating map") - figure = visualisemap(model, date[]-Day(1), landcovermap) - display(screen, figure.scene) + #FIXME I'm not sure date-1 is the real fix + #FIXME repeatedly calling `visualisemap` causes the memory leak + # (regardless of whether individuals are plotted or just the map) + #mapimage[] = visualisemap(model, date[]-Day(1), landcovermap) + mapimage[] = vismap(model, date[]-Day(1), landcovermap) + display(screen, mapimage[]) +end + +function vismap(model::AgentBasedModel,date=nothing,landcover=nothing) + # load and plot the map + # Note: if the landcover map is supplied, it needs to be rotr90'ed + isnothing(landcover) && (landcover = load(@param(world.landcovermap))) + f = Figure() + ax = Axis(f[1,1]) + hidedecorations!(ax) + image!(f[1,1], landcover) + ax.aspect = DataAspect() + # check if there are individuals and plot them + inds = model.datatables["individuals"] + if iszero(size(inds)[1]) + @debug "No individual data to map" + return f.scene + end + isnothing(date) && (date = inds.Date[end]) + for s in unique(inds.Species) + points = @select!(@subset(inds, :Species .== s, :Date .== date), + :X, :Y) + # The origin in Makie is in the bottom-left rather than in the top-left as + # on the model map, so we have to invert the Y coordinates + @transform!(points, :Y = size(model.landscape)[2] .- :Y) + scatter!(f[1,1], Matrix{Float32}(points), markersize=8) + end + f.scene end function render_plot(screen) - #FIXME causes segfault when called during a simulation - global model + #FIXME causes segfault when called during a simulation? + global model, launching + launching && display(screen, Figure().scene) # blank screen at launch println("Updating plot") figure = populationtrends(model) display(screen, figure.scene) @@ -138,14 +200,16 @@ end datestring = () -> Dates.format(date[], "dd U yyyy") -function splashscreen() - qmlfile = joinpath(dirname(@__FILE__), "splash.qml") - quick_view = init_qquickview() - set_source(quick_view, QUrlFromLocalFile(qmlfile)) - QML.show(quick_view) - #XXX exec_async() is currently still broken, but should be fixed soon - # (https://github.com/JuliaGraphics/QML.jl/issues/174) - @async exec() #FIXME do this with the timer? +function activateqmlfunctions() + @qmlfunction newsimulation + @qmlfunction configwindow + @qmlfunction loadsimulation + @qmlfunction savesimulation + @qmlfunction nextstep + @qmlfunction previousstep + @qmlfunction togglerunning + @qmlfunction datestring + @qmlfunction writeconfig end """ @@ -154,33 +218,27 @@ end The main function that creates the application. """ function launch() - global model, timer - #splashscreen() - isnothing(model) && newsimulation() - - @qmlfunction newsimulation - @qmlfunction nextstep - @qmlfunction previousstep - @qmlfunction togglerunning - @qmlfunction datestring + global model, timer, launching + launching = true + activateqmlfunctions() timer = QTimer() + GLMakie.activate!() ENV["QSG_RENDER_LOOP"] = "basic" QML.setGraphicsApi(QML.OpenGL) qmlfile = joinpath(dirname(@__FILE__), "main.qml") loadqml(qmlfile, timer = timer, - vars = JuliaPropertyMap("running" => running, - "ticks" => ticks, - "date" => date, - "delay" => delay, - "progress" => progress, - "runbuttontext" => runbuttontext, - "runbuttontip" => runbuttontip), + vars = JuliaPropertyMap(guiproperties...,configproperties...), render_map_callback = @safe_cfunction(render_map, Cvoid, (Any,)), render_plot_callback = @safe_cfunction(render_plot, Cvoid, (Any,))) + + @emit showSplash() + isnothing(model) && newsimulation() + @emit closeSplash() + launching = false @emit updateMakie() println("Launched Persefone Desktop.") exec() diff --git a/src/PersefoneDesktop.jl b/src/PersefoneDesktop.jl index 72ab48235775c2a972905de24b4ee972448409fd..f8c58990f49c225fb90e19096bbc097affe917fc 100644 --- a/src/PersefoneDesktop.jl +++ b/src/PersefoneDesktop.jl @@ -23,14 +23,17 @@ using GLMakie, Persefone, Observables, + DataFramesMeta, ResumableFunctions +include("config.jl") include("GUI.jl") precompile(launch, ()) -#precompile(render_map, (Screen)) #what's the input type? +precompile(render_map, (Any,)) #what's the input type? export + configwindow, launch end diff --git a/src/config.jl b/src/config.jl index f8d2c111ac25525f49b2ae1bf80c010016eec5f9..89c5b21e7cfa49a2ae963832d7e1bb4f627a93a1 100644 --- a/src/config.jl +++ b/src/config.jl @@ -16,30 +16,47 @@ const startdateyear = Observable(2022) const enddateday = Observable(31) const enddatemonth = Observable(12) const enddateyear = Observable(2022) -const region = Observable("Jena") +const region = Observable("Jena") #XXX alternatives not yet implemented #const farmmodel = Observable("FieldManager") #XXX not yet implemented -#const targetspecies = Observable() +#const targetspecies = Observable() #TODO not sure how to configure a list? #const insectmodel = Observable() #TODO not sure how to configure a list? const cropmodel = Observable("ALMaSS") #XXX alternatives not yet implemented - -# TODO create config file with fixed options: -# - overwrite = true (?) -# - storedata = true -# - indoutfreq = "daily" -# - popoutfreq = "daily" - +global configproperties = + ["userconfigfile" => userconfigfile, + "outdir" => outdir, + "csvoutput" => csvoutput, + "loglevel" => loglevel, + "processors" => processors, + "seed" => seed, + "startdateday" => startdateday, + "startdatemonth" => startdatemonth, + "startdateyear" => startdateyear, + "enddateday" => enddateday, + "enddatemonth" => enddatemonth, + "enddateyear" => enddateyear, + "region" => region, + "cropmodel" => cropmodel] function writeconfig() println("Wrote config file.") - #TODO + + # TODO create config file with fixed options: + # - overwrite = true (?) + # - storedata = true + # - indoutfreq = "daily" + # - popoutfreq = "daily" + + #TODO create settings dict + settings = Dict{String,Any}() + prepareTOML(settings) + open(joinpath(settings[core.outdir], settings[core.configfile]), "w") do f + TOML.print(f, prepareTOML(model.settings)) + end end function configwindow() - #TODO - @qmlfunction splashscreen - @qmlfunction writeconfig - @qmlfunction newsimulation + #FIXME can't start a new QML engine qmlfile = joinpath(dirname(@__FILE__), "config.qml") loadqml(qmlfile, conf = JuliaPropertyMap("userconfigfile" => userconfigfile, diff --git a/src/config.qml b/src/config.qml index 3acab11631661dfb8eb247571c99dcca9627e749..299db81625df173d556ef0d9d5c4339ed8443b89 100644 --- a/src/config.qml +++ b/src/config.qml @@ -222,7 +222,7 @@ ApplicationWindow { Button { text: "Start new simulation" onClicked: { - //Julia.splashscreen() //FIXME crashes + //TODO show splash Julia.writeconfig() Julia.newsimulation() configWindow.close() diff --git a/src/main.qml b/src/main.qml index 3e6f7e5f8c85e0cfe1eaa27bcfb82813ee5a36b3..b622cdffa4ceccdcda1c097cb880d7c395cd933f 100644 --- a/src/main.qml +++ b/src/main.qml @@ -26,11 +26,25 @@ ApplicationWindow { } Action { text: "&Configure Simulation" - onTriggered: { SetupWindow.show() } //FIXME + onTriggered: { Julia.configwindow() } + } + Action { + text: "&Load Saved State" + onTriggered: { + loadFileChooser.open() + } + } + Action { + text: "&Save Current State" + onTriggered: { + saveFileChooser.open() + } } - Action { text: "&Load Saved State" } - Action { text: "&Save Current State" } MenuSeparator { } + Action { + text: "&Show splash screen" + onTriggered: { splashPopup.show() } + } Action { text: "&Quit" onTriggered: { mainWindow.close() } @@ -43,7 +57,7 @@ ApplicationWindow { onTriggered: { populationGraph.visible = true } } Action { text: "Save &Graphical Output" } - Action { text: "Save CSV Output" } + Action { text: "Save &CSV Output" } } Menu { title: "&Help" @@ -123,6 +137,7 @@ ApplicationWindow { Text { id: dateText text: Julia.datestring() + //width: //TODO } } } @@ -137,6 +152,21 @@ and ecosystems in Europe.\n\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" @@ -151,6 +181,26 @@ Distributed under the MIT license." } } + Popup { + id: splashPopup + parent: Overlay.overlay + //closePolicy: Popup.NoAutoClose + modal: true + + width: 600 + height: 250 + x: Math.round((parent.width - width) / 2) + y: Math.round((parent.height - height) / 2) + + Rectangle { + anchors.fill: parent + + Image { + source: "persefonejl_logo_v3_splash.png" + } + } + } + // set up connections and signals to update the simulation and the display Connections { target: timer @@ -164,6 +214,14 @@ Distributed under the MIT license." mapviewport.update(); //plotviewport.update(); } + signal showSplash() + onShowSplash: { + splashPopup.open() + } + signal closeSplash() + onCloseSplash: { + splashPopup.close() + } } } diff --git a/src/persefonejl_logo_v3_splash.png b/src/persefonejl_logo_v3_splash.png index c2ec0ece3a50394ac3dafa2f5baa0250846a7c61..35048125f676014a3007804dba95f80ab1b01553 100644 Binary files a/src/persefonejl_logo_v3_splash.png and b/src/persefonejl_logo_v3_splash.png differ diff --git a/src/splash.qml b/src/splash.qml index f7f3ca1d05061802791e02c767e5441836e835ad..4108ee5e74f6eacbdab6ed27904696e44f65e483 100644 --- a/src/splash.qml +++ b/src/splash.qml @@ -7,18 +7,28 @@ import QtQuick import QtQuick.Controls import org.julialang -Rectangle { +Popup { + id: splashPopupExt + parent: Overlay.overlay + width: 600 height: 250 - - Image { - source: "persefonejl_logo_v3_splash.png" - fillMode: Image.Stretch - } - Timer { - // Show the splash screen while the model object is initialised - //XXX is 15s a good time here? - interval: 15000; running: true; repeat: false - onTriggered: parent.Window.window.close() - } + + x: Math.round((parent.width - width) / 2) + y: Math.round((parent.height - height) / 2) + + Rectangle { + anchors.fill: parent + + Image { + source: "persefonejl_logo_v3_splash.png" + fillMode: Image.Stretch + } + Timer { + // Show the splash screen while the model object is initialised + //XXX is 15s a good time here? + interval: 15000; running: true; repeat: false + onTriggered: parent.Window.window.close() + } + } }