From 812dace7e10384eb9f64f164d5a879480b4ad0c4 Mon Sep 17 00:00:00 2001 From: Foxino Date: Wed, 17 Apr 2024 17:41:29 +0100 Subject: [PATCH 1/2] init --- .../projects/modelling/components/index.ts | 2 + .../modelling/components/orval_component.ts | 95 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 app/javascript/projects/modelling/components/orval_component.ts diff --git a/app/javascript/projects/modelling/components/index.ts b/app/javascript/projects/modelling/components/index.ts index 092184eb..5c0b5915 100644 --- a/app/javascript/projects/modelling/components/index.ts +++ b/app/javascript/projects/modelling/components/index.ts @@ -32,6 +32,7 @@ import { LehLandCoverComponent } from "./leh_land_cover_component" import { MlTreeHedgeComponent } from "./ml_tree_hedge_component" import { ATIComponent } from "./ati_component" import { DesignationsComponent } from "./designations_component" +import { ORValComponent } from "./orval_component" export function createDefaultComponents(saveMapLayer: SaveMapLayer, saveModel: SaveModel, getDatasets: getDatasets, extent: Extent, zoom: number): BaseComponent[] { return [ @@ -41,6 +42,7 @@ export function createDefaultComponents(saveMapLayer: SaveMapLayer, saveModel: S new MlTreeHedgeComponent(extent, zoom), new BiodiversityComponent(extent, zoom), new NevoLayerComponent(extent, zoom), + new ORValComponent(extent, zoom), new OSMLandUseComponent(extent, zoom), new NumericConstantComponent(), new DigitalModelComponent(extent, zoom), diff --git a/app/javascript/projects/modelling/components/orval_component.ts b/app/javascript/projects/modelling/components/orval_component.ts new file mode 100644 index 00000000..964ad783 --- /dev/null +++ b/app/javascript/projects/modelling/components/orval_component.ts @@ -0,0 +1,95 @@ +import { Extent } from "ol/extent"; +import { NodeData, WorkerInputs, WorkerOutputs } from "rete/types/core/data"; +import { BaseComponent } from "./base_component" +import { Node, Output, Socket } from "rete" +import { booleanDataSocket, categoricalDataSocket } from "../socket_types"; +import { createXYZ } from "ol/tilegrid"; +import { retrieveModelData } from "../model_retrieval"; +import { BooleanTileGrid } from "../tile_grid"; +import { TypedArray } from "d3"; + +interface OutputData { + name: string + prettyName: string + socket: Socket + layer: string + fn?: (extent: Extent, zoom: number, type: string, layer: string) => Promise +} + +const OutputDatas : OutputData[] = [ + { + name: "Paths", + prettyName: "Paths", + socket: booleanDataSocket, + layer: "ORVAL:paths_england", + fn: retrievePathData + } +] + +async function retrievePathData(extent: Extent, zoom: number, type: string, layer: string) { + const tileGrid = createXYZ() + const outputTileRange = tileGrid.getTileRangeForExtentAndZ(extent, zoom) + + const geotiff = await retrieveModelData(extent, layer, outputTileRange) + + const rasters = await geotiff.readRasters({ bbox: extent, width: outputTileRange.getWidth(), height: outputTileRange.getHeight() }) + const image = await geotiff.getImage() + + const result = new BooleanTileGrid( + zoom, + outputTileRange.minX, + outputTileRange.minY, + outputTileRange.getWidth(), + outputTileRange.getHeight() + ) + + for (let i = 0; i < (rasters[0] as TypedArray).length; i++) { + + let x = (outputTileRange.minX + i % image.getWidth()) + let y = (outputTileRange.minY + Math.floor(i / image.getWidth())) + + result.set(x, y, !rasters[0][i]) + + } + + return result +} + +export class ORValComponent extends BaseComponent { + projectExtent: Extent + projectZoom: number + + constructor(projectExtent: Extent, projectZoom: number) { + super("ORVal") + this.category = "Inputs" + this.projectExtent = projectExtent + this.projectZoom = projectZoom + } + + async builder(node: Node) { + OutputDatas.forEach(outputData => node.addOutput(new Output(outputData.name, outputData.prettyName, outputData.socket))) + } + + async worker(node: NodeData, inputs: WorkerInputs, outputs: WorkerOutputs, ...args: unknown[]) { + + const editorNode = this.editor?.nodes.find(n => n.id === node.id) + if (editorNode === undefined) { return } + + const promises = OutputDatas.filter(d => node.outputs[d.name].connections.length > 0) + .map(d => { + if (d.fn) { + return d.fn(this.projectExtent, this.projectZoom, d.name, d.layer) + .then(data => { + outputs[d.name] = data; + }) + } + return null + }) + + await Promise.all(promises) + + editorNode.update() + + } + +} \ No newline at end of file From af089d083ee169f1224e95a55c3d84ded43e0c99 Mon Sep 17 00:00:00 2001 From: Foxino Date: Mon, 22 Apr 2024 15:51:30 +0100 Subject: [PATCH 2/2] Orval model view --- .../modelling/components/orval_component.ts | 261 +++++++++++++++++- .../projects/modelling/model_retrieval.ts | 4 +- 2 files changed, 248 insertions(+), 17 deletions(-) diff --git a/app/javascript/projects/modelling/components/orval_component.ts b/app/javascript/projects/modelling/components/orval_component.ts index 964ad783..db7dadf1 100644 --- a/app/javascript/projects/modelling/components/orval_component.ts +++ b/app/javascript/projects/modelling/components/orval_component.ts @@ -1,19 +1,26 @@ -import { Extent } from "ol/extent"; -import { NodeData, WorkerInputs, WorkerOutputs } from "rete/types/core/data"; +import { Extent } from "ol/extent" +import { NodeData, WorkerInputs, WorkerOutputs } from "rete/types/core/data" import { BaseComponent } from "./base_component" import { Node, Output, Socket } from "rete" -import { booleanDataSocket, categoricalDataSocket } from "../socket_types"; +import { booleanDataSocket } from "../socket_types" import { createXYZ } from "ol/tilegrid"; -import { retrieveModelData } from "../model_retrieval"; -import { BooleanTileGrid } from "../tile_grid"; -import { TypedArray } from "d3"; +import { retrieveModelData } from "../model_retrieval" +import { BooleanTileGrid } from "../tile_grid" +import { TypedArray } from "d3" +import { bboxFromExtent } from "../bounding_box" +import { GeoJSON } from "ol/format" +import { Feature } from "ol" +import { Geometry } from "ol/geom" + + +const FeaturesCache : Feature[] | null = null interface OutputData { name: string prettyName: string socket: Socket layer: string - fn?: (extent: Extent, zoom: number, type: string, layer: string) => Promise + fn: (extent: Extent, zoom: number, type: string, layer: string) => Promise } const OutputDatas : OutputData[] = [ @@ -23,9 +30,230 @@ const OutputDatas : OutputData[] = [ socket: booleanDataSocket, layer: "ORVAL:paths_england", fn: retrievePathData + }, + { + name: "Path Access", + prettyName: "Path Access", + socket: booleanDataSocket, + layer: "ORVAL:paths_england_accesspts", + fn: retrievePathData + }, + { + name: "Beaches", + prettyName: "Beaches", + socket: booleanDataSocket, + layer: "ORVAL:beaches_england", + fn: retrievePathData + }, + { + name: "Parks - any", + prettyName: "Parks - any", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrievePathData + }, + { + name: "allotment", + prettyName: "Parks - Allotment", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData + }, + { + name: "anemity_park", + prettyName: "Parks - Amenity Park", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData + }, + { + name: "anemity_woods", + prettyName: "Parks - Amenity Woods", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData + }, + { + name: "cemetery", + prettyName: "Parks - Cemetery", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData + }, + { + name: "churchyard", + prettyName: "Parks - Churchyard", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData + }, + { + name: "club", + prettyName: "Parks - Club", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData + }, + { + name: "common", + prettyName: "Parks - Common", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData + }, + { + name: "country_park", + prettyName: "Parks - Country Park", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData + }, + { + name: "doorstep_green", + prettyName: "Parks - Doorstep Green", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData + }, + { + name: "FC_woods", + prettyName: "Parks - FC Woods", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData + }, + { + name: "garden", + prettyName: "Parks - Garden", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData + }, + { + name: "golf", + prettyName: "Parks - Golf", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData + }, + { + name: "grave_yard", + prettyName: "Parks - Grave Yard", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData + }, + { + name: "millenium_green", + prettyName: "Parks - Millenium Green", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData + }, + { + name: "nature", + prettyName: "Parks - Nature", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData + }, + { + name: "park", + prettyName: "Parks - Park", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData + }, + { + name: "recreation_ground", + prettyName: "Parks - Recreation Ground", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData + }, + { + name: "village_green", + prettyName: "Parks - Village Green", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData + }, + { + name: "wood", + prettyName: "Parks - Wood", + socket: booleanDataSocket, + layer: "ORVAL:parks_england", + fn: retrieveCatData } ] +async function retrieveCatData(extent: Extent, zoom: number, type: string, layer: string) { + + const tileGrid = createXYZ() + const outputTileRange = tileGrid.getTileRangeForExtentAndZ(extent, zoom) + + let features = FeaturesCache + + if (features === null) { + + const response = await fetch( + "https://landscapes.wearepal.ai/geoserver/wfs?" + + new URLSearchParams( + { + outputFormat: 'application/json', + request: 'GetFeature', + typeName: layer, + srsName: 'EPSG:3857', + bbox : bboxFromExtent(extent), + } + ) + ) + + if (!response.ok) throw new Error() + + features = new GeoJSON().readFeatures(await response.json()) + + } + + const result = new BooleanTileGrid( + zoom, + outputTileRange.minX, + outputTileRange.minY, + outputTileRange.getWidth(), + outputTileRange.getHeight() + ) + + for (let feature of features) { + const geom = feature.getGeometry() + if (geom === undefined) { continue } + + const featureTileRange = tileGrid.getTileRangeForExtentAndZ( + geom.getExtent(), + 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 + ) { + const center = tileGrid.getTileCoordCenter([zoom, x, y]) + if (geom.intersectsCoordinate(center) && feature.get("TYPE") === type) { + result.set(x, y, true) + } + } + } + + } + + return result +} + async function retrievePathData(extent: Extent, zoom: number, type: string, layer: string) { const tileGrid = createXYZ() const outputTileRange = tileGrid.getTileRangeForExtentAndZ(extent, zoom) @@ -48,7 +276,7 @@ async function retrievePathData(extent: Extent, zoom: number, type: string, laye let x = (outputTileRange.minX + i % image.getWidth()) let y = (outputTileRange.minY + Math.floor(i / image.getWidth())) - result.set(x, y, !rasters[0][i]) + result.set(x, y, rasters[3][i]) } @@ -58,15 +286,19 @@ async function retrievePathData(extent: Extent, zoom: number, type: string, laye export class ORValComponent extends BaseComponent { projectExtent: Extent projectZoom: number + outputCache: Map constructor(projectExtent: Extent, projectZoom: number) { super("ORVal") this.category = "Inputs" this.projectExtent = projectExtent this.projectZoom = projectZoom + this.outputCache = new Map() } async builder(node: Node) { + node.meta.toolTip = "Data from ORVal (Outdoor Recreation Valuation Tool)" + node.meta.toolTipLink = "https://www.leep.exeter.ac.uk/orval/" OutputDatas.forEach(outputData => node.addOutput(new Output(outputData.name, outputData.prettyName, outputData.socket))) } @@ -77,13 +309,12 @@ export class ORValComponent extends BaseComponent { const promises = OutputDatas.filter(d => node.outputs[d.name].connections.length > 0) .map(d => { - if (d.fn) { - return d.fn(this.projectExtent, this.projectZoom, d.name, d.layer) - .then(data => { - outputs[d.name] = data; - }) - } - return null + return this.outputCache.has(d.name) ? this.outputCache.get(d.name) : d.fn(this.projectExtent, this.projectZoom, d.name, d.layer) + .then(data => { + data.name = d.name + this.outputCache.set(d.name, data) + outputs[d.name] = data + }) }) await Promise.all(promises) diff --git a/app/javascript/projects/modelling/model_retrieval.ts b/app/javascript/projects/modelling/model_retrieval.ts index 2fd70ffa..017fd5d4 100644 --- a/app/javascript/projects/modelling/model_retrieval.ts +++ b/app/javascript/projects/modelling/model_retrieval.ts @@ -4,7 +4,7 @@ import * as GeoTIFF from 'geotiff/dist-browser/geotiff' import { Extent } from 'ol/extent' import { bboxFromExtent } from './bounding_box' -export async function retrieveModelData(extent: Extent, source: string, tileRange: any) { +export async function retrieveModelData(extent: Extent, source: string, tileRange: any, style?: string) { // Uses WMS server: Returns data between 0 and 255 @@ -20,7 +20,7 @@ export async function retrieveModelData(extent: Extent, source: string, tileRang version: '1.3.0', request: 'GetMap', layers: source, - styles: '', + styles: style || '', format: 'image/geotiff', transparent: 'true', width,