Skip to content

Commit

Permalink
Merge pull request #386 from wearepal/soil-component
Browse files Browse the repository at this point in the history
Soil component with WRB Classifications
  • Loading branch information
paulthatjazz authored Jun 26, 2024
2 parents f0208aa + 020ae5a commit 2d88f96
Show file tree
Hide file tree
Showing 4 changed files with 306 additions and 0 deletions.
2 changes: 2 additions & 0 deletions app/javascript/projects/modelling/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { ORValComponent } from "./orval_component"
import { IMDComponent } from "./imd_component"
import { HedgerowComponent } from "./hedgerow_component"
import { ProjectPermissions } from "../../project_editor"
import { SoilComponent } from "./soil_component"

export interface ProjectProperties {
extent: Extent
Expand Down Expand Up @@ -75,6 +76,7 @@ export function createDefaultComponents(saveMapLayer: SaveMapLayer, saveModel: S
new CROMEComponent(extent, zoom, mask, maskLayer, maskCQL),
new ATIComponent(extent, zoom, mask, maskLayer, maskCQL),
new DesignationsComponent(extent, zoom, mask, maskLayer, maskCQL),
new SoilComponent(projectProps),

// Outputs
new MapLayerComponent(saveMapLayer),
Expand Down
257 changes: 257 additions & 0 deletions app/javascript/projects/modelling/components/soil_component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import { BaseComponent } from "./base_component"
import { NodeData, WorkerInputs, WorkerOutputs } from 'rete/types/core/data'
import { Input, Node, Output, Socket } from 'rete'
import { ProjectProperties } from "."
import { Extent } from "ol/extent"
import { booleanDataSocket, categoricalDataSocket, numericDataSocket } from "../socket_types"
import { createXYZ } from "ol/tilegrid"
import { retrieveISRICData } from "../model_retrieval"
import { CategoricalTileGrid, NumericTileGrid } from "../tile_grid"
import { TypedArray } from "d3"
import { maskFromExtentAndShape } from "../bounding_box"
import { SelectControl, SelectControlOptions } from "../controls/select"

const ERBSoilTypes = [
"Acrisols",
"Albeluvisols",
"Alisols",
"Andosols",
"Arenosols",
"Calcisols",
"Cambisols",
"Chernozems",
"Cryosols",
"Durisols",
"Ferralsols",
"Fluvisols",
"Gleysols",
"Gypsisols",
"Histosols",
"Kastanozems",
"Leptosols",
"Lixisols",
"Luvisols",
"Nitisols",
"Phaeozems",
"Planosols",
"Plinthosols",
"Podzols",
"Regosols",
"Solonchaks",
"Solonetz",
"Stagnosols",
"Umbrisols",
"Vertisols"
]

interface SoilGridOptions {
SCOId : number
name : string
map: string
coverageId: string
outputSocket: Socket
}

const SoilGrids : SelectControlOptions[] = [
{ id: 0, name: 'WRB (most probable)'},
{ id: 1, name: 'WRB (probability)' }
]

const SoilGridOptions : SoilGridOptions[] = [
{
SCOId: 0,
name: 'All',
map: 'wrb',
coverageId: 'MostProbable',
outputSocket: categoricalDataSocket
}
]

ERBSoilTypes.forEach((soilType, index) => {
SoilGridOptions.push({
SCOId: 0,
name: soilType,
map: 'wrb',
coverageId: 'MostProbable',
outputSocket: booleanDataSocket
})
SoilGridOptions.push({
SCOId: 1,
name: soilType,
map: 'wrb',
coverageId: soilType,
outputSocket: numericDataSocket

})
})

async function renderCategoricalData(extent: Extent, zoom: number, maskMode: boolean, maskLayer: string, maskCQL: string) {

const tileGrid = createXYZ()
const mask = await maskFromExtentAndShape(extent, zoom, maskLayer, maskCQL, maskMode)
const outputTileRange = tileGrid.getTileRangeForExtentAndZ(extent, zoom)

const geotiff = await retrieveISRICData(extent, 'MostProbable', 'wrb', outputTileRange)


const rasters = await geotiff.readRasters({ bbox: extent, width: outputTileRange.getWidth(), height: outputTileRange.getHeight() })
const image = await geotiff.getImage()


const result = new CategoricalTileGrid(
zoom,
outputTileRange.minX,
outputTileRange.minY,
outputTileRange.getWidth(),
outputTileRange.getHeight()
)

for (let i = 0; i < (rasters[0] as TypedArray).length; i++) {

let x = (outputTileRange.minX + i % image.getWidth())
let y = (outputTileRange.minY + Math.floor(i / image.getWidth()))
result.set(x, y, mask.get(x, y) ? rasters[0][i]+1 : 255)

}

result.setLabels(new Map(ERBSoilTypes.map((soilType, index) => [index+1, soilType])))

return result
}

async function renderNumericData(extent: Extent, zoom: number, maskMode: boolean, maskLayer: string, maskCQL: string, map: string, coverageId: string) {

const tileGrid = createXYZ()
const mask = await maskFromExtentAndShape(extent, zoom, maskLayer, maskCQL, maskMode)
const outputTileRange = tileGrid.getTileRangeForExtentAndZ(extent, zoom)

const geotiff = await retrieveISRICData(extent, coverageId, map, outputTileRange)

const rasters = await geotiff.readRasters({ bbox: extent, width: outputTileRange.getWidth(), height: outputTileRange.getHeight() })
const image = await geotiff.getImage()

const result = new NumericTileGrid(
zoom,
outputTileRange.minX,
outputTileRange.minY,
outputTileRange.getWidth(),
outputTileRange.getHeight()
)

for (let i = 0; i < (rasters[0] as TypedArray).length; i++) {

let x = (outputTileRange.minX + i % image.getWidth())
let y = (outputTileRange.minY + Math.floor(i / image.getWidth()))

const value = rasters[0][i]
result.set(x, y, mask.get(x, y) ? (value === 255 ? NaN : value) : NaN)

}

return result
}

export class SoilComponent extends BaseComponent {
projectZoom: number
projectExtent: Extent
maskMode: boolean
maskLayer: string
maskCQL: string


constructor(projectProps: ProjectProperties) {
super("ISRIC Soil Data")
this.category = "Inputs"
this.projectZoom = projectProps.zoom
this.projectExtent = projectProps.extent
this.maskMode = projectProps.mask
this.maskLayer = projectProps.maskLayer
this.maskCQL = projectProps.maskCQL
}

changeOutputs(node: Node) {

node.getConnections().forEach(c => {
if (c.output.node !== node) {
this.editor?.removeConnection(c)
}
})
node.getConnections().forEach(c => this.editor?.removeConnection(c))

Array.from(node.outputs.values()).forEach(output => node.removeOutput(output))

this.updateOutputs(node)

}

updateOutputs(node: Node) {

const soilgridId = node.data.soilgridId || 0
// TODO - add outputs based on soilgridId
const soilGridOpts = SoilGridOptions.filter(opt => opt.SCOId == soilgridId)
soilGridOpts.forEach(
opt => node.addOutput(new Output(`${opt.name}-${opt.map}`, opt.name, opt.outputSocket))
)
node.update()

}

async builder(node: Node) {

node.meta.toolTip = "Data from ISRIC SoilGrids API."
node.meta.toolTipLink = "https://www.isric.org/"

node.addControl(
new SelectControl(
this.editor,
'soilgridId',
() => SoilGrids,
() => this.changeOutputs(node),
'Soil Grid'
)
)

this.updateOutputs(node)

}


async worker(node: NodeData, inputs: WorkerInputs, outputs: WorkerOutputs, ...args: unknown[]) {

const soilgridId = node.data.soilgridId || 0
const soilGrid = SoilGrids.find(opt => opt.id == soilgridId)
const soilGridOpts = SoilGridOptions.filter(opt => opt.SCOId == soilgridId)
let catData: CategoricalTileGrid | undefined = undefined

const promises = soilGridOpts.filter(
opt => node.outputs[`${opt.name}-${opt.map}`].connections.length > 0
).map(async opt => {
if(soilGrid?.name == 'WRB (most probable)') {

if(!catData) {
catData = await renderCategoricalData(this.projectExtent, this.projectZoom, this.maskMode, this.maskLayer, this.maskCQL)
}

switch(opt.name) {
case 'All':
outputs[`${opt.name}-${opt.map}`] = catData
break
default:
outputs[`${opt.name}-${opt.map}`] = catData?.getBoolFromLabel(opt.name)
break
}

}else{

outputs[`${opt.name}-${opt.map}`] = await renderNumericData(this.projectExtent, this.projectZoom, this.maskMode, this.maskLayer, this.maskCQL, opt.map, opt.coverageId)

}
})

await Promise.all(promises)

//outputs["WRB"] = await renderCategoricalData(this.projectExtent, this.projectZoom, this.maskMode, this.maskLayer, this.maskCQL)

}

}
31 changes: 31 additions & 0 deletions app/javascript/projects/modelling/model_retrieval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as GeoTIFF from 'geotiff/dist-browser/geotiff'
import { Extent } from 'ol/extent'
import { bboxFromExtent } from './bounding_box'

// Returns a GeoTIFF object from a WMS server. Useful for some categorical/boolean data but may be susceptible to data loss. Fatest option usually
export async function retrieveModelData(extent: Extent, source: string, tileRange: any, style?: string) {

// Uses WMS server: Returns data between 0 and 255
Expand Down Expand Up @@ -39,6 +40,7 @@ export async function retrieveModelData(extent: Extent, source: string, tileRang

}

// Returns a GeoTIFF object from a WCS server. Useful for continuous data but may be slower.
export async function retrieveModelDataWCS(extent: Extent, source: string, tileRange: any) {

// Uses WCS server: Returns raw data
Expand Down Expand Up @@ -68,4 +70,33 @@ export async function retrieveModelDataWCS(extent: Extent, source: string, tileR
const tiff = await GeoTIFF.fromArrayBuffer(arrayBuffer)

return tiff
}

// For interaction with ISRIC SoilGrids API
export async function retrieveISRICData(extent: Extent, coverageId: string, map: string, tileRange: any) {

const server = `https://maps.isric.org/mapserv?map=/map/${map}.map&`
const [width, height] = [tileRange.getWidth(), tileRange.getHeight()]
const response = await fetch(
server + new URLSearchParams(
{
service: 'WCS',
version: '1.0.0',
request: 'GetCoverage',
COVERAGE: coverageId,
FORMAT: 'GEOTIFF_INT16',
WIDTH: width,
HEIGHT: height,
CRS: 'EPSG:3857',
RESPONSE_CRS: 'EPSG:3857',
BBOX: extent.toString()
}
)
)

const arrayBuffer = await response.arrayBuffer()
const tiff = await GeoTIFF.fromArrayBuffer(arrayBuffer)

return tiff

}
16 changes: 16 additions & 0 deletions app/javascript/projects/modelling/tile_grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,22 @@ export class CategoricalTileGrid extends TileGrid {
this.minMax = [0, this.labels.size]
}

getBoolFromLabel(label: string): BooleanTileGrid {

const boolGrid = new BooleanTileGrid(this.zoom, this.x, this.y, this.width, this.height)

const key = Array.from(this.labels).find(([key, value]) => value === label)?.[0]

this.iterate((x, y, value) => {
if (value === key) {
boolGrid.set(x, y, true)
}
})

return boolGrid

}

getMinMax() {
if (this.minMax == null) {
//no labels given.
Expand Down

0 comments on commit 2d88f96

Please sign in to comment.