Skip to content

Commit

Permalink
Merge pull request #336 from wearepal/kew-data
Browse files Browse the repository at this point in the history
Added KewLayer to map view
  • Loading branch information
paulthatjazz authored Feb 15, 2024
2 parents 1706d31 + 2d8fcb3 commit d3342b8
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 3 deletions.
86 changes: 85 additions & 1 deletion app/javascript/projects/layer_palette.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react'
import { DBModels } from './db_models'
import { NevoLevel } from './nevo'
import { Layer } from './state'
import { KewOption, Layer } from './state'
import { iconForLayerType } from "./util"
import { CompiledDatasetRecord } from './saved_dataset'

Expand Down Expand Up @@ -94,6 +94,90 @@ export const LayerPalette = ({ addLayer, hide, dbModels, getTeamDatasets, teamNa
}}
/>
</Section>
<Section title="Kew Samples">
{
Array<{ name: string, location: string, metric: string, loc : string | undefined, periodOptions: KewOption[], typeOptions: KewOption[] }>(
{
name: "SSSI Woodland",
location: "kew:sssi_wood_os_bng_04m_grid",
metric: "grasses",
loc: undefined,
periodOptions: [
{ value: "Summer22", label: "Summer 22", selected: false },
{ value: "Autumn22", label: "Autumn 22", selected: false },
{ value: "Spring23", label: "Spring 23", selected: false },
{ value: "Summer23", label: "Summer 23", selected: false },
{ value: "Autumn23", label: "Autumn 23", selected: true },
],
typeOptions: [
{ value: "grasses", label: "Grass", max : 100 },
{ value: "forbs", label: "Forbs" , max : 100},
{ value: "bryos", label: "Bryos" , max : 100},
{ value: "leaf_litter", label: "Leaf Liter", max : 100 },
{ value: "bare_ground", label: "Bare Ground" , max : 100},
{ value: "canopy_cover", label: "Canopy" , max : 100},
]
},
{
name: "Young Conifer",
location: "kew:young_conifer_os_bng_04m_grid",
metric: "totalCarbon",
loc: "DenseCon",
periodOptions: [
{ value: "Sp21", label: "Spring 2022", selected: false },
{ value: "Aut21", label: "Autumn 2021", selected: false },
{ value: "Su22", label: "Summer 2022", selected: false },
{ value: "Su22_2", label: "Summer 2022 - 2" , selected: false},
{ value: "Aut22", label: "Autumn 2022", selected: false },
{ value: "Sp23", label: "Spring 2023" , selected: false},
{ value: "Su23", label: "Summer 2023", selected: true }
],
typeOptions: [
{ value: "totalCarbon", label: "Total Carbon", max : 7},
{ value: "soil_density", label: "Soil Density", max : 1200 },
{ value: "pH", label: "pH", max : 14},
{ value: "dry_matter", label: "Dry Matter", max : 100},
]
},
{
name: "Coronation Meadow",
location: "kew:coronation_meadow_os_bng_02m_grid",
metric: "totalCarbon",
loc: "Meadow",
periodOptions: [
{ value: "Sp21", label: "Spring 2022", selected: false },
{ value: "Aut21", label: "Autumn 2021", selected: false },
{ value: "Su22", label: "Summer 2022", selected: false },
{ value: "Su22_2", label: "Summer 2022 - 2" , selected: false},
{ value: "Aut22", label: "Autumn 2022", selected: false },
{ value: "Sp23", label: "Spring 2023" , selected: false},
{ value: "Su23", label: "Summer 2023", selected: true }
],
typeOptions: [
{ value: "totalCarbon", label: "Total Carbon", max : 7},
{ value: "soil_density", label: "Soil Density", max : 1200 },
{ value: "pH", label: "pH", max : 14},
{ value: "dry_matter", label: "Dry Matter", max : 100},
]
}
).map(({ name, location, metric, periodOptions, typeOptions, loc }) =>
<AddLayerButton
addLayer={addLayer}
prototype={{
type: "KewLayer",
name,
location,
metric,
periodOptions,
typeOptions,
visible: true,
opacity: 1,
loc
}}
/>
)
}
</Section>
<Section title="Ancient Tree Inventory">
<AddLayerButton
addLayer={addLayer}
Expand Down
2 changes: 2 additions & 0 deletions app/javascript/projects/reify_layer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { reifyAtiLayer } from './ati'
import { reifyShapeFileLayer } from './shapefile'
import { reifyBoundaryLayer } from './boundary'
import { reifyGeoserverWMSLayer } from './geoserver'
import { reifyKewLayer } from './kew'

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
Expand All @@ -31,6 +32,7 @@ export const reifyLayer = (layer: Layer, existingLayer: BaseLayer | null, dbMode
case "ShapeLayer": return reifyShapeFileLayer(layer, existingLayer, map)
case "BoundaryLayer": return reifyBoundaryLayer(layer, existingLayer, map)
case "MLLayer": return reifyGeoserverWMSLayer(layer, existingLayer)
case "KewLayer": return reifyKewLayer(layer, existingLayer, map)
default: {
// Ensure this switch statement is exhaustive
const unreachable: never = layerType
Expand Down
72 changes: 72 additions & 0 deletions app/javascript/projects/reify_layer/kew.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import BaseLayer from "ol/layer/Base"
import { KewLayer } from "../state"
import VectorLayer from "ol/layer/Vector"
import { memoize } from "lodash"
import VectorSource from "ol/source/Vector"
import GeoJSON from "ol/format/GeoJSON"
import { bbox } from "ol/loadingstrategy"
import { Fill, Stroke, Style, Text } from "ol/style"
import { Map, Overlay } from "ol"



const getSource = memoize((location: string) => {
const source = new VectorSource({
url: extent => `https://landscapes.wearepal.ai/geoserver/kew/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${location}&outputFormat=application/json&bbox=${extent.join(',')},EPSG:3857&crs=EPSG:3857`,
format: new GeoJSON({
extractGeometryName: true
}),
strategy: bbox,
attributions: '&copy; <a href="https://www.kew.org/">Kew</a> Partners',
})

return source

})

const getStyle = (layer: KewLayer, zoom: number | undefined) => (
(feature) => {
const properties = feature.getProperties()
let metric
let dmax = layer.typeOptions.filter(opt => opt.value == layer.metric)[0].max || 100
layer.periodOptions.filter(opt => opt.selected).forEach(option => {
metric = properties[[option.value, layer.loc, layer.metric].filter(a => a).join("_")] || metric
})
const [min, max] = [0, dmax]
const color = metric ? `rgba(${255 * (1 - ((metric - min) / (max - min)))}, ${(metric - min) / (max - min) * 255}, 0, 1)` : "rgba(0,0,0,.1)"

return new Style({
fill : new Fill({
color
}),
stroke: new Stroke({
color: `rgba(0,0,0,.5)`,
width: 1
}),
text: new Text({
text: `${metric ? metric : ""} ${zoom ? (zoom > 21 ? `(${properties.GRID_ID})` : "") : ""}`,
font: `${zoom ? (zoom > 21 ? 16 : 12) : 12}px Calibri,sans-serif`,
fill: new Fill({
color: metric ? '#fff' : "rgba(0,0,0,.1)"
}),
stroke: new Stroke({
color: metric ? '#fff' : "rgba(0,0,0,.1)",
width: zoom ? (zoom > 10 ? .2 : 0) : 0
})

})
})
}
)


export function reifyKewLayer (layer: KewLayer, existingLayer: BaseLayer | null , map: Map) {

const vectLayer = new VectorLayer({
source: getSource(layer.location),
style: getStyle(layer, map.getView().getZoom())
})

return vectLayer

}
55 changes: 54 additions & 1 deletion app/javascript/projects/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react'
import './sidebar.css'
import { ReactSortable } from 'react-sortablejs'
import { nevoLevelNames, nevoPropertyNames } from './nevo'
import { AtiLayer, CropMapLayer, DatasetLayer, Layer, MLLayer, ModelOutputLayer, NevoLayer, OverlayLayer, ShapeLayer, State } from './state'
import { AtiLayer, CropMapLayer, DatasetLayer, KewLayer, Layer, MLLayer, ModelOutputLayer, NevoLayer, OverlayLayer, ShapeLayer, State } from './state'
import { iconForLayerType } from "./util"
import { getColorStops } from './reify_layer/model_output'
import { tileGridStats } from './modelling/tile_grid'
Expand Down Expand Up @@ -246,6 +246,49 @@ const MLLayerSettings = ({ layer }: MLLayerSettingsProps) => (
</details>
)

interface KewLayerSettingsProps {
layer: KewLayer
mutate: (data: any) => void
}

const KewLayerSettings = ({ layer, mutate }: KewLayerSettingsProps) => {


const handlePeriodChange = (value: string) => {
const updatedPeriodOptions = layer.periodOptions.map(opt => {
if (opt.value === value) {
return { ...opt, selected: !opt.selected }
}
return opt
})
mutate({ periodOptions: updatedPeriodOptions })
}

return (
<div className="mt-3">
<select className="custom-select" value={layer.metric} onChange={e => mutate({ metric: e.target.value })}>
{
layer.typeOptions.map(opt =>
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
)
}
</select>
<ul className="list-group mt-1">
{
layer.periodOptions.map(opt =>
<li key={opt.value} className="list-group-item" style={{border: 0, padding: 0}}>
<input type="checkbox" checked={opt.selected} onChange={() => handlePeriodChange(opt.value)} />
{opt.label}
</li>
)
}
</ul>
</div>
)
}

export function ZoomData({zoom, area, length}) {
const unit = area < 1 ? "cm²" : (area > 1000000 ? "km²" : "m²")
length = area < 1 ? length * 100 : (area > 1000000 ? length / 1000 : length)
Expand Down Expand Up @@ -717,6 +760,16 @@ export const Sidebar = ({ state, selectLayer, mutateLayer, deleteLayer, setLayer
selectedLayer?.type == "MLLayer" &&
<MLLayerSettings layer={selectedLayer} />
}
{
selectedLayer?.type == "KewLayer" &&
<KewLayerSettings
layer={selectedLayer}
mutate={
data => state.selectedLayer !== undefined &&
mutateLayer(state.selectedLayer, data)
}
/>
}
</> :
<em>No layer selected</em>
}
Expand Down
17 changes: 16 additions & 1 deletion app/javascript/projects/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ export interface StrokeFill {
fill: [r: number, g: number, b: number, a: number]
}

export interface KewOption {
value: string
label: string
selected?: boolean
max?: number
}

type fillType = "greyscale" | "heatmap" | "jet" | "hsv" | "hot" | "cool" | "spring" | "summer" | "autumn" | "winter" | "copper" | "WIGnBu" | "greens" | "YIOrRd" | "bluered" | "RdBu" | "picnic" | "rainbow" | "portland" | "blackbody" | "earth" | "electric" | "viridis" | "inferno" | "magma" | "plasma" | "warm" | "cool" | "rainbow-soft" | "bathymetry" | "cdom" | "chlorophyll" | "density" | "freesurface-blue" | "freesurface-red" | "oxygen" | "par" | "phase" | "salinity" | "temperature" | "turbidity" | "velocity-blue" | "velocity-green" | "cubehelix"

export interface OsmLayer extends BaseLayer {
Expand Down Expand Up @@ -55,6 +62,14 @@ export interface BoundaryLayer extends BaseLayer {
identifier: string
}

export interface KewLayer extends BaseLayer {
type: "KewLayer"
location: string
periodOptions: KewOption[]
typeOptions: KewOption[]
metric: string
loc?: string
}

export interface NevoLayer extends BaseLayer {
type: "NevoLayer"
Expand Down Expand Up @@ -97,7 +112,7 @@ export interface MLLayer extends BaseLayer {
layerName: string
}

export type Layer = OsmLayer | MapTileLayer | OverlayLayer | NevoLayer | CehLandCoverLayer | ModelOutputLayer | DatasetLayer | CropMapLayer | AtiLayer | ShapeLayer | BoundaryLayer | MLLayer
export type Layer = OsmLayer | MapTileLayer | OverlayLayer | NevoLayer | CehLandCoverLayer | ModelOutputLayer | DatasetLayer | CropMapLayer | AtiLayer | ShapeLayer | BoundaryLayer | MLLayer | KewLayer

export interface Project {
name: string
Expand Down
2 changes: 2 additions & 0 deletions app/javascript/projects/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export function iconForLayerType(type: Layer['type']) {
return "fa-draw-polygon"
case "AtiLayer":
return "fa-tree"
case "KewLayer":
return "fa-leaf"
default:
return "fa-layer-group"
}
Expand Down

0 comments on commit d3342b8

Please sign in to comment.