astro-viewer 3.10.0 → 3.11.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.
package/README.md CHANGED
@@ -21,8 +21,12 @@ AstroViewer is developed from scratch without depending on visualization framewo
21
21
  - HiPS coverage handling
22
22
  - HEALPix hierarchical sky tessellation
23
23
  - FITS astronomical data support
24
+ - FITS display scaling (linear, sqrt, log, asinh, gamma)
25
+ - FITS display range modes
26
+ - Runtime color maps
24
27
  - Astronomical coordinate systems and transformations
25
28
  - Field-of-view calculations
29
+ - Interactive navigation and animated Fly To
26
30
  - Grid and coordinate visualization
27
31
  - TAP-based astronomical data access
28
32
 
@@ -34,6 +38,7 @@ AstroViewer is developed from scratch without depending on visualization framewo
34
38
  - GeoJSON geometries and overlays
35
39
  - Satellite tracks and observation geometry
36
40
  - Sensor footprints and cones
41
+ - Longitude/latitude navigation and animated Fly To
37
42
  - Earth-oriented coordinate and picking utilities
38
43
 
39
44
  ### 3D and Scientific Visualization
@@ -42,7 +47,7 @@ AstroViewer is developed from scratch without depending on visualization framewo
42
47
  - Inside/outside sphere visualization
43
48
  - MeshHiPS OBJ tiled meshes
44
49
  - Runtime color maps
45
- - Interactive camera and navigation
50
+ - Interactive camera, navigation and animated Fly To
46
51
  - Ray picking
47
52
  - Scientific overlays and geometries
48
53
  - Framework-independent architecture
@@ -100,6 +105,7 @@ A HiPS survey can be activated using a `HiPSDescriptor`:
100
105
  import { AstroViewer, HiPSDescriptor } from "astro-viewer";
101
106
 
102
107
  const canvas = document.getElementById("astrocanvas");
108
+
103
109
  const viewer = new AstroViewer(canvas);
104
110
 
105
111
  const hipsUrl = "https://alasky.cds.unistra.fr/DSS/DSSColor/";
@@ -189,6 +195,26 @@ AstroViewer supports conventional image-based HiPS tiles and FITS HiPS tiles.
189
195
 
190
196
  ---
191
197
 
198
+ ## FITS HiPS Visualization
199
+
200
+ AstroViewer can render FITS tiles directly as part of the HiPS rendering pipeline.
201
+
202
+ FITS visualization supports multiple display scaling functions:
203
+
204
+ ```text
205
+ linear
206
+ sqrt
207
+ log
208
+ asinh
209
+ gamma
210
+ ```
211
+
212
+ The display range and scaling parameters can be changed at runtime, allowing scientific image data with different dynamic ranges to be explored interactively.
213
+
214
+ Color maps can also be applied at runtime to FITS-based HiPS data.
215
+
216
+ ---
217
+
192
218
  ## HiPS Coverage
193
219
 
194
220
  AstroViewer supports survey coverage information associated with HiPS datasets.
@@ -199,6 +225,38 @@ This is particularly useful for partial-sky surveys.
199
225
 
200
226
  ---
201
227
 
228
+ ## Navigation and Fly To
229
+
230
+ AstroViewer provides immediate and animated camera navigation.
231
+
232
+ The viewer can move directly to a target coordinate:
233
+
234
+ ```js
235
+ viewer.goTo(firstDeg, secondDeg);
236
+ ```
237
+
238
+ or smoothly animate the camera towards the target:
239
+
240
+ ```js
241
+ viewer.flyTo(firstDeg, secondDeg);
242
+ ```
243
+
244
+ An optional duration can be specified in milliseconds:
245
+
246
+ ```js
247
+ viewer.flyTo(firstDeg, secondDeg, 1500);
248
+ ```
249
+
250
+ The coordinate semantics depend on the active visualization domain:
251
+
252
+ - Astronomy uses right ascension and declination.
253
+ - Earth visualization uses longitude and latitude.
254
+ - Planetary and MeshHiPS visualization uses longitude and latitude.
255
+
256
+ Fly To uses spherical interpolation and integrates with the viewer rendering loop. User interaction interrupts an active Fly To operation, allowing normal interactive navigation to resume immediately.
257
+
258
+ ---
259
+
202
260
  ## Earth Maps
203
261
 
204
262
  AstroViewer also supports tiled Earth observation maps.
@@ -265,17 +323,21 @@ addHiPS(...)
265
323
  addHiPSFromUrl(...)
