Skip to content

Commit

Permalink
Merge pull request #448 from wearepal/idw-interpolation
Browse files Browse the repository at this point in the history
Inverse Distance Weighting Interpolation
  • Loading branch information
paulthatjazz authored Nov 15, 2024
2 parents 3fb6654 + b02ebf4 commit cb82d23
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 16 deletions.
39 changes: 31 additions & 8 deletions app/javascript/modelling/worker/interpolation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,55 @@ 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<number, Map<NumericTileGrid, NumericTileGrid>>

constructor(projectProps : ProjectProperties) {
super("Interpolation")
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))
}

Expand All @@ -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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
2 changes: 2 additions & 0 deletions app/javascript/projects/modelling/controls/date.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<EventsTypes> | null, key: string) {
super(key)
this.type = "DateControl"

const process = debounce(() => emitter?.trigger("process"), 1000)
this.props = {
Expand Down
2 changes: 2 additions & 0 deletions app/javascript/projects/modelling/controls/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<EventsTypes> | null, key: string, getOptions: () => Array<SelectControlOptions>, change: () => void, label: string | undefined = undefined) {
super(key)
this.type = 'SelectControl'

this.props = {
emitter,
Expand Down
14 changes: 13 additions & 1 deletion app/javascript/projects/node_component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -73,6 +77,14 @@ export class NodeComponent extends Node {
/>
}
</div>

{select_controls.map(control => (
<Control className="control" key={control.key} control={control} innerRef={bindControl} />
))}
{date_controls.map(control => (
<Control className="control" key={control.key} control={control} innerRef={bindControl} />
))}

<div style={{ display: "flex", justifyContent: "space-between" }}>
<div>
{inputs.map(input => (
Expand All @@ -92,7 +104,7 @@ export class NodeComponent extends Node {
))}
</div>
</div>
{controls.map(control => (
{non_select_controls.map(control => (
<Control className="control" key={control.key} control={control} innerRef={bindControl} />
))}
</div>
Expand Down

0 comments on commit cb82d23

Please sign in to comment.