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 }