@epam/pdf-highlighter-kit 0.0.1

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.
Files changed (77) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +219 -0
  3. package/dist/PDFHighlightViewer.d.ts +219 -0
  4. package/dist/PDFHighlightViewer.d.ts.map +1 -0
  5. package/dist/PDFHighlightViewer.js +1551 -0
  6. package/dist/PDFHighlightViewer.js.map +1 -0
  7. package/dist/api.d.ts +59 -0
  8. package/dist/api.d.ts.map +1 -0
  9. package/dist/api.js +2 -0
  10. package/dist/api.js.map +1 -0
  11. package/dist/config.d.ts +12 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +32 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/core/interaction-handler.d.ts +63 -0
  16. package/dist/core/interaction-handler.d.ts.map +1 -0
  17. package/dist/core/interaction-handler.js +430 -0
  18. package/dist/core/interaction-handler.js.map +1 -0
  19. package/dist/core/pdf-engine.d.ts +37 -0
  20. package/dist/core/pdf-engine.d.ts.map +1 -0
  21. package/dist/core/pdf-engine.js +281 -0
  22. package/dist/core/pdf-engine.js.map +1 -0
  23. package/dist/core/performance-optimizer.d.ts +91 -0
  24. package/dist/core/performance-optimizer.d.ts.map +1 -0
  25. package/dist/core/performance-optimizer.js +473 -0
  26. package/dist/core/performance-optimizer.js.map +1 -0
  27. package/dist/core/style-manager.d.ts +88 -0
  28. package/dist/core/style-manager.d.ts.map +1 -0
  29. package/dist/core/style-manager.js +413 -0
  30. package/dist/core/style-manager.js.map +1 -0
  31. package/dist/core/text-segmentation.d.ts +41 -0
  32. package/dist/core/text-segmentation.d.ts.map +1 -0
  33. package/dist/core/text-segmentation.js +338 -0
  34. package/dist/core/text-segmentation.js.map +1 -0
  35. package/dist/core/unified-layer-builder.d.ts +27 -0
  36. package/dist/core/unified-layer-builder.d.ts.map +1 -0
  37. package/dist/core/unified-layer-builder.js +331 -0
  38. package/dist/core/unified-layer-builder.js.map +1 -0
  39. package/dist/core/viewport-manager.d.ts +103 -0
  40. package/dist/core/viewport-manager.d.ts.map +1 -0
  41. package/dist/core/viewport-manager.js +222 -0
  42. package/dist/core/viewport-manager.js.map +1 -0
  43. package/dist/index.d.ts +27 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +57 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/pdf-highlight-viewer.css +488 -0
  48. package/dist/types.d.ts +279 -0
  49. package/dist/types.d.ts.map +1 -0
  50. package/dist/types.js +2 -0
  51. package/dist/types.js.map +1 -0
  52. package/dist/utils/highlight-adapter.d.ts +31 -0
  53. package/dist/utils/highlight-adapter.d.ts.map +1 -0
  54. package/dist/utils/highlight-adapter.js +202 -0
  55. package/dist/utils/highlight-adapter.js.map +1 -0
  56. package/dist/utils/pdf-utils.d.ts +14 -0
  57. package/dist/utils/pdf-utils.d.ts.map +1 -0
  58. package/dist/utils/pdf-utils.js +120 -0
  59. package/dist/utils/pdf-utils.js.map +1 -0
  60. package/dist/utils/worker-loader-simple.d.ts +8 -0
  61. package/dist/utils/worker-loader-simple.d.ts.map +1 -0
  62. package/dist/utils/worker-loader-simple.js +65 -0
  63. package/dist/utils/worker-loader-simple.js.map +1 -0
  64. package/dist/vite.config.d.ts +3 -0
  65. package/dist/vite.config.d.ts.map +1 -0
  66. package/dist/vite.config.js +20 -0
  67. package/dist/vite.config.js.map +1 -0
  68. package/dist/vitest.config.d.ts +3 -0
  69. package/dist/vitest.config.d.ts.map +1 -0
  70. package/dist/vitest.config.js +24 -0
  71. package/dist/vitest.config.js.map +1 -0
  72. package/dist/vitest.setup.d.ts +2 -0
  73. package/dist/vitest.setup.d.ts.map +1 -0
  74. package/dist/vitest.setup.js +8 -0
  75. package/dist/vitest.setup.js.map +1 -0
  76. package/package.json +74 -0
  77. package/styles/pdf-highlight-viewer.css +488 -0
