diva.js 7.2.5 → 7.2.6
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 +65 -11
- package/build/diva.debug.js +29165 -0
- package/build/diva.esm.js +17 -0
- package/build/diva.js +17 -0
- package/package.json +15 -1
- package/.clang-format +0 -7
- package/.github/workflows/npm-publish.yml +0 -45
- package/Makefile +0 -75
- package/elm.json +0 -32
- package/review/elm.json +0 -52
- package/review/src/ReviewConfig.elm +0 -87
- package/scripts/elm-esm.sh +0 -40
- package/scripts/minify-css.mjs +0 -31
- package/src/Filters.elm +0 -1044
- package/src/Main.elm +0 -1217
- package/src/Model.elm +0 -213
- package/src/Msg.elm +0 -59
- package/src/Utilities.elm +0 -46
- package/src/View/CollectionExplorer.elm +0 -172
- package/src/View/Helpers.elm +0 -86
- package/src/View/HtmlRenderer.elm +0 -136
- package/src/View/Icons.elm +0 -159
- package/src/View/ManifestInfoModal.elm +0 -363
- package/src/View/PageViewModal.elm +0 -1046
- package/src/View/Sidebar.elm +0 -786
- package/src/View/Toolbar.elm +0 -189
- package/src/View.elm +0 -244
- package/src/diva.ts +0 -802
- package/src/filters.ts +0 -1843
- package/src/styles/app.css +0 -328
- package/src/styles/collection.css +0 -75
- package/src/styles/modal.css +0 -388
- package/src/styles/sidebar.css +0 -215
- package/src/styles/theme.css +0 -39
- package/src/styles/toolbar.css +0 -154
- package/src/viewer-element.ts +0 -1307
- package/testing/index.html +0 -52
- package/testing/testing.html +0 -231
- package/tsconfig.json +0 -12
package/src/diva.ts
DELETED
|
@@ -1,802 +0,0 @@
|
|
|
1
|
-
import "./viewer-element";
|
|
2
|
-
|
|
3
|
-
// @ts-ignore
|
|
4
|
-
import divaCss from "../cache/diva.css";
|
|
5
|
-
// @ts-ignore
|
|
6
|
-
import {Elm} from "../cache/elm-esm.js";
|
|
7
|
-
|
|
8
|
-
import {Filters, setFilterOptions} from "./filters";
|
|
9
|
-
|
|
10
|
-
declare const OpenSeadragon: any;
|
|
11
|
-
|
|
12
|
-
const DIVA_STYLE_ID = "diva-inline-styles";
|
|
13
|
-
|
|
14
|
-
const injectStyles = (cssText: string) => {
|
|
15
|
-
if (typeof document === "undefined")
|
|
16
|
-
{
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (document.getElementById(DIVA_STYLE_ID))
|
|
21
|
-
{
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const styleEl = document.createElement("style");
|
|
26
|
-
styleEl.id = DIVA_STYLE_ID;
|
|
27
|
-
styleEl.textContent = cssText;
|
|
28
|
-
|
|
29
|
-
const target = document.head || document.getElementsByTagName("head")[0] || document.documentElement;
|
|
30
|
-
target.appendChild(styleEl);
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
injectStyles(divaCss);
|
|
34
|
-
|
|
35
|
-
type FilterSettings = {
|
|
36
|
-
rotation?: number;
|
|
37
|
-
flip?: boolean;
|
|
38
|
-
thresholdEnabled?: boolean;
|
|
39
|
-
threshold?: number;
|
|
40
|
-
brightnessEnabled?: boolean;
|
|
41
|
-
brightness?: number;
|
|
42
|
-
saturationEnabled?: boolean;
|
|
43
|
-
saturation?: number;
|
|
44
|
-
vibranceEnabled?: boolean;
|
|
45
|
-
vibrance?: number;
|
|
46
|
-
hueEnabled?: boolean;
|
|
47
|
-
hue?: number;
|
|
48
|
-
ccRedEnabled?: boolean;
|
|
49
|
-
ccRed?: number;
|
|
50
|
-
ccGreenEnabled?: boolean;
|
|
51
|
-
ccGreen?: number;
|
|
52
|
-
ccBlueEnabled?: boolean;
|
|
53
|
-
ccBlue?: number;
|
|
54
|
-
contrastEnabled?: boolean;
|
|
55
|
-
contrast?: number;
|
|
56
|
-
gammaEnabled?: boolean;
|
|
57
|
-
gamma?: number;
|
|
58
|
-
grayscale?: boolean;
|
|
59
|
-
invert?: boolean;
|
|
60
|
-
morphEnabled?: boolean;
|
|
61
|
-
morphOperation?: string;
|
|
62
|
-
morphKernel?: number;
|
|
63
|
-
convolutionEnabled?: boolean;
|
|
64
|
-
convolutionPreset?: string;
|
|
65
|
-
colourmapEnabled?: boolean;
|
|
66
|
-
colourmapPreset?: string;
|
|
67
|
-
colourmapCenter?: number;
|
|
68
|
-
pseudoColourEnabled?: boolean;
|
|
69
|
-
pseudoColourMode?: string;
|
|
70
|
-
pseudoColourRed?: number;
|
|
71
|
-
pseudoColourGreen?: number;
|
|
72
|
-
pseudoColourBlue?: number;
|
|
73
|
-
globalPcaEnabled?: boolean;
|
|
74
|
-
pcaMode?: string;
|
|
75
|
-
pcaHue?: number;
|
|
76
|
-
colourReplaceEnabled?: boolean;
|
|
77
|
-
colourReplaceSource?: string;
|
|
78
|
-
colourReplaceTarget?: string;
|
|
79
|
-
colourReplaceTolerance?: number;
|
|
80
|
-
colourReplaceBlend?: number;
|
|
81
|
-
colourReplacePreserveLum?: boolean;
|
|
82
|
-
normalizeEnabled?: boolean;
|
|
83
|
-
normalizeStrength?: number;
|
|
84
|
-
unsharpEnabled?: boolean;
|
|
85
|
-
unsharpAmount?: number;
|
|
86
|
-
adaptiveEnabled?: boolean;
|
|
87
|
-
adaptiveWindow?: number;
|
|
88
|
-
adaptiveOffset?: number;
|
|
89
|
-
altRedGamma?: number;
|
|
90
|
-
altRedGammaEnabled?: boolean;
|
|
91
|
-
altRedSigmoid?: number;
|
|
92
|
-
altRedSigmoidEnabled?: boolean;
|
|
93
|
-
altRedHue?: number;
|
|
94
|
-
altRedHueEnabled?: boolean;
|
|
95
|
-
altRedHueWindow?: number;
|
|
96
|
-
altGreenGamma?: number;
|
|
97
|
-
altGreenGammaEnabled?: boolean;
|
|
98
|
-
altGreenSigmoid?: number;
|
|
99
|
-
altGreenSigmoidEnabled?: boolean;
|
|
100
|
-
altGreenHue?: number;
|
|
101
|
-
altGreenHueEnabled?: boolean;
|
|
102
|
-
altGreenHueWindow?: number;
|
|
103
|
-
altGreenVibranceEnabled?: boolean;
|
|
104
|
-
altGreenVibrance?: number;
|
|
105
|
-
altBlueGamma?: number;
|
|
106
|
-
altBlueGammaEnabled?: boolean;
|
|
107
|
-
altBlueSigmoid?: number;
|
|
108
|
-
altBlueSigmoidEnabled?: boolean;
|
|
109
|
-
altBlueHue?: number;
|
|
110
|
-
altBlueHueEnabled?: boolean;
|
|
111
|
-
altBlueHueWindow?: number;
|
|
112
|
-
altBlueVibranceEnabled?: boolean;
|
|
113
|
-
altBlueVibrance?: number;
|
|
114
|
-
altRedVibrance?: number;
|
|
115
|
-
altRedVibranceEnabled?: boolean;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
type FilterPreviewPayload = {
|
|
119
|
-
tileSource: string; aspect : number;
|
|
120
|
-
filters?: FilterSettings;
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
type ElmPorts = {
|
|
124
|
-
tileSourcesUpdated: {subscribe: (callback: (tileSources: string[]) => void) => void};
|
|
125
|
-
pageAspectsUpdated : {subscribe : (callback: (aspects: number[]) => void) => void};
|
|
126
|
-
pageLabelsUpdated : {subscribe : (callback: (labels: string[]) => void) => void};
|
|
127
|
-
zoomLevelUpdated : {subscribe : (callback: (zoom: number) => void) => void};
|
|
128
|
-
zoomBy : {subscribe : (callback: (factor: number) => void) => void};
|
|
129
|
-
scrollToIndex : {subscribe : (callback: (index: number) => void) => void};
|
|
130
|
-
filterPreviewUpdated : {subscribe : (callback: (payload: FilterPreviewPayload|null) => void) => void};
|
|
131
|
-
setFullscreen : {subscribe : (callback: (enabled: boolean) => void) => void};
|
|
132
|
-
saveFilteredImage : {subscribe : (callback: () => void) => void};
|
|
133
|
-
layoutModeUpdated : {subscribe : (callback: (mode: string) => void) => void};
|
|
134
|
-
layoutConfigUpdated : {subscribe : (callback: (config: {mode: string; direction : string}) => void) => void};
|
|
135
|
-
pageIndexChanged : {send : (index: number) => void};
|
|
136
|
-
pageIndexChangedInstant : {send : (index: number) => void};
|
|
137
|
-
fullscreenChanged : {send : (enabled: boolean) => void};
|
|
138
|
-
zoomChanged : {send : (zoom: number) => void};
|
|
139
|
-
viewerLoadingChanged : {send : (loading: boolean) => void};
|
|
140
|
-
copyToClipboard : {subscribe : (callback: (text: string) => void) => void};
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
type ElmApp = {
|
|
144
|
-
ports: ElmPorts;
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
type DivaFlags = {
|
|
148
|
-
objectData: string;
|
|
149
|
-
acceptHeaders?: string[];
|
|
150
|
-
showSidebar?: boolean;
|
|
151
|
-
showTitle?: boolean;
|
|
152
|
-
setLanguage?: string;
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
type DivaRoot = HTMLElement&
|
|
156
|
-
{
|
|
157
|
-
elmTree?: unknown;
|
|
158
|
-
__divaInstance?: Diva;
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
class Diva
|
|
162
|
-
{
|
|
163
|
-
private readonly root: HTMLElement;
|
|
164
|
-
private app: ElmApp;
|
|
165
|
-
private mainViewer: any = null;
|
|
166
|
-
private filterViewer: any = null;
|
|
167
|
-
private filterViewerElement: HTMLElement|null = null;
|
|
168
|
-
private filterOptions: FilterSettings|null = null;
|
|
169
|
-
private filterViewerFlipped = false;
|
|
170
|
-
private currentFilterTileSource: string|null = null;
|
|
171
|
-
private pendingFilterPreview: FilterPreviewPayload|null = null;
|
|
172
|
-
private filterPreviewRetries = 0;
|
|
173
|
-
private filterPreviewRafId: number|null = null;
|
|
174
|
-
private isDestroyed = false;
|
|
175
|
-
private readonly handlePageChangeBound: (event: Event) => void;
|
|
176
|
-
private readonly handleZoomChangeBound: (event: Event) => void;
|
|
177
|
-
private readonly handleLoadingChangeBound: (event: Event) => void;
|
|
178
|
-
private readonly handleFullscreenChangeBound: () => void;
|
|
179
|
-
|
|
180
|
-
constructor(rootId: string, flags: DivaFlags)
|
|
181
|
-
{
|
|
182
|
-
const root = document.getElementById(rootId);
|
|
183
|
-
if (!root)
|
|
184
|
-
{
|
|
185
|
-
throw new Error(`Missing root element: ${rootId}`);
|
|
186
|
-
}
|
|
187
|
-
const rootAny = root as DivaRoot;
|
|
188
|
-
|
|
189
|
-
if (rootAny.__divaInstance)
|
|
190
|
-
{
|
|
191
|
-
rootAny.__divaInstance.destroy();
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// if an elmTree instance is already defined on this element, destroy
|
|
195
|
-
// it.
|
|
196
|
-
if (rootAny.elmTree)
|
|
197
|
-
{
|
|
198
|
-
delete rootAny.elmTree;
|
|
199
|
-
root.innerHTML = "";
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
this.root = root;
|
|
203
|
-
this.isDestroyed = false;
|
|
204
|
-
|
|
205
|
-
this.handlePageChangeBound = this.handlePageChange.bind(this);
|
|
206
|
-
this.handleZoomChangeBound = this.handleZoomChange.bind(this);
|
|
207
|
-
this.handleLoadingChangeBound = this.handleLoadingChange.bind(this);
|
|
208
|
-
this.handleFullscreenChangeBound = this.handleFullscreenChange.bind(this);
|
|
209
|
-
|
|
210
|
-
let langCode = this.detectLanguage();
|
|
211
|
-
|
|
212
|
-
this.app = Elm.Main.init({
|
|
213
|
-
node : root,
|
|
214
|
-
flags : {
|
|
215
|
-
rootElementId : rootId,
|
|
216
|
-
objectData : flags.objectData,
|
|
217
|
-
acceptHeaders : flags.acceptHeaders || [],
|
|
218
|
-
showSidebar : flags.showSidebar !== false,
|
|
219
|
-
showTitle : flags.showTitle !== false,
|
|
220
|
-
userLanguage : flags.setLanguage || langCode
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
rootAny.__divaInstance = this;
|
|
224
|
-
|
|
225
|
-
this.bindPorts();
|
|
226
|
-
this.bindPageChange();
|
|
227
|
-
this.bindFullscreenChange();
|
|
228
|
-
this.bindZoomChange();
|
|
229
|
-
this.bindLoadingChange();
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Detects the current locale of the browser,
|
|
234
|
-
* and return the first part of it.
|
|
235
|
-
*
|
|
236
|
-
* @returns {string}
|
|
237
|
-
*/
|
|
238
|
-
private detectLanguage(): string { return navigator.language.split("-")[0]; }
|
|
239
|
-
|
|
240
|
-
private bindPorts(): void
|
|
241
|
-
{
|
|
242
|
-
this.getPort("tileSourcesUpdated")
|
|
243
|
-
.subscribe((tileSources: string[]) => { this.callViewerMethod("setTileSources", tileSources); });
|
|
244
|
-
|
|
245
|
-
this.getPort("pageAspectsUpdated")
|
|
246
|
-
.subscribe((aspects: number[]) => { this.callViewerMethod("setPageAspects", aspects); });
|
|
247
|
-
|
|
248
|
-
this.getPort("pageLabelsUpdated")
|
|
249
|
-
.subscribe((labels: string[]) => { this.callViewerMethod("setPageLabels", labels); });
|
|
250
|
-
|
|
251
|
-
this.getPort("zoomLevelUpdated").subscribe((zoom: number) => { this.callViewerMethod("setZoomLevel", zoom); });
|
|
252
|
-
|
|
253
|
-
this.getPort("zoomBy").subscribe((factor: number) => { this.callViewerMethod("zoomBy", factor); });
|
|
254
|
-
|
|
255
|
-
this.getPort("scrollToIndex").subscribe((index: number) => { this.callViewerMethod("scrollToIndex", index); });
|
|
256
|
-
|
|
257
|
-
this.getPort("filterPreviewUpdated").subscribe((payload: FilterPreviewPayload|null) => {
|
|
258
|
-
if (!payload)
|
|
259
|
-
{
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
this.pendingFilterPreview = payload;
|
|
263
|
-
this.applyFilterPreview();
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
this.getPort("setFullscreen").subscribe((enabled: boolean) => { this.setFullscreen(enabled); });
|
|
267
|
-
|
|
268
|
-
this.getPort("saveFilteredImage").subscribe(() => { this.saveFilteredImage(); });
|
|
269
|
-
|
|
270
|
-
this.getPort("layoutConfigUpdated").subscribe((config: {mode: string; direction : string}) => {
|
|
271
|
-
if (this.callViewerMethod("setLayoutConfig", config.mode, config.direction))
|
|
272
|
-
{
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
this.callViewerMethod("setViewingDirection", config.direction);
|
|
276
|
-
this.callViewerMethod("setLayoutMode", config.mode);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
this.getPort("layoutModeUpdated")
|
|
280
|
-
.subscribe((mode: string) => { this.callViewerMethod("setLayoutMode", mode); });
|
|
281
|
-
|
|
282
|
-
this.getPort("copyToClipboard").subscribe((text: string) => { this.copyToClipboard(text); });
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
private ensureMainViewer(): any
|
|
286
|
-
{
|
|
287
|
-
if (!this.mainViewer)
|
|
288
|
-
{
|
|
289
|
-
this.mainViewer = document.getElementById("main-viewer");
|
|
290
|
-
}
|
|
291
|
-
return this.mainViewer;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
private applyFilterPreview(): void
|
|
295
|
-
{
|
|
296
|
-
if (this.isDestroyed)
|
|
297
|
-
{
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
if (!this.pendingFilterPreview)
|
|
301
|
-
{
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
const element = this.ensureFilterViewerElement();
|
|
305
|
-
if (!element)
|
|
306
|
-
{
|
|
307
|
-
if (this.filterPreviewRetries < 10)
|
|
308
|
-
{
|
|
309
|
-
this.filterPreviewRetries += 1;
|
|
310
|
-
this.filterPreviewRafId = requestAnimationFrame(() => {
|
|
311
|
-
this.filterPreviewRafId = null;
|
|
312
|
-
this.applyFilterPreview();
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const payload = this.pendingFilterPreview;
|
|
319
|
-
this.pendingFilterPreview = null;
|
|
320
|
-
this.filterPreviewRetries = 0;
|
|
321
|
-
this.filterOptions = payload.filters || null;
|
|
322
|
-
this.ensureFilterViewer();
|
|
323
|
-
if (this.filterViewer)
|
|
324
|
-
{
|
|
325
|
-
const tileSourceChanged = this.currentFilterTileSource !== payload.tileSource;
|
|
326
|
-
if (tileSourceChanged)
|
|
327
|
-
{
|
|
328
|
-
this.currentFilterTileSource = payload.tileSource;
|
|
329
|
-
this.filterViewer.open(payload.tileSource);
|
|
330
|
-
}
|
|
331
|
-
else
|
|
332
|
-
{
|
|
333
|
-
this.applyFilterOptions();
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
private copyToClipboard(text: string): void
|
|
339
|
-
{
|
|
340
|
-
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function")
|
|
341
|
-
{
|
|
342
|
-
navigator.clipboard.writeText(text).catch(() => {});
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
private bindPageChange(): void
|
|
348
|
-
{
|
|
349
|
-
this.bindViewerEvent("diva-page-change", this.handlePageChangeBound as EventListener);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
private bindFullscreenChange(): void
|
|
353
|
-
{
|
|
354
|
-
document.addEventListener("fullscreenchange", this.handleFullscreenChangeBound);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
private bindZoomChange(): void
|
|
358
|
-
{
|
|
359
|
-
this.bindViewerEvent("diva-zoom-change", this.handleZoomChangeBound as EventListener);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
private bindLoadingChange(): void
|
|
363
|
-
{
|
|
364
|
-
this.bindViewerEvent("diva-loading-change", this.handleLoadingChangeBound as EventListener);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
public destroy(): void
|
|
368
|
-
{
|
|
369
|
-
this.isDestroyed = true;
|
|
370
|
-
if (this.filterPreviewRafId !== null)
|
|
371
|
-
{
|
|
372
|
-
cancelAnimationFrame(this.filterPreviewRafId);
|
|
373
|
-
this.filterPreviewRafId = null;
|
|
374
|
-
}
|
|
375
|
-
this.removeViewerEvent("diva-page-change", this.handlePageChangeBound as EventListener);
|
|
376
|
-
this.removeViewerEvent("diva-zoom-change", this.handleZoomChangeBound as EventListener);
|
|
377
|
-
this.removeViewerEvent("diva-loading-change", this.handleLoadingChangeBound as EventListener);
|
|
378
|
-
document.removeEventListener("fullscreenchange", this.handleFullscreenChangeBound);
|
|
379
|
-
if (this.filterViewer && typeof this.filterViewer.destroy === "function")
|
|
380
|
-
{
|
|
381
|
-
this.filterViewer.destroy();
|
|
382
|
-
}
|
|
383
|
-
this.filterViewer = null;
|
|
384
|
-
this.filterViewerElement = null;
|
|
385
|
-
this.currentFilterTileSource = null;
|
|
386
|
-
this.pendingFilterPreview = null;
|
|
387
|
-
|
|
388
|
-
if (this.root)
|
|
389
|
-
{
|
|
390
|
-
const rootAny = this.root as DivaRoot;
|
|
391
|
-
if (rootAny.__divaInstance === this)
|
|
392
|
-
{
|
|
393
|
-
delete rootAny.__divaInstance;
|
|
394
|
-
}
|
|
395
|
-
if (rootAny.elmTree)
|
|
396
|
-
{
|
|
397
|
-
delete rootAny.elmTree;
|
|
398
|
-
}
|
|
399
|
-
this.root.innerHTML = "";
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
private setFullscreen(enabled: boolean): void
|
|
404
|
-
{
|
|
405
|
-
if (enabled)
|
|
406
|
-
{
|
|
407
|
-
if (document.fullscreenElement || !document.fullscreenEnabled)
|
|
408
|
-
{
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
this.root.requestFullscreen().catch(() => {});
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
if (!document.fullscreenElement)
|
|
416
|
-
{
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
document.exitFullscreen().catch(() => {});
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
private handlePageChange(event: Event): void
|
|
423
|
-
{
|
|
424
|
-
const detail = (event as CustomEvent).detail;
|
|
425
|
-
if (!detail || typeof detail.index !== "number")
|
|
426
|
-
{
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
if (detail.instant)
|
|
430
|
-
{
|
|
431
|
-
this.getPort("pageIndexChangedInstant").send(detail.index);
|
|
432
|
-
}
|
|
433
|
-
else
|
|
434
|
-
{
|
|
435
|
-
this.getPort("pageIndexChanged").send(detail.index);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
private handleZoomChange(event: Event): void
|
|
440
|
-
{
|
|
441
|
-
const detail = (event as CustomEvent).detail;
|
|
442
|
-
if (!detail || typeof detail.zoom !== "number")
|
|
443
|
-
{
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
this.getPort("zoomChanged").send(detail.zoom);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
private handleLoadingChange(event: Event): void
|
|
450
|
-
{
|
|
451
|
-
const detail = (event as CustomEvent).detail;
|
|
452
|
-
if (!detail || typeof detail.loading !== "boolean")
|
|
453
|
-
{
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
this.getPort("viewerLoadingChanged").send(detail.loading);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
private handleFullscreenChange(): void
|
|
460
|
-
{
|
|
461
|
-
const isFullscreen = Boolean(document.fullscreenElement);
|
|
462
|
-
this.getPort("fullscreenChanged").send(isFullscreen);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
private ensureFilterViewer(): void
|
|
466
|
-
{
|
|
467
|
-
if (this.filterViewer || !this.filterViewerElement)
|
|
468
|
-
{
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
this.filterViewer = OpenSeadragon({
|
|
473
|
-
element : this.filterViewerElement,
|
|
474
|
-
showNavigationControl : false,
|
|
475
|
-
preserveViewport : true,
|
|
476
|
-
visibilityRatio : 0,
|
|
477
|
-
drawer : "canvas",
|
|
478
|
-
crossOriginPolicy : "Anonymous",
|
|
479
|
-
loadTilesWithAjax : true,
|
|
480
|
-
ajaxWithCredentials : false
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
this.filterViewer.addHandler("open", () => {
|
|
484
|
-
const drawer = this.filterViewer?.drawer as any;
|
|
485
|
-
const canvas = drawer && drawer.canvas ? drawer.canvas : null;
|
|
486
|
-
if (canvas && typeof canvas.getContext === "function")
|
|
487
|
-
{
|
|
488
|
-
const context = canvas.getContext("2d", {willReadFrequently : true});
|
|
489
|
-
if (context)
|
|
490
|
-
{
|
|
491
|
-
drawer.context = context;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
if (this.filterViewer && this.filterViewer.viewport)
|
|
495
|
-
{
|
|
496
|
-
this.filterViewer.viewport.fitBounds(this.filterViewer.world.getHomeBounds(), true);
|
|
497
|
-
}
|
|
498
|
-
this.filterViewerFlipped = false;
|
|
499
|
-
this.applyFilterOptions();
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
private ensureFilterViewerElement(): HTMLElement|null
|
|
504
|
-
{
|
|
505
|
-
const element = document.getElementById("filter-viewer");
|
|
506
|
-
if (!element)
|
|
507
|
-
{
|
|
508
|
-
this.filterViewerElement = null;
|
|
509
|
-
return null;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
if (this.filterViewerElement !== element)
|
|
513
|
-
{
|
|
514
|
-
if (this.filterViewer && typeof this.filterViewer.destroy === "function")
|
|
515
|
-
{
|
|
516
|
-
this.filterViewer.destroy();
|
|
517
|
-
}
|
|
518
|
-
this.filterViewer = null;
|
|
519
|
-
this.currentFilterTileSource = null;
|
|
520
|
-
this.filterViewerFlipped = false;
|
|
521
|
-
this.filterViewerElement = element;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
return this.filterViewerElement;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
private applyFilterOptions(): void
|
|
528
|
-
{
|
|
529
|
-
if (!this.filterViewer)
|
|
530
|
-
{
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
const options = buildFilterOptions(this.filterOptions);
|
|
534
|
-
setFilterOptions(this.filterViewer, options);
|
|
535
|
-
|
|
536
|
-
if (this.filterViewer.viewport)
|
|
537
|
-
{
|
|
538
|
-
const rotation = this.filterOptions?.rotation || 0;
|
|
539
|
-
this.filterViewer.viewport.setRotation(rotation);
|
|
540
|
-
|
|
541
|
-
const shouldFlip = Boolean(this.filterOptions?.flip);
|
|
542
|
-
const viewport = this.filterViewer.viewport;
|
|
543
|
-
if (typeof viewport.toggleFlip === "function")
|
|
544
|
-
{
|
|
545
|
-
let isFlipped = this.filterViewerFlipped;
|
|
546
|
-
if (typeof viewport.getFlip === "function")
|
|
547
|
-
{
|
|
548
|
-
isFlipped = Boolean(viewport.getFlip());
|
|
549
|
-
}
|
|
550
|
-
if (shouldFlip != isFlipped)
|
|
551
|
-
{
|
|
552
|
-
viewport.toggleFlip();
|
|
553
|
-
this.filterViewerFlipped = shouldFlip;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
private saveFilteredImage(): void
|
|
560
|
-
{
|
|
561
|
-
if (!this.filterViewer)
|
|
562
|
-
{
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
const drawer = (this.filterViewer as any).drawer;
|
|
566
|
-
const canvas = drawer && drawer.canvas ? drawer.canvas as HTMLCanvasElement : null;
|
|
567
|
-
if (!canvas)
|
|
568
|
-
{
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
try
|
|
572
|
-
{
|
|
573
|
-
const dataUrl = canvas.toDataURL("image/png");
|
|
574
|
-
const link = document.createElement("a");
|
|
575
|
-
link.href = dataUrl;
|
|
576
|
-
link.download = `diva-filtered-${Date.now()}.png`;
|
|
577
|
-
document.body.appendChild(link);
|
|
578
|
-
link.click();
|
|
579
|
-
link.remove();
|
|
580
|
-
}
|
|
581
|
-
catch (error)
|
|
582
|
-
{
|
|
583
|
-
console.error("Failed to save filtered image", error);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
private getPort<Name extends keyof ElmPorts>(name: Name): ElmPorts[Name]
|
|
588
|
-
{
|
|
589
|
-
const ports = this.app.ports as Partial<ElmPorts>| undefined;
|
|
590
|
-
const port = ports ? ports[name] : undefined;
|
|
591
|
-
if (!port)
|
|
592
|
-
{
|
|
593
|
-
throw new Error(`Missing Elm port: ${String(name)}`);
|
|
594
|
-
}
|
|
595
|
-
return port as ElmPorts[Name];
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
private callViewerMethod(name: string, ...args: any[]): boolean
|
|
599
|
-
{
|
|
600
|
-
const viewer = this.ensureMainViewer();
|
|
601
|
-
if (!viewer)
|
|
602
|
-
{
|
|
603
|
-
return false;
|
|
604
|
-
}
|
|
605
|
-
const method = viewer[name];
|
|
606
|
-
if (typeof method !== "function")
|
|
607
|
-
{
|
|
608
|
-
return false;
|
|
609
|
-
}
|
|
610
|
-
method.apply(viewer, args);
|
|
611
|
-
return true;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
private bindViewerEvent(name: string, handler: EventListener): void
|
|
615
|
-
{
|
|
616
|
-
const viewer = this.ensureMainViewer();
|
|
617
|
-
if (!viewer)
|
|
618
|
-
{
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
viewer.removeEventListener(name, handler);
|
|
622
|
-
viewer.addEventListener(name, handler);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
private removeViewerEvent(name: string, handler: EventListener): void
|
|
626
|
-
{
|
|
627
|
-
const viewer = this.ensureMainViewer();
|
|
628
|
-
if (!viewer)
|
|
629
|
-
{
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
|
-
viewer.removeEventListener(name, handler);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
type FilterMapping = {
|
|
637
|
-
enabled: keyof FilterSettings; filter : keyof typeof filterFunctions;
|
|
638
|
-
args?: (keyof FilterSettings)[];
|
|
639
|
-
defaults?: number[];
|
|
640
|
-
};
|
|
641
|
-
|
|
642
|
-
const filterFunctions = {
|
|
643
|
-
THRESHOLDING : Filters.THRESHOLDING,
|
|
644
|
-
BRIGHTNESS : Filters.BRIGHTNESS,
|
|
645
|
-
SATURATION : Filters.SATURATION,
|
|
646
|
-
VIBRANCE : Filters.VIBRANCE,
|
|
647
|
-
HUE : Filters.HUE,
|
|
648
|
-
CC_RED : Filters.CC_RED,
|
|
649
|
-
CC_GREEN : Filters.CC_GREEN,
|
|
650
|
-
CC_BLUE : Filters.CC_BLUE,
|
|
651
|
-
CONTRAST : Filters.CONTRAST,
|
|
652
|
-
GAMMA : Filters.GAMMA,
|
|
653
|
-
GREYSCALE : Filters.GREYSCALE,
|
|
654
|
-
INVERT : Filters.INVERT,
|
|
655
|
-
BACKGROUND_NORMALIZE : Filters.BACKGROUND_NORMALIZE,
|
|
656
|
-
UNSHARP_MASK : Filters.UNSHARP_MASK,
|
|
657
|
-
ALT_RED_GAMMA : Filters.ALT_RED_GAMMA,
|
|
658
|
-
ALT_GREEN_GAMMA : Filters.ALT_GREEN_GAMMA,
|
|
659
|
-
ALT_BLUE_GAMMA : Filters.ALT_BLUE_GAMMA,
|
|
660
|
-
ALT_RED_SIGMOID : Filters.ALT_RED_SIGMOID,
|
|
661
|
-
ALT_GREEN_SIGMOID : Filters.ALT_GREEN_SIGMOID,
|
|
662
|
-
ALT_BLUE_SIGMOID : Filters.ALT_BLUE_SIGMOID,
|
|
663
|
-
ALT_RED_HUE : Filters.ALT_RED_HUE,
|
|
664
|
-
ALT_GREEN_HUE : Filters.ALT_GREEN_HUE,
|
|
665
|
-
ALT_BLUE_HUE : Filters.ALT_BLUE_HUE,
|
|
666
|
-
ALT_RED_VIBRANCE : Filters.ALT_RED_VIBRANCE,
|
|
667
|
-
ALT_GREEN_VIBRANCE : Filters.ALT_GREEN_VIBRANCE,
|
|
668
|
-
ALT_BLUE_VIBRANCE : Filters.ALT_BLUE_VIBRANCE,
|
|
669
|
-
GLOBAL_PCA_COLOR : Filters.GLOBAL_PCA_COLOR,
|
|
670
|
-
ADAPTIVE_THRESHOLD : Filters.ADAPTIVE_THRESHOLD
|
|
671
|
-
};
|
|
672
|
-
|
|
673
|
-
const simpleFilterMappings: FilterMapping[] = [
|
|
674
|
-
{enabled : "thresholdEnabled", filter : "THRESHOLDING", args : [ "threshold" ]},
|
|
675
|
-
{enabled : "brightnessEnabled", filter : "BRIGHTNESS", args : [ "brightness" ]},
|
|
676
|
-
{enabled : "saturationEnabled", filter : "SATURATION", args : [ "saturation" ]},
|
|
677
|
-
{enabled : "vibranceEnabled", filter : "VIBRANCE", args : [ "vibrance" ]},
|
|
678
|
-
{enabled : "hueEnabled", filter : "HUE", args : [ "hue" ]},
|
|
679
|
-
{enabled : "ccRedEnabled", filter : "CC_RED", args : [ "ccRed" ]},
|
|
680
|
-
{enabled : "ccGreenEnabled", filter : "CC_GREEN", args : [ "ccGreen" ]},
|
|
681
|
-
{enabled : "ccBlueEnabled", filter : "CC_BLUE", args : [ "ccBlue" ]},
|
|
682
|
-
{enabled : "contrastEnabled", filter : "CONTRAST", args : [ "contrast" ]},
|
|
683
|
-
{enabled : "gammaEnabled", filter : "GAMMA", args : [ "gamma" ]},
|
|
684
|
-
{enabled : "grayscale", filter : "GREYSCALE"},
|
|
685
|
-
{enabled : "invert", filter : "INVERT"},
|
|
686
|
-
{enabled : "normalizeEnabled", filter : "BACKGROUND_NORMALIZE", args : [ "normalizeStrength" ]},
|
|
687
|
-
{enabled : "unsharpEnabled", filter : "UNSHARP_MASK", args : [ "unsharpAmount" ]},
|
|
688
|
-
];
|
|
689
|
-
|
|
690
|
-
const altFilterMappings: FilterMapping[] = [
|
|
691
|
-
{enabled : "altRedGammaEnabled", filter : "ALT_RED_GAMMA", args : [ "altRedGamma" ]},
|
|
692
|
-
{enabled : "altGreenGammaEnabled", filter : "ALT_GREEN_GAMMA", args : [ "altGreenGamma" ]},
|
|
693
|
-
{enabled : "altBlueGammaEnabled", filter : "ALT_BLUE_GAMMA", args : [ "altBlueGamma" ]},
|
|
694
|
-
{enabled : "altRedSigmoidEnabled", filter : "ALT_RED_SIGMOID", args : [ "altRedSigmoid" ]},
|
|
695
|
-
{enabled : "altGreenSigmoidEnabled", filter : "ALT_GREEN_SIGMOID", args : [ "altGreenSigmoid" ]},
|
|
696
|
-
{enabled : "altBlueSigmoidEnabled", filter : "ALT_BLUE_SIGMOID", args : [ "altBlueSigmoid" ]},
|
|
697
|
-
{
|
|
698
|
-
enabled : "altRedHueEnabled",
|
|
699
|
-
filter : "ALT_RED_HUE",
|
|
700
|
-
args : [ "altRedHue", "altRedHueWindow" ],
|
|
701
|
-
defaults : [ 0, 8 ]
|
|
702
|
-
},
|
|
703
|
-
{
|
|
704
|
-
enabled : "altGreenHueEnabled",
|
|
705
|
-
filter : "ALT_GREEN_HUE",
|
|
706
|
-
args : [ "altGreenHue", "altGreenHueWindow" ],
|
|
707
|
-
defaults : [ 0, 8 ]
|
|
708
|
-
},
|
|
709
|
-
{
|
|
710
|
-
enabled : "altBlueHueEnabled",
|
|
711
|
-
filter : "ALT_BLUE_HUE",
|
|
712
|
-
args : [ "altBlueHue", "altBlueHueWindow" ],
|
|
713
|
-
defaults : [ 0, 8 ]
|
|
714
|
-
},
|
|
715
|
-
{enabled : "altRedVibranceEnabled", filter : "ALT_RED_VIBRANCE", args : [ "altRedVibrance" ]},
|
|
716
|
-
{enabled : "altGreenVibranceEnabled", filter : "ALT_GREEN_VIBRANCE", args : [ "altGreenVibrance" ]},
|
|
717
|
-
{enabled : "altBlueVibranceEnabled", filter : "ALT_BLUE_VIBRANCE", args : [ "altBlueVibrance" ]},
|
|
718
|
-
{
|
|
719
|
-
enabled : "adaptiveEnabled",
|
|
720
|
-
filter : "ADAPTIVE_THRESHOLD",
|
|
721
|
-
args : [ "adaptiveWindow", "adaptiveOffset" ],
|
|
722
|
-
defaults : [ 15, 10 ]
|
|
723
|
-
},
|
|
724
|
-
];
|
|
725
|
-
|
|
726
|
-
const appendMappedProcessors = (processors: any[], filters: FilterSettings, mappings: FilterMapping[]): void => {
|
|
727
|
-
for (const mapping of mappings)
|
|
728
|
-
{
|
|
729
|
-
if (filters[mapping.enabled])
|
|
730
|
-
{
|
|
731
|
-
const filterArgs = mapping.args?.map((key, i) => filters[key] ?? (mapping.defaults?.[i] ?? 0)) ?? [];
|
|
732
|
-
const filterFn = filterFunctions[mapping.filter] as (...args: any[]) => any;
|
|
733
|
-
processors.push(filterFn(...(filterArgs as any[])));
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
};
|
|
737
|
-
|
|
738
|
-
const buildFilterOptions = (filters: FilterSettings|null): any => {
|
|
739
|
-
if (!filters)
|
|
740
|
-
{
|
|
741
|
-
return {filters : [], loadMode : "sync"};
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
const processors: any[] = [];
|
|
745
|
-
|
|
746
|
-
// Keep this order stable: simple -> special-case -> alt mappings.
|
|
747
|
-
appendMappedProcessors(processors, filters, simpleFilterMappings);
|
|
748
|
-
|
|
749
|
-
if (filters.morphEnabled)
|
|
750
|
-
{
|
|
751
|
-
const comparator = filters.morphOperation === "dilate" ? Math.max : Math.min;
|
|
752
|
-
processors.push(Filters.MORPHOLOGICAL_OPERATION(filters.morphKernel ?? 3, comparator));
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
if (filters.convolutionEnabled)
|
|
756
|
-
{
|
|
757
|
-
const kernel = Filters.CONVOLUTION_PRESET(filters.convolutionPreset || "");
|
|
758
|
-
if (kernel)
|
|
759
|
-
{
|
|
760
|
-
processors.push(Filters.CONVOLUTION(kernel));
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
if (filters.colourmapEnabled)
|
|
765
|
-
{
|
|
766
|
-
const colourmap = Filters.COLORMAP_PRESET(filters.colourmapPreset || "");
|
|
767
|
-
if (colourmap)
|
|
768
|
-
{
|
|
769
|
-
processors.push(Filters.COLORMAP(colourmap, filters.colourmapCenter ?? 128));
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
if (filters.pseudoColourEnabled)
|
|
774
|
-
{
|
|
775
|
-
processors.push(Filters.PSEUDOCOLOR(filters.pseudoColourMode || "", filters.pseudoColourRed ?? 1,
|
|
776
|
-
filters.pseudoColourGreen ?? 1, filters.pseudoColourBlue ?? 1));
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
if (filters.globalPcaEnabled)
|
|
780
|
-
{
|
|
781
|
-
processors.push(Filters.GLOBAL_PCA_COLOR(filters.pcaMode || "", filters.pcaHue ?? 0));
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
if (filters.colourReplaceEnabled)
|
|
785
|
-
{
|
|
786
|
-
processors.push(Filters.COLOR_REPLACE(filters.colourReplaceSource || "#ffffff",
|
|
787
|
-
filters.colourReplaceTarget || "#ffffff",
|
|
788
|
-
filters.colourReplaceTolerance ?? 24, filters.colourReplaceBlend ?? 1,
|
|
789
|
-
filters.colourReplacePreserveLum ?? false));
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
appendMappedProcessors(processors, filters, altFilterMappings);
|
|
793
|
-
|
|
794
|
-
if (processors.length === 0)
|
|
795
|
-
{
|
|
796
|
-
return {filters : [], loadMode : "sync"};
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
return {filters : {processors}, loadMode : "sync"};
|
|
800
|
-
};
|
|
801
|
-
|
|
802
|
-
(window as any).Diva = Diva;
|