import World from './World.js'
import GeoWorld from './GeoWorld.js'
import Patches from './Patches.js'
import Patch from './Patch.js'
import Turtles from './Turtles.js'
import Turtle from './Turtle.js'
import Links from './Links.js'
import Link from './Link.js'
/**
* Class Model is the primary interface for modelers, integrating
* the Patches/Patch Turtles/Turtle and Links/Link AgentSets .. i.e.:
*
* - model.Patches: an array {@link Patches} of {@link Patch} instances
* - model.Turtles: an array {@link Turtles} of {@link Turtle} instances
* - model.Links: an array {@link Links} of {@link Link} instances
* - model.breed: a sub-array of any of the three above.
* - All of which are subclasses of {@link AgentSet}.
*
* Convention: Three abstract methods are provided by the modeler
*
* - Startup(): (Optional) Called once to import images, data etc
* - Setup(): Called to initialize the model state.
* - Step(): Step the model. Will advance ticks.
*
* @param {Object|World} [worldOptions=World.defaultOptions()]
* Can be Object of min/max X,Y,Z values or an instance of World or GeoWorld
*/
// * @param {boolean} [autoTick=true] Automatically advancee tick count each step if true
class Model {
world
patches
turtles
links
ticks
constructor(worldOptions = World.defaultOptions()) {
this.resetModel(worldOptions)
this.setAutoTick(true)
this.setGeometry('heading')
}
initAgentSet(name, AgentsetClass, AgentClass) {
this[name] = new AgentsetClass(this, AgentClass, name)
}
options2world(worldOptions) {
return worldOptions.bbox
? new GeoWorld(worldOptions)
: new World(worldOptions)
}
resetModel(worldOptions) {
this.ticks = 0
this.world =
worldOptions.maxXcor === undefined // is already a world object
? this.options2world(worldOptions)
: worldOptions
// Base AgentSets setup here. Breeds handled by setup
this.initAgentSet('patches', Patches, Patch)
this.initAgentSet('turtles', Turtles, Turtle)
this.initAgentSet('links', Links, Link)
}
/**
* Resets model to initial state w/ new Patches, Turtles, Links.
* The worldOptions will default to initial values but can be
* changed by modeler. Setup() often called after reset() to
* re-initialize the model.
*
* @param {Object|World} [worldOptions=this.world] World object
*/
reset(worldOptions = this.world) {
this.resetModel(worldOptions)
}
/**
* Increment the tick cound. Not needed if autoTick true, the default
*/
tick() {
this.ticks++
}
/**
* An abstract method to perform one-time initialization.
*
* @abstract
*/
async startup() {}
/**
* An abstract method for initializing the model
*
* Note: can be used with reset(). This will reinitialize
* the Patches, Turtles, Links for re-running the model
* * reset()
* * setup()
*
* @abstract
*/
setup() {}
/**
* An abstract method to run the model one step.
*
* @abstract
*/
step() {}
// A trick to auto advance ticks every step
setAutoTick(autoTick = true) {
const isAutoTick = this.hasOwnProperty('step')
if (autoTick) {
if (isAutoTick) return
this.step0 = this.step
this.step = this.stepAndTick
} else {
delete this.step
delete this.step0
}
}
stepAndTick() {
this.step0()
// super.step()
this.tick()
}
/**
* Create breeds (sub-arrays) of Patches. Used in the Exit model:
* * this.patchBreeds('exits inside wall')
*
* @param {string} breedNames A string of space separated breeds names
*/
patchBreeds(breedNames) {
for (const breedName of breedNames.split(' ')) {
this[breedName] = this.patches.newBreed(breedName)
}
}
/**
* Create breeds (sub-arrays) of Turtles. Used in Wallfollower model:
* * this.turtleBreeds('lefty righty')
*
* @param {string} breedNames A string of space separated breeds names
*/
turtleBreeds(breedNames) {
for (const breedName of breedNames.split(' ')) {
this[breedName] = this.turtles.newBreed(breedName)
}
}
/**
* Create breeds (sub-arrays) of Links. Used in Roads model:
* * this.linkBreeds('trips')
*
* @param {string} breedNames A string of space separated breeds names
*/
linkBreeds(breedNames) {
for (const breedName of breedNames.split(' ')) {
this[breedName] = this.links.newBreed(breedName)
}
}
setGeometry(name = 'heading') {
const geometry = geometries[name]
if (!geometry) throw Error(`setGeometry: ${name} geometry not defined`)
Object.assign(this, geometry)
}
}
// Five geometry functions converting to the JavaScript
// radians: radians counter-clockwise from the x-axis.
// degrees: degrees counter-clockwise from the x-axis.
// heading: "clock geometry", clockwise degrees from y axis
// Add mod2pi & mod360?
const toDeg = 180 / Math.PI
const toRad = Math.PI / 180
const geometries = {
radians: {
toRads: rads => rads,
fromRads: rads => rads,
toAngleRads: rads => rads,
fromAngleRads: rads => rads,
toCCW: angle => angle,
},
degrees: {
toRads: deg => deg * toRad,
fromRads: rads => rads * toDeg,
toAngleRads: deg => deg * toRad,
fromAngleRads: rads => rads * toDeg,
toCCW: angle => angle,
},
heading: {
toRads: deg => (90 - deg) * toRad,
fromRads: rads => 90 - rads * toDeg,
toAngleRads: deg => deg * toRad,
fromAngleRads: rads => rads * toDeg,
toCCW: angle => -angle,
},
}
export default Model