242 lines
5.9 KiB
Go
242 lines
5.9 KiB
Go
//go:build js && wasm
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"syscall/js"
|
|
|
|
pd "github.com/Snider/Poindexter"
|
|
)
|
|
|
|
// Simple registry for KDTree instances created from JS.
|
|
// We keep values as string for simplicity across the WASM boundary.
|
|
var (
|
|
treeRegistry = map[int]*pd.KDTree[string]{}
|
|
nextTreeID = 1
|
|
)
|
|
|
|
func export(name string, fn func(this js.Value, args []js.Value) (any, error)) {
|
|
js.Global().Set(name, js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
res, err := fn(this, args)
|
|
if err != nil {
|
|
return map[string]any{"ok": false, "error": err.Error()}
|
|
}
|
|
return map[string]any{"ok": true, "data": res}
|
|
}))
|
|
}
|
|
|
|
func getInt(v js.Value, idx int) (int, error) {
|
|
if len := v.Length(); len > idx {
|
|
return v.Index(idx).Int(), nil
|
|
}
|
|
return 0, errors.New("missing integer argument")
|
|
}
|
|
|
|
func getFloatSlice(arg js.Value) ([]float64, error) {
|
|
if arg.IsUndefined() || arg.IsNull() {
|
|
return nil, errors.New("coords/query is undefined or null")
|
|
}
|
|
ln := arg.Length()
|
|
res := make([]float64, ln)
|
|
for i := 0; i < ln; i++ {
|
|
res[i] = arg.Index(i).Float()
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func version(_ js.Value, _ []js.Value) (any, error) {
|
|
return pd.Version(), nil
|
|
}
|
|
|
|
func hello(_ js.Value, args []js.Value) (any, error) {
|
|
name := ""
|
|
if len(args) > 0 {
|
|
name = args[0].String()
|
|
}
|
|
return pd.Hello(name), nil
|
|
}
|
|
|
|
func newTree(_ js.Value, args []js.Value) (any, error) {
|
|
if len(args) < 1 {
|
|
return nil, errors.New("newTree(dim) requires dim")
|
|
}
|
|
dim := args[0].Int()
|
|
if dim <= 0 {
|
|
return nil, pd.ErrZeroDim
|
|
}
|
|
t, err := pd.NewKDTreeFromDim[string](dim)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
id := nextTreeID
|
|
nextTreeID++
|
|
treeRegistry[id] = t
|
|
return map[string]any{"treeId": id, "dim": dim}, nil
|
|
}
|
|
|
|
func treeLen(_ js.Value, args []js.Value) (any, error) {
|
|
if len(args) < 1 {
|
|
return nil, errors.New("len(treeId)")
|
|
}
|
|
id := args[0].Int()
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown treeId %d", id)
|
|
}
|
|
return t.Len(), nil
|
|
}
|
|
|
|
func treeDim(_ js.Value, args []js.Value) (any, error) {
|
|
if len(args) < 1 {
|
|
return nil, errors.New("dim(treeId)")
|
|
}
|
|
id := args[0].Int()
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown treeId %d", id)
|
|
}
|
|
return t.Dim(), nil
|
|
}
|
|
|
|
func insert(_ js.Value, args []js.Value) (any, error) {
|
|
// insert(treeId, {id: string, coords: number[], value?: string})
|
|
if len(args) < 2 {
|
|
return nil, errors.New("insert(treeId, point)")
|
|
}
|
|
id := args[0].Int()
|
|
pt := args[1]
|
|
pid := pt.Get("id").String()
|
|
coords, err := getFloatSlice(pt.Get("coords"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
val := pt.Get("value").String()
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown treeId %d", id)
|
|
}
|
|
okIns := t.Insert(pd.KDPoint[string]{ID: pid, Coords: coords, Value: val})
|
|
return okIns, nil
|
|
}
|
|
|
|
func deleteByID(_ js.Value, args []js.Value) (any, error) {
|
|
// deleteByID(treeId, id)
|
|
if len(args) < 2 {
|
|
return nil, errors.New("deleteByID(treeId, id)")
|
|
}
|
|
id := args[0].Int()
|
|
pid := args[1].String()
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown treeId %d", id)
|
|
}
|
|
return t.DeleteByID(pid), nil
|
|
}
|
|
|
|
func nearest(_ js.Value, args []js.Value) (any, error) {
|
|
// nearest(treeId, query:number[]) -> {point, dist, found}
|
|
if len(args) < 2 {
|
|
return nil, errors.New("nearest(treeId, query)")
|
|
}
|
|
id := args[0].Int()
|
|
query, err := getFloatSlice(args[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown treeId %d", id)
|
|
}
|
|
p, d, found := t.Nearest(query)
|
|
out := map[string]any{
|
|
"point": map[string]any{"id": p.ID, "coords": p.Coords, "value": p.Value},
|
|
"dist": d,
|
|
"found": found,
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func kNearest(_ js.Value, args []js.Value) (any, error) {
|
|
// kNearest(treeId, query:number[], k:int) -> {points:[...], dists:[...]}
|
|
if len(args) < 3 {
|
|
return nil, errors.New("kNearest(treeId, query, k)")
|
|
}
|
|
id := args[0].Int()
|
|
query, err := getFloatSlice(args[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
k := args[2].Int()
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown treeId %d", id)
|
|
}
|
|
pts, dists := t.KNearest(query, k)
|
|
jsPts := make([]any, len(pts))
|
|
for i, p := range pts {
|
|
jsPts[i] = map[string]any{"id": p.ID, "coords": p.Coords, "value": p.Value}
|
|
}
|
|
return map[string]any{"points": jsPts, "dists": dists}, nil
|
|
}
|
|
|
|
func radius(_ js.Value, args []js.Value) (any, error) {
|
|
// radius(treeId, query:number[], r:number) -> {points:[...], dists:[...]}
|
|
if len(args) < 3 {
|
|
return nil, errors.New("radius(treeId, query, r)")
|
|
}
|
|
id := args[0].Int()
|
|
query, err := getFloatSlice(args[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r := args[2].Float()
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown treeId %d", id)
|
|
}
|
|
pts, dists := t.Radius(query, r)
|
|
jsPts := make([]any, len(pts))
|
|
for i, p := range pts {
|
|
jsPts[i] = map[string]any{"id": p.ID, "coords": p.Coords, "value": p.Value}
|
|
}
|
|
return map[string]any{"points": jsPts, "dists": dists}, nil
|
|
}
|
|
|
|
func exportJSON(_ js.Value, args []js.Value) (any, error) {
|
|
// exportJSON(treeId) -> string (all points)
|
|
if len(args) < 1 {
|
|
return nil, errors.New("exportJSON(treeId)")
|
|
}
|
|
id := args[0].Int()
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown treeId %d", id)
|
|
}
|
|
// naive export: ask for all points by radius from origin with large r; or keep
|
|
// internal slice? KDTree doesn't expose iteration, so skip heavy export here.
|
|
// Return metrics only for now.
|
|
m := map[string]any{"dim": t.Dim(), "len": t.Len()}
|
|
b, _ := json.Marshal(m)
|
|
return string(b), nil
|
|
}
|
|
|
|
func main() {
|
|
// Export core API
|
|
export("pxVersion", version)
|
|
export("pxHello", hello)
|
|
export("pxNewTree", newTree)
|
|
export("pxTreeLen", treeLen)
|
|
export("pxTreeDim", treeDim)
|
|
export("pxInsert", insert)
|
|
export("pxDeleteByID", deleteByID)
|
|
export("pxNearest", nearest)
|
|
export("pxKNearest", kNearest)
|
|
export("pxRadius", radius)
|
|
export("pxExportJSON", exportJSON)
|
|
|
|
// Keep running
|
|
select {}
|
|
}
|