go-hep.org/x/hep@v0.38.1/groot/rsrv/endpoints.go (about)

     1  // Copyright ©2018 The go-hep Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package rsrv
     6  
     7  import (
     8  	"encoding/base64"
     9  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  	"math"
    13  	"net/http"
    14  	"os"
    15  	stdpath "path"
    16  	"path/filepath"
    17  	"sort"
    18  	"strings"
    19  
    20  	uuid "github.com/hashicorp/go-uuid"
    21  	"go-hep.org/x/hep/groot/rhist"
    22  	"go-hep.org/x/hep/groot/riofs"
    23  	"go-hep.org/x/hep/groot/root"
    24  	"go-hep.org/x/hep/groot/rtree"
    25  	"go-hep.org/x/hep/hbook"
    26  	"go-hep.org/x/hep/hbook/rootcnv"
    27  	"go-hep.org/x/hep/hplot"
    28  )
    29  
    30  // Ping verifies the connection to the server is alive.
    31  // Ping replies with a StatusOK.
    32  func (srv *Server) Ping(w http.ResponseWriter, r *http.Request) {
    33  	srv.wrap(srv.handlePing)(w, r)
    34  }
    35  
    36  func (srv *Server) handlePing(w http.ResponseWriter, r *http.Request) error {
    37  	w.Header().Set("Content-Type", "application/json")
    38  	w.WriteHeader(http.StatusOK)
    39  	return json.NewEncoder(w).Encode(nil)
    40  }
    41  
    42  // OpenFile opens a ROOT file located at the provided URI.
    43  // OpenFile expects an OpenFileRequest payload as JSON:
    44  //
    45  //	{"uri": "file:///some/file.root"}
    46  //	{"uri": "root://example.org/some/file.root"}
    47  //
    48  // OpenFile replies with a STATUS/OK or STATUS/NotFound if no such file exist.
    49  func (srv *Server) OpenFile(w http.ResponseWriter, r *http.Request) {
    50  	srv.wrap(srv.handleOpen)(w, r)
    51  }
    52  
    53  func (srv *Server) handleOpen(w http.ResponseWriter, r *http.Request) error {
    54  	dec := json.NewDecoder(r.Body)
    55  	defer r.Body.Close()
    56  
    57  	var req OpenFileRequest
    58  
    59  	err := dec.Decode(&req)
    60  	if err != nil {
    61  		return fmt.Errorf("could not decode open-file request: %w", err)
    62  	}
    63  
    64  	db, err := srv.db(r)
    65  	if err != nil {
    66  		return fmt.Errorf("could not open file database: %w", err)
    67  	}
    68  
    69  	if f := db.get(req.URI); f != nil {
    70  		w.Header().Set("Content-Type", "application/json")
    71  		w.WriteHeader(http.StatusConflict)
    72  		return json.NewEncoder(w).Encode(nil)
    73  	}
    74  
    75  	f, err := riofs.Open(req.URI)
    76  	if err != nil {
    77  		return fmt.Errorf("could not open ROOT file: %w", err)
    78  	}
    79  
    80  	db.set(req.URI, f)
    81  
    82  	w.Header().Set("Content-Type", "application/json")
    83  	w.WriteHeader(http.StatusOK)
    84  	return json.NewEncoder(w).Encode(nil)
    85  }
    86  
    87  // UploadFile uploads a ROOT file, provided as a multipart form data under
    88  // the key "groot-file", to the remote server.
    89  // The destination of that ROOT file is also taken from the multipart form,
    90  // under the key "groot-dst".
    91  //
    92  // UploadFile replies with a StatusConflict if a file with the named file
    93  // already exists in the remote server.
    94  func (srv *Server) UploadFile(w http.ResponseWriter, r *http.Request) {
    95  	srv.wrap(srv.handleUpload)(w, r)
    96  }
    97  
    98  func (srv *Server) handleUpload(w http.ResponseWriter, r *http.Request) error {
    99  	err := r.ParseMultipartForm(500 << 20)
   100  	if err != nil {
   101  		return fmt.Errorf("could not parse multipart form: %w", err)
   102  	}
   103  
   104  	const (
   105  		destKey = "groot-dst"
   106  		fileKey = "groot-file"
   107  	)
   108  
   109  	dst := r.FormValue(destKey)
   110  	if dst == "" {
   111  		return fmt.Errorf("empty destination for uploaded ROOT file")
   112  	}
   113  
   114  	db, err := srv.db(r)
   115  	if err != nil {
   116  		return fmt.Errorf("could not open file database: %w", err)
   117  	}
   118  
   119  	if f := db.get(dst); f != nil {
   120  		w.Header().Set("Content-Type", "application/json")
   121  		w.WriteHeader(http.StatusConflict)
   122  		return json.NewEncoder(w).Encode(nil)
   123  	}
   124  
   125  	f, handler, err := r.FormFile(fileKey)
   126  	if err != nil {
   127  		return fmt.Errorf("could not retrieve ROOT file from multipart form: %w", err)
   128  	}
   129  
   130  	fid, err := uuid.GenerateUUID()
   131  	if err != nil {
   132  		return fmt.Errorf("could not generate UUID for %q: %w", handler.Filename, err)
   133  	}
   134  
   135  	fname := filepath.Join(srv.dir, fid+".root")
   136  	o, err := os.Create(fname)
   137  	if err != nil {
   138  		return fmt.Errorf("could not create temporary file: %w", err)
   139  	}
   140  	_, err = io.CopyBuffer(o, f, make([]byte, 16*1024*1024))
   141  	if err != nil {
   142  		return fmt.Errorf("could not copy uploaded file: %w", err)
   143  	}
   144  	o.Close()
   145  	f.Close()
   146  
   147  	rfile, err := riofs.Open(o.Name())
   148  	if err != nil {
   149  		return fmt.Errorf("could not open ROOT file %q: %w", dst, err)
   150  	}
   151  
   152  	db.set(dst, rfile)
   153  
   154  	w.Header().Set("Content-Type", "application/json")
   155  	w.WriteHeader(http.StatusOK)
   156  	return json.NewEncoder(w).Encode(nil)
   157  }
   158  
   159  // CloseFile closes a file specified by the CloseFileRequest:
   160  //
   161  //	{"uri": "file:///some/file.root"}
   162  func (srv *Server) CloseFile(w http.ResponseWriter, r *http.Request) {
   163  	srv.wrap(srv.handleCloseFile)(w, r)
   164  }
   165  
   166  func (srv *Server) handleCloseFile(w http.ResponseWriter, r *http.Request) error {
   167  	db, err := srv.db(r)
   168  	if err != nil {
   169  		return fmt.Errorf("could not open file database: %w", err)
   170  	}
   171  
   172  	dec := json.NewDecoder(r.Body)
   173  	defer r.Body.Close()
   174  
   175  	var req CloseFileRequest
   176  	err = dec.Decode(&req)
   177  	if err != nil {
   178  		return fmt.Errorf("could not decode request: %w", err)
   179  	}
   180  
   181  	db.del(req.URI)
   182  
   183  	w.WriteHeader(http.StatusOK)
   184  	return nil
   185  }
   186  
   187  // ListFiles lists all the files currently known to the server.
   188  // ListFiles replies with a StatusOK and a ListResponse:
   189  //
   190  //	[{"uri": "file:///some/file.root"},
   191  //	 {"uri": "root://example.org/file.root"}]
   192  func (srv *Server) ListFiles(w http.ResponseWriter, r *http.Request) {
   193  	srv.wrap(srv.handleListFiles)(w, r)
   194  }
   195  
   196  func (srv *Server) handleListFiles(w http.ResponseWriter, r *http.Request) error {
   197  	db, err := srv.db(r)
   198  	if err != nil {
   199  		return fmt.Errorf("could not open file database: %w", err)
   200  	}
   201  
   202  	var resp ListResponse
   203  	db.RLock()
   204  	defer db.RUnlock()
   205  
   206  	for uri, f := range db.files {
   207  		resp.Files = append(resp.Files, File{URI: uri, Version: f.Version()})
   208  	}
   209  	sort.Slice(resp.Files, func(i, j int) bool {
   210  		return resp.Files[i].URI < resp.Files[j].URI
   211  	})
   212  
   213  	w.WriteHeader(http.StatusOK)
   214  	w.Header().Set("Content-Type", "application/json")
   215  	return json.NewEncoder(w).Encode(resp)
   216  }
   217  
   218  // Dirent lists the content of a ROOT directory inside a ROOT file.
   219  // Dirent expects a DirentRequest:
   220  //
   221  //	{"uri": "file:///some/file.root", "dir": "/some/dir", "recursive": true}
   222  //	{"uri": "root://example.org/some/file.root", "dir": "/some/dir"}
   223  //
   224  // Dirent replies with a DirentResponse:
   225  //
   226  //	{"uri": "file:///some/file.root", "content": [
   227  //	  {"path": "/dir", "type": "TDirectoryFile", "name": "dir", "title": "my title"},
   228  //	  {"path": "/dir/obj", "type": "TObjString", "name": "obj", "title": "obj string"},
   229  //	  {"path": "/dir/sub", "type": "TDirectoryFile", "name": "sub", "title": "my sub dir"},
   230  //	  {"path": "/dir/sub/obj", "type": "TObjString", "name": "obj", "title": "my sub obj string"}
   231  //	]}
   232  func (srv *Server) Dirent(w http.ResponseWriter, r *http.Request) {
   233  	srv.wrap(srv.handleDirent)(w, r)
   234  }
   235  
   236  func (srv *Server) handleDirent(w http.ResponseWriter, r *http.Request) error {
   237  	dec := json.NewDecoder(r.Body)
   238  	defer r.Body.Close()
   239  
   240  	var (
   241  		req  DirentRequest
   242  		resp DirentResponse
   243  	)
   244  
   245  	err := dec.Decode(&req)
   246  	if err != nil {
   247  		return fmt.Errorf("could not decode dirent request: %w", err)
   248  	}
   249  
   250  	resp.URI = req.URI
   251  
   252  	db, err := srv.db(r)
   253  	if err != nil {
   254  		return fmt.Errorf("could not open file database: %w", err)
   255  	}
   256  
   257  	f := db.get(req.URI)
   258  	if f == nil {
   259  		return fmt.Errorf("rsrv: could not find ROOT file %q", req.URI)
   260  	}
   261  
   262  	if !strings.HasPrefix(req.Dir, "/") {
   263  		req.Dir = "/" + req.Dir
   264  	}
   265  
   266  	// FIXME(sbinet): also handle relative dir-paths? (eg: ./foo/../dir/obj)
   267  
   268  	var dir riofs.Directory
   269  	switch req.Dir {
   270  	default:
   271  		obj, err := riofs.Dir(f).Get(req.Dir)
   272  		if err != nil {
   273  			return fmt.Errorf("rsrv: could not find directory %q in ROOT file %q: %w", req.Dir, req.URI, err)
   274  		}
   275  		var ok bool
   276  		dir, ok = obj.(riofs.Directory)
   277  		if !ok {
   278  			return fmt.Errorf("rsrv: %q not a directory", req.Dir)
   279  		}
   280  	case "/":
   281  		dir = f
   282  	}
   283  
   284  	switch req.Recursive {
   285  	default:
   286  		obj := dir.(root.Named)
   287  		resp.Content = append(resp.Content, Dirent{
   288  			Path:  req.Dir,
   289  			Type:  obj.Class(),
   290  			Name:  obj.Name(),
   291  			Title: obj.Title(),
   292  		})
   293  		for _, key := range dir.Keys() {
   294  			resp.Content = append(resp.Content, Dirent{
   295  				Path:  stdpath.Join(req.Dir, key.Name()),
   296  				Type:  key.ClassName(),
   297  				Name:  key.Name(),
   298  				Title: key.Title(),
   299  				Cycle: key.Cycle(),
   300  			})
   301  		}
   302  	case true:
   303  		err = riofs.Walk(dir, func(path string, obj root.Object, err error) error {
   304  			var (
   305  				name  = ""
   306  				title = ""
   307  				cycle = 0
   308  			)
   309  			if o, ok := obj.(root.Named); ok {
   310  				name = o.Name()
   311  				title = o.Title()
   312  			}
   313  
   314  			type cycler interface {
   315  				Cycle() int
   316  			}
   317  			if o, ok := obj.(cycler); ok {
   318  				cycle = o.Cycle()
   319  			}
   320  
   321  			opath := strings.Replace("/"+path, "/"+f.Name(), "/", 1)
   322  			if strings.HasPrefix(opath, "//") {
   323  				opath = strings.Replace(opath, "//", "/", 1)
   324  			}
   325  			resp.Content = append(resp.Content, Dirent{
   326  				Path:  opath,
   327  				Type:  obj.Class(),
   328  				Name:  name,
   329  				Title: title,
   330  				Cycle: cycle,
   331  			})
   332  			return nil
   333  		})
   334  		if err != nil {
   335  			return fmt.Errorf("could not list directory: %w", err)
   336  		}
   337  	}
   338  
   339  	w.Header().Set("Content-Type", "application/json")
   340  	return json.NewEncoder(w).Encode(resp)
   341  }
   342  
   343  // Tree returns the structure of a TTree specified by the TreeRequest:
   344  //
   345  //	{"uri": "file:///some/file.root", "dir": "/some/dir", "obj": "myTree"}
   346  //
   347  // Tree replies with a TreeResponse:
   348  //
   349  //	{"uri": "file:///some/file.root", "dir": "/some/dir", "obj": "myTree",
   350  //	  "tree": {
   351  //	    "type": "TTree", "name": "myTree", "title": "my title", "cycle": 1,
   352  //	    "entries": 42,
   353  //	    "branches": [{"type": "TBranch", "name": "Int64"}, ...],
   354  //	    "leaves": [{"type": "TLeafL", "name": "Int64"}, ...]
   355  //	  }
   356  //	}
   357  func (srv *Server) Tree(w http.ResponseWriter, r *http.Request) {
   358  	srv.wrap(srv.handleTree)(w, r)
   359  }
   360  
   361  func (srv *Server) handleTree(w http.ResponseWriter, r *http.Request) error {
   362  	dec := json.NewDecoder(r.Body)
   363  	defer r.Body.Close()
   364  
   365  	var req TreeRequest
   366  
   367  	err := dec.Decode(&req)
   368  	if err != nil {
   369  		return fmt.Errorf("could not decode tree request: %w", err)
   370  	}
   371  
   372  	resp := TreeResponse{
   373  		URI: req.URI,
   374  		Dir: req.Dir,
   375  		Obj: req.Obj,
   376  	}
   377  
   378  	db, err := srv.db(r)
   379  	if err != nil {
   380  		return fmt.Errorf("could not open ROOT file database: %w", err)
   381  	}
   382  
   383  	f := db.get(req.URI)
   384  	if f == nil {
   385  		return fmt.Errorf("rsrv: could not find ROOT file named %q", req.URI)
   386  	}
   387  
   388  	obj, err := riofs.Dir(f).Get(req.Dir)
   389  	if err != nil {
   390  		return fmt.Errorf("could not find directory %q in file %q: %w", req.Dir, req.URI, err)
   391  	}
   392  	dir, ok := obj.(riofs.Directory)
   393  	if !ok {
   394  		return fmt.Errorf("rsrv: %q in file %q is not a directory", req.Dir, req.URI)
   395  	}
   396  
   397  	obj, err = dir.Get(req.Obj)
   398  	if err != nil {
   399  		return fmt.Errorf("could not find object %q under directory %q in file %q: %w", req.Obj, req.Dir, req.URI, err)
   400  	}
   401  
   402  	tree, ok := obj.(rtree.Tree)
   403  	if !ok {
   404  		return fmt.Errorf("rsrv: object %v:%s/%q is not a tree (type=%s)", req.URI, req.Dir, req.Obj, obj.Class())
   405  	}
   406  
   407  	resp.Tree.Type = tree.Class()
   408  	resp.Tree.Name = tree.Name()
   409  	resp.Tree.Title = tree.Title()
   410  	resp.Tree.Entries = tree.Entries()
   411  
   412  	var cnvBranch func(b rtree.Branch) Branch
   413  	var cnvLeaf func(b rtree.Leaf) Leaf
   414  
   415  	cnvBranch = func(b rtree.Branch) Branch {
   416  		o := Branch{
   417  			Type: b.Class(),
   418  			Name: b.Name(),
   419  		}
   420  		for _, sub := range b.Branches() {
   421  			o.Branches = append(o.Branches, cnvBranch(sub))
   422  		}
   423  		for _, sub := range b.Leaves() {
   424  			o.Leaves = append(o.Leaves, cnvLeaf(sub))
   425  		}
   426  		return o
   427  	}
   428  
   429  	cnvLeaf = func(leaf rtree.Leaf) Leaf {
   430  		o := Leaf{
   431  			Type: leaf.TypeName(),
   432  			Name: leaf.Name(),
   433  		}
   434  		return o
   435  	}
   436  
   437  	for _, b := range tree.Branches() {
   438  		resp.Tree.Branches = append(resp.Tree.Branches, cnvBranch(b))
   439  	}
   440  
   441  	for _, leaf := range tree.Leaves() {
   442  		resp.Tree.Leaves = append(resp.Tree.Leaves, cnvLeaf(leaf))
   443  	}
   444  
   445  	return json.NewEncoder(w).Encode(resp)
   446  }
   447  
   448  // PlotH1 plots the 1-dim histogram specified by the PlotH1Request:
   449  //
   450  //	{"uri": "file:///some/file.root", "dir": "/some/dir", "obj": "h1", "type": "png"}
   451  //	{"uri": "file:///some/file.root", "dir": "/some/dir", "obj": "h1", "type": "svg",
   452  //	   "options": {
   453  //	     "title": "my histo title", "x": "my x-axis", "y": "my y-axis",
   454  //	     "line": {"color": "#ff0000ff", ...},
   455  //	     "fill_color": "#00ff00ff"}
   456  //	}}
   457  //
   458  // PlotH1 replies with a PlotResponse, where "data" contains the base64 encoded representation of
   459  // the plot.
   460  func (srv *Server) PlotH1(w http.ResponseWriter, r *http.Request) {
   461  	srv.wrap(srv.handlePlotH1)(w, r)
   462  }
   463  
   464  func (srv *Server) handlePlotH1(w http.ResponseWriter, r *http.Request) error {
   465  	dec := json.NewDecoder(r.Body)
   466  	defer r.Body.Close()
   467  
   468  	var (
   469  		req  PlotH1Request
   470  		resp PlotResponse
   471  	)
   472  
   473  	err := dec.Decode(&req)
   474  	if err != nil {
   475  		return fmt.Errorf("could not decode plot-h1 request: %w", err)
   476  	}
   477  
   478  	db, err := srv.db(r)
   479  	if err != nil {
   480  		return fmt.Errorf("could not open ROOT file database: %w", err)
   481  	}
   482  
   483  	err = db.Tx(req.URI, func(f *riofs.File) error {
   484  		if f == nil {
   485  			return fmt.Errorf("rsrv: could not find ROOT file named %q", req.URI)
   486  		}
   487  
   488  		obj, err := riofs.Dir(f).Get(req.Dir)
   489  		if err != nil {
   490  			return fmt.Errorf("could not find directory %q in file %q: %w", req.Dir, req.URI, err)
   491  		}
   492  		dir, ok := obj.(riofs.Directory)
   493  		if !ok {
   494  			return fmt.Errorf("rsrv: %q in file %q is not a directory", req.Dir, req.URI)
   495  		}
   496  
   497  		obj, err = dir.Get(req.Obj)
   498  		if err != nil {
   499  			return fmt.Errorf("could not find object %q under directory %q in file %q: %w", req.Obj, req.Dir, req.URI, err)
   500  		}
   501  
   502  		robj, ok := obj.(rhist.H1)
   503  		if !ok {
   504  			return fmt.Errorf("rsrv: object %v:%s/%q is not a 1-dim histogram (type=%s)", req.URI, req.Dir, req.Obj, obj.Class())
   505  		}
   506  
   507  		h1 := rootcnv.H1D(robj)
   508  
   509  		req.Options.init()
   510  
   511  		pl := hplot.New()
   512  		pl.Title.Text = robj.Title()
   513  		if req.Options.Title != "" {
   514  			pl.Title.Text = req.Options.Title
   515  		}
   516  		pl.X.Label.Text = req.Options.X
   517  		pl.Y.Label.Text = req.Options.Y
   518  
   519  		h := hplot.NewH1D(h1)
   520  		h.Infos.Style = hplot.HInfoSummary
   521  		h.Color = req.Options.Line.Color
   522  		h.FillColor = req.Options.FillColor
   523  
   524  		pl.Add(h, hplot.NewGrid())
   525  
   526  		out, err := srv.render(pl, req.Options)
   527  		if err != nil {
   528  			return fmt.Errorf("could not render H1 plot: %w", err)
   529  		}
   530  
   531  		resp.URI = req.URI
   532  		resp.Dir = req.Dir
   533  		resp.Obj = req.Obj
   534  		resp.Data = base64.StdEncoding.EncodeToString(out)
   535  		return nil
   536  	})
   537  	if err != nil {
   538  		return err
   539  	}
   540  
   541  	w.Header().Set("Content-Type", "application/json")
   542  	w.WriteHeader(http.StatusOK)
   543  	return json.NewEncoder(w).Encode(resp)
   544  }
   545  
   546  // PlotH2 plots the 2-dim histogram specified by the PlotH2Request:
   547  //
   548  //	{"uri": "file:///some/file.root", "dir": "/some/dir", "obj": "h2", "type": "png"}
   549  //	{"uri": "file:///some/file.root", "dir": "/some/dir", "obj": "h2", "type": "svg",
   550  //	   "options": {
   551  //	     "title": "my histo title", "x": "my x-axis", "y": "my y-axis"
   552  //	}}
   553  //
   554  // PlotH2 replies with a PlotResponse, where "data" contains the base64 encoded representation of
   555  // the plot.
   556  func (srv *Server) PlotH2(w http.ResponseWriter, r *http.Request) {
   557  	srv.wrap(srv.handlePlotH2)(w, r)
   558  }
   559  
   560  func (srv *Server) handlePlotH2(w http.ResponseWriter, r *http.Request) error {
   561  	dec := json.NewDecoder(r.Body)
   562  	defer r.Body.Close()
   563  
   564  	var (
   565  		req  PlotH2Request
   566  		resp PlotResponse
   567  	)
   568  
   569  	err := dec.Decode(&req)
   570  	if err != nil {
   571  		return fmt.Errorf("could not decode plot-h2 request: %w", err)
   572  	}
   573  
   574  	db, err := srv.db(r)
   575  	if err != nil {
   576  		return fmt.Errorf("could not open ROOT file database: %w", err)
   577  	}
   578  
   579  	err = db.Tx(req.URI, func(f *riofs.File) error {
   580  		if f == nil {
   581  			return fmt.Errorf("rsrv: could not find ROOT file named %q", req.URI)
   582  		}
   583  
   584  		obj, err := riofs.Dir(f).Get(req.Dir)
   585  		if err != nil {
   586  			return fmt.Errorf("could not find directory %q in file %q: %w", req.Dir, req.URI, err)
   587  		}
   588  		dir, ok := obj.(riofs.Directory)
   589  		if !ok {
   590  			return fmt.Errorf("rsrv: %q in file %q is not a directory", req.Dir, req.URI)
   591  		}
   592  
   593  		obj, err = dir.Get(req.Obj)
   594  		if err != nil {
   595  			return fmt.Errorf("could not find object %q under directory %q in file %q: %w", req.Obj, req.Dir, req.URI, err)
   596  		}
   597  
   598  		robj, ok := obj.(rhist.H2)
   599  		if !ok {
   600  			return fmt.Errorf("rsrv: object %v:%s/%q is not a 2-dim histogram (type=%s)", req.URI, req.Dir, req.Obj, obj.Class())
   601  		}
   602  
   603  		h2 := rootcnv.H2D(robj)
   604  
   605  		req.Options.init()
   606  
   607  		pl := hplot.New()
   608  		pl.Title.Text = robj.Title()
   609  		if req.Options.Title != "" {
   610  			pl.Title.Text = req.Options.Title
   611  		}
   612  		pl.X.Label.Text = req.Options.X
   613  		pl.Y.Label.Text = req.Options.Y
   614  
   615  		h := hplot.NewH2D(h2, nil)
   616  		h.Infos.Style = hplot.HInfoSummary
   617  
   618  		pl.Add(h, hplot.NewGrid())
   619  
   620  		out, err := srv.render(pl, req.Options)
   621  		if err != nil {
   622  			return fmt.Errorf("could not render H2 plot: %w", err)
   623  		}
   624  
   625  		resp.URI = req.URI
   626  		resp.Dir = req.Dir
   627  		resp.Obj = req.Obj
   628  		resp.Data = base64.StdEncoding.EncodeToString(out)
   629  		return nil
   630  	})
   631  	if err != nil {
   632  		return err
   633  	}
   634  
   635  	w.Header().Set("Content-Type", "application/json")
   636  	w.WriteHeader(http.StatusOK)
   637  	return json.NewEncoder(w).Encode(resp)
   638  }
   639  
   640  // PlotS2 plots the 2-dim scatter specified by the PlotS2Request:
   641  //
   642  //	{"uri": "file:///some/file.root", "dir": "/some/dir", "obj": "gr", "type": "png"}
   643  //	{"uri": "file:///some/file.root", "dir": "/some/dir", "obj": "gr", "type": "svg",
   644  //	   "options": {
   645  //	     "title": "my scatter title", "x": "my x-axis", "y": "my y-axis",
   646  //	     "line": {"color": "#ff0000ff", ...}
   647  //	}}
   648  //
   649  // PlotS2 replies with a PlotResponse, where "data" contains the base64 encoded representation of
   650  // the plot.
   651  func (srv *Server) PlotS2(w http.ResponseWriter, r *http.Request) {
   652  	srv.wrap(srv.handlePlotS2)(w, r)
   653  }
   654  
   655  func (srv *Server) handlePlotS2(w http.ResponseWriter, r *http.Request) error {
   656  	dec := json.NewDecoder(r.Body)
   657  	defer r.Body.Close()
   658  
   659  	var (
   660  		req  PlotS2Request
   661  		resp PlotResponse
   662  	)
   663  
   664  	err := dec.Decode(&req)
   665  	if err != nil {
   666  		return fmt.Errorf("could not decode plot-s2 request: %w", err)
   667  	}
   668  
   669  	db, err := srv.db(r)
   670  	if err != nil {
   671  		return fmt.Errorf("could not open ROOT file database: %w", err)
   672  	}
   673  
   674  	err = db.Tx(req.URI, func(f *riofs.File) error {
   675  		if f == nil {
   676  			return fmt.Errorf("rsrv: could not find ROOT file named %q", req.URI)
   677  		}
   678  
   679  		obj, err := riofs.Dir(f).Get(req.Dir)
   680  		if err != nil {
   681  			return fmt.Errorf("could not find directory %q in file %q: %w", req.Dir, req.URI, err)
   682  		}
   683  		dir, ok := obj.(riofs.Directory)
   684  		if !ok {
   685  			return fmt.Errorf("rsrv: %q in file %q is not a directory", req.Dir, req.URI)
   686  		}
   687  
   688  		obj, err = dir.Get(req.Obj)
   689  		if err != nil {
   690  			return fmt.Errorf("could not find object %q under directory %q in file %q: %w", req.Obj, req.Dir, req.URI, err)
   691  		}
   692  
   693  		robj, ok := obj.(rhist.Graph)
   694  		if !ok {
   695  			return fmt.Errorf("rsrv: object %v:%s/%q is not a 2-dim scatter (type=%s)", req.URI, req.Dir, req.Obj, obj.Class())
   696  		}
   697  
   698  		s2 := rootcnv.S2D(robj)
   699  
   700  		req.Options.init()
   701  
   702  		pl := hplot.New()
   703  		pl.Title.Text = robj.Title()
   704  		if req.Options.Title != "" {
   705  			pl.Title.Text = req.Options.Title
   706  		}
   707  		pl.X.Label.Text = req.Options.X
   708  		pl.Y.Label.Text = req.Options.Y
   709  
   710  		var opts []hplot.Options
   711  		if _, ok := robj.(rhist.GraphErrors); ok {
   712  			opts = append(
   713  				opts,
   714  				hplot.WithXErrBars(true), hplot.WithYErrBars(true),
   715  			)
   716  		}
   717  		h := hplot.NewS2D(s2, opts...)
   718  		h.Color = req.Options.Line.Color
   719  
   720  		pl.Add(h, hplot.NewGrid())
   721  
   722  		out, err := srv.render(pl, req.Options)
   723  		if err != nil {
   724  			return fmt.Errorf("could not render S2 plot: %w", err)
   725  		}
   726  
   727  		resp.URI = req.URI
   728  		resp.Dir = req.Dir
   729  		resp.Obj = req.Obj
   730  		resp.Data = base64.StdEncoding.EncodeToString(out)
   731  		return nil
   732  	})
   733  	if err != nil {
   734  		return err
   735  	}
   736  
   737  	w.Header().Set("Content-Type", "application/json")
   738  	w.WriteHeader(http.StatusOK)
   739  	return json.NewEncoder(w).Encode(resp)
   740  }
   741  
   742  // PlotTree plots the Tree branch(es) specified by the PlotBranchRequest:
   743  //
   744  //	{"uri": "file:///some/file.root", "dir": "/some/dir", "obj": "gr", "type": "png", "vars": ["pt"]}
   745  //	{"uri": "file:///some/file.root", "dir": "/some/dir", "obj": "gr", "type": "svg", "vars": ["pt", "eta"],
   746  //	   "options": {
   747  //	     "title": "my plot title", "x": "my x-axis", "y": "my y-axis",
   748  //	     "line": {"color": "#ff0000ff", ...}
   749  //	}}
   750  //
   751  // PlotBranch replies with a PlotResponse, where "data" contains the base64 encoded representation of
   752  // the plot.
   753  func (srv *Server) PlotTree(w http.ResponseWriter, r *http.Request) {
   754  	srv.wrap(srv.handlePlotTree)(w, r)
   755  }
   756  
   757  func (srv *Server) handlePlotTree(w http.ResponseWriter, r *http.Request) error {
   758  	dec := json.NewDecoder(r.Body)
   759  	defer r.Body.Close()
   760  
   761  	var (
   762  		req  PlotTreeRequest
   763  		resp PlotResponse
   764  	)
   765  
   766  	err := dec.Decode(&req)
   767  	if err != nil {
   768  		return fmt.Errorf("could not decode plot-tree request: %w", err)
   769  	}
   770  
   771  	db, err := srv.db(r)
   772  	if err != nil {
   773  		return fmt.Errorf("could not open ROOT file database: %w", err)
   774  	}
   775  
   776  	err = db.Tx(req.URI, func(f *riofs.File) error {
   777  		if f == nil {
   778  			return fmt.Errorf("rsrv: could not find ROOT file named %q", req.URI)
   779  		}
   780  
   781  		obj, err := riofs.Dir(f).Get(req.Dir)
   782  		if err != nil {
   783  			return fmt.Errorf("could not find directory %q in file %q: %w", req.Dir, req.URI, err)
   784  		}
   785  		dir, ok := obj.(riofs.Directory)
   786  		if !ok {
   787  			return fmt.Errorf("rsrv: %q in file %q is not a directory", req.Dir, req.URI)
   788  		}
   789  
   790  		obj, err = dir.Get(req.Obj)
   791  		if err != nil {
   792  			return fmt.Errorf("could not find object %q under directory %q in file %q: %w", req.Obj, req.Dir, req.URI, err)
   793  		}
   794  
   795  		tree, ok := obj.(rtree.Tree)
   796  		if !ok {
   797  			return fmt.Errorf("rsrv: object %v:%s/%q is not a tree (type=%s)", req.URI, req.Dir, req.Obj, obj.Class())
   798  		}
   799  
   800  		if len(req.Vars) != 1 {
   801  			return fmt.Errorf("rsrv: tree-draw of %d variables not supported", len(req.Vars))
   802  		}
   803  
   804  		var (
   805  			bname = req.Vars[0]
   806  			br    = tree.Branch(bname)
   807  		)
   808  		if br == nil {
   809  			return fmt.Errorf("rsrv: tree %v:%s/%s has no branch %q", req.URI, req.Dir, req.Obj, bname)
   810  		}
   811  
   812  		var (
   813  			leaves = br.Leaves()
   814  			leaf   = leaves[0] // FIXME(sbinet) handle sub-leaves
   815  		)
   816  
   817  		fv, err := newFloats(leaf)
   818  		if err != nil {
   819  			return fmt.Errorf("could not create float-leaf: %w", err)
   820  		}
   821  
   822  		min := +math.MaxFloat64
   823  		max := -math.MaxFloat64
   824  		vals := make([]float64, 0, int(tree.Entries()))
   825  		r, err := rtree.NewReader(tree, []rtree.ReadVar{{
   826  			Name:  bname,
   827  			Leaf:  leaf.Name(),
   828  			Value: fv.ptr,
   829  		}})
   830  		if err != nil {
   831  			return fmt.Errorf(
   832  				"could not create reader for branch %q in tree %q of file %q: %w",
   833  				bname, tree.Name(), req.URI, err,
   834  			)
   835  		}
   836  		defer r.Close()
   837  
   838  		err = r.Read(func(ctx rtree.RCtx) error {
   839  			for _, v := range fv.vals() {
   840  				if !math.IsNaN(v) && !math.IsInf(v, 0) {
   841  					max = math.Max(max, v)
   842  					min = math.Min(min, v)
   843  				}
   844  				vals = append(vals, v)
   845  			}
   846  			return nil
   847  		})
   848  		if err != nil {
   849  			return fmt.Errorf("could not complete scan: %w", err)
   850  		}
   851  
   852  		err = r.Close()
   853  		if err != nil {
   854  			return fmt.Errorf("could not close reader: %w", err)
   855  		}
   856  
   857  		min = math.Nextafter(min, min-1)
   858  		max = math.Nextafter(max, max+1)
   859  		h1 := hbook.NewH1D(100, min, max)
   860  		for _, v := range vals {
   861  			h1.Fill(v, 1)
   862  		}
   863  
   864  		req.Options.init()
   865  
   866  		pl := hplot.New()
   867  		pl.Title.Text = leaf.Name()
   868  		if req.Options.Title != "" {
   869  			pl.Title.Text = req.Options.Title
   870  		}
   871  		pl.X.Label.Text = req.Options.X
   872  		pl.Y.Label.Text = req.Options.Y
   873  
   874  		h := hplot.NewH1D(h1)
   875  		h.Infos.Style = hplot.HInfoSummary
   876  		h.Color = req.Options.Line.Color
   877  		h.FillColor = req.Options.FillColor
   878  
   879  		pl.Add(h, hplot.NewGrid())
   880  
   881  		out, err := srv.render(pl, req.Options)
   882  		if err != nil {
   883  			return fmt.Errorf("could not render tree plot: %w", err)
   884  		}
   885  
   886  		resp.URI = req.URI
   887  		resp.Dir = req.Dir
   888  		resp.Obj = req.Obj
   889  		resp.Data = base64.StdEncoding.EncodeToString(out)
   890  		return nil
   891  	})
   892  	if err != nil {
   893  		return err
   894  	}
   895  
   896  	w.Header().Set("Content-Type", "application/json")
   897  	w.WriteHeader(http.StatusOK)
   898  	return json.NewEncoder(w).Encode(resp)
   899  }