266
324
  removeHiPS(...)
267
325
  removeAllHiPS()
268
-
269
326
  getActiveHiPS()
270
327
  getActiveHiPSLayers()
271
328
  setActiveHiPS(...)
272
-
273
329
  getActiveHiPSFormats()
274
330
  changeHiPSFormat(...)
275
-
276
331
  setHiPSOpacity(...)
277
332
  ```
278
333
 
334
+ ### Navigation
335
+
336
+ ```ts
337
+ goTo(...)
338
+ flyTo(...)
339
+ ```
340
+
279
341
  ### Maps and visualization
280
342
 
281
343
  AstroViewer also exposes functionality around:
@@ -43,6 +43,7 @@ __webpack_require__.d(vec3_namespaceObject, {
43
43
  dot: () => (dot),
44
44
  fromValues: () => (vec3_fromValues),
45
45
  length: () => (vec3_length),
46
+ lerp: () => (lerp),
46
47
  normalize: () => (normalize),
47
48
  scale: () => (vec3_scale),
48
49
  scaleAndAdd: () => (scaleAndAdd),
@@ -3895,6 +3896,7 @@ class AstroSphere {
3895
3896
  const handleMouseDown = (event) => {
3896
3897
  canvas.setPointerCapture(event.pointerId);
3897
3898
  this.mouseDown = true;
3899
+ this._camera.cancelFlyTo();
3898
3900
  const rect = canvas.getBoundingClientRect();
3899
3901
  this.lastMouseX = event.clientX - rect.left; // locale al canvas
3900
3902
  this.lastMouseY = event.clientY - rect.top; // locale al canvas
@@ -4021,6 +4023,7 @@ class AstroSphere {
4021
4023
  const handleMouseWheel = (event) => {
4022
4024
  const currentFov = this._healpixGrid.getMinFoV();
4023
4025
  const zoomStep = this.computeZoomStep(currentFov, event.deltaY);
4026
+ this._camera.cancelFlyTo();
4024
4027
  // Apply wheel zoom immediately and discard any queued inertia so reversing
4025
4028
  // direction feels responsive instead of "buffered".
4026
4029
  this.zoomInertia = 0;
@@ -4286,6 +4289,9 @@ class AstroSphere {
4286
4289
  goTo(raDeg, decDeg) {
4287
4290
  this._camera.goTo(raDeg, decDeg);
4288
4291
  }
4292
+ flyTo(raDeg, decDeg, durationMs = 1200) {
4293
+ this._camera.flyTo(raDeg, decDeg, durationMs);
4294
+ }
4289
4295
  getActiveCoordinateMode() {
4290
4296
  if (this._activeBaseLayer === "xyz") {
4291
4297
  return "lonlat";
@@ -4521,6 +4527,16 @@ class AstroSphere {
4521
4527
  let PHI = 0;
4522
4528
  this._webgl.viewport(0, 0, this._webgl.drawingBufferWidth, this._webgl.drawingBufferHeight);
4523
4529
  this._webgl.clear(this._webgl.COLOR_BUFFER_BIT | this._webgl.DEPTH_BUFFER_BIT);
4530
+ const now = performance.now();
4531
+ const cameraFlying = this._camera.updateFlyTo(now);
4532
+ if (cameraFlying) {
4533
+ this.inertiaX = 0;
4534
+ this.inertiaY = 0;
4535
+ this.lastCameraMotionAt = now;
4536
+ this._cameraStatusChanged = true;
4537
+ this.updateCentralPoint();
4538
+ this._perspectiveMatrixManager.computePerspectiveMatrix(canvas, this._camera, Config_js_1.bootSetup.camera_fov_deg, Config_js_1.bootSetup.camera_near_plane, Global_js_1.default.insideSphere);
4539
+ }
4524
4540
  // Zoom inertia
4525
4541
  if (this.zoomInertia !== 0) {
4526
4542
  if (Math.abs(this.zoomInertia) > 0.0001) {
@@ -5051,6 +5067,9 @@ class AstroViewer {
5051
5067
  // console.log(`AstroViewer.goTo goto(${raDeg}, ${decDeg})`)
5052
5068
  this.astroSphere.goTo(raDeg, decDeg);
5053
5069
  }
5070
+ flyTo(raDeg, decDeg, durationMs = 1200) {
5071
+ this.astroSphere.flyTo(raDeg, decDeg, durationMs);
5072
+ }
5054
5073
  getActiveCoordinateMode() {
5055
5074
  return this.astroSphere.getActiveCoordinateMode();
5056
5075
  }
@@ -5372,6 +5391,7 @@ class Camera {
5372
5391
  lockRotX = false;
5373
5392
  lockRotY = false;
5374
5393
  lockRotZ = false;
5394
+ flyToAnimation = null;
5375
5395
  constructor(in_position, in_sphere) {
5376
5396
  this.init(in_position, in_sphere);
5377
5397
  }
@@ -5381,7 +5401,11 @@ class Camera {
5381
5401
  this.vMatrix = gl_matrix_1.mat4.create();
5382
5402
  this.T = gl_matrix_1.mat4.create();
5383
5403
  this.R = gl_matrix_1.mat4.create();
5384
- gl_matrix_1.mat4.translate(this.T, this.T, [this.cam_pos[0], this.cam_pos[1], this.cam_pos[2]]);
5404
+ gl_matrix_1.mat4.translate(this.T, this.T, [
5405
+ this.cam_pos[0],
5406
+ this.cam_pos[1],
5407
+ this.cam_pos[2],
5408
+ ]);
5385
5409
  // reset helpers
5386
5410
  this.FoV = this.previousFoV = 180.0;
5387
5411
  this.move = gl_matrix_1.vec3.clone([0, 0, 0]);
@@ -5390,11 +5414,15 @@ class Camera {
5390
5414
  this.goTo(raDeg, decDeg);
5391
5415
  }
5392
5416
  goTo(raDeg, decDeg) {
5417
+ this.cancelFlyTo();
5393
5418
  this.goToPhiTheta((0, Utils_js_1.astroDegToSpherical)(raDeg, decDeg));
5394
5419
  }
5395
5420
  goToPhiTheta(ptDeg) {
5396
5421
  const xyz = (0, Utils_js_1.sphericalToCartesian)(ptDeg.phi, ptDeg.theta, this.cam_pos[2]);
5397
- const targetDirection = gl_matrix_1.vec3.normalize(gl_matrix_1.vec3.create(), gl_matrix_1.vec3.fromValues(xyz[0], xyz[1], xyz[2]));
5422
+ this.applyCameraPosition(gl_matrix_1.vec3.fromValues(xyz[0], xyz[1], xyz[2]));
5423
+ }
5424
+ applyCameraPosition(cameraPosition) {
5425
+ const targetDirection = gl_matrix_1.vec3.normalize(gl_matrix_1.vec3.create(), cameraPosition);
5398
5426
  const celestialNorth = gl_matrix_1.vec3.fromValues(0.0, 0.0, 1.0);
5399
5427
  const northProjection = gl_matrix_1.vec3.scale(gl_matrix_1.vec3.create(), targetDirection, gl_matrix_1.vec3.dot(celestialNorth, targetDirection));
5400
5428
  const cameraUp = gl_matrix_1.vec3.subtract(gl_matrix_1.vec3.create(), celestialNorth, northProjection);
@@ -5405,9 +5433,13 @@ class Camera {
5405
5433
  gl_matrix_1.vec3.normalize(cameraUp, cameraUp);
5406
5434
  }
5407
5435
  let cameraMatrix = gl_matrix_1.mat4.create();
5408
- cameraMatrix = gl_matrix_1.mat4.translate(cameraMatrix, cameraMatrix, gl_matrix_1.vec3.fromValues(xyz[0], xyz[1], xyz[2]));
5436
+ cameraMatrix = gl_matrix_1.mat4.translate(cameraMatrix, cameraMatrix, cameraPosition);
5409
5437
  const focusPoint = [0.0, 0.0, 0.0];
5410
- const cameraPos = [cameraMatrix[12], cameraMatrix[13], cameraMatrix[14]];
5438
+ const cameraPos = [
5439
+ cameraMatrix[12],
5440
+ cameraMatrix[13],
5441
+ cameraMatrix[14],
5442
+ ];
5411
5443
  cameraMatrix = gl_matrix_1.mat4.targetTo(cameraMatrix, cameraPos, focusPoint, cameraUp);
5412
5444
  this.R = gl_matrix_1.mat4.clone(cameraMatrix);
5413
5445
  this.R[12] = 0;
@@ -5419,6 +5451,81 @@ class Camera {
5419
5451
  }
5420
5452
  this.vMatrix = viewMatrix;
5421
5453
  }
5454
+ flyTo(raDeg, decDeg, durationMs = 1200) {
5455
+ const targetSpherical = (0, Utils_js_1.astroDegToSpherical)(raDeg, decDeg);
5456
+ const targetPositionValues = (0, Utils_js_1.sphericalToCartesian)(targetSpherical.phi, targetSpherical.theta, this.cam_pos[2]);
5457
+ const targetPosition = gl_matrix_1.vec3.fromValues(targetPositionValues[0], targetPositionValues[1], targetPositionValues[2]);
5458
+ const currentPosition = this.getCameraPosition();
5459
+ const startDirection = gl_matrix_1.vec3.normalize(gl_matrix_1.vec3.create(), gl_matrix_1.vec3.fromValues(currentPosition[0], currentPosition[1], currentPosition[2]));
5460
+ const targetDirection = gl_matrix_1.vec3.normalize(gl_matrix_1.vec3.create(), targetPosition);
5461
+ this.flyToAnimation = {
5462
+ startDirection,
5463
+ targetDirection,
5464
+ targetPosition,
5465
+ startTime: performance.now(),
5466
+ durationMs: Math.max(1, durationMs),
5467
+ };
5468
+ }
5469
+ updateFlyTo(now) {
5470
+ const animation = this.flyToAnimation;
5471
+ if (!animation) {
5472
+ return false;
5473
+ }
5474
+ const elapsed = now - animation.startTime;
5475
+ const progress = Math.min(1, elapsed / animation.durationMs);
5476
+ const easedProgress = progress < 0.5
5477
+ ? 4 * progress * progress * progress
5478
+ : 1 - Math.pow(-2 * progress + 2, 3) / 2;
5479
+ const direction = this.slerpDirection(animation.startDirection, animation.targetDirection, easedProgress);
5480
+ const radius = Math.abs(this.cam_pos[2]);
5481
+ const cameraPosition = gl_matrix_1.vec3.scale(gl_matrix_1.vec3.create(), direction, radius);
5482
+ this.applyCameraPosition(cameraPosition);
5483
+ if (progress >= 1) {
5484
+ this.applyCameraPosition(animation.targetPosition);
5485
+ this.flyToAnimation = null;
5486
+ }
5487
+ return true;
5488
+ }
5489
+ slerpDirection(start, end, t) {
5490
+ const dot = Math.max(-1, Math.min(1, gl_matrix_1.vec3.dot(start, end)));
5491
+ /*
5492
+ * For almost identical directions, normalized linear interpolation is
5493
+ * numerically more stable than the general SLERP formula.
5494
+ */
5495
+ if (dot > 0.999999) {
5496
+ const result = gl_matrix_1.vec3.lerp(gl_matrix_1.vec3.create(), start, end, t);
5497
+ return gl_matrix_1.vec3.normalize(result, result);
5498
+ }
5499
+ /*
5500
+ * The normal SLERP formula becomes singular for antipodal vectors because
5501
+ * sin(pi) = 0. In that case choose a stable vector orthogonal to start and
5502
+ * rotate through that great circle.
5503
+ */
5504
+ if (dot < -0.999999) {
5505
+ const referenceAxis = Math.abs(start[0]) < 0.9
5506
+ ? gl_matrix_1.vec3.fromValues(1, 0, 0)
5507
+ : gl_matrix_1.vec3.fromValues(0, 1, 0);
5508
+ const orthogonal = gl_matrix_1.vec3.cross(gl_matrix_1.vec3.create(), start, referenceAxis);
5509
+ gl_matrix_1.vec3.normalize(orthogonal, orthogonal);
5510
+ const angle = Math.PI * t;
5511
+ const result = gl_matrix_1.vec3.create();
5512
+ gl_matrix_1.vec3.scale(result, start, Math.cos(angle));
5513
+ gl_matrix_1.vec3.scaleAndAdd(result, result, orthogonal, Math.sin(angle));
5514
+ return gl_matrix_1.vec3.normalize(result, result);
5515
+ }
5516
+ const angle = Math.acos(dot);
5517
+ const sinAngle = Math.sin(angle);
5518
+ const startWeight = Math.sin((1 - t) * angle) / sinAngle;
5519
+ const endWeight = Math.sin(t * angle) / sinAngle;
5520
+ const result = gl_matrix_1.vec3.create();
5521
+ result[0] = start[0] * startWeight + end[0] * endWeight;
5522
+ result[1] = start[1] * startWeight + end[1] * endWeight;
5523
+ result[2] = start[2] * startWeight + end[2] * endWeight;
5524
+ return gl_matrix_1.vec3.normalize(result, result);
5525
+ }
5526
+ cancelFlyTo() {
5527
+ this.flyToAnimation = null;
5528
+ }
5422
5529
  toggleInsideSphere() {
5423
5530
  // if (inside !== global.insideSphere) {
5424
5531
  // global.insideSphere = inside;
@@ -5676,9 +5783,15 @@ class Camera {
5676
5783
  clearRotationLock() {
5677
5784
  this.lockRotX = this.lockRotY = this.lockRotZ = false;
5678
5785
  }
5679
- isRotationLockedX() { return this.lockRotX; }
5680
- isRotationLockedY() { return this.lockRotY; }
5681
- isRotationLockedZ() { return this.lockRotZ; }
5786
+ isRotationLockedX() {
5787
+ return this.lockRotX;
5788
+ }
5789
+ isRotationLockedY() {
5790
+ return this.lockRotY;
5791
+ }
5792
+ isRotationLockedZ() {
5793
+ return this.lockRotZ;
5794
+ }
5682
5795
  }
5683
5796
  exports["default"] = Camera;
5684
5797
 
@@ -7880,6 +7993,7 @@ class Source {
7880
7993
  _h_pix;
7881
7994
  _shapesize;
7882
7995
  _brightnessFactor;
7996
+ _mediaStyle;
7883
7997
  /**
7884
7998
  * @param in_point Point.js (Cartesian/RA-Dec wrapper)
7885
7999
  * @param in_details Optional array of key/value metadata
@@ -7932,6 +8046,12 @@ class Source {
7932
8046
  set brightnessFactor(factor) {
7933
8047
  this._brightnessFactor = factor;
7934
8048
  }
8049
+ get mediaStyle() {
8050
+ return this._mediaStyle;
8051
+ }
8052
+ set mediaStyle(style) {
8053
+ this._mediaStyle = style;
8054
+ }
7935
8055
  }
7936
8056
  exports.Source = Source;
7937
8057
 
@@ -8157,6 +8277,9 @@ exports.SphereFoV = SphereFoV;
8157
8277
  *
8158
8278
  * See LICENSE.md, LICENSE-AGPL.md, and LICENSE-COMMERCIAL.md for details.
8159
8279
  */
8280
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8281
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8282
+ };
8160
8283
  Object.defineProperty(exports, "__esModule", ({ value: true }));
8161
8284
  exports.CatalogueGL = void 0;
8162
8285
  const Source_js_1 = __webpack_require__(146);
@@ -8165,6 +8288,8 @@ const CoordsType_js_1 = __webpack_require__(8145);
8165
8288
  const Utils_js_1 = __webpack_require__(7930);
8166
8289
  const CatalogueShaderProgram_js_1 = __webpack_require__(3559);
8167
8290
  const MetadataManager_js_1 = __webpack_require__(5403);
8291
+ const gl_matrix_1 = __webpack_require__(3890);
8292
+ const Global_js_1 = __importDefault(__webpack_require__(4382));
8168
8293
  class CatalogueGL {
8169
8294
  _kind = "CatalogueGL";
8170
8295
  static ELEM_SIZE = 6;
@@ -8196,6 +8321,9 @@ class CatalogueGL {
8196
8321
  _providerUrl;
8197
8322
  _catalogueShaderProgram;
8198
8323
  _visibleTilesManager;
8324
+ _mediaOverlayHost = null;
8325
+ _mediaElements = [];
8326
+ _svgMediaDataUrls = new Map();
8199
8327
  constructor(catalogueName, catalogueDescription, providerUrl, metadataManager, webgl, visibleTilesManager) {
8200
8328
  this._webgl = webgl;
8201
8329
  this._ready = false;
@@ -8366,9 +8494,10 @@ class CatalogueGL {
8366
8494
  * @param in_data Rows of TAP results
8367
8495
  * @param columnsmeta TapMetadataList (unused here because `CatalogueProps` already holds indices)
8368
8496
  */
8369
- addSources(in_data, columnsmeta) {
8497
+ addSources(in_data, columnsmeta, options = {}) {
8370
8498
  this._ready = false;
8371
8499
  this._sources = [];
8500
+ this.clearMediaOverlay();
8372
8501
  this._metadataManager = new MetadataManager_js_1.MetadataManager(columnsmeta);
8373
8502
  // const raDataIndex = (this.catalogueProps.raColumn as any).index ?? (this.catalogueProps.raColumn as any)._index;
8374
8503
  // const decDataIndex = (this.catalogueProps.decColumn as any).index ?? (this.catalogueProps.decColumn as any)._index;
@@ -8382,6 +8511,10 @@ class CatalogueGL {
8382
8511
  decDeg: in_data[j][decDataIndex],
8383
8512
  }, CoordsType_js_1.CoordsType.ASTRO);
8384
8513
  const source = new Source_js_1.Source(point, in_data[j]);
8514
+ const mediaStyle = this.extractMediaStyle(in_data[j], columnsmeta, options.mediaColumns);
8515
+ if (mediaStyle) {
8516
+ source.mediaStyle = mediaStyle;
8517
+ }
8385
8518
  // Ensure optional fields exist
8386
8519
  source.shapeSize = source.shapeSize ?? CatalogueGL.STANDARD_SHAPE_SIZE;
8387
8520
  source.brightnessFactor = 3;
@@ -8401,11 +8534,222 @@ class CatalogueGL {
8401
8534
  this._ready = true;
8402
8535
  this._bufferInitialised = false;
8403
8536
  }
8537
+ getColumnIndex(columnsmeta, columnName) {
8538
+ if (!columnName)
8539
+ return -1;
8540
+ return columnsmeta.find((column) => column.name === columnName)?.index ?? -1;
8541
+ }
8542
+ readCell(row, columnsmeta, columnName) {
8543
+ const idx = this.getColumnIndex(columnsmeta, columnName);
8544
+ return idx >= 0 ? row[idx] : undefined;
8545
+ }
8546
+ extractMediaStyle(row, columnsmeta, mediaColumns) {
8547
+ if (!mediaColumns?.src)
8548
+ return undefined;
8549
+ const rawSrc = this.readCell(row, columnsmeta, mediaColumns.src);
8550
+ const src = String(rawSrc ?? "").trim();
8551
+ if (!src)
8552
+ return undefined;
8553
+ const rawKind = String(this.readCell(row, columnsmeta, mediaColumns.type) ?? "sprite").trim().toLowerCase();
8554
+ const kind = rawKind === "model"
8555
+ ? "model"
8556
+ : rawKind === "image"
8557
+ ? "image"
8558
+ : rawKind === "icon"
8559
+ ? "icon"
8560
+ : rawKind === "circle"
8561
+ ? "circle"
8562
+ : "sprite";
8563
+ if (kind === "circle" || kind === "model")
8564
+ return undefined;
8565
+ const scale = Number(this.readCell(row, columnsmeta, mediaColumns.scale));
8566
+ const rotationDeg = Number(this.readCell(row, columnsmeta, mediaColumns.rotation));
8567
+ const opacity = Number(this.readCell(row, columnsmeta, mediaColumns.opacity));
8568
+ return {
8569
+ kind,
8570
+ src,
8571
+ scale: Number.isFinite(scale) && scale > 0 ? scale : undefined,
8572
+ rotationDeg: Number.isFinite(rotationDeg) ? rotationDeg : undefined,
8573
+ opacity: Number.isFinite(opacity) ? Math.max(0, Math.min(1, opacity)) : undefined,
8574
+ };
8575
+ }
8404
8576
  clearSources() {
8405
8577
  this._sources = [];
8406
8578
  this.hoveredIndexes = [];
8407
8579
  this._healpixDensityMap.clear();
8408
8580
  this.vertexCataloguePosition = new Float32Array(0);
8581
+ this.clearMediaOverlay();
8582
+ }
8583
+ clearMediaOverlay() {
8584
+ this._mediaElements.forEach((el) => el.remove());
8585
+ this._mediaElements = [];
8586
+ if (this._mediaOverlayHost) {
8587
+ this._mediaOverlayHost.remove();
8588
+ this._mediaOverlayHost = null;
8589
+ }
8590
+ }
8591
+ ensureMediaOverlayHost() {
8592
+ const canvas = this._webgl?.canvas;
8593
+ const parent = canvas?.parentElement;
8594
+ if (!canvas || !parent)
8595
+ return null;
8596
+ const parentStyle = window.getComputedStyle(parent);
8597
+ if (parentStyle.position === "static") {
8598
+ parent.style.position = "relative";
8599
+ }
8600
+ if (!this._mediaOverlayHost) {
8601
+ const host = document.createElement("div");
8602
+ host.dataset.astroCatalogueMedia = "true";
8603
+ host.style.position = "absolute";
8604
+ host.style.left = `${canvas.offsetLeft}px`;
8605
+ host.style.top = `${canvas.offsetTop}px`;
8606
+ host.style.width = `${canvas.clientWidth}px`;
8607
+ host.style.height = `${canvas.clientHeight}px`;
8608
+ host.style.pointerEvents = "none";
8609
+ host.style.overflow = "hidden";
8610
+ host.style.zIndex = "4";
8611
+ canvas.style.position = canvas.style.position || "relative";
8612
+ canvas.style.zIndex = canvas.style.zIndex || "0";
8613
+ parent.appendChild(host);
8614
+ this._mediaOverlayHost = host;
8615
+ }
8616
+ else {
8617
+ this._mediaOverlayHost.style.left = `${canvas.offsetLeft}px`;
8618
+ this._mediaOverlayHost.style.top = `${canvas.offsetTop}px`;
8619
+ this._mediaOverlayHost.style.width = `${canvas.clientWidth}px`;
8620
+ this._mediaOverlayHost.style.height = `${canvas.clientHeight}px`;
8621
+ }
8622
+ return this._mediaOverlayHost;
8623
+ }
8624
+ ensureMediaElement(index, style) {
8625
+ let img = this._mediaElements[index];
8626
+ if (!img) {
8627
+ img = document.createElement("img");
8628
+ img.loading = "lazy";
8629
+ img.decoding = "async";
8630
+ img.style.position = "absolute";
8631
+ img.style.objectFit = "contain";
8632
+ img.style.pointerEvents = "none";
8633
+ img.style.transformOrigin = "center center";
8634
+ img.style.filter = "drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55))";
8635
+ img.style.willChange = "transform, left, top";
8636
+ this._mediaElements[index] = img;
8637
+ }
8638
+ const src = String(style.src ?? "").trim();
8639
+ const resolvedSrc = src ? new URL(src, document.baseURI).href : "";
8640
+ if (resolvedSrc && img.getAttribute("src") !== resolvedSrc) {
8641
+ img.removeAttribute("data-astro-media-src-index");
8642
+ img.removeAttribute("data-astro-media-loaded");
8643
+ img.onerror = null;
8644
+ img.onload = null;
8645
+ img.setAttribute("src", resolvedSrc);
8646
+ if (/\.svg(?:[?#].*)?$/i.test(resolvedSrc)) {
8647
+ void this.loadSvgMediaAsDataUrl(img, resolvedSrc);
8648
+ }
8649
+ }
8650
+ return img;
8651
+ }
8652
+ async loadSvgMediaAsDataUrl(img, resolvedSrc) {
8653
+ try {
8654
+ let dataUrl = this._svgMediaDataUrls.get(resolvedSrc);
8655
+ if (!dataUrl) {
8656
+ const response = await fetch(resolvedSrc);
8657
+ if (!response.ok)
8658
+ return;
8659
+ const svg = await response.text();
8660
+ if (!/^\s*<svg[\s>]/i.test(svg))
8661
+ return;
8662
+ dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
8663
+ this._svgMediaDataUrls.set(resolvedSrc, dataUrl);
8664
+ }
8665
+ if (img.getAttribute("src") === resolvedSrc) {
8666
+ img.setAttribute("src", dataUrl);
8667
+ }
8668
+ }
8669
+ catch {
8670
+ // Keep the direct image src; remote servers may already provide a valid MIME type.
8671
+ }
8672
+ }
8673
+ isSourceOnVisibleHemisphere(source, in_mMatrix, vMatrix) {
8674
+ if (Global_js_1.default.insideSphere)
8675
+ return true;
8676
+ const invView = gl_matrix_1.mat4.create();
8677
+ if (!gl_matrix_1.mat4.invert(invView, vMatrix))
8678
+ return true;
8679
+ const cameraPos = gl_matrix_1.vec3.fromValues(invView[12], invView[13], invView[14]);
8680
+ const worldPoint4 = gl_matrix_1.vec4.fromValues(source.point.x, source.point.y, source.point.z, 1);
8681
+ gl_matrix_1.vec4.transformMat4(worldPoint4, worldPoint4, in_mMatrix);
8682
+ const worldPoint = gl_matrix_1.vec3.fromValues(worldPoint4[0], worldPoint4[1], worldPoint4[2]);
8683
+ const normal = gl_matrix_1.vec3.normalize(gl_matrix_1.vec3.create(), worldPoint);
8684
+ const toCamera = gl_matrix_1.vec3.subtract(gl_matrix_1.vec3.create(), cameraPos, worldPoint);
8685
+ return gl_matrix_1.vec3.dot(normal, toCamera) > 0;
8686
+ }
8687
+ updateMediaOverlay(in_mMatrix, vMatrix, pMatrix) {
8688
+ const sourcesWithMedia = this._sources.filter((source) => !!source.mediaStyle?.src);
8689
+ if (!sourcesWithMedia.length) {
8690
+ this.clearMediaOverlay();
8691
+ return;
8692
+ }
8693
+ const host = this.ensureMediaOverlayHost();
8694
+ const canvas = this._webgl.canvas;
8695
+ if (!host || !canvas.clientWidth || !canvas.clientHeight)
8696
+ return;
8697
+ const modelView = gl_matrix_1.mat4.create();
8698
+ const mvp = gl_matrix_1.mat4.create();
8699
+ gl_matrix_1.mat4.multiply(modelView, vMatrix, in_mMatrix);
8700
+ gl_matrix_1.mat4.multiply(mvp, pMatrix, modelView);
8701
+ const width = canvas.clientWidth;
8702
+ const height = canvas.clientHeight;
8703
+ for (let i = 0; i < this._sources.length; i++) {
8704
+ const source = this._sources[i];
8705
+ const style = source.mediaStyle;
8706
+ const img = this._mediaElements[i];
8707
+ if (!style?.src) {
8708
+ if (img)
8709
+ img.style.display = "none";
8710
+ continue;
8711
+ }
8712
+ if (!this.isSourceOnVisibleHemisphere(source, in_mMatrix, vMatrix)) {
8713
+ if (img)
8714
+ img.style.display = "none";
8715
+ continue;
8716
+ }
8717
+ const projected = gl_matrix_1.vec4.fromValues(source.point.x, source.point.y, source.point.z, 1);
8718
+ gl_matrix_1.vec4.transformMat4(projected, projected, mvp);
8719
+ if (!projected[3]) {
8720
+ if (img)
8721
+ img.style.display = "none";
8722
+ continue;
8723
+ }
8724
+ const ndcX = projected[0] / projected[3];
8725
+ const ndcY = projected[1] / projected[3];
8726
+ const ndcZ = projected[2] / projected[3];
8727
+ if (ndcX < -1.15 ||
8728
+ ndcX > 1.15 ||
8729
+ ndcY < -1.15 ||
8730
+ ndcY > 1.15 ||
8731
+ ndcZ < -1.15 ||
8732
+ ndcZ > 1.15) {
8733
+ if (img)
8734
+ img.style.display = "none";
8735
+ continue;
8736
+ }
8737
+ const mediaEl = this.ensureMediaElement(i, style);
8738
+ if (!mediaEl.parentElement)
8739
+ host.appendChild(mediaEl);
8740
+ const px = (ndcX * 0.5 + 0.5) * width;
8741
+ const py = (1 - (ndcY * 0.5 + 0.5)) * height;
8742
+ const baseSize = source.shapeSize || CatalogueGL.STANDARD_SHAPE_SIZE;
8743
+ const size = Math.max(8, baseSize * (style.scale ?? 2.4));
8744
+ const rotation = style.rotationDeg ?? 0;
8745
+ mediaEl.style.display = "block";
8746
+ mediaEl.style.left = `${px - size / 2}px`;
8747
+ mediaEl.style.top = `${py - size / 2}px`;
8748
+ mediaEl.style.width = `${size}px`;
8749
+ mediaEl.style.height = `${size}px`;
8750
+ mediaEl.style.opacity = `${style.opacity ?? 1}`;
8751
+ mediaEl.style.transform = `rotate(${rotation}deg)`;
8752
+ }
8409
8753
  }
8410
8754
  sourceMatches(left, right) {
8411
8755
  if (left === right)
@@ -8745,6 +9089,7 @@ class CatalogueGL {
8745
9089
  // draw
8746
9090
  const numItems = this.vertexCataloguePosition.length / CatalogueGL.ELEM_SIZE;
8747
9091
  this._webgl.drawArrays(this._webgl.POINTS, 0, numItems);
9092
+ this.updateMediaOverlay(in_mMatrix, vMatrix, pMatrix);
8748
9093
  this._oldMouseCoords = in_mouseHelper.xyz;
8749
9094
  }
8750
9095
  }