Model.js

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.
 */

class Model {
    world
    patches
    turtles
    links
    ticks

    /**
     * Creates an instance of Model.
     * The worldOptions define the coordinate system for this model
     * The  {@link World} options sets xMin, xMax, yMin, yMax
     * and when using 3D: zMin, zMax
     *
     * Fine point: Can also be an instance of World or GeoWorld
     *
     * @constructor
     * @param {Object} [worldOptions=World.defaultOptions()]
     */
    constructor(worldOptions = World.defaultOptions()) {
        this.initModel(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)
    }
    /**
     * Initialize model to initial state w/ new Patches, Turtles, Links.
     * The worldOptions will default to initial values but can be
     * changed by modeler.
     *
     * @param {Object|World} [worldOptions=this.world] World object
     */
    initModel(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)
    }

    /**
     * Reset the model by clearing the turtles, setting ID & ticks to 0
     * and calling setup()
     *
     * @param {boolean} [callSetup=true]
     */
    reset(callSetup = true) {
        this.turtles.clear()
        this.turtles.ID = 0
        this.ticks = 0
        if (callSetup) this.setup()
    }

    /**
     * Increment the tick cound. Not needed if autoTick true, the default
     */
    tick() {
        this.ticks++
    }

    /**
     * An abstract method to perform one-time initialization.
     * Subclasses provide their versions of this to import data.
     *
     * @abstract
     */
    async startup() {}

    /**
     * An abstract method for initializing the model
     * Subclasses provide their version of this to initialice the model
     *
     * @abstract
     */
    setup() {}

    /**
     * An abstract method to run the model one step.
     *
     * @abstract
     */
    step() {}

    // A trick to auto advance ticks every step using a Proxy
    setAutoTick(autoTick = true) {
        const isAutoTick = !!this.stepTarget
        if (autoTick) {
            if (isAutoTick) return
            this.stepTarget = this.step
            this.step = new Proxy(this.stepTarget, {
                apply: (target, thisArg, args) => {
                    this.stepTarget()
                    this.tick()
                    // console.log('ticks', this.ticks)
                },
            })
        } else {
            this.step = this.stepTarget
            delete this.stepTarget
        }
    }

    /**
     * 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