UNPKG

agentscript

Version:

AgentScript Model in Model/View architecture

377 lines (334 loc) 12 kB
import * as util from './utils.js' // class World defines the coordinate system for the model. // It will be upgraded with methods converting from other // transforms like GIS and DataSets. // const defaultZ = (maxX, maxY) => Math.max(maxX, maxY) /** * @private * @typedef {Object} WorldOptions * @property {number} minX Min world patch x integer value * @property {number} minY Min world patch y integer value * @property {number} minZ Min world patch z integer value * @property {number} maxX Max world patch x integer value * @property {number} maxY Max world patch y integer value * @property {number} maxZ Max world patch z integer value */ /** * Class World defines the coordinate system for the model. * It has transforms for multiple coordinate systems. * * The world is defined by an object with 6 properties: * * WorldOptions = { * minX: integer, * maxX: integer, * minY: integer, * maxY: integer, * minZ: integer, * maxZ: integer, * } */ class World { maxX = 16 minX = -16 maxY = 16 minY = -16 // maxZ = 0 // minZ = 0 maxZ = 16 minZ = -16 /** * Return a default options object, origin at center. * * @param {number} [maxX=16] Integer max X value * @param {number} [maxY=maxX] Integer max Y value * @param {number} [maxZ=Math.max(maxX, maxY)] Integer max Z value * @returns WorldOptions */ static defaultOptions(maxX = 16, maxY = maxX, maxZ = Math.max(maxX, maxY)) { return { minX: -maxX, maxX: maxX, minY: -maxY, maxY: maxY, minZ: -maxZ, maxZ: maxZ, } } /** * Factory to create a default World instance. * * @param {number} [maxX=16] Integer max X value * @param {number} [maxY=maxX] Integer max Y value * @param {number} [maxZ=Math.max(maxX, maxY)] Integer max Z value * @returns {World} */ static defaultWorld(maxX = 16, maxY = maxX, maxZ = maxX) { return new World(World.defaultOptions(maxX, maxY, maxZ)) } // ====================== /** * Create a new World object given an Object with optional * minX, maxX, minY, maxY, minZ, maxZ overriding class properties. * @param {World|WorldOptions|Object} options Object with overrides for class properties */ constructor(options = {}) { Object.assign(this, options) // set the option override values this.setWorld() // convert these to rest of world parameters } // ====================== setWorld() { // Complete properties derived from minX/Y, maxX/Y (patchSize === 1) let { minX, maxX, minY, maxY, minZ, maxZ } = this util.forLoop({ minX, maxX, minY, maxY, minZ, maxZ }, (val, key) => { if (!Number.isInteger(val)) throw Error(`World: ${key}:${val} must be an integer`) }) this.numX = this.width = maxX - minX + 1 this.numY = this.height = maxY - minY + 1 // if (maxZ == null) maxZ = this.maxZ = Math.max(this.width, this.height) this.numZ = this.depth = maxZ - minZ + 1 this.minXcor = minX - 0.5 this.maxXcor = maxX + 0.5 this.minYcor = minY - 0.5 this.maxYcor = maxY + 0.5 this.minZcor = minZ - 0.5 this.maxZcor = maxZ + 0.5 // The midpoints of the world, in world coords. this.centerX = (minX + maxX) / 2 this.centerY = (minY + maxY) / 2 this.centerZ = (minZ + maxZ) / 2 this.numPatches = this.numX * this.numY } /** * Return the options (minX, maxX, ..) used to create this world. * * @returns {Object} A WorldOptions object */ getOptions() { const { minX, minY, minZ, maxX, maxY, maxZ } = this return { minX, minY, minZ, maxX, maxY, maxZ } } /** * Return a random 2D float point within the World * * @returns {Array} A random x,y float array */ randomPoint() { return [ util.randomFloat2(this.minXcor, this.maxXcor), util.randomFloat2(this.minYcor, this.maxYcor), ] } /** * Return a random 3D point within the World * * @returns {Array} A random x,y,z float array */ random3DPoint() { return [ util.randomFloat2(this.minXcor, this.maxXcor), util.randomFloat2(this.minYcor, this.maxYcor), util.randomFloat2(this.minZcor, this.maxZcor), ] } /** * Return a random Patch 2D integer point * * @returns {Array} A random x,y integer array */ randomPatchPoint() { return [ util.randomInt2(this.minX, this.maxX), util.randomInt2(this.minY, this.maxY), ] } /** * Given x,y,z values return true if within the world * * @param {number} x x value * @param {number} y y value * @param {number} [z=this.centerZ] z value * @returns {boolean} Whether or not on-world */ isOnWorld(x, y, z = this.centerZ) { return ( this.minXcor <= x && x <= this.maxXcor && // this.minYcor <= y && y <= this.maxYcor && // this.minZcor <= z && z <= this.maxZcor ) } // cropToWorld(x, y) {} /** * Return an instance of a bounding box 2D transform. * It linearly interpolates between the given minX, minY, maxX, maxY, * and the world's values of the same properties. * * The parameters are in the popular geojson order: west, south, east, north * * Useful for Canvas (pixel) top-left transforms and geojson transforms. * * Note minX etc NOT the world's but of the coord sys we want to use. * * @param {number} minX min bounding box x value * @param {number} minY min bounding box y value * @param {number} maxX max bounding box x value * @param {number} maxY max bounding box y value * @returns {BBoxTransform} Instance of the BBoxTransform */ bboxTransform(minX, minY, maxX, maxY) { return new BBoxTransform(minX, minY, maxX, maxY, this) } // ### Following functions use PatchSize // Get the world size in pixels. PatchSize is optional, defalting to 1 getWorldSize(patchSize = 1) { return [this.numX * patchSize, this.numY * patchSize] } // Convert a canvas context to world euclidean coordinates // Change the ctx.canvas size, determined by patchSize. setEuclideanTransform(ctx, patchSize) { // ctx.canvas.width = this.numX * patchSize // ctx.canvas.height = this.numY * patchSize this.setCanvasSize(ctx.canvas, patchSize) ctx.restore() // close earlier save(). OK if no save called yet. ctx.save() ctx.scale(patchSize, -patchSize) ctx.translate(-this.minXcor, -this.maxYcor) } // Return patch size for given canvas. // Error if canvas patch width/height differ. patchSize(canvas) { const { numX, numY } = this const { clientWidth: width, clientHeight: height } = canvas const xSize = width / numX const ySize = height / numY if (xSize !== ySize) { throw Error(`World patchSize: x/y sizes differ ${xSize}, ${ySize}`) } return xSize } // Change canvas size to this world's size. // Does not change size if already the same, preserving the ctx content. setCanvasSize(canvas, patchSize) { const [width, height] = this.getWorldSize(patchSize) util.setCanvasSize(canvas, width, height) } // Convert pixel location (top/left offset i.e. mouse) to patch coords (float) pixelXYtoPatchXY(x, y, patchSize) { return [this.minXcor + x / patchSize, this.maxYcor - y / patchSize] } // Convert patch coords (float) to pixel location (top/left offset i.e. mouse) patchXYtoPixelXY(x, y, patchSize) { return [(x - this.minXcor) * patchSize, (this.maxYcor - y) * patchSize] } xyToPatchIndex(x, y) { if (!this.isOnWorld(x, y)) return undefined const { minX, maxX, maxY, numX, maxXcor, maxYcor } = this x = x === maxXcor ? maxX : Math.round(x) y = y === maxYcor ? maxY : Math.round(y) return x - minX + numX * (maxY - y) } // patchIndexToXY(index) {} } export default World /** * A linear transformer between world coords and the given bounding box. * @private */ class BBoxTransform { /** * Creates an instance of BBoxTransform. * * - See [ietf reference](https://tools.ietf.org/html/rfc7946#section-5) * - .. which defines bbox as [west, south, east, north] * - .. which is the bbox [minX, minY, maxX, maxY] * * @param {number} minX min bounding box x value * @param {number} minY max bounding box x value * @param {number} maxX min bounding box y value * @param {number} maxY max bounding box y value * @param {World} world instance of a World object */ constructor(minX, minY, maxX, maxY, world) { this.bbox = [minX, minY, maxX, maxY] if (minX < maxX) console.log('flipX') if (maxY < minY) console.log('flipY') if (minX < maxX) [minX, maxX] = [maxX, minX] if (maxY < minY) [maxY, minY] = [minY, maxY] const { maxXcor, maxYcor, minXcor, minYcor } = world const mx = (minX - maxX) / (maxXcor - minXcor) const my = (maxY - minY) / (maxYcor - minYcor) const bx = (minX + maxX - mx * (maxXcor + minXcor)) / 2 const by = (maxY + minY - my * (maxYcor + minYcor)) / 2 Object.assign(this, { mx, my, bx, by }) } /** * Convert from bbox point to world point * * @param {Array} bboxPoint A point in the bbox coordinates * @returns {Array} A point in the world coordinates */ toWorld(bboxPoint) { const { mx, my, bx, by } = this const [bboxX, bboxY] = bboxPoint const x = (bboxX - bx) / mx const y = (bboxY - by) / my return [x, y] } /** * Convert from world point to bbox point * * @param {Array} worldPoint A point in the world coordinates * @returns {Array} A point in the bbox coordinates */ toBBox(worldPoint) { const { mx, my, bx, by } = this const [worldX, worldY] = worldPoint const x = mx * worldX + bx const y = my * worldY + by return [x, y] } // aspectRatio() { // const [west, south, east, north] = this.bbox // return Math.abs((east - west) / (north - south)) // } // Return bbox in xfm coords // bbox() { // const [west, south, east, north] = this.bbox // return [west, south, east, north] // // return this.bbox // name conflict? // } // // Return center [x,y] of bbox in bbox coords. // bboxCenter() { // const [west, south, east, north] = this.bbox // return [(west + east) / 2, (south + north) / 2] // } // // Return 4 coordinate points of bbox corners, from topLeft, clockwise. // bboxCoords() { // const [west, south, east, north] = this.bbox // return [ // [west, north], // [east, north], // [east, south], // [west, south], // ] // } // // Return a geojson feature for this bbox // bboxFeature(properties = {}) { // const coords = this.bboxCoords(bbox) // coords.push(coords[0]) // polys are closed, repeat first coord // return { // type: 'feature', // geometry: { // cordinates: coords, // type: 'Polygon', // }, // properties, // } // } }