astro-viewer 3.2.0 → 3.3.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.
@@ -13,6 +13,9 @@ import type { WMTSLayerConfig, XYZDebugStats, XYZLayerConfig } from "./model/ear
13
13
  import type { HiPSDebugStats } from "./model/hips/HiPSConfig.js";
14
14
  import { XYZMapDescriptor } from "./model/earth/XYZMapDescriptor.js";
15
15
  import { XYZMap } from "./model/earth/XYZMap.js";
16
+ import { MeshHiPS } from "./model/meships/MeshHiPS.js";
17
+ import { MeshHiPSDescriptor } from "./model/meships/MeshHiPSDescriptor.js";
18
+ import type { MeshHiPSDebugStats } from "./model/meships/MeshHiPSTypes.js";
16
19
  export type PointCoordinates = {
17
20
  astroDeg: AstroCoords;
18
21
  raHMS: HMS;
@@ -59,6 +62,7 @@ declare class AstroSphere {
59
62
  private pointerDownAt;
60
63
  private _activeHiPS;
61
64
  private _activeXYZ2;
65
+ private _activeMeshHiPS;
62
66
  private _activeBaseLayer;
63
67
  private startup;
64
68
  private fov;
@@ -103,6 +107,7 @@ declare class AstroSphere {
103
107
  activateHiPS(hipsDescriptor: HiPSDescriptor): void;
104
108
  activateXYZ(config: XYZLayerConfig): void;
105
109
  activateXYZ2(config: XYZMapDescriptor): void;
110
+ activateMeshHiPS(descriptor: MeshHiPSDescriptor): void;
106
111
  activateWMTS(config: WMTSLayerConfig): void;
107
112
  showCatalogue(cat: CatalogueGL): Promise<CatalogueGL>;
108
113
  deleteCatalogue(catalogue: CatalogueGL): void;
@@ -132,6 +137,7 @@ declare class AstroSphere {
132
137
  private prevCentralDecDeg;
133
138
  get activeHiPS(): HiPS | null;
134
139
  get activeXYZ(): XYZMap | null;
140
+ get activeMeshHiPS(): MeshHiPS | null;
135
141
  isLonLatGridVisible(): boolean;
136
142
  toggleLonLatGrid(): boolean;
137
143
  setEastWestRotationLocked(locked: boolean): void;
@@ -140,6 +146,7 @@ declare class AstroSphere {
140
146
  isNorthSouthRotationLocked(): boolean;
141
147
  getXYZDebugStats(): XYZDebugStats;
142
148
  getHiPSDebugStats(): HiPSDebugStats | null;
149
+ getMeshHiPSDebugStats(): MeshHiPSDebugStats | null;
143
150
  draw(canvas: HTMLCanvasElement): void;
144
151
  private emitHoveredSourceIfChanged;
145
152
  }
@@ -28,6 +28,7 @@ import { xyzTileRequestScheduler } from "./model/earth/XYZTileRequestScheduler.j
28
28
  import { WMTSAdapter } from "./model/earth/WMTSAdapter.js";
29
29
  import { XYZMapDescriptor } from "./model/earth/XYZMapDescriptor.js";
30
30
  import { XYZMap } from "./model/earth/XYZMap.js";
31
+ import { MeshHiPS } from "./model/meships/MeshHiPS.js";
31
32
  import { mat4, vec3, vec4 } from "gl-matrix";
32
33
  /**
33
34
  * AstroSphere — main WebGL scene controller (TS port)
@@ -54,6 +55,7 @@ class AstroSphere {
54
55
  pointerDownAt = 0;
55
56
  _activeHiPS = null;
56
57
  _activeXYZ2 = null;
58
+ _activeMeshHiPS = null;
57
59
  _activeBaseLayer = null;
58
60
  startup = true;
59
61
  fov;
@@ -287,7 +289,7 @@ class AstroSphere {
287
289
  }
288
290
  emitCameraChanged(reason) {
289
291
  // avoid dispatch before scene is ready
290
- if (!this._activeHiPS)
292
+ if (!this._activeHiPS && !this._activeXYZ2 && !this._activeMeshHiPS)
291
293
  return;
292
294
  if (!this._healpixGrid?.fovObj)
293
295
  return;
@@ -566,6 +568,10 @@ class AstroSphere {
566
568
  this._activeXYZ2 = new XYZMap(1, [0.0, 0.0, 0.0], 0, 0, config, this._webgl);
567
569
  this._activeBaseLayer = "xyz";
568
570
  }
571
+ activateMeshHiPS(descriptor) {
572
+ this._activeMeshHiPS = new MeshHiPS(descriptor.meshRadius, [0.0, 0.0, 0.0], 0, 0, descriptor, this._webgl, this._healpixGrid);
573
+ this._activeBaseLayer = "meships";
574
+ }
569
575
  activateWMTS(config) {
570
576
  const adapter = new WMTSAdapter(config);
571
577
  const xyzConfig = adapter.toXYZLayerConfig();
@@ -718,6 +724,13 @@ class AstroSphere {
718
724
  if (centralradeg == null || centraldecdeg == null) {
719
725
  return null;
720
726
  }
727
+ let fovPolygon = [];
728
+ try {
729
+ fovPolygon = this.getFoVPolygon();
730
+ }
731
+ catch (error) {
732
+ console.warn("[AstroSphere] getCurrentStatus: FoV polygon is not available.", error);
733
+ }
721
734
  // if (this._rotating && centraldecdeg && centralradeg) {
722
735
  const detail = {
723
736
  fovDeg: this.fov.minFoV,
@@ -732,14 +745,14 @@ class AstroSphere {
732
745
  centralPoint: new Point({ raDeg: centralradeg, decDeg: centraldecdeg }, CoordsType.ASTRO),
733
746
  mouseHoverPoint: this.mousePointCoords,
734
747
  colorMap: this._selectedColorMap,
735
- getFoVPolygon: [],
748
+ getFoVPolygon: fovPolygon,
736
749
  };
737
750
  return detail;
738
751
  // }
739
752
  // return null
740
753
  }
741
754
  changeColorMap(cm) {
742
- if (!this._activeHiPS && !this._activeXYZ2)
755
+ if (!this._activeHiPS && !this._activeXYZ2 && !this._activeMeshHiPS)
743
756
  return;
744
757
  this._selectedColorMap = cm;
745
758
  this._activeHiPS?.changeColorMap(cm);
@@ -754,6 +767,9 @@ class AstroSphere {
754
767
  get activeXYZ() {
755
768
  return this._activeXYZ2;
756
769
  }
770
+ get activeMeshHiPS() {
771
+ return this._activeMeshHiPS;
772
+ }
757
773
  isLonLatGridVisible() {
758
774
  return this._activeXYZ2?.isLonLatGridVisible() ?? false;
759
775
  }
@@ -794,12 +810,17 @@ class AstroSphere {
794
810
  return null;
795
811
  return this._activeHiPS.getDebugStats();
796
812
  }
813
+ getMeshHiPSDebugStats() {
814
+ if (!this._activeMeshHiPS)
815
+ return null;
816
+ return this._activeMeshHiPS.getDebugStats();
817
+ }
797
818
  draw(canvas) {
798
819
  if (this._refreshingStatus)
799
820
  return;
800
821
  if (!this._webgl)
801
822
  return;
802
- if (!this._activeHiPS && !this._activeXYZ2)
823
+ if (!this._activeHiPS && !this._activeXYZ2 && !this._activeMeshHiPS)
803
824
  return;
804
825
  if (!this._healpixGrid || Object.keys(this._healpixGrid).length === 0)
805
826
  return;
@@ -906,6 +927,10 @@ class AstroSphere {
906
927
  const visibleOrder = Math.min(this._healpixGrid.visibleorder, this._activeHiPS.maxOrder);
907
928
  this._healpixGrid.visibleTilesManager.computeVisiblePixels(visibleOrder, this._webgl, this._camera, this._perspectiveMatrixManager.pMatrix);
908
929
  }
930
+ if (this._activeBaseLayer === "meships" && this._activeMeshHiPS) {
931
+ const visibleOrder = this._activeMeshHiPS.refreshOrder(this.fov?.minFoV ?? this._healpixGrid.getMinFoV());
932
+ this._healpixGrid.visibleTilesManager.computeVisiblePixels(visibleOrder, this._webgl, this._camera, this._perspectiveMatrixManager.pMatrix);
933
+ }
909
934
  // DRAW HiPS
910
935
  const stableFovDeg = this.fov?.minFoV ?? this._healpixGrid.getMinFoV();
911
936
  const nowForGrid = performance.now();
@@ -937,6 +962,9 @@ class AstroSphere {
937
962
  if (this._activeBaseLayer === "xyz") {
938
963
  this._activeXYZ2?.draw(skyEntityDrawInput);
939
964
  }
965
+ if (this._activeBaseLayer === "meships") {
966
+ this._activeMeshHiPS?.draw(skyEntityDrawInput);
967
+ }
940
968
  this._healpixGrid.draw(skyEntityDrawInput);
941
969
  this._equatorialGrid.draw(skyEntityDrawInput);
942
970
  this._webgl.enable(this._webgl.DEPTH_TEST);
@@ -959,7 +987,8 @@ class AstroSphere {
959
987
  }
960
988
  this.activeCatalogues.forEach((cat) => {
961
989
  const activeModelMatrix = this._activeHiPS?.getModelMatrix() ??
962
- this._activeXYZ2?.getModelMatrix();
990
+ this._activeXYZ2?.getModelMatrix() ??
991
+ this._activeMeshHiPS?.getModelMatrix();
963
992
  if (activeModelMatrix) {
964
993
  cat.draw(activeModelMatrix, this.mouseHelper, this._camera.getCameraMatrix(), this._perspectiveMatrixManager.pMatrix);
965
994
  }
@@ -967,7 +996,8 @@ class AstroSphere {
967
996
  this.emitHoveredSourceIfChanged();
968
997
  this.activeFootprintSets.forEach((fst) => {
969
998
  const activeModelMatrix = this._activeHiPS?.getModelMatrix() ??
970
- this._activeXYZ2?.getModelMatrix();
999
+ this._activeXYZ2?.getModelMatrix() ??
1000
+ this._activeMeshHiPS?.getModelMatrix();
971
1001
  if (activeModelMatrix) {
972
1002
  fst.draw(activeModelMatrix, this.mouseHelper, this._camera.getCameraMatrix(), this._perspectiveMatrixManager.pMatrix);
973
1003
  }
@@ -14,6 +14,7 @@ import type { HiPSDebugStats } from './model/hips/HiPSConfig.js';
14
14
  import { XYZMap } from './model/earth/XYZMap.js';
15
15
  import { TerraPointSetGL } from './model/terra/TerraPointSetGL.js';
16
16
  import { TerraFootprintSetGL } from './model/terra/TerraFootprintSetGL.js';
17
+ import type { MeshHiPSConfig, MeshHiPSDebugStats } from './model/meships/MeshHiPSTypes.js';
17
18
  export declare class AstroViewer {
18
19
  private astroSphere;
19
20
  private canvas;
@@ -54,11 +55,14 @@ export declare class AstroViewer {
54
55
  name?: string;
55
56
  }): void;
56
57
  activateWMTS(config: WMTSLayerConfig): void;
58
+ activateMeshHiPS(config: MeshHiPSConfig): void;
57
59
  setXYZMaxConcurrentRequests(value: number): void;
58
60
  getXYZMaxConcurrentRequests(): number;
59
61
  getXYZDebugStats(): XYZDebugStats;
60
62
  getHiPSDebugStats(): HiPSDebugStats | null;
63
+ getMeshHiPSDebugStats(): MeshHiPSDebugStats | null;
61
64
  loadHiPS(baseUrl: string): Promise<string>;
65
+ loadMeshHiPS(baseUrl: string, config?: Omit<MeshHiPSConfig, 'baseUrl'>): Promise<string>;
62
66
  changeColorMap(colorMapName: ColorMapName): void;
63
67
  changeCustomColorMap(colorMap: ColorMap): void;
64
68
  getActiveHiPS(): HiPS | null;
@@ -21,6 +21,7 @@ import { xyzTileRequestScheduler } from './model/earth/XYZTileRequestScheduler.j
21
21
  import { XYZMapDescriptor } from './model/earth/XYZMapDescriptor.js';
22
22
  import { TerraPointSetGL } from './model/terra/TerraPointSetGL.js';
23
23
  import { TerraFootprintSetGL } from './model/terra/TerraFootprintSetGL.js';
24
+ import { MeshHiPSDescriptor } from './model/meships/MeshHiPSDescriptor.js';
24
25
  // & {
25
26
  // viewportWidth: number
26
27
  // viewportHeight: number
@@ -136,6 +137,9 @@ export class AstroViewer {
136
137
  activateWMTS(config) {
137
138
  this.astroSphere.activateWMTS(config);
138
139
  }
140
+ activateMeshHiPS(config) {
141
+ this.astroSphere.activateMeshHiPS(new MeshHiPSDescriptor(config));
142
+ }
139
143
  setXYZMaxConcurrentRequests(value) {
140
144
  xyzTileRequestScheduler.setMaxConcurrent(value);
141
145
  }
@@ -148,6 +152,9 @@ export class AstroViewer {
148
152
  getHiPSDebugStats() {
149
153
  return this.astroSphere.getHiPSDebugStats();
150
154
  }
155
+ getMeshHiPSDebugStats() {
156
+ return this.astroSphere.getMeshHiPSDebugStats();
157
+ }
151
158
  async loadHiPS(baseUrl) {
152
159
  const hipsUrl = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/';
153
160
  const resp = await fetch(hipsUrl + 'properties');
@@ -159,6 +166,26 @@ export class AstroViewer {
159
166
  return desc.surveyName;
160
167
  // this.activateHiPS(desc);
161
168
  }
169
+ async loadMeshHiPS(baseUrl, config = {}) {
170
+ const meshHiPSUrl = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/';
171
+ let propsText = '';
172
+ const resp = await fetch(meshHiPSUrl + 'properties');
173
+ if (resp.ok) {
174
+ propsText = await resp.text();
175
+ }
176
+ else {
177
+ const jsonResp = await fetch(meshHiPSUrl + 'properties.json');
178
+ if (!jsonResp.ok)
179
+ throw new Error(`HTTP ${jsonResp.status} fetching MeshHiPS properties`);
180
+ const json = await jsonResp.json();
181
+ propsText = Object.entries(json)
182
+ .map(([key, value]) => `${key}=${String(value)}`)
183
+ .join('\n');
184
+ }
185
+ const desc = new MeshHiPSDescriptor({ ...config, baseUrl: meshHiPSUrl }, propsText);
186
+ this.astroSphere.activateMeshHiPS(desc);
187
+ return desc.name;
188
+ }
162
189
  // changeColorMap(hips: HiPS, colorMapName: ColorMapName) {
163
190
  changeColorMap(colorMapName) {
164
191
  const colorMap = ColorMaps[colorMapName];
@@ -19,7 +19,10 @@ export type { ColorMap, ColorMapName } from './model/ColorMaps.js';
19
19
  export { HiPS } from './model/hips/HiPS.js';
20
20
  export { XYZMap } from './model/earth/XYZMap.js';
21
21
  export { WMTSAdapter } from './model/earth/WMTSAdapter.js';
22
+ export { MeshHiPS } from './model/meships/MeshHiPS.js';
23
+ export { MeshHiPSDescriptor } from './model/meships/MeshHiPSDescriptor.js';
22
24
  export type { XYZLayerConfig, WMTSLayerConfig, WMTSRequestEncoding } from './model/earth/XYZConfig.js';
23
25
  export type { XYZTileCoord, XYZTileMesh } from './model/earth/XYZTypes.js';
26
+ export type { MeshHiPSConfig, MeshHiPSDebugStats, MeshHiPSTileCoord } from './model/meships/MeshHiPSTypes.js';
24
27
  export { Source } from './model/Source.js';
25
28
  export { Footprint } from './model/footprints/Footprint.js';
package/lib-esm/index.js CHANGED
@@ -28,6 +28,8 @@ export { ColorMaps, COLOR_MAP_SAMPLE_COUNT, createColorMapFromSamples } from './
28
28
  export { HiPS } from './model/hips/HiPS.js';
29
29
  export { XYZMap } from './model/earth/XYZMap.js';
30
30
  export { WMTSAdapter } from './model/earth/WMTSAdapter.js';
31
+ export { MeshHiPS } from './model/meships/MeshHiPS.js';
32
+ export { MeshHiPSDescriptor } from './model/meships/MeshHiPSDescriptor.js';
31
33
  export { Source } from './model/Source.js';
32
34
  export { Footprint } from './model/footprints/Footprint.js';
33
35
  console.log('astroviewer UMD loaded');
@@ -48,7 +48,7 @@ export type XYZRequestSchedulerDebugStats = {
48
48
  hostsInBackoff: XYZRequestBackoffDebugEntry[];
49
49
  };
50
50
  export type XYZDebugStats = {
51
- activeBaseLayer: 'hips' | 'xyz' | null;
51
+ activeBaseLayer: 'hips' | 'xyz' | 'meships' | null;
52
52
  layer: XYZLayerDebugStats | null;
53
53
  requests: XYZRequestSchedulerDebugStats;
54
54
  };
@@ -1,5 +1,5 @@
1
1
  export type HiPSDebugStats = {
2
- activeBaseLayer: 'hips' | 'xyz' | null;
2
+ activeBaseLayer: 'hips' | 'xyz' | 'meships' | null;
3
3
  hipsName: string | null;
4
4
  hipsUrl: string | null;
5
5
  isGalactic: boolean | null;
@@ -0,0 +1,30 @@
1
+ import { AbstractSkyEntity, SkyEntityDrawInput } from '../AbstractSkyEntity.js';
2
+ import { HealpixGrid } from '../grid/HealpixGrid.js';
3
+ import { MeshHiPSDescriptor } from './MeshHiPSDescriptor.js';
4
+ import type { MeshHiPSDebugStats } from './MeshHiPSTypes.js';
5
+ export declare class MeshHiPS extends AbstractSkyEntity {
6
+ private _descriptor;
7
+ private _healpixGrid;
8
+ private _shaderProgram;
9
+ private _tiles;
10
+ private _currentOrder;
11
+ private _visibleTiles;
12
+ private _coverageTiles;
13
+ constructor(radius: number, position: [number, number, number], xrad: number, yrad: number, _descriptor: MeshHiPSDescriptor, webgl: WebGL2RenderingContext, _healpixGrid: HealpixGrid);
14
+ get currentOrder(): number;
15
+ get maxOrder(): number;
16
+ get minOrder(): number;
17
+ get baseURL(): string;
18
+ get propertiesRawText(): string;
19
+ get properties(): ReadonlyMap<string, string>;
20
+ getProperty(key: string): string | undefined;
21
+ refreshOrder(fovDeg: number | undefined): number;
22
+ draw(input: SkyEntityDrawInput): void;
23
+ getDebugStats(): MeshHiPSDebugStats;
24
+ private resolveVisibleTiles;
25
+ private resolveCoverageTiles;
26
+ private groupTilesByOrder;
27
+ private ensureTiles;
28
+ private evictCached;
29
+ private tileKey;
30
+ }
@@ -0,0 +1,182 @@
1
+ /*
2
+ * AstroViewer
3
+ * Copyright (C) Fabrizio Giordano
4
+ * SPDX-License-Identifier: LicenseRef-AstroViewer-Dual-License
5
+ */
6
+ import { AbstractSkyEntity } from '../AbstractSkyEntity.js';
7
+ import { fovHelper } from '../hips/FoVHelper.js';
8
+ import { MeshHiPSShaderProgram } from '../../shader/MeshHiPSShaderProgram.js';
9
+ import { MeshHiPSTile } from './MeshHiPSTile.js';
10
+ export class MeshHiPS extends AbstractSkyEntity {
11
+ _descriptor;
12
+ _healpixGrid;
13
+ _shaderProgram;
14
+ _tiles = new Map();
15
+ _currentOrder;
16
+ _visibleTiles = [];
17
+ _coverageTiles = [];
18
+ constructor(radius, position, xrad, yrad, _descriptor, webgl, _healpixGrid) {
19
+ super(radius, position, xrad, yrad, _descriptor.name, webgl, false);
20
+ this._descriptor = _descriptor;
21
+ this._healpixGrid = _healpixGrid;
22
+ this.initGL(webgl);
23
+ this._shaderProgram = new MeshHiPSShaderProgram(webgl);
24
+ this._shaderProgram.enableProgram();
25
+ this._currentOrder = _descriptor.selectedOrder;
26
+ }
27
+ get currentOrder() {
28
+ return this._currentOrder;
29
+ }
30
+ get maxOrder() {
31
+ return this._descriptor.maxOrder;
32
+ }
33
+ get minOrder() {
34
+ return this._descriptor.minOrder;
35
+ }
36
+ get baseURL() {
37
+ return this._descriptor.baseUrl;
38
+ }
39
+ get propertiesRawText() {
40
+ return this._descriptor.propertiesRawText;
41
+ }
42
+ get properties() {
43
+ return this._descriptor.properties;
44
+ }
45
+ getProperty(key) {
46
+ return this._descriptor.getProperty(key);
47
+ }
48
+ refreshOrder(fovDeg) {
49
+ if (this._descriptor.fixedOrder) {
50
+ this._currentOrder = this._descriptor.selectedOrder;
51
+ return this._currentOrder;
52
+ }
53
+ const fov = Number.isFinite(fovDeg) && fovDeg > 0
54
+ ? fovDeg
55
+ : this._healpixGrid.getMinFoV();
56
+ const safeFov = Number.isFinite(fov) && fov > 0 ? fov : 1e-6;
57
+ const order = fovHelper.getHiPSNorder(safeFov, this._currentOrder);
58
+ this._currentOrder = Math.max(this._descriptor.minOrder, Math.min(this._descriptor.maxOrder, order));
59
+ return this._currentOrder;
60
+ }
61
+ draw(input) {
62
+ const vMatrix = input.camera.getCameraMatrix();
63
+ if (!vMatrix || !input.pMatrix)
64
+ return;
65
+ this.refreshOrder(input.fovDeg);
66
+ this._visibleTiles = this.resolveVisibleTiles();
67
+ this._coverageTiles = this.resolveCoverageTiles(this._visibleTiles);
68
+ this.ensureTiles(this._coverageTiles);
69
+ this.evictCached();
70
+ const gl = this._webgl;
71
+ const pMatrix = input.pMatrix;
72
+ const mMatrix = this.getModelMatrix();
73
+ const wasCullFace = gl.isEnabled(gl.CULL_FACE);
74
+ const wasDepthTest = gl.isEnabled(gl.DEPTH_TEST);
75
+ const wasDepthMask = gl.getParameter(gl.DEPTH_WRITEMASK);
76
+ const previousDepthFunc = gl.getParameter(gl.DEPTH_FUNC);
77
+ gl.enable(gl.DEPTH_TEST);
78
+ gl.depthMask(true);
79
+ gl.depthFunc(gl.LEQUAL);
80
+ gl.disable(gl.CULL_FACE);
81
+ const byOrder = this.groupTilesByOrder(this._coverageTiles);
82
+ const orders = Array.from(byOrder.keys()).sort((a, b) => a - b);
83
+ for (const order of orders) {
84
+ for (const coord of byOrder.get(order) ?? []) {
85
+ this._tiles.get(this.tileKey(coord))?.draw(pMatrix, vMatrix, mMatrix, this._descriptor.color, this._descriptor.wireframe);
86
+ }
87
+ }
88
+ if (wasCullFace)
89
+ gl.enable(gl.CULL_FACE);
90
+ if (!wasDepthTest)
91
+ gl.disable(gl.DEPTH_TEST);
92
+ gl.depthFunc(previousDepthFunc);
93
+ gl.depthMask(wasDepthMask);
94
+ }
95
+ getDebugStats() {
96
+ const tiles = Array.from(this._tiles.values());
97
+ return {
98
+ activeBaseLayer: 'meships',
99
+ meshHiPSName: this._descriptor.name,
100
+ meshHiPSUrl: this._descriptor.baseUrl,
101
+ currentOrder: this._currentOrder,
102
+ visibleTileCount: this._visibleTiles.length,
103
+ coverageTileCount: this._coverageTiles.length,
104
+ cacheSize: this._tiles.size,
105
+ readyTileCount: tiles.filter((tile) => tile.ready).length,
106
+ loadingTileCount: tiles.filter((tile) => tile.loading).length,
107
+ failedTileCount: tiles.filter((tile) => tile.failed).length,
108
+ };
109
+ }
110
+ resolveVisibleTiles() {
111
+ const manager = this._healpixGrid.visibleTilesManager;
112
+ const byOrder = manager?.visibleTilesByOrder;
113
+ const pixels = byOrder?.order === this._currentOrder && Array.isArray(byOrder.pixels)
114
+ ? byOrder.pixels
115
+ : [];
116
+ if (pixels.length > 0) {
117
+ return pixels.map((ipix) => ({ order: this._currentOrder, ipix }));
118
+ }
119
+ const tileCount = 12 * 4 ** this._currentOrder;
120
+ return Array.from({ length: tileCount }, (_, ipix) => ({ order: this._currentOrder, ipix }));
121
+ }
122
+ resolveCoverageTiles(visibleTiles) {
123
+ const tiles = new Map();
124
+ const add = (coord) => {
125
+ if (coord.order < this._descriptor.minOrder || coord.order > this._descriptor.maxOrder)
126
+ return;
127
+ tiles.set(this.tileKey(coord), coord);
128
+ };
129
+ for (const coord of visibleTiles) {
130
+ add(coord);
131
+ for (let order = coord.order - 1; order >= this._descriptor.minOrder; order--) {
132
+ const shift = 2 * (coord.order - order);
133
+ add({ order, ipix: coord.ipix >> shift });
134
+ }
135
+ }
136
+ const manager = this._healpixGrid.visibleTilesManager;
137
+ const ancestorsMap = manager?.ancestorsMap;
138
+ if (ancestorsMap) {
139
+ for (const [order, ipixes] of ancestorsMap) {
140
+ if (order < this._descriptor.minOrder || order > this._descriptor.maxOrder)
141
+ continue;
142
+ for (const ipix of ipixes)
143
+ add({ order, ipix });
144
+ }
145
+ }
146
+ return Array.from(tiles.values());
147
+ }
148
+ groupTilesByOrder(coords) {
149
+ const byOrder = new Map();
150
+ for (const coord of coords) {
151
+ const list = byOrder.get(coord.order) ?? [];
152
+ list.push(coord);
153
+ byOrder.set(coord.order, list);
154
+ }
155
+ return byOrder;
156
+ }
157
+ ensureTiles(coords) {
158
+ for (const coord of coords) {
159
+ const key = this.tileKey(coord);
160
+ if (this._tiles.has(key))
161
+ continue;
162
+ this._tiles.set(key, new MeshHiPSTile(coord, this._descriptor.getTileUrl(coord.order, coord.ipix), this._webgl, this._shaderProgram));
163
+ }
164
+ }
165
+ evictCached() {
166
+ const maxCached = this._descriptor.maxCachedTiles;
167
+ if (this._tiles.size <= maxCached)
168
+ return;
169
+ const visibleKeys = new Set(this._coverageTiles.map((coord) => this.tileKey(coord)));
170
+ const evictable = Array.from(this._tiles.entries())
171
+ .filter(([key]) => !visibleKeys.has(key))
172
+ .sort((a, b) => a[1].lastUsedAt - b[1].lastUsedAt);
173
+ while (this._tiles.size > maxCached && evictable.length > 0) {
174
+ const [key, tile] = evictable.shift();
175
+ tile.dispose();
176
+ this._tiles.delete(key);
177
+ }
178
+ }
179
+ tileKey(coord) {
180
+ return `${coord.order}/${coord.ipix}`;
181
+ }
182
+ }
@@ -0,0 +1,34 @@
1
+ import type { MeshHiPSConfig } from './MeshHiPSTypes.js';
2
+ export declare class MeshHiPSDescriptor {
3
+ private _name;
4
+ private _baseUrl;
5
+ private _minOrder;
6
+ private _maxOrder;
7
+ private _selectedOrder;
8
+ private _fixedOrder;
9
+ private _maxCachedTiles;
10
+ private _color;
11
+ private _wireframe;
12
+ private _propertiesRawText;
13
+ private _propertiesMap;
14
+ private _meshRadius;
15
+ constructor(config: MeshHiPSConfig, propertiesText?: string);
16
+ get name(): string;
17
+ get baseUrl(): string;
18
+ get minOrder(): number;
19
+ get maxOrder(): number;
20
+ get selectedOrder(): number;
21
+ get fixedOrder(): boolean;
22
+ get maxCachedTiles(): number;
23
+ get color(): [number, number, number, number];
24
+ get wireframe(): boolean;
25
+ get propertiesRawText(): string;
26
+ get properties(): ReadonlyMap<string, string>;
27
+ get meshRadius(): number;
28
+ getProperty(key: string): string | undefined;
29
+ getTileUrl(order: number, ipix: number): string;
30
+ private parseProperties;
31
+ private readNumber;
32
+ private clampOrder;
33
+ private normalizeBaseUrl;
34
+ }
@@ -0,0 +1,132 @@
1
+ /*
2
+ * AstroViewer
3
+ * Copyright (C) Fabrizio Giordano
4
+ * SPDX-License-Identifier: LicenseRef-AstroViewer-Dual-License
5
+ */
6
+ export class MeshHiPSDescriptor {
7
+ _name = 'MeshHiPS';
8
+ _baseUrl;
9
+ _minOrder = 0;
10
+ _maxOrder = 0;
11
+ _selectedOrder;
12
+ _fixedOrder = false;
13
+ _maxCachedTiles = 384;
14
+ // default neutral color (contrasts with page background)
15
+ _color = [0.32, 0.34, 0.36, 1.0];
16
+ _wireframe = false;
17
+ _propertiesRawText = '';
18
+ _propertiesMap = new Map();
19
+ _meshRadius = null;
20
+ constructor(config, propertiesText = '') {
21
+ this._baseUrl = this.normalizeBaseUrl(config.baseUrl);
22
+ this._propertiesRawText = propertiesText;
23
+ this.parseProperties(propertiesText);
24
+ this._name = config.name ?? this._propertiesMap.get('obs_collection') ?? this._propertiesMap.get('label') ?? this._name;
25
+ this._minOrder = config.minOrder ?? this.readNumber('hips_order_min', this._minOrder);
26
+ this._maxOrder = config.maxOrder ?? this.readNumber('hips_order', this.readNumber('hips_order_max', this._maxOrder));
27
+ this._fixedOrder = config.order !== undefined;
28
+ this._selectedOrder = config.order ?? this._minOrder;
29
+ this._maxCachedTiles = config.maxCachedTiles ?? this._maxCachedTiles;
30
+ // color: prefer explicit config, then properties.mesh_color, then default
31
+ if (config.color) {
32
+ this._color = config.color;
33
+ }
34
+ else if (this._propertiesMap.has('mesh_color')) {
35
+ const raw = this._propertiesMap.get('mesh_color') || '';
36
+ const parts = raw.split(/[,\s]+/).map((v) => Number(v)).filter(Number.isFinite);
37
+ if (parts.length >= 3) {
38
+ let [r, g, b, a] = parts;
39
+ if (r > 1 || g > 1 || b > 1) {
40
+ // assume 0-255 range
41
+ r = r / 255;
42
+ g = g / 255;
43
+ b = b / 255;
44
+ }
45
+ if (!Number.isFinite(a))
46
+ a = 1;
47
+ this._color = [r, g, b, a];
48
+ }
49
+ }
50
+ else {
51
+ this._color = this._color;
52
+ }
53
+ this._wireframe = config.wireframe ?? this._wireframe;
54
+ this._selectedOrder = this.clampOrder(this._selectedOrder);
55
+ // mesh radius: prefer explicit value from config, otherwise read mandatory property
56
+ if (Number.isFinite(config.meshRadius)) {
57
+ this._meshRadius = config.meshRadius;
58
+ }
59
+ else {
60
+ const radiusFromProps = this.readNumber('mesh_radius', NaN);
61
+ if (!Number.isFinite(radiusFromProps)) {
62
+ throw new Error('Missing mandatory property "mesh_radius" in MeshHiPS properties');
63
+ }
64
+ this._meshRadius = radiusFromProps;
65
+ }
66
+ }
67
+ get name() {
68
+ return this._name;
69
+ }
70
+ get baseUrl() {
71
+ return this._baseUrl;
72
+ }
73
+ get minOrder() {
74
+ return this._minOrder;
75
+ }
76
+ get maxOrder() {
77
+ return this._maxOrder;
78
+ }
79
+ get selectedOrder() {
80
+ return this._selectedOrder;
81
+ }
82
+ get fixedOrder() {
83
+ return this._fixedOrder;
84
+ }
85
+ get maxCachedTiles() {
86
+ return this._maxCachedTiles;
87
+ }
88
+ get color() {
89
+ return this._color;
90
+ }
91
+ get wireframe() {
92
+ return this._wireframe;
93
+ }
94
+ get propertiesRawText() {
95
+ return this._propertiesRawText;
96
+ }
97
+ get properties() {
98
+ return new Map(this._propertiesMap);
99
+ }
100
+ get meshRadius() {
101
+ return this._meshRadius;
102
+ }
103
+ getProperty(key) {
104
+ return this._propertiesMap.get(key);
105
+ }
106
+ getTileUrl(order, ipix) {
107
+ const dir = Math.floor(ipix / 10_000) * 10_000;
108
+ return `${this._baseUrl}Norder${order}/Dir${dir}/Npix${ipix}.obj`;
109
+ }
110
+ parseProperties(propertiesText) {
111
+ const lines = propertiesText.split(/\r\n|\n/);
112
+ for (const raw of lines) {
113
+ const line = raw.trim();
114
+ if (!line || line.startsWith('#'))
115
+ continue;
116
+ const idx = line.indexOf('=');
117
+ if (idx < 0)
118
+ continue;
119
+ this._propertiesMap.set(line.slice(0, idx).trim(), line.slice(idx + 1).trim());
120
+ }
121
+ }
122
+ readNumber(key, fallback) {
123
+ const parsed = Number(this._propertiesMap.get(key));
124
+ return Number.isFinite(parsed) ? parsed : fallback;
125
+ }
126
+ clampOrder(order) {
127
+ return Math.max(this._minOrder, Math.min(this._maxOrder, order));
128
+ }
129
+ normalizeBaseUrl(baseUrl) {
130
+ return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
131
+ }
132
+ }