diff --git a/app/javascript/projects/layer_palette.tsx b/app/javascript/projects/layer_palette.tsx index c4a18e0..7e7e405 100644 --- a/app/javascript/projects/layer_palette.tsx +++ b/app/javascript/projects/layer_palette.tsx @@ -9,6 +9,7 @@ import { IMDProperties } from './reify_layer/imd' import { ProjectPermissions } from './project_editor' import { KewPointOptions } from './reify_layer/kew' import { seasonYearOptions } from './modelling/components/kew_samples_component' +import { natmap_outputs } from './modelling/components/natmap_soil_component' interface AddLayerButtonProps { prototype: Layer @@ -330,6 +331,25 @@ export const LayerPalette = ({ addLayer, hide, dbModels, getTeamDatasets, teamNa />) } + { + permissions.NATMAPSoil && +
+ + +
+ } { dbModels.overlays.length > 0 &&
diff --git a/app/javascript/projects/modelling/components/natmap_soil_component.ts b/app/javascript/projects/modelling/components/natmap_soil_component.ts index 8ba7e66..239bbda 100644 --- a/app/javascript/projects/modelling/components/natmap_soil_component.ts +++ b/app/javascript/projects/modelling/components/natmap_soil_component.ts @@ -15,134 +15,172 @@ interface NatmapSoilOptions extends SelectControlOptions { key: string socket: Socket unit: string + min: number + max: number } -const natmap_outputs : NatmapSoilOptions[] = [ +export const natmap_outputs : NatmapSoilOptions[] = [ { id: 0, name: 'Min Carbon stock 0-10cm (kg/m²)', key: 'MIN_STK_10', socket: numericDataSocket, - unit: 'kg/m^2' + unit: 'kg/m^2', + min: 0, + max: 82.32000000000 }, { id: 1, name: 'Max Carbon stock 0-10cm (kg/m²)', key: 'MAX_STK_10', socket: numericDataSocket, - unit: 'kg/m^2' + unit: 'kg/m^2', + min: 0, + max: 95.13000000000 }, { id: 2, name: 'Min Carbon stock 0-15cm (kg/m²)', key: 'MIN_STK_15', socket: numericDataSocket, - unit: 'kg/m^2' + unit: 'kg/m^2', + min: 0, + max: 59.64000000000 }, { id: 3, name: 'Max Carbon stock 0-15cm (kg/m²)', key: 'MAX_STK_15', socket: numericDataSocket, - unit: 'kg/m^2' + unit: 'kg/m^2', + min: 0, + max: 59.64000000000 }, { id: 4, name: 'Min Carbon stock 0-30cm (kg/m²)', key: 'MIN_STK_30', socket: numericDataSocket, - unit: 'kg/m^2' + unit: 'kg/m^2', + min: 0, + max: 35.45000000000 }, { id: 5, name: 'Max Carbon stock 0-30cm (kg/m²)', key: 'MAX_STK_30', socket: numericDataSocket, - unit: 'kg/m^2' + unit: 'kg/m^2', + min: 0, + max: 63.23000000000 }, { id: 6, name: 'Average Carbon stock 0-30cm (kg/m²)', key: 'AV_STK_30', socket: numericDataSocket, - unit: 'kg/m^2' + unit: 'kg/m^2', + min: 0, + max: 51.07000000000 }, { id: 7, name: 'Average Carbon stock 30-100cm (kg/m²)', key: 'AV_STK_100', socket: numericDataSocket, - unit: 'kg/m^2' + unit: 'kg/m^2', + min: 0, + max: 86.19000000000 }, { id: 8, name: 'Average Carbon stock 100-150cm (kg/m²)', key: 'AV_STK_150', socket: numericDataSocket, - unit: 'kg/m^2' + unit: 'kg/m^2', + min: 0, + max: 59.64000000000 }, { id: 9, name: 'Average Organic Carbon 0-30cm (%)', key: 'AV_OC_30', socket: numericDataSocket, - unit: '%' + unit: '%', + min: 0, + max: 100 }, { id: 10, name: 'Min Organic Carbon 0-30cm (%)', key: 'MIN_OC_30', socket: numericDataSocket, - unit: '%' + unit: '%', + min: 0, + max: 100 }, { id: 11, name: 'Max Organic Carbon 0-30cm (%)', key: 'MAX_OC_30', socket: numericDataSocket, - unit: '%' + unit: '%', + min: 0, + max: 100 }, { id: 12, name: 'Average Organic Carbon 30-100cm (%)', key: 'AV_OC_100', socket: numericDataSocket, - unit: '%' + unit: '%', + min: 0, + max: 100 }, { id: 13, name: 'Min Organic Carbon 30-100cm (%)', key: 'MIN_OC_100', socket: numericDataSocket, - unit: '%' + unit: '%', + min: 0, + max: 100 }, { id: 14, name: 'Max Organic Carbon 30-100cm (%)', key: 'MAX_OC_100', socket: numericDataSocket, - unit: '%' + unit: '%', + min: 0, + max: 100 }, { id: 15, name: 'Average Organic Carbon 100-150cm (%)', key: 'AV_OC_150', socket: numericDataSocket, - unit: '%' + unit: '%', + min: 0, + max: 100 }, { id: 16, name: 'Min Organic Carbon 100-150cm (%)', key: 'MIN_OC_150', socket: numericDataSocket, - unit: '%' + unit: '%', + min: 0, + max: 100 }, { id: 17, name: 'Max Organic Carbon 100-150cm (%)', key: 'MAX_OC_150', socket: numericDataSocket, - unit: '%' + unit: '%', + min: 0, + max: 100 } ] diff --git a/app/javascript/projects/reify_layer/index.ts b/app/javascript/projects/reify_layer/index.ts index 84d0d62..008a56d 100644 --- a/app/javascript/projects/reify_layer/index.ts +++ b/app/javascript/projects/reify_layer/index.ts @@ -18,6 +18,7 @@ import { reifyGeoserverWMSLayer } from './geoserver' import { reifyKewLayer, reifyKewPointLayer } from './kew' import { reifyOrvalLayer } from './orval' import { reifyIMDLayer } from './imd' +import { reifyWFSLayer } from './wfs' export const reifyLayer = (layer: Layer, existingLayer: BaseLayer | null, dbModels: DBModels, map: Map, modelOutputCache: ModelOutputCache, DatasetCache: DatasetCache, loadteamDataset: (layer: DatasetLayer) => void): BaseLayer => { const layerType = layer.type @@ -38,6 +39,7 @@ export const reifyLayer = (layer: Layer, existingLayer: BaseLayer | null, dbMode case "ORValLayer": return reifyOrvalLayer(layer, existingLayer, map) case "IMDLayer": return reifyIMDLayer(layer, existingLayer, map) case "KewPointLayer": return reifyKewPointLayer(layer, existingLayer, map) + case "WFSLayer": return reifyWFSLayer(layer, existingLayer, map) default: { // Ensure this switch statement is exhaustive const unreachable: never = layerType diff --git a/app/javascript/projects/reify_layer/wfs.ts b/app/javascript/projects/reify_layer/wfs.ts new file mode 100644 index 0000000..e35eaf8 --- /dev/null +++ b/app/javascript/projects/reify_layer/wfs.ts @@ -0,0 +1,60 @@ +import BaseLayer from "ol/layer/Base" +import { WFSLayer } from "../state" +import GeoJSON from "ol/format/GeoJSON" +import Map from "ol/Map" +import VectorLayer from "ol/layer/Vector" +import { memoize } from "lodash" +import VectorSource from "ol/source/Vector" +import { bbox } from "ol/loadingstrategy" +import { Fill, Stroke, Style } from "ol/style" +import { natmap_outputs } from "../modelling/components/natmap_soil_component" +import { findColor } from "../analysis_panel_tools/subsection" +import { getColorStops } from "./model_output" + +const getSource = memoize((id: string, attribution: undefined | string) => { + + const store = id.split(":")[0] + + const source = new VectorSource({ + url: extent => `https://landscapes.wearepal.ai/geoserver/${store}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${id}&outputFormat=application/json&bbox=${extent.join(',')},EPSG:3857&crs=EPSG:3857`, + format: new GeoJSON({ + extractGeometryName: true + }), + strategy: bbox, + attributions: attribution + }) + + return source + +}) + +const getStyle = (layer: WFSLayer, colMap: any[]) => ( + (feature) => { + + const propIdx = layer.propIdx + const prop = natmap_outputs[propIdx] + const val = feature.get(prop.key) + const [min, max] = [prop.min, prop.max] + + const normalisedValue = prop.min > prop.max ? 1 - (val - min) / (max - min) : (val - min) / (max - min) + const col = findColor(normalisedValue, colMap) + + return new Style({ + fill: new Fill({ color: `rgba(${col[0]}, ${col[1]}, ${col[2]}, 1)`}) + }) + + } +) + +export function reifyWFSLayer (layer: WFSLayer, existingLayer: BaseLayer | null, map: Map) { + + const colMap = getColorStops(layer.fill === "heatmap" ? "jet" : (layer.fill === "greyscale" ? "greys" : layer.fill), 100).reverse() + + const vectLayer = new VectorLayer({ + source: getSource(layer.layer, layer.attribution), + style: getStyle(layer, colMap), + }) + + return vectLayer + +} \ No newline at end of file diff --git a/app/javascript/projects/sidebar.tsx b/app/javascript/projects/sidebar.tsx index 0751b90..759ef1b 100644 --- a/app/javascript/projects/sidebar.tsx +++ b/app/javascript/projects/sidebar.tsx @@ -2,13 +2,60 @@ import * as React from 'react' import './sidebar.css' import { ReactSortable } from 'react-sortablejs' import { nevoLevelNames, nevoPropertyNames } from './nevo' -import { AtiLayer, CropMapLayer, DatasetLayer, IMDLayer, KewLayer, KewPointLayer, Layer, ModelOutputLayer, NevoLayer, OverlayLayer, ShapeLayer, State } from './state' +import { AtiLayer, CropMapLayer, DatasetLayer, IMDLayer, KewLayer, KewPointLayer, Layer, ModelOutputLayer, NevoLayer, OverlayLayer, ShapeLayer, State, WFSLayer } from './state' import { iconForLayerType } from "./util" import { getColorStops } from './reify_layer/model_output' import { tileGridStats } from './modelling/tile_grid' import { IMDProperties } from './reify_layer/imd' import { KewPointOptions } from './reify_layer/kew' -import { seasonYearOptions } from './modelling/components/kew_samples_component' +import { natmap_outputs } from './modelling/components/natmap_soil_component' + +const colMapList = <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + interface OverlayLayerSettingsProps { layer: OverlayLayer @@ -43,6 +90,32 @@ const OverlayLayerSettings = ({ layer, mutate }: OverlayLayerSettingsProps) => ( ) +interface WFSLayerSettingsProps { + layer: WFSLayer + mutate: (data: any) => void +} + +const WFSLayerSettings = ({ layer, mutate }: WFSLayerSettingsProps) => ( + <> +
+ Fill mode + +
+
+ Property + +
+ +) + interface NevoLayerSettingsProps { layer: NevoLayer mutate: (data: any) => void @@ -163,53 +236,6 @@ const CehLandCoverLayerSettings = () => ( ) -const colMapList = <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - interface ModelOutputLayerSettingsProps { layer: ModelOutputLayer | DatasetLayer mutate: (data: any) => void @@ -811,7 +837,7 @@ export const Sidebar = ({ state, selectLayer, mutateLayer, deleteLayer, setLayer { - (selectedLayer?.type == "ModelOutputLayer" || selectedLayer?.type == "DatasetLayer" || selectedLayer?.type == "IMDLayer" || selectedLayer?.type == "KewPointLayer") && + (selectedLayer?.type == "ModelOutputLayer" || selectedLayer?.type == "DatasetLayer" || selectedLayer?.type == "IMDLayer" || selectedLayer?.type == "KewPointLayer" || selectedLayer?.type == "WFSLayer") && (
Layer legend @@ -854,6 +880,15 @@ export const Sidebar = ({ state, selectLayer, mutateLayer, deleteLayer, setLayer toggle={isLegCollapsed} /> } + { + selectedLayer?.type == "WFSLayer" && + + }
Layer settings
{ @@ -969,6 +1004,16 @@ export const Sidebar = ({ state, selectLayer, mutateLayer, deleteLayer, setLayer mutateLayer(state.selectedLayer, data) } /> + } + { + selectedLayer?.type == "WFSLayer" && + state.selectedLayer !== undefined && + mutateLayer(state.selectedLayer, data) + } + /> } : No layer selected diff --git a/app/javascript/projects/state.ts b/app/javascript/projects/state.ts index 4777bd2..3574b68 100644 --- a/app/javascript/projects/state.ts +++ b/app/javascript/projects/state.ts @@ -151,7 +151,15 @@ export interface GeoserverLayer extends BaseLayer { yOffset?: number } -export type Layer = OsmLayer | MapTileLayer | OverlayLayer | NevoLayer | CehLandCoverLayer | ModelOutputLayer | DatasetLayer | CropMapLayer | AtiLayer | ShapeLayer | BoundaryLayer | GeoserverLayer | KewLayer | ORValLayer | IMDLayer | KewPointLayer +export interface WFSLayer extends BaseLayer { + type: "WFSLayer" + layer: string + propIdx: number + attribution: string + fill: fillType +} + +export type Layer = OsmLayer | MapTileLayer | OverlayLayer | NevoLayer | CehLandCoverLayer | ModelOutputLayer | DatasetLayer | CropMapLayer | AtiLayer | ShapeLayer | BoundaryLayer | GeoserverLayer | KewLayer | ORValLayer | IMDLayer | KewPointLayer | WFSLayer export interface Project { name: string