Skip to content

Commit

Permalink
Merge pull request #329 from wearepal/ati-modelview
Browse files Browse the repository at this point in the history
Updated ATI on map view and added to model view as component
  • Loading branch information
paulthatjazz authored Feb 6, 2024
2 parents 8228f49 + 47f3dd8 commit c0db96c
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 19 deletions.
3 changes: 1 addition & 2 deletions app/javascript/projects/layer_palette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,7 @@ export const LayerPalette = ({ addLayer, hide, dbModels, getTeamDatasets, teamNa
colors: {
ancient: [255, 183, 0, 1],
veteran: [0, 82, 11, 1],
lost_ancient: [245, 218, 149, 1],
lost_veteran: [77, 92, 79, 1],
notable: [158, 52, 235, 1],
public: [0, 217, 255, 1],
private: [235, 0, 0, 1],
}
Expand Down
193 changes: 193 additions & 0 deletions app/javascript/projects/modelling/components/ati_component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { createXYZ } from "ol/tilegrid"
import { Node, Output, Socket } from "rete"
import { NodeData, WorkerInputs, WorkerOutputs } from "rete/types/core/data"
import { booleanDataSocket, categoricalDataSocket } from "../socket_types"
import { BooleanTileGrid, CategoricalTileGrid } from "../tile_grid"
import { BaseComponent } from "./base_component"
import { Extent } from "ol/extent"
import { bboxFromExtent } from "../bounding_box"
import { GeoJSON } from "ol/format";
import { find } from "lodash"

interface TreeType {
id : number
value : string
socket : Socket
}

const trees: TreeType[] = [
{
id: 1,
value: "Ancient tree",
socket: booleanDataSocket
},
{
id: 2,
value: "Lost Ancient tree",
socket: booleanDataSocket
},
{
id: 3,
value: "Veteran tree",
socket: booleanDataSocket
},
{
id: 4,
value: "Lost Veteran tree",
socket: booleanDataSocket
},
{
id: 5,
value: "Notable tree",
socket: booleanDataSocket
},
{
id: 6,
value: "Lost Notable tree",
socket: booleanDataSocket
},
{
id: 0,
value: "All",
socket: categoricalDataSocket
}
]


async function renderCategoricalData(extent: Extent, zoom: number) : Promise<CategoricalTileGrid>{

const tileGrid = createXYZ()
const outputTileRange = tileGrid.getTileRangeForExtentAndZ(extent, zoom)

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

if (!response.ok) throw new Error()

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

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


const treeCrown = 2.5

for (const feature of features) {

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

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

const key = find(trees, hab => hab.value === val)
if (key?.id === undefined) { continue }

const expandedExtent = [geom.getExtent()[0] - treeCrown, geom.getExtent()[1] - treeCrown, geom.getExtent()[2] + treeCrown, geom.getExtent()[3] + treeCrown]

const featureTileRange = tileGrid.getTileRangeForExtentAndZ(
expandedExtent,
zoom
)
for (
let x = Math.max(featureTileRange.minX, outputTileRange.minX);
x <= Math.min(featureTileRange.maxX, outputTileRange.maxX);
++x
) {
for (
let y = Math.max(featureTileRange.minY, outputTileRange.minY);
y <= Math.min(featureTileRange.maxY, outputTileRange.maxY);
++y
) {
result.set(x, y, key?.id ?? 255)
}
}

}

const labels = new Map()
trees.forEach(type => {
if(type.id === 0) return
labels.set(type.id, type.value)
})
result.setLabels(labels)

return result

}

export class ATIComponent extends BaseComponent {
categoricalData: CategoricalTileGrid | null
outputCache: Map<number, BooleanTileGrid>
projectExtent: Extent
projectZoom: number
zoom: number

constructor(projectExtent: Extent, projectZoom: number) {
super("Ancient Tree Inventory")
this.category = "Inputs"
this.categoricalData = null
this.outputCache = new Map()
this.projectExtent = projectExtent
this.projectZoom = projectZoom
}


async builder(node: Node) {
node.meta.toolTip = "ATI (Ancient Tree Inventory) data."
node.meta.toolTipLink = "https://ati.woodlandtrust.org.uk/"

trees.forEach(type => {
node.addOutput(new Output(
type.id.toString(),
type.value,
type.socket
))
})

}


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

if (this.categoricalData === null) {
this.categoricalData = await renderCategoricalData(this.projectExtent, this.projectZoom)
}
const categoricalData = this.categoricalData!

trees.filter(
type => node.outputs[type.id].connections.length > 0
).forEach(type => {
if (type.id == 0) {
outputs[type.id] = this.categoricalData
} else {
if (this.outputCache.has(type.id)) {
outputs[type.id] = this.outputCache.get(type.id)
}
else {
const out = outputs[type.id] = new BooleanTileGrid(categoricalData.zoom, categoricalData.x, categoricalData.y, categoricalData.width, categoricalData.height)
out.name = type.value

categoricalData.iterate((x, y, value) => out.set(x, y, value === type.id))

this.outputCache.set(type.id, out)
}
}
})
}
}
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 @@ -30,6 +30,7 @@ import { ReplaceNaNComponent } from "./replace_nan_component"
import { BiodiversityComponent } from "./biodiversity_component"
import { LehLandCoverComponent } from "./leh_land_cover_component"
import { MlTreeHedgeComponent } from "./ml_tree_hedge_component"
import { ATIComponent } from "./ati_component"

export function createDefaultComponents(saveMapLayer: SaveMapLayer, saveModel: SaveModel, getDatasets: getDatasets, extent: Extent, zoom: number): BaseComponent[] {
return [
Expand All @@ -46,6 +47,7 @@ export function createDefaultComponents(saveMapLayer: SaveMapLayer, saveModel: S
new CensusComponent(extent, zoom),
new OSGreenSpacesComponent(extent, zoom),
new CROMEComponent(extent, zoom),
new ATIComponent(extent, zoom),

// Outputs
new MapLayerComponent(saveMapLayer),
Expand Down
48 changes: 35 additions & 13 deletions app/javascript/projects/reify_layer/ati.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,40 @@ import { AtiLayer, ColourMapATI } from "../state"
import VectorLayer from "ol/layer/Vector"
import VectorSource from "ol/source/Vector"
import GeoJSON from "ol/format/GeoJSON"
import { Circle, Fill, Stroke, Style } from "ol/style"
import { Circle, Fill, RegularShape, Stroke, Style } from "ol/style"
import { bbox } from "ol/loadingstrategy"
import { Map } from "ol"
import Select from "ol/interaction/Select"
import { click } from "ol/events/condition"
import { bboxFromExtent } from "../modelling/bounding_box"

const statusToColor = (status: string, colors: ColourMapATI) => {
switch (status) {
case "Ancient tree": return `rgba(${colors.ancient[0]}, ${colors.ancient[1]}, ${colors.ancient[2]}, 1)`
case "Lost Ancient tree": return `rgba(${colors.lost_ancient[0]}, ${colors.lost_ancient[1]}, ${colors.lost_ancient[2]}, 1)`
case "Veteran tree": return `rgba(${colors.veteran[0]}, ${colors.veteran[1]}, ${colors.veteran[2]}, 1)`
case "Lost Veteran tree": return `rgba(${colors.lost_veteran[0]}, ${colors.lost_veteran[1]}, ${colors.lost_veteran[2]}, 1)`
default: return 'rgba(0, 255, 251, 1)'
case "Ancient tree":
case "Lost Ancient tree":
return `rgba(${colors.ancient[0]}, ${colors.ancient[1]}, ${colors.ancient[2]}, 1)`
case "Veteran tree":
case "Lost Veteran tree":
return `rgba(${colors.veteran[0]}, ${colors.veteran[1]}, ${colors.veteran[2]}, 1)`
case "Notable tree":
case "Lost Notable tree":
return `rgba(${colors.notable[0]}, ${colors.notable[1]}, ${colors.notable[2]}, 1)`
default:
return 'rgba(0, 255, 251, 1)'
}
}

const accessibilityToStroke = (accessibility: string, colors: ColourMapATI) => {
switch (accessibility) {
case "Public": return `rgba(${colors.public[0]}, ${colors.public[1]}, ${colors.public[2]}, 1)`
case "Private": return `rgba(${colors.private[0]}, ${colors.private[1]}, ${colors.private[2]}, 1)`
default: return 'rgba(255, 255, 255, 1)'
default: return 'rgba(200, 200, 200, 1)'
}
}

const getSource = () => (
new VectorSource({
url: extent => `https://services-eu1.arcgis.com/WIfgdJeDbrZU1cnA/arcgis/rest/services/Ancient_Tree_Inventory_ATI/FeatureServer/29/query?where=1%3D1&outFields=*&geometry=${extent.join(",")}&geometryType=esriGeometryEnvelope&inSR=3857&spatialRel=esriSpatialRelIntersects&outSR=3857&f=geojson`,
url: extent => `https://landscapes.wearepal.ai/geoserver/nateng/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=nateng:ATIData_Public&bbox=${bboxFromExtent(extent)}&outputFormat=application%2Fjson`,
attributions: '&copy; <a href="https://ati.woodlandtrust.org.uk/">Woodland Trust ATI</a>',
format: new GeoJSON(),
strategy: bbox,
Expand All @@ -40,12 +47,13 @@ const getStyleFunction = (layer: AtiLayer, zoom: number | undefined) => (
(feature) => {
const properties = feature.getProperties()

const status = properties.VeteranStatus
const accessibility = properties.PublicAccessibilityStatus
const status = properties.VeteranSta
const accessibility = properties.PublicAcce
const lost = (properties.VeteranSta === "Lost Veteran tree" || properties.VeteranSta === "Lost Ancient tree" || properties.VeteranSta === "Lost Notable tree")

const pointStyle = new Style({
image: new Circle({
radius: zoom ? (zoom > 10 ? 6 : 3) : 3,
radius: zoom ? (zoom > 16 ? 10 : 5) : 5,
fill: new Fill({
color: statusToColor(status, layer.colors)
}),
Expand All @@ -56,7 +64,20 @@ const getStyleFunction = (layer: AtiLayer, zoom: number | undefined) => (
})
})

return [pointStyle]
const cross = new Style({
image: new RegularShape({
stroke: new Stroke({
color: 'rgba(255, 0, 0, 1)',
width: 3
}),
points: 4,
radius: zoom ? (zoom > 16 ? 10 : 5) : 5,
radius2: 0,
angle: Math.PI / 4,
})
})

return !lost ? [pointStyle] : [pointStyle, cross]
}
)

Expand All @@ -71,7 +92,8 @@ export function reifyAtiLayer (layer: AtiLayer, existingLayer: BaseLayer | null,

const vectLayer = new VectorLayer({
source: getSource(),
style: getStyleFunction(layer, map.getView().getZoom())
style: getStyleFunction(layer, map.getView().getZoom()),
minZoom: 10,
})

const select = new Select({
Expand Down
5 changes: 3 additions & 2 deletions app/javascript/projects/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,15 @@ interface ATILayerSettingsProps {
const ATILayerSettings = ({ layer }: ATILayerSettingsProps) => {

const colors = layer.colors
// backwards compatibility for old layers
const notable = colors.notable ?? [158, 52, 235, 1]

return (
<details className="mt-3">
<summary>Legend</summary>
<span className="swatch" style={{ backgroundColor: `rgb(${colors.ancient[0]},${colors.ancient[1]},${colors.ancient[2]},1)` }} /> Ancient Tree<br />
<span className="swatch" style={{ backgroundColor: `rgb(${colors.veteran[0]},${colors.veteran[1]},${colors.veteran[2]},1)` }} /> Veteran Tree<br />
<span className="swatch" style={{ backgroundColor: `rgb(${colors.lost_ancient[0]},${colors.lost_ancient[1]},${colors.lost_ancient[2]},1)` }} /> Lost Ancient Tree<br />
<span className="swatch" style={{ backgroundColor: `rgb(${colors.lost_veteran[0]},${colors.lost_veteran[1]},${colors.lost_veteran[2]},1)` }} /> Lost Veteran Tree<br />
<span className="swatch" style={{ backgroundColor: `rgb(${notable[0]},${notable[1]},${notable[2]},1)` }} /> Notable Tree<br />
<br />
<span className="swatch" style={{ backgroundColor: `rgb(${colors.public[0]},${colors.public[1]},${colors.public[2]},1)` }} /> Public<br />
<span className="swatch" style={{ backgroundColor: `rgb(${colors.private[0]},${colors.private[1]},${colors.private[2]},1)` }} /> Private<br />
Expand Down
3 changes: 1 addition & 2 deletions app/javascript/projects/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ interface BaseLayer {

export interface ColourMapATI {
ancient: [r: number, g: number, b: number, a: number]
lost_ancient: [r: number, g: number, b: number, a: number]
veteran: [r: number, g: number, b: number, a: number]
lost_veteran: [r: number, g: number, b: number, a: number]
notable: [r: number, g: number, b: number, a: number]
public: [r: number, g: number, b: number, a: number]
private: [r: number, g: number, b: number, a: number]
}
Expand Down

0 comments on commit c0db96c

Please sign in to comment.