astro-viewer 3.11.0 → 3.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/astroviewer.cjs +837 -173
  2. package/dist/astroviewer.cjs.map +1 -1
  3. package/dist/astroviewer.js +837 -173
  4. package/dist/astroviewer.js.map +1 -1
  5. package/dist/astroviewer.min.js +1 -1
  6. package/dist/astroviewer.min.js.map +1 -1
  7. package/lib-esm/AstroSphere.d.ts +57 -2
  8. package/lib-esm/AstroSphere.d.ts.map +1 -1
  9. package/lib-esm/AstroSphere.js +370 -83
  10. package/lib-esm/AstroSphere.js.map +1 -1
  11. package/lib-esm/AstroViewer.d.ts +21 -1
  12. package/lib-esm/AstroViewer.d.ts.map +1 -1
  13. package/lib-esm/AstroViewer.js +56 -7
  14. package/lib-esm/AstroViewer.js.map +1 -1
  15. package/lib-esm/Camera.d.ts +22 -0
  16. package/lib-esm/Camera.d.ts.map +1 -1
  17. package/lib-esm/Camera.js +71 -0
  18. package/lib-esm/Camera.js.map +1 -1
  19. package/lib-esm/index.d.ts +1 -0
  20. package/lib-esm/index.d.ts.map +1 -1
  21. package/lib-esm/index.js +1 -0
  22. package/lib-esm/index.js.map +1 -1
  23. package/lib-esm/model/Source.js +1 -1
  24. package/lib-esm/model/Source.js.map +1 -1
  25. package/lib-esm/model/catalogues/CatalogueGL.d.ts +13 -4
  26. package/lib-esm/model/catalogues/CatalogueGL.d.ts.map +1 -1
  27. package/lib-esm/model/catalogues/CatalogueGL.js +109 -59
  28. package/lib-esm/model/catalogues/CatalogueGL.js.map +1 -1
  29. package/lib-esm/model/earth/XYZMap.d.ts +4 -1
  30. package/lib-esm/model/earth/XYZMap.d.ts.map +1 -1
  31. package/lib-esm/model/earth/XYZMap.js +35 -2
  32. package/lib-esm/model/earth/XYZMap.js.map +1 -1
  33. package/lib-esm/model/footprints/FootprintSetGL.d.ts +1 -0
  34. package/lib-esm/model/footprints/FootprintSetGL.d.ts.map +1 -1
  35. package/lib-esm/model/footprints/FootprintSetGL.js +13 -3
  36. package/lib-esm/model/footprints/FootprintSetGL.js.map +1 -1
  37. package/lib-esm/model/grid/EquatorialGrid.d.ts +1 -0
  38. package/lib-esm/model/grid/EquatorialGrid.d.ts.map +1 -1
  39. package/lib-esm/model/grid/EquatorialGrid.js +3 -0
  40. package/lib-esm/model/grid/EquatorialGrid.js.map +1 -1
  41. package/lib-esm/model/grid/HealpixGrid.d.ts +1 -0
  42. package/lib-esm/model/grid/HealpixGrid.d.ts.map +1 -1
  43. package/lib-esm/model/grid/HealpixGrid.js +3 -0
  44. package/lib-esm/model/grid/HealpixGrid.js.map +1 -1
  45. package/lib-esm/model/grid/LonLatGrid.d.ts +1 -0
  46. package/lib-esm/model/grid/LonLatGrid.d.ts.map +1 -1
  47. package/lib-esm/model/grid/LonLatGrid.js +3 -0
  48. package/lib-esm/model/grid/LonLatGrid.js.map +1 -1
  49. package/lib-esm/model/hips/AllSky.d.ts.map +1 -1
  50. package/lib-esm/model/hips/AllSky.js +4 -2
  51. package/lib-esm/model/hips/AllSky.js.map +1 -1
  52. package/lib-esm/model/hips/AncestorTile.d.ts.map +1 -1
  53. package/lib-esm/model/hips/AncestorTile.js +4 -2
  54. package/lib-esm/model/hips/AncestorTile.js.map +1 -1
  55. package/lib-esm/model/hips/Tile.d.ts.map +1 -1
  56. package/lib-esm/model/hips/Tile.js +4 -2
  57. package/lib-esm/model/hips/Tile.js.map +1 -1
  58. package/lib-esm/model/hips/TileBuffer.d.ts +11 -0
  59. package/lib-esm/model/hips/TileBuffer.d.ts.map +1 -1
  60. package/lib-esm/model/hips/TileBuffer.js +47 -0
  61. package/lib-esm/model/hips/TileBuffer.js.map +1 -1
  62. package/lib-esm/model/hips/VisibleTilesManager.d.ts.map +1 -1
  63. package/lib-esm/model/hips/VisibleTilesManager.js +8 -2
  64. package/lib-esm/model/hips/VisibleTilesManager.js.map +1 -1
  65. package/lib-esm/model/meships/MeshHiPS.d.ts +4 -0
  66. package/lib-esm/model/meships/MeshHiPS.d.ts.map +1 -1
  67. package/lib-esm/model/meships/MeshHiPS.js +11 -1
  68. package/lib-esm/model/meships/MeshHiPS.js.map +1 -1
  69. package/lib-esm/model/terra/TerraPointSetGL.d.ts +1 -0
  70. package/lib-esm/model/terra/TerraPointSetGL.d.ts.map +1 -1
  71. package/lib-esm/model/terra/TerraPointSetGL.js +39 -1
  72. package/lib-esm/model/terra/TerraPointSetGL.js.map +1 -1
  73. package/lib-esm/shader/CatalogueShaderProgram.d.ts +4 -0
  74. package/lib-esm/shader/CatalogueShaderProgram.d.ts.map +1 -1
  75. package/lib-esm/shader/CatalogueShaderProgram.js +6 -0
  76. package/lib-esm/shader/CatalogueShaderProgram.js.map +1 -1
  77. package/lib-esm/shader/ShaderManager.d.ts.map +1 -1
  78. package/lib-esm/shader/ShaderManager.js +7 -4
  79. package/lib-esm/shader/ShaderManager.js.map +1 -1
  80. package/lib-esm/shader/XYZShaderProgram.d.ts +3 -0
  81. package/lib-esm/shader/XYZShaderProgram.d.ts.map +1 -1
  82. package/lib-esm/shader/XYZShaderProgram.js +11 -3
  83. package/lib-esm/shader/XYZShaderProgram.js.map +1 -1
  84. package/lib-esm/utils/Utils.d.ts +12 -0
  85. package/lib-esm/utils/Utils.d.ts.map +1 -1
  86. package/lib-esm/utils/Utils.js +26 -0
  87. package/lib-esm/utils/Utils.js.map +1 -1
  88. package/package.json +2 -2
