Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inverse Distance Weighting Interpolation #448

Merged
merged 1 commit into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading