From b02ebf472ca039c6ef33e4469ed7017779a557bf Mon Sep 17 00:00:00 2001 From: Paul Crossley Date: Fri, 15 Nov 2024 18:10:09 +0000 Subject: [PATCH] IDW --- .../modelling/worker/interpolation.ts | 39 ++++++++++++---- .../components/interpolation_component.ts | 44 ++++++++++++++++--- .../components/kew_samples_component.ts | 4 +- .../projects/modelling/controls/date.tsx | 2 + .../projects/modelling/controls/select.tsx | 2 + app/javascript/projects/node_component.tsx | 14 +++++- 6 files changed, 89 insertions(+), 16 deletions(-) diff --git a/app/javascript/modelling/worker/interpolation.ts b/app/javascript/modelling/worker/interpolation.ts index 543406a6..1fb89e92 100644 --- a/app/javascript/modelling/worker/interpolation.ts +++ b/app/javascript/modelling/worker/interpolation.ts @@ -2,10 +2,10 @@ import { getMedianCellSize } from "../../projects/modelling/components/cell_area import { BooleanTileGrid, NumericTileGrid } from "../../projects/modelling/tile_grid" import { kdTree } from 'kd-tree-javascript' -export type InterpolationType = "NearestNeighbour" | "Bilinear" +export type InterpolationType = "NearestNeighbour" | "Bilinear" | "InverseDistanceWeighting" | "RadialBasisFunction" -export function interpolateGrid(input : NumericTileGrid, mask : BooleanTileGrid, type: InterpolationType, maxDist: number) : NumericTileGrid { +export function interpolateGrid(input : NumericTileGrid, mask : BooleanTileGrid, type: InterpolationType, maxDist: number, p: number, k: number) : NumericTileGrid { const result = new NumericTileGrid(input.zoom, input.x, input.y, input.width, input.height) @@ -28,17 +28,40 @@ export function interpolateGrid(input : NumericTileGrid, mask : BooleanTileGrid, ['x', 'y', 'val'] ) - const tileSize = getMedianCellSize(input).length + const tile_length = getMedianCellSize(input).length result.iterate((x, y) => { switch (type) { case "NearestNeighbour": - const nearest = tree.nearest({x, y}, 1)[0] - const dist = nearest[1] * tileSize - if (maxDist === 0 || dist < maxDist) result.set(x, y, nearest[0].val) + const [n, d] = tree.nearest({x, y}, 1)[0] + const dist = d * tile_length + if (maxDist === 0 || dist < maxDist) result.set(x, y, n.val) break - case "Bilinear": - // WIP + case "InverseDistanceWeighting": + const neighbors = tree.nearest({x, y}, k === 0 ? points.length : k) + + let numerator = 0 + let denominator = 0 + + neighbors.forEach(([neighbor, distance]) => { + const adjustedDistance = distance * tile_length; + if (adjustedDistance === 0) { + // If distance is zero, return the neighbor's value directly + result.set(x, y, neighbor.val); + return; + }else if (maxDist !== 0 && adjustedDistance > maxDist) { + return; + } + + const weight = 1 / Math.pow(adjustedDistance, p); + numerator += weight * neighbor.val; + denominator += weight; + }); + + if (denominator !== 0) { + const interpolatedValue = numerator / denominator; + result.set(x, y, interpolatedValue); + } break default: break diff --git a/app/javascript/projects/modelling/components/interpolation_component.ts b/app/javascript/projects/modelling/components/interpolation_component.ts index 555d6edb..5f0a2261 100644 --- a/app/javascript/projects/modelling/components/interpolation_component.ts +++ b/app/javascript/projects/modelling/components/interpolation_component.ts @@ -5,12 +5,33 @@ import { ProjectProperties } from "." import { numberSocket, numericDataSocket } from "../socket_types" import { workerPool } from '../../../modelling/workerPool' import { maskFromExtentAndShape } from "../bounding_box" -import { NumericConstant } from "../numeric_constant" import { NumericTileGrid } from "../tile_grid" +import { SelectControl, SelectControlOptions } from "../controls/select" +import { InterpolationType } from "../../../modelling/worker/interpolation" +import { NumericConstant } from "../numeric_constant" + +interface InterpolationMethodOption extends SelectControlOptions { + value: InterpolationType +} + +const InterpolationMethods : InterpolationMethodOption[] = [ + { + id: 0, + name: 'Nearest Neighbour', + value: 'NearestNeighbour' + }, + { + id: 1, + name: "Inverse Distance Weighting", + value: 'InverseDistanceWeighting' + } +] export class InterpolationComponent extends BaseComponent { projectProps : ProjectProperties maxdist : number + p : number + closest_points : number cache : Map> constructor(projectProps : ProjectProperties) { @@ -18,12 +39,21 @@ export class InterpolationComponent extends BaseComponent { this.category = "Calculations" this.projectProps = projectProps this.maxdist = 50 + this.p = 2 + this.closest_points = 10 this.cache = new Map() } async builder(node: Node) { + + node.addControl(new SelectControl(this.editor, 'methodId', () => InterpolationMethods, () => {}, 'Method')) + node.addInput(new Input('input', 'Input', numericDataSocket)) - node.addInput(new Input('maxdist', `maxdist (default: ${this.maxdist})`, numberSocket)) + node.addInput(new Input('maxdist', `Max Distance (default: ${this.maxdist})`, numberSocket)) + node.addInput(new Input('p', `Power (default: ${this.p})`, numberSocket)) + node.addInput(new Input('closest_points', `Closest Points (default: ${this.closest_points})`, numberSocket)) + + node.addOutput(new Output('output', 'Output', numericDataSocket)) } @@ -40,7 +70,12 @@ export class InterpolationComponent extends BaseComponent { this.projectProps.mask ) - const maxDist = inputs['maxdist'].length > 0 ? (inputs['maxdist'][0] as NumericConstant).value : this.maxdist + + const method = InterpolationMethods[node.data.methodId as number ?? 0] + + const maxDist = inputs['maxdist'].length > 0 ? (inputs['maxdist'][0] as NumericConstant).value : this.maxdist + const p = inputs['p'].length > 0 ? (inputs['p'][0] as NumericConstant).value : this.p + const closest_points = inputs['closest_points'].length > 0 ? (inputs['closest_points'][0] as NumericConstant).value : this.closest_points const input = inputs['input'][0] as NumericTileGrid // TODO: Caching doesn't work @@ -53,9 +88,8 @@ export class InterpolationComponent extends BaseComponent { } } - const out = await workerPool.queue(async worker => - worker.interpolateGrid(inputs['input'][0], mask, "NearestNeighbour", maxDist) + worker.interpolateGrid(inputs['input'][0], mask, method.value, maxDist, p, closest_points) ) const map = this.cache.get(maxDist) || new Map() diff --git a/app/javascript/projects/modelling/components/kew_samples_component.ts b/app/javascript/projects/modelling/components/kew_samples_component.ts index a687a7c9..07220057 100644 --- a/app/javascript/projects/modelling/components/kew_samples_component.ts +++ b/app/javascript/projects/modelling/components/kew_samples_component.ts @@ -67,8 +67,8 @@ function applyFeaturesToGrid(features: Feature[], grid: NumericTileGrid, project if(value && geom){ // geom is point data const [fx, fy] = (geom as any).getCoordinates() - const extent = [fx-2, fy-2, fx+2, fy+2] - //const extent = [fx, fy, fx, fy] + //const extent = [fx-2, fy-2, fx+2, fy+2] + const extent = [fx, fy, fx, fy] const tileGrid = createXYZ() diff --git a/app/javascript/projects/modelling/controls/date.tsx b/app/javascript/projects/modelling/controls/date.tsx index ad2b1c30..b88af1bf 100644 --- a/app/javascript/projects/modelling/controls/date.tsx +++ b/app/javascript/projects/modelling/controls/date.tsx @@ -35,9 +35,11 @@ const DateField = ({ getValue, setValue, label }: DateFieldProps) => { export class DateControl extends Control { props: DateFieldProps component: (props: DateFieldProps) => JSX.Element + type: string constructor(emitter: Emitter | null, key: string) { super(key) + this.type = "DateControl" const process = debounce(() => emitter?.trigger("process"), 1000) this.props = { diff --git a/app/javascript/projects/modelling/controls/select.tsx b/app/javascript/projects/modelling/controls/select.tsx index 584bab5a..e1f76248 100644 --- a/app/javascript/projects/modelling/controls/select.tsx +++ b/app/javascript/projects/modelling/controls/select.tsx @@ -104,9 +104,11 @@ const SelectInput = ({ emitter, getId, setId, getOptions, change, label }: Selec export class SelectControl extends Control { props: SelectControlProps component: (props: SelectControlProps) => JSX.Element + type: string constructor(emitter: Emitter | null, key: string, getOptions: () => Array, change: () => void, label: string | undefined = undefined) { super(key) + this.type = 'SelectControl' this.props = { emitter, diff --git a/app/javascript/projects/node_component.tsx b/app/javascript/projects/node_component.tsx index 797c1a16..547252d6 100644 --- a/app/javascript/projects/node_component.tsx +++ b/app/javascript/projects/node_component.tsx @@ -28,6 +28,10 @@ export class NodeComponent extends Node { const { node, editor, bindSocket, bindControl } = this.props const { outputs, controls, inputs, selected } = this.state + const select_controls = controls.filter((control: any) => control.type && control.type === "SelectControl") + const date_controls = controls.filter((control: any) => control.type && control.type === "DateControl") + const non_select_controls = controls.filter((control: any) => control.type !== "SelectControl" && control.type !== "DateControl") + setTimeout(() => $('[title]').tooltip('dispose').tooltip()) return ( @@ -73,6 +77,14 @@ export class NodeComponent extends Node { /> } + + {select_controls.map(control => ( + + ))} + {date_controls.map(control => ( + + ))} +
{inputs.map(input => ( @@ -92,7 +104,7 @@ export class NodeComponent extends Node { ))}
- {controls.map(control => ( + {non_select_controls.map(control => ( ))}