diff --git a/gis/LeafletDataSet.js b/gis/LeafletDataSet.js index ad801c8f..b468c9c9 100644 --- a/gis/LeafletDataSet.js +++ b/gis/LeafletDataSet.js @@ -1,6 +1,7 @@ // import * as TileData from './TileData.js' import * as util from '../src/utils.js' import * as gis from '../src/gis.js' +import GeoDataSet from '../src/GeoDataSet.js' // import RGBDataSet from './RGBDataSet.js' class LeafletDataSet { @@ -91,8 +92,8 @@ class LeafletDataSet { const dataSetMatrix = this.dataSetsMatrix(tilesBBox, z) const tilesDataSet = this.dataSetMatrixToDataSet(dataSetMatrix) const cropParameters = this.getCropParameters(bbox, z) - const bboxDataSet = tilesDataSet.crop(cropParameters) - bboxDataSet.bbox = bbox + const croppedDataSet = tilesDataSet.crop(cropParameters) + const bboxDataSet = GeoDataSet.viewFromDataSet(croppedDataSet, bbox) console.log('bbox', bbox) console.log('tilesBBox', tilesBBox) diff --git a/models/DropletsModel.js b/models/DropletsModel.js index 529d0bce..2bf9763f 100644 --- a/models/DropletsModel.js +++ b/models/DropletsModel.js @@ -15,7 +15,7 @@ export default class DropletsModel extends Model { // 'patchAspect', // 'dataSetAspectNearest', // 'dataSetAspectBilinear', - stepType = 'minNeighbor' + stepType = 'dataSetAspectNearest' moves // how many moves in a step // Installed datasets: elevation diff --git a/models/scripts/DropletsModel.js b/models/scripts/DropletsModel.js index 84d3544e..ab1d79d5 100644 --- a/models/scripts/DropletsModel.js +++ b/models/scripts/DropletsModel.js @@ -15,7 +15,7 @@ class DropletsModel extends Model { // 'patchAspect', // 'dataSetAspectNearest', // 'dataSetAspectBilinear', - stepType = 'minNeighbor' + stepType = 'dataSetAspectNearest' moves // how many moves in a step // Installed datasets: elevation diff --git a/src/AS.js b/src/AS.js index 0942a43e..66543f48 100644 --- a/src/AS.js +++ b/src/AS.js @@ -52,3 +52,5 @@ export { default as Mouse } from './Mouse.js' export * as gis from './gis.js' export * as geojson from './geojson.js' export { default as GeoWorld } from './GeoWorld.js' +export { default as GeoDataSet } from './GeoDataSet.js' + diff --git a/src/AS0.js b/src/AS0.js index 11762c9b..34dc72d0 100644 --- a/src/AS0.js +++ b/src/AS0.js @@ -52,3 +52,4 @@ export { default as Mouse } from './Mouse.js' export * as gis from './gis.js' export * as geojson from './geojson.js' export { default as GeoWorld } from './GeoWorld.js' +export { default as GeoDataSet } from './GeoDataSet.js' diff --git a/src/GeoDataSet.js b/src/GeoDataSet.js new file mode 100644 index 00000000..ee686b97 --- /dev/null +++ b/src/GeoDataSet.js @@ -0,0 +1,182 @@ +import DataSet from "./DataSet.js" +import { bboxMetricSize } from './gis.js' +class GeoDataSet extends DataSet { + + /** + * Mostly the same a DataSet except it has bounds. + * A few methods, like slope, dzdx, dzdy, and aspect are different because they take into account the size of the bbox in meters + * + * @param {Number} width width of the DataSet in pixels + * @param {Number} height height of the DataSet in pixels + * @param {Array} bbox [west, south, east, north] + * @param {TypedArray} data + */ + constructor(width, height, bbox, data) { + super(width, height, data) + this.bbox = bbox + } + + /** + * Create a view of a Dataset including the bounds. + * + * @static + * @param {DataSet} dataSet + * @param {Array} bbox [west, south, east, north] + * @returns {GeoDataSet} GeoDataSet view of the dataset data. It is a view not a copy. + */ + static viewFromDataSet(dataSet, bbox) { + return new GeoDataSet(dataSet.width, dataSet.height, bbox, dataSet.data) + } + + lat2y(lat) { + const [west, south, east, north] = this.bbox + const y = Math.round(this.height * (lat - south) / (north - south)) + return y + } + + lon2x(lng) { + const [west, south, east, north] = this.bbox + const x = Math.round(this.width * (lng - west) / (east - west)) + return x + } + + // Convert from geoon/lat coords to pixel coords + toPixel(geoX, geoY) { + return [this.lon2x(geoX), this.lat2y(geoY)] + } + + // Get pixel from geo lon/lat coords to pixel coords + getGeo(geoX, geoY) { + const [x, y] = this.toPixel(geoX, geoY) + return this.getXY(x, y) + } + + // Set pixel from geo lon/lat coords to pixel coords + // I think this could be a slow way to set data for large amounts + setGeo(geoX, geoY, value) { + const [x, y] = this.toPixel(geoX, geoY) + return this.setXY(x, y, value) + } + + /** + * Samples a pixel at a given latitude and longitude + * + * @param {Number} geoX longitude + * @param {Number} geoY latitude + * @param {Boolean} useNearest + * @throws Out Of Range Error - when it is outside of the bbox + * @returns {Number} + */ + sampleGeo(geoX, geoY, useNearest = true) { + const [x, y] = this.toPixel(geoX, geoY) + return this.sample(x, y, useNearest) + } + + /** + * Change in z value in terms of x. Finite difference. + * + * @returns {GeoDataset} + */ + dzdx() { + const [widthMeters, heightMeters] = bboxMetricSize(this.bbox) + const pixelScale = widthMeters / this.width + const dzdx = super.dzdx(2, (1 / 8) * (1 / pixelScale)) // (1/8) for the kernel and 1/pixelscale to get units right + const dzdx2 = GeoDataSet.viewFromDataSet(dzdx, this.bbox) + return dzdx2 + } + + /** + * Change in z value in terms of y. Finite difference. + * + * @returns {GeoDataSet} + */ + dzdy() { + const [widthMeters, heightMeters] = bboxMetricSize(this.bbox) + const pixelScale = heightMeters / this.height + const dzdy = super.dzdy(2, (1 / 8) * (1 / pixelScale)) + const dzdy2 = GeoDataSet.viewFromDataSet(dzdy, this.bbox) + return dzdy2 + } + + /** + * Create dzdx, dzdy, slope, and aspect in one function. + * + * @returns {SlopeAndAspect} {dzdx, dzdy, slope, aspect} + */ + slopeAndAspect() { + const dzdx = this.dzdx() + const dzdy = this.dzdy() + const slope = this.slope(dzdx, dzdy) + const aspect = this.aspect(dzdx, dzdy) + return { dzdx, dzdy, slope, aspect } + } + + /** + * The aspect of each pixel in radians. + * + * @param {GeoDataset} dzdx + * @param {GeoDataSet} dzdy + * @returns {GeoDataSet} Aspect in radians. + */ + aspect(dzdx = this.dzdx(), dzdy = this.dzdy()) { + const asp = dzdx.map((x, i) => { + const y = dzdy.data[i] + const a = Math.atan2(-y, -x) + return a + }) + return asp + } + + /** + * Returns the slope in radians + * + * @param {GeoDataset} dzdx + * @param {GeoDataset} dzdy + * @returns {GeoDataset} + */ + slope(dzdx = this.dzdx(), dzdy = this.dzdy()) { + const slop = dzdx.map((x, i) => { + const y = dzdy.data[i] + const a = Math.hypot(-x, -y) + const sl = (Math.PI / 2) - Math.atan2(1, a) + return sl + }) + return slop + } + + // + // The functions below are the same as DataSet's version except they return a GeoDataset instead of a dataset. + // Is there a better way to do this? + // + // + clone() { + return new GeoDataSet(this.width, this.height, this.bbox, this.data) + } + + resample(width, height, useNearest = true, Type = Array) { + const a = super.resample(width, height, useNearest, Type) + const b = GeoDataSet.viewFromDataSet(a, this.bbox) + return b + } + + convolve(kernel, factor = 1, crop = false) { + const a = super.convolve(kernel, factor, crop) + const b = GeoDataSet.viewFromDataSet(a, this.bbox) + return b + } + + normalize(lo = 0, hi = 1, round = false) { + const a = super.normalize(lo, hi, round) + const b = GeoDataSet.viewFromDataSet(a, this.bbox) + return b + } + + map(f) { + const a = super.map(f) + const b = GeoDataSet.viewFromDataSet(a, this.bbox) + return b + } +} + +export default GeoDataSet + diff --git a/src/TileData.js b/src/TileData.js index 7e11bb0c..b2925043 100644 --- a/src/TileData.js +++ b/src/TileData.js @@ -1,5 +1,7 @@ import * as util from './utils.js' import RGBDataSet from './RGBDataSet.js' +import GeoDataSet from './GeoDataSet.js' +import {xyz2bbox} from './gis.js' // ============= rgb to elevation/numeric ============= @@ -28,7 +30,9 @@ const sharedTileObject = { }, zxyToDataSet: async function (z, x, y, ArrayType = Float32Array) { const img = await this.zxyToTile(z, x, y) - return this.tileDataSet(img, ArrayType) + const ds1 = this.tileDataSet(img, ArrayType) + const bbox = xyz2bbox(x, y, z) + return GeoDataSet.viewFromDataSet(ds1, bbox) }, tileDataSet: function (img, ArrayType = Float32Array) { const tileDecoder = this.elevationFcn diff --git a/test/samples.txt b/test/samples.txt index 9ee2a4f4..9419e240 100644 --- a/test/samples.txt +++ b/test/samples.txt @@ -2,7 +2,7 @@ "ants": "{\"ticks\":500,\"model\":[\"world\",\"patches\",\"turtles\",\"links\",\"ticks\",\"step0\",\"step\",\"toRads\",\"fromRads\",\"toAngleRads\",\"fromAngleRads\",\"toCCW\",\"population\",\"speed\",\"maxPheromone\",\"diffusionRate\",\"evaporationRate\",\"wiggleAngle\",\"foodX\",\"foodY\",\"nestX\",\"nestY\"],\"patches\":6561,\"patch\":{\"id\":4174,\"isFood\":false,\"isNest\":false,\"foodPheromone\":0.0257680536425004,\"nestPheromone\":0.04021862087665316,\"_diffuseNext\":0,\"neighbors\":[4092,4093,4094,4175,4256,4255,4254,4173]},\"turtles\":255,\"turtle\":{\"id\":80,\"theta\":0.35187919106928417,\"x\":4.569458552541104,\"y\":1.1474232505464783,\"carryingFood\":true,\"pheromone\":0.37713422825070403},\"links\":0}", "buttons": "{\"ticks\":500,\"model\":[\"world\",\"patches\",\"turtles\",\"links\",\"ticks\",\"step0\",\"step\",\"toRads\",\"fromRads\",\"toAngleRads\",\"fromAngleRads\",\"toCCW\",\"population\",\"cluster\",\"done\"],\"patches\":1089,\"patch\":{\"id\":273},\"turtles\":200,\"turtle\":{\"id\":136,\"theta\":2.283142302887487,\"x\":-10,\"y\":-16,\"links\":[112,150,412,438]},\"links\":500,\"link\":{\"id\":482,\"end0\":{\"id\":121,\"theta\":3.7783750618803937,\"x\":10,\"y\":9,\"links\":[194,200,482,484]},\"end1\":{\"id\":192,\"theta\":5.1900867760030405,\"x\":-15,\"y\":-8,\"links\":[77,274,366,454,482,499]}}}", "diffuse": "{\"ticks\":500,\"model\":[\"world\",\"patches\",\"turtles\",\"links\",\"ticks\",\"step0\",\"step\",\"toRads\",\"fromRads\",\"toAngleRads\",\"fromAngleRads\",\"toCCW\",\"population\",\"speed\",\"wiggleAngle\",\"radius\",\"diffuseRate\",\"seedDelta\",\"seedMax\"],\"patches\":80601,\"patch\":{\"id\":45451,\"ran\":0.6014832474734432,\"_diffuseNext\":0,\"neighbors\":[45049,45050,45051,45452,45853,45852,45851,45450]},\"turtles\":2,\"turtle\":{\"id\":1,\"theta\":4.017593726538558,\"x\":-169.46259631353462,\"y\":-13.334433990431691},\"links\":0}", - "droplets": "{\"ticks\":500,\"model\":[\"world\",\"patches\",\"turtles\",\"links\",\"ticks\",\"step0\",\"step\",\"toRads\",\"fromRads\",\"toAngleRads\",\"fromAngleRads\",\"toCCW\",\"speed\",\"stepType\",\"moves\",\"elevation\",\"dzdx\",\"dzdy\",\"slope\",\"aspect\",\"localMins\"],\"patches\":10201,\"patch\":{\"id\":5057,\"elevation\":1723.09375,\"aspect\":4.24928055747772,\"neighbors\":[4955,4956,4957,5058,5159,5158,5157,5056],\"isLocalMin\":false},\"turtles\":10201,\"turtle\":{\"id\":7694,\"theta\":0.6867621740671395,\"x\":-29.91422888528167,\"y\":-19.749582869876278,\"done\":true},\"links\":0}", + "droplets": "{\"ticks\":500,\"model\":[\"world\",\"patches\",\"turtles\",\"links\",\"ticks\",\"step0\",\"step\",\"toRads\",\"fromRads\",\"toAngleRads\",\"fromAngleRads\",\"toCCW\",\"speed\",\"stepType\",\"moves\",\"elevation\",\"dzdx\",\"dzdy\",\"slope\",\"aspect\",\"localMins\"],\"patches\":10201,\"patch\":{\"id\":5057,\"elevation\":1723.09375,\"aspect\":4.24928055747772,\"neighbors\":[4955,4956,4957,5058,5159,5158,5157,5056],\"isLocalMin\":false},\"turtles\":9584,\"turtle\":{\"id\":7632,\"theta\":1.4478503085133343,\"x\":5.4447649576110795,\"y\":-24.843932317600657,\"done\":true},\"links\":0}", "exit": "{\"ticks\":500,\"model\":[\"world\",\"patches\",\"turtles\",\"links\",\"ticks\",\"step0\",\"step\",\"toRads\",\"fromRads\",\"toAngleRads\",\"fromAngleRads\",\"toCCW\",\"numExits\",\"population\",\"exits\",\"inside\",\"wall\"],\"patches\":5041,\"patch\":{\"id\":4012,\"neighbors4\":[3941,4013,4083,4011],\"turtles\":[],\"neighbors\":[3940,3941,3942,4013,4084,4083,4082,4011]},\"turtles\":9,\"turtle\":{\"id\":1039,\"theta\":-0.7853981633974483,\"x\":15,\"y\":-22,\"exit\":{\"id\":4313,\"exitNumber\":0,\"turtles\":[]}},\"links\":0}", "fire": "{\"ticks\":500,\"model\":[\"world\",\"patches\",\"turtles\",\"links\",\"ticks\",\"step0\",\"step\",\"toRads\",\"fromRads\",\"toAngleRads\",\"fromAngleRads\",\"toCCW\",\"density\",\"fires\",\"embers\",\"patchTypes\",\"dirtType\",\"treeType\",\"fireType\",\"burnedTrees\",\"initialTrees\"],\"patches\":63001,\"patch\":{\"id\":52543,\"type\":\"ember0\",\"neighbors4\":[52292,52544,52794,52542]},\"turtles\":0,\"links\":0}", "flock": "{\"ticks\":500,\"model\":[\"world\",\"patches\",\"turtles\",\"links\",\"ticks\",\"step0\",\"step\",\"toRads\",\"fromRads\",\"toAngleRads\",\"fromAngleRads\",\"toCCW\",\"population\",\"vision\",\"speed\",\"maxTurn\",\"minSeparation\"],\"patches\":1089,\"patch\":{\"id\":901,\"turtles\":[]},\"turtles\":100,\"turtle\":{\"id\":62,\"theta\":8.580787915677849,\"x\":-0.5027269158976337,\"y\":-6.08771206064941},\"links\":0}", diff --git a/views3d/avalanche.html b/views3d/avalanche.html new file mode 100644 index 00000000..f20589f2 --- /dev/null +++ b/views3d/avalanche.html @@ -0,0 +1,135 @@ + + +
+