@epam/pdf-highlighter-kit 0.0.3 → 0.0.4
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 +185 -61
- package/dist/PDFHighlightViewer.d.ts +31 -24
- package/dist/PDFHighlightViewer.d.ts.map +1 -1
- package/dist/PDFHighlightViewer.js +332 -292
- package/dist/PDFHighlightViewer.js.map +1 -1
- package/dist/api.d.ts +7 -12
- package/dist/api.d.ts.map +1 -1
- package/dist/core/interaction-handler.d.ts +1 -1
- package/dist/core/interaction-handler.d.ts.map +1 -1
- package/dist/core/interaction-handler.js +43 -31
- package/dist/core/interaction-handler.js.map +1 -1
- package/dist/core/performance-optimizer.d.ts +3 -3
- package/dist/core/performance-optimizer.d.ts.map +1 -1
- package/dist/core/performance-optimizer.js +7 -8
- package/dist/core/performance-optimizer.js.map +1 -1
- package/dist/core/text-segmentation.d.ts +8 -7
- package/dist/core/text-segmentation.d.ts.map +1 -1
- package/dist/core/text-segmentation.js +101 -133
- package/dist/core/text-segmentation.js.map +1 -1
- package/dist/core/unified-layer-builder.d.ts +7 -8
- package/dist/core/unified-layer-builder.d.ts.map +1 -1
- package/dist/core/unified-layer-builder.js +124 -174
- package/dist/core/unified-layer-builder.js.map +1 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +32 -48
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/highlight-adapter.d.ts +2 -30
- package/dist/utils/highlight-adapter.d.ts.map +1 -1
- package/dist/utils/highlight-adapter.js +21 -197
- package/dist/utils/highlight-adapter.js.map +1 -1
- package/package.json +1 -1
- package/dist/core/style-manager.d.ts +0 -88
- package/dist/core/style-manager.d.ts.map +0 -1
- package/dist/core/style-manager.js +0 -413
- package/dist/core/style-manager.js.map +0 -1
|
@@ -3,26 +3,30 @@ import { ViewportManager } from './core/viewport-manager';
|
|
|
3
3
|
import { UnifiedLayerBuilder } from './core/unified-layer-builder';
|
|
4
4
|
import { UnifiedInteractionHandler } from './core/interaction-handler';
|
|
5
5
|
import { PerformanceOptimizer } from './core/performance-optimizer';
|
|
6
|
-
import {
|
|
7
|
-
import { adaptHighlightData } from './utils/highlight-adapter';
|
|
6
|
+
import { buildHighlightsIndex } from './utils/highlight-adapter';
|
|
8
7
|
export class PDFHighlightViewer {
|
|
9
8
|
constructor() {
|
|
10
9
|
this.container = null;
|
|
11
10
|
this.pdfContainer = null;
|
|
12
11
|
this.pageContainers = new Map();
|
|
13
|
-
this.
|
|
12
|
+
this.highlightsIndex = {
|
|
13
|
+
highlights: [],
|
|
14
|
+
byId: new Map(),
|
|
15
|
+
pages: {},
|
|
16
|
+
occurrences: [],
|
|
17
|
+
};
|
|
14
18
|
this.currentPage = 1;
|
|
15
19
|
this.currentScale = 1.5;
|
|
16
20
|
this.totalPages = 0;
|
|
17
21
|
this.selectedTermId = null;
|
|
18
22
|
this.isInitialized = false;
|
|
23
|
+
this.navIndex = -1;
|
|
19
24
|
this.pageDimensions = new Map();
|
|
20
25
|
this.defaultPageHeight = 800;
|
|
21
26
|
this.eventListeners = [];
|
|
22
27
|
this.scrollListener = null;
|
|
23
28
|
this.analytics = {
|
|
24
29
|
totalHighlights: 0,
|
|
25
|
-
categoryBreakdown: {},
|
|
26
30
|
mostViewedPages: [],
|
|
27
31
|
interactionHeatmap: {},
|
|
28
32
|
averageTimePerPage: 0,
|
|
@@ -58,22 +62,25 @@ export class PDFHighlightViewer {
|
|
|
58
62
|
this.emit('selectionCopied', { text: selection, format });
|
|
59
63
|
}
|
|
60
64
|
},
|
|
61
|
-
createHighlightFromSelection: (
|
|
65
|
+
createHighlightFromSelection: (style) => {
|
|
62
66
|
const selectionData = this.textSelection.getSelectionWithContext();
|
|
63
67
|
if (!selectionData)
|
|
64
68
|
return null;
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
const id = `selection-${Date.now()}`;
|
|
70
|
+
const highlight = {
|
|
71
|
+
id,
|
|
72
|
+
bboxes: [], // still TODO: compute bboxes from selection
|
|
73
|
+
style,
|
|
74
|
+
tooltipText: selectionData.text,
|
|
75
|
+
metadata: {
|
|
76
|
+
pages: selectionData.pages,
|
|
77
|
+
context: selectionData.context,
|
|
78
|
+
range: selectionData.range,
|
|
79
|
+
},
|
|
70
80
|
};
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
});
|
|
75
|
-
this.emit('selectionHighlighted', { text: selectionData.text, category, coordinates: [] });
|
|
76
|
-
return occurrence;
|
|
81
|
+
this.addHighlight(highlight);
|
|
82
|
+
this.emit('selectionHighlighted', { text: selectionData.text, termId: id });
|
|
83
|
+
return highlight;
|
|
77
84
|
},
|
|
78
85
|
};
|
|
79
86
|
// =============================================================================
|
|
@@ -124,7 +131,6 @@ export class PDFHighlightViewer {
|
|
|
124
131
|
maxCacheSize: this.options.maxCachedPages ? this.options.maxCachedPages * 10 : 100,
|
|
125
132
|
frameBudget: this.options.performanceMode ? 8 : 16,
|
|
126
133
|
});
|
|
127
|
-
this.styleManager = new CategoryStyleManager();
|
|
128
134
|
// Setup interaction callbacks
|
|
129
135
|
const interactionCallbacks = {
|
|
130
136
|
onHighlightHover: (event) => this.emit('highlightHover', event),
|
|
@@ -252,31 +258,6 @@ export class PDFHighlightViewer {
|
|
|
252
258
|
opacity: 0.3;
|
|
253
259
|
}
|
|
254
260
|
|
|
255
|
-
/* Category-specific highlight colors */
|
|
256
|
-
.protein-highlight .highlight-background {
|
|
257
|
-
background-color: #ff6b6b;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
.species-highlight .highlight-background {
|
|
261
|
-
background-color: #4ecdc4;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
.chemical-highlight .highlight-background {
|
|
265
|
-
background-color: #45b7d1;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
.disease-highlight .highlight-background {
|
|
269
|
-
background-color: #f7b731;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
.gene-highlight .highlight-background {
|
|
273
|
-
background-color: #5f27cd;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
.cell_line-highlight .highlight-background {
|
|
277
|
-
background-color: #00d2d3;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
261
|
/* Text segment styling */
|
|
281
262
|
.text-segment {
|
|
282
263
|
position: relative;
|
|
@@ -339,6 +320,12 @@ export class PDFHighlightViewer {
|
|
|
339
320
|
};
|
|
340
321
|
this.container.addEventListener('scroll', this.scrollListener);
|
|
341
322
|
}
|
|
323
|
+
getPageScrollTop(pageNumber) {
|
|
324
|
+
const el = this.pageContainers.get(pageNumber);
|
|
325
|
+
if (el)
|
|
326
|
+
return el.offsetTop;
|
|
327
|
+
return this.viewportManager.getScrollPositionForPage(pageNumber);
|
|
328
|
+
}
|
|
342
329
|
/**
|
|
343
330
|
* Handle scroll events for virtual viewport management
|
|
344
331
|
*/
|
|
@@ -502,6 +489,9 @@ export class PDFHighlightViewer {
|
|
|
502
489
|
await this.createPageContainers();
|
|
503
490
|
// Load initial pages
|
|
504
491
|
await this.loadInitialPages();
|
|
492
|
+
if (this.options.enableVirtualScrolling === false) {
|
|
493
|
+
await this.renderAllPagesBatched(2);
|
|
494
|
+
}
|
|
505
495
|
this.emit('pdfLoaded', { totalPages: this.totalPages });
|
|
506
496
|
}
|
|
507
497
|
catch (error) {
|
|
@@ -575,6 +565,21 @@ export class PDFHighlightViewer {
|
|
|
575
565
|
});
|
|
576
566
|
}, 100);
|
|
577
567
|
}
|
|
568
|
+
async renderAllPagesBatched(batchSize = 2) {
|
|
569
|
+
for (let i = 1; i <= this.totalPages; i += batchSize) {
|
|
570
|
+
const batch = [];
|
|
571
|
+
for (let j = i; j < i + batchSize && j <= this.totalPages; j++) {
|
|
572
|
+
const pageContainer = this.pageContainers.get(j);
|
|
573
|
+
if (pageContainer && !pageContainer.classList.contains('rendered')) {
|
|
574
|
+
batch.push(this.renderPage(j).catch((error) => {
|
|
575
|
+
console.debug(`Failed to render page ${j}:`, error);
|
|
576
|
+
}));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
await Promise.all(batch);
|
|
580
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
581
|
+
}
|
|
582
|
+
}
|
|
578
583
|
/**
|
|
579
584
|
* Render a specific page
|
|
580
585
|
*/
|
|
@@ -626,9 +631,8 @@ export class PDFHighlightViewer {
|
|
|
626
631
|
setPage(pageNumber) {
|
|
627
632
|
if (pageNumber < 1 || pageNumber > this.totalPages)
|
|
628
633
|
return;
|
|
629
|
-
const pagePosition = this.viewportManager.getScrollPositionForPage(pageNumber);
|
|
630
634
|
if (this.container) {
|
|
631
|
-
this.container.scrollTop =
|
|
635
|
+
this.container.scrollTop = this.getPageScrollTop(pageNumber);
|
|
632
636
|
}
|
|
633
637
|
}
|
|
634
638
|
getZoom() {
|
|
@@ -713,73 +717,84 @@ export class PDFHighlightViewer {
|
|
|
713
717
|
// Highlight Management
|
|
714
718
|
// =============================================================================
|
|
715
719
|
loadHighlights(data) {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
categoryResolver: (highlight) => highlight.metadata?.category || 'default',
|
|
719
|
-
termNameResolver: (highlight) => highlight.metadata?.term || highlight.id,
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
else {
|
|
723
|
-
this.highlightData = data;
|
|
724
|
-
}
|
|
720
|
+
this.highlightsIndex = buildHighlightsIndex(data);
|
|
721
|
+
this.navIndex = -1;
|
|
725
722
|
this.updateAnalytics();
|
|
726
|
-
//
|
|
723
|
+
// refresh rendered pages
|
|
727
724
|
this.updateAllUnifiedLayers();
|
|
728
|
-
// Update spatial indices
|
|
729
725
|
this.buildAllSpatialIndices();
|
|
730
|
-
this.emit('highlightsLoaded', {
|
|
731
|
-
}
|
|
732
|
-
addHighlight(
|
|
733
|
-
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
this.
|
|
744
|
-
this.
|
|
726
|
+
this.emit('highlightsLoaded', { count: data.length });
|
|
727
|
+
}
|
|
728
|
+
addHighlight(highlight) {
|
|
729
|
+
const prev = this.highlightsIndex.byId.get(highlight.id);
|
|
730
|
+
const affectedPages = new Set();
|
|
731
|
+
prev?.bboxes?.forEach((b) => affectedPages.add(b.page));
|
|
732
|
+
highlight.bboxes.forEach((b) => affectedPages.add(b.page));
|
|
733
|
+
const nextList = [...this.highlightsIndex.highlights];
|
|
734
|
+
const idx = nextList.findIndex((h) => h.id === highlight.id);
|
|
735
|
+
if (idx >= 0)
|
|
736
|
+
nextList[idx] = highlight;
|
|
737
|
+
else
|
|
738
|
+
nextList.push(highlight);
|
|
739
|
+
this.highlightsIndex = buildHighlightsIndex(nextList);
|
|
740
|
+
this.navIndex = -1;
|
|
745
741
|
this.updateAnalytics();
|
|
746
|
-
|
|
742
|
+
// Refresh only affected pages
|
|
743
|
+
for (const pageNumber of affectedPages) {
|
|
744
|
+
this.refreshHighlightLayerForPage(pageNumber);
|
|
745
|
+
this.updatePageUnifiedLayer(pageNumber);
|
|
746
|
+
this.buildSpatialIndexForPage(pageNumber);
|
|
747
|
+
}
|
|
748
|
+
this.emit('highlightAdded', { highlight, pages: Array.from(affectedPages) });
|
|
747
749
|
}
|
|
748
750
|
removeHighlight(termId) {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
});
|
|
762
|
-
// Remove from terms
|
|
763
|
-
delete this.highlightData[category].terms[termId];
|
|
764
|
-
});
|
|
765
|
-
if (found) {
|
|
766
|
-
this.updateAnalytics();
|
|
767
|
-
this.emit('highlightRemoved', { termId });
|
|
751
|
+
const prev = this.highlightsIndex.byId.get(termId);
|
|
752
|
+
if (!prev)
|
|
753
|
+
return;
|
|
754
|
+
const affectedPages = new Set();
|
|
755
|
+
prev.bboxes.forEach((b) => affectedPages.add(b.page));
|
|
756
|
+
const nextList = this.highlightsIndex.highlights.filter((h) => h.id !== termId);
|
|
757
|
+
this.highlightsIndex = buildHighlightsIndex(nextList);
|
|
758
|
+
this.navIndex = -1;
|
|
759
|
+
// If removed highlight was selected — clear selection visuals
|
|
760
|
+
if (this.selectedTermId === termId) {
|
|
761
|
+
this.selectedTermId = null;
|
|
762
|
+
this.clearSelectedTermHighlighting();
|
|
768
763
|
}
|
|
764
|
+
this.updateAnalytics();
|
|
765
|
+
for (const pageNumber of affectedPages) {
|
|
766
|
+
this.refreshHighlightLayerForPage(pageNumber);
|
|
767
|
+
this.updatePageUnifiedLayer(pageNumber);
|
|
768
|
+
this.buildSpatialIndexForPage(pageNumber);
|
|
769
|
+
}
|
|
770
|
+
this.emit('highlightRemoved', { termId, pages: Array.from(affectedPages) });
|
|
769
771
|
}
|
|
770
|
-
updateHighlightStyle(
|
|
771
|
-
this.
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
772
|
+
updateHighlightStyle(termId, stylePatch) {
|
|
773
|
+
const prev = this.highlightsIndex.byId.get(termId);
|
|
774
|
+
if (!prev)
|
|
775
|
+
return;
|
|
776
|
+
const next = {
|
|
777
|
+
...prev,
|
|
778
|
+
style: {
|
|
779
|
+
...(prev.style ?? {}),
|
|
780
|
+
...stylePatch,
|
|
781
|
+
},
|
|
782
|
+
};
|
|
783
|
+
const affectedPages = new Set();
|
|
784
|
+
prev.bboxes.forEach((b) => affectedPages.add(b.page));
|
|
785
|
+
const nextList = [...this.highlightsIndex.highlights];
|
|
786
|
+
const idx = nextList.findIndex((h) => h.id === termId);
|
|
787
|
+
if (idx >= 0)
|
|
788
|
+
nextList[idx] = next;
|
|
789
|
+
this.highlightsIndex = buildHighlightsIndex(nextList);
|
|
790
|
+
this.navIndex = -1;
|
|
791
|
+
this.updateAnalytics();
|
|
792
|
+
for (const pageNumber of affectedPages) {
|
|
793
|
+
this.refreshHighlightLayerForPage(pageNumber);
|
|
794
|
+
this.updatePageUnifiedLayer(pageNumber);
|
|
795
|
+
this.buildSpatialIndexForPage(pageNumber);
|
|
796
|
+
}
|
|
797
|
+
this.emit('styleUpdated', { termId, style: next.style, patch: stylePatch });
|
|
783
798
|
}
|
|
784
799
|
/**
|
|
785
800
|
* Update unified layer for a specific page
|
|
@@ -789,9 +804,9 @@ export class PDFHighlightViewer {
|
|
|
789
804
|
if (!pageContainer)
|
|
790
805
|
return;
|
|
791
806
|
const pageData = this.pdfEngine.getPageData(pageNumber);
|
|
792
|
-
if (!pageData
|
|
807
|
+
if (!pageData?.textContent)
|
|
793
808
|
return;
|
|
794
|
-
this.layerBuilder.updateHighlights(this.
|
|
809
|
+
this.layerBuilder.updateHighlights(pageContainer, this.highlightsIndex.highlights, pageNumber, pageData.textContent, this.currentScale);
|
|
795
810
|
}
|
|
796
811
|
/**
|
|
797
812
|
* Update all unified layers
|
|
@@ -839,7 +854,7 @@ export class PDFHighlightViewer {
|
|
|
839
854
|
totalHeight += newDimensions.height;
|
|
840
855
|
validDimensions++;
|
|
841
856
|
}
|
|
842
|
-
catch
|
|
857
|
+
catch {
|
|
843
858
|
pageContainer.style.height = `${this.defaultPageHeight}px`;
|
|
844
859
|
}
|
|
845
860
|
}
|
|
@@ -864,60 +879,91 @@ export class PDFHighlightViewer {
|
|
|
864
879
|
// =============================================================================
|
|
865
880
|
// Navigation
|
|
866
881
|
// =============================================================================
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
for (const
|
|
870
|
-
for (
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
const page = parseInt(pageNumber);
|
|
874
|
-
this.setPage(page);
|
|
875
|
-
// TODO: Scroll to specific coordinates within page
|
|
876
|
-
this.emit('navigationComplete', { termId, pageNumber: page, occurrenceIndex });
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
882
|
+
getNavOccurrences() {
|
|
883
|
+
const list = [];
|
|
884
|
+
for (const h of this.highlightsIndex.highlights) {
|
|
885
|
+
for (let i = 0; i < h.bboxes.length; i++) {
|
|
886
|
+
const b = h.bboxes[i];
|
|
887
|
+
list.push({ termId: h.id, pageNumber: b.page, occurrenceIndex: i, x1: b.x1, y1: b.y1 });
|
|
879
888
|
}
|
|
880
889
|
}
|
|
890
|
+
list.sort((a, b) => a.pageNumber - b.pageNumber || a.y1 - b.y1 || a.x1 - b.x1);
|
|
891
|
+
return list;
|
|
881
892
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
893
|
+
goToHighlight(termId, occurrenceIndex = 0) {
|
|
894
|
+
const highlight = this.getHighlightById(termId);
|
|
895
|
+
if (!highlight)
|
|
896
|
+
return;
|
|
897
|
+
const bbox = highlight.bboxes[occurrenceIndex];
|
|
898
|
+
if (!bbox)
|
|
899
|
+
return;
|
|
900
|
+
const page = bbox.page;
|
|
901
|
+
this.highlightSelectedTerm(termId);
|
|
902
|
+
this.setPage(page);
|
|
903
|
+
void this.renderPage(page)
|
|
904
|
+
.then(() => {
|
|
905
|
+
const pageContainer = this.pageContainers.get(page);
|
|
906
|
+
if (!pageContainer || !this.container) {
|
|
907
|
+
this.emit('navigationComplete', { termId, pageNumber: page, occurrenceIndex });
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
const pageTop = pageContainer.offsetTop;
|
|
911
|
+
const y = bbox.y1 * this.currentScale;
|
|
912
|
+
this.container.scrollTop = Math.max(0, pageTop + y - 60);
|
|
913
|
+
this.emit('navigationComplete', { termId, pageNumber: page, occurrenceIndex });
|
|
914
|
+
})
|
|
915
|
+
.catch((error) => {
|
|
916
|
+
console.error('goToHighlight render/scroll failed:', error);
|
|
917
|
+
this.emit('navigationError', { termId, pageNumber: page, occurrenceIndex, error });
|
|
918
|
+
});
|
|
885
919
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
920
|
+
nextHighlight() {
|
|
921
|
+
const list = this.getNavOccurrences();
|
|
922
|
+
if (list.length === 0)
|
|
923
|
+
return;
|
|
924
|
+
this.navIndex = (this.navIndex + 1) % list.length;
|
|
925
|
+
const next = list[this.navIndex];
|
|
926
|
+
this.goToHighlight(next.termId, next.occurrenceIndex);
|
|
927
|
+
}
|
|
928
|
+
previousHighlight() {
|
|
929
|
+
const list = this.getNavOccurrences();
|
|
930
|
+
if (list.length === 0)
|
|
931
|
+
return;
|
|
932
|
+
this.navIndex = (this.navIndex - 1 + list.length) % list.length;
|
|
933
|
+
const prev = list[this.navIndex];
|
|
934
|
+
this.goToHighlight(prev.termId, prev.occurrenceIndex);
|
|
889
935
|
}
|
|
890
936
|
goToCoordinate(pageNumber, x, y) {
|
|
891
937
|
this.setPage(pageNumber);
|
|
892
|
-
|
|
938
|
+
if (!this.container)
|
|
939
|
+
return;
|
|
940
|
+
const pageTop = this.getPageScrollTop(pageNumber);
|
|
941
|
+
const targetY = pageTop + y * this.currentScale - this.container.clientHeight * 0.3;
|
|
942
|
+
this.container.scrollTop = Math.max(0, targetY);
|
|
893
943
|
this.emit('coordinateNavigation', { pageNumber, x, y });
|
|
894
944
|
}
|
|
895
945
|
// =============================================================================
|
|
896
946
|
// Search & Filter
|
|
897
947
|
// =============================================================================
|
|
898
|
-
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
948
|
+
searchHighlights(query) {
|
|
949
|
+
const q = query.trim().toLowerCase();
|
|
950
|
+
if (!q)
|
|
951
|
+
return [];
|
|
952
|
+
return this.highlightsIndex.highlights.filter((h) => {
|
|
953
|
+
if (h.id.toLowerCase().includes(q))
|
|
954
|
+
return true;
|
|
955
|
+
if ((h.tooltipText ?? '').toLowerCase().includes(q))
|
|
956
|
+
return true;
|
|
957
|
+
if (h.metadata) {
|
|
958
|
+
try {
|
|
959
|
+
return JSON.stringify(h.metadata).toLowerCase().includes(q);
|
|
905
960
|
}
|
|
906
|
-
|
|
961
|
+
catch {
|
|
962
|
+
// ignore non-serializable metadata
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return false;
|
|
907
966
|
});
|
|
908
|
-
return results;
|
|
909
|
-
}
|
|
910
|
-
filterByCategory(categories) {
|
|
911
|
-
// TODO: Implement category filtering
|
|
912
|
-
console.log('filterByCategory not yet implemented:', categories);
|
|
913
|
-
}
|
|
914
|
-
highlightSearchResults(query) {
|
|
915
|
-
// TODO: Implement search result highlighting
|
|
916
|
-
console.log('highlightSearchResults not yet implemented:', query);
|
|
917
|
-
}
|
|
918
|
-
clearSearchResults() {
|
|
919
|
-
// TODO: Implement search result clearing
|
|
920
|
-
console.log('clearSearchResults not yet implemented');
|
|
921
967
|
}
|
|
922
968
|
// =============================================================================
|
|
923
969
|
// Interaction Modes
|
|
@@ -933,14 +979,13 @@ export class PDFHighlightViewer {
|
|
|
933
979
|
// =============================================================================
|
|
934
980
|
getPerformanceMetrics() {
|
|
935
981
|
const baseMetrics = this.performanceOptimizer.getPerformanceMetrics();
|
|
936
|
-
|
|
937
|
-
const
|
|
938
|
-
const memoryEstimate = renderedPages * 2; // ~2MB per rendered page
|
|
982
|
+
const renderedPages = Array.from(this.pageContainers.values()).filter((container) => container.classList.contains('rendered')).length;
|
|
983
|
+
const memoryEstimate = renderedPages * 2; // ~2MB per rendered page (rough estimate)
|
|
939
984
|
return {
|
|
940
985
|
...baseMetrics,
|
|
941
986
|
memoryUsage: {
|
|
942
987
|
pages: renderedPages,
|
|
943
|
-
highlights:
|
|
988
|
+
highlights: this.highlightsIndex.highlights.length,
|
|
944
989
|
cache: this.pageDimensions.size,
|
|
945
990
|
total: memoryEstimate,
|
|
946
991
|
},
|
|
@@ -1037,6 +1082,18 @@ export class PDFHighlightViewer {
|
|
|
1037
1082
|
// =============================================================================
|
|
1038
1083
|
// Private Helper Methods
|
|
1039
1084
|
// =============================================================================
|
|
1085
|
+
refreshHighlightLayerForPage(pageNumber) {
|
|
1086
|
+
const pageContainer = this.pageContainers.get(pageNumber);
|
|
1087
|
+
if (!pageContainer || !pageContainer.classList.contains('rendered'))
|
|
1088
|
+
return;
|
|
1089
|
+
const existingHighlightLayer = pageContainer.querySelector('.highlight-layer');
|
|
1090
|
+
if (existingHighlightLayer)
|
|
1091
|
+
existingHighlightLayer.remove();
|
|
1092
|
+
const canvas = pageContainer.querySelector('canvas');
|
|
1093
|
+
if (!canvas)
|
|
1094
|
+
return;
|
|
1095
|
+
this.addHighlightsToPage(pageNumber, canvas.width, canvas.height);
|
|
1096
|
+
}
|
|
1040
1097
|
/**
|
|
1041
1098
|
* Add highlights to a rendered page
|
|
1042
1099
|
*/
|
|
@@ -1048,12 +1105,8 @@ export class PDFHighlightViewer {
|
|
|
1048
1105
|
if (pageContainer.querySelector('.highlight-layer')) {
|
|
1049
1106
|
return;
|
|
1050
1107
|
}
|
|
1051
|
-
// Get the actual PDF page to get proper dimensions
|
|
1052
1108
|
try {
|
|
1053
|
-
// Simple approach - just scale coordinates by the current zoom level
|
|
1054
|
-
// The coordinates in the JSON appear to be at scale 1.0
|
|
1055
1109
|
const scale = this.currentScale;
|
|
1056
|
-
// Create highlight layer
|
|
1057
1110
|
const highlightLayer = document.createElement('div');
|
|
1058
1111
|
highlightLayer.className = 'highlight-layer';
|
|
1059
1112
|
highlightLayer.style.position = 'absolute';
|
|
@@ -1061,78 +1114,73 @@ export class PDFHighlightViewer {
|
|
|
1061
1114
|
highlightLayer.style.left = '0';
|
|
1062
1115
|
highlightLayer.style.width = `${canvasWidth}px`;
|
|
1063
1116
|
highlightLayer.style.height = `${canvasHeight}px`;
|
|
1064
|
-
highlightLayer.style.zIndex = '2';
|
|
1117
|
+
highlightLayer.style.zIndex = '2';
|
|
1065
1118
|
highlightLayer.style.pointerEvents = 'none';
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
allBoxes.forEach((highlightBox) => {
|
|
1122
|
-
highlightBox.style.opacity = originalOpacity;
|
|
1123
|
-
});
|
|
1124
|
-
}
|
|
1125
|
-
else {
|
|
1126
|
-
highlightDiv.style.opacity = originalOpacity;
|
|
1127
|
-
}
|
|
1119
|
+
for (const highlight of this.highlightsIndex.highlights) {
|
|
1120
|
+
const style = highlight.style;
|
|
1121
|
+
const backgroundColor = style?.backgroundColor ?? '#666666';
|
|
1122
|
+
const borderColor = style?.borderColor ?? backgroundColor;
|
|
1123
|
+
const borderWidth = style?.borderWidth ?? '1px';
|
|
1124
|
+
for (let bboxIndex = 0; bboxIndex < highlight.bboxes.length; bboxIndex++) {
|
|
1125
|
+
const bbox = highlight.bboxes[bboxIndex];
|
|
1126
|
+
if (bbox.page !== pageNumber)
|
|
1127
|
+
continue;
|
|
1128
|
+
const highlightDiv = document.createElement('div');
|
|
1129
|
+
highlightDiv.className = 'highlight';
|
|
1130
|
+
highlightDiv.setAttribute('data-term-id', highlight.id);
|
|
1131
|
+
highlightDiv.setAttribute('data-page', String(pageNumber));
|
|
1132
|
+
highlightDiv.setAttribute('data-bbox-index', String(bboxIndex));
|
|
1133
|
+
const left = bbox.x1 * scale;
|
|
1134
|
+
const top = bbox.y1 * scale;
|
|
1135
|
+
const width = (bbox.x2 - bbox.x1) * scale;
|
|
1136
|
+
const height = (bbox.y2 - bbox.y1) * scale;
|
|
1137
|
+
highlightDiv.style.position = 'absolute';
|
|
1138
|
+
highlightDiv.style.left = `${left}px`;
|
|
1139
|
+
highlightDiv.style.top = `${top}px`;
|
|
1140
|
+
highlightDiv.style.width = `${width}px`;
|
|
1141
|
+
highlightDiv.style.height = `${height}px`;
|
|
1142
|
+
highlightDiv.style.backgroundColor = backgroundColor;
|
|
1143
|
+
highlightDiv.style.border = `${borderWidth} solid ${borderColor}`;
|
|
1144
|
+
highlightDiv.style.pointerEvents = 'auto';
|
|
1145
|
+
highlightDiv.style.cursor = 'pointer';
|
|
1146
|
+
highlightDiv.style.boxSizing = 'border-box';
|
|
1147
|
+
highlightDiv.style.userSelect = 'none';
|
|
1148
|
+
highlightDiv.style.mixBlendMode = 'multiply';
|
|
1149
|
+
const baseOpacity = typeof style?.opacity === 'number' ? style.opacity : 0.3;
|
|
1150
|
+
const overlappingCount = this.countOverlappingHighlights(highlightLayer, bbox, scale);
|
|
1151
|
+
const effectiveOpacity = Math.max(0.05, baseOpacity / Math.max(1, overlappingCount * 0.7));
|
|
1152
|
+
highlightDiv.style.opacity = effectiveOpacity.toString();
|
|
1153
|
+
highlightDiv.dataset.originalOpacity = effectiveOpacity.toString();
|
|
1154
|
+
const hoverOpacity = typeof style?.hoverOpacity === 'number'
|
|
1155
|
+
? style.hoverOpacity
|
|
1156
|
+
: Math.min(0.6, effectiveOpacity + 0.2);
|
|
1157
|
+
highlightDiv.addEventListener('mouseenter', () => {
|
|
1158
|
+
if (this.options.highlightsConfig?.enableMultilineHover) {
|
|
1159
|
+
const same = highlightLayer.querySelectorAll(`[data-term-id="${highlight.id}"]`);
|
|
1160
|
+
same.forEach((el) => (el.style.opacity = String(hoverOpacity)));
|
|
1161
|
+
const other = highlightLayer.querySelectorAll(`div[data-term-id]:not([data-term-id="${highlight.id}"])`);
|
|
1162
|
+
other.forEach((el) => (el.style.opacity = '0.1'));
|
|
1163
|
+
}
|
|
1164
|
+
else {
|
|
1165
|
+
highlightDiv.style.opacity = String(hoverOpacity);
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
highlightDiv.addEventListener('mouseleave', () => {
|
|
1169
|
+
if (this.options.highlightsConfig?.enableMultilineHover) {
|
|
1170
|
+
const all = highlightLayer.querySelectorAll(`div[data-term-id]`);
|
|
1171
|
+
all.forEach((el) => {
|
|
1172
|
+
const original = el.dataset.originalOpacity ?? '0.3';
|
|
1173
|
+
el.style.opacity = original;
|
|
1128
1174
|
});
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1175
|
+
}
|
|
1176
|
+
else {
|
|
1177
|
+
highlightDiv.style.opacity = highlightDiv.dataset.originalOpacity ?? '0.3';
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
highlightLayer.appendChild(highlightDiv);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1134
1183
|
pageContainer.appendChild(highlightLayer);
|
|
1135
|
-
// Apply selected term highlighting if there's a selected term
|
|
1136
1184
|
if (this.selectedTermId) {
|
|
1137
1185
|
this.applySelectionToPage(pageNumber);
|
|
1138
1186
|
}
|
|
@@ -1141,76 +1189,66 @@ export class PDFHighlightViewer {
|
|
|
1141
1189
|
console.error(`Failed to add highlights to page ${pageNumber}:`, error);
|
|
1142
1190
|
}
|
|
1143
1191
|
}
|
|
1192
|
+
getHighlightById(termId) {
|
|
1193
|
+
const byId = this.highlightsIndex?.byId;
|
|
1194
|
+
if (byId && typeof byId.get === 'function') {
|
|
1195
|
+
const fromMap = byId.get(termId);
|
|
1196
|
+
if (fromMap)
|
|
1197
|
+
return fromMap;
|
|
1198
|
+
}
|
|
1199
|
+
return this.highlightsIndex.highlights.find((h) => h.id === termId);
|
|
1200
|
+
}
|
|
1201
|
+
getHighlightStyle(termId) {
|
|
1202
|
+
return this.getHighlightById(termId)?.style;
|
|
1203
|
+
}
|
|
1144
1204
|
/**
|
|
1145
1205
|
* Update highlights colors for specified page
|
|
1146
1206
|
* */
|
|
1147
1207
|
updateHighlightsStyles(pageNumber, hoveredIds) {
|
|
1148
1208
|
const pageContainer = this.pageContainers.get(pageNumber);
|
|
1149
|
-
if (!pageContainer)
|
|
1209
|
+
if (!pageContainer)
|
|
1150
1210
|
return;
|
|
1151
|
-
}
|
|
1152
1211
|
const highlightLayer = pageContainer.querySelector('.highlight-layer');
|
|
1153
|
-
if (!highlightLayer)
|
|
1212
|
+
if (!highlightLayer)
|
|
1154
1213
|
return;
|
|
1155
|
-
}
|
|
1156
|
-
// Find all highlights in this page
|
|
1157
1214
|
const allHighlights = pageContainer.querySelectorAll('.highlight, .highlight-wrapper');
|
|
1158
|
-
allHighlights.forEach((
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1215
|
+
allHighlights.forEach((el) => {
|
|
1216
|
+
const termId = el.getAttribute('data-term-id');
|
|
1217
|
+
if (!termId)
|
|
1218
|
+
return;
|
|
1219
|
+
const style = this.getHighlightStyle(termId);
|
|
1220
|
+
const bg = style?.backgroundColor ?? el.style.backgroundColor ?? '#666666';
|
|
1221
|
+
const borderColor = style?.borderColor ?? bg;
|
|
1222
|
+
const borderWidth = style?.borderWidth ?? '1px';
|
|
1223
|
+
el.style.backgroundColor = bg;
|
|
1224
|
+
el.style.border = `${borderWidth} solid ${borderColor}`;
|
|
1225
|
+
if (this.options.highlightsConfig?.enableMultilineHover &&
|
|
1226
|
+
hoveredIds &&
|
|
1227
|
+
Array.isArray(hoveredIds)) {
|
|
1228
|
+
const baseOpacity = typeof style?.opacity === 'number'
|
|
1229
|
+
? style.opacity
|
|
1230
|
+
: parseFloat(el.dataset.originalOpacity ?? '0.3');
|
|
1231
|
+
const hoverOpacity = typeof style?.hoverOpacity === 'number'
|
|
1232
|
+
? style.hoverOpacity
|
|
1233
|
+
: Math.min(0.6, baseOpacity + 0.2);
|
|
1234
|
+
if (hoveredIds.includes(termId)) {
|
|
1235
|
+
el.style.opacity = String(hoverOpacity);
|
|
1236
|
+
}
|
|
1237
|
+
else if (hoveredIds.length > 0) {
|
|
1238
|
+
el.style.opacity = '0.1';
|
|
1239
|
+
}
|
|
1240
|
+
else {
|
|
1241
|
+
el.style.opacity = String(baseOpacity);
|
|
1181
1242
|
}
|
|
1182
1243
|
}
|
|
1183
1244
|
});
|
|
1184
1245
|
}
|
|
1185
|
-
/**
|
|
1186
|
-
* Get highlight color
|
|
1187
|
-
*/
|
|
1188
|
-
getHighlightColor(termId, category) {
|
|
1189
|
-
if (this.options.highlightsConfig && this.options.highlightsConfig.getHighlightColor) {
|
|
1190
|
-
return this.options.highlightsConfig.getHighlightColor(termId);
|
|
1191
|
-
}
|
|
1192
|
-
return this.getCategoryColor(category);
|
|
1193
|
-
}
|
|
1194
|
-
/**
|
|
1195
|
-
* Get color for highlight category
|
|
1196
|
-
*/
|
|
1197
|
-
getCategoryColor(category) {
|
|
1198
|
-
const colors = {
|
|
1199
|
-
protein: '#ff6b6b',
|
|
1200
|
-
species: '#4ecdc4',
|
|
1201
|
-
chemical: '#45b7d1',
|
|
1202
|
-
disease: '#f7b731',
|
|
1203
|
-
gene: '#5f27cd',
|
|
1204
|
-
cell_line: '#00d2d3',
|
|
1205
|
-
};
|
|
1206
|
-
return colors[category] || '#666666';
|
|
1207
|
-
}
|
|
1208
1246
|
/**
|
|
1209
1247
|
* Build spatial index for a specific page
|
|
1210
1248
|
*/
|
|
1211
1249
|
buildSpatialIndexForPage(pageNumber) {
|
|
1212
|
-
const
|
|
1213
|
-
this.performanceOptimizer.buildSpatialIndex(
|
|
1250
|
+
const refs = this.highlightsIndex.pages[String(pageNumber)] ?? [];
|
|
1251
|
+
this.performanceOptimizer.buildSpatialIndex(refs, pageNumber);
|
|
1214
1252
|
}
|
|
1215
1253
|
/**
|
|
1216
1254
|
* Build spatial indices for all pages
|
|
@@ -1221,29 +1259,33 @@ export class PDFHighlightViewer {
|
|
|
1221
1259
|
}
|
|
1222
1260
|
}
|
|
1223
1261
|
/**
|
|
1224
|
-
* Get
|
|
1262
|
+
* Get the number of highlights on a given page.
|
|
1263
|
+
*
|
|
1264
|
+
* This reads from the precomputed `highlightsIndex.pages` map, which stores
|
|
1265
|
+
* arrays of highlight references keyed by the page number as a string.
|
|
1266
|
+
* If there are no references for the requested page, an empty array is used,
|
|
1267
|
+
* and the method returns 0.
|
|
1268
|
+
*
|
|
1269
|
+
* @param pageNumber - The 1-based page number to count highlights for.
|
|
1270
|
+
* @returns The total number of highlight references on the page.
|
|
1225
1271
|
*/
|
|
1226
1272
|
getHighlightCountForPage(pageNumber) {
|
|
1227
|
-
|
|
1273
|
+
const refs = this.highlightsIndex.pages[String(pageNumber)] ?? [];
|
|
1274
|
+
return refs.length;
|
|
1228
1275
|
}
|
|
1229
1276
|
/**
|
|
1230
|
-
* Update analytics
|
|
1277
|
+
* Update analytics information based on the current highlights index.
|
|
1278
|
+
*
|
|
1279
|
+
* This method recalculates the total number of highlights by reading the
|
|
1280
|
+
* length of `this.highlightsIndex.highlights` and updates the
|
|
1281
|
+
* `this.analytics` object with the new `totalHighlights` value, while
|
|
1282
|
+
* preserving all other existing analytics properties.
|
|
1231
1283
|
*/
|
|
1232
1284
|
updateAnalytics() {
|
|
1233
|
-
|
|
1234
|
-
const categoryBreakdown = {};
|
|
1235
|
-
Object.entries(this.highlightData).forEach(([category, categoryData]) => {
|
|
1236
|
-
let categoryCount = 0;
|
|
1237
|
-
Object.values(categoryData.pages).forEach((highlights) => {
|
|
1238
|
-
categoryCount += highlights.length;
|
|
1239
|
-
});
|
|
1240
|
-
categoryBreakdown[category] = categoryCount;
|
|
1241
|
-
totalHighlights += categoryCount;
|
|
1242
|
-
});
|
|
1285
|
+
const totalHighlights = this.highlightsIndex.highlights.length;
|
|
1243
1286
|
this.analytics = {
|
|
1244
1287
|
...this.analytics,
|
|
1245
1288
|
totalHighlights,
|
|
1246
|
-
categoryBreakdown,
|
|
1247
1289
|
};
|
|
1248
1290
|
}
|
|
1249
1291
|
/**
|
|
@@ -1535,14 +1577,12 @@ export class PDFHighlightViewer {
|
|
|
1535
1577
|
this.pdfEngine.destroy();
|
|
1536
1578
|
this.interactionHandler.destroy();
|
|
1537
1579
|
this.performanceOptimizer.destroy();
|
|
1538
|
-
this.styleManager.destroy();
|
|
1539
1580
|
// Clear DOM references
|
|
1540
1581
|
this.container = null;
|
|
1541
1582
|
this.pdfContainer = null;
|
|
1542
1583
|
this.pageContainers.clear();
|
|
1543
1584
|
// Clear state
|
|
1544
1585
|
this.eventListeners = [];
|
|
1545
|
-
this.highlightData = {};
|
|
1546
1586
|
this.isInitialized = false;
|
|
1547
1587
|
this.emit('destroyed');
|
|
1548
1588
|
}
|