@@ -0,0 +1,1551 @@
1
+ import { PDFEngine } from './core/pdf-engine';
2
+ import { ViewportManager } from './core/viewport-manager';
3
+ import { UnifiedLayerBuilder } from './core/unified-layer-builder';
4
+ import { UnifiedInteractionHandler } from './core/interaction-handler';
5
+ import { PerformanceOptimizer } from './core/performance-optimizer';
6
+ import { CategoryStyleManager } from './core/style-manager';
7
+ import { adaptHighlightData } from './utils/highlight-adapter';
8
+ export class PDFHighlightViewer {
9
+ constructor() {
10
+ this.container = null;
11
+ this.pdfContainer = null;
12
+ this.pageContainers = new Map();
13
+ this.highlightData = {};
14
+ this.currentPage = 1;
15
+ this.currentScale = 1.5;
16
+ this.totalPages = 0;
17
+ this.selectedTermId = null;
18
+ this.isInitialized = false;
19
+ this.pageDimensions = new Map();
20
+ this.defaultPageHeight = 800;
21
+ this.eventListeners = [];
22
+ this.scrollListener = null;
23
+ this.analytics = {
24
+ totalHighlights: 0,
25
+ categoryBreakdown: {},
26
+ mostViewedPages: [],
27
+ interactionHeatmap: {},
28
+ averageTimePerPage: 0,
29
+ };
30
+ // =============================================================================
31
+ // Text Selection Implementation
32
+ // =============================================================================
33
+ this.textSelection = {
34
+ enable: () => {
35
+ this.interactionHandler.setInteractionMode('select');
36
+ },
37
+ disable: () => {
38
+ this.interactionHandler.setInteractionMode('highlight');
39
+ },
40
+ getSelection: () => {
41
+ const selection = window.getSelection();
42
+ return selection ? selection.toString() : '';
43
+ },
44
+ getSelectionWithContext: () => {
45
+ return this.interactionHandler.getSelectionWithContext();
46
+ },
47
+ clearSelection: () => {
48
+ this.interactionHandler.clearSelection();
49
+ },
50
+ selectText: (range) => {
51
+ // TODO: Implement programmatic text selection
52
+ console.log('selectText not yet implemented:', range);
53
+ },
54
+ copySelection: (format = 'plain') => {
55
+ const selection = this.textSelection.getSelection();
56
+ if (selection) {
57
+ navigator.clipboard.writeText(selection).catch(console.error);
58
+ this.emit('selectionCopied', { text: selection, format });
59
+ }
60
+ },
61
+ createHighlightFromSelection: (category) => {
62
+ const selectionData = this.textSelection.getSelectionWithContext();
63
+ if (!selectionData)
64
+ return null;
65
+ // Create new term occurrence from selection
66
+ const termId = `selection-${Date.now()}`;
67
+ const occurrence = {
68
+ termId,
69
+ coordinates: [], // TODO: Calculate coordinates from selection
70
+ };
71
+ // Add to highlights
72
+ selectionData.pages.forEach((pageNumber) => {
73
+ this.addHighlight(pageNumber, occurrence);
74
+ });
75
+ this.emit('selectionHighlighted', { text: selectionData.text, category, coordinates: [] });
76
+ return occurrence;
77
+ },
78
+ };
79
+ // =============================================================================
80
+ // Accessibility
81
+ // =============================================================================
82
+ this.accessibility = {
83
+ enableKeyboardNavigation: () => {
84
+ // Already enabled in setupAccessibility
85
+ },
86
+ enableScreenReader: () => {
87
+ // Add screen reader support
88
+ if (this.container) {
89
+ this.container.setAttribute('aria-live', 'polite');
90
+ }
91
+ },
92
+ setAriaLabels: (labels) => {
93
+ // Apply ARIA labels
94
+ Object.entries(labels).forEach(([selector, label]) => {
95
+ const elements = this.container?.querySelectorAll(selector);
96
+ elements?.forEach((el) => el.setAttribute('aria-label', label));
97
+ });
98
+ },
99
+ announceHighlight: (termId) => {
100
+ // Announce highlight to screen readers
101
+ const announcement = document.createElement('div');
102
+ announcement.setAttribute('aria-live', 'assertive');
103
+ announcement.setAttribute('aria-atomic', 'true');
104
+ announcement.style.position = 'absolute';
105
+ announcement.style.left = '-10000px';
106
+ announcement.textContent = `Highlighted term: ${termId}`;
107
+ document.body.appendChild(announcement);
108
+ setTimeout(() => document.body.removeChild(announcement), 1000);
109
+ },
110
+ };
111
+ this.options = {
112
+ enableTextSelection: false,
113
+ enableVirtualScrolling: true,
114
+ bufferPages: 2,
115
+ maxCachedPages: 10,
116
+ interactionMode: 'hybrid',
117
+ performanceMode: false,
118
+ accessibility: true,
119
+ };
120
+ this.pdfEngine = new PDFEngine(this.options);
121
+ this.viewportManager = new ViewportManager(this.options.bufferPages, this.options.maxCachedPages);
122
+ this.layerBuilder = new UnifiedLayerBuilder();
123
+ this.performanceOptimizer = new PerformanceOptimizer({
124
+ maxCacheSize: this.options.maxCachedPages ? this.options.maxCachedPages * 10 : 100,
125
+ frameBudget: this.options.performanceMode ? 8 : 16,
126
+ });
127
+ this.styleManager = new CategoryStyleManager();
128
+ // Setup interaction callbacks
129
+ const interactionCallbacks = {
130
+ onHighlightHover: (event) => this.emit('highlightHover', event),
131
+ onHighlightBlur: (termId) => this.emit('highlightBlur', termId),
132
+ onHighlightClick: (event) => this.emit('highlightClick', event),
133
+ onTextSelected: (event) => this.emit('textSelected', event),
134
+ onSelectionChanged: (selection) => this.emit('selectionChanged', selection),
135
+ onInteractionModeChanged: (mode) => this.emit('interactionModeChanged', mode),
136
+ };
137
+ this.interactionHandler = new UnifiedInteractionHandler(interactionCallbacks);
138
+ }
139
+ // =============================================================================
140
+ // Initialization
141
+ // =============================================================================
142
+ async init(container, options) {
143
+ if (this.isInitialized) {
144
+ throw new Error('Viewer is already initialized');
145
+ }
146
+ // Merge options
147
+ this.options = { ...this.options, ...options };
148
+ // Store container reference
149
+ this.container = container;
150
+ // Setup container
151
+ this.setupContainer();
152
+ // Load CSS
153
+ this.loadCSS();
154
+ // Initialize interaction handler
155
+ this.interactionHandler.init(this.container);
156
+ // Setup scroll handling
157
+ this.setupScrollHandling();
158
+ // Initialize accessibility if enabled
159
+ if (this.options.accessibility) {
160
+ this.setupAccessibility();
161
+ }
162
+ this.isInitialized = true;
163
+ this.emit('initialized');
164
+ }
165
+ /**
166
+ * Setup container with proper structure and styling
167
+ */
168
+ setupContainer() {
169
+ if (!this.container)
170
+ return;
171
+ // Add viewer class
172
+ this.container.className = (this.container.className + ' pdf-highlight-viewer').trim();
173
+ // Create PDF container
174
+ this.pdfContainer = document.createElement('div');
175
+ this.pdfContainer.className = 'pdf-container';
176
+ this.container.appendChild(this.pdfContainer);
177
+ // Setup container styles
178
+ this.container.style.position = 'relative';
179
+ this.container.style.overflow = 'auto';
180
+ }
181
+ /**
182
+ * Load CSS dynamically
183
+ */
184
+ loadCSS() {
185
+ // Check if CSS is already loaded
186
+ if (document.getElementById('pdf-highlight-viewer-styles')) {
187
+ return;
188
+ }
189
+ const style = document.createElement('style');
190
+ style.id = 'pdf-highlight-viewer-styles';
191
+ style.textContent = `
192
+ .pdf-highlight-viewer {
193
+ position: relative;
194
+ width: 100%;
195
+ height: 100%;
196
+ overflow: auto;
197
+ background: #f5f5f5;
198
+ }
199
+
200
+ .pdf-container {
201
+ position: relative;
202
+ margin: 0 auto;
203
+ padding: 20px;
204
+ }
205
+
206
+ .pdf-page-container {
207
+ position: relative;
208
+ margin: 0 auto 20px;
209
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
210
+ background: white;
211
+ border-radius: 4px;
212
+ }
213
+
214
+ .pdf-canvas {
215
+ position: relative;
216
+ z-index: 1;
217
+ display: block;
218
+ width: 100%;
219
+ height: auto;
220
+ user-select: none;
221
+ }
222
+
223
+ .unified-layer {
224
+ position: absolute;
225
+ top: 0;
226
+ left: 0;
227
+ right: 0;
228
+ bottom: 0;
229
+ z-index: 2;
230
+ pointer-events: none;
231
+ }
232
+
233
+ .highlight-wrapper {
234
+ position: absolute;
235
+ pointer-events: all;
236
+ cursor: pointer;
237
+ transition: opacity 0.2s ease;
238
+ }
239
+
240
+ .highlight-wrapper:hover {
241
+ opacity: 0.8;
242
+ }
243
+
244
+ .highlight-background {
245
+ position: absolute;
246
+ top: 0;
247
+ left: 0;
248
+ right: 0;
249
+ bottom: 0;
250
+ z-index: -1;
251
+ border-radius: 2px;
252
+ opacity: 0.3;
253
+ }
254
+
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
+ /* Text segment styling */
281
+ .text-segment {
282
+ position: relative;
283
+ z-index: 1;
284
+ color: #333;
285
+ font-family: inherit;
286
+ white-space: pre;
287
+ }
288
+
289
+ .text-segment.selectable {
290
+ user-select: text;
291
+ }
292
+
293
+ /* Loading states */
294
+ .pdf-page-container.loading {
295
+ min-height: 800px;
296
+ display: flex;
297
+ align-items: center;
298
+ justify-content: center;
299
+ color: #666;
300
+ }
301
+
302
+ .pdf-page-container.loading::after {
303
+ content: "Loading page...";
304
+ position: absolute;
305
+ top: 50%;
306
+ left: 50%;
307
+ transform: translate(-50%, -50%);
308
+ font-size: 12px;
309
+ color: #999;
310
+ background: rgba(255, 255, 255, 0.9);
311
+ padding: 4px 8px;
312
+ border-radius: 4px;
313
+ pointer-events: none;
314
+ z-index: 10;
315
+ }
316
+
317
+ `;
318
+ document.head.appendChild(style);
319
+ }
320
+ /**
321
+ * Setup scroll handling for virtual viewport management
322
+ */
323
+ setupScrollHandling() {
324
+ if (!this.container)
325
+ return;
326
+ let scrollTimeout;
327
+ let isScrolling = false;
328
+ this.scrollListener = () => {
329
+ if (!isScrolling) {
330
+ isScrolling = true;
331
+ this.container?.classList.add('scrolling');
332
+ }
333
+ clearTimeout(scrollTimeout);
334
+ scrollTimeout = window.setTimeout(() => {
335
+ isScrolling = false;
336
+ this.container?.classList.remove('scrolling');
337
+ this.handleScroll();
338
+ }, 50); // Reduced debounce time for better responsiveness
339
+ };
340
+ this.container.addEventListener('scroll', this.scrollListener);
341
+ }
342
+ /**
343
+ * Handle scroll events for virtual viewport management
344
+ */
345
+ handleScroll() {
346
+ if (!this.container)
347
+ return;
348
+ const scrollTop = this.container.scrollTop;
349
+ const containerHeight = this.container.clientHeight;
350
+ // Get what pages should be visible
351
+ const visiblePages = this.viewportManager.getVisiblePages(scrollTop, containerHeight);
352
+ const bufferPages = this.viewportManager.getBufferPages(visiblePages, 1);
353
+ // 1. RENDER VISIBLE PAGES IMMEDIATELY - No delays, no complex logic
354
+ visiblePages.forEach((pageNumber) => {
355
+ const pageContainer = this.pageContainers.get(pageNumber);
356
+ if (pageContainer && !pageContainer.classList.contains('rendered')) {
357
+ // Render immediately for visible pages
358
+ this.renderPage(pageNumber).catch((error) => {
359
+ console.error(`Failed to render visible page ${pageNumber}:`, error);
360
+ });
361
+ }
362
+ });
363
+ // 2. RENDER BUFFER PAGES WITH SMALL DELAY
364
+ setTimeout(() => {
365
+ bufferPages.forEach((pageNumber) => {
366
+ const pageContainer = this.pageContainers.get(pageNumber);
367
+ if (pageContainer && !pageContainer.classList.contains('rendered')) {
368
+ this.renderPage(pageNumber).catch((error) => {
369
+ console.error(`Failed to render buffer page ${pageNumber}:`, error);
370
+ });
371
+ }
372
+ });
373
+ }, 100);
374
+ // 3. UNLOAD DISTANT PAGES (simple logic)
375
+ const allRelevantPages = new Set([...visiblePages, ...bufferPages]);
376
+ this.pageContainers.forEach((pageContainer, pageNumber) => {
377
+ if (!allRelevantPages.has(pageNumber) && pageContainer.classList.contains('rendered')) {
378
+ const distance = Math.min(...visiblePages.map((vp) => Math.abs(pageNumber - vp)));
379
+ if (distance > 5) {
380
+ // Only unload if really far
381
+ pageContainer.innerHTML = '';
382
+ pageContainer.classList.remove('rendered');
383
+ this.setPagePlaceholderSize(pageContainer, pageNumber);
384
+ }
385
+ }
386
+ });
387
+ // Update current page
388
+ if (visiblePages.length > 0) {
389
+ const newCurrentPage = visiblePages[0];
390
+ if (newCurrentPage !== this.currentPage) {
391
+ const previousPage = this.currentPage;
392
+ this.currentPage = newCurrentPage;
393
+ this.emit('pageChanged', {
394
+ currentPage: newCurrentPage,
395
+ previousPage: previousPage,
396
+ totalPages: this.totalPages,
397
+ });
398
+ }
399
+ }
400
+ }
401
+ /**
402
+ * Set placeholder size for a page container using real PDF dimensions
403
+ */
404
+ setPagePlaceholderSize(pageContainer, pageNumber) {
405
+ const dimensions = this.pageDimensions.get(pageNumber);
406
+ if (dimensions) {
407
+ pageContainer.style.height = `${dimensions.height}px`;
408
+ pageContainer.style.width = `${dimensions.width}px`;
409
+ }
410
+ else {
411
+ // Fallback to default height
412
+ pageContainer.style.height = `${this.defaultPageHeight}px`;
413
+ }
414
+ }
415
+ /**
416
+ * Get and cache real page dimensions
417
+ */
418
+ async getPageDimensions(pageNumber) {
419
+ // Return cached dimensions if available
420
+ const cached = this.pageDimensions.get(pageNumber);
421
+ if (cached)
422
+ return cached;
423
+ try {
424
+ // Get PDF page and its viewport at current scale
425
+ const page = await this.pdfEngine.getPage(pageNumber);
426
+ const viewport = page.getViewport({ scale: this.currentScale });
427
+ const dimensions = {
428
+ width: viewport.width,
429
+ height: viewport.height,
430
+ };
431
+ // Cache the dimensions
432
+ this.pageDimensions.set(pageNumber, dimensions);
433
+ return dimensions;
434
+ }
435
+ catch (error) {
436
+ console.error(`Failed to get dimensions for page ${pageNumber}:`, error);
437
+ return { width: 600, height: this.defaultPageHeight };
438
+ }
439
+ }
440
+ /**
441
+ * Setup accessibility features
442
+ */
443
+ setupAccessibility() {
444
+ if (!this.container)
445
+ return;
446
+ // Add ARIA attributes
447
+ this.container.setAttribute('role', 'application');
448
+ this.container.setAttribute('aria-label', 'PDF Highlight Viewer');
449
+ this.container.setAttribute('tabindex', '0');
450
+ // Add keyboard navigation
451
+ this.container.addEventListener('keydown', this.handleKeyboardNavigation.bind(this));
452
+ }
453
+ /**
454
+ * Handle keyboard navigation
455
+ */
456
+ handleKeyboardNavigation(event) {
457
+ switch (event.key) {
458
+ case 'PageDown':
459
+ case 'ArrowDown':
460
+ this.setPage(Math.min(this.totalPages, this.currentPage + 1));
461
+ event.preventDefault();
462
+ break;
463
+ case 'PageUp':
464
+ case 'ArrowUp':
465
+ this.setPage(Math.max(1, this.currentPage - 1));
466
+ event.preventDefault();
467
+ break;
468
+ case 'Home':
469
+ this.setPage(1);
470
+ event.preventDefault();
471
+ break;
472
+ case 'End':
473
+ this.setPage(this.totalPages);
474
+ event.preventDefault();
475
+ break;
476
+ case '+':
477
+ case '=':
478
+ this.setZoom(this.currentScale * 1.2);
479
+ event.preventDefault();
480
+ break;
481
+ case '-':
482
+ this.setZoom(this.currentScale / 1.2);
483
+ event.preventDefault();
484
+ break;
485
+ }
486
+ }
487
+ // =============================================================================
488
+ // PDF Management
489
+ // =============================================================================
490
+ async loadPDF(source) {
491
+ if (!this.isInitialized) {
492
+ throw new Error('Viewer must be initialized before loading PDF');
493
+ }
494
+ try {
495
+ // Load document with PDF engine
496
+ await this.pdfEngine.loadDocument(source);
497
+ const docInfo = this.pdfEngine.getDocumentInfo();
498
+ this.totalPages = docInfo.numPages;
499
+ // Update viewport manager with total pages
500
+ this.viewportManager.setTotalPages(this.totalPages);
501
+ // Create page containers
502
+ await this.createPageContainers();
503
+ // Load initial pages
504
+ await this.loadInitialPages();
505
+ this.emit('pdfLoaded', { totalPages: this.totalPages });
506
+ }
507
+ catch (error) {
508
+ this.emit('error', { type: 'pdf-load-error', error });
509
+ throw error;
510
+ }
511
+ }
512
+ /**
513
+ * Create DOM containers for all pages with real PDF dimensions
514
+ */
515
+ async createPageContainers() {
516
+ if (!this.pdfContainer)
517
+ return;
518
+ // Clear existing containers
519
+ this.pdfContainer.innerHTML = '';
520
+ this.pageContainers.clear();
521
+ // Get dimensions for the first page to estimate all others
522
+ let avgPageHeight = this.defaultPageHeight;
523
+ try {
524
+ const firstPageDimensions = await this.getPageDimensions(1);
525
+ avgPageHeight = firstPageDimensions.height;
526
+ }
527
+ catch (_error) {
528
+ console.warn('Could not get first page dimensions, using default');
529
+ }
530
+ for (let pageNumber = 1; pageNumber <= this.totalPages; pageNumber++) {
531
+ const pageContainer = document.createElement('div');
532
+ pageContainer.className = 'pdf-page-container';
533
+ pageContainer.setAttribute('data-page-number', pageNumber.toString());
534
+ pageContainer.style.marginBottom = '20px';
535
+ pageContainer.style.position = 'relative';
536
+ // Try to set real dimensions, or use estimated height
537
+ try {
538
+ const dimensions = await this.getPageDimensions(pageNumber);
539
+ pageContainer.style.height = `${dimensions.height}px`;
540
+ pageContainer.style.width = `${dimensions.width}px`;
541
+ }
542
+ catch (_error) {
543
+ // Use average height as fallback
544
+ pageContainer.style.height = `${avgPageHeight}px`;
545
+ }
546
+ this.pdfContainer.appendChild(pageContainer);
547
+ this.pageContainers.set(pageNumber, pageContainer);
548
+ }
549
+ // Update viewport manager with real page dimensions
550
+ this.viewportManager.updateDimensions(this.container?.clientHeight || 600, avgPageHeight);
551
+ }
552
+ /**
553
+ * Load initial pages (simple and reliable)
554
+ */
555
+ async loadInitialPages() {
556
+ if (!this.container)
557
+ return;
558
+ // Get what should be visible at the top
559
+ const visiblePages = this.viewportManager.getVisiblePages(0, this.container.clientHeight);
560
+ // Load first page immediately
561
+ try {
562
+ await this.renderPage(1);
563
+ }
564
+ catch (error) {
565
+ console.error('Failed to load initial page:', error);
566
+ }
567
+ // Load other visible pages with small delay
568
+ setTimeout(() => {
569
+ visiblePages
570
+ .filter((page) => page > 1)
571
+ .forEach((pageNumber) => {
572
+ this.renderPage(pageNumber).catch((error) => {
573
+ console.error(`Failed to load initial page ${pageNumber}:`, error);
574
+ });
575
+ });
576
+ }, 100);
577
+ }
578
+ /**
579
+ * Render a specific page
580
+ */
581
+ async renderPage(pageNumber) {
582
+ const pageContainer = this.pageContainers.get(pageNumber);
583
+ if (!pageContainer)
584
+ return;
585
+ // Check if already rendered
586
+ if (pageContainer.classList.contains('rendered')) {
587
+ console.log(`Page ${pageNumber}: skipping, already has 'rendered' class`);
588
+ return;
589
+ }
590
+ console.log(`Page ${pageNumber}: rendering at scale ${this.currentScale}`);
591
+ try {
592
+ // Add loading state
593
+ pageContainer.classList.add('loading');
594
+ // Render PDF canvas
595
+ const canvas = await this.pdfEngine.renderPage(pageNumber, this.currentScale);
596
+ // Clear container and add canvas
597
+ pageContainer.innerHTML = '';
598
+ pageContainer.appendChild(canvas);
599
+ pageContainer.style.height = `${canvas.height}px`;
600
+ pageContainer.style.width = `${canvas.width}px`;
601
+ pageContainer.style.position = 'relative';
602
+ // Add text layer for selection (must be added before highlights)
603
+ await this.addTextLayerToPage(pageNumber);
604
+ // Add simple highlight overlays
605
+ await this.addHighlightsToPage(pageNumber, canvas.width, canvas.height);
606
+ // Mark as rendered and remove loading state
607
+ pageContainer.classList.add('rendered');
608
+ pageContainer.classList.remove('loading');
609
+ this.emit('renderComplete', {
610
+ pageNumber,
611
+ renderTime: 0,
612
+ highlightCount: this.getHighlightCountForPage(pageNumber),
613
+ });
614
+ }
615
+ catch (error) {
616
+ pageContainer.classList.remove('loading');
617
+ console.error(`Failed to render page ${pageNumber}:`, error);
618
+ this.emit('renderError', { pageNumber, error });
619
+ }
620
+ }
621
+ preloadPages(pageNumbers) {
622
+ return Promise.all(pageNumbers.map((pageNumber) => this.renderPage(pageNumber))).then(() => {
623
+ return;
624
+ });
625
+ }
626
+ setPage(pageNumber) {
627
+ if (pageNumber < 1 || pageNumber > this.totalPages)
628
+ return;
629
+ const pagePosition = this.viewportManager.getScrollPositionForPage(pageNumber);
630
+ if (this.container) {
631
+ this.container.scrollTop = pagePosition;
632
+ }
633
+ }
634
+ getZoom() {
635
+ return this.currentScale;
636
+ }
637
+ setZoom(scale) {
638
+ const previousScale = this.currentScale;
639
+ this.currentScale = Math.max(0.5, Math.min(5.0, scale));
640
+ // Re-render visible pages with new scale
641
+ this.reRenderVisiblePages();
642
+ this.emit('zoomChanged', { scale: this.currentScale, previousScale });
643
+ }
644
+ zoomIn() {
645
+ this.setZoom(this.currentScale * 1.2);
646
+ }
647
+ zoomOut() {
648
+ this.setZoom(this.currentScale / 1.2);
649
+ }
650
+ resetZoom() {
651
+ this.setZoom(1.5);
652
+ }
653
+ getCurrentPage() {
654
+ return this.currentPage;
655
+ }
656
+ getTotalPages() {
657
+ return this.totalPages;
658
+ }
659
+ // =============================================================================
660
+ // Text Selection Management
661
+ // =============================================================================
662
+ /**
663
+ * Enable text selection functionality
664
+ */
665
+ enableTextSelection() {
666
+ if (!this.options.enableTextSelection) {
667
+ this.options.enableTextSelection = true;
668
+ // Add text layers to all currently rendered pages
669
+ this.pageContainers.forEach(async (pageContainer, pageNumber) => {
670
+ if (pageContainer.classList.contains('rendered')) {
671
+ await this.addTextLayerToPage(pageNumber);
672
+ }
673
+ });
674
+ this.emit('textSelectionEnabled');
675
+ }
676
+ }
677
+ /**
678
+ * Disable text selection functionality
679
+ */
680
+ disableTextSelection() {
681
+ if (this.options.enableTextSelection) {
682
+ this.options.enableTextSelection = false;
683
+ // Remove text layers from all pages
684
+ this.pageContainers.forEach((pageContainer) => {
685
+ const textLayer = pageContainer.querySelector('.text-layer');
686
+ if (textLayer) {
687
+ textLayer.remove();
688
+ }
689
+ });
690
+ this.emit('textSelectionDisabled');
691
+ }
692
+ }
693
+ /**
694
+ * Toggle text selection functionality
695
+ */
696
+ toggleTextSelection() {
697
+ if (this.options.enableTextSelection) {
698
+ this.disableTextSelection();
699
+ return false;
700
+ }
701
+ else {
702
+ this.enableTextSelection();
703
+ return true;
704
+ }
705
+ }
706
+ /**
707
+ * Check if text selection is currently enabled
708
+ */
709
+ isTextSelectionEnabled() {
710
+ return this.options.enableTextSelection || false;
711
+ }
712
+ // =============================================================================
713
+ // Highlight Management
714
+ // =============================================================================
715
+ loadHighlights(data) {
716
+ if (Array.isArray(data)) {
717
+ this.highlightData = adaptHighlightData(data, {
718
+ categoryResolver: (highlight) => highlight.metadata?.category || 'default',
719
+ termNameResolver: (highlight) => highlight.metadata?.term || highlight.id,
720
+ });
721
+ }
722
+ else {
723
+ this.highlightData = data;
724
+ }
725
+ this.updateAnalytics();
726
+ // Update unified layers for all rendered pages
727
+ this.updateAllUnifiedLayers();
728
+ // Update spatial indices
729
+ this.buildAllSpatialIndices();
730
+ this.emit('highlightsLoaded', { data: this.highlightData });
731
+ }
732
+ addHighlight(pageNumber, highlight) {
733
+ // Add to first available category or create 'custom' category
734
+ const categoryKey = Object.keys(this.highlightData)[0] || 'custom';
735
+ if (!this.highlightData[categoryKey]) {
736
+ this.highlightData[categoryKey] = { pages: {}, terms: {} };
737
+ }
738
+ if (!this.highlightData[categoryKey].pages[pageNumber.toString()]) {
739
+ this.highlightData[categoryKey].pages[pageNumber.toString()] = [];
740
+ }
741
+ this.highlightData[categoryKey].pages[pageNumber.toString()].push(highlight);
742
+ // Update page if rendered
743
+ this.updatePageUnifiedLayer(pageNumber);
744
+ this.buildSpatialIndexForPage(pageNumber);
745
+ this.updateAnalytics();
746
+ this.emit('highlightAdded', { pageNumber, highlight });
747
+ }
748
+ removeHighlight(termId) {
749
+ let found = false;
750
+ // Remove from all categories
751
+ Object.keys(this.highlightData).forEach((category) => {
752
+ Object.keys(this.highlightData[category].pages).forEach((pageNumber) => {
753
+ const page = this.highlightData[category].pages[pageNumber];
754
+ const initialLength = page.length;
755
+ this.highlightData[category].pages[pageNumber] = page.filter((highlight) => highlight.termId !== termId);
756
+ if (page.length !== initialLength) {
757
+ found = true;
758
+ this.updatePageUnifiedLayer(parseInt(pageNumber));
759
+ this.buildSpatialIndexForPage(parseInt(pageNumber));
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 });
768
+ }
769
+ }
770
+ updateHighlightStyle(category, style) {
771
+ this.styleManager.updateCategoryStyle(category, style);
772
+ this.emit('styleUpdated', { category, style });
773
+ }
774
+ getHighlightsForPage(pageNumber) {
775
+ const highlights = [];
776
+ Object.values(this.highlightData).forEach((categoryData) => {
777
+ const pageHighlights = categoryData.pages[pageNumber.toString()];
778
+ if (pageHighlights) {
779
+ highlights.push(...pageHighlights);
780
+ }
781
+ });
782
+ return highlights;
783
+ }
784
+ /**
785
+ * Update unified layer for a specific page
786
+ */
787
+ updatePageUnifiedLayer(pageNumber) {
788
+ const pageContainer = this.pageContainers.get(pageNumber);
789
+ if (!pageContainer)
790
+ return;
791
+ const pageData = this.pdfEngine.getPageData(pageNumber);
792
+ if (!pageData || !pageData.textContent)
793
+ return;
794
+ this.layerBuilder.updateHighlights(this.highlightData, pageNumber, pageData.textContent);
795
+ }
796
+ /**
797
+ * Update all unified layers
798
+ */
799
+ updateAllUnifiedLayers() {
800
+ this.pageContainers.forEach((pageContainer, pageNumber) => {
801
+ // Only update pages that are already rendered
802
+ if (pageContainer.classList.contains('rendered')) {
803
+ // Remove existing highlight layer first
804
+ const existingHighlightLayer = pageContainer.querySelector('.highlight-layer');
805
+ if (existingHighlightLayer) {
806
+ existingHighlightLayer.remove();
807
+ }
808
+ // Re-add highlights to already rendered pages
809
+ const canvas = pageContainer.querySelector('canvas');
810
+ if (canvas) {
811
+ this.addHighlightsToPage(pageNumber, canvas.width, canvas.height);
812
+ }
813
+ }
814
+ });
815
+ }
816
+ /**
817
+ * Re-render visible pages (e.g., after zoom change)
818
+ */
819
+ async reRenderVisiblePages() {
820
+ if (!this.container)
821
+ return;
822
+ console.log('Zoom changed to:', this.currentScale);
823
+ // Clear ALL cached data for the new scale
824
+ this.pdfEngine.clearAllPageCache();
825
+ this.pageDimensions.clear(); // Clear dimension cache
826
+ // Clear all containers and update their sizes for new scale
827
+ let totalHeight = 0;
828
+ let validDimensions = 0;
829
+ for (const [pageNumber, pageContainer] of this.pageContainers) {
830
+ // Clear content
831
+ pageContainer.innerHTML = '';
832
+ pageContainer.classList.remove('rendered');
833
+ // Get new dimensions for the new scale
834
+ try {
835
+ const newDimensions = await this.getPageDimensions(pageNumber);
836
+ pageContainer.style.height = `${newDimensions.height}px`;
837
+ pageContainer.style.width = `${newDimensions.width}px`;
838
+ // Track for average calculation
839
+ totalHeight += newDimensions.height;
840
+ validDimensions++;
841
+ }
842
+ catch (_error) {
843
+ pageContainer.style.height = `${this.defaultPageHeight}px`;
844
+ }
845
+ }
846
+ // Calculate new average page height and update viewport manager
847
+ const avgPageHeight = validDimensions > 0 ? totalHeight / validDimensions : this.defaultPageHeight;
848
+ this.viewportManager.updateDimensions(this.container?.clientHeight || 600, avgPageHeight);
849
+ // Render currently visible pages
850
+ const scrollTop = this.container.scrollTop;
851
+ const containerHeight = this.container.clientHeight;
852
+ const visiblePages = this.viewportManager.getVisiblePages(scrollTop, containerHeight);
853
+ console.log('Re-rendering visible pages at new scale:', visiblePages);
854
+ // Render visible pages immediately
855
+ for (const pageNumber of visiblePages) {
856
+ try {
857
+ await this.renderPage(pageNumber);
858
+ }
859
+ catch (error) {
860
+ console.error(`Failed to re-render page ${pageNumber}:`, error);
861
+ }
862
+ }
863
+ }
864
+ // =============================================================================
865
+ // Navigation
866
+ // =============================================================================
867
+ goToHighlight(termId, occurrenceIndex = 0) {
868
+ // Find highlight location
869
+ for (const [, categoryData] of Object.entries(this.highlightData)) {
870
+ for (const [pageNumber, highlights] of Object.entries(categoryData.pages)) {
871
+ const highlight = highlights.find((h) => h.termId === termId);
872
+ if (highlight && highlight.coordinates[occurrenceIndex]) {
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
+ }
879
+ }
880
+ }
881
+ }
882
+ nextHighlight(category) {
883
+ // TODO: Implement next highlight navigation
884
+ console.log('nextHighlight not yet implemented:', category);
885
+ }
886
+ previousHighlight(category) {
887
+ // TODO: Implement previous highlight navigation
888
+ console.log('previousHighlight not yet implemented:', category);
889
+ }
890
+ goToCoordinate(pageNumber, x, y) {
891
+ this.setPage(pageNumber);
892
+ // TODO: Scroll to specific coordinates
893
+ this.emit('coordinateNavigation', { pageNumber, x, y });
894
+ }
895
+ // =============================================================================
896
+ // Search & Filter
897
+ // =============================================================================
898
+ searchTerms(query) {
899
+ const results = [];
900
+ Object.values(this.highlightData).forEach((categoryData) => {
901
+ Object.values(categoryData.terms).forEach((term) => {
902
+ if (term.term.toLowerCase().includes(query.toLowerCase()) ||
903
+ term.aliases.some((alias) => alias.toLowerCase().includes(query.toLowerCase()))) {
904
+ results.push(term);
905
+ }
906
+ });
907
+ });
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
+ }
922
+ // =============================================================================
923
+ // Interaction Modes
924
+ // =============================================================================
925
+ setInteractionMode(mode) {
926
+ this.interactionHandler.setInteractionMode(mode);
927
+ }
928
+ getInteractionMode() {
929
+ return this.interactionHandler.getInteractionMode();
930
+ }
931
+ // =============================================================================
932
+ // Performance & Analytics
933
+ // =============================================================================
934
+ getPerformanceMetrics() {
935
+ const baseMetrics = this.performanceOptimizer.getPerformanceMetrics();
936
+ // Add memory usage info
937
+ const renderedPages = Array.from(this.pageContainers.entries()).filter(([_, container]) => container.classList.contains('rendered')).length;
938
+ const memoryEstimate = renderedPages * 2; // ~2MB per rendered page
939
+ return {
940
+ ...baseMetrics,
941
+ memoryUsage: {
942
+ pages: renderedPages,
943
+ highlights: Object.keys(this.highlightData).length,
944
+ cache: this.pageDimensions.size,
945
+ total: memoryEstimate,
946
+ },
947
+ };
948
+ }
949
+ /**
950
+ * Get current memory and performance stats
951
+ */
952
+ getMemoryStats() {
953
+ const renderedPages = Array.from(this.pageContainers.entries()).filter(([_, container]) => container.classList.contains('rendered')).length;
954
+ return {
955
+ renderedPages,
956
+ totalPages: this.totalPages,
957
+ estimatedMemoryMB: renderedPages * 2, // ~2MB per page
958
+ cachedDimensions: this.pageDimensions.size,
959
+ };
960
+ }
961
+ getAnalytics() {
962
+ return { ...this.analytics };
963
+ }
964
+ enableProfiling() {
965
+ // TODO: Enable detailed performance profiling
966
+ console.log('Profiling enabled');
967
+ }
968
+ disableProfiling() {
969
+ // TODO: Disable detailed performance profiling
970
+ console.log('Profiling disabled');
971
+ }
972
+ // =============================================================================
973
+ // Event Management
974
+ // =============================================================================
975
+ addEventListener(event, callback) {
976
+ this.eventListeners.push({ event, callback });
977
+ }
978
+ removeEventListener(event, callback) {
979
+ const index = this.eventListeners.findIndex((listener) => listener.event === event && listener.callback === callback);
980
+ if (index !== -1) {
981
+ this.eventListeners.splice(index, 1);
982
+ }
983
+ }
984
+ emit(event, data) {
985
+ this.eventListeners
986
+ .filter((listener) => listener.event === event)
987
+ .forEach((listener) => {
988
+ try {
989
+ listener.callback(data);
990
+ }
991
+ catch (error) {
992
+ console.error(`Error in event listener for ${event}:`, error);
993
+ }
994
+ });
995
+ }
996
+ // =============================================================================
997
+ // Utility Methods
998
+ // =============================================================================
999
+ exportAsImage(format = 'png', quality = 0.9) {
1000
+ return new Promise((resolve, reject) => {
1001
+ const canvas = document.createElement('canvas');
1002
+ const ctx = canvas.getContext('2d');
1003
+ if (!ctx || !this.pdfContainer) {
1004
+ reject(new Error('Canvas context not available'));
1005
+ return;
1006
+ }
1007
+ // TODO: Implement image export
1008
+ canvas.toBlob((blob) => {
1009
+ if (blob) {
1010
+ resolve(blob);
1011
+ }
1012
+ else {
1013
+ reject(new Error('Failed to create blob'));
1014
+ }
1015
+ }, `image/${format}`, quality);
1016
+ });
1017
+ }
1018
+ getViewport() {
1019
+ return {
1020
+ pageNumber: this.currentPage,
1021
+ scale: this.currentScale,
1022
+ scrollTop: this.container?.scrollTop || 0,
1023
+ visibleArea: {
1024
+ x: 0,
1025
+ y: this.container?.scrollTop || 0,
1026
+ width: this.container?.clientWidth || 0,
1027
+ height: this.container?.clientHeight || 0,
1028
+ },
1029
+ };
1030
+ }
1031
+ refresh() {
1032
+ // Re-render current viewport
1033
+ this.reRenderVisiblePages();
1034
+ this.updateAllUnifiedLayers();
1035
+ this.emit('refreshComplete');
1036
+ }
1037
+ // =============================================================================
1038
+ // Private Helper Methods
1039
+ // =============================================================================
1040
+ /**
1041
+ * Add highlights to a rendered page
1042
+ */
1043
+ async addHighlightsToPage(pageNumber, canvasWidth, canvasHeight) {
1044
+ const pageContainer = this.pageContainers.get(pageNumber);
1045
+ if (!pageContainer)
1046
+ return;
1047
+ // Check if highlights already added
1048
+ if (pageContainer.querySelector('.highlight-layer')) {
1049
+ return;
1050
+ }
1051
+ // Get the actual PDF page to get proper dimensions
1052
+ 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
+ const scale = this.currentScale;
1056
+ // Create highlight layer
1057
+ const highlightLayer = document.createElement('div');
1058
+ highlightLayer.className = 'highlight-layer';
1059
+ highlightLayer.style.position = 'absolute';
1060
+ highlightLayer.style.top = '0';
1061
+ highlightLayer.style.left = '0';
1062
+ highlightLayer.style.width = `${canvasWidth}px`;
1063
+ highlightLayer.style.height = `${canvasHeight}px`;
1064
+ highlightLayer.style.zIndex = '2'; // Above text layer
1065
+ highlightLayer.style.pointerEvents = 'none';
1066
+ // Add highlights from each category
1067
+ Object.entries(this.highlightData).forEach(([category, categoryData]) => {
1068
+ const pageHighlights = categoryData.pages[pageNumber.toString()];
1069
+ if (!pageHighlights)
1070
+ return;
1071
+ pageHighlights.forEach((highlight) => {
1072
+ if (highlight.coordinates && highlight.coordinates.length > 0) {
1073
+ highlight.coordinates.forEach((coord) => {
1074
+ const highlightDiv = document.createElement('div');
1075
+ highlightDiv.className = `highlight ${category}-highlight`;
1076
+ highlightDiv.setAttribute('data-term-id', highlight.termId);
1077
+ highlightDiv.setAttribute('data-category', category);
1078
+ // Scale coordinates by current zoom level
1079
+ const left = coord.x1 * scale;
1080
+ const top = coord.y1 * scale;
1081
+ const width = (coord.x2 - coord.x1) * scale;
1082
+ const height = (coord.y2 - coord.y1) * scale;
1083
+ highlightDiv.style.position = 'absolute';
1084
+ highlightDiv.style.left = `${left}px`;
1085
+ highlightDiv.style.top = `${top}px`;
1086
+ highlightDiv.style.width = `${width}px`;
1087
+ highlightDiv.style.height = `${height}px`;
1088
+ highlightDiv.style.backgroundColor = this.getHighlightColor(highlight.termId, category);
1089
+ highlightDiv.style.border = `1px solid ${this.getHighlightColor(highlight.termId, category)}`;
1090
+ highlightDiv.style.pointerEvents = 'auto';
1091
+ highlightDiv.style.cursor = 'pointer';
1092
+ highlightDiv.style.boxSizing = 'border-box';
1093
+ highlightDiv.style.userSelect = 'none'; // Prevent highlight div from being selected
1094
+ highlightDiv.style.mixBlendMode = 'multiply'; // Better color blending
1095
+ // Check for overlapping highlights and adjust opacity
1096
+ const overlappingCount = this.countOverlappingHighlights(highlightLayer, coord, scale);
1097
+ const baseOpacity = Math.max(0.15, 0.3 / Math.max(1, overlappingCount * 0.7));
1098
+ highlightDiv.style.opacity = baseOpacity.toString();
1099
+ // todo make hover opacities configurable?
1100
+ // Add hover effect with dynamic opacity
1101
+ const originalOpacity = baseOpacity.toString();
1102
+ const hoverOpacity = Math.min(0.6, baseOpacity + 0.2).toString();
1103
+ highlightDiv.addEventListener('mouseenter', () => {
1104
+ if (this.options.highlightsConfig?.enableMultilineHover) {
1105
+ const highlightBoxes = highlightLayer.querySelectorAll(`[data-term-id="${highlight.termId}"]`);
1106
+ highlightBoxes.forEach((highlightBox) => {
1107
+ highlightBox.style.opacity = hoverOpacity;
1108
+ });
1109
+ const unhoveredBoxes = highlightLayer.querySelectorAll(`div[data-term-id]:not([data-term-id="${highlight.termId}"])`);
1110
+ unhoveredBoxes.forEach((highlightBox) => {
1111
+ highlightBox.style.opacity = '0.1';
1112
+ });
1113
+ }
1114
+ else {
1115
+ highlightDiv.style.opacity = hoverOpacity;
1116
+ }
1117
+ });
1118
+ highlightDiv.addEventListener('mouseleave', () => {
1119
+ if (this.options.highlightsConfig?.enableMultilineHover) {
1120
+ const allBoxes = highlightLayer.querySelectorAll(`div[data-term-id]`);
1121
+ allBoxes.forEach((highlightBox) => {
1122
+ highlightBox.style.opacity = originalOpacity;
1123
+ });
1124
+ }
1125
+ else {
1126
+ highlightDiv.style.opacity = originalOpacity;
1127
+ }
1128
+ });
1129
+ highlightLayer.appendChild(highlightDiv);
1130
+ });
1131
+ }
1132
+ });
1133
+ });
1134
+ pageContainer.appendChild(highlightLayer);
1135
+ // Apply selected term highlighting if there's a selected term
1136
+ if (this.selectedTermId) {
1137
+ this.applySelectionToPage(pageNumber);
1138
+ }
1139
+ }
1140
+ catch (error) {
1141
+ console.error(`Failed to add highlights to page ${pageNumber}:`, error);
1142
+ }
1143
+ }
1144
+ /**
1145
+ * Update highlights colors for specified page
1146
+ * */
1147
+ updateHighlightsStyles(pageNumber, hoveredIds) {
1148
+ const pageContainer = this.pageContainers.get(pageNumber);
1149
+ if (!pageContainer) {
1150
+ return;
1151
+ }
1152
+ const highlightLayer = pageContainer.querySelector('.highlight-layer');
1153
+ if (!highlightLayer) {
1154
+ return;
1155
+ }
1156
+ // Find all highlights in this page
1157
+ const allHighlights = pageContainer.querySelectorAll('.highlight, .highlight-wrapper');
1158
+ allHighlights.forEach((highlight) => {
1159
+ const elementTermId = highlight.getAttribute('data-term-id');
1160
+ const category = highlight.getAttribute('data-category');
1161
+ if (elementTermId && category) {
1162
+ highlight.style.backgroundColor = this.getHighlightColor(elementTermId, category);
1163
+ highlight.style.border =
1164
+ `1px solid ${this.getHighlightColor(elementTermId, category)}`;
1165
+ if (this.options.highlightsConfig?.enableMultilineHover &&
1166
+ hoveredIds &&
1167
+ Array.isArray(hoveredIds)) {
1168
+ const baseOpacity = 0.3;
1169
+ const originalOpacity = baseOpacity.toString();
1170
+ const hoverOpacity = Math.min(0.6, baseOpacity + 0.2).toString();
1171
+ const unhoveredOpacity = '0.1';
1172
+ if (hoveredIds.includes(elementTermId)) {
1173
+ highlight.style.opacity = hoverOpacity;
1174
+ }
1175
+ else if (hoveredIds.length > 0) {
1176
+ highlight.style.opacity = unhoveredOpacity;
1177
+ }
1178
+ else {
1179
+ highlight.style.opacity = originalOpacity;
1180
+ }
1181
+ }
1182
+ }
1183
+ });
1184
+ }
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
+ /**
1209
+ * Build spatial index for a specific page
1210
+ */
1211
+ buildSpatialIndexForPage(pageNumber) {
1212
+ const highlights = this.getHighlightsForPage(pageNumber);
1213
+ this.performanceOptimizer.buildSpatialIndex(highlights, pageNumber);
1214
+ }
1215
+ /**
1216
+ * Build spatial indices for all pages
1217
+ */
1218
+ buildAllSpatialIndices() {
1219
+ for (let pageNumber = 1; pageNumber <= this.totalPages; pageNumber++) {
1220
+ this.buildSpatialIndexForPage(pageNumber);
1221
+ }
1222
+ }
1223
+ /**
1224
+ * Get highlight count for a page
1225
+ */
1226
+ getHighlightCountForPage(pageNumber) {
1227
+ return this.getHighlightsForPage(pageNumber).length;
1228
+ }
1229
+ /**
1230
+ * Update analytics data
1231
+ */
1232
+ updateAnalytics() {
1233
+ let totalHighlights = 0;
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
+ });
1243
+ this.analytics = {
1244
+ ...this.analytics,
1245
+ totalHighlights,
1246
+ categoryBreakdown,
1247
+ };
1248
+ }
1249
+ /**
1250
+ * Highlight all instances of a selected term
1251
+ */
1252
+ highlightSelectedTerm(termId) {
1253
+ if (!this.container)
1254
+ return;
1255
+ // Store the selected term ID for persistence across page renders
1256
+ this.selectedTermId = termId;
1257
+ // Remove previous selection highlighting
1258
+ this.clearSelectedTermHighlighting();
1259
+ // Add selected class to all instances of this term
1260
+ const termElements = this.container.querySelectorAll(`[data-term-id="${termId}"]`);
1261
+ termElements.forEach((element) => {
1262
+ element.classList.add('selected-term');
1263
+ // Override inline styles for selected term
1264
+ const htmlElement = element;
1265
+ htmlElement.style.opacity = '0.75';
1266
+ htmlElement.style.filter = 'brightness(1.05) contrast(1.05) saturate(1.1)';
1267
+ htmlElement.style.boxShadow =
1268
+ '0 0 0 1px rgba(255, 255, 255, 0.6), 0 0 4px rgba(102, 126, 234, 0.3)';
1269
+ htmlElement.style.transform = 'scale(1.02)';
1270
+ htmlElement.style.zIndex = '12';
1271
+ htmlElement.style.borderWidth = '1px';
1272
+ htmlElement.style.transition = 'all 0.3s ease';
1273
+ });
1274
+ // Also dim all other highlights
1275
+ const allHighlights = this.container.querySelectorAll('.highlight, .highlight-wrapper');
1276
+ allHighlights.forEach((element) => {
1277
+ const elementTermId = element.getAttribute('data-term-id');
1278
+ if (!elementTermId || elementTermId !== termId) {
1279
+ element.classList.add('dimmed-highlight');
1280
+ // Override inline styles for dimmed highlights
1281
+ const htmlElement = element;
1282
+ htmlElement.style.opacity = '0.15';
1283
+ htmlElement.style.filter = 'brightness(0.6) contrast(0.7) saturate(0.4) grayscale(0.3)';
1284
+ htmlElement.style.transition = 'all 0.3s ease';
1285
+ }
1286
+ });
1287
+ }
1288
+ /**
1289
+ * Clear selected term highlighting
1290
+ */
1291
+ clearSelectedTermHighlighting() {
1292
+ if (!this.container)
1293
+ return;
1294
+ // Clear the selected term ID
1295
+ this.selectedTermId = null;
1296
+ const selectedElements = this.container.querySelectorAll('.selected-term');
1297
+ selectedElements.forEach((element) => {
1298
+ element.classList.remove('selected-term');
1299
+ // Reset inline styles for selected elements
1300
+ const htmlElement = element;
1301
+ htmlElement.style.filter = '';
1302
+ htmlElement.style.boxShadow = '';
1303
+ htmlElement.style.transform = '';
1304
+ htmlElement.style.borderWidth = '';
1305
+ // Keep original opacity as it was set by the original rendering
1306
+ });
1307
+ const dimmedElements = this.container.querySelectorAll('.dimmed-highlight');
1308
+ dimmedElements.forEach((element) => {
1309
+ element.classList.remove('dimmed-highlight');
1310
+ // Reset inline styles for dimmed elements
1311
+ const htmlElement = element;
1312
+ htmlElement.style.filter = '';
1313
+ htmlElement.style.transform = '';
1314
+ htmlElement.style.boxShadow = '';
1315
+ htmlElement.style.borderWidth = '';
1316
+ // Restore original opacity from stored value
1317
+ const originalOpacity = htmlElement.dataset.originalOpacity;
1318
+ if (originalOpacity) {
1319
+ htmlElement.style.opacity = originalOpacity;
1320
+ delete htmlElement.dataset.originalOpacity;
1321
+ }
1322
+ else {
1323
+ htmlElement.style.opacity = '0.3'; // Default fallback
1324
+ }
1325
+ });
1326
+ }
1327
+ /**
1328
+ * Add text layer for text selection functionality
1329
+ */
1330
+ async addTextLayerToPage(pageNumber) {
1331
+ // Check if text selection is enabled
1332
+ if (!this.options.enableTextSelection) {
1333
+ return;
1334
+ }
1335
+ const pageContainer = this.pageContainers.get(pageNumber);
1336
+ if (!pageContainer)
1337
+ return;
1338
+ // Check if text layer already exists
1339
+ if (pageContainer.querySelector('.text-layer')) {
1340
+ return;
1341
+ }
1342
+ try {
1343
+ // Get the PDF page and its viewport
1344
+ const pdfPage = await this.pdfEngine.getPage(pageNumber);
1345
+ const viewport = pdfPage.getViewport({ scale: this.currentScale });
1346
+ // Extract text content from PDF
1347
+ const textContent = await pdfPage.getTextContent();
1348
+ // Create text layer container
1349
+ const textLayer = document.createElement('div');
1350
+ textLayer.className = 'text-layer';
1351
+ textLayer.style.position = 'absolute';
1352
+ textLayer.style.left = '0';
1353
+ textLayer.style.top = '0';
1354
+ textLayer.style.width = `${viewport.width}px`;
1355
+ textLayer.style.height = `${viewport.height}px`;
1356
+ textLayer.style.overflow = 'hidden';
1357
+ textLayer.style.lineHeight = '1';
1358
+ textLayer.style.zIndex = '1'; // Below highlights but above canvas
1359
+ // textLayer.style.opacity = '0.1'; // Uncomment for debugging text boundaries
1360
+ // Process text items with proper PDF coordinate transformation
1361
+ textContent.items.forEach((item) => {
1362
+ if (!item.str || !item.str.trim())
1363
+ return; // Skip empty text
1364
+ const textSpan = document.createElement('span');
1365
+ textSpan.textContent = item.str;
1366
+ textSpan.style.position = 'absolute';
1367
+ textSpan.style.color = 'transparent'; // Invisible text for selection
1368
+ textSpan.style.backgroundColor = 'transparent'; // No background interference
1369
+ // textSpan.style.border = '1px solid blue'; // Uncomment for debugging boundaries
1370
+ textSpan.style.cursor = 'text';
1371
+ textSpan.style.userSelect = 'text';
1372
+ textSpan.style.whiteSpace = 'pre';
1373
+ textSpan.style.pointerEvents = 'auto';
1374
+ // Use PDF.js transform matrix directly for accurate positioning
1375
+ const transform = item.transform;
1376
+ // Extract position and scaling from transform matrix
1377
+ const scaleX = transform[0];
1378
+ const scaleY = transform[3];
1379
+ const translateX = transform[4];
1380
+ const translateY = transform[5];
1381
+ // Apply viewport transformation to get screen coordinates
1382
+ const [screenX, screenY] = viewport.convertToViewportPoint(translateX, translateY);
1383
+ // Calculate font size with proper viewport scaling
1384
+ const fontSize = Math.abs(scaleY) * this.currentScale;
1385
+ // Calculate text width with proper viewport scaling
1386
+ let textWidth = 0;
1387
+ if (item.width) {
1388
+ // Use PDF.js provided width and apply viewport scaling
1389
+ textWidth = item.width * this.currentScale;
1390
+ }
1391
+ else {
1392
+ // Calculate based on font metrics with scaling
1393
+ const canvas = document.createElement('canvas');
1394
+ const ctx = canvas.getContext('2d');
1395
+ if (ctx) {
1396
+ ctx.font = `${fontSize}px ${item.fontName || 'serif'}`;
1397
+ const metrics = ctx.measureText(item.str);
1398
+ textWidth = metrics.width;
1399
+ }
1400
+ else {
1401
+ // Fallback calculation with scaling
1402
+ textWidth = fontSize * item.str.length * 0.6;
1403
+ }
1404
+ }
1405
+ // Apply horizontal scaling from transform matrix
1406
+ const horizontalScale = Math.abs(scaleX);
1407
+ const verticalScale = Math.abs(scaleY);
1408
+ if (Math.abs(horizontalScale - verticalScale) > 0.01) {
1409
+ const scaleRatio = horizontalScale / verticalScale;
1410
+ textWidth = textWidth * scaleRatio;
1411
+ }
1412
+ // Set position and dimensions using screen coordinates
1413
+ textSpan.style.left = `${screenX}px`;
1414
+ textSpan.style.top = `${screenY - fontSize}px`; // Adjust for text baseline
1415
+ textSpan.style.width = `${textWidth}px`;
1416
+ textSpan.style.height = `${fontSize}px`;
1417
+ textSpan.style.fontSize = `${fontSize}px`;
1418
+ textSpan.style.fontFamily = item.fontName || 'serif';
1419
+ textSpan.style.overflow = 'hidden'; // Prevent text from extending beyond width
1420
+ textSpan.style.textOverflow = 'clip'; // Clip text at boundaries
1421
+ textSpan.style.whiteSpace = 'nowrap'; // Prevent text wrapping
1422
+ textSpan.style.letterSpacing = '0px'; // Remove any letter spacing interference
1423
+ textSpan.style.wordSpacing = '0px'; // Remove word spacing interference
1424
+ // Add data attributes for debugging
1425
+ textSpan.setAttribute('data-text', item.str);
1426
+ textSpan.setAttribute('data-width', textWidth.toString());
1427
+ textSpan.setAttribute('data-original-width', item.width?.toString() || 'unknown');
1428
+ // Handle horizontal scaling if different from vertical
1429
+ if (Math.abs(horizontalScale - verticalScale) > 0.1) {
1430
+ const scaleRatio = horizontalScale / verticalScale;
1431
+ textSpan.style.transform = `scaleX(${scaleRatio})`;
1432
+ textSpan.style.transformOrigin = '0 0';
1433
+ }
1434
+ // Handle rotation if present in transform
1435
+ if (Math.abs(transform[1]) > 0.01 || Math.abs(transform[2]) > 0.01) {
1436
+ const angle = (Math.atan2(transform[1], transform[0]) * 180) / Math.PI;
1437
+ textSpan.style.transform = `rotate(${angle}deg)`;
1438
+ }
1439
+ textLayer.appendChild(textSpan);
1440
+ });
1441
+ pageContainer.appendChild(textLayer);
1442
+ console.log(`Text layer added to page ${pageNumber} with ${textContent.items.length} text items`);
1443
+ console.log('Text span style applied:', textLayer.children[0]?.getAttribute('style'));
1444
+ }
1445
+ catch (error) {
1446
+ console.error(`Failed to add text layer to page ${pageNumber}:`, error);
1447
+ }
1448
+ }
1449
+ /**
1450
+ * Count overlapping highlights at the same coordinates
1451
+ */
1452
+ countOverlappingHighlights(highlightLayer, coord, scale) {
1453
+ const targetLeft = coord.x1 * scale;
1454
+ const targetTop = coord.y1 * scale;
1455
+ const targetWidth = (coord.x2 - coord.x1) * scale;
1456
+ const targetHeight = (coord.y2 - coord.y1) * scale;
1457
+ const existingHighlights = highlightLayer.children;
1458
+ let overlappingCount = 0;
1459
+ for (let i = 0; i < existingHighlights.length; i++) {
1460
+ const existing = existingHighlights[i];
1461
+ const existingLeft = parseFloat(existing.style.left);
1462
+ const existingTop = parseFloat(existing.style.top);
1463
+ const existingWidth = parseFloat(existing.style.width);
1464
+ const existingHeight = parseFloat(existing.style.height);
1465
+ // Check if highlights overlap significantly (>80% overlap)
1466
+ const overlapLeft = Math.max(targetLeft, existingLeft);
1467
+ const overlapTop = Math.max(targetTop, existingTop);
1468
+ const overlapRight = Math.min(targetLeft + targetWidth, existingLeft + existingWidth);
1469
+ const overlapBottom = Math.min(targetTop + targetHeight, existingTop + existingHeight);
1470
+ if (overlapRight > overlapLeft && overlapBottom > overlapTop) {
1471
+ const overlapArea = (overlapRight - overlapLeft) * (overlapBottom - overlapTop);
1472
+ const targetArea = targetWidth * targetHeight;
1473
+ const overlapPercentage = overlapArea / targetArea;
1474
+ if (overlapPercentage > 0.8) {
1475
+ overlappingCount++;
1476
+ }
1477
+ }
1478
+ }
1479
+ return overlappingCount;
1480
+ }
1481
+ /**
1482
+ * Apply selected term highlighting to a specific page
1483
+ */
1484
+ applySelectionToPage(pageNumber) {
1485
+ if (!this.selectedTermId)
1486
+ return;
1487
+ const pageContainer = this.pageContainers.get(pageNumber);
1488
+ if (!pageContainer)
1489
+ return;
1490
+ // Find all highlights in this page
1491
+ const allHighlights = pageContainer.querySelectorAll('.highlight, .highlight-wrapper');
1492
+ // Apply styling to all highlights
1493
+ allHighlights.forEach((element) => {
1494
+ const elementTermId = element.getAttribute('data-term-id');
1495
+ const htmlElement = element;
1496
+ if (elementTermId === this.selectedTermId) {
1497
+ // Apply selected styling to matching terms
1498
+ element.classList.add('selected-term');
1499
+ element.classList.remove('dimmed-highlight');
1500
+ htmlElement.style.opacity = '0.75';
1501
+ htmlElement.style.filter = 'brightness(1.05) contrast(1.05) saturate(1.1)';
1502
+ htmlElement.style.boxShadow =
1503
+ '0 0 0 1px rgba(255, 255, 255, 0.6), 0 0 4px rgba(102, 126, 234, 0.3)';
1504
+ htmlElement.style.transform = 'scale(1.02)';
1505
+ htmlElement.style.zIndex = '12';
1506
+ htmlElement.style.borderWidth = '1px';
1507
+ htmlElement.style.transition = 'all 0.3s ease';
1508
+ }
1509
+ else {
1510
+ // Apply dimmed styling to non-selected highlights
1511
+ element.classList.add('dimmed-highlight');
1512
+ element.classList.remove('selected-term');
1513
+ // Store original opacity to restore later
1514
+ if (!htmlElement.dataset.originalOpacity) {
1515
+ htmlElement.dataset.originalOpacity = htmlElement.style.opacity || '0.3';
1516
+ }
1517
+ htmlElement.style.opacity = '0.15';
1518
+ htmlElement.style.filter = 'brightness(0.6) contrast(0.7) saturate(0.4) grayscale(0.3)';
1519
+ htmlElement.style.transform = '';
1520
+ htmlElement.style.boxShadow = '';
1521
+ htmlElement.style.borderWidth = '';
1522
+ htmlElement.style.transition = 'all 0.3s ease';
1523
+ }
1524
+ });
1525
+ }
1526
+ // =============================================================================
1527
+ // Cleanup
1528
+ // =============================================================================
1529
+ destroy() {
1530
+ // Remove event listeners
1531
+ if (this.scrollListener && this.container) {
1532
+ this.container.removeEventListener('scroll', this.scrollListener);
1533
+ }
1534
+ // Destroy components
1535
+ this.pdfEngine.destroy();
1536
+ this.interactionHandler.destroy();
1537
+ this.performanceOptimizer.destroy();
1538
+ this.styleManager.destroy();
1539
+ // Clear DOM references
1540
+ this.container = null;
1541
+ this.pdfContainer = null;
1542
+ this.pageContainers.clear();
1543
+ // Clear state
1544
+ this.eventListeners = [];
1545
+ this.highlightData = {};
1546
+ this.isInitialized = false;
1547
+ this.emit('destroyed');
1548
+ }
1549
+ }
1550
+ export default PDFHighlightViewer;
1551
+ //# sourceMappingURL=PDFHighlightViewer.js.map