Skip to content

Commit cb82d23

Browse files
authored
Merge pull request #448 from wearepal/idw-interpolation
Inverse Distance Weighting Interpolation
2 parents 3fb6654 + b02ebf4 commit cb82d23

File tree

6 files changed

+89
-16
lines changed

6 files changed

+89
-16
lines changed

app/javascript/modelling/worker/interpolation.ts

+31-8
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { getMedianCellSize } from "../../projects/modelling/components/cell_area
22
import { BooleanTileGrid, NumericTileGrid } from "../../projects/modelling/tile_grid"
33
import { kdTree } from 'kd-tree-javascript'
44

5-
export type InterpolationType = "NearestNeighbour" | "Bilinear"
5+
export type InterpolationType = "NearestNeighbour" | "Bilinear" | "InverseDistanceWeighting" | "RadialBasisFunction"
66

77

8-
export function interpolateGrid(input : NumericTileGrid, mask : BooleanTileGrid, type: InterpolationType, maxDist: number) : NumericTileGrid {
8+
export function interpolateGrid(input : NumericTileGrid, mask : BooleanTileGrid, type: InterpolationType, maxDist: number, p: number, k: number) : NumericTileGrid {
99

1010
const result = new NumericTileGrid(input.zoom, input.x, input.y, input.width, input.height)
1111

@@ -28,17 +28,40 @@ export function interpolateGrid(input : NumericTileGrid, mask : BooleanTileGrid,
2828
['x', 'y', 'val']
2929
)
3030

31-
const tileSize = getMedianCellSize(input).length
31+
const tile_length = getMedianCellSize(input).length
3232

3333
result.iterate((x, y) => {
3434
switch (type) {
3535
case "NearestNeighbour":
36-
const nearest = tree.nearest({x, y}, 1)[0]
37-
const dist = nearest[1] * tileSize
38-
if (maxDist === 0 || dist < maxDist) result.set(x, y, nearest[0].val)
36+
const [n, d] = tree.nearest({x, y}, 1)[0]
37+
const dist = d * tile_length
38+
if (maxDist === 0 || dist < maxDist) result.set(x, y, n.val)
3939
break
40-
case "Bilinear":
41-
// WIP
40+
case "InverseDistanceWeighting":
41+
const neighbors = tree.nearest({x, y}, k === 0 ? points.length : k)
42+
43+
let numerator = 0
44+
let denominator = 0
45+
46+
neighbors.forEach(([neighbor, distance]) => {
47+
const adjustedDistance = distance * tile_length;
48+
if (adjustedDistance === 0) {
49+
// If distance is zero, return the neighbor's value directly
50+
result.set(x, y, neighbor.val);
51+
return;
52+
}else if (maxDist !== 0 && adjustedDistance > maxDist) {
53+
return;
54+
}
55+
56+
const weight = 1 / Math.pow(adjustedDistance, p);
57+
numerator += weight * neighbor.val;
58+
denominator += weight;
59+
});
60+
61+
if (denominator !== 0) {
62+
const interpolatedValue = numerator / denominator;
63+
result.set(x, y, interpolatedValue);
64+
}
4265
break
4366
default:
4467
break

app/javascript/projects/modelling/components/interpolation_component.ts

+39-5
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,55 @@ import { ProjectProperties } from "."
55
import { numberSocket, numericDataSocket } from "../socket_types"
66
import { workerPool } from '../../../modelling/workerPool'
77
import { maskFromExtentAndShape } from "../bounding_box"
8-
import { NumericConstant } from "../numeric_constant"
98
import { NumericTileGrid } from "../tile_grid"
9+
import { SelectControl, SelectControlOptions } from "../controls/select"
10+
import { InterpolationType } from "../../../modelling/worker/interpolation"
11+
import { NumericConstant } from "../numeric_constant"
12+
13+
interface InterpolationMethodOption extends SelectControlOptions {
14+
value: InterpolationType
15+
}
16+
17+
const InterpolationMethods : InterpolationMethodOption[] = [
18+
{
19+
id: 0,
20+
name: 'Nearest Neighbour',
21+
value: 'NearestNeighbour'
22+
},
23+
{
24+
id: 1,
25+
name: "Inverse Distance Weighting",
26+
value: 'InverseDistanceWeighting'
27+
}
28+
]
1029

1130
export class InterpolationComponent extends BaseComponent {
1231
projectProps : ProjectProperties
1332
maxdist : number
33+
p : number
34+
closest_points : number
1435
cache : Map<number, Map<NumericTileGrid, NumericTileGrid>>
1536

1637
constructor(projectProps : ProjectProperties) {
1738
super("Interpolation")
1839
this.category = "Calculations"
1940
this.projectProps = projectProps
2041
this.maxdist = 50
42+
this.p = 2
43+
this.closest_points = 10
2144
this.cache = new Map()
2245
}
2346

2447
async builder(node: Node) {
48+
49+
node.addControl(new SelectControl(this.editor, 'methodId', () => InterpolationMethods, () => {}, 'Method'))
50+
2551
node.addInput(new Input('input', 'Input', numericDataSocket))
26-
node.addInput(new Input('maxdist', `maxdist (default: ${this.maxdist})`, numberSocket))
52+
node.addInput(new Input('maxdist', `Max Distance (default: ${this.maxdist})`, numberSocket))
53+
node.addInput(new Input('p', `Power (default: ${this.p})`, numberSocket))
54+
node.addInput(new Input('closest_points', `Closest Points (default: ${this.closest_points})`, numberSocket))
55+
56+
2757
node.addOutput(new Output('output', 'Output', numericDataSocket))
2858
}
2959

@@ -40,7 +70,12 @@ export class InterpolationComponent extends BaseComponent {
4070
this.projectProps.mask
4171
)
4272

43-
const maxDist = inputs['maxdist'].length > 0 ? (inputs['maxdist'][0] as NumericConstant).value : this.maxdist
73+
74+
const method = InterpolationMethods[node.data.methodId as number ?? 0]
75+
76+
const maxDist = inputs['maxdist'].length > 0 ? (inputs['maxdist'][0] as NumericConstant).value : this.maxdist
77+
const p = inputs['p'].length > 0 ? (inputs['p'][0] as NumericConstant).value : this.p
78+
const closest_points = inputs['closest_points'].length > 0 ? (inputs['closest_points'][0] as NumericConstant).value : this.closest_points
4479
const input = inputs['input'][0] as NumericTileGrid
4580

4681
// TODO: Caching doesn't work
@@ -53,9 +88,8 @@ export class InterpolationComponent extends BaseComponent {
5388
}
5489
}
5590

56-
5791
const out = await workerPool.queue(async worker =>
58-
worker.interpolateGrid(inputs['input'][0], mask, "NearestNeighbour", maxDist)
92+
worker.interpolateGrid(inputs['input'][0], mask, method.value, maxDist, p, closest_points)
5993
)
6094

6195
const map = this.cache.get(maxDist) || new Map()

app/javascript/projects/modelling/components/kew_samples_component.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ function applyFeaturesToGrid(features: Feature[], grid: NumericTileGrid, project
6767
if(value && geom){
6868
// geom is point data
6969
const [fx, fy] = (geom as any).getCoordinates()
70-
const extent = [fx-2, fy-2, fx+2, fy+2]
71-
//const extent = [fx, fy, fx, fy]
70+
//const extent = [fx-2, fy-2, fx+2, fy+2]
71+
const extent = [fx, fy, fx, fy]
7272

7373
const tileGrid = createXYZ()
7474

app/javascript/projects/modelling/controls/date.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ const DateField = ({ getValue, setValue, label }: DateFieldProps) => {
3535
export class DateControl extends Control {
3636
props: DateFieldProps
3737
component: (props: DateFieldProps) => JSX.Element
38+
type: string
3839

3940
constructor(emitter: Emitter<EventsTypes> | null, key: string) {
4041
super(key)
42+
this.type = "DateControl"
4143

4244
const process = debounce(() => emitter?.trigger("process"), 1000)
4345
this.props = {

app/javascript/projects/modelling/controls/select.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,11 @@ const SelectInput = ({ emitter, getId, setId, getOptions, change, label }: Selec
104104
export class SelectControl extends Control {
105105
props: SelectControlProps
106106
component: (props: SelectControlProps) => JSX.Element
107+
type: string
107108

108109
constructor(emitter: Emitter<EventsTypes> | null, key: string, getOptions: () => Array<SelectControlOptions>, change: () => void, label: string | undefined = undefined) {
109110
super(key)
111+
this.type = 'SelectControl'
110112

111113
this.props = {
112114
emitter,

app/javascript/projects/node_component.tsx

+13-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export class NodeComponent extends Node {
2828
const { node, editor, bindSocket, bindControl } = this.props
2929
const { outputs, controls, inputs, selected } = this.state
3030

31+
const select_controls = controls.filter((control: any) => control.type && control.type === "SelectControl")
32+
const date_controls = controls.filter((control: any) => control.type && control.type === "DateControl")
33+
const non_select_controls = controls.filter((control: any) => control.type !== "SelectControl" && control.type !== "DateControl")
34+
3135
setTimeout(() => $('[title]').tooltip('dispose').tooltip())
3236

3337
return (
@@ -73,6 +77,14 @@ export class NodeComponent extends Node {
7377
/>
7478
}
7579
</div>
80+
81+
{select_controls.map(control => (
82+
<Control className="control" key={control.key} control={control} innerRef={bindControl} />
83+
))}
84+
{date_controls.map(control => (
85+
<Control className="control" key={control.key} control={control} innerRef={bindControl} />
86+
))}
87+
7688
<div style={{ display: "flex", justifyContent: "space-between" }}>
7789
<div>
7890
{inputs.map(input => (
@@ -92,7 +104,7 @@ export class NodeComponent extends Node {
92104
))}
93105
</div>
94106
</div>
95-
{controls.map(control => (
107+
{non_select_controls.map(control => (
96108
<Control className="control" key={control.key} control={control} innerRef={bindControl} />
97109
))}
98110
</div>

0 commit comments

Comments
 (0)