import * as util from './utils.js'
// import AgentArray from './AgentArray.js'
import AgentList from './AgentList.js'
import AgentSet from './AgentSet.js'
import DataSet from './DataSet.js'
/**
* Patches are the world other AgentSets live on.
* They define a coord system from the Model's World values:
* minX, maxX, minY, maxY, (minZ, maxZ) (z optional)
*
* Patches form a grid of Patch objects which can store world data
* (elevation, fires, ant pheromones, buildings, roads, gis spatial data, water and so on)
*
* Created by class Model. Used by modeler in their Model subclass
*/
class Patches extends AgentSet {
/**
* Creates an instance of Patches.
* @param {Model} model An instance of class Model
* @param {Patch} AgentClass The Patch class managed by Patches
* @param {string} name Name of the AgentSet
*/
constructor(model, AgentClass, name, baseSet = null) {
// AgentSet sets these variables:
// model, name, baseSet, world: model.world, agentProto: new AgentClass
// REMIND: agentProto: defaults, agentSet, world, [name]=agentSet.baseSet
super(model, AgentClass, name, baseSet)
// Skip if a breedSet (don't rebuild patches!).
if (this.isBreedSet()) return
this.populate()
// this.setPixels()
this.labels = [] // sparse array for labels
}
// Set up all the patches.
populate() {
util.repeat(this.model.world.numX * this.model.world.numY, i => {
this.addAgent() // Object.create(this.agentProto))
})
}
// Return the offsets from a patch for its 8 element neighbors.
// Specialized to be faster than inRect below.
neighborsOffsets(x, y) {
const { minX, maxX, minY, maxY, numX } = this.model.world
if (x === minX) {
if (y === minY) return [-numX, -numX + 1, 1]
if (y === maxY) return [1, numX + 1, numX]
return [-numX, -numX + 1, 1, numX + 1, numX]
}
if (x === maxX) {
if (y === minY) return [-numX - 1, -numX, -1]
if (y === maxY) return [numX, numX - 1, -1]
return [-numX - 1, -numX, numX, numX - 1, -1]
}
if (y === minY) return [-numX - 1, -numX, -numX + 1, 1, -1]
if (y === maxY) return [1, numX + 1, numX, numX - 1, -1]
return [-numX - 1, -numX, -numX + 1, 1, numX + 1, numX, numX - 1, -1]
}
// Return the offsets from a patch for its 4 element neighbors (N,S,E,W)
neighbors4Offsets(x, y) {
const numX = this.model.world.numX
return this.neighborsOffsets(x, y).filter(
n => Math.abs(n) === 1 || Math.abs(n) === numX
) // slightly faster
// .filter((n) => [1, -1, numX, -numX].indexOf(n) >= 0)
// .filter((n) => [1, -1, numX, -numX].includes(n)) // slower than indexOf
}
/**
* Return the 8 patch
* ["Moore" neighbors](https://en.wikipedia.org/wiki/Moore_neighborhood)
* of the given patch.
* Will be less than 8 on the edge of the patches
*
* @param {Patch} patch a Patch instance
* @returns {AgentList} An array of the neighboring patches
*/
neighbors(patch) {
const { id, x, y } = patch
const offsets = this.neighborsOffsets(x, y)
// const as = new AgentArray(offsets.length)
const as = new AgentList(this.model, offsets.length)
offsets.forEach((o, i) => {
as[i] = this[o + id]
})
return as
}
/**
* Return the 4 patch
* ["Van Neumann" neighbors](https://en.wikipedia.org/wiki/Von_Neumann_neighborhood)
* of the given patch.
* Will be less than 4 on the edge of the patches
*
* @param {Patch} patch a Patch instance
* @returns {AgentList} An array of the neighboring patches
*/
neighbors4(patch) {
const { id, x, y } = patch
const offsets = this.neighbors4Offsets(x, y)
// const as = new AgentArray(offsets.length)
const as = new AgentList(this.model, offsets.length)
offsets.forEach((o, i) => {
as[i] = this[o + id]
})
return as
}
/**
* Assign a DataSet's values into the patches as the given property name
*
* @param {DataSet} dataSet An instance of [DataSet](./DataSet.html)
* @param {string} property A Patch property name
* @param {boolean} [useNearest=false] Resample to nearest dataset value?
*/
importDataSet(dataSet, property, useNearest = false) {
if (this.isBreedSet()) {
// REMIND: error
util.warn('Patches: exportDataSet called with breed, using patches')
this.baseSet.importDataSet(dataSet, property, useNearest)
return
}
const { numX, numY } = this.model.world
const dataset = dataSet.resample(numX, numY, useNearest)
this.ask(p => {
p[property] = dataset.data[p.id]
})
}
/**
* Extract a property from each Patch as a DataSet
*
* @param {string} property The patch numeric property to extract
* @param {Type} [Type=Array] The DataSet array's type
* @returns {DataSet} A DataSet of the patche's values
*/
exportDataSet(property, Type = Array) {
if (this.isBreedSet()) {
util.warn('Patches: exportDataSet called with breed, using patches')
return this.baseSet.exportDataSet(property, Type)
}
const { numX, numY } = this.model.world
// let data = util.arrayProps(this, property)
let data = this.props(property)
data = util.convertArrayType(data, Type)
return new DataSet(numX, numY, data)
}
// Return id/index given valid x,y integers
/**
* Return index into Patches given valid x,y integers
*
* @param {number} x Integer X value
* @param {number} y Integer Y value
* @returns {number} Integer index into Patches array
*/
patchIndex(x, y) {
const { minX, maxY, numX } = this.model.world
return x - minX + numX * (maxY - y)
}
// Return patch at x,y float values
// Return undefined if off-world
patch(x, y) {
// Benny suggests: (in PR wrap x and y in patches.patch(x, y))
// const intX = Math.round(util.wrap(x, this.model.world.minXcor, this.model.world.maxXcor))
// const intY = Math.round(util.wrap(y, this.model.world.minYcor, this.model.world.maxYcor))
if (!this.model.world.isOnWorld(x, y)) return undefined
const intX =
x === this.model.world.maxXcor
? this.model.world.maxX
: Math.round(x) // handle n.5 round up to n + 1
const intY =
y === this.model.world.maxYcor
? this.model.world.maxY
: Math.round(y)
return this.patchXY(intX, intY)
}
// Return the patch at x,y where both are valid integer patch coordinates.
patchXY(x, y) {
return this[this.patchIndex(x, y)]
}
// Patches in rectangle dx, dy from p, dx, dy integers.
// Both dx & dy are half width/height of rect
patchRect(p, dx, dy = dx, meToo = true) {
// Return cached rect if one exists.
if (p.rectCache) {
const index = this.cacheIndex(dx, dy, meToo)
const rect = p.rectCache[index]
if (rect) return rect
}
// const rect = new AgentArray()
const rect = new AgentList(this.model)
let { minX, maxX, minY, maxY } = this.model.world
minX = Math.max(minX, p.x - dx)
maxX = Math.min(maxX, p.x + dx)
minY = Math.max(minY, p.y - dy)
maxY = Math.min(maxY, p.y + dy)
for (let y = minY; y <= maxY; y++) {
for (let x = minX; x <= maxX; x++) {
const pnext = this.patchXY(x, y)
if (p !== pnext || meToo) rect.push(pnext)
}
}
return rect
}
// Return patchRect given legal x, y values
patchRectXY(x, y, dx, dy = dx, meToo = true) {
return this.patchRect(this.patch(x, y), dx, dy, meToo)
}
// Performance: create a cached rect of this size in sparse array.
// Index of cached rect is dx * dy + meToo ? 0 : -1.
// This works for edge rects that are not that full size.
// patchRect will use this if matches dx, dy, meToo.
cacheIndex(dx, dy = dx, meToo = true) {
return (2 * dx + 1) * (2 * dy + 1) + (meToo ? 0 : -1)
}
cacheRect(dx, dy = dx, meToo = true, clear = true) {
const index = this.cacheIndex(dx, dy, meToo)
this.ask(p => {
if (!p.rectCache || clear) p.rectCache = []
const rect = this.inRect(p, dx, dy, meToo)
p.rectCache[index] = rect
})
}
// Return patches within the patch rect, dx, dy integers
// default is square & meToo
inRect(patch, dx, dy = dx, meToo = true) {
const pRect = this.patchRect(patch, dx, dy, meToo)
if (this.isBaseSet()) return pRect
return pRect.withBreed(this)
}
// Return patches within float radius distance of patch
inRadius(patch, radius, meToo = true) {
const dxy = Math.ceil(radius)
const pRect = this.inRect(patch, dxy, dxy, meToo)
return pRect.inRadius(patch, radius, meToo)
}
// Patches in cone from patch in direction `heading`,
// with `coneAngle` width and within float `radius`
inCone(patch, radius, coneAngle, heading, meToo = true) {
const dxy = Math.ceil(radius)
const pRect = this.inRect(patch, dxy, dxy, meToo)
// // Using AgentArray's inCone, using radians
// heading = this.model.toRads(heading)
// coneAngle = this.model.toAngleRads(coneAngle)
// return pRect.inCone(patch, radius, coneAngle, heading, meToo)
return pRect.inCone(patch, radius, coneAngle, heading, meToo)
}
// Return patch at distance and angle from obj's (patch or turtle)
// x, y (floats). If off world, return undefined.
// Does not take into account the angle of the agent.
patchAtHeadingAndDistance(agent, heading, distance) {
heading = this.model.toRads(heading)
let { x, y } = agent
x = x + distance * Math.cos(heading)
y = y + distance * Math.sin(heading)
return this.patch(x, y)
}
// Return true if patch on edge of world
isOnEdge(patch) {
const { x, y } = patch
const { minX, maxX, minY, maxY } = this.model.world
return x === minX || x === maxX || y === minY || y === maxY
}
// returns the edge patches for this breed.
// generally called with patches.baseSet/model.patches.
edgePatches() {
return this.filter(p => this.isOnEdge(p))
}
// Diffuse the value of patch variable `p.v` by distributing `rate` percent
// of each patch's value of `v` to its neighbors.
// If the patch has less than 4/8 neighbors, return the extra to the patch.
diffuse(v, rate) {
this.diffuseN(8, v, rate)
}
diffuse4(v, rate) {
this.diffuseN(4, v, rate)
}
diffuseN(n, v, rate) {
// Note: for-of loops removed: chrome can't optimize them
// test/apps/patches.js 22fps -> 60fps
// zero temp variable if not yet set
if (this[0]._diffuseNext === undefined) {
// for (const p of this) p._diffuseNext = 0
for (let i = 0; i < this.length; i++) this[i]._diffuseNext = 0
}
// pass 1: calculate contribution of all patches to themselves and neighbors
// for (const p of this) {
for (let i = 0; i < this.length; i++) {
const p = this[i]
const dv = p[v] * rate
const dvn = dv / n
const neighbors = n === 8 ? p.neighbors : p.neighbors4
const nn = neighbors.length
p._diffuseNext += p[v] - dv + (n - nn) * dvn
// for (const n of neighbors) n._diffuseNext += dvn
for (let i = 0; i < neighbors.length; i++) {
neighbors[i]._diffuseNext += dvn
}
}
// pass 2: set new value for all patches, zero temp,
// for (const p of this) {
for (let i = 0; i < this.length; i++) {
const p = this[i]
p[v] = p._diffuseNext
p._diffuseNext = 0
}
}
}
export default Patches