From 8a958fd968be09936bbb29d2cacdae61ce6d7337 Mon Sep 17 00:00:00 2001
From: Daniel Vedder <daniel.vedder@idiv.de>
Date: Thu, 23 May 2024 12:54:31 +0200
Subject: [PATCH] Added @here and nagents(), fixed farmplot() and directionto()

---
 src/core/simulation.jl |  9 +++++++++
 src/nature/macros.jl   | 16 +++++++++++++---
 src/world/landscape.jl | 37 ++++++++++++++++++++++++-------------
 3 files changed, 46 insertions(+), 16 deletions(-)

diff --git a/src/core/simulation.jl b/src/core/simulation.jl
index c9ed15a..b4a0228 100644
--- a/src/core/simulation.jl
+++ b/src/core/simulation.jl
@@ -29,6 +29,15 @@ mutable struct AgricultureModel <: SimulationModel
     events::Vector{FarmEvent}
 end
 
+"""
+    nagents(model)
+
+Return the total number of agents in a model object.
+"""
+function nagents(model::AgricultureModel)
+    length(model.animals)+length(model.farmers)+length(model.farmplots)
+end
+    
 """
     stepagent!(agent, model)
 
diff --git a/src/nature/macros.jl b/src/nature/macros.jl
index ccb82d3..fb2f107 100644
--- a/src/nature/macros.jl
+++ b/src/nature/macros.jl
@@ -124,7 +124,7 @@ is explicitly defined by the user in the species definition block.
 """
 macro phase(species, phase, body)
     quote
-        Core.@__doc__ function $(esc(phase))($(esc(:self))::$(species),
+        Core.@__doc__ function $(esc(phase))($(esc(:self))::$(esc(species)),
                                              $(esc(:model))::SimulationModel)
             $(esc(:pos)) = $(esc(:self)).pos # needed for landscape macros
             $(esc(body))
@@ -145,7 +145,6 @@ can thus use all macros available in [`@phase`](@ref).
 """
 macro create(species, body)
     quote
-        #XXX species are created/referenced as Persefone.<speciesname>, is this relevant?
         Core.@__doc__ function $(esc(:create!))($(esc(:self))::$(esc(species)),
                                                 $(esc(:model))::SimulationModel)
             $(esc(:pos)) = $(esc(:self)).pos # needed for landscape macros
@@ -176,10 +175,21 @@ macro isalive(id)
     :(isalive($(esc(id)), $(esc(:model))))
 end
 
+"""
+    @here()
+
+Return the landscape pixel of this animal's current location.
+This can only be used nested within [`@phase`](@ref).
+"""
+macro here()
+    :($(esc(:model)).landscape[$(esc(:self)).pos...])
+end
+
 """
     @setphase(newphase)
 
-Switch this animal over to a different phase. This can only be used nested within [`@phase`](@ref).
+Switch this animal over to a different phase.
+This can only be used nested within [`@phase`](@ref).
 """
 macro setphase(newphase)
     :($(esc(:self)).phase = $(newphase))
diff --git a/src/world/landscape.jl b/src/world/landscape.jl
index ac6cc80..f310cc6 100644
--- a/src/world/landscape.jl
+++ b/src/world/landscape.jl
@@ -116,11 +116,8 @@ end
 Return the farm plot at this position, or nothing if there is none (utility wrapper).
 """
 function farmplot(pos::Tuple{Int64,Int64}, model::SimulationModel)
-    if ismissing(model.landscape[pos...].fieldid)
-        nothing
-    else
-        model[model.landscape[pos...].fieldid]
-    end
+    id = model.landscape[pos...].fieldid
+    ismissing(id) ? nothing : model.farmplots[id]
 end
 
 """
@@ -131,19 +128,22 @@ habitat descriptor function. Returns a coordinate tuple (target - position), or
 if no matching habitat is found. Caution: can be computationally expensive!
 """
 function directionto(pos::Tuple{Int64,Int64}, model::SimulationModel, habitatdescriptor::Function)
-    #TODO test
+    #XXX split this up into a findnearest() and a directionto() function?
+    # search in expanding rectangles around the origin
     (habitatdescriptor(pos, model)) && (return (0,0))
     dist = 1
     width, height = size(model.landscape)
-    while dist <= width || dist <= height
+    while dist <= width || dist <= height #XXX is there a quicker method?
         # check the upper and lower bounds of the enclosing rectangle
         y1 = pos[2] - dist
         y2 = pos[2] + dist
         minx = maximum((pos[1]-dist, 1))
         maxx = minimum((pos[1]+dist, width))
+        #XXX alternately, we could check individually whether y1 and y2 are in bounds, and loop
+        # over x for each separately (not sure if that is quicker, but it may be)
         for x in minx:maxx
-            (y1 > 0 && habitatdescriptor((x,y1), model)) && (return (x, y1))
-            (y2 <= height && habitatdescriptor((x,y2), model)) && (return (x, y2))
+            (y1 > 0 && habitatdescriptor((x,y1), model)) && (return (x, y1).-pos)
+            (y2 <= height && habitatdescriptor((x,y2), model)) && (return (x, y2).-pos)
         end
         # check the left and right bounds of the enclosing rectangle
         x1 = pos[1] - dist
@@ -151,14 +151,26 @@ function directionto(pos::Tuple{Int64,Int64}, model::SimulationModel, habitatdes
         miny = maximum((pos[2]-dist+1, 1))
         maxy = minimum((pos[2]+dist-1, height))
         for y in miny:maxy
-            (x1 > 0 && habitatdescriptor((x1,y), model)) && (return (x1,y))
-            (x2 <= width && habitatdescriptor((x2,y), model)) && (return (x2,y))
+            (x1 > 0 && habitatdescriptor((x1,y), model)) && (return (x1,y).-pos)
+            (x2 <= width && habitatdescriptor((x2,y), model)) && (return (x2,y).-pos)
         end
         dist += 1
     end
     return nothing
 end
 
+"""
+    directionto(pos, model, habitattype)
+
+Calculate the direction from the given location to the closest habitat of the specified type.
+Returns a coordinate tuple (target - position), or nothing if no matching habitat is found.
+Caution: can be computationally expensive!
+"""
+function directionto(pos::Tuple{Int64,Int64}, model::SimulationModel, habitattype::LandCover)
+    # can't use @habitat here because macros.jl is loaded later than this file
+    directionto(pos, model, function(p,m) landcover(p,m) == habitattype end)
+end
+
 """
     distanceto(pos, model, habitatdescriptor)
 
@@ -166,10 +178,9 @@ Calculate the distance from the given location to the closest location matching
 habitat descriptor function. Caution: can be computationally expensive!
 """
 function distanceto(pos::Tuple{Int64,Int64}, model::SimulationModel, habitatdescriptor::Function)
-    #TODO test
     target = directionto(pos, model, habitatdescriptor)
     isnothing(target) && return Inf
-    return maximum(abs.(target.-pos))
+    return maximum(abs.(target))
 end
 
 """
-- 
GitLab