Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NATMAP Soil carbon data in model view & permissions #450

Merged
merged 3 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading