Skip to content

Commit

Permalink
Merge pull request #450 from wearepal/natmap-soil
Browse files Browse the repository at this point in the history
NATMAP Soil carbon data in model view & permissions
  • Loading branch information
paulthatjazz authored Nov 21, 2024
2 parents 2ab269d + 69786ce commit a7c6ae9
Show file tree
Hide file tree
Showing 10 changed files with 289 additions and 2 deletions.
5 changes: 4 additions & 1 deletion app/javascript/controllers/projects_controller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default class extends Controller {
projectDefraHedgerowPermission: Boolean,
projectKewRgb25cmPermission: Boolean,
projectKewSamplesPermission: Boolean,
projectNatmapSoilPermission: Boolean,
projectExtents: Array,
backButtonPath: String,
dbModels: Object
Expand All @@ -26,6 +27,7 @@ export default class extends Controller {
declare readonly projectDefraHedgerowPermissionValue: boolean
declare readonly projectKewRgb25cmPermissionValue: boolean
declare readonly projectKewSamplesPermissionValue: boolean
declare readonly projectNatmapSoilPermissionValue: boolean
declare readonly projectExtentsValue: Array<any>
declare readonly backButtonPathValue: string
declare readonly dbModelsValue: DBModels
Expand All @@ -43,7 +45,8 @@ export default class extends Controller {
{
DefraHedgerows: this.projectDefraHedgerowPermissionValue,
KewRgb25cm: this.projectKewRgb25cmPermissionValue,
KewSamples: this.projectKewSamplesPermissionValue
KewSamples: this.projectKewSamplesPermissionValue,
NATMAPSoil: this.projectNatmapSoilPermissionValue
}
}
teamExtents={this.projectExtentsValue as TeamExtentData[]}
Expand Down
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 @@ -40,6 +40,7 @@ import { SoilComponent } from "./soil_component"
import { SegmentComponent } from "./segment_component"
import { KewSamplesComponent } from "./kew_samples_component"
import { InterpolationComponent } from "./interpolation_component"
import { NatmapSoilComponent } from "./natmap_soil_component"

export interface ProjectProperties {
extent: Extent
Expand All @@ -58,6 +59,7 @@ export function createDefaultComponents(saveMapLayer: SaveMapLayer, saveModel: S
// Team permissions restrict some components. Add them here.
if (permissions.DefraHedgerows) restrictedComponents.push(new HedgerowComponent(projectProps))
if (permissions.KewSamples) restrictedComponents.push(new KewSamplesComponent(projectProps))
if (permissions.NATMAPSoil) restrictedComponents.push(new NatmapSoilComponent(projectProps))

// Freely available components here.
const components : BaseComponent[] = [
Expand Down
220 changes: 220 additions & 0 deletions app/javascript/projects/modelling/components/natmap_soil_component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { NodeData, WorkerInputs, WorkerOutputs } from "rete/types/core/data"
import { ProjectProperties } from "."
import { Node, Output, Socket } from "rete"
import { BaseComponent } from "./base_component"
import { SelectControlOptions } from "../controls/select"
import { numericDataSocket } from "../socket_types"
import { retrieveWFSData } from "../model_retrieval"
import { Feature } from "ol"
import { Geometry } from "ol/geom"
import { BooleanTileGrid, NumericTileGrid } from "../tile_grid"
import { createXYZ } from "ol/tilegrid"
import { maskFromExtentAndShape } from "../bounding_box"

interface NatmapSoilOptions extends SelectControlOptions {
key: string
socket: Socket
unit: string
}

const natmap_outputs : NatmapSoilOptions[] = [
{
id: 0,
name: 'Min Carbon stock 0-10cm (kg/m²)',
key: 'MIN_STK_10',
socket: numericDataSocket,
unit: 'kg/m^2'
},
{
id: 1,
name: 'Max Carbon stock 0-10cm (kg/m²)',
key: 'MAX_STK_10',
socket: numericDataSocket,
unit: 'kg/m^2'
},
{
id: 2,
name: 'Min Carbon stock 0-15cm (kg/m²)',
key: 'MIN_STK_15',
socket: numericDataSocket,
unit: 'kg/m^2'
},
{
id: 3,
name: 'Max Carbon stock 0-15cm (kg/m²)',
key: 'MAX_STK_15',
socket: numericDataSocket,
unit: 'kg/m^2'
},
{
id: 4,
name: 'Min Carbon stock 0-30cm (kg/m²)',
key: 'MIN_STK_30',
socket: numericDataSocket,
unit: 'kg/m^2'
},
{
id: 5,
name: 'Max Carbon stock 0-30cm (kg/m²)',
key: 'MAX_STK_30',
socket: numericDataSocket,
unit: 'kg/m^2'
},
{
id: 6,
name: 'Average Carbon stock 0-30cm (kg/m²)',
key: 'AV_STK_30',
socket: numericDataSocket,
unit: 'kg/m^2'
},
{
id: 7,
name: 'Average Carbon stock 30-100cm (kg/m²)',
key: 'AV_STK_100',
socket: numericDataSocket,
unit: 'kg/m^2'
},
{
id: 8,
name: 'Average Carbon stock 100-150cm (kg/m²)',
key: 'AV_STK_150',
socket: numericDataSocket,
unit: 'kg/m^2'
},
{
id: 9,
name: 'Average Organic Carbon 0-30cm (%)',
key: 'AV_OC_30',
socket: numericDataSocket,
unit: '%'
},
{
id: 10,
name: 'Min Organic Carbon 0-30cm (%)',
key: 'MIN_OC_30',
socket: numericDataSocket,
unit: '%'
},
{
id: 11,
name: 'Max Organic Carbon 0-30cm (%)',
key: 'MAX_OC_30',
socket: numericDataSocket,
unit: '%'
},
{
id: 12,
name: 'Average Organic Carbon 30-100cm (%)',
key: 'AV_OC_100',
socket: numericDataSocket,
unit: '%'
},
{
id: 13,
name: 'Min Organic Carbon 30-100cm (%)',
key: 'MIN_OC_100',
socket: numericDataSocket,
unit: '%'
},
{
id: 14,
name: 'Max Organic Carbon 30-100cm (%)',
key: 'MAX_OC_100',
socket: numericDataSocket,
unit: '%'
},
{
id: 15,
name: 'Average Organic Carbon 100-150cm (%)',
key: 'AV_OC_150',
socket: numericDataSocket,
unit: '%'
},
{
id: 16,
name: 'Min Organic Carbon 100-150cm (%)',
key: 'MIN_OC_150',
socket: numericDataSocket,
unit: '%'
},
{
id: 17,
name: 'Max Organic Carbon 100-150cm (%)',
key: 'MAX_OC_150',
socket: numericDataSocket,
unit: '%'
}

]

function applyFeaturesToGrid(features: Feature<Geometry>[], projectProps: ProjectProperties, prop: string, mask: BooleanTileGrid) : NumericTileGrid {

const tileGrid = createXYZ()
const outputTileRange = tileGrid.getTileRangeForExtentAndZ(projectProps.extent, projectProps.zoom)
const grid = new NumericTileGrid(
projectProps.zoom,
outputTileRange.minX,
outputTileRange.minY,
outputTileRange.getWidth(),
outputTileRange.getHeight()
)

for (let feature of features) {

const geom = feature.getGeometry()
if (geom === undefined) { continue }

const val = feature.get(prop)
if (val === undefined) { continue }

const featureTileRange = tileGrid.getTileRangeForExtentAndZ(
geom.getExtent(),
projectProps.zoom
)

grid.iterateOverTileRange(featureTileRange, (x, y) => {
const center = tileGrid.getTileCoordCenter([projectProps.zoom, x, y])

if (geom.intersectsCoordinate(center)) {
grid.set(x, y, mask.get(x, y) ? val : NaN)
}
}
)
}

return grid
}

export class NatmapSoilComponent extends BaseComponent {
projectProps: ProjectProperties
cachedFeatures: Feature<Geometry>[]
cachedOutputs: Map<string, NumericTileGrid>

constructor(projectProps: ProjectProperties) {
super("NATMAP Soil")
this.category = "Inputs"
this.projectProps = projectProps
this.cachedFeatures = []
this.cachedOutputs = new Map()
}

async builder(node: Node) {
natmap_outputs.forEach(opt => {
node.addOutput(new Output(opt.key, opt.name, opt.socket))
})
}

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

this.cachedFeatures = this.cachedFeatures.length === 0 ? await retrieveWFSData('cranfield_soil:NATMAPcarbon', this.projectProps) : this.cachedFeatures

const mask = await maskFromExtentAndShape(this.projectProps.extent, this.projectProps.zoom, this.projectProps.maskLayer, this.projectProps.maskCQL, this.projectProps.mask)

natmap_outputs.filter(opt => node.outputs[opt.key].connections.length > 0).forEach(opt => {
const res = this.cachedOutputs.has(opt.key) ? this.cachedOutputs.get(opt.key) : applyFeaturesToGrid(this.cachedFeatures, this.projectProps, opt.key, mask)
this.cachedOutputs.set(opt.key, res as NumericTileGrid)
outputs[opt.key] = res
})

}
}
24 changes: 24 additions & 0 deletions app/javascript/projects/modelling/model_retrieval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,30 @@
import * as GeoTIFF from 'geotiff/dist-browser/geotiff'
import { Extent } from 'ol/extent'
import { bboxFromExtent } from './bounding_box'
import { ProjectProperties } from './components'
import { GeoJSON } from "ol/format"
import { Geometry } from 'ol/geom'
import { Feature } from 'ol'

export async function retrieveWFSData(source: string, projectProps: ProjectProperties) : Promise<Feature<Geometry>[]> {

const response = await fetch(
"https://landscapes.wearepal.ai/geoserver/wfs?" +
new URLSearchParams(
{
outputFormat: 'application/json',
request: 'GetFeature',
typeName: source,
srsName: 'EPSG:3857',
bbox : bboxFromExtent(projectProps.extent),
}
)
)

const features = new GeoJSON().readFeatures(await response.json())

return features
}

// 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) {
Expand Down
19 changes: 19 additions & 0 deletions app/javascript/projects/modelling/tile_grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Extent } from "ol/extent"
import { createXYZ } from "ol/tilegrid"
import { registerSerializer } from "threads"
import { getMedianCellSize } from "./components/cell_area_component"
import { TileRange } from "ol"

function validateZoom(zoom: number) {
if (!(
Expand Down Expand Up @@ -244,12 +245,30 @@ export class NumericTileGrid extends TileGrid {
}

iterate(callback: (x: number, y: number, value: number) => void) {

const { x, y, width, height } = this
for (let i = x; i < x + width; i++) {
for (let j = y; j < y + height; j++) {
callback(i, j, this.get(i, j))
}
}

}

iterateOverTileRange(range: TileRange, callback: (x: number, y: number, value: number) => void) {

const { x, y, width, height } = this
const minX = Math.max(x, range.minX)
const maxX = Math.min(x + width, range.maxX)
const minY = Math.max(y, range.minY)
const maxY = Math.min(y + height, range.maxY)

for (let i = minX; i < maxX; i++) {
for (let j = minY; j < maxY; j++) {
callback(i, j, this.get(i, j))
}
}

}

get(x: number, y: number, zoom = this.zoom): number {
Expand Down
1 change: 1 addition & 0 deletions app/javascript/projects/project_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface ProjectPermissions {
DefraHedgerows: boolean
KewRgb25cm: boolean
KewSamples: boolean
NATMAPSoil: boolean
}

export interface TeamExtentData {
Expand Down
8 changes: 8 additions & 0 deletions app/models/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ def kew_samples_permission
tp ? tp.enabled : false
end

def natmap_soil_permission
p = Permission.find_by(name: 'natmap_soil')
return false unless p

tp = team.team_permissions.find_by(permission: p)
tp ? tp.enabled : false
end

def extents
team_extents = Extent.where(team_id: team.id).to_json
end
Expand Down
1 change: 1 addition & 0 deletions app/views/projects/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
data-projects-project-defra-hedgerow-permission-value="<%= @project.defra_hedgerow_permission %>"
data-projects-project-kew-rgb25cm-permission-value="<%= @project.kew_rgb25cm_permission %>"
data-projects-project-kew-samples-permission-value="<%= @project.kew_samples_permission %>"
data-projects-project-natmap-soil-permission-value="<%= @project.natmap_soil_permission %>"
data-projects-project-extents-value="<%= @project.extents %>"
data-projects-back-button-path-value="<%= team_projects_path(@project.team) %>"
data-projects-db-models-value="<%= render partial: "defs", formats: [:json] %>"
Expand Down
9 changes: 9 additions & 0 deletions db/migrate/20241121163637_add_natmap_permission.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class AddNatmapPermission < ActiveRecord::Migration[6.1]
def change
permission = Permission.find_or_create_by(name: 'natmap_soil')

Team.all.each do |team|
TeamPermission.find_or_create_by(team: team, permission: permission, enabled: false)
end
end
end
2 changes: 1 addition & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2024_10_21_150643) do
ActiveRecord::Schema.define(version: 2024_11_21_163637) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand Down

0 comments on commit a7c6ae9

Please sign in to comment.