# An **AgentSet** is an array, along with a class, agentClass, whose instances
# are the items of the array. Instances of the class are created
# by the `create` factory method of an AgentSet.
#
# It is a subclass of `Array` and is the base class for
# `Patches`, `Turtles`, and `Links`. An AgentSet keeps track of all
# its created instances. It also provides, much like the **Util**
# module, many methods shared by all subclasses of AgentSet.
#
# A model contains three agentsets:
#
# * `patches`: the model's "world" grid
# * `turtles`: the model's turtles living on the patches
# * `links`: the network links connecting agent pairs
#
# See NetLogo [documentation](http://ccl.northwestern.edu/netlogo/docs/)
# for explanation of the overall semantics of Agent Based Modeling
# used by AgentSets as well as Patches, Turtles, and Links.
#
# Note: subclassing `Array` can be dangerous and we may have to convert
# to a different style. See Trevor Burnham's [comments](http://goo.gl/Lca8g)
# but thus far we've resolved all related problems.
#
# Because we are an array subset, @[i] == this[i] == agentset[i]
class AgentSet extends Array
# ### Static members
# `asSet` is a static wrapper function converting an array of agents into
# an `AgentSet` .. except for the ID which only impacts the add method.
# It is primarily used to turn a comprehension into an AgentSet instance
# which then gains access to all the methods below. Ex:
#
# evens = (a for a in @model.turtles when a.id % 2 is 0)
# ABM.AgentSet.asSet(evens)
# randomEven = evens.oneOf()
@asSet: (a, setType = AgentSet) -> #(a, setType = ABM.AgentSet)
a.__proto__ = setType.prototype ? setType.constructor.prototype # setType.__proto__
a.model=a[0].model if a[0]? # Used by geometric methods
a
# In the examples below, we'll use an array of primitive agent objects
# with three fields: id, x, y.
#
# AS = for i in [1..5] # long form comprehension
# {id:i, x:u.randomInt(10), y:u.randomInt(10)}
# ABM.AgentSet.asSet AS # Convert AS to AgentSet in place
# [{id:1,x:0,y:1}, {id:2,x:8,y:0}, {id:3,x:6,y:4},
# {id:4,x:1,y:3}, {id:5,x:1,y:1}]
# ### Constructor and add/remove agents.
# Create an empty `AgentSet` and initialize the `ID` counter for add().
# If mainSet is supplied, the new agentset is a sub-array of mainSet.
# This sub-array feature is how breeds are managed, see class `Model`
constructor: (@model, @agentClass, @name, @mainSet) ->
super(0) # doesn't yield empty array if already instances in the mainSet
u.mixin(@, new Evented())
@breeds = [] unless @mainSet?
@agentClass::breed = @ # let the breed know I'm it's agentSet
@agentClass::model = @model # let the breed know its model
@ownVariables = [] # keep list of user variables
@ID = 0 unless @mainSet? # Do not set ID if I'm a subset
# Abstract method used by subclasses to create and add their instances.
create: ->
# Add an agent to the list. Only used by agentset factory methods. Adds
# the `id` property to all agents. Increment `ID`.
# Returns the object for chaining. The set will be sorted by `id`.
#
# By "agent" we mean an instance of `Patch`, `Turtle` and `Link` and their breeds
add: (o) ->
if @mainSet? then @mainSet.add o else o.id = @ID++
@push o; o
# Remove an agent from the agentset, returning the agentset.
# Note this does not change ID, thus an
# agentset can have gaps in terms of their ids. Assumes set is
# sorted by `id`. If the set is one created by `asSet`, and the original
# array is unsorted, simply call `sortById` first, see `sortById` below.
#
# AS.remove(AS[3]) # [{id:0,x:0,y:1}, {id:1,x:8,y:0},
# {id:2,x:6,y:4}, {id:4,x:1,y:1}]
remove: (o) ->
u.removeItem @mainSet, o if @mainSet?
u.removeItem @, o
@
# Set/get the default value of an agent class
setDefault: (name, value) -> @agentClass::[name] = value
getDefault: (name) -> @agentClass::[name]
# Declare variables of an agent class.
# Vars = a string of space separated names or an array of name strings
# Return agentset.
own: (vars) -> # maybe not set default if val is null?
for name in vars.split(" ")
@setDefault name, null
@ownVariables.push name
@
# Move an agent from its AgentSet/breed to be in this AgentSet/breed.
# REMIND: match NetLogo sematics in terms of own variables.
setBreed: (a) -> # change agent a to be in this breed
u.removeItem a.breed, a, "id" if a.breed.mainSet?
u.insertItem @, a, "id" if @mainSet?
proto = a.__proto__ = @agentClass.prototype
delete a[k] for own k,v of a when proto[k]?
a
# Return all agents that are not of the given breeds argument.
# Breeds is a string of space separated names:
# @patches.exclude "roads houses"
exclude: (breeds) -> # Not used, remove??
breeds = breeds.split(" ")
@asSet (o for o in @ when o.breed.name not in breeds)
# Remove adjacent duplicates, by reference, in a sorted agentset.
# Use `sortById` first if agentset not sorted.
#
# as = (AS.oneOf() for i in [1..4]) # 4 random agents w/ dups
# ABM.AgentSet.asSet as # [{id:1,x:8,y:0}, {id:0,x:0,y:1},
# {id:0,x:0,y:1}, {id:2,x:6,y:4}]
# as.sortById().uniq() # [{id:0,x:0,y:1}, {id:1,x:8,y:0},
# {id:2,x:6,y:4}]
uniq: -> u.uniq(@)
# The static `ABM.AgentSet.asSet` as a method.
# Used by agentset methods creating new agentsets.
asSet: (a, setType = AgentSet) -> AgentSet.asSet a, setType # setType = AgentSet
# Is the given array an agentset of the given type?
# It does not use the prototype chain, so
#
# isSet(turtles, "AgentSet")
#
# is false. Current names: AgentSet, Turtles, Patches, Links.
# Default name is "AgentSet", good test for derived sets using asSet()
isSet: (name = "AgentSet") -> @constructor.name is name
# Similar for above but includes breeds of Turtles, Patches, Links too
# isBreed("Turtles") returns true for an agent that isn't a breed
isBreed: (name) ->
# @isSet(name) or (@agentClass?.name is name)
if @agentClass? then (@agentClass.name is name) else @isSet(name)
# Similar to above but sorted via `id`.
asOrderedSet: (a) -> @asSet(a).sortById()
# Return string representative of agentset.
toString: -> "["+(a.toString() for a in @).join(", ")+"]"
# ### Property Utilities
# Property access, also useful for debugging
# Return an array of a property of the agentset
#
# AS.getProp "x" # [0, 8, 6, 1, 1]
getProp: (prop) -> u.aProp(@, prop)
# Return an array of agents with the property equal to the given value
#
# AS.getPropWith "x", 1
# [{id:4,x:1,y:3},{id:5,x:1,y:1}]
getPropWith: (prop, value) -> @asSet (o for o in @ when o[prop] is value)
# Set the property of the agents to a given value. If value
# is an array, its values will be used, indexed by agentSet's index.
# This is generally used via: getProp, modify results, setProp
#
# # increment x for agents with x=1
# AS1 = ABM.AgentSet.asSet AS.getPropWith("x",1)
# AS1.setProp "x", 2 # {id:4,x:2,y:3},{id:5,x:2,y:1}
#
# Note this changes the last two objects in the original AS above
setProp: (prop, value) ->
if u.isArray value
then o[prop] = value[i] for o,i in @; @
else o[prop] = value for o in @; @
# Get the agent with the min/max prop value in the agentset
#
# min = AS.minProp "y" # 0
# max = AS.maxProp "y" # 4
maxProp: (prop) -> u.aMax @getProp(prop)
minProp: (prop) -> u.aMin @getProp(prop)
histOfProp: (prop, bin=1) -> u.histOf @, bin, prop
# ### Array Utilities, often from Util
# Randomize the agentset
#
# AS.shuffle(); AS.getProp "id" # [3, 2, 1, 4, 5]
shuffle: -> u.shuffle @
# Sort the agentset by the agent's `id`.
#
# AS.shuffle(); AS.getProp "id" # [3, 2, 1, 4, 5]
# AS.sortById(); AS.getProp "id" # [1, 2, 3, 4, 5]
sortById: -> u.sortBy @, "id"
# Make a copy of an agentset, return as new agentset.
# NOTE: does *not* duplicate the objects, simply creates a new agentset
# with references to the same agents. Ex: create a randomized version of AS
# but without mangling AS itself:
#
# as = AS.clone().shuffle()
# AS.getProp "id" # [1, 2, 3, 4, 5]
# as.getProp "id" # [2, 4, 0, 1, 3]
clone: -> @asSet u.clone @
# Return the last agent in the agentset
#
# AS.last().id # l5
# l=AS.last(); p=[l.x,l.y] # [1,1]
last: -> u.last @
# Returns true if the agentset has any agents
#
# AS.any() # true
# AS.getPropWith("x", 99).any() #false
any: -> u.any @
# Return an agentset without given agent a
#
# as = AS.clone().other(AS[0])
# as.getProp "id" # [1, 2, 3, 4]
other: (a) ->
# If simple agentset derived by functions returning agentsets, use
# remove. Otherwise iterate over myself.
if @isSet()
u.removeItem @, a
else
@asSet (o for o in @ when o isnt a)
# Return random agent in agentset
#
# AS.oneOf() # {id:2,x:6,y:4}
oneOf: -> u.oneOf @
# Return agentset made of n distinct agents
#
# AS.nOf(3) # [{id:0,x:0,y:1}, {id:4,x:1,y:1}, {id:1,x:8,y:0}]
nOf: (n) -> @asSet u.nOf @, n
# Return agent when f(o) min/max in agentset. If multiple agents have
# min/max value, return the first. Error if agentset empty.
# If f is a string, return element with min/max value of that property.
# If "valueToo" then return an array of the agent and the value.
#
# AS.minOneOf("x") # {id:0,x:0,y:1}
# AS.maxOneOf((a)->a.x+a.y, true) # {id:2,x:6,y:4},10
minOneOf: (f, valueToo=false) -> u.minOneOf @, f, valueToo
maxOneOf: (f, valueToo=false) -> u.maxOneOf @, f, valueToo
# ### Drawing
# For agentsets whose agents have a `draw` method.
# Clears the graphics context (transparent), then
# calls each agent's draw(ctx) method.
draw: (ctx) ->
u.clearCtx(ctx); o.draw(ctx) for o in @ when not o.hidden; null
# Show/Hide all of an agentset or breed. Does not redraw.
# To show/hide an individual object, set its prototype: o.hidden = bool
show: -> o.hidden = false for o in @
hide: -> o.hidden = true for o in @
# ### Topology
# For patches & turtles, which have x,y. See Util doc.
# Typically a subclass uses a rect/quadtree array to minimize
# the size, then uses asSet(array) to call inRadius or inCone
#
inRect: (o, radius) ->
rect = [];
minX = o.x - radius; maxX = o.x + radius
minY = o.y - radius; maxY = o.y + radius
patches = @model.patches
# Is the o +/- radius entirely inside the patches?
outside = (minX < patches.minX) or (maxX > patches.maxX) or
(minY < patches.minY) or (maxY > patches.maxY)
checkTorus = patches.isTorus and outside
for a in @
x = a.x; y = a.y # agent's x,y
if checkTorus # Adjust torus x,y if appropriate
if xmaxX then x -= patches.numX
if ymaxY then y -= patches.numY
# Test x,y inside rect
rect.push a if (minX <= x <= maxX and minY <= y <= maxY)
@asSet rect
# Return all agents in agentset within d distance from given object.
# By default excludes the given object. Uses linear/torus distance
# depending on patches.isTorus, and patches width/height if needed.
inRadius: (o, radius) ->
d2 = radius * radius; x = o.x; y = o.y
if @model.patches.isTorus
w = @model.patches.numX; h = @model.patches.numY
@asSet (a for a in @ when \
u.torusSqDistance(x, y, a.x, a.y, w, h) <= d2 )
else
@asSet (a for a in @ when \
u.sqDistance(x, y, a.x, a.y) <= d2)
# As above, but also limited to the angle `angle` around
# a `heading` from object `o`.
inCone: (o, radius, angle, heading) ->
x = o.x; y = o.y
if @model.patches.isTorus
w = @model.patches.numX; h = @model.patches.numY
@asSet (a for a in @ when \
u.inTorusCone(radius, angle, heading, x, y, a.x, a.y, w, h))
else
@asSet (a for a in @ when \
u.inCone(radius, angle, heading, x, y, a.x, a.y))
# ### Debugging
# Useful in console.
# Also see [CoffeeConsole](http://goo.gl/1i7bd) Chrome extension.
# Similar to NetLogo ask & with operators.
# Allows functions as strings. Use:
#
# AS.getProp("x") # [1, 8, 6, 2, 2]
# AS.with("o.x<5").ask("o.x=o.x+1")
# AS.getProp("x") # [2, 8, 6, 3, 3]
#
# myModel.turtles.with("o.id<100").ask("o.color=[255,0,0]")
ask: (f) ->
eval("f=function(o){return "+f+";}") if u.isString f
f(o) for o in @; @
with: (f) ->
eval("f=function(o){return "+f+";}") if u.isString f
@asSet (o for o in @ when f(o))
# The example agentset AS used in the code fragments was made like this,
# slightly more useful than shown above due to the toString method.
#
# class XY
# constructor: (@x,@y) ->
# toString: -> "{id:#{@id},x:#{@x},y:#{@y}}"
# @AS = new ABM.AgentSet # @ => global name space
#
# The result of
#
# AS.add new XY(u.randomInt(10), u.randomInt(10)) for i in [1..5]
#
# random run, captured so we can reuse.
#
# AS.add new XY(pt...) for pt in [[0,1],[8,0],[6,4],[1,3],[1,1]]