retold-remote 0.0.23 → 0.0.26
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/css/retold-remote.css +343 -20
- package/docs/.nojekyll +0 -0
- package/docs/README.md +64 -12
- package/docs/_cover.md +6 -6
- package/docs/_sidebar.md +2 -0
- package/docs/_topbar.md +1 -1
- package/docs/_version.json +7 -0
- package/docs/collections.md +30 -0
- package/docs/css/docuserve.css +327 -0
- package/docs/ebook-reader.md +75 -1
- package/docs/image-explorer.md +62 -2
- package/docs/index.html +39 -0
- package/docs/retold-catalog.json +254 -0
- package/docs/retold-keyword-index.json +31216 -0
- package/docs/server-setup.md +122 -91
- package/docs/stack-launcher.md +218 -0
- package/docs/synology.md +585 -0
- package/docs/ultravisor-configuration.md +5 -5
- package/docs/ultravisor-integration.md +4 -2
- package/package.json +20 -14
- package/source/Pict-Application-RetoldRemote.js +22 -0
- package/source/RetoldRemote-ExtensionMaps.js +1 -1
- package/source/cli/RetoldRemote-Server-Setup.js +460 -7
- package/source/cli/RetoldRemote-Stack-Launcher.js +563 -0
- package/source/cli/RetoldRemote-Stack-Run.js +41 -0
- package/source/cli/commands/RetoldRemote-Command-Serve.js +129 -54
- package/source/providers/CollectionManager-AddItems.js +166 -0
- package/source/providers/Pict-Provider-GalleryNavigation.js +55 -0
- package/source/providers/Pict-Provider-OperationStatus.js +597 -0
- package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +20 -1
- package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
- package/source/server/RetoldRemote-AudioWaveformService.js +49 -3
- package/source/server/RetoldRemote-CollectionExportService.js +763 -0
- package/source/server/RetoldRemote-CollectionService.js +5 -0
- package/source/server/RetoldRemote-EbookService.js +218 -3
- package/source/server/RetoldRemote-ImageService.js +221 -46
- package/source/server/RetoldRemote-MediaService.js +63 -4
- package/source/server/RetoldRemote-MetadataCache.js +25 -5
- package/source/server/RetoldRemote-OperationBroadcaster.js +363 -0
- package/source/server/RetoldRemote-SubimageService.js +680 -0
- package/source/server/RetoldRemote-ToolDetector.js +50 -0
- package/source/server/RetoldRemote-UltravisorBeacon.js +18 -3
- package/source/server/RetoldRemote-UltravisorDispatcher.js +65 -491
- package/source/server/RetoldRemote-UltravisorOperations.js +133 -20
- package/source/server/RetoldRemote-VideoFrameService.js +302 -9
- package/source/views/MediaViewer-EbookViewer.js +419 -1
- package/source/views/MediaViewer-PdfViewer.js +1050 -0
- package/source/views/PictView-Remote-AudioExplorer.js +77 -1
- package/source/views/PictView-Remote-CollectionsPanel.js +213 -0
- package/source/views/PictView-Remote-Gallery.js +365 -64
- package/source/views/PictView-Remote-ImageExplorer.js +1529 -44
- package/source/views/PictView-Remote-ImageViewer.js +2 -2
- package/source/views/PictView-Remote-Layout.js +58 -0
- package/source/views/PictView-Remote-MediaViewer.js +100 -25
- package/source/views/PictView-Remote-RegionsBrowser.js +554 -0
- package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
- package/source/views/PictView-Remote-TopBar.js +1 -0
- package/source/views/PictView-Remote-VideoExplorer.js +77 -1
- package/web-application/css/docuserve.css +277 -23
- package/web-application/css/retold-remote.css +343 -20
- package/web-application/docs/README.md +64 -12
- package/web-application/docs/_cover.md +6 -6
- package/web-application/docs/_sidebar.md +2 -0
- package/web-application/docs/_topbar.md +1 -1
- package/web-application/docs/collections.md +30 -0
- package/web-application/docs/ebook-reader.md +75 -1
- package/web-application/docs/image-explorer.md +62 -2
- package/web-application/docs/server-setup.md +122 -91
- package/web-application/docs/stack-launcher.md +218 -0
- package/web-application/docs/synology.md +585 -0
- package/web-application/docs/ultravisor-configuration.md +5 -5
- package/web-application/docs/ultravisor-integration.md +4 -2
- package/web-application/js/pict-docuserve.min.js +12 -12
- package/web-application/js/pict.min.js +2 -2
- package/web-application/js/pict.min.js.map +1 -1
- package/web-application/retold-remote.js +6596 -1784
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +75 -23
- package/web-application/retold-remote.min.js.map +1 -1
|
@@ -23,6 +23,28 @@ class RetoldRemoteImageExplorerView extends libPictView
|
|
|
23
23
|
this._dziData = null;
|
|
24
24
|
this._osdLoaded = false;
|
|
25
25
|
this._loading = false;
|
|
26
|
+
|
|
27
|
+
// Selection mode state
|
|
28
|
+
this._selectionMode = false;
|
|
29
|
+
this._selectionTracker = null;
|
|
30
|
+
this._selectionOverlay = null;
|
|
31
|
+
this._selectionRegion = null; // { X, Y, Width, Height } in image coords
|
|
32
|
+
this._selectionStart = null; // viewport point where drag began
|
|
33
|
+
this._selectionStartScreenPos = null; // screen-pixel position of press (for click-vs-drag filter)
|
|
34
|
+
this._savedRegions = []; // loaded from server
|
|
35
|
+
|
|
36
|
+
// Viewer-ready flag — set to true when the OSD viewer's 'open' event
|
|
37
|
+
// fires and the viewport coordinate math is safe to run. This MUST
|
|
38
|
+
// reset on every viewer destroy (including the DZI preview→tile swap)
|
|
39
|
+
// so the saved-region overlays get re-rendered against the new viewer.
|
|
40
|
+
this._viewerReady = false;
|
|
41
|
+
|
|
42
|
+
// Edit mode state (Part B)
|
|
43
|
+
this._editingRegionID = null;
|
|
44
|
+
this._editDragMode = null; // 'tl'|'tr'|'bl'|'br'|'t'|'r'|'b'|'l'|'body'|null
|
|
45
|
+
this._editDragStart = null; // viewport point where edit drag began
|
|
46
|
+
this._editOriginalRect = null; // OSD Rect captured at press time for delta math
|
|
47
|
+
this._editTracker = null;
|
|
26
48
|
}
|
|
27
49
|
|
|
28
50
|
/**
|
|
@@ -40,6 +62,36 @@ class RetoldRemoteImageExplorerView extends libPictView
|
|
|
40
62
|
this._dziData = null;
|
|
41
63
|
this._loading = false;
|
|
42
64
|
|
|
65
|
+
// Notify the layout so any active sidebar tab (Info, Regions, etc.)
|
|
66
|
+
// refreshes to this file instead of showing stale content from the
|
|
67
|
+
// previous file. See PictView-Remote-Layout.js#notifyCurrentFileChanged.
|
|
68
|
+
let tmpLayout = this.pict.views['ContentEditor-Layout'];
|
|
69
|
+
if (tmpLayout && typeof tmpLayout.notifyCurrentFileChanged === 'function')
|
|
70
|
+
{
|
|
71
|
+
tmpLayout.notifyCurrentFileChanged(pFilePath);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Reset selection state
|
|
75
|
+
this._selectionMode = false;
|
|
76
|
+
this._selectionTracker = null;
|
|
77
|
+
this._selectionOverlay = null;
|
|
78
|
+
this._selectionRegion = null;
|
|
79
|
+
this._selectionStart = null;
|
|
80
|
+
this._selectionStartScreenPos = null;
|
|
81
|
+
this._savedRegions = [];
|
|
82
|
+
this._viewerReady = false;
|
|
83
|
+
|
|
84
|
+
// Reset any in-progress edit mode (Part B)
|
|
85
|
+
this._editingRegionID = null;
|
|
86
|
+
this._editDragMode = null;
|
|
87
|
+
this._editDragStart = null;
|
|
88
|
+
this._editOriginalRect = null;
|
|
89
|
+
if (this._editTracker)
|
|
90
|
+
{
|
|
91
|
+
try { this._editTracker.destroy(); } catch (pErr) { /* ignore */ }
|
|
92
|
+
this._editTracker = null;
|
|
93
|
+
}
|
|
94
|
+
|
|
43
95
|
// Clean up existing viewer
|
|
44
96
|
if (this._osdViewer)
|
|
45
97
|
{
|
|
@@ -89,6 +141,7 @@ class RetoldRemoteImageExplorerView extends libPictView
|
|
|
89
141
|
tmpHTML += '<button class="retold-remote-iex-nav-btn" onclick="pict.views[\'RetoldRemote-ImageExplorer\'].goBack()" title="Back (Esc)">← Back</button>';
|
|
90
142
|
tmpHTML += '<div class="retold-remote-iex-title">Image Explorer — ' + tmpFmt.escapeHTML(tmpFileName) + '</div>';
|
|
91
143
|
tmpHTML += '<div class="retold-remote-iex-actions">';
|
|
144
|
+
tmpHTML += '<button class="retold-remote-iex-action-btn" id="RetoldRemote-IEX-SelectBtn" onclick="pict.views[\'RetoldRemote-ImageExplorer\'].toggleSelectionMode()" title="Select a region (s)">✂ Select</button>';
|
|
92
145
|
tmpHTML += '<button class="retold-remote-iex-action-btn" onclick="pict.views[\'RetoldRemote-ImageExplorer\'].viewInBrowser()" title="View in standard viewer">🖼 View</button>';
|
|
93
146
|
tmpHTML += '</div>';
|
|
94
147
|
tmpHTML += '</div>';
|
|
@@ -111,6 +164,11 @@ class RetoldRemoteImageExplorerView extends libPictView
|
|
|
111
164
|
tmpHTML += '<button onclick="pict.views[\'RetoldRemote-ImageExplorer\'].zoomOut()" title="Zoom Out (-)">- Zoom Out</button>';
|
|
112
165
|
tmpHTML += '<button onclick="pict.views[\'RetoldRemote-ImageExplorer\'].zoomHome()" title="Fit to view (0)">Fit</button>';
|
|
113
166
|
tmpHTML += '<span style="flex:1;"></span>';
|
|
167
|
+
tmpHTML += '<span id="RetoldRemote-IEX-LabelInput" style="display:none;">';
|
|
168
|
+
tmpHTML += '<input type="text" id="RetoldRemote-IEX-LabelField" placeholder="Label this region\u2026" style="background:var(--retold-bg-input,#1e1e1e);color:var(--retold-text,#abb2bf);border:1px solid var(--retold-border,#3e4451);border-radius:4px;padding:2px 8px;font-size:0.78rem;width:180px;margin-right:4px;" onkeydown="if(event.key===\'Enter\'){pict.views[\'RetoldRemote-ImageExplorer\'].saveSelectionLabel();event.preventDefault();event.stopPropagation();}if(event.key===\'Escape\'){pict.views[\'RetoldRemote-ImageExplorer\'].cancelSelection();event.preventDefault();event.stopPropagation();}">';
|
|
169
|
+
tmpHTML += '<button onclick="pict.views[\'RetoldRemote-ImageExplorer\'].saveSelectionLabel()" style="font-size:0.75rem;padding:2px 8px;">Save</button>';
|
|
170
|
+
tmpHTML += '<button onclick="pict.views[\'RetoldRemote-ImageExplorer\'].cancelSelection()" style="font-size:0.75rem;padding:2px 8px;margin-left:2px;">Cancel</button>';
|
|
171
|
+
tmpHTML += '</span>';
|
|
114
172
|
tmpHTML += '<span id="RetoldRemote-IEX-Coords" style="color:var(--retold-text-dim);font-size:0.72rem;"></span>';
|
|
115
173
|
tmpHTML += '</div>';
|
|
116
174
|
|
|
@@ -129,9 +187,11 @@ class RetoldRemoteImageExplorerView extends libPictView
|
|
|
129
187
|
}
|
|
130
188
|
|
|
131
189
|
// Load OpenSeadragon, then decide whether to use simple image or DZI tiles
|
|
190
|
+
let tmpSelfShow = this;
|
|
132
191
|
this._ensureOSDLoaded(() =>
|
|
133
192
|
{
|
|
134
|
-
|
|
193
|
+
tmpSelfShow._probeAndShow(pFilePath);
|
|
194
|
+
tmpSelfShow._loadSavedRegions(pFilePath);
|
|
135
195
|
});
|
|
136
196
|
}
|
|
137
197
|
|
|
@@ -392,6 +452,15 @@ class RetoldRemoteImageExplorerView extends libPictView
|
|
|
392
452
|
}
|
|
393
453
|
}
|
|
394
454
|
tmpSelf._updateZoomLabel();
|
|
455
|
+
|
|
456
|
+
// Viewer is ready — mark the flag and re-render any saved regions
|
|
457
|
+
// that were loaded before the viewer was open. This is the single
|
|
458
|
+
// source of truth for "overlays render when ready" (see Part A).
|
|
459
|
+
tmpSelf._viewerReady = true;
|
|
460
|
+
if (tmpSelf._savedRegions && tmpSelf._savedRegions.length > 0)
|
|
461
|
+
{
|
|
462
|
+
tmpSelf._renderSavedRegionOverlays();
|
|
463
|
+
}
|
|
395
464
|
});
|
|
396
465
|
|
|
397
466
|
if (typeof OpenSeadragon.MouseTracker !== 'undefined')
|
|
@@ -435,8 +504,35 @@ class RetoldRemoteImageExplorerView extends libPictView
|
|
|
435
504
|
this._initSimpleViewer(null, tmpPreviewURL);
|
|
436
505
|
}
|
|
437
506
|
|
|
507
|
+
// Cancel any previous DZI fetch (fast navigation between big images)
|
|
508
|
+
this._cancelActiveDziOperation();
|
|
509
|
+
|
|
510
|
+
// Start operation tracking for the DZI generation
|
|
511
|
+
let tmpStatus = this.pict.providers['RetoldRemote-OperationStatus'];
|
|
512
|
+
let tmpOp = tmpStatus ? tmpStatus.startOperation(
|
|
513
|
+
{
|
|
514
|
+
Label: 'Generating deep-zoom tiles',
|
|
515
|
+
Phase: 'Reading image…',
|
|
516
|
+
Cancelable: true
|
|
517
|
+
}) : null;
|
|
518
|
+
if (tmpOp)
|
|
519
|
+
{
|
|
520
|
+
this._activeDziOperationId = tmpOp.OperationId;
|
|
521
|
+
this._activeDziAbortController = tmpOp.AbortController;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
let tmpFetchOptions = {};
|
|
525
|
+
if (tmpOp && tmpOp.AbortController)
|
|
526
|
+
{
|
|
527
|
+
tmpFetchOptions.signal = tmpOp.AbortController.signal;
|
|
528
|
+
}
|
|
529
|
+
if (tmpOp)
|
|
530
|
+
{
|
|
531
|
+
tmpFetchOptions.headers = { 'X-Op-Id': tmpOp.OperationId };
|
|
532
|
+
}
|
|
533
|
+
|
|
438
534
|
// 2. Generate DZI tiles in the background
|
|
439
|
-
fetch('/api/media/dzi?path=' + pPathParam)
|
|
535
|
+
fetch('/api/media/dzi?path=' + pPathParam, tmpFetchOptions)
|
|
440
536
|
.then((pResponse) => pResponse.json())
|
|
441
537
|
.then((pResult) =>
|
|
442
538
|
{
|
|
@@ -444,6 +540,10 @@ class RetoldRemoteImageExplorerView extends libPictView
|
|
|
444
540
|
|
|
445
541
|
if (!pResult || !pResult.Success)
|
|
446
542
|
{
|
|
543
|
+
if (tmpOp && tmpStatus)
|
|
544
|
+
{
|
|
545
|
+
tmpStatus.errorOperation(tmpOp.OperationId, { message: (pResult && pResult.Error) || 'DZI generation failed' });
|
|
546
|
+
}
|
|
447
547
|
// DZI generation failed — the preview is already showing
|
|
448
548
|
if (!tmpPreviewURL)
|
|
449
549
|
{
|
|
@@ -452,16 +552,29 @@ class RetoldRemoteImageExplorerView extends libPictView
|
|
|
452
552
|
return;
|
|
453
553
|
}
|
|
454
554
|
|
|
555
|
+
if (tmpOp && tmpStatus)
|
|
556
|
+
{
|
|
557
|
+
tmpStatus.completeOperation(tmpOp.OperationId);
|
|
558
|
+
}
|
|
559
|
+
tmpSelf._activeDziOperationId = null;
|
|
560
|
+
tmpSelf._activeDziAbortController = null;
|
|
561
|
+
|
|
455
562
|
// 3. Swap the preview for the full DZI tile viewer
|
|
456
563
|
tmpSelf._dziData = pResult;
|
|
457
564
|
tmpSelf._showInfo(pResult);
|
|
458
565
|
|
|
459
|
-
// Destroy the preview viewer and replace with tile viewer
|
|
566
|
+
// Destroy the preview viewer and replace with tile viewer.
|
|
567
|
+
// Reset _viewerReady so the new viewer's open handler
|
|
568
|
+
// re-renders saved overlays against the new viewer instance.
|
|
569
|
+
// Any overlays attached to the preview viewer are gone once
|
|
570
|
+
// it's destroyed, so we MUST re-render from _savedRegions
|
|
571
|
+
// against the new viewer.
|
|
460
572
|
if (tmpSelf._osdViewer)
|
|
461
573
|
{
|
|
462
574
|
try { tmpSelf._osdViewer.destroy(); } catch (e) { /* ignore */ }
|
|
463
575
|
tmpSelf._osdViewer = null;
|
|
464
576
|
}
|
|
577
|
+
tmpSelf._viewerReady = false;
|
|
465
578
|
|
|
466
579
|
// Clear the viewer div for fresh OSD init
|
|
467
580
|
let tmpViewerDiv = document.getElementById('RetoldRemote-IEX-Viewer');
|
|
@@ -475,6 +588,14 @@ class RetoldRemoteImageExplorerView extends libPictView
|
|
|
475
588
|
.catch((pError) =>
|
|
476
589
|
{
|
|
477
590
|
tmpSelf._loading = false;
|
|
591
|
+
if (pError && pError.name === 'AbortError')
|
|
592
|
+
{
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (tmpOp && tmpStatus)
|
|
596
|
+
{
|
|
597
|
+
tmpStatus.errorOperation(tmpOp.OperationId, pError);
|
|
598
|
+
}
|
|
478
599
|
// Tiles failed — the preview is already showing, leave it
|
|
479
600
|
if (!tmpPreviewURL)
|
|
480
601
|
{
|
|
@@ -483,6 +604,25 @@ class RetoldRemoteImageExplorerView extends libPictView
|
|
|
483
604
|
});
|
|
484
605
|
}
|
|
485
606
|
|
|
607
|
+
/**
|
|
608
|
+
* Cancel any in-flight DZI generation. Called on navigate-away and
|
|
609
|
+
* when launching a new explorer session.
|
|
610
|
+
*/
|
|
611
|
+
_cancelActiveDziOperation()
|
|
612
|
+
{
|
|
613
|
+
if (this._activeDziAbortController)
|
|
614
|
+
{
|
|
615
|
+
try { this._activeDziAbortController.abort(); } catch (pErr) { /* ignore */ }
|
|
616
|
+
}
|
|
617
|
+
let tmpStatus = this.pict.providers['RetoldRemote-OperationStatus'];
|
|
618
|
+
if (this._activeDziOperationId && tmpStatus)
|
|
619
|
+
{
|
|
620
|
+
tmpStatus.cancelOperation(this._activeDziOperationId);
|
|
621
|
+
}
|
|
622
|
+
this._activeDziOperationId = null;
|
|
623
|
+
this._activeDziAbortController = null;
|
|
624
|
+
}
|
|
625
|
+
|
|
486
626
|
/**
|
|
487
627
|
* Show the info bar while the preview is displayed and tiles are generating.
|
|
488
628
|
*
|
|
@@ -623,6 +763,15 @@ class RetoldRemoteImageExplorerView extends libPictView
|
|
|
623
763
|
this._osdViewer.addHandler('open', function ()
|
|
624
764
|
{
|
|
625
765
|
tmpSelf._updateZoomLabel();
|
|
766
|
+
|
|
767
|
+
// Viewer is ready — mark the flag and re-render any saved regions
|
|
768
|
+
// that may have loaded before the tile viewer finished opening
|
|
769
|
+
// (or before the preview→tile swap completed). See Part A notes.
|
|
770
|
+
tmpSelf._viewerReady = true;
|
|
771
|
+
if (tmpSelf._savedRegions && tmpSelf._savedRegions.length > 0)
|
|
772
|
+
{
|
|
773
|
+
tmpSelf._renderSavedRegionOverlays();
|
|
774
|
+
}
|
|
626
775
|
});
|
|
627
776
|
|
|
628
777
|
// Mouse tracker for coordinates
|
|
@@ -736,80 +885,1416 @@ class RetoldRemoteImageExplorerView extends libPictView
|
|
|
736
885
|
}
|
|
737
886
|
}
|
|
738
887
|
|
|
888
|
+
// -----------------------------------------------------------------
|
|
889
|
+
// Selection mode — draw rectangles to create labeled subimage regions
|
|
890
|
+
// -----------------------------------------------------------------
|
|
891
|
+
|
|
739
892
|
/**
|
|
740
|
-
*
|
|
893
|
+
* Toggle selection mode on/off.
|
|
741
894
|
*/
|
|
742
|
-
|
|
895
|
+
toggleSelectionMode()
|
|
743
896
|
{
|
|
744
|
-
|
|
745
|
-
if (this._osdViewer)
|
|
897
|
+
if (this._selectionMode)
|
|
746
898
|
{
|
|
747
|
-
|
|
899
|
+
this._exitSelectionMode();
|
|
900
|
+
}
|
|
901
|
+
else
|
|
902
|
+
{
|
|
903
|
+
this._enterSelectionMode();
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Enter selection mode: disable panning, install drag tracker.
|
|
909
|
+
*/
|
|
910
|
+
_enterSelectionMode()
|
|
911
|
+
{
|
|
912
|
+
if (!this._osdViewer)
|
|
913
|
+
{
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
this._selectionMode = true;
|
|
918
|
+
|
|
919
|
+
// Highlight the Select button
|
|
920
|
+
let tmpBtn = document.getElementById('RetoldRemote-IEX-SelectBtn');
|
|
921
|
+
if (tmpBtn)
|
|
922
|
+
{
|
|
923
|
+
tmpBtn.style.background = 'rgba(97, 175, 239, 0.4)';
|
|
924
|
+
tmpBtn.style.color = '#fff';
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Disable OSD panning so drag draws a selection instead
|
|
928
|
+
this._osdViewer.setMouseNavEnabled(false);
|
|
929
|
+
|
|
930
|
+
let tmpSelf = this;
|
|
931
|
+
let tmpViewerDiv = document.getElementById('RetoldRemote-IEX-Viewer');
|
|
932
|
+
if (!tmpViewerDiv)
|
|
933
|
+
{
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
tmpViewerDiv.style.cursor = 'crosshair';
|
|
937
|
+
|
|
938
|
+
this._selectionTracker = new OpenSeadragon.MouseTracker(
|
|
939
|
+
{
|
|
940
|
+
element: tmpViewerDiv,
|
|
941
|
+
pressHandler: function (pEvent)
|
|
748
942
|
{
|
|
749
|
-
|
|
750
|
-
}
|
|
751
|
-
|
|
943
|
+
tmpSelf._onSelectionPress(pEvent);
|
|
944
|
+
},
|
|
945
|
+
dragHandler: function (pEvent)
|
|
752
946
|
{
|
|
753
|
-
|
|
947
|
+
tmpSelf._onSelectionDrag(pEvent);
|
|
948
|
+
},
|
|
949
|
+
releaseHandler: function (pEvent)
|
|
950
|
+
{
|
|
951
|
+
tmpSelf._onSelectionRelease(pEvent);
|
|
754
952
|
}
|
|
755
|
-
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Exit selection mode: re-enable panning, remove drag tracker.
|
|
958
|
+
*/
|
|
959
|
+
_exitSelectionMode()
|
|
960
|
+
{
|
|
961
|
+
this._selectionMode = false;
|
|
962
|
+
|
|
963
|
+
let tmpBtn = document.getElementById('RetoldRemote-IEX-SelectBtn');
|
|
964
|
+
if (tmpBtn)
|
|
965
|
+
{
|
|
966
|
+
tmpBtn.style.background = '';
|
|
967
|
+
tmpBtn.style.color = '';
|
|
756
968
|
}
|
|
757
969
|
|
|
758
|
-
|
|
759
|
-
if (tmpNav)
|
|
970
|
+
if (this._osdViewer)
|
|
760
971
|
{
|
|
761
|
-
|
|
972
|
+
this._osdViewer.setMouseNavEnabled(true);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (this._selectionTracker)
|
|
976
|
+
{
|
|
977
|
+
this._selectionTracker.destroy();
|
|
978
|
+
this._selectionTracker = null;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
let tmpViewerDiv = document.getElementById('RetoldRemote-IEX-Viewer');
|
|
982
|
+
if (tmpViewerDiv)
|
|
983
|
+
{
|
|
984
|
+
tmpViewerDiv.style.cursor = '';
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Remove in-progress selection overlay (keep saved ones)
|
|
988
|
+
this._removeActiveSelectionOverlay();
|
|
989
|
+
this._selectionRegion = null;
|
|
990
|
+
this._selectionStart = null;
|
|
991
|
+
this._selectionStartScreenPos = null;
|
|
992
|
+
|
|
993
|
+
// Hide label input
|
|
994
|
+
let tmpLabelWrap = document.getElementById('RetoldRemote-IEX-LabelInput');
|
|
995
|
+
if (tmpLabelWrap)
|
|
996
|
+
{
|
|
997
|
+
tmpLabelWrap.style.display = 'none';
|
|
998
|
+
}
|
|
999
|
+
let tmpCoords = document.getElementById('RetoldRemote-IEX-Coords');
|
|
1000
|
+
if (tmpCoords)
|
|
1001
|
+
{
|
|
1002
|
+
tmpCoords.style.display = '';
|
|
762
1003
|
}
|
|
763
1004
|
}
|
|
764
1005
|
|
|
765
1006
|
/**
|
|
766
|
-
*
|
|
1007
|
+
* Handle the start of a selection drag.
|
|
767
1008
|
*/
|
|
768
|
-
|
|
1009
|
+
_onSelectionPress(pEvent)
|
|
769
1010
|
{
|
|
770
|
-
|
|
771
|
-
if (this._osdViewer)
|
|
1011
|
+
if (!this._osdViewer)
|
|
772
1012
|
{
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Remove any previous in-progress selection overlay
|
|
1017
|
+
this._removeActiveSelectionOverlay();
|
|
1018
|
+
|
|
1019
|
+
// Capture BOTH the screen-pixel position (for the click-vs-drag
|
|
1020
|
+
// test on release) and the viewport-space point (for the overlay
|
|
1021
|
+
// math during the drag). We need the screen-pixel reference to
|
|
1022
|
+
// filter out accidental clicks — on a high-res image the viewport
|
|
1023
|
+
// pixel math would mis-classify sensor-jitter as a real drag.
|
|
1024
|
+
this._selectionStartScreenPos = { x: pEvent.position.x, y: pEvent.position.y };
|
|
1025
|
+
this._selectionStart = this._osdViewer.viewport.pointFromPixel(pEvent.position);
|
|
1026
|
+
|
|
1027
|
+
// Create the selection rectangle overlay element
|
|
1028
|
+
let tmpOverlay = document.createElement('div');
|
|
1029
|
+
tmpOverlay.id = 'RetoldRemote-IEX-ActiveSelection';
|
|
1030
|
+
tmpOverlay.style.cssText = 'border: 2px solid rgba(97, 175, 239, 0.9); background: rgba(97, 175, 239, 0.15); pointer-events: none;';
|
|
1031
|
+
this._selectionOverlay = tmpOverlay;
|
|
1032
|
+
|
|
1033
|
+
// Add the overlay at zero size, will expand during drag
|
|
1034
|
+
this._osdViewer.addOverlay(
|
|
1035
|
+
{
|
|
1036
|
+
element: tmpOverlay,
|
|
1037
|
+
location: new OpenSeadragon.Rect(
|
|
1038
|
+
this._selectionStart.x, this._selectionStart.y, 0, 0)
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Handle selection dragging — update the rectangle size.
|
|
1044
|
+
*/
|
|
1045
|
+
_onSelectionDrag(pEvent)
|
|
1046
|
+
{
|
|
1047
|
+
if (!this._osdViewer || !this._selectionStart || !this._selectionOverlay)
|
|
1048
|
+
{
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
let tmpCurrent = this._osdViewer.viewport.pointFromPixel(pEvent.position);
|
|
1053
|
+
let tmpX = Math.min(this._selectionStart.x, tmpCurrent.x);
|
|
1054
|
+
let tmpY = Math.min(this._selectionStart.y, tmpCurrent.y);
|
|
1055
|
+
let tmpW = Math.abs(tmpCurrent.x - this._selectionStart.x);
|
|
1056
|
+
let tmpH = Math.abs(tmpCurrent.y - this._selectionStart.y);
|
|
1057
|
+
|
|
1058
|
+
this._osdViewer.updateOverlay(
|
|
1059
|
+
this._selectionOverlay,
|
|
1060
|
+
new OpenSeadragon.Rect(tmpX, tmpY, tmpW, tmpH));
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Handle selection release — compute image-coordinate region and show label input.
|
|
1065
|
+
*/
|
|
1066
|
+
_onSelectionRelease(pEvent)
|
|
1067
|
+
{
|
|
1068
|
+
if (!this._osdViewer || !this._selectionStart)
|
|
1069
|
+
{
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Screen-pixel click-vs-drag filter. If the mouse moved less than
|
|
1074
|
+
// CLICK_THRESHOLD screen pixels between press and release, treat
|
|
1075
|
+
// this as a click (not a selection) and bail. This MUST be checked
|
|
1076
|
+
// in screen pixels, not image pixels, because on a high-resolution
|
|
1077
|
+
// image a 1-screen-pixel jitter can translate to 6+ image pixels,
|
|
1078
|
+
// which would otherwise slip past the image-pixel min-size guard
|
|
1079
|
+
// below and pop the label dialog on a single click.
|
|
1080
|
+
const CLICK_THRESHOLD = 5;
|
|
1081
|
+
if (this._selectionStartScreenPos)
|
|
1082
|
+
{
|
|
1083
|
+
let tmpDx = Math.abs(pEvent.position.x - this._selectionStartScreenPos.x);
|
|
1084
|
+
let tmpDy = Math.abs(pEvent.position.y - this._selectionStartScreenPos.y);
|
|
1085
|
+
if (tmpDx < CLICK_THRESHOLD && tmpDy < CLICK_THRESHOLD)
|
|
778
1086
|
{
|
|
779
|
-
|
|
1087
|
+
this._removeActiveSelectionOverlay();
|
|
1088
|
+
this._selectionStart = null;
|
|
1089
|
+
this._selectionStartScreenPos = null;
|
|
1090
|
+
return;
|
|
780
1091
|
}
|
|
781
|
-
this._osdViewer = null;
|
|
782
1092
|
}
|
|
783
1093
|
|
|
784
|
-
let
|
|
785
|
-
|
|
1094
|
+
let tmpEnd = this._osdViewer.viewport.pointFromPixel(pEvent.position);
|
|
1095
|
+
|
|
1096
|
+
// Convert viewport rectangle to image pixel coordinates
|
|
1097
|
+
let tmpVpX = Math.min(this._selectionStart.x, tmpEnd.x);
|
|
1098
|
+
let tmpVpY = Math.min(this._selectionStart.y, tmpEnd.y);
|
|
1099
|
+
let tmpVpW = Math.abs(tmpEnd.x - this._selectionStart.x);
|
|
1100
|
+
let tmpVpH = Math.abs(tmpEnd.y - this._selectionStart.y);
|
|
1101
|
+
|
|
1102
|
+
let tmpTopLeft = this._osdViewer.viewport.viewportToImageCoordinates(
|
|
1103
|
+
new OpenSeadragon.Point(tmpVpX, tmpVpY));
|
|
1104
|
+
let tmpBottomRight = this._osdViewer.viewport.viewportToImageCoordinates(
|
|
1105
|
+
new OpenSeadragon.Point(tmpVpX + tmpVpW, tmpVpY + tmpVpH));
|
|
1106
|
+
|
|
1107
|
+
let tmpRegion =
|
|
786
1108
|
{
|
|
787
|
-
|
|
1109
|
+
X: Math.max(0, Math.round(tmpTopLeft.x)),
|
|
1110
|
+
Y: Math.max(0, Math.round(tmpTopLeft.y)),
|
|
1111
|
+
Width: Math.round(tmpBottomRight.x - tmpTopLeft.x),
|
|
1112
|
+
Height: Math.round(tmpBottomRight.y - tmpTopLeft.y)
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
// Clamp to image dimensions
|
|
1116
|
+
this._clampRegionToImage(tmpRegion);
|
|
1117
|
+
|
|
1118
|
+
// Defensive net: ignore tiny selections in image pixels. The
|
|
1119
|
+
// screen-pixel CLICK_THRESHOLD guard above catches accidental
|
|
1120
|
+
// clicks; this check covers the remaining case of a drag that's
|
|
1121
|
+
// large in screen pixels but degenerate in image pixels (e.g.
|
|
1122
|
+
// the user somehow ended up dragging entirely outside the image
|
|
1123
|
+
// bounds so everything got clamped away).
|
|
1124
|
+
if (tmpRegion.Width < 5 || tmpRegion.Height < 5)
|
|
1125
|
+
{
|
|
1126
|
+
this._removeActiveSelectionOverlay();
|
|
1127
|
+
this._selectionStart = null;
|
|
1128
|
+
this._selectionStartScreenPos = null;
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
this._selectionRegion = tmpRegion;
|
|
1133
|
+
|
|
1134
|
+
// Show the inline label input in the controls bar
|
|
1135
|
+
let tmpLabelWrap = document.getElementById('RetoldRemote-IEX-LabelInput');
|
|
1136
|
+
let tmpCoords = document.getElementById('RetoldRemote-IEX-Coords');
|
|
1137
|
+
if (tmpLabelWrap)
|
|
1138
|
+
{
|
|
1139
|
+
tmpLabelWrap.style.display = '';
|
|
1140
|
+
}
|
|
1141
|
+
if (tmpCoords)
|
|
1142
|
+
{
|
|
1143
|
+
tmpCoords.style.display = 'none';
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Focus the label field
|
|
1147
|
+
let tmpField = document.getElementById('RetoldRemote-IEX-LabelField');
|
|
1148
|
+
if (tmpField)
|
|
1149
|
+
{
|
|
1150
|
+
tmpField.value = '';
|
|
1151
|
+
tmpField.focus();
|
|
788
1152
|
}
|
|
789
1153
|
}
|
|
790
1154
|
|
|
791
1155
|
/**
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
*
|
|
1156
|
+
* Save a NEW selection (drawn by the user) with the entered label.
|
|
1157
|
+
* Renamed from saveSelectionLabel so the public saveSelectionLabel()
|
|
1158
|
+
* can dispatch to either this or the edit-mode PUT helper based on
|
|
1159
|
+
* whether _editingRegionID is set.
|
|
795
1160
|
*/
|
|
796
|
-
|
|
1161
|
+
_saveNewRegionLabel()
|
|
797
1162
|
{
|
|
798
|
-
|
|
799
|
-
if (tmpLoading)
|
|
1163
|
+
if (!this._selectionRegion)
|
|
800
1164
|
{
|
|
801
|
-
|
|
1165
|
+
return;
|
|
802
1166
|
}
|
|
803
1167
|
|
|
804
|
-
let
|
|
805
|
-
|
|
1168
|
+
let tmpField = document.getElementById('RetoldRemote-IEX-LabelField');
|
|
1169
|
+
let tmpLabel = tmpField ? tmpField.value.trim() : '';
|
|
1170
|
+
|
|
1171
|
+
let tmpSelf = this;
|
|
1172
|
+
let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
|
|
1173
|
+
let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(this._currentPath) : encodeURIComponent(this._currentPath);
|
|
1174
|
+
|
|
1175
|
+
fetch('/api/media/subimage-regions',
|
|
806
1176
|
{
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1177
|
+
method: 'POST',
|
|
1178
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1179
|
+
body: JSON.stringify(
|
|
1180
|
+
{
|
|
1181
|
+
Path: this._currentPath,
|
|
1182
|
+
Region:
|
|
1183
|
+
{
|
|
1184
|
+
Label: tmpLabel,
|
|
1185
|
+
X: this._selectionRegion.X,
|
|
1186
|
+
Y: this._selectionRegion.Y,
|
|
1187
|
+
Width: this._selectionRegion.Width,
|
|
1188
|
+
Height: this._selectionRegion.Height
|
|
1189
|
+
}
|
|
1190
|
+
})
|
|
1191
|
+
})
|
|
1192
|
+
.then((pResponse) => pResponse.json())
|
|
1193
|
+
.then((pResult) =>
|
|
1194
|
+
{
|
|
1195
|
+
if (pResult && pResult.Success)
|
|
1196
|
+
{
|
|
1197
|
+
tmpSelf._savedRegions = pResult.Regions || [];
|
|
1198
|
+
|
|
1199
|
+
// Remove the active selection overlay and render persistent ones
|
|
1200
|
+
tmpSelf._removeActiveSelectionOverlay();
|
|
1201
|
+
tmpSelf._renderSavedRegionOverlays();
|
|
1202
|
+
|
|
1203
|
+
// Notify the user
|
|
1204
|
+
let tmpToast = tmpSelf.pict.providers['RetoldRemote-ToastNotification'];
|
|
1205
|
+
if (tmpToast)
|
|
1206
|
+
{
|
|
1207
|
+
tmpToast.showToast('Subimage region saved' + (tmpLabel ? ': ' + tmpLabel : ''));
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Update the sidebar panel if visible
|
|
1211
|
+
let tmpSubPanel = tmpSelf.pict.views['RetoldRemote-SubimagesPanel'];
|
|
1212
|
+
if (tmpSubPanel)
|
|
1213
|
+
{
|
|
1214
|
+
tmpSubPanel.render();
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
})
|
|
1218
|
+
.catch((pErr) =>
|
|
1219
|
+
{
|
|
1220
|
+
let tmpToast = tmpSelf.pict.providers['RetoldRemote-ToastNotification'];
|
|
1221
|
+
if (tmpToast)
|
|
1222
|
+
{
|
|
1223
|
+
tmpToast.showToast('Failed to save region: ' + pErr.message);
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
// Hide label input, show coords
|
|
1228
|
+
this._selectionRegion = null;
|
|
1229
|
+
this._selectionStart = null;
|
|
1230
|
+
this._selectionStartScreenPos = null;
|
|
1231
|
+
|
|
1232
|
+
let tmpLabelWrap = document.getElementById('RetoldRemote-IEX-LabelInput');
|
|
1233
|
+
if (tmpLabelWrap)
|
|
1234
|
+
{
|
|
1235
|
+
tmpLabelWrap.style.display = 'none';
|
|
1236
|
+
}
|
|
1237
|
+
let tmpCoords = document.getElementById('RetoldRemote-IEX-Coords');
|
|
1238
|
+
if (tmpCoords)
|
|
1239
|
+
{
|
|
1240
|
+
tmpCoords.style.display = '';
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* Cancel a new in-progress selection. Renamed from cancelSelection
|
|
1246
|
+
* so the public cancelSelection() can dispatch to either this or
|
|
1247
|
+
* _exitRegionEditMode based on state.
|
|
1248
|
+
*/
|
|
1249
|
+
_cancelNewSelection()
|
|
1250
|
+
{
|
|
1251
|
+
this._removeActiveSelectionOverlay();
|
|
1252
|
+
this._selectionRegion = null;
|
|
1253
|
+
this._selectionStart = null;
|
|
1254
|
+
this._selectionStartScreenPos = null;
|
|
1255
|
+
|
|
1256
|
+
let tmpLabelWrap = document.getElementById('RetoldRemote-IEX-LabelInput');
|
|
1257
|
+
if (tmpLabelWrap)
|
|
1258
|
+
{
|
|
1259
|
+
tmpLabelWrap.style.display = 'none';
|
|
1260
|
+
}
|
|
1261
|
+
let tmpCoords = document.getElementById('RetoldRemote-IEX-Coords');
|
|
1262
|
+
if (tmpCoords)
|
|
1263
|
+
{
|
|
1264
|
+
tmpCoords.style.display = '';
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Remove the active (in-progress) selection overlay.
|
|
1270
|
+
*/
|
|
1271
|
+
_removeActiveSelectionOverlay()
|
|
1272
|
+
{
|
|
1273
|
+
let tmpActive = document.getElementById('RetoldRemote-IEX-ActiveSelection');
|
|
1274
|
+
if (tmpActive && this._osdViewer)
|
|
1275
|
+
{
|
|
1276
|
+
try
|
|
1277
|
+
{
|
|
1278
|
+
this._osdViewer.removeOverlay(tmpActive);
|
|
1279
|
+
}
|
|
1280
|
+
catch (pErr)
|
|
1281
|
+
{
|
|
1282
|
+
// May not be an overlay; just remove from DOM
|
|
1283
|
+
if (tmpActive.parentElement)
|
|
1284
|
+
{
|
|
1285
|
+
tmpActive.parentElement.removeChild(tmpActive);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
this._selectionOverlay = null;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// -----------------------------------------------------------------
|
|
1293
|
+
// Saved region overlays
|
|
1294
|
+
// -----------------------------------------------------------------
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Load saved subimage regions from the server.
|
|
1298
|
+
*
|
|
1299
|
+
* Stores the regions in `_savedRegions` and triggers a render IF the
|
|
1300
|
+
* viewer is already open. Otherwise the open handler will call the
|
|
1301
|
+
* render once it fires — this is the single source of truth for
|
|
1302
|
+
* "render when ready", avoiding the race between the regions fetch
|
|
1303
|
+
* and the OSD 'open' event.
|
|
1304
|
+
*
|
|
1305
|
+
* Captures pFilePath in closure so a stale async response from an
|
|
1306
|
+
* earlier file doesn't overwrite the regions of a later-opened file
|
|
1307
|
+
* during rapid navigation.
|
|
1308
|
+
*
|
|
1309
|
+
* @param {string} pFilePath - Relative file path
|
|
1310
|
+
*/
|
|
1311
|
+
_loadSavedRegions(pFilePath)
|
|
1312
|
+
{
|
|
1313
|
+
let tmpSelf = this;
|
|
1314
|
+
let tmpRequestedPath = pFilePath;
|
|
1315
|
+
let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
|
|
1316
|
+
let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(pFilePath) : encodeURIComponent(pFilePath);
|
|
1317
|
+
|
|
1318
|
+
fetch('/api/media/subimage-regions?path=' + tmpPathParam)
|
|
1319
|
+
.then((pResponse) => pResponse.json())
|
|
1320
|
+
.then((pResult) =>
|
|
1321
|
+
{
|
|
1322
|
+
// Abort if the user has already navigated to a different file.
|
|
1323
|
+
if (tmpSelf._currentPath !== tmpRequestedPath)
|
|
1324
|
+
{
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (pResult && pResult.Success && Array.isArray(pResult.Regions))
|
|
1328
|
+
{
|
|
1329
|
+
tmpSelf._savedRegions = pResult.Regions;
|
|
1330
|
+
if (tmpSelf._viewerReady)
|
|
1331
|
+
{
|
|
1332
|
+
tmpSelf._renderSavedRegionOverlays();
|
|
1333
|
+
}
|
|
1334
|
+
// If not ready yet, the viewer's 'open' handler will
|
|
1335
|
+
// detect _savedRegions.length > 0 and render.
|
|
1336
|
+
}
|
|
1337
|
+
})
|
|
1338
|
+
.catch(() =>
|
|
1339
|
+
{
|
|
1340
|
+
// Silently ignore — regions are optional
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* Render all saved regions as OSD overlays with colored borders and labels.
|
|
1346
|
+
*
|
|
1347
|
+
* Idempotent: safely removes any existing saved-region overlays (scoped
|
|
1348
|
+
* to the current viewer's container to avoid clobbering overlays in
|
|
1349
|
+
* other viewer instances during rapid swaps) before re-adding each
|
|
1350
|
+
* region. Can be called any number of times after the viewer's 'open'
|
|
1351
|
+
* event has fired.
|
|
1352
|
+
*/
|
|
1353
|
+
_renderSavedRegionOverlays()
|
|
1354
|
+
{
|
|
1355
|
+
if (!this._osdViewer)
|
|
1356
|
+
{
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// Remove existing saved-region overlays scoped to this viewer's
|
|
1361
|
+
// container (NOT the whole document — during the DZI preview→tile
|
|
1362
|
+
// swap two viewer instances may briefly coexist).
|
|
1363
|
+
let tmpViewerDiv = document.getElementById('RetoldRemote-IEX-Viewer');
|
|
1364
|
+
if (tmpViewerDiv)
|
|
1365
|
+
{
|
|
1366
|
+
let tmpExisting = tmpViewerDiv.querySelectorAll('.retold-remote-iex-region-overlay');
|
|
1367
|
+
for (let i = 0; i < tmpExisting.length; i++)
|
|
1368
|
+
{
|
|
1369
|
+
try
|
|
1370
|
+
{
|
|
1371
|
+
this._osdViewer.removeOverlay(tmpExisting[i]);
|
|
1372
|
+
}
|
|
1373
|
+
catch (pErr)
|
|
1374
|
+
{
|
|
1375
|
+
if (tmpExisting[i].parentElement)
|
|
1376
|
+
{
|
|
1377
|
+
tmpExisting[i].parentElement.removeChild(tmpExisting[i]);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// Render each saved region
|
|
1384
|
+
for (let i = 0; i < this._savedRegions.length; i++)
|
|
1385
|
+
{
|
|
1386
|
+
let tmpRegion = this._savedRegions[i];
|
|
1387
|
+
this._addRegionOverlay(tmpRegion);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// If an edit is in progress, re-append drag handles to the
|
|
1391
|
+
// freshly-rendered overlay element. (The old overlay element was
|
|
1392
|
+
// destroyed above, so the handles went with it.)
|
|
1393
|
+
if (this._editingRegionID && tmpViewerDiv)
|
|
1394
|
+
{
|
|
1395
|
+
let tmpEditingEl = tmpViewerDiv.querySelector(
|
|
1396
|
+
'.retold-remote-iex-region-overlay[data-region-id="' + this._editingRegionID + '"]');
|
|
1397
|
+
if (tmpEditingEl)
|
|
1398
|
+
{
|
|
1399
|
+
this._appendEditHandles(tmpEditingEl);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* Add a single region overlay to the OSD viewer.
|
|
1406
|
+
*
|
|
1407
|
+
* Saved overlays use `pointer-events: auto` so the double-click edit
|
|
1408
|
+
* handler can fire; a `mousedown` listener calls `stopPropagation()`
|
|
1409
|
+
* so single-click pan still works outside the overlay rectangles.
|
|
1410
|
+
* See Part B (edit mode) for the full interaction model.
|
|
1411
|
+
*
|
|
1412
|
+
* @param {object} pRegion - { ID, Label, X, Y, Width, Height }
|
|
1413
|
+
*/
|
|
1414
|
+
_addRegionOverlay(pRegion)
|
|
1415
|
+
{
|
|
1416
|
+
if (!this._osdViewer)
|
|
1417
|
+
{
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
let tmpSelf = this;
|
|
1422
|
+
let tmpEl = document.createElement('div');
|
|
1423
|
+
tmpEl.className = 'retold-remote-iex-region-overlay';
|
|
1424
|
+
tmpEl.setAttribute('data-region-id', pRegion.ID);
|
|
1425
|
+
tmpEl.style.cssText = 'border: 2px solid rgba(229, 192, 123, 0.85); background: rgba(229, 192, 123, 0.08); pointer-events: auto; position: relative; cursor: pointer;';
|
|
1426
|
+
|
|
1427
|
+
// Label badge
|
|
1428
|
+
if (pRegion.Label)
|
|
1429
|
+
{
|
|
1430
|
+
let tmpLabelEl = document.createElement('span');
|
|
1431
|
+
tmpLabelEl.className = 'retold-remote-iex-region-label';
|
|
1432
|
+
tmpLabelEl.style.cssText = 'position:absolute;top:-1px;left:-1px;background:rgba(229,192,123,0.9);color:#282c34;font-size:0.65rem;padding:1px 5px;border-radius:0 0 3px 0;white-space:nowrap;pointer-events:none;';
|
|
1433
|
+
tmpLabelEl.textContent = pRegion.Label;
|
|
1434
|
+
tmpEl.appendChild(tmpLabelEl);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// Highlight the currently-edited region and dim others.
|
|
1438
|
+
if (this._editingRegionID && this._editingRegionID === pRegion.ID)
|
|
1439
|
+
{
|
|
1440
|
+
tmpEl.style.border = '2px solid rgba(97, 175, 239, 0.95)';
|
|
1441
|
+
tmpEl.style.background = 'rgba(97, 175, 239, 0.15)';
|
|
1442
|
+
tmpEl.style.opacity = '1';
|
|
1443
|
+
}
|
|
1444
|
+
else if (this._editingRegionID)
|
|
1445
|
+
{
|
|
1446
|
+
tmpEl.style.opacity = '0.35';
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Single mousedown on a saved overlay does NOT enter edit mode
|
|
1450
|
+
// (that would steal pan). Just stop propagation so OSD's click-to-
|
|
1451
|
+
// zoom / pan-drag doesn't happen inside the rectangle. Actual edit-
|
|
1452
|
+
// mode entry happens on double-click (see below).
|
|
1453
|
+
tmpEl.addEventListener('mousedown', function (pEvent)
|
|
1454
|
+
{
|
|
1455
|
+
// Only block propagation when NOT already in edit mode on this region.
|
|
1456
|
+
// When editing this region, the viewer-level edit tracker handles
|
|
1457
|
+
// press/drag/release on the overlay and its handles.
|
|
1458
|
+
if (tmpSelf._editingRegionID !== pRegion.ID)
|
|
1459
|
+
{
|
|
1460
|
+
pEvent.stopPropagation();
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
tmpEl.addEventListener('dblclick', function (pEvent)
|
|
1464
|
+
{
|
|
1465
|
+
pEvent.stopPropagation();
|
|
1466
|
+
pEvent.preventDefault();
|
|
1467
|
+
tmpSelf._enterRegionEditMode(pRegion.ID);
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
// Convert image coordinates to viewport coordinates
|
|
1471
|
+
let tmpImageRect = new OpenSeadragon.Rect(pRegion.X, pRegion.Y, pRegion.Width, pRegion.Height);
|
|
1472
|
+
let tmpViewportRect = this._osdViewer.viewport.imageToViewportRectangle(tmpImageRect);
|
|
1473
|
+
|
|
1474
|
+
this._osdViewer.addOverlay(
|
|
1475
|
+
{
|
|
1476
|
+
element: tmpEl,
|
|
1477
|
+
location: tmpViewportRect
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* Navigate to (zoom into) a specific saved region by ID.
|
|
1483
|
+
*
|
|
1484
|
+
* @param {string} pRegionID - The region ID to navigate to
|
|
1485
|
+
*/
|
|
1486
|
+
zoomToRegion(pRegionID)
|
|
1487
|
+
{
|
|
1488
|
+
if (!this._osdViewer || !this._dziData)
|
|
1489
|
+
{
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
let tmpRegion = null;
|
|
1494
|
+
for (let i = 0; i < this._savedRegions.length; i++)
|
|
1495
|
+
{
|
|
1496
|
+
if (this._savedRegions[i].ID === pRegionID)
|
|
1497
|
+
{
|
|
1498
|
+
tmpRegion = this._savedRegions[i];
|
|
1499
|
+
break;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
if (!tmpRegion)
|
|
1504
|
+
{
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// Convert image rect to viewport rect and fit to it
|
|
1509
|
+
let tmpImageRect = new OpenSeadragon.Rect(tmpRegion.X, tmpRegion.Y, tmpRegion.Width, tmpRegion.Height);
|
|
1510
|
+
let tmpViewportRect = this._osdViewer.viewport.imageToViewportRectangle(tmpImageRect);
|
|
1511
|
+
this._osdViewer.viewport.fitBounds(tmpViewportRect);
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
/**
|
|
1515
|
+
* Delete a saved region by ID.
|
|
1516
|
+
*
|
|
1517
|
+
* @param {string} pRegionID - The region ID to delete
|
|
1518
|
+
*/
|
|
1519
|
+
deleteRegion(pRegionID)
|
|
1520
|
+
{
|
|
1521
|
+
let tmpSelf = this;
|
|
1522
|
+
let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
|
|
1523
|
+
let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(this._currentPath) : encodeURIComponent(this._currentPath);
|
|
1524
|
+
|
|
1525
|
+
fetch('/api/media/subimage-regions/' + encodeURIComponent(pRegionID) + '?path=' + tmpPathParam,
|
|
1526
|
+
{
|
|
1527
|
+
method: 'DELETE'
|
|
1528
|
+
})
|
|
1529
|
+
.then((pResponse) => pResponse.json())
|
|
1530
|
+
.then((pResult) =>
|
|
1531
|
+
{
|
|
1532
|
+
if (pResult && pResult.Success)
|
|
1533
|
+
{
|
|
1534
|
+
tmpSelf._savedRegions = pResult.Regions || [];
|
|
1535
|
+
tmpSelf._renderSavedRegionOverlays();
|
|
1536
|
+
|
|
1537
|
+
let tmpToast = tmpSelf.pict.providers['RetoldRemote-ToastNotification'];
|
|
1538
|
+
if (tmpToast)
|
|
1539
|
+
{
|
|
1540
|
+
tmpToast.showToast('Region deleted');
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// Update sidebar
|
|
1544
|
+
let tmpSubPanel = tmpSelf.pict.views['RetoldRemote-SubimagesPanel'];
|
|
1545
|
+
if (tmpSubPanel)
|
|
1546
|
+
{
|
|
1547
|
+
tmpSubPanel.render();
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
})
|
|
1551
|
+
.catch(() =>
|
|
1552
|
+
{
|
|
1553
|
+
// ignore
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* Get the current selection region (for use by collection add).
|
|
1559
|
+
*
|
|
1560
|
+
* @returns {object|null} The current selection or null
|
|
1561
|
+
*/
|
|
1562
|
+
getActiveSelection()
|
|
1563
|
+
{
|
|
1564
|
+
return this._selectionRegion;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/**
|
|
1568
|
+
* Get the saved regions array.
|
|
1569
|
+
*
|
|
1570
|
+
* @returns {Array}
|
|
1571
|
+
*/
|
|
1572
|
+
getSavedRegions()
|
|
1573
|
+
{
|
|
1574
|
+
return this._savedRegions;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// -----------------------------------------------------------------
|
|
1578
|
+
// Navigation
|
|
1579
|
+
// -----------------------------------------------------------------
|
|
1580
|
+
|
|
1581
|
+
/**
|
|
1582
|
+
* Navigate back to the gallery / file listing.
|
|
1583
|
+
*/
|
|
1584
|
+
goBack()
|
|
1585
|
+
{
|
|
1586
|
+
// Cancel any in-flight DZI generation
|
|
1587
|
+
this._cancelActiveDziOperation();
|
|
1588
|
+
|
|
1589
|
+
// Clean up selection mode
|
|
1590
|
+
if (this._selectionMode)
|
|
1591
|
+
{
|
|
1592
|
+
this._exitSelectionMode();
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// Destroy the OSD viewer
|
|
1596
|
+
if (this._osdViewer)
|
|
1597
|
+
{
|
|
1598
|
+
try
|
|
1599
|
+
{
|
|
1600
|
+
this._osdViewer.destroy();
|
|
1601
|
+
}
|
|
1602
|
+
catch (pErr)
|
|
1603
|
+
{
|
|
1604
|
+
// ignore
|
|
1605
|
+
}
|
|
1606
|
+
this._osdViewer = null;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
let tmpNav = this.pict.providers['RetoldRemote-GalleryNavigation'];
|
|
1610
|
+
if (tmpNav)
|
|
1611
|
+
{
|
|
1612
|
+
tmpNav.closeViewer();
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
/**
|
|
1617
|
+
* Leave the image explorer and view the image in the standard viewer.
|
|
1618
|
+
*/
|
|
1619
|
+
viewInBrowser()
|
|
1620
|
+
{
|
|
1621
|
+
// Cancel any in-flight DZI generation
|
|
1622
|
+
this._cancelActiveDziOperation();
|
|
1623
|
+
|
|
1624
|
+
// Destroy the OSD viewer
|
|
1625
|
+
if (this._osdViewer)
|
|
1626
|
+
{
|
|
1627
|
+
try
|
|
1628
|
+
{
|
|
1629
|
+
this._osdViewer.destroy();
|
|
1630
|
+
}
|
|
1631
|
+
catch (pErr)
|
|
1632
|
+
{
|
|
1633
|
+
// ignore
|
|
1634
|
+
}
|
|
1635
|
+
this._osdViewer = null;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
let tmpViewer = this.pict.views['RetoldRemote-MediaViewer'];
|
|
1639
|
+
if (tmpViewer)
|
|
1640
|
+
{
|
|
1641
|
+
tmpViewer.showMedia(this._currentPath, 'image');
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* Show an error message.
|
|
1647
|
+
*
|
|
1648
|
+
* @param {string} pMessage - Error message
|
|
1649
|
+
*/
|
|
1650
|
+
_showError(pMessage)
|
|
1651
|
+
{
|
|
1652
|
+
let tmpLoading = document.getElementById('RetoldRemote-IEX-Loading');
|
|
1653
|
+
if (tmpLoading)
|
|
1654
|
+
{
|
|
1655
|
+
tmpLoading.style.display = 'none';
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
let tmpBody = document.getElementById('RetoldRemote-IEX-Body');
|
|
1659
|
+
if (tmpBody)
|
|
1660
|
+
{
|
|
1661
|
+
let tmpFmt = this.pict.providers['RetoldRemote-FormattingUtilities'];
|
|
1662
|
+
tmpBody.innerHTML = '<div class="retold-remote-iex-error">'
|
|
1663
|
+
+ '<div class="retold-remote-iex-error-message">' + tmpFmt.escapeHTML(pMessage || 'An error occurred.') + '</div>'
|
|
1664
|
+
+ '<button class="retold-remote-iex-nav-btn" onclick="pict.views[\'RetoldRemote-ImageExplorer\'].goBack()">Back to Image</button>'
|
|
1665
|
+
+ '</div>';
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// -----------------------------------------------------------------
|
|
1670
|
+
// Shared helpers
|
|
1671
|
+
// -----------------------------------------------------------------
|
|
1672
|
+
|
|
1673
|
+
/**
|
|
1674
|
+
* Clamp a region's X/Y/Width/Height to the image dimensions,
|
|
1675
|
+
* mutating the passed object in place. No-op if the image dimensions
|
|
1676
|
+
* aren't known yet.
|
|
1677
|
+
*
|
|
1678
|
+
* Reused by both the new-region release handler and the edit-mode
|
|
1679
|
+
* drag/resize logic so bounds-checking stays consistent.
|
|
1680
|
+
*
|
|
1681
|
+
* @param {object} pRegion - { X, Y, Width, Height } mutated in place
|
|
1682
|
+
*/
|
|
1683
|
+
_clampRegionToImage(pRegion)
|
|
1684
|
+
{
|
|
1685
|
+
if (!pRegion) return;
|
|
1686
|
+
|
|
1687
|
+
// Non-negative origin
|
|
1688
|
+
if (pRegion.X < 0)
|
|
1689
|
+
{
|
|
1690
|
+
pRegion.Width += pRegion.X;
|
|
1691
|
+
pRegion.X = 0;
|
|
1692
|
+
}
|
|
1693
|
+
if (pRegion.Y < 0)
|
|
1694
|
+
{
|
|
1695
|
+
pRegion.Height += pRegion.Y;
|
|
1696
|
+
pRegion.Y = 0;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// Fit within image width/height
|
|
1700
|
+
if (this._dziData)
|
|
1701
|
+
{
|
|
1702
|
+
if (pRegion.X + pRegion.Width > this._dziData.Width)
|
|
1703
|
+
{
|
|
1704
|
+
pRegion.Width = this._dziData.Width - pRegion.X;
|
|
1705
|
+
}
|
|
1706
|
+
if (pRegion.Y + pRegion.Height > this._dziData.Height)
|
|
1707
|
+
{
|
|
1708
|
+
pRegion.Height = this._dziData.Height - pRegion.Y;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// Note: do NOT enforce a minimum size here. Callers must do their
|
|
1713
|
+
// own "ignore accidental click" check in SCREEN pixels before
|
|
1714
|
+
// calling this helper. Enforcing a minimum here would bump a
|
|
1715
|
+
// zero-size selection up to the minimum and cause the caller's
|
|
1716
|
+
// image-pixel min-size guard to silently pass, which created the
|
|
1717
|
+
// bug where a single click on a high-res image popped up the
|
|
1718
|
+
// label dialog as if the user had dragged.
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// -----------------------------------------------------------------
|
|
1722
|
+
// Edit mode for saved regions (Part B)
|
|
1723
|
+
// -----------------------------------------------------------------
|
|
1724
|
+
|
|
1725
|
+
/**
|
|
1726
|
+
* Enter edit mode for a specific saved region. Highlights the region,
|
|
1727
|
+
* dims the others, appends drag handles, and installs drag listeners
|
|
1728
|
+
* for move/resize. Also populates the inline label input with the
|
|
1729
|
+
* region's current label so the user can edit it in place.
|
|
1730
|
+
*
|
|
1731
|
+
* Entering edit mode automatically exits new-region selection mode
|
|
1732
|
+
* (they're mutually exclusive).
|
|
1733
|
+
*
|
|
1734
|
+
* @param {string} pRegionID - ID of the region to edit
|
|
1735
|
+
*/
|
|
1736
|
+
_enterRegionEditMode(pRegionID)
|
|
1737
|
+
{
|
|
1738
|
+
// Find the region
|
|
1739
|
+
let tmpRegion = null;
|
|
1740
|
+
for (let i = 0; i < this._savedRegions.length; i++)
|
|
1741
|
+
{
|
|
1742
|
+
if (this._savedRegions[i].ID === pRegionID)
|
|
1743
|
+
{
|
|
1744
|
+
tmpRegion = this._savedRegions[i];
|
|
1745
|
+
break;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
if (!tmpRegion)
|
|
1749
|
+
{
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// Mutually exclusive with new-region selection mode
|
|
1754
|
+
if (this._selectionMode)
|
|
1755
|
+
{
|
|
1756
|
+
this._exitSelectionMode();
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// If already editing a different region, exit that first
|
|
1760
|
+
if (this._editingRegionID && this._editingRegionID !== pRegionID)
|
|
1761
|
+
{
|
|
1762
|
+
this._exitRegionEditMode();
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
this._editingRegionID = pRegionID;
|
|
1766
|
+
this._editDragMode = null;
|
|
1767
|
+
this._editDragStart = null;
|
|
1768
|
+
this._editOriginalRect = null;
|
|
1769
|
+
|
|
1770
|
+
// Disable OSD panning while in edit mode so drags on the handles
|
|
1771
|
+
// don't also pan the viewer.
|
|
1772
|
+
if (this._osdViewer)
|
|
1773
|
+
{
|
|
1774
|
+
this._osdViewer.setMouseNavEnabled(false);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// Re-render overlays so the selected one gets highlight/other dimmed.
|
|
1778
|
+
// _addRegionOverlay checks this._editingRegionID for styling, and
|
|
1779
|
+
// _renderSavedRegionOverlays re-appends the drag handles via
|
|
1780
|
+
// _appendEditHandles now that this._editingRegionID is set.
|
|
1781
|
+
this._renderSavedRegionOverlays();
|
|
1782
|
+
|
|
1783
|
+
// Show the inline label input with the current label pre-filled
|
|
1784
|
+
let tmpLabelWrap = document.getElementById('RetoldRemote-IEX-LabelInput');
|
|
1785
|
+
let tmpCoords = document.getElementById('RetoldRemote-IEX-Coords');
|
|
1786
|
+
let tmpField = document.getElementById('RetoldRemote-IEX-LabelField');
|
|
1787
|
+
if (tmpLabelWrap) tmpLabelWrap.style.display = '';
|
|
1788
|
+
if (tmpCoords) tmpCoords.style.display = 'none';
|
|
1789
|
+
if (tmpField)
|
|
1790
|
+
{
|
|
1791
|
+
tmpField.value = tmpRegion.Label || '';
|
|
1792
|
+
// Repurpose the Save button to save the edited region rather
|
|
1793
|
+
// than create a new one. We swap the onclick via a flag.
|
|
1794
|
+
tmpField.setAttribute('data-edit-mode', '1');
|
|
1795
|
+
tmpField.focus();
|
|
1796
|
+
tmpField.select();
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/**
|
|
1801
|
+
* Exit edit mode, cleaning up handles, restoring overlay styles,
|
|
1802
|
+
* and re-enabling OSD panning.
|
|
1803
|
+
*/
|
|
1804
|
+
_exitRegionEditMode()
|
|
1805
|
+
{
|
|
1806
|
+
this._editingRegionID = null;
|
|
1807
|
+
this._editDragMode = null;
|
|
1808
|
+
this._editDragStart = null;
|
|
1809
|
+
this._editOriginalRect = null;
|
|
1810
|
+
|
|
1811
|
+
// Remove any lingering document-level drag listeners (should be
|
|
1812
|
+
// gone already, but be defensive)
|
|
1813
|
+
if (this._editDocMoveHandler)
|
|
1814
|
+
{
|
|
1815
|
+
document.removeEventListener('mousemove', this._editDocMoveHandler);
|
|
1816
|
+
this._editDocMoveHandler = null;
|
|
1817
|
+
}
|
|
1818
|
+
if (this._editDocUpHandler)
|
|
1819
|
+
{
|
|
1820
|
+
document.removeEventListener('mouseup', this._editDocUpHandler);
|
|
1821
|
+
this._editDocUpHandler = null;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// Re-enable OSD panning
|
|
1825
|
+
if (this._osdViewer)
|
|
1826
|
+
{
|
|
1827
|
+
this._osdViewer.setMouseNavEnabled(true);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// Re-render overlays to restore normal styling (no highlight/dim)
|
|
1831
|
+
this._renderSavedRegionOverlays();
|
|
1832
|
+
|
|
1833
|
+
// Hide the label input
|
|
1834
|
+
let tmpLabelWrap = document.getElementById('RetoldRemote-IEX-LabelInput');
|
|
1835
|
+
let tmpCoords = document.getElementById('RetoldRemote-IEX-Coords');
|
|
1836
|
+
let tmpField = document.getElementById('RetoldRemote-IEX-LabelField');
|
|
1837
|
+
if (tmpLabelWrap) tmpLabelWrap.style.display = 'none';
|
|
1838
|
+
if (tmpCoords) tmpCoords.style.display = '';
|
|
1839
|
+
if (tmpField)
|
|
1840
|
+
{
|
|
1841
|
+
tmpField.removeAttribute('data-edit-mode');
|
|
1842
|
+
tmpField.value = '';
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/**
|
|
1847
|
+
* Append 8 drag handles (4 corners + 4 edges) to a region overlay.
|
|
1848
|
+
* Each handle has a 16×16 invisible hit area over a 6×6 visual dot
|
|
1849
|
+
* so they're easy to grab on both mouse and touch.
|
|
1850
|
+
*
|
|
1851
|
+
* @param {HTMLElement} pOverlayEl - The overlay element to decorate
|
|
1852
|
+
*/
|
|
1853
|
+
_appendEditHandles(pOverlayEl)
|
|
1854
|
+
{
|
|
1855
|
+
let tmpSelf = this;
|
|
1856
|
+
let tmpHandles =
|
|
1857
|
+
[
|
|
1858
|
+
{ key: 'tl', css: 'top:-8px;left:-8px;cursor:nwse-resize;' },
|
|
1859
|
+
{ key: 'tr', css: 'top:-8px;right:-8px;cursor:nesw-resize;' },
|
|
1860
|
+
{ key: 'bl', css: 'bottom:-8px;left:-8px;cursor:nesw-resize;' },
|
|
1861
|
+
{ key: 'br', css: 'bottom:-8px;right:-8px;cursor:nwse-resize;' },
|
|
1862
|
+
{ key: 't', css: 'top:-8px;left:50%;transform:translateX(-50%);cursor:ns-resize;' },
|
|
1863
|
+
{ key: 'b', css: 'bottom:-8px;left:50%;transform:translateX(-50%);cursor:ns-resize;' },
|
|
1864
|
+
{ key: 'l', css: 'top:50%;left:-8px;transform:translateY(-50%);cursor:ew-resize;' },
|
|
1865
|
+
{ key: 'r', css: 'top:50%;right:-8px;transform:translateY(-50%);cursor:ew-resize;' }
|
|
1866
|
+
];
|
|
1867
|
+
|
|
1868
|
+
for (let i = 0; i < tmpHandles.length; i++)
|
|
1869
|
+
{
|
|
1870
|
+
let tmpHandleInfo = tmpHandles[i];
|
|
1871
|
+
let tmpH = document.createElement('div');
|
|
1872
|
+
tmpH.className = 'retold-remote-iex-edit-handle';
|
|
1873
|
+
tmpH.setAttribute('data-handle', tmpHandleInfo.key);
|
|
1874
|
+
tmpH.style.cssText = 'position:absolute;width:16px;height:16px;pointer-events:auto;z-index:10;' + tmpHandleInfo.css;
|
|
1875
|
+
|
|
1876
|
+
// Visible dot inside the hit area
|
|
1877
|
+
let tmpDot = document.createElement('div');
|
|
1878
|
+
tmpDot.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:8px;height:8px;background:#61afef;border:1.5px solid #fff;border-radius:50%;box-shadow:0 1px 3px rgba(0,0,0,0.5);';
|
|
1879
|
+
tmpH.appendChild(tmpDot);
|
|
1880
|
+
|
|
1881
|
+
tmpH.addEventListener('mousedown', function (pEvent)
|
|
1882
|
+
{
|
|
1883
|
+
pEvent.stopPropagation();
|
|
1884
|
+
pEvent.preventDefault();
|
|
1885
|
+
tmpSelf._onEditHandlePress(tmpHandleInfo.key, pEvent);
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
pOverlayEl.appendChild(tmpH);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
// Body drag (move) — the overlay element itself
|
|
1892
|
+
pOverlayEl.addEventListener('mousedown', function (pEvent)
|
|
1893
|
+
{
|
|
1894
|
+
// Only handle body clicks when clicking the overlay itself (not a child handle)
|
|
1895
|
+
if (pEvent.target !== pOverlayEl)
|
|
1896
|
+
{
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
if (tmpSelf._editingRegionID !== pOverlayEl.getAttribute('data-region-id'))
|
|
1900
|
+
{
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
pEvent.stopPropagation();
|
|
1904
|
+
pEvent.preventDefault();
|
|
1905
|
+
tmpSelf._onEditHandlePress('body', pEvent);
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1908
|
+
// Make the overlay element display cursor:move while in edit mode
|
|
1909
|
+
pOverlayEl.style.cursor = 'move';
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
/**
|
|
1913
|
+
* Start an edit-mode drag. Captures the starting point and the
|
|
1914
|
+
* overlay's current rect, installs document-level move/up listeners,
|
|
1915
|
+
* and tracks which handle (or body) the drag is operating on.
|
|
1916
|
+
*
|
|
1917
|
+
* @param {string} pHandleKey - 'tl'|'tr'|'bl'|'br'|'t'|'r'|'b'|'l'|'body'
|
|
1918
|
+
* @param {MouseEvent} pEvent
|
|
1919
|
+
*/
|
|
1920
|
+
_onEditHandlePress(pHandleKey, pEvent)
|
|
1921
|
+
{
|
|
1922
|
+
if (!this._osdViewer) return;
|
|
1923
|
+
if (!this._editingRegionID) return;
|
|
1924
|
+
|
|
1925
|
+
let tmpViewerDiv = document.getElementById('RetoldRemote-IEX-Viewer');
|
|
1926
|
+
if (!tmpViewerDiv) return;
|
|
1927
|
+
|
|
1928
|
+
this._editDragMode = pHandleKey;
|
|
1929
|
+
|
|
1930
|
+
// Capture starting viewport point (converted from client coords)
|
|
1931
|
+
let tmpRect = tmpViewerDiv.getBoundingClientRect();
|
|
1932
|
+
let tmpLocalX = pEvent.clientX - tmpRect.left;
|
|
1933
|
+
let tmpLocalY = pEvent.clientY - tmpRect.top;
|
|
1934
|
+
this._editDragStart = this._osdViewer.viewport.pointFromPixel(
|
|
1935
|
+
new OpenSeadragon.Point(tmpLocalX, tmpLocalY));
|
|
1936
|
+
|
|
1937
|
+
// Capture the overlay's current viewport rect (from the saved region)
|
|
1938
|
+
let tmpRegion = this._findSavedRegion(this._editingRegionID);
|
|
1939
|
+
if (!tmpRegion)
|
|
1940
|
+
{
|
|
1941
|
+
this._editDragMode = null;
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
let tmpImageRect = new OpenSeadragon.Rect(
|
|
1945
|
+
tmpRegion.X, tmpRegion.Y, tmpRegion.Width, tmpRegion.Height);
|
|
1946
|
+
this._editOriginalRect = this._osdViewer.viewport.imageToViewportRectangle(tmpImageRect);
|
|
1947
|
+
|
|
1948
|
+
// Install document-level move + up listeners. Using native DOM
|
|
1949
|
+
// listeners rather than OSD MouseTracker so the drag isn't
|
|
1950
|
+
// constrained to the viewer div — the user can drag past the edge.
|
|
1951
|
+
let tmpSelf = this;
|
|
1952
|
+
this._editDocMoveHandler = function (pMoveEvent)
|
|
1953
|
+
{
|
|
1954
|
+
tmpSelf._onEditHandleMove(pMoveEvent);
|
|
1955
|
+
};
|
|
1956
|
+
this._editDocUpHandler = function (pUpEvent)
|
|
1957
|
+
{
|
|
1958
|
+
tmpSelf._onEditHandleRelease(pUpEvent);
|
|
1959
|
+
};
|
|
1960
|
+
document.addEventListener('mousemove', this._editDocMoveHandler);
|
|
1961
|
+
document.addEventListener('mouseup', this._editDocUpHandler);
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
/**
|
|
1965
|
+
* Update the overlay rect during an edit drag.
|
|
1966
|
+
*
|
|
1967
|
+
* @param {MouseEvent} pEvent
|
|
1968
|
+
*/
|
|
1969
|
+
_onEditHandleMove(pEvent)
|
|
1970
|
+
{
|
|
1971
|
+
if (!this._osdViewer || !this._editDragStart || !this._editOriginalRect || !this._editDragMode)
|
|
1972
|
+
{
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
let tmpViewerDiv = document.getElementById('RetoldRemote-IEX-Viewer');
|
|
1977
|
+
if (!tmpViewerDiv) return;
|
|
1978
|
+
|
|
1979
|
+
let tmpRect = tmpViewerDiv.getBoundingClientRect();
|
|
1980
|
+
let tmpLocalX = pEvent.clientX - tmpRect.left;
|
|
1981
|
+
let tmpLocalY = pEvent.clientY - tmpRect.top;
|
|
1982
|
+
let tmpCurrent = this._osdViewer.viewport.pointFromPixel(
|
|
1983
|
+
new OpenSeadragon.Point(tmpLocalX, tmpLocalY));
|
|
1984
|
+
|
|
1985
|
+
let tmpDx = tmpCurrent.x - this._editDragStart.x;
|
|
1986
|
+
let tmpDy = tmpCurrent.y - this._editDragStart.y;
|
|
1987
|
+
|
|
1988
|
+
// Apply the delta to the original rect based on drag mode
|
|
1989
|
+
let tmpX = this._editOriginalRect.x;
|
|
1990
|
+
let tmpY = this._editOriginalRect.y;
|
|
1991
|
+
let tmpW = this._editOriginalRect.width;
|
|
1992
|
+
let tmpH = this._editOriginalRect.height;
|
|
1993
|
+
|
|
1994
|
+
switch (this._editDragMode)
|
|
1995
|
+
{
|
|
1996
|
+
case 'body':
|
|
1997
|
+
tmpX += tmpDx;
|
|
1998
|
+
tmpY += tmpDy;
|
|
1999
|
+
break;
|
|
2000
|
+
case 'tl':
|
|
2001
|
+
tmpX += tmpDx; tmpY += tmpDy; tmpW -= tmpDx; tmpH -= tmpDy;
|
|
2002
|
+
break;
|
|
2003
|
+
case 'tr':
|
|
2004
|
+
tmpY += tmpDy; tmpW += tmpDx; tmpH -= tmpDy;
|
|
2005
|
+
break;
|
|
2006
|
+
case 'bl':
|
|
2007
|
+
tmpX += tmpDx; tmpW -= tmpDx; tmpH += tmpDy;
|
|
2008
|
+
break;
|
|
2009
|
+
case 'br':
|
|
2010
|
+
tmpW += tmpDx; tmpH += tmpDy;
|
|
2011
|
+
break;
|
|
2012
|
+
case 't':
|
|
2013
|
+
tmpY += tmpDy; tmpH -= tmpDy;
|
|
2014
|
+
break;
|
|
2015
|
+
case 'b':
|
|
2016
|
+
tmpH += tmpDy;
|
|
2017
|
+
break;
|
|
2018
|
+
case 'l':
|
|
2019
|
+
tmpX += tmpDx; tmpW -= tmpDx;
|
|
2020
|
+
break;
|
|
2021
|
+
case 'r':
|
|
2022
|
+
tmpW += tmpDx;
|
|
2023
|
+
break;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// Handle inversion: if width or height went negative, flip
|
|
2027
|
+
if (tmpW < 0)
|
|
2028
|
+
{
|
|
2029
|
+
tmpX += tmpW;
|
|
2030
|
+
tmpW = -tmpW;
|
|
2031
|
+
}
|
|
2032
|
+
if (tmpH < 0)
|
|
2033
|
+
{
|
|
2034
|
+
tmpY += tmpH;
|
|
2035
|
+
tmpH = -tmpH;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// Get the overlay element and update its position
|
|
2039
|
+
let tmpOverlayEl = tmpViewerDiv.querySelector(
|
|
2040
|
+
'.retold-remote-iex-region-overlay[data-region-id="' + this._editingRegionID + '"]');
|
|
2041
|
+
if (tmpOverlayEl)
|
|
2042
|
+
{
|
|
2043
|
+
this._osdViewer.updateOverlay(
|
|
2044
|
+
tmpOverlayEl,
|
|
2045
|
+
new OpenSeadragon.Rect(tmpX, tmpY, tmpW, tmpH));
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
/**
|
|
2050
|
+
* Finalize an edit drag: convert viewport rect back to image coords,
|
|
2051
|
+
* clamp, mutate the saved region optimistically, and PUT to the server.
|
|
2052
|
+
*/
|
|
2053
|
+
_onEditHandleRelease(pEvent)
|
|
2054
|
+
{
|
|
2055
|
+
// Remove document-level listeners
|
|
2056
|
+
if (this._editDocMoveHandler)
|
|
2057
|
+
{
|
|
2058
|
+
document.removeEventListener('mousemove', this._editDocMoveHandler);
|
|
2059
|
+
this._editDocMoveHandler = null;
|
|
2060
|
+
}
|
|
2061
|
+
if (this._editDocUpHandler)
|
|
2062
|
+
{
|
|
2063
|
+
document.removeEventListener('mouseup', this._editDocUpHandler);
|
|
2064
|
+
this._editDocUpHandler = null;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
if (!this._osdViewer || !this._editingRegionID || !this._editDragMode)
|
|
2068
|
+
{
|
|
2069
|
+
this._editDragMode = null;
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
let tmpViewerDiv = document.getElementById('RetoldRemote-IEX-Viewer');
|
|
2074
|
+
if (!tmpViewerDiv)
|
|
2075
|
+
{
|
|
2076
|
+
this._editDragMode = null;
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
let tmpOverlayEl = tmpViewerDiv.querySelector(
|
|
2081
|
+
'.retold-remote-iex-region-overlay[data-region-id="' + this._editingRegionID + '"]');
|
|
2082
|
+
if (!tmpOverlayEl)
|
|
2083
|
+
{
|
|
2084
|
+
this._editDragMode = null;
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// Get the current viewport rect from the overlay registry
|
|
2089
|
+
let tmpOverlayRec = this._osdViewer.getOverlayById(tmpOverlayEl);
|
|
2090
|
+
if (!tmpOverlayRec || !tmpOverlayRec.location)
|
|
2091
|
+
{
|
|
2092
|
+
this._editDragMode = null;
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
let tmpVpRect = tmpOverlayRec.location;
|
|
2097
|
+
let tmpTopLeft = this._osdViewer.viewport.viewportToImageCoordinates(
|
|
2098
|
+
new OpenSeadragon.Point(tmpVpRect.x, tmpVpRect.y));
|
|
2099
|
+
let tmpBottomRight = this._osdViewer.viewport.viewportToImageCoordinates(
|
|
2100
|
+
new OpenSeadragon.Point(tmpVpRect.x + tmpVpRect.width, tmpVpRect.y + tmpVpRect.height));
|
|
2101
|
+
|
|
2102
|
+
let tmpNewRegion =
|
|
2103
|
+
{
|
|
2104
|
+
X: Math.round(tmpTopLeft.x),
|
|
2105
|
+
Y: Math.round(tmpTopLeft.y),
|
|
2106
|
+
Width: Math.round(tmpBottomRight.x - tmpTopLeft.x),
|
|
2107
|
+
Height: Math.round(tmpBottomRight.y - tmpTopLeft.y)
|
|
2108
|
+
};
|
|
2109
|
+
this._clampRegionToImage(tmpNewRegion);
|
|
2110
|
+
|
|
2111
|
+
// Find the saved region and store the previous rect for revert-on-fail
|
|
2112
|
+
let tmpRegion = this._findSavedRegion(this._editingRegionID);
|
|
2113
|
+
if (!tmpRegion)
|
|
2114
|
+
{
|
|
2115
|
+
this._editDragMode = null;
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
let tmpPrevious =
|
|
2119
|
+
{
|
|
2120
|
+
X: tmpRegion.X, Y: tmpRegion.Y, Width: tmpRegion.Width, Height: tmpRegion.Height
|
|
2121
|
+
};
|
|
2122
|
+
|
|
2123
|
+
// Only PUT if something actually changed
|
|
2124
|
+
if (tmpPrevious.X === tmpNewRegion.X && tmpPrevious.Y === tmpNewRegion.Y
|
|
2125
|
+
&& tmpPrevious.Width === tmpNewRegion.Width && tmpPrevious.Height === tmpNewRegion.Height)
|
|
2126
|
+
{
|
|
2127
|
+
this._editDragMode = null;
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
// Optimistic update: mutate local state and PUT in background
|
|
2132
|
+
tmpRegion.X = tmpNewRegion.X;
|
|
2133
|
+
tmpRegion.Y = tmpNewRegion.Y;
|
|
2134
|
+
tmpRegion.Width = tmpNewRegion.Width;
|
|
2135
|
+
tmpRegion.Height = tmpNewRegion.Height;
|
|
2136
|
+
|
|
2137
|
+
this._editDragMode = null;
|
|
2138
|
+
this._putRegionUpdate(tmpRegion.ID, {
|
|
2139
|
+
X: tmpNewRegion.X,
|
|
2140
|
+
Y: tmpNewRegion.Y,
|
|
2141
|
+
Width: tmpNewRegion.Width,
|
|
2142
|
+
Height: tmpNewRegion.Height
|
|
2143
|
+
}, tmpPrevious);
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
/**
|
|
2147
|
+
* Save the label being edited (called from the Save button and Enter key
|
|
2148
|
+
* when data-edit-mode is set on the label field). Dispatches to either
|
|
2149
|
+
* the new-region saver or the edit-mode PUT based on state.
|
|
2150
|
+
*/
|
|
2151
|
+
saveSelectionLabel()
|
|
2152
|
+
{
|
|
2153
|
+
// If we're in edit mode, save the label via PUT instead of creating
|
|
2154
|
+
// a new region.
|
|
2155
|
+
if (this._editingRegionID)
|
|
2156
|
+
{
|
|
2157
|
+
let tmpField = document.getElementById('RetoldRemote-IEX-LabelField');
|
|
2158
|
+
let tmpNewLabel = tmpField ? tmpField.value.trim() : '';
|
|
2159
|
+
|
|
2160
|
+
let tmpRegion = this._findSavedRegion(this._editingRegionID);
|
|
2161
|
+
if (!tmpRegion)
|
|
2162
|
+
{
|
|
2163
|
+
this._exitRegionEditMode();
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
let tmpPreviousLabel = tmpRegion.Label || '';
|
|
2167
|
+
if (tmpNewLabel === tmpPreviousLabel)
|
|
2168
|
+
{
|
|
2169
|
+
// No change — just exit edit mode
|
|
2170
|
+
this._exitRegionEditMode();
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
// Optimistic update
|
|
2175
|
+
tmpRegion.Label = tmpNewLabel;
|
|
2176
|
+
let tmpRegionID = this._editingRegionID;
|
|
2177
|
+
this._exitRegionEditMode();
|
|
2178
|
+
this._putRegionUpdate(tmpRegionID, { Label: tmpNewLabel }, { Label: tmpPreviousLabel });
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// Not in edit mode — fall through to the original new-region save
|
|
2183
|
+
return this._saveNewRegionLabel();
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
/**
|
|
2187
|
+
* PUT a partial update for a region (label or geometry). Reverts the
|
|
2188
|
+
* passed `pPrevious` fields on failure and shows a toast.
|
|
2189
|
+
*
|
|
2190
|
+
* @param {string} pRegionID - Region ID
|
|
2191
|
+
* @param {object} pFields - Fields to update (subset of Label/X/Y/Width/Height)
|
|
2192
|
+
* @param {object} pPrevious - Previous values for revert-on-failure
|
|
2193
|
+
*/
|
|
2194
|
+
_putRegionUpdate(pRegionID, pFields, pPrevious)
|
|
2195
|
+
{
|
|
2196
|
+
let tmpSelf = this;
|
|
2197
|
+
let tmpBody = Object.assign({ Path: this._currentPath }, pFields);
|
|
2198
|
+
|
|
2199
|
+
fetch('/api/media/subimage-regions/' + encodeURIComponent(pRegionID),
|
|
2200
|
+
{
|
|
2201
|
+
method: 'PUT',
|
|
2202
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2203
|
+
body: JSON.stringify(tmpBody)
|
|
2204
|
+
})
|
|
2205
|
+
.then((pResponse) => pResponse.json())
|
|
2206
|
+
.then((pResult) =>
|
|
2207
|
+
{
|
|
2208
|
+
if (!pResult || !pResult.Success)
|
|
2209
|
+
{
|
|
2210
|
+
// Revert optimistic changes
|
|
2211
|
+
tmpSelf._revertRegion(pRegionID, pPrevious);
|
|
2212
|
+
tmpSelf._renderSavedRegionOverlays();
|
|
2213
|
+
let tmpToast = tmpSelf.pict.providers['RetoldRemote-ToastNotification'];
|
|
2214
|
+
if (tmpToast)
|
|
2215
|
+
{
|
|
2216
|
+
let tmpMsg = pResult && pResult.Error
|
|
2217
|
+
? ('Failed to save: ' + pResult.Error)
|
|
2218
|
+
: 'Failed to save region update (file may have been modified).';
|
|
2219
|
+
tmpToast.showToast(tmpMsg);
|
|
2220
|
+
}
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
// Success — the server response contains the authoritative list
|
|
2224
|
+
if (Array.isArray(pResult.Regions))
|
|
2225
|
+
{
|
|
2226
|
+
tmpSelf._savedRegions = pResult.Regions;
|
|
2227
|
+
}
|
|
2228
|
+
tmpSelf._renderSavedRegionOverlays();
|
|
2229
|
+
|
|
2230
|
+
// Update sidebar — use refresh() to force a re-fetch so the
|
|
2231
|
+
// panel picks up the updated geometry/label instead of using
|
|
2232
|
+
// its stale cached _regions array.
|
|
2233
|
+
let tmpSubPanel = tmpSelf.pict.views['RetoldRemote-SubimagesPanel'];
|
|
2234
|
+
if (tmpSubPanel && typeof tmpSubPanel.refresh === 'function')
|
|
2235
|
+
{
|
|
2236
|
+
tmpSubPanel.refresh();
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
let tmpToast = tmpSelf.pict.providers['RetoldRemote-ToastNotification'];
|
|
2240
|
+
if (tmpToast)
|
|
2241
|
+
{
|
|
2242
|
+
tmpToast.showToast('Region updated');
|
|
2243
|
+
}
|
|
2244
|
+
})
|
|
2245
|
+
.catch((pError) =>
|
|
2246
|
+
{
|
|
2247
|
+
// Revert optimistic changes
|
|
2248
|
+
tmpSelf._revertRegion(pRegionID, pPrevious);
|
|
2249
|
+
tmpSelf._renderSavedRegionOverlays();
|
|
2250
|
+
let tmpToast = tmpSelf.pict.providers['RetoldRemote-ToastNotification'];
|
|
2251
|
+
if (tmpToast)
|
|
2252
|
+
{
|
|
2253
|
+
tmpToast.showToast('Network error saving region: ' + pError.message);
|
|
2254
|
+
}
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
/**
|
|
2259
|
+
* Revert in-memory region fields to a previous snapshot.
|
|
2260
|
+
*/
|
|
2261
|
+
_revertRegion(pRegionID, pPrevious)
|
|
2262
|
+
{
|
|
2263
|
+
if (!pPrevious) return;
|
|
2264
|
+
let tmpRegion = this._findSavedRegion(pRegionID);
|
|
2265
|
+
if (!tmpRegion) return;
|
|
2266
|
+
for (let tmpKey in pPrevious)
|
|
2267
|
+
{
|
|
2268
|
+
tmpRegion[tmpKey] = pPrevious[tmpKey];
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
/**
|
|
2273
|
+
* Look up a saved region by ID.
|
|
2274
|
+
*/
|
|
2275
|
+
_findSavedRegion(pRegionID)
|
|
2276
|
+
{
|
|
2277
|
+
for (let i = 0; i < this._savedRegions.length; i++)
|
|
2278
|
+
{
|
|
2279
|
+
if (this._savedRegions[i].ID === pRegionID)
|
|
2280
|
+
{
|
|
2281
|
+
return this._savedRegions[i];
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
return null;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
/**
|
|
2288
|
+
* Cancel handler — also exits edit mode if active.
|
|
2289
|
+
*/
|
|
2290
|
+
cancelSelection()
|
|
2291
|
+
{
|
|
2292
|
+
if (this._editingRegionID)
|
|
2293
|
+
{
|
|
2294
|
+
this._exitRegionEditMode();
|
|
2295
|
+
return;
|
|
812
2296
|
}
|
|
2297
|
+
return this._cancelNewSelection();
|
|
813
2298
|
}
|
|
814
2299
|
}
|
|
815
2300
|
|