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()
+		}
+	}
 }