astro-viewer 2.0.0 → 3.0.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/dist/astroviewer.cjs +18851 -17175
- package/dist/astroviewer.js +1 -1
- package/dist/astroviewer.min.js +1 -1
- package/lib-esm/AstroSphere.d.ts +9 -0
- package/lib-esm/AstroSphere.js +63 -9
- package/lib-esm/AstroViewer.d.ts +18 -0
- package/lib-esm/AstroViewer.js +46 -0
- package/lib-esm/index.d.ts +5 -0
- package/lib-esm/index.js +4 -0
- package/lib-esm/model/AbstractSkyEntity.d.ts +10 -0
- package/lib-esm/model/earth/XYZAncestorMeshCache.d.ts +10 -0
- package/lib-esm/model/earth/XYZAncestorMeshCache.js +31 -0
- package/lib-esm/model/earth/XYZLayer.d.ts +31 -0
- package/lib-esm/model/earth/XYZLayer.js +231 -0
- package/lib-esm/model/earth/XYZMeshBuilder.d.ts +6 -0
- package/lib-esm/model/earth/XYZMeshBuilder.js +97 -0
- package/lib-esm/model/earth/XYZTile.d.ts +39 -0
- package/lib-esm/model/earth/XYZTile.js +187 -0
- package/lib-esm/model/earth/XYZTileBuffer.d.ts +39 -0
- package/lib-esm/model/earth/XYZTileBuffer.js +149 -0
- package/lib-esm/model/earth/XYZTileProvider.d.ts +29 -0
- package/lib-esm/model/earth/XYZTileProvider.js +187 -0
- package/lib-esm/model/earth/XYZTileRequestScheduler.d.ts +25 -0
- package/lib-esm/model/earth/XYZTileRequestScheduler.js +161 -0
- package/lib-esm/model/earth/XYZVisibleTilesManager.d.ts +30 -0
- package/lib-esm/model/earth/XYZVisibleTilesManager.js +208 -0
- package/lib-esm/model/earth/types.d.ts +87 -0
- package/lib-esm/model/earth/types.js +1 -0
- package/lib-esm/model/earth/wmts/WMTSAdapter.d.ts +11 -0
- package/lib-esm/model/earth/wmts/WMTSAdapter.js +85 -0
- package/lib-esm/model/earth/wmts/types.d.ts +1 -0
- package/lib-esm/model/earth/wmts/types.js +1 -0
- package/lib-esm/model/footprints/FootprintSetGL.js +1 -1
- package/lib-esm/model/terra/TerraFootprintSetGL.d.ts +4 -0
- package/lib-esm/model/terra/TerraFootprintSetGL.js +4 -0
- package/lib-esm/model/terra/TerraPointSetGL.d.ts +4 -0
- package/lib-esm/model/terra/TerraPointSetGL.js +4 -0
- package/lib-esm/shader/XYZShaderProgram.d.ts +20 -0
- package/lib-esm/shader/XYZShaderProgram.js +88 -0
- package/package.json +1 -2
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import global from '../../Global.js';
|
|
2
|
+
const MAX_MERCATOR_LAT = 85.0511287798066;
|
|
3
|
+
export class XYZTileProvider {
|
|
4
|
+
_config;
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this._config = config;
|
|
7
|
+
}
|
|
8
|
+
get config() {
|
|
9
|
+
return this._config;
|
|
10
|
+
}
|
|
11
|
+
get minZoom() {
|
|
12
|
+
return Math.max(0, Math.floor(this._config.minZoom ?? 0));
|
|
13
|
+
}
|
|
14
|
+
get maxZoom() {
|
|
15
|
+
return Math.max(this.minZoom, Math.floor(this._config.maxZoom ?? 6));
|
|
16
|
+
}
|
|
17
|
+
getInitialTiles() {
|
|
18
|
+
return this.getVisibleTilesAtZoom(1, null, [], [], 1);
|
|
19
|
+
}
|
|
20
|
+
getTileUrl(tile) {
|
|
21
|
+
if (this._config.urlResolver) {
|
|
22
|
+
return this._config.urlResolver(tile);
|
|
23
|
+
}
|
|
24
|
+
const effectiveY = this.getEffectiveTileY(tile);
|
|
25
|
+
const subdomain = this.getSubdomain(tile);
|
|
26
|
+
return this._config.urlTemplate
|
|
27
|
+
.replace(/\{z\}/g, String(tile.z))
|
|
28
|
+
.replace(/\{x\}/g, String(tile.x))
|
|
29
|
+
.replace(/\{y\}/g, String(effectiveY))
|
|
30
|
+
.replace(/\{s\}/g, subdomain);
|
|
31
|
+
}
|
|
32
|
+
resolveZoom(fovDeg) {
|
|
33
|
+
const safeFov = Math.max(0.01, Math.min(180, fovDeg));
|
|
34
|
+
const targetTileWidthDeg = Math.max(0.01, safeFov / 2);
|
|
35
|
+
const rawZoom = Math.ceil(Math.log2(360 / targetTileWidthDeg));
|
|
36
|
+
return this.clampZoom(rawZoom);
|
|
37
|
+
}
|
|
38
|
+
clampZoom(zoom) {
|
|
39
|
+
return Math.max(this.minZoom, Math.min(this.maxZoom, zoom));
|
|
40
|
+
}
|
|
41
|
+
getEffectiveTileY(tile) {
|
|
42
|
+
if (!this._config.flipY) {
|
|
43
|
+
return tile.y;
|
|
44
|
+
}
|
|
45
|
+
const dim = 2 ** tile.z;
|
|
46
|
+
return dim - 1 - tile.y;
|
|
47
|
+
}
|
|
48
|
+
getSubdomain(tile) {
|
|
49
|
+
const subdomains = this._config.subdomains ?? [];
|
|
50
|
+
if (subdomains.length === 0) {
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
const index = Math.abs(tile.x + tile.y + tile.z) % subdomains.length;
|
|
54
|
+
return subdomains[index] ?? '';
|
|
55
|
+
}
|
|
56
|
+
getVisibleTilesAtZoom(z, centerSphericalDeg, fovPolygon, viewportSphericalSamples, padding = 1) {
|
|
57
|
+
const dim = 2 ** z;
|
|
58
|
+
const center = this.resolveViewCenter(null, centerSphericalDeg);
|
|
59
|
+
const normalizedCenterLon = this.normalizeLonAround(center.lonDeg, center.lonDeg);
|
|
60
|
+
const polygonPoints = fovPolygon.length > 0
|
|
61
|
+
? fovPolygon.map((point) => ({
|
|
62
|
+
lonDeg: this.normalizeLonAround(point.raDeg > 180 ? point.raDeg - 360 : point.raDeg, normalizedCenterLon),
|
|
63
|
+
latDeg: point.decDeg,
|
|
64
|
+
}))
|
|
65
|
+
: [center];
|
|
66
|
+
const samplePoints = viewportSphericalSamples.map((sample) => ({
|
|
67
|
+
lonDeg: this.normalizeLonAround(sample.phi > 180 ? sample.phi - 360 : sample.phi, normalizedCenterLon),
|
|
68
|
+
latDeg: 90 - sample.theta,
|
|
69
|
+
}));
|
|
70
|
+
const coveragePoints = [center, ...polygonPoints, ...samplePoints];
|
|
71
|
+
let minLon = normalizedCenterLon;
|
|
72
|
+
let maxLon = normalizedCenterLon;
|
|
73
|
+
let minLat = center.latDeg;
|
|
74
|
+
let maxLat = center.latDeg;
|
|
75
|
+
for (const point of coveragePoints) {
|
|
76
|
+
minLon = Math.min(minLon, point.lonDeg);
|
|
77
|
+
maxLon = Math.max(maxLon, point.lonDeg);
|
|
78
|
+
minLat = Math.min(minLat, point.latDeg);
|
|
79
|
+
maxLat = Math.max(maxLat, point.latDeg);
|
|
80
|
+
}
|
|
81
|
+
const adaptivePadding = Math.max(padding, Math.min(3, Math.max(1, Math.ceil(z / 3))));
|
|
82
|
+
const minX = Math.floor(((minLon + 180) / 360) * dim) - adaptivePadding;
|
|
83
|
+
const maxX = Math.floor(((maxLon + 180) / 360) * dim) + adaptivePadding;
|
|
84
|
+
const minY = Math.floor(this.latToTileY(maxLat, z)) - adaptivePadding;
|
|
85
|
+
const maxY = Math.floor(this.latToTileY(minLat, z)) + adaptivePadding;
|
|
86
|
+
const tiles = [];
|
|
87
|
+
for (let x = minX; x <= maxX; x++) {
|
|
88
|
+
for (let y = minY; y <= maxY; y++) {
|
|
89
|
+
tiles.push({
|
|
90
|
+
z,
|
|
91
|
+
x: this.wrapTileX(x, dim),
|
|
92
|
+
y: this.clampTileY(y, dim),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const point of coveragePoints) {
|
|
97
|
+
const centerTileX = Math.floor(((point.lonDeg + 180) / 360) * dim);
|
|
98
|
+
const centerTileY = Math.floor(this.latToTileY(point.latDeg, z));
|
|
99
|
+
for (let dx = -adaptivePadding; dx <= adaptivePadding; dx++) {
|
|
100
|
+
for (let dy = -adaptivePadding; dy <= adaptivePadding; dy++) {
|
|
101
|
+
tiles.push({
|
|
102
|
+
z,
|
|
103
|
+
x: this.wrapTileX(centerTileX + dx, dim),
|
|
104
|
+
y: this.clampTileY(centerTileY + dy, dim),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return this.deduplicateTiles(tiles);
|
|
110
|
+
}
|
|
111
|
+
tileFromSpherical(zoom, sphericalDeg) {
|
|
112
|
+
const lonDeg = sphericalDeg.phi > 180 ? sphericalDeg.phi - 360 : sphericalDeg.phi;
|
|
113
|
+
const latDeg = 90 - sphericalDeg.theta;
|
|
114
|
+
const dim = 2 ** zoom;
|
|
115
|
+
const x = Math.floor(((lonDeg + 180) / 360) * dim);
|
|
116
|
+
const y = Math.floor(this.latToTileY(latDeg, zoom));
|
|
117
|
+
return {
|
|
118
|
+
z: zoom,
|
|
119
|
+
x: this.wrapTileX(x, dim),
|
|
120
|
+
y: this.clampTileY(y, dim),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
getNeighborTiles(tile, ring = 1) {
|
|
124
|
+
const dim = 2 ** tile.z;
|
|
125
|
+
const neighbors = [];
|
|
126
|
+
for (let dx = -ring; dx <= ring; dx++) {
|
|
127
|
+
for (let dy = -ring; dy <= ring; dy++) {
|
|
128
|
+
neighbors.push({
|
|
129
|
+
z: tile.z,
|
|
130
|
+
x: this.wrapTileX(tile.x + dx, dim),
|
|
131
|
+
y: this.clampTileY(tile.y + dy, dim),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return this.deduplicateTiles(neighbors);
|
|
136
|
+
}
|
|
137
|
+
deduplicateTiles(tiles) {
|
|
138
|
+
const unique = new Map();
|
|
139
|
+
for (const tile of tiles) {
|
|
140
|
+
unique.set(`${tile.z}/${tile.x}/${tile.y}`, tile);
|
|
141
|
+
}
|
|
142
|
+
return Array.from(unique.values());
|
|
143
|
+
}
|
|
144
|
+
resolveViewCenter(camera, centerSphericalDeg) {
|
|
145
|
+
if (centerSphericalDeg) {
|
|
146
|
+
return {
|
|
147
|
+
lonDeg: centerSphericalDeg.phi > 180 ? centerSphericalDeg.phi - 360 : centerSphericalDeg.phi,
|
|
148
|
+
latDeg: 90 - centerSphericalDeg.theta,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (!camera) {
|
|
152
|
+
return { lonDeg: 0, latDeg: 0 };
|
|
153
|
+
}
|
|
154
|
+
const [x, y, z] = camera.getCameraPosition();
|
|
155
|
+
const len = Math.hypot(x, y, z);
|
|
156
|
+
if (!Number.isFinite(len) || len === 0) {
|
|
157
|
+
return { lonDeg: 0, latDeg: 0 };
|
|
158
|
+
}
|
|
159
|
+
const scale = global.insideSphere ? 1 / len : -1 / len;
|
|
160
|
+
const vx = x * scale;
|
|
161
|
+
const vy = y * scale;
|
|
162
|
+
const vz = z * scale;
|
|
163
|
+
const lonDeg = (Math.atan2(vy, vx) * 180) / Math.PI;
|
|
164
|
+
const latDeg = (Math.asin(Math.max(-1, Math.min(1, vz))) * 180) / Math.PI;
|
|
165
|
+
return { lonDeg, latDeg };
|
|
166
|
+
}
|
|
167
|
+
latToTileY(latDeg, z) {
|
|
168
|
+
const lat = Math.max(-MAX_MERCATOR_LAT, Math.min(MAX_MERCATOR_LAT, latDeg));
|
|
169
|
+
const latRad = (lat * Math.PI) / 180;
|
|
170
|
+
const n = 2 ** z;
|
|
171
|
+
return ((1 - Math.asinh(Math.tan(latRad)) / Math.PI) / 2) * n;
|
|
172
|
+
}
|
|
173
|
+
wrapTileX(x, dim) {
|
|
174
|
+
return ((x % dim) + dim) % dim;
|
|
175
|
+
}
|
|
176
|
+
normalizeLonAround(lonDeg, referenceLonDeg) {
|
|
177
|
+
let lon = lonDeg;
|
|
178
|
+
while (lon - referenceLonDeg > 180)
|
|
179
|
+
lon -= 360;
|
|
180
|
+
while (lon - referenceLonDeg < -180)
|
|
181
|
+
lon += 360;
|
|
182
|
+
return lon;
|
|
183
|
+
}
|
|
184
|
+
clampTileY(y, dim) {
|
|
185
|
+
return Math.max(0, Math.min(dim - 1, y));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { XYZRequestSchedulerDebugStats } from './types.js';
|
|
2
|
+
export declare class XYZTileRequestError extends Error {
|
|
3
|
+
cooldownMs: number;
|
|
4
|
+
retryable: boolean;
|
|
5
|
+
constructor(message: string, cooldownMs: number, retryable?: boolean);
|
|
6
|
+
}
|
|
7
|
+
export declare class XYZTileRequestScheduler {
|
|
8
|
+
private _maxConcurrent;
|
|
9
|
+
private _activeCount;
|
|
10
|
+
private _queue;
|
|
11
|
+
private _inflight;
|
|
12
|
+
private _hostBackoff;
|
|
13
|
+
private _sequence;
|
|
14
|
+
constructor(maxConcurrent?: number);
|
|
15
|
+
setMaxConcurrent(maxConcurrent: number): void;
|
|
16
|
+
getMaxConcurrent(): number;
|
|
17
|
+
getDebugStats(): XYZRequestSchedulerDebugStats;
|
|
18
|
+
load(url: string, priority?: number): Promise<Blob>;
|
|
19
|
+
private pump;
|
|
20
|
+
private fetchBlob;
|
|
21
|
+
private getHostCooldown;
|
|
22
|
+
private registerSuccess;
|
|
23
|
+
private registerFailure;
|
|
24
|
+
}
|
|
25
|
+
export declare const xyzTileRequestScheduler: XYZTileRequestScheduler;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
export class XYZTileRequestError extends Error {
|
|
2
|
+
cooldownMs;
|
|
3
|
+
retryable;
|
|
4
|
+
constructor(message, cooldownMs, retryable = true) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'XYZTileRequestError';
|
|
7
|
+
this.cooldownMs = cooldownMs;
|
|
8
|
+
this.retryable = retryable;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class XYZTileRequestScheduler {
|
|
12
|
+
_maxConcurrent;
|
|
13
|
+
_activeCount = 0;
|
|
14
|
+
_queue = [];
|
|
15
|
+
_inflight = new Map();
|
|
16
|
+
_hostBackoff = new Map();
|
|
17
|
+
_sequence = 0;
|
|
18
|
+
constructor(maxConcurrent = 4) {
|
|
19
|
+
this._maxConcurrent = maxConcurrent;
|
|
20
|
+
}
|
|
21
|
+
setMaxConcurrent(maxConcurrent) {
|
|
22
|
+
this._maxConcurrent = Math.max(1, Math.floor(maxConcurrent));
|
|
23
|
+
this.pump();
|
|
24
|
+
}
|
|
25
|
+
getMaxConcurrent() {
|
|
26
|
+
return this._maxConcurrent;
|
|
27
|
+
}
|
|
28
|
+
getDebugStats() {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const hostsInBackoff = Array.from(this._hostBackoff.entries())
|
|
31
|
+
.map(([host, state]) => ({
|
|
32
|
+
host,
|
|
33
|
+
cooldownMs: Math.max(0, state.cooldownUntil - now),
|
|
34
|
+
consecutiveFailures: state.consecutiveFailures,
|
|
35
|
+
}))
|
|
36
|
+
.filter((entry) => entry.cooldownMs > 0 || entry.consecutiveFailures > 0)
|
|
37
|
+
.sort((a, b) => b.cooldownMs - a.cooldownMs);
|
|
38
|
+
return {
|
|
39
|
+
activeRequests: this._activeCount,
|
|
40
|
+
queuedRequests: this._queue.length,
|
|
41
|
+
inflightRequests: this._inflight.size,
|
|
42
|
+
maxConcurrentRequests: this._maxConcurrent,
|
|
43
|
+
highestQueuedPriority: this._queue[0]?.priority ?? null,
|
|
44
|
+
hostsInBackoff,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
load(url, priority = 0) {
|
|
48
|
+
const inflight = this._inflight.get(url);
|
|
49
|
+
if (inflight) {
|
|
50
|
+
return inflight;
|
|
51
|
+
}
|
|
52
|
+
const hostCooldown = this.getHostCooldown(url);
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
if (hostCooldown > now) {
|
|
55
|
+
return Promise.reject(new XYZTileRequestError(`Cooldown active for ${new URL(url).host}`, hostCooldown - now));
|
|
56
|
+
}
|
|
57
|
+
const promise = new Promise((resolve, reject) => {
|
|
58
|
+
this._queue.push({
|
|
59
|
+
url,
|
|
60
|
+
resolve,
|
|
61
|
+
reject,
|
|
62
|
+
priority,
|
|
63
|
+
sequence: this._sequence++,
|
|
64
|
+
});
|
|
65
|
+
this._queue.sort((a, b) => b.priority - a.priority || a.sequence - b.sequence);
|
|
66
|
+
this.pump();
|
|
67
|
+
}).finally(() => {
|
|
68
|
+
this._inflight.delete(url);
|
|
69
|
+
});
|
|
70
|
+
this._inflight.set(url, promise);
|
|
71
|
+
return promise;
|
|
72
|
+
}
|
|
73
|
+
pump() {
|
|
74
|
+
while (this._activeCount < this._maxConcurrent && this._queue.length > 0) {
|
|
75
|
+
const item = this._queue.shift();
|
|
76
|
+
if (!item) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const hostCooldown = this.getHostCooldown(item.url);
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
if (hostCooldown > now) {
|
|
82
|
+
item.reject(new XYZTileRequestError(`Cooldown active for ${new URL(item.url).host}`, hostCooldown - now));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
this._activeCount += 1;
|
|
86
|
+
this.fetchBlob(item)
|
|
87
|
+
.then(item.resolve)
|
|
88
|
+
.catch(item.reject)
|
|
89
|
+
.finally(() => {
|
|
90
|
+
this._activeCount -= 1;
|
|
91
|
+
this.pump();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async fetchBlob(item) {
|
|
96
|
+
try {
|
|
97
|
+
const response = await fetch(item.url, {
|
|
98
|
+
mode: 'cors',
|
|
99
|
+
cache: 'force-cache',
|
|
100
|
+
});
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
const isTransient = response.status === 429 || response.status >= 500;
|
|
103
|
+
const cooldownMs = this.registerFailure(item.url, isTransient);
|
|
104
|
+
throw new XYZTileRequestError(`HTTP ${response.status} loading ${item.url}`, isTransient ? cooldownMs : 5 * 60 * 1000, isTransient);
|
|
105
|
+
}
|
|
106
|
+
this.registerSuccess(item.url);
|
|
107
|
+
return await response.blob();
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error instanceof XYZTileRequestError) {
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
const cooldownMs = this.registerFailure(item.url, false);
|
|
114
|
+
throw new XYZTileRequestError(`Network/CORS failure loading ${item.url}`, cooldownMs, true);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
getHostCooldown(url) {
|
|
118
|
+
try {
|
|
119
|
+
return this._hostBackoff.get(new URL(url).host)?.cooldownUntil ?? 0;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
registerSuccess(url) {
|
|
126
|
+
try {
|
|
127
|
+
const host = new URL(url).host;
|
|
128
|
+
this._hostBackoff.set(host, {
|
|
129
|
+
cooldownUntil: 0,
|
|
130
|
+
consecutiveFailures: 0,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// no-op
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
registerFailure(url, isRateLimited) {
|
|
138
|
+
if (!isRateLimited) {
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const host = new URL(url).host;
|
|
143
|
+
const previous = this._hostBackoff.get(host) ?? {
|
|
144
|
+
cooldownUntil: 0,
|
|
145
|
+
consecutiveFailures: 0,
|
|
146
|
+
};
|
|
147
|
+
const consecutiveFailures = previous.consecutiveFailures + 1;
|
|
148
|
+
const baseDelayMs = 4000;
|
|
149
|
+
const cooldownMs = Math.min(120000, baseDelayMs * (2 ** Math.min(consecutiveFailures - 1, 5)));
|
|
150
|
+
this._hostBackoff.set(host, {
|
|
151
|
+
cooldownUntil: Date.now() + cooldownMs,
|
|
152
|
+
consecutiveFailures,
|
|
153
|
+
});
|
|
154
|
+
return cooldownMs;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return 30000;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export const xyzTileRequestScheduler = new XYZTileRequestScheduler();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { XYZTileCoord } from './types.js';
|
|
2
|
+
import { XYZTileProvider } from './XYZTileProvider.js';
|
|
3
|
+
import type { SkyEntityDrawInput } from '../AbstractSkyEntity.js';
|
|
4
|
+
export type XYZTileSelection = {
|
|
5
|
+
key: string;
|
|
6
|
+
currentTiles: XYZTileCoord[];
|
|
7
|
+
fallbackTiles: XYZTileCoord[];
|
|
8
|
+
currentZoom: number;
|
|
9
|
+
coreTileCount: number;
|
|
10
|
+
coverageTileCount: number;
|
|
11
|
+
};
|
|
12
|
+
export declare class XYZVisibleTilesManager {
|
|
13
|
+
private _provider;
|
|
14
|
+
constructor(provider: XYZTileProvider);
|
|
15
|
+
selectTiles(input: SkyEntityDrawInput): XYZTileSelection;
|
|
16
|
+
private buildCoreVisibleTiles;
|
|
17
|
+
private buildCoverageTiles;
|
|
18
|
+
private collectCoverageSamples;
|
|
19
|
+
private interpolateFoVPolygon;
|
|
20
|
+
private getNeighborRing;
|
|
21
|
+
private isBoundaryTile;
|
|
22
|
+
private normalizeTile;
|
|
23
|
+
private normalizePhi;
|
|
24
|
+
private buildFallbackMap;
|
|
25
|
+
private buildFallbackSeedTiles;
|
|
26
|
+
private orderTilesByScreenRelevance;
|
|
27
|
+
private orderFallbackTiles;
|
|
28
|
+
private getCenterTileCoord;
|
|
29
|
+
private key;
|
|
30
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
export class XYZVisibleTilesManager {
|
|
2
|
+
_provider;
|
|
3
|
+
constructor(provider) {
|
|
4
|
+
this._provider = provider;
|
|
5
|
+
}
|
|
6
|
+
selectTiles(input) {
|
|
7
|
+
const currentZoom = this._provider.resolveZoom(input.fovDeg ?? 180);
|
|
8
|
+
const coreTiles = this.orderTilesByScreenRelevance(this.buildCoreVisibleTiles(currentZoom, input), currentZoom, input.centerSphericalDeg ?? null);
|
|
9
|
+
const coverageTiles = this.orderTilesByScreenRelevance(this.buildCoverageTiles(currentZoom, input, coreTiles), currentZoom, input.centerSphericalDeg ?? null);
|
|
10
|
+
const currentTiles = [...coreTiles, ...coverageTiles];
|
|
11
|
+
const fallbackSeedTiles = this.buildFallbackSeedTiles(coreTiles, currentZoom, input.centerSphericalDeg ?? null);
|
|
12
|
+
const fallbackTiles = this.orderFallbackTiles(Array.from(this.buildFallbackMap(fallbackSeedTiles, currentZoom).values()), currentZoom, input.centerSphericalDeg ?? null);
|
|
13
|
+
const key = `${currentZoom}:${currentTiles.map((tile) => `${tile.x}/${tile.y}`).join('|')}`;
|
|
14
|
+
return {
|
|
15
|
+
key,
|
|
16
|
+
currentTiles,
|
|
17
|
+
fallbackTiles,
|
|
18
|
+
currentZoom,
|
|
19
|
+
coreTileCount: coreTiles.length,
|
|
20
|
+
coverageTileCount: coverageTiles.length,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
buildCoreVisibleTiles(currentZoom, input) {
|
|
24
|
+
const samples = this.collectCoverageSamples(input);
|
|
25
|
+
if (samples.length === 0) {
|
|
26
|
+
return this._provider.getVisibleTilesAtZoom(currentZoom, null, [], [], 0);
|
|
27
|
+
}
|
|
28
|
+
const tiles = [];
|
|
29
|
+
for (const sample of samples) {
|
|
30
|
+
tiles.push(this._provider.tileFromSpherical(currentZoom, sample));
|
|
31
|
+
}
|
|
32
|
+
return this._provider.deduplicateTiles(tiles);
|
|
33
|
+
}
|
|
34
|
+
buildCoverageTiles(currentZoom, input, coreTiles) {
|
|
35
|
+
if (coreTiles.length === 0) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const ring = this.getNeighborRing(currentZoom, input.fovDeg ?? 180);
|
|
39
|
+
if (ring <= 0) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
const coreSet = new Set(coreTiles.map((tile) => this.key(tile)));
|
|
43
|
+
const coverageTiles = [];
|
|
44
|
+
for (const tile of coreTiles) {
|
|
45
|
+
if (!this.isBoundaryTile(tile, coreSet)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const neighbors = this._provider.getNeighborTiles(tile, ring);
|
|
49
|
+
for (const neighbor of neighbors) {
|
|
50
|
+
const neighborKey = this.key(neighbor);
|
|
51
|
+
if (coreSet.has(neighborKey)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
coverageTiles.push(neighbor);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return this._provider.deduplicateTiles(coverageTiles);
|
|
58
|
+
}
|
|
59
|
+
collectCoverageSamples(input) {
|
|
60
|
+
const samples = [];
|
|
61
|
+
if (input.centerSphericalDeg) {
|
|
62
|
+
samples.push(input.centerSphericalDeg);
|
|
63
|
+
}
|
|
64
|
+
for (const sample of input.viewportSphericalSamples ?? []) {
|
|
65
|
+
samples.push(sample);
|
|
66
|
+
}
|
|
67
|
+
const polygonSamples = this.interpolateFoVPolygon(input.fovPolygon ?? []);
|
|
68
|
+
for (const sample of polygonSamples) {
|
|
69
|
+
samples.push(sample);
|
|
70
|
+
}
|
|
71
|
+
return samples;
|
|
72
|
+
}
|
|
73
|
+
interpolateFoVPolygon(fovPolygon) {
|
|
74
|
+
if (fovPolygon.length === 0) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const polygonSpherical = fovPolygon.map((point) => ({
|
|
78
|
+
phi: point.raDeg < 0 ? point.raDeg + 360 : point.raDeg,
|
|
79
|
+
theta: 90 - point.decDeg,
|
|
80
|
+
}));
|
|
81
|
+
const samples = [...polygonSpherical];
|
|
82
|
+
const segmentInterpolationCount = polygonSpherical.length >= 4 ? 3 : 1;
|
|
83
|
+
for (let i = 0; i < polygonSpherical.length; i++) {
|
|
84
|
+
const start = polygonSpherical[i];
|
|
85
|
+
const end = polygonSpherical[(i + 1) % polygonSpherical.length];
|
|
86
|
+
if (!start || !end)
|
|
87
|
+
continue;
|
|
88
|
+
const startPhi = this.normalizePhi(start.phi);
|
|
89
|
+
let endPhi = this.normalizePhi(end.phi);
|
|
90
|
+
if (Math.abs(endPhi - startPhi) > 180) {
|
|
91
|
+
endPhi += endPhi > startPhi ? -360 : 360;
|
|
92
|
+
}
|
|
93
|
+
for (let step = 1; step <= segmentInterpolationCount; step++) {
|
|
94
|
+
const t = step / (segmentInterpolationCount + 1);
|
|
95
|
+
const phi = this.normalizePhi(startPhi + (endPhi - startPhi) * t);
|
|
96
|
+
const theta = start.theta + (end.theta - start.theta) * t;
|
|
97
|
+
samples.push({ phi, theta });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return samples;
|
|
101
|
+
}
|
|
102
|
+
getNeighborRing(currentZoom, fovDeg) {
|
|
103
|
+
if (currentZoom >= 14 && fovDeg < 10) {
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
isBoundaryTile(tile, coreTileKeys) {
|
|
109
|
+
const directNeighbors = [
|
|
110
|
+
{ z: tile.z, x: tile.x - 1, y: tile.y },
|
|
111
|
+
{ z: tile.z, x: tile.x + 1, y: tile.y },
|
|
112
|
+
{ z: tile.z, x: tile.x, y: tile.y - 1 },
|
|
113
|
+
{ z: tile.z, x: tile.x, y: tile.y + 1 },
|
|
114
|
+
];
|
|
115
|
+
return directNeighbors.some((neighbor) => !coreTileKeys.has(this.key(this.normalizeTile(neighbor))));
|
|
116
|
+
}
|
|
117
|
+
normalizeTile(tile) {
|
|
118
|
+
const dim = 2 ** tile.z;
|
|
119
|
+
return {
|
|
120
|
+
z: tile.z,
|
|
121
|
+
x: ((tile.x % dim) + dim) % dim,
|
|
122
|
+
y: Math.max(0, Math.min(dim - 1, tile.y)),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
normalizePhi(phi) {
|
|
126
|
+
let value = phi;
|
|
127
|
+
while (value < 0)
|
|
128
|
+
value += 360;
|
|
129
|
+
while (value >= 360)
|
|
130
|
+
value -= 360;
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
buildFallbackMap(currentTiles, currentZoom) {
|
|
134
|
+
const fallbackMap = new Map();
|
|
135
|
+
const minFallbackZoom = Math.max(this._provider.minZoom, currentZoom - 2);
|
|
136
|
+
for (let z = currentZoom - 1; z >= minFallbackZoom; z--) {
|
|
137
|
+
const dz = currentZoom - z;
|
|
138
|
+
for (const tile of currentTiles) {
|
|
139
|
+
const fallback = {
|
|
140
|
+
z,
|
|
141
|
+
x: tile.x >> dz,
|
|
142
|
+
y: tile.y >> dz,
|
|
143
|
+
};
|
|
144
|
+
fallbackMap.set(`${fallback.z}/${fallback.x}/${fallback.y}`, fallback);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return fallbackMap;
|
|
148
|
+
}
|
|
149
|
+
buildFallbackSeedTiles(coreTiles, zoom, centerSphericalDeg) {
|
|
150
|
+
if (coreTiles.length === 0) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
const centerTile = this.getCenterTileCoord(zoom, centerSphericalDeg);
|
|
154
|
+
if (!centerTile) {
|
|
155
|
+
return coreTiles;
|
|
156
|
+
}
|
|
157
|
+
const centerKey = this.key(centerTile);
|
|
158
|
+
if (coreTiles.some((tile) => this.key(tile) === centerKey)) {
|
|
159
|
+
return coreTiles;
|
|
160
|
+
}
|
|
161
|
+
return [...coreTiles, centerTile];
|
|
162
|
+
}
|
|
163
|
+
orderTilesByScreenRelevance(tiles, zoom, centerSphericalDeg) {
|
|
164
|
+
const centerTile = this.getCenterTileCoord(zoom, centerSphericalDeg);
|
|
165
|
+
if (!centerTile) {
|
|
166
|
+
return tiles;
|
|
167
|
+
}
|
|
168
|
+
return [...tiles].sort((a, b) => {
|
|
169
|
+
const distanceA = Math.abs(a.x - centerTile.x) + Math.abs(a.y - centerTile.y);
|
|
170
|
+
const distanceB = Math.abs(b.x - centerTile.x) + Math.abs(b.y - centerTile.y);
|
|
171
|
+
return distanceA - distanceB;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
orderFallbackTiles(tiles, currentZoom, centerSphericalDeg) {
|
|
175
|
+
return [...tiles].sort((a, b) => {
|
|
176
|
+
if (b.z !== a.z) {
|
|
177
|
+
return b.z - a.z;
|
|
178
|
+
}
|
|
179
|
+
const centerTileA = this.getCenterTileCoord(a.z, centerSphericalDeg);
|
|
180
|
+
const centerTileB = this.getCenterTileCoord(b.z, centerSphericalDeg);
|
|
181
|
+
if (!centerTileA || !centerTileB) {
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
const distanceA = Math.abs(a.x - centerTileA.x) + Math.abs(a.y - centerTileA.y);
|
|
185
|
+
const distanceB = Math.abs(b.x - centerTileB.x) + Math.abs(b.y - centerTileB.y);
|
|
186
|
+
return distanceA - distanceB || currentZoom - a.z - (currentZoom - b.z);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
getCenterTileCoord(zoom, centerSphericalDeg) {
|
|
190
|
+
if (!centerSphericalDeg) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const lonDeg = centerSphericalDeg.phi > 180 ? centerSphericalDeg.phi - 360 : centerSphericalDeg.phi;
|
|
194
|
+
const latDeg = 90 - centerSphericalDeg.theta;
|
|
195
|
+
const dim = 2 ** zoom;
|
|
196
|
+
const x = Math.floor(((lonDeg + 180) / 360) * dim);
|
|
197
|
+
const latRad = (Math.max(-85.0511287798066, Math.min(85.0511287798066, latDeg)) * Math.PI) / 180;
|
|
198
|
+
const y = Math.floor(((1 - Math.asinh(Math.tan(latRad)) / Math.PI) / 2) * dim);
|
|
199
|
+
return {
|
|
200
|
+
z: zoom,
|
|
201
|
+
x: ((x % dim) + dim) % dim,
|
|
202
|
+
y: Math.max(0, Math.min(dim - 1, y)),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
key(tile) {
|
|
206
|
+
return `${tile.z}/${tile.x}/${tile.y}`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export type XYZLayerConfig = {
|
|
2
|
+
urlTemplate: string;
|
|
3
|
+
minZoom?: number;
|
|
4
|
+
maxZoom?: number;
|
|
5
|
+
segmentsPerSide?: number;
|
|
6
|
+
tileSize?: number;
|
|
7
|
+
maxCachedTiles?: number;
|
|
8
|
+
interactionDebounceMs?: number;
|
|
9
|
+
subdomains?: string[];
|
|
10
|
+
attribution?: string;
|
|
11
|
+
flipY?: boolean;
|
|
12
|
+
urlResolver?: (tile: XYZTileCoord) => string;
|
|
13
|
+
};
|
|
14
|
+
export type XYZLayerDebugStats = {
|
|
15
|
+
cacheSize: number;
|
|
16
|
+
visibleTileCount: number;
|
|
17
|
+
currentTileCount: number;
|
|
18
|
+
fallbackTileCount: number;
|
|
19
|
+
coreTileCount: number;
|
|
20
|
+
coverageTileCount: number;
|
|
21
|
+
readyTileCount: number;
|
|
22
|
+
loadingTileCount: number;
|
|
23
|
+
coolingDownTileCount: number;
|
|
24
|
+
currentZoom: number | null;
|
|
25
|
+
tileSelectionKey: string | null;
|
|
26
|
+
isSettling: boolean;
|
|
27
|
+
coarseTileCount: number;
|
|
28
|
+
hasPendingSelection: boolean;
|
|
29
|
+
pendingSelectionKey: string | null;
|
|
30
|
+
};
|
|
31
|
+
export type XYZRequestBackoffDebugEntry = {
|
|
32
|
+
host: string;
|
|
33
|
+
cooldownMs: number;
|
|
34
|
+
consecutiveFailures: number;
|
|
35
|
+
};
|
|
36
|
+
export type XYZRequestSchedulerDebugStats = {
|
|
37
|
+
activeRequests: number;
|
|
38
|
+
queuedRequests: number;
|
|
39
|
+
inflightRequests: number;
|
|
40
|
+
maxConcurrentRequests: number;
|
|
41
|
+
highestQueuedPriority: number | null;
|
|
42
|
+
hostsInBackoff: XYZRequestBackoffDebugEntry[];
|
|
43
|
+
};
|
|
44
|
+
export type XYZDebugStats = {
|
|
45
|
+
activeBaseLayer: 'hips' | 'xyz' | null;
|
|
46
|
+
layer: XYZLayerDebugStats | null;
|
|
47
|
+
requests: XYZRequestSchedulerDebugStats;
|
|
48
|
+
};
|
|
49
|
+
export type XYZTileCoord = {
|
|
50
|
+
z: number;
|
|
51
|
+
x: number;
|
|
52
|
+
y: number;
|
|
53
|
+
};
|
|
54
|
+
export type XYZTileMesh = {
|
|
55
|
+
positions: Float32Array;
|
|
56
|
+
uvs: Float32Array;
|
|
57
|
+
indices: Uint16Array | Uint32Array;
|
|
58
|
+
};
|
|
59
|
+
export type XYZTileGpuMesh = {
|
|
60
|
+
positionBuffer: WebGLBuffer | null;
|
|
61
|
+
uvBuffer: WebGLBuffer | null;
|
|
62
|
+
indexBuffer: WebGLBuffer | null;
|
|
63
|
+
indexCount: number;
|
|
64
|
+
indexType: number;
|
|
65
|
+
};
|
|
66
|
+
export type WMTSRequestEncoding = 'kvp' | 'rest';
|
|
67
|
+
export type WMTSLayerConfig = {
|
|
68
|
+
baseUrl: string;
|
|
69
|
+
layer: string;
|
|
70
|
+
tileMatrixSet: string;
|
|
71
|
+
style?: string;
|
|
72
|
+
time?: string;
|
|
73
|
+
format?: string;
|
|
74
|
+
requestEncoding?: WMTSRequestEncoding;
|
|
75
|
+
version?: string;
|
|
76
|
+
dimensions?: Record<string, string>;
|
|
77
|
+
matrixLabels?: string[];
|
|
78
|
+
urlTemplate?: string;
|
|
79
|
+
minZoom?: number;
|
|
80
|
+
maxZoom?: number;
|
|
81
|
+
segmentsPerSide?: number;
|
|
82
|
+
tileSize?: number;
|
|
83
|
+
maxCachedTiles?: number;
|
|
84
|
+
subdomains?: string[];
|
|
85
|
+
attribution?: string;
|
|
86
|
+
flipY?: boolean;
|
|
87
|
+
};
|