@@ -54,12 +54,18 @@ class AstroSphere {
54
54
  _activeHiPS = null;
55
55
  _activeHiPSLayers = [];
56
56
  _activeXYZ2 = null;
57
+ _earthRasterOverlays = [];
58
+ _earthRasterOverlaySequence = 0;
57
59
  _activeMeshHiPS = null;
58
60
  _activeBaseLayer = null;
61
+ _activeDomain = "astronomy";
62
+ domainCameraStates = {};
59
63
  startup = true;
60
64
  fov;
61
- activeCatalogues = [];
62
- activeFootprintSets = [];
65
+ astronomyCatalogues = [];
66
+ earthPointSets = [];
67
+ astronomyFootprintSets = [];
68
+ earthFootprintSets = [];
63
69
  activePolylineSets = [];
64
70
  activeSensorCones = [];
65
71
  activeSatelliteObjects = [];
@@ -75,6 +81,7 @@ class AstroSphere {
75
81
  lockedNorthSouthDecDeg = null;
76
82
  keepCameraNorthUp = true;
77
83
  gridLabelContainers;
84
+ fovAnimationResolve = null;
78
85
  constructor(canvas, webgl, options = {}) {
79
86
  console.log("[AstroSphere] new instance for canvas", canvas.id);
80
87
  // Keep global GL context (as in original JS)
@@ -292,9 +299,7 @@ class AstroSphere {
292
299
  return true;
293
300
  }
294
301
  emitCameraChanged(reason) {
295
- // avoid dispatch before scene is ready
296
- if (!this._activeHiPS && !this._activeXYZ2 && !this._activeMeshHiPS)
297
- return;
302
+ // Camera interaction remains meaningful even when no base layer is active.
298
303
  if (!this._healpixGrid?.fovObj)
299
304
  return;
300
305
  const detail = this.getCurrentStatus();
@@ -321,6 +326,7 @@ class AstroSphere {
321
326
  canvas.setPointerCapture(event.pointerId);
322
327
  this.mouseDown = true;
323
328
  this._camera.cancelFlyTo();
329
+ this.cancelFoVAnimation();
324
330
  const rect = canvas.getBoundingClientRect();
325
331
  this.lastMouseX = event.clientX - rect.left; // locale al canvas
326
332
  this.lastMouseY = event.clientY - rect.top; // locale al canvas
@@ -359,8 +365,11 @@ class AstroSphere {
359
365
  if (mousePoint && mousePoint.length > 0) {
360
366
  this.mouseHelper.update(mousePoint);
361
367
  this.updateLastMousePoint();
362
- for (const cat of this.activeCatalogues) {
363
- const clickResult = cat.selectPrimarySourceFromClick(this.mouseHelper);
368
+ for (const cat of this.getActivePointSets()) {
369
+ const activeModelMatrix = this.getActiveDomainModelMatrix();
370
+ if (!activeModelMatrix)
371
+ continue;
372
+ const clickResult = cat.selectPrimarySourceFromClick(this.mouseHelper, activeModelMatrix, this._camera.getCameraMatrix(), this._perspectiveMatrixManager.pMatrix);
364
373
  if (!clickResult?.sources.length)
365
374
  continue;
366
375
  this._webgl.canvas.dispatchEvent(new CustomEvent("source-clicked", {
@@ -373,7 +382,7 @@ class AstroSphere {
373
382
  composed: true,
374
383
  }));
375
384
  }
376
- for (const fset of this.activeFootprintSets) {
385
+ for (const fset of this.getActiveFootprintSets()) {
377
386
  const clickResult = fset.selectPrimaryFootprintFromClick(this.mouseHelper);
378
387
  if (!clickResult?.footprints.length)
379
388
  continue;
@@ -448,6 +457,7 @@ class AstroSphere {
448
457
  const currentFov = this._healpixGrid.getMinFoV();
449
458
  const zoomStep = this.computeZoomStep(currentFov, event.deltaY);
450
459
  this._camera.cancelFlyTo();
460
+ this.cancelFoVAnimation();
451
461
  // Apply wheel zoom immediately and discard any queued inertia so reversing
452
462
  // direction feels responsive instead of "buffered".
453
463
  this.zoomInertia = 0;
@@ -470,8 +480,11 @@ class AstroSphere {
470
480
  }
471
481
  this.mouseHelper.update(mousePoint);
472
482
  this.updateLastMousePoint();
473
- for (const cat of this.activeCatalogues) {
474
- const pickResult = cat.getSourcesFromPointer(this.mouseHelper);
483
+ for (const cat of this.getActivePointSets()) {
484
+ const activeModelMatrix = this.getActiveDomainModelMatrix();
485
+ if (!activeModelMatrix)
486
+ continue;
487
+ const pickResult = cat.getSourcesFromPointer(this.mouseHelper, activeModelMatrix, this._camera.getCameraMatrix(), this._perspectiveMatrixManager.pMatrix);
475
488
  if (!pickResult?.sources.length)
476
489
  continue;
477
490
  this._webgl.canvas.dispatchEvent(new CustomEvent("source-contextmenu", {
@@ -486,7 +499,7 @@ class AstroSphere {
486
499
  }));
487
500
  break;
488
501
  }
489
- for (const fset of this.activeFootprintSets) {
502
+ for (const fset of this.getActiveFootprintSets()) {
490
503
  const pickResult = fset.getFootprintsFromPointer(this.mouseHelper);
491
504
  if (!pickResult?.footprints.length)
492
505
  continue;
@@ -576,19 +589,22 @@ class AstroSphere {
576
589
  return new HiPS(1, [0.0, 0.0, 0.0], 0, 0, hipsDescriptor, this._webgl, this._healpixGrid);
577
590
  }
578
591
  activateHiPS(hipsDescriptor) {
592
+ if (this._activeHiPSLayers.length > 0) {
593
+ throw new Error("Cannot load a HiPS base layer while stacked layers are active.");
594
+ }
579
595
  const tileBuffer = this._healpixGrid.visibleTilesManager.tileBuffer;
580
- for (const hips of this._activeHiPSLayers) {
581
- tileBuffer.removeHiPS(hips);
596
+ if (this._activeHiPS) {
597
+ tileBuffer.removeHiPS(this._activeHiPS);
582
598
  }
583
- this._activeHiPSLayers = [];
584
599
  const hips = this.createHiPS(hipsDescriptor);
585
- this._activeHiPSLayers.push(hips);
586
600
  this._activeHiPS = hips;
601
+ this._activeDomain = "astronomy";
587
602
  this._activeBaseLayer = "hips";
588
603
  return hips;
589
604
  }
590
605
  setHiPSOpacity(hips, opacity) {
591
- if (!this._activeHiPSLayers.includes(hips)) {
606
+ const isBaseLayer = this._activeHiPSLayers.length === 0 && this._activeHiPS === hips;
607
+ if (!isBaseLayer && !this._activeHiPSLayers.includes(hips)) {
592
608
  throw new Error("HiPS layer is not active in this AstroSphere.");
593
609
  }
594
610
  hips.setOpacity(opacity);
@@ -596,6 +612,13 @@ class AstroSphere {
596
612
  addHiPS(hipsDescriptor) {
597
613
  const normalizeURL = (url) => String(url).replace(/\/+$/, "");
598
614
  const descriptorURL = normalizeURL(hipsDescriptor.url);
615
+ // Entering stack mode discards the standalone base HiPS. Base mode and
616
+ // stack mode are intentionally mutually exclusive.
617
+ if (this._activeHiPSLayers.length === 0 && this._activeHiPS) {
618
+ const tileBuffer = this._healpixGrid.visibleTilesManager.tileBuffer;
619
+ tileBuffer.removeHiPS(this._activeHiPS);
620
+ this._activeHiPS = null;
621
+ }
599
622
  const existing = this._activeHiPSLayers.find((hips) => normalizeURL(hips.baseURL) === descriptorURL);
600
623
  if (existing) {
601
624
  throw new Error(`HiPS already active: ${String(hipsDescriptor.url)}`);
@@ -603,6 +626,7 @@ class AstroSphere {
603
626
  const hips = this.createHiPS(hipsDescriptor);
604
627
  this._activeHiPSLayers.push(hips);
605
628
  this._activeHiPS = hips;
629
+ this._activeDomain = "astronomy";
606
630
  this._activeBaseLayer = "hips";
607
631
  return hips;
608
632
  }
@@ -627,8 +651,13 @@ class AstroSphere {
627
651
  }
628
652
  removeAllHiPS() {
629
653
  const tileBuffer = this._healpixGrid.visibleTilesManager.tileBuffer;
630
- for (const hips of this._activeHiPSLayers) {
631
- tileBuffer.removeHiPS(hips);
654
+ if (this._activeHiPSLayers.length === 0 && this._activeHiPS) {
655
+ tileBuffer.removeHiPS(this._activeHiPS);
656
+ }
657
+ else {
658
+ for (const hips of this._activeHiPSLayers) {
659
+ tileBuffer.removeHiPS(hips);
660
+ }
632
661
  }
633
662
  this._activeHiPSLayers = [];
634
663
  this._activeHiPS = null;
@@ -641,39 +670,169 @@ class AstroSphere {
641
670
  this._activeBaseLayer = "xyz";
642
671
  }
643
672
  activateXYZ2(config) {
673
+ this._activeDomain = "earth";
644
674
  this._activeXYZ2 = new XYZMap(1, [0.0, 0.0, 0.0], 0, 0, config, this._webgl, this.gridLabelContainers);
645
675
  this._activeBaseLayer = "xyz";
646
676
  }
647
677
  activateMeshHiPS(descriptor) {
678
+ this._activeDomain = "mesh";
648
679
  this._activeMeshHiPS = new MeshHiPS(descriptor.meshRadius, [0.0, 0.0, 0.0], 0, 0, descriptor, this._webgl, this._healpixGrid);
649
680
  this._activeBaseLayer = "meships";
650
681
  }
682
+ setMeshHiPSColor(color) {
683
+ this._activeMeshHiPS?.setColor(color);
684
+ }
685
+ setMeshHiPSWireframe(wireframe) {
686
+ this._activeMeshHiPS?.setWireframe(wireframe);
687
+ }
651
688
  activateWMTS(config) {
689
+ this._activeDomain = "earth";
652
690
  const adapter = new WMTSAdapter(config);
653
691
  const xyzConfig = adapter.toXYZLayerConfig();
654
692
  this._activeXYZ2 = new XYZMap(1, [0.0, 0.0, 0.0], 0, 0, new XYZMapDescriptor(config.layer ? `WMTS ${config.layer}` : "WMTS Earth2 Layer", xyzConfig.urlTemplate, xyzConfig.minZoom ?? 0, xyzConfig.maxZoom ?? 8, xyzConfig.segmentsPerSide ?? 48, xyzConfig.maxCachedTiles ?? 384, 8, xyzConfig.urlResolver), this._webgl, this.gridLabelContainers);
655
693
  this._activeBaseLayer = "xyz";
656
694
  }
657
- // Catalogue section
695
+ addXYZRasterOverlay(config) {
696
+ const map = new XYZMap(1, [0.0, 0.0, 0.0], 0, 0, config, this._webgl, this.gridLabelContainers);
697
+ return this.addEarthRasterOverlay(config.name, "xyz", map);
698
+ }
699
+ addWMTSRasterOverlay(config) {
700
+ const adapter = new WMTSAdapter(config);
701
+ const xyzConfig = adapter.toXYZLayerConfig();
702
+ const name = config.layer ? `WMTS ${config.layer}` : "WMTS Earth Raster Overlay";
703
+ const map = new XYZMap(1, [0.0, 0.0, 0.0], 0, 0, new XYZMapDescriptor(name, xyzConfig.urlTemplate, xyzConfig.minZoom ?? 0, xyzConfig.maxZoom ?? 8, xyzConfig.segmentsPerSide ?? 48, xyzConfig.maxCachedTiles ?? 384, 8, xyzConfig.urlResolver), this._webgl, this.gridLabelContainers);
704
+ return this.addEarthRasterOverlay(name, "wmts", map);
705
+ }
706
+ getEarthRasterOverlays() {
707
+ return this._earthRasterOverlays.map(({ map: _map, ...overlay }) => ({ ...overlay }));
708
+ }
709
+ setEarthRasterOverlayOpacity(id, opacity) {
710
+ const overlay = this._earthRasterOverlays.find((entry) => entry.id === id);
711
+ if (!overlay)
712
+ return;
713
+ overlay.opacity = Math.min(1, Math.max(0, opacity));
714
+ overlay.map.setOpacity(overlay.opacity);
715
+ }
716
+ setEarthRasterOverlayVisible(id, visible) {
717
+ const overlay = this._earthRasterOverlays.find((entry) => entry.id === id);
718
+ if (overlay)
719
+ overlay.visible = visible;
720
+ }
721
+ removeEarthRasterOverlay(id) {
722
+ this._earthRasterOverlays = this._earthRasterOverlays.filter((entry) => entry.id !== id);
723
+ }
724
+ removeAllEarthRasterOverlays() {
725
+ this._earthRasterOverlays = [];
726
+ }
727
+ addEarthRasterOverlay(name, sourceType, map) {
728
+ const overlay = {
729
+ id: `earth-raster-${++this._earthRasterOverlaySequence}`,
730
+ name,
731
+ sourceType,
732
+ visible: true,
733
+ opacity: 0.65,
734
+ map,
735
+ };
736
+ map.setOpacity(overlay.opacity);
737
+ this._earthRasterOverlays.push(overlay);
738
+ const { map: _map, ...info } = overlay;
739
+ return { ...info };
740
+ }
741
+ setActiveDomain(domain) {
742
+ if (domain !== this._activeDomain) {
743
+ this.clearGridLabelsForDomain(this._activeDomain);
744
+ this.domainCameraStates[this._activeDomain] = this._camera.getViewState();
745
+ const targetCameraState = this.domainCameraStates[domain];
746
+ if (targetCameraState) {
747
+ this._camera.restoreViewState(targetCameraState);
748
+ }
749
+ this.inertiaX = 0;
750
+ this.inertiaY = 0;
751
+ this.zoomInertia = 0;
752
+ this._cameraStatusChanged = true;
753
+ }
754
+ this._activeDomain = domain;
755
+ if (domain === "astronomy") {
756
+ this._activeBaseLayer = this._activeHiPS ? "hips" : null;
757
+ }
758
+ else if (domain === "earth") {
759
+ this._activeBaseLayer = this._activeXYZ2 ? "xyz" : null;
760
+ }
761
+ else {
762
+ this._activeBaseLayer = this._activeMeshHiPS ? "meships" : null;
763
+ }
764
+ // A hover from the previous vertical must not leak into the new one.
765
+ this.lastHoveredSource = null;
766
+ this.lastHoveredCatalogue = null;
767
+ }
768
+ clearGridLabelsForDomain(domain) {
769
+ if (domain === "astronomy") {
770
+ this._healpixGrid?.clearLabels();
771
+ this._equatorialGrid?.clearLabels();
772
+ return;
773
+ }
774
+ if (domain === "earth") {
775
+ this._activeXYZ2?.clearLonLatGridLabels?.();
776
+ }
777
+ }
778
+ get activeDomain() {
779
+ return this._activeDomain;
780
+ }
781
+ getActivePointSets() {
782
+ if (this._activeDomain === "astronomy")
783
+ return this.astronomyCatalogues;
784
+ if (this._activeDomain === "earth")
785
+ return this.earthPointSets;
786
+ return [];
787
+ }
788
+ getActiveFootprintSets() {
789
+ if (this._activeDomain === "astronomy")
790
+ return this.astronomyFootprintSets;
791
+ if (this._activeDomain === "earth")
792
+ return this.earthFootprintSets;
793
+ return [];
794
+ }
795
+ // Astronomy catalogue ownership
658
796
  async showCatalogue(cat) {
659
- // console.log(cat)
660
- if (cat)
661
- this.activeCatalogues.push(cat);
797
+ if (cat && !this.astronomyCatalogues.includes(cat)) {
798
+ this.astronomyCatalogues.push(cat);
799
+ }
662
800
  return cat;
663
801
  }
664
802
  deleteCatalogue(catalogue) {
665
- this.activeCatalogues = this.activeCatalogues.filter((c) => c !== catalogue);
803
+ this.astronomyCatalogues = this.astronomyCatalogues.filter((c) => c !== catalogue);
666
804
  }
667
- // End Catalogue section
668
- // Footprint section
805
+ // Earth point-set ownership. TerraPointSetGL still extends CatalogueGL in
806
+ // 3.12, but it no longer shares the Astronomy lifecycle collection.
807
+ async showTerraPointSet(pointSet) {
808
+ if (pointSet && !this.earthPointSets.includes(pointSet)) {
809
+ this.earthPointSets.push(pointSet);
810
+ }
811
+ return pointSet;
812
+ }
813
+ deleteTerraPointSet(pointSet) {
814
+ this.earthPointSets = this.earthPointSets.filter((set) => set !== pointSet);
815
+ }
816
+ // Astronomy footprint ownership
669
817
  async showFootprintSet(fset) {
670
- // console.log(fset)
671
- if (fset)
672
- this.activeFootprintSets.push(fset);
818
+ if (fset && !this.astronomyFootprintSets.includes(fset)) {
819
+ this.astronomyFootprintSets.push(fset);
820
+ }
673
821
  return fset;
674
822
  }
675
823
  deleteFootprintSet(footprintSet) {
676
- this.activeFootprintSets = this.activeFootprintSets.filter((fst) => fst !== footprintSet);
824
+ this.astronomyFootprintSets = this.astronomyFootprintSets.filter((fst) => fst !== footprintSet);
825
+ }
826
+ // Earth footprint ownership. TerraFootprintSetGL still reuses the common
827
+ // FootprintSetGL rendering implementation, but has an independent lifecycle.
828
+ async showTerraFootprintSet(footprintSet) {
829
+ if (footprintSet && !this.earthFootprintSets.includes(footprintSet)) {
830
+ this.earthFootprintSets.push(footprintSet);
831
+ }
832
+ return footprintSet;
833
+ }
834
+ deleteTerraFootprintSet(footprintSet) {
835
+ this.earthFootprintSets = this.earthFootprintSets.filter((fst) => fst !== footprintSet);
677
836
  }
678
837
  async showPolylineSet(polylineSet) {
679
838
  if (polylineSet)
@@ -703,8 +862,8 @@ class AstroSphere {
703
862
  satelliteObject.dispose();
704
863
  }
705
864
  getHoveredFootprints() {
706
- let footprintsHovered = [];
707
- this.activeFootprintSets.forEach((fset) => {
865
+ const footprintsHovered = [];
866
+ this.getActiveFootprintSets().forEach((fset) => {
708
867
  footprintsHovered.push(fset.hoveredFootprints);
709
868
  });
710
869
  return footprintsHovered;
@@ -756,25 +915,128 @@ class AstroSphere {
756
915
  throw new Error(`healpixGrid is ${this.healpixGrid}`);
757
916
  return FoVUtils.getFoVPolygon(this._camera, this.canvas, this._healpixGrid, this._healpixGrid, this._webgl, this._perspectiveMatrixManager.pMatrix);
758
917
  }
759
- changeFoV(deg) {
760
- const distance = this._healpixGrid.getFoV().computeDistanceFromAngle(deg);
761
- this._camera.translate(distance);
918
+ validateFoV(deg) {
919
+ if (!Number.isFinite(deg) || deg <= 0 || deg >= 180) {
920
+ throw new RangeError(`FoV must be > 0 and < 180 degrees. Received ${deg}.`);
921
+ }
922
+ if (global.insideSphere) {
923
+ throw new Error("FoV navigation is currently supported only outside the sphere.");
924
+ }
925
+ }
926
+ /**
927
+ * Resolve the radial camera distance that produces the requested minimum FoV.
928
+ *
929
+ * The search temporarily samples radial distances, then restores the original
930
+ * camera distance before returning. Callers can therefore either apply the
931
+ * result immediately or animate toward it.
932
+ */
933
+ findRadialDistanceForFoV(deg) {
934
+ this.validateFoV(deg);
935
+ const originalDistance = this._camera.getRadialDistance();
936
+ const minDistance = 1.000001;
937
+ const maxDistance = 4.0;
938
+ const tolerance = Math.max(1e-5, deg * 0.001);
939
+ const maxIterations = 40;
940
+ let low = minDistance;
941
+ let high = maxDistance;
942
+ let bestDistance = originalDistance;
943
+ let bestError = Number.POSITIVE_INFINITY;
944
+ for (let iteration = 0; iteration < maxIterations; iteration += 1) {
945
+ const candidateDistance = (low + high) / 2;
946
+ this._camera.setRadialDistance(candidateDistance);
947
+ this._perspectiveMatrixManager.computePerspectiveMatrix(this.canvas, this._camera, bootSetup.camera_fov_deg, bootSetup.camera_near_plane, global.insideSphere);
948
+ const candidateFoV = this._healpixGrid.refreshFoV(this._camera, this._perspectiveMatrixManager.pMatrix);
949
+ const measuredFoV = candidateFoV.minFoV;
950
+ const error = Math.abs(measuredFoV - deg);
951
+ if (error < bestError) {
952
+ bestError = error;
953
+ bestDistance = candidateDistance;
954
+ }
955
+ if (error <= tolerance) {
956
+ break;
957
+ }
958
+ if (measuredFoV >= deg || measuredFoV >= 179.999) {
959
+ high = candidateDistance;
960
+ }
961
+ else {
962
+ low = candidateDistance;
963
+ }
964
+ }
965
+ this._camera.setRadialDistance(originalDistance);
966
+ this._perspectiveMatrixManager.computePerspectiveMatrix(this.canvas, this._camera, bootSetup.camera_fov_deg, bootSetup.camera_near_plane, global.insideSphere);
967
+ this.fov = this._healpixGrid.refreshFoV(this._camera, this._perspectiveMatrixManager.pMatrix);
968
+ this._camera.refreshFoV(this.fov.minFoV);
969
+ return bestDistance;
970
+ }
971
+ finishFoVAnimation() {
972
+ const resolve = this.fovAnimationResolve;
973
+ this.fovAnimationResolve = null;
974
+ resolve?.();
975
+ }
976
+ cancelFoVAnimation() {
977
+ this._camera.cancelRadialAnimation();
978
+ this.finishFoVAnimation();
979
+ }
980
+ /** Set the minimum angular field of view immediately, in degrees. */
981
+ setFoV(deg) {
982
+ this.validateFoV(deg);
983
+ this.cancelFoVAnimation();
984
+ this._camera.cancelFlyTo();
985
+ this.zoomInertia = 0;
986
+ const targetDistance = this.findRadialDistanceForFoV(deg);
987
+ this._camera.setRadialDistance(targetDistance);
988
+ this._perspectiveMatrixManager.computePerspectiveMatrix(this.canvas, this._camera, bootSetup.camera_fov_deg, bootSetup.camera_near_plane, global.insideSphere);
762
989
  this.fov = this._healpixGrid.refreshFoV(this._camera, this._perspectiveMatrixManager.pMatrix);
763
990
  this._camera.refreshFoV(this.fov.minFoV);
991
+ this.lastCameraMotionAt = performance.now();
992
+ this._cameraStatusChanged = true;
993
+ this.emitCameraChanged("set-fov");
994
+ }
995
+ async flyToFoV(deg, durationMs = 1200) {
996
+ this.validateFoV(deg);
997
+ if (!Number.isFinite(durationMs) || durationMs < 0) {
998
+ throw new RangeError(`FoV animation duration must be >= 0 ms. Received ${durationMs}.`);
999
+ }
1000
+ if (durationMs === 0) {
1001
+ this.setFoV(deg);
1002
+ return;
1003
+ }
1004
+ this.cancelFoVAnimation();
1005
+ this.zoomInertia = 0;
1006
+ const targetDistance = this.findRadialDistanceForFoV(deg);
1007
+ this._camera.flyToRadialDistance(targetDistance, durationMs);
1008
+ this.lastCameraMotionAt = performance.now();
1009
+ this._cameraStatusChanged = true;
1010
+ return new Promise((resolve) => {
1011
+ this.fovAnimationResolve = resolve;
1012
+ });
1013
+ }
1014
+ async zoomIn(steps = 1, durationMs = 500) {
1015
+ this.validateZoomSteps(steps);
1016
+ const targetFoV = this.getFoV().minFoV / Math.pow(2, steps);
1017
+ return this.flyToFoV(targetFoV, durationMs);
1018
+ }
1019
+ async zoomOut(steps = 1, durationMs = 500) {
1020
+ this.validateZoomSteps(steps);
1021
+ const targetFoV = this.getFoV().minFoV * Math.pow(2, steps);
1022
+ return this.flyToFoV(targetFoV, durationMs);
764
1023
  }
1024
+ validateZoomSteps(steps) {
1025
+ if (!Number.isInteger(steps) || steps < 1) {
1026
+ throw new RangeError(`Zoom steps must be a positive integer. Received ${steps}.`);
1027
+ }
1028
+ }
1029
+ /** @deprecated Use setFoV(). */
1030
+ changeFoV(deg) {
1031
+ this.setFoV(deg);
1032
+ }
1033
+ /** @deprecated Use setFoV(). */
765
1034
  changeFoV2(deg) {
766
- const newCameraPos = this._healpixGrid
767
- .getFoV()
768
- .computeCameraPositionForFoV(deg);
769
- this._camera.setCameraPosition(newCameraPos);
1035
+ this.setFoV(deg);
770
1036
  }
1037
+ /** @deprecated Use setFoV(). */
771
1038
  changeFoV3(deg) {
772
- const newPos = this._healpixGrid
773
- .getFoV()
774
- .computeCameraPositionForAngularDiameter(deg);
775
- this._camera.setCameraPosition(newPos);
776
- // Recompute projection after moving the camera
777
- this._perspectiveMatrixManager.computePerspectiveMatrix(this.canvas, this._camera, bootSetup.camera_fov_deg, bootSetup.camera_near_plane, false);
1039
+ this.setFoV(deg);
778
1040
  }
779
1041
  getInsideSphere() {
780
1042
  return global.insideSphere;
@@ -784,6 +1046,7 @@ class AstroSphere {
784
1046
  this.inertiaX = 0;
785
1047
  this.inertiaY = 0;
786
1048
  this.zoomInertia = 0;
1049
+ this.cancelFoVAnimation();
787
1050
  global.insideSphere = !global.insideSphere;
788
1051
  // console.log(global.insideSphere)
789
1052
  this._camera.toggleInsideSphere();
@@ -936,8 +1199,6 @@ class AstroSphere {
936
1199
  return;
937
1200
  if (!this._webgl)
938
1201
  return;
939
- if (!this._activeHiPS && !this._activeXYZ2 && !this._activeMeshHiPS)
940
- return;
941
1202
  if (!this._healpixGrid || Object.keys(this._healpixGrid).length === 0)
942
1203
  return;
943
1204
  if (this._healpixGrid.fovObj === undefined)
@@ -961,6 +1222,14 @@ class AstroSphere {
961
1222
  this.updateCentralPoint();
962
1223
  this._perspectiveMatrixManager.computePerspectiveMatrix(canvas, this._camera, bootSetup.camera_fov_deg, bootSetup.camera_near_plane, global.insideSphere);
963
1224
  }
1225
+ const fovFlying = this._camera.updateRadialAnimation(now);
1226
+ const fovAnimationFinished = fovFlying && !this._camera.isRadialAnimationActive();
1227
+ if (fovFlying) {
1228
+ this.zoomInertia = 0;
1229
+ this.lastCameraMotionAt = now;
1230
+ this._cameraStatusChanged = true;
1231
+ this._perspectiveMatrixManager.computePerspectiveMatrix(canvas, this._camera, bootSetup.camera_fov_deg, bootSetup.camera_near_plane, global.insideSphere);
1232
+ }
964
1233
  // Zoom inertia
965
1234
  if (this.zoomInertia !== 0) {
966
1235
  if (Math.abs(this.zoomInertia) > 0.0001) {
@@ -1009,6 +1278,9 @@ class AstroSphere {
1009
1278
  this._camera.refreshFoV(this.fov.minFoV);
1010
1279
  this.prevFov = this.fov.minFoV;
1011
1280
  }
1281
+ if (fovAnimationFinished) {
1282
+ this.finishFoVAnimation();
1283
+ }
1012
1284
  // Se la camera è ruotata (anche solo per inerzia), aggiorna punto centrale + emetti cameraChanged
1013
1285
  if (cameraRotated) {
1014
1286
  // Ricalcola il punto centrale
@@ -1049,8 +1321,14 @@ class AstroSphere {
1049
1321
  this._webgl.enable(this._webgl.CULL_FACE);
1050
1322
  this._webgl.cullFace(global.insideSphere ? this._webgl.FRONT : this._webgl.BACK);
1051
1323
  this._webgl.blendFunc(this._webgl.SRC_ALPHA, this._webgl.ONE_MINUS_SRC_ALPHA);
1052
- if (this._activeBaseLayer === "hips" && this._activeHiPSLayers.length > 0) {
1053
- const maxHiPSOrder = Math.max(...this._activeHiPSLayers.map((hips) => hips.maxOrder));
1324
+ if (this._activeBaseLayer === "hips" && this._activeHiPS) {
1325
+ // During directional flyTo navigation, keep visibility current but render
1326
+ // only HiPS tiles that are already available. Final-view refinement
1327
+ // resumes automatically on the first frame after the flight.
1328
+ this._healpixGrid.visibleTilesManager.tileBuffer.setTileAcquisitionEnabled(!cameraFlying);
1329
+ const maxHiPSOrder = this._activeHiPSLayers.length > 0
1330
+ ? Math.max(...this._activeHiPSLayers.map((hips) => hips.maxOrder))
1331
+ : this._activeHiPS.maxOrder;
1054
1332
  const visibleOrder = Math.min(this._healpixGrid.visibleorder, maxHiPSOrder);
1055
1333
  this._healpixGrid.visibleTilesManager.computeVisiblePixels(visibleOrder, this._webgl, this._camera, this._perspectiveMatrixManager.pMatrix);
1056
1334
  }
@@ -1065,6 +1343,7 @@ class AstroSphere {
1065
1343
  Math.abs(this.zoomInertia) > 0.0001 ||
1066
1344
  Math.abs(this.inertiaX) > 0.02 ||
1067
1345
  Math.abs(this.inertiaY) > 0.02 ||
1346
+ this._camera.isRadialAnimationActive() ||
1068
1347
  nowForGrid - this.lastCameraMotionAt < 220;
1069
1348
  const skyEntityDrawInput = {
1070
1349
  fovDeg: stableFovDeg,
@@ -1085,12 +1364,19 @@ class AstroSphere {
1085
1364
  }
1086
1365
  if (this._activeBaseLayer === "xyz") {
1087
1366
  this._activeXYZ2?.draw(skyEntityDrawInput);
1367
+ for (const overlay of this._earthRasterOverlays) {
1368
+ if (overlay.visible) {
1369
+ overlay.map.draw(skyEntityDrawInput, true);
1370
+ }
1371
+ }
1088
1372
  }
1089
1373
  if (this._activeBaseLayer === "meships") {
1090
1374
  this._activeMeshHiPS?.draw(skyEntityDrawInput);
1091
1375
  }
1092
- this._healpixGrid.draw(skyEntityDrawInput);
1093
- this._equatorialGrid.draw(skyEntityDrawInput);
1376
+ if (this._activeDomain === "astronomy") {
1377
+ this._healpixGrid.draw(skyEntityDrawInput);
1378
+ this._equatorialGrid.draw(skyEntityDrawInput);
1379
+ }
1094
1380
  this._webgl.enable(this._webgl.DEPTH_TEST);
1095
1381
  this._webgl.disable(this._webgl.CULL_FACE);
1096
1382
  if (this.startup) {
@@ -1109,52 +1395,53 @@ class AstroSphere {
1109
1395
  decDMS,
1110
1396
  });
1111
1397
  }
1112
- this.activeCatalogues.forEach((cat) => {
1113
- const activeModelMatrix = this._activeHiPS?.getModelMatrix() ??
1114
- this._activeXYZ2?.getModelMatrix() ??
1115
- this._activeMeshHiPS?.getModelMatrix();
1398
+ this.getActivePointSets().forEach((cat) => {
1399
+ const activeModelMatrix = this.getActiveDomainModelMatrix();
1116
1400
  if (activeModelMatrix) {
1117
1401
  cat.draw(activeModelMatrix, this.mouseHelper, this._camera.getCameraMatrix(), this._perspectiveMatrixManager.pMatrix);
1118
1402
  }
1119
1403
  });
1120
1404
  this.emitHoveredSourceIfChanged();
1121
- this.activeFootprintSets.forEach((fst) => {
1122
- const activeModelMatrix = this._activeHiPS?.getModelMatrix() ??
1123
- this._activeXYZ2?.getModelMatrix() ??
1124
- this._activeMeshHiPS?.getModelMatrix();
1405
+ this.getActiveFootprintSets().forEach((fst) => {
1406
+ const activeModelMatrix = this.getActiveDomainModelMatrix();
1125
1407
  if (activeModelMatrix) {
1126
1408
  fst.draw(activeModelMatrix, this.mouseHelper, this._camera.getCameraMatrix(), this._perspectiveMatrixManager.pMatrix);
1127
1409
  }
1128
1410
  });
1129
- this.activePolylineSets.forEach((polylineSet) => {
1130
- const activeModelMatrix = this._activeHiPS?.getModelMatrix() ??
1131
- this._activeXYZ2?.getModelMatrix() ??
1132
- this._activeMeshHiPS?.getModelMatrix();
1133
- if (activeModelMatrix) {
1134
- polylineSet.draw(activeModelMatrix, this.mouseHelper, this._camera.getCameraMatrix(), this._perspectiveMatrixManager.pMatrix);
1135
- }
1136
- });
1137
- this.activeSensorCones.forEach((sensorCone) => {
1138
- const activeModelMatrix = this._activeHiPS?.getModelMatrix() ??
1139
- this._activeXYZ2?.getModelMatrix() ??
1140
- this._activeMeshHiPS?.getModelMatrix();
1141
- if (activeModelMatrix) {
1142
- sensorCone.draw(this._perspectiveMatrixManager.pMatrix, this._camera.getCameraMatrix(), activeModelMatrix);
1143
- }
1144
- });
1145
- this.activeSatelliteObjects.forEach((satelliteObject) => {
1146
- const activeModelMatrix = this._activeHiPS?.getModelMatrix() ??
1147
- this._activeXYZ2?.getModelMatrix() ??
1148
- this._activeMeshHiPS?.getModelMatrix();
1149
- if (activeModelMatrix) {
1150
- satelliteObject.draw(this._perspectiveMatrixManager.pMatrix, this._camera.getCameraMatrix(), activeModelMatrix);
1151
- }
1152
- });
1411
+ if (this._activeDomain === "earth") {
1412
+ this.activePolylineSets.forEach((polylineSet) => {
1413
+ const activeModelMatrix = this.getActiveDomainModelMatrix();
1414
+ if (activeModelMatrix) {
1415
+ polylineSet.draw(activeModelMatrix, this.mouseHelper, this._camera.getCameraMatrix(), this._perspectiveMatrixManager.pMatrix);
1416
+ }
1417
+ });
1418
+ this.activeSensorCones.forEach((sensorCone) => {
1419
+ const activeModelMatrix = this.getActiveDomainModelMatrix();
1420
+ if (activeModelMatrix) {
1421
+ sensorCone.draw(this._perspectiveMatrixManager.pMatrix, this._camera.getCameraMatrix(), activeModelMatrix);
1422
+ }
1423
+ });
1424
+ this.activeSatelliteObjects.forEach((satelliteObject) => {
1425
+ const activeModelMatrix = this.getActiveDomainModelMatrix();
1426
+ if (activeModelMatrix) {
1427
+ satelliteObject.draw(this._perspectiveMatrixManager.pMatrix, this._camera.getCameraMatrix(), activeModelMatrix);
1428
+ }
1429
+ });
1430
+ }
1431
+ }
1432
+ getActiveDomainModelMatrix() {
1433
+ if (this._activeDomain === "astronomy") {
1434
+ return (this._activeHiPS?.getModelMatrix() ?? this._healpixGrid.getModelMatrix());
1435
+ }
1436
+ if (this._activeDomain === "earth") {
1437
+ return (this._activeXYZ2?.getModelMatrix() ?? this._healpixGrid.getModelMatrix());
1438
+ }
1439
+ return (this._activeMeshHiPS?.getModelMatrix() ?? this._healpixGrid.getModelMatrix());
1153
1440
  }
1154
1441
  emitHoveredSourceIfChanged() {
1155
1442
  let nextHoveredSource = null;
1156
1443
  let nextHoveredCatalogue = null;
1157
- for (const cat of this.activeCatalogues) {
1444
+ for (const cat of this.getActivePointSets()) {
1158
1445
  const hovered = cat.getPrimaryHoveredSource();
1159
1446
  if (!hovered)
1160
1447
  continue;