triiiceratops 0.16.2 → 0.16.3
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/components/OSDViewer.svelte +206 -88
- package/dist/triiiceratops-bundle.js +2896 -2829
- package/dist/triiiceratops-element.iife.js +24 -24
- package/package.json +1 -1
|
@@ -15,6 +15,19 @@
|
|
|
15
15
|
let viewer: any | undefined = $state.raw();
|
|
16
16
|
let OSD: any | undefined = $state();
|
|
17
17
|
|
|
18
|
+
const IIIF_LEVEL0_V2_HTTPS = 'https://iiif.io/api/image/2/level0.json';
|
|
19
|
+
const IIIF_LEVEL0_V2_HTTP = 'http://iiif.io/api/image/2/level0.json';
|
|
20
|
+
const IIIF_LEVEL0_V3_HTTPS = 'https://iiif.io/api/image/3/level0.json';
|
|
21
|
+
|
|
22
|
+
const IIIF_LEVEL0_PROFILES = new Set([
|
|
23
|
+
'level0',
|
|
24
|
+
IIIF_LEVEL0_V2_HTTP,
|
|
25
|
+
IIIF_LEVEL0_V2_HTTPS,
|
|
26
|
+
IIIF_LEVEL0_V3_HTTPS,
|
|
27
|
+
'http://library.stanford.edu/iiif/image-api/compliance.html#level0',
|
|
28
|
+
'http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level0',
|
|
29
|
+
]);
|
|
30
|
+
|
|
18
31
|
// Track OSD state changes for reactivity
|
|
19
32
|
let osdVersion = $state(0);
|
|
20
33
|
// Track last opened tile source to prevent unnecessary resets
|
|
@@ -169,6 +182,7 @@
|
|
|
169
182
|
if (!mounted) return;
|
|
170
183
|
|
|
171
184
|
OSD = osdModule.default || osdModule;
|
|
185
|
+
patchIiifLevel0ProfileCompatibility(OSD);
|
|
172
186
|
|
|
173
187
|
// Initialize OpenSeadragon viewer
|
|
174
188
|
viewer = OSD({
|
|
@@ -193,6 +207,16 @@
|
|
|
193
207
|
},
|
|
194
208
|
});
|
|
195
209
|
|
|
210
|
+
viewer.addHandler('open', () => {
|
|
211
|
+
const overrides = viewerState.config?.openSeadragonConfig ?? {};
|
|
212
|
+
if (overrides.minZoomLevel !== undefined) return;
|
|
213
|
+
|
|
214
|
+
// Prevent zooming into the "no tiles rendered" range seen on
|
|
215
|
+
// some level-0 services when zoomed far past home view.
|
|
216
|
+
const homeZoom = viewer.viewport.getHomeZoom();
|
|
217
|
+
viewer.viewport.minZoomLevel = homeZoom * 0.5;
|
|
218
|
+
});
|
|
219
|
+
|
|
196
220
|
// Notify plugins that OSD is ready
|
|
197
221
|
viewerState.notifyOSDReady(viewer);
|
|
198
222
|
})();
|
|
@@ -239,6 +263,78 @@
|
|
|
239
263
|
}
|
|
240
264
|
});
|
|
241
265
|
|
|
266
|
+
function normalizeIiifLevel0Profile<T>(source: T): T {
|
|
267
|
+
if (!source || typeof source !== 'object') return source;
|
|
268
|
+
|
|
269
|
+
const obj = source as any;
|
|
270
|
+
const profile = obj.profile;
|
|
271
|
+
|
|
272
|
+
if (typeof profile === 'string' && profile === IIIF_LEVEL0_V2_HTTPS) {
|
|
273
|
+
obj.profile = IIIF_LEVEL0_V2_HTTP;
|
|
274
|
+
} else if (Array.isArray(profile) && profile.length > 0) {
|
|
275
|
+
const first = profile[0];
|
|
276
|
+
if (typeof first === 'string' && first === IIIF_LEVEL0_V2_HTTPS) {
|
|
277
|
+
obj.profile = [IIIF_LEVEL0_V2_HTTP, ...profile.slice(1)];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return source;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isLevel0Profile(profile: unknown): boolean {
|
|
285
|
+
if (typeof profile === 'string') {
|
|
286
|
+
return IIIF_LEVEL0_PROFILES.has(profile);
|
|
287
|
+
}
|
|
288
|
+
if (Array.isArray(profile)) {
|
|
289
|
+
return profile.some(
|
|
290
|
+
(item) =>
|
|
291
|
+
typeof item === 'string' && IIIF_LEVEL0_PROFILES.has(item),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function buildLevel0ImageSource(info: any): { type: 'image'; url: string } | null {
|
|
298
|
+
const id = info?.id || info?.['@id'];
|
|
299
|
+
if (!id || typeof id !== 'string') return null;
|
|
300
|
+
|
|
301
|
+
const serviceId = id.endsWith('/info.json')
|
|
302
|
+
? id.slice(0, -'/info.json'.length)
|
|
303
|
+
: id;
|
|
304
|
+
|
|
305
|
+
const context = info?.['@context'];
|
|
306
|
+
const contextValues = Array.isArray(context)
|
|
307
|
+
? context.filter((v: unknown) => typeof v === 'string')
|
|
308
|
+
: [context].filter((v: unknown) => typeof v === 'string');
|
|
309
|
+
const isV3 = contextValues.some((v: string) =>
|
|
310
|
+
v.includes('/api/image/3/context.json'),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// IIIF level-0 is fundamentally a non-tiled target. Using a plain image
|
|
314
|
+
// source avoids zoom-out blanking caused by sparse level math.
|
|
315
|
+
const size = isV3 ? 'max' : 'full';
|
|
316
|
+
return { type: 'image', url: `${serviceId}/full/${size}/0/default.jpg` };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function patchIiifLevel0ProfileCompatibility(osd: any) {
|
|
320
|
+
if (
|
|
321
|
+
!osd?.IIIFTileSource?.prototype ||
|
|
322
|
+
osd.__triiiceratopsIiifLevel0Patched
|
|
323
|
+
)
|
|
324
|
+
return;
|
|
325
|
+
|
|
326
|
+
const proto = osd.IIIFTileSource.prototype;
|
|
327
|
+
const originalConfigure = proto.configure;
|
|
328
|
+
if (typeof originalConfigure !== 'function') return;
|
|
329
|
+
|
|
330
|
+
proto.configure = function (data: any, url: string, postData: any) {
|
|
331
|
+
const configured = originalConfigure.call(this, data, url, postData);
|
|
332
|
+
return normalizeIiifLevel0Profile(configured);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
osd.__triiiceratopsIiifLevel0Patched = true;
|
|
336
|
+
}
|
|
337
|
+
|
|
242
338
|
// Pre-fetch info.json URLs to detect 401 auth errors before passing to OSD
|
|
243
339
|
async function resolveTileSources(
|
|
244
340
|
sources: any[],
|
|
@@ -248,18 +344,31 @@
|
|
|
248
344
|
> {
|
|
249
345
|
const resolved = await Promise.all(
|
|
250
346
|
sources.map(async (source) => {
|
|
251
|
-
// Only
|
|
252
|
-
|
|
347
|
+
// Only probe string sources (info.json URLs); preserve string tile
|
|
348
|
+
// sources so OSD follows its normal source-loading path.
|
|
349
|
+
if (typeof source !== 'string')
|
|
350
|
+
return normalizeIiifLevel0Profile(source);
|
|
253
351
|
try {
|
|
254
352
|
const response = await fetch(source);
|
|
255
353
|
if (response.status === 401) {
|
|
256
354
|
return { __authError: true };
|
|
257
355
|
}
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
|
|
356
|
+
if (response.ok) {
|
|
357
|
+
try {
|
|
358
|
+
const info = normalizeIiifLevel0Profile(
|
|
359
|
+
await response.json(),
|
|
360
|
+
) as any;
|
|
361
|
+
if (isLevel0Profile(info?.profile)) {
|
|
362
|
+
const level0Source = buildLevel0ImageSource(info);
|
|
363
|
+
if (level0Source) return level0Source;
|
|
364
|
+
}
|
|
365
|
+
} catch {
|
|
366
|
+
// Not JSON or malformed response; let OSD handle source URL.
|
|
367
|
+
}
|
|
261
368
|
}
|
|
262
|
-
|
|
369
|
+
// For non-401 responses, keep passing through the original source
|
|
370
|
+
// unless we positively detect and convert level-0.
|
|
371
|
+
return source;
|
|
263
372
|
} catch {
|
|
264
373
|
// Network errors: pass through and let OSD handle it
|
|
265
374
|
return source;
|
|
@@ -311,99 +420,108 @@
|
|
|
311
420
|
const capturedKey = stateKey;
|
|
312
421
|
|
|
313
422
|
if (mode === 'continuous') {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (isVertical) {
|
|
323
|
-
const yPos = index * (1 + gap);
|
|
324
|
-
y = isBTT ? -yPos : yPos;
|
|
325
|
-
} else {
|
|
326
|
-
const xPos = index * (1 + gap);
|
|
327
|
-
x = isRTL ? -xPos : xPos;
|
|
423
|
+
resolveTileSources(sources).then((result) => {
|
|
424
|
+
// Staleness guard: if tile sources changed while we were fetching, discard
|
|
425
|
+
if (capturedKey !== lastTileSourceStr) return;
|
|
426
|
+
|
|
427
|
+
if (!result.ok) {
|
|
428
|
+
viewerState.tileSourceError = result.error;
|
|
429
|
+
viewer.close();
|
|
430
|
+
return;
|
|
328
431
|
}
|
|
329
|
-
return { tileSource: source, x, y, width: 1.0 };
|
|
330
|
-
});
|
|
331
432
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const INITIAL_WINDOW = 3; // canvases on each side of current
|
|
335
|
-
const currentIndex = viewerState.currentCanvasIndex;
|
|
336
|
-
const startIdx = Math.max(0, currentIndex - INITIAL_WINDOW);
|
|
337
|
-
const endIdx = Math.min(
|
|
338
|
-
allPositions.length,
|
|
339
|
-
currentIndex + INITIAL_WINDOW + 1,
|
|
340
|
-
);
|
|
433
|
+
viewerState.tileSourceError = null;
|
|
434
|
+
const resolvedSources = result.resolved;
|
|
341
435
|
|
|
342
|
-
|
|
343
|
-
viewer.open(initialSpread);
|
|
344
|
-
|
|
345
|
-
viewer.addOnceHandler('open', () => {
|
|
346
|
-
// Zoom to the active canvas
|
|
347
|
-
const itemIdx = currentIndex - startIdx;
|
|
348
|
-
const item = viewer.world.getItemAt(itemIdx);
|
|
349
|
-
if (item) {
|
|
350
|
-
viewer.viewport.fitBounds(item.getBounds(), true);
|
|
351
|
-
}
|
|
436
|
+
const gap = 0.025;
|
|
352
437
|
|
|
353
|
-
//
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
438
|
+
// Build position info for all sources
|
|
439
|
+
const allPositions = resolvedSources.map((source, index) => {
|
|
440
|
+
let x = 0;
|
|
441
|
+
let y = 0;
|
|
442
|
+
if (isVertical) {
|
|
443
|
+
const yPos = index * (1 + gap);
|
|
444
|
+
y = isBTT ? -yPos : yPos;
|
|
445
|
+
} else {
|
|
446
|
+
const xPos = index * (1 + gap);
|
|
447
|
+
x = isRTL ? -xPos : xPos;
|
|
448
|
+
}
|
|
449
|
+
return { tileSource: source, x, y, width: 1.0 };
|
|
450
|
+
});
|
|
365
451
|
|
|
366
|
-
|
|
367
|
-
|
|
452
|
+
// Only open a window of canvases around the active one for fast initial load.
|
|
453
|
+
// The rest are added progressively after the viewer opens.
|
|
454
|
+
const INITIAL_WINDOW = 3; // canvases on each side of current
|
|
455
|
+
const currentIndex = viewerState.currentCanvasIndex;
|
|
456
|
+
const startIdx = Math.max(0, currentIndex - INITIAL_WINDOW);
|
|
457
|
+
const endIdx = Math.min(
|
|
458
|
+
allPositions.length,
|
|
459
|
+
currentIndex + INITIAL_WINDOW + 1,
|
|
460
|
+
);
|
|
368
461
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (capturedKey !== lastTileSourceStr) return;
|
|
462
|
+
const initialSpread = allPositions.slice(startIdx, endIdx);
|
|
463
|
+
viewer.open(initialSpread);
|
|
372
464
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
);
|
|
377
|
-
if (
|
|
378
|
-
|
|
379
|
-
const worldBounds = viewer.world.getHomeBounds();
|
|
380
|
-
const viewportBounds = viewer.viewport.getBounds();
|
|
381
|
-
const minZoom =
|
|
382
|
-
(Math.min(
|
|
383
|
-
viewportBounds.width / worldBounds.width,
|
|
384
|
-
viewportBounds.height / worldBounds.height,
|
|
385
|
-
) *
|
|
386
|
-
viewer.viewport.getZoom()) *
|
|
387
|
-
0.8;
|
|
388
|
-
viewer.viewport.minZoomLevel = minZoom;
|
|
389
|
-
viewer.viewport.visibilityRatio = 1;
|
|
390
|
-
return;
|
|
465
|
+
viewer.addOnceHandler('open', () => {
|
|
466
|
+
// Zoom to the active canvas
|
|
467
|
+
const itemIdx = currentIndex - startIdx;
|
|
468
|
+
const item = viewer.world.getItemAt(itemIdx);
|
|
469
|
+
if (item) {
|
|
470
|
+
viewer.viewport.fitBounds(item.getBounds(), true);
|
|
391
471
|
}
|
|
392
472
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
473
|
+
// Progressively add remaining canvases in batches
|
|
474
|
+
const remaining = [
|
|
475
|
+
...allPositions
|
|
476
|
+
.slice(0, startIdx)
|
|
477
|
+
.map((pos, i) => ({ ...pos, originalIndex: i })),
|
|
478
|
+
...allPositions
|
|
479
|
+
.slice(endIdx)
|
|
480
|
+
.map((pos, i) => ({
|
|
481
|
+
...pos,
|
|
482
|
+
originalIndex: endIdx + i,
|
|
483
|
+
})),
|
|
484
|
+
];
|
|
485
|
+
|
|
486
|
+
const BATCH_SIZE = 5;
|
|
487
|
+
let batchIdx = 0;
|
|
488
|
+
|
|
489
|
+
function addNextBatch() {
|
|
490
|
+
// Staleness guard
|
|
491
|
+
if (capturedKey !== lastTileSourceStr) return;
|
|
492
|
+
|
|
493
|
+
const batch = remaining.slice(
|
|
494
|
+
batchIdx * BATCH_SIZE,
|
|
495
|
+
(batchIdx + 1) * BATCH_SIZE,
|
|
496
|
+
);
|
|
497
|
+
if (batch.length === 0) {
|
|
498
|
+
// All loaded — set a stable min zoom floor from world home zoom.
|
|
499
|
+
// Avoid forcing visibilityRatio=1; it can over-constrain level-0
|
|
500
|
+
// services and cause abrupt blanking at zoom-out extremes.
|
|
501
|
+
const overrides =
|
|
502
|
+
viewerState.config?.openSeadragonConfig ?? {};
|
|
503
|
+
if (overrides.minZoomLevel === undefined) {
|
|
504
|
+
const homeZoom = viewer.viewport.getHomeZoom();
|
|
505
|
+
viewer.viewport.minZoomLevel = homeZoom * 0.8;
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
for (const pos of batch) {
|
|
511
|
+
viewer.addTiledImage({
|
|
512
|
+
tileSource: pos.tileSource,
|
|
513
|
+
x: pos.x,
|
|
514
|
+
y: pos.y,
|
|
515
|
+
width: pos.width,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
batchIdx++;
|
|
519
|
+
setTimeout(addNextBatch, 100);
|
|
400
520
|
}
|
|
401
|
-
batchIdx++;
|
|
402
|
-
setTimeout(addNextBatch, 100);
|
|
403
|
-
}
|
|
404
521
|
|
|
405
|
-
|
|
406
|
-
|
|
522
|
+
// Start adding remaining canvases after a short delay
|
|
523
|
+
setTimeout(addNextBatch, 200);
|
|
524
|
+
});
|
|
407
525
|
});
|
|
408
526
|
|
|
409
527
|
return;
|