diff --git a/src/core/landscape.jl b/src/core/landscape.jl index eba5b73b9b86daff20e109cf9cb3522e9d58ff94..3ea00c3f322cd1159c1d4f85e6a3fc89a839b0bb 100644 --- a/src/core/landscape.jl +++ b/src/core/landscape.jl @@ -84,9 +84,9 @@ function updateevents!(model::AgentBasedModel) end """ - createevent(model, pos, name, duration=1) + createevent!(model, pixels, name, duration=1) -Add a farm event to the specified pixels for a given duration. +Add a farm event to the specified pixels (a vector of position tuples) for a given duration. """ function createevent!(model::AgentBasedModel, pixels::Vector{Tuple{Int64,Int64}}, name::EventType, duration::Int64=1) @@ -97,7 +97,7 @@ function createevent!(model::AgentBasedModel, pixels::Vector{Tuple{Int64,Int64}} end """ - landcover(model, position) + landcover(position, model) Return the land cover class at this position (utility wrapper). """ @@ -106,7 +106,7 @@ function landcover(pos::Tuple{Int64,Int64}, model::AgentBasedModel) end """ - farmplot(model, position) + farmplot(position, model) Return the farm plot at this position, or nothing if there is none (utility wrapper). """ @@ -115,27 +115,60 @@ function farmplot(pos::Tuple{Int64,Int64}, model::AgentBasedModel) model[model.landscape[pos...].fieldid] end + """ - distanceto(model, pos, habitattype) + distanceto(pos, model, habitatdescriptor) + +Calculate the distance from the given location to the closest location matching the +habitat descriptor function. Caution: can be computationally expensive! +""" +function distanceto(pos::Tuple{Int64,Int64}, model::AgentBasedModel, habitatdescriptor::Function) + (habitatdescriptor(pos, model)) && (return 0) + dist = 1 + width, height = size(model.landscape) + while dist <= width || dist <= height + # 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)) + for x in minx:maxx + (y1 > 0 && habitatdescriptor((x,y1), model)) && (return dist) + (y2 <= height && habitatdescriptor((x,y2), model)) && (return dist) + end + # check the left and right bounds of the enclosing rectangle + x1 = pos[1] - dist + x2 = pos[1] + dist + 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 dist) + (x2 <= width && habitatdescriptor((x2,y), model)) && (return dist) + end + dist += 1 + end + return Inf +end + +""" + distanceto(pos, model, habitattype) Calculate the distance from the given location to the closest habitat of the specified type. Caution: can be computationally expensive! """ function distanceto(pos::Tuple{Int64,Int64}, model::AgentBasedModel, habitattype::LandCover) - #XXX can I make this check for both land cover type and crop type? - # (Or even take a full habitat descriptor?) - #TODO + # can't use @habitat here because nature.jl is loaded later than this file + distanceto(pos, model, function(p,m) landcover(p,m) == habitattype end) end """ - distancetoedge(model, pos) + distancetoedge(pos, model) Calculate the distance from the given location to the closest neighbouring habitat. Caution: can be computationally expensive! """ function distancetoedge(pos::Tuple{Int64,Int64}, model::AgentBasedModel) - lc = landcover(model, pos) - dist = 1 - #TODO + # can't use @habitat here because nature.jl is loaded later than this file + distanceto(pos, model, function(p,m) landcover(p,m) != landcover(pos, model) end) end diff --git a/test/landscape_tests.jl b/test/landscape_tests.jl index 11422d28b3efd2c28a47dbb33fd1a1b31c6c58f0..c54ca64785c93d92db9ecdd2098852c60b1877f6 100644 --- a/test/landscape_tests.jl +++ b/test/landscape_tests.jl @@ -13,16 +13,16 @@ Persephone.initfields!(model) # these tests are specific to the Jena maps @test size(model.landscape) == (1754, 1602) - @test Persephone.landcover(model, (100,100)) == Persephone.forest - @test Persephone.landcover(model, (300,1)) == Persephone.soil - @test Persephone.landcover(model, (500,1)) == Persephone.nodata - @test Persephone.landcover(model, (400,400)) == Persephone.grass - @test Persephone.landcover(model, (800,800)) == Persephone.agriculture - @test Persephone.landcover(model, (1100,1100)) == Persephone.builtup + @test Persephone.landcover((100,100), model) == Persephone.forest + @test Persephone.landcover((300,1), model) == Persephone.soil + @test Persephone.landcover((500,1), model) == Persephone.nodata + @test Persephone.landcover((400,400), model) == Persephone.grass + @test Persephone.landcover((800,800), model) == Persephone.agriculture + @test Persephone.landcover((1100,1100), model) == Persephone.builtup @test Persephone.countfields(model) == 2092 @test Persephone.averagefieldsize(model) == 5.37 @test count(f -> ismissing(f.fieldid), model.landscape) == 1685573 - @test length(Persephone.farmplot(model, (800,800)).pixels) == 4049 + @test length(Persephone.farmplot((800,800), model).pixels) == 4049 end @testset "Event system" begin @@ -49,3 +49,26 @@ end @test model.landscape[2,1].events == [] @test model.landscape[2,2].events == [] end + +@testset "Landscape functions" begin + # initialise a simple 6x6 test landscape + landscape = Matrix{Pixel}(undef, 6, 6) + for x in 1:6 + for y in 1:6 + (x in (1:2) || y in (1:4)) ? lc = Persephone.forest : lc = Persephone.grass + landscape[x,y] = Pixel(lc, 0, []) + end + end + landscape[6,4] = Pixel(Persephone.water, 0, []) + space = GridSpace(size(landscape), periodic=false) + properties = Dict{Symbol,Any}(:landscape=>landscape) + model = AgentBasedModel(Animal, space, properties=properties, warn=false) + # test the distance functions + @test Persephone.distanceto((2,3), model, Persephone.forest) == 0 + @test Persephone.distanceto((2,3), model, Persephone.grass) == 2 + @test Persephone.distanceto((2,3), model, Persephone.water) == 4 + @test Persephone.distanceto((2,3), model, Persephone.soil) == Inf + @test Persephone.distancetoedge((1,1), model) == 4 + @test Persephone.distancetoedge((4,4), model) == 1 + @test Persephone.distancetoedge((6,6), model) == 2 +end