Skip to content

Commit ae99d1e

Browse files
committed
refactor move grayscale img convert logic to dedicated module
1 parent f40e43c commit ae99d1e

File tree

3 files changed

+225
-189
lines changed

3 files changed

+225
-189
lines changed

src/components/CytoscapeGraph.jsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,16 @@
2323
import React, { useEffect, useRef } from "react";
2424
import {
2525
mountCy, syncElements, wireEvents,
26-
hasPendingGrayscaleConversions, updateCompletedGrayscaleImages,
2726
updateOverlays
2827
} from "../graph/cyAdapter.js";
28+
import {
29+
hasPendingGrayscaleConversions,
30+
updateCompletedGrayscaleImages
31+
} from "../utils/grayscaleUtils.js";
2932
import { setNoteCountsVisible, refreshPositions as refreshOverlayPositions } from '../graph/overlayManager.js';
3033
import { printDebug, printError, printWarn } from "../utils/debug.js";
34+
import { TEST_ICON_SVG } from "../constants/testAssets.js";
35+
import { GRAYSCALE_IMAGES } from "../config/features.js";
3136

3237
function CytoscapeGraph({
3338
nodes = [],
@@ -62,6 +67,7 @@ function CytoscapeGraph({
6267
visited = { nodes: new Set(), edges: new Set() },
6368
onCytoscapeInstanceReady
6469
}) {
70+
6571
const containerRef = useRef(null);
6672
const cyRef = useRef(null);
6773

src/graph/cyAdapter.js

Lines changed: 27 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,19 @@ import cytoscapeStyles from "../cytoscapeStyles.js";
2525
import { deserializeGraph } from "./ops.js";
2626
import { TEST_ICON_SVG } from "../constants/testAssets.js";
2727
import { GRAYSCALE_IMAGES } from "../config/features.js";
28-
import { convertImageToGrayscale } from "../utils/grayscaleUtils.js";
28+
import {
29+
preprocessImageToGrayscale,
30+
getCachedGrayscaleImage,
31+
shouldProcessForGrayscale,
32+
clearGrayscaleCache as clearGrayscaleCacheUtil
33+
} from "../utils/grayscaleUtils.js";
2934
import { getCdnBaseUrl } from "../utils/cdnHelpers.js";
3035
import { loadImageWithFallback, imageCache, getDefaultPlaceholderSvg, ensureDefaultPlaceholderLoaded, onDefaultPlaceholderLoaded } from "../utils/imageLoader.js";
3136
import { printDebug } from "../utils/debug.js"; // printWarn
3237
import { installAppearOnAdd } from '../anim/appear.js';
3338
import { isSearchInProgress, isCurrentSearchSelection, getCurrentSearchIds } from '../search/searchHighlighter.js';
3439
import { ensure as ensureOverlays, refreshPositions as refreshOverlayPositions, attach as attachOverlayManager, detach as detachOverlayManager, setNoteCountsVisible } from './overlayManager.js';
3540

36-
// Cache for grayscale images to avoid reprocessing
37-
const GRAYSCALE_CACHE_KEY = 'shipLogGrayscaleCache';
38-
const grayscaleCache = new Map();
39-
const pendingConversions = new Set();
40-
4141
// Track pending image loads to prevent duplicate requests
4242
const pendingImageLoads = new Set();
4343

@@ -47,144 +47,11 @@ const pendingNodeImageUpdates = [];
4747
// Track current active Cytoscape instance (helps with React 18 StrictMode double mount)
4848
let currentActiveCy = null;
4949

50-
// Load grayscale cache from localStorage on module initialization
51-
function loadGrayscaleCacheFromStorage() {
52-
try {
53-
const cached = localStorage.getItem(GRAYSCALE_CACHE_KEY);
54-
if (cached) {
55-
const parsedCache = JSON.parse(cached);
56-
const entryCount = Object.keys(parsedCache).length;
57-
printDebug(`💾 [cyAdapter] Loading grayscale cache from localStorage with ${entryCount} entries`);
58-
Object.entries(parsedCache).forEach(([key, value]) => {
59-
grayscaleCache.set(key, value);
60-
printDebug(`💾 [cyAdapter] Loaded grayscale cache entry: "${key.substring(0, 50)}..." -> value length: ${value?.length || 0}`);
61-
});
62-
printDebug(`✅ [cyAdapter] Grayscale cache loaded successfully with ${grayscaleCache.size} entries`);
63-
} else {
64-
printDebug(`💾 [cyAdapter] No grayscale cache found in localStorage`);
65-
}
66-
} catch (error) {
67-
console.warn('Failed to load grayscale cache from localStorage:', error);
68-
}
69-
}
70-
71-
// Save grayscale cache to localStorage
72-
function saveGrayscaleCacheToStorage() {
73-
try {
74-
const cacheObject = {};
75-
let savedCount = 0;
76-
grayscaleCache.forEach((value, key) => {
77-
// Only save completed conversions (strings), not promises
78-
if (typeof value === 'string') {
79-
cacheObject[key] = value;
80-
savedCount++;
81-
}
82-
});
83-
printDebug(`💾 [cyAdapter] Saving grayscale cache to localStorage: ${savedCount} entries out of ${grayscaleCache.size} total`);
84-
localStorage.setItem(GRAYSCALE_CACHE_KEY, JSON.stringify(cacheObject));
85-
printDebug(`✅ [cyAdapter] Grayscale cache saved successfully`);
86-
} catch (error) {
87-
console.warn('Failed to save grayscale cache to localStorage:', error);
88-
}
89-
}
90-
91-
// Initialize cache from localStorage
92-
loadGrayscaleCacheFromStorage();
93-
94-
// Preprocess images to grayscale in the background to avoid race conditions
95-
function preprocessImageToGrayscale(imageUrl, originalImagePath = null) {
96-
printDebug(`🎨 [cyAdapter] preprocessImageToGrayscale called with: "${imageUrl?.substring(0, 50)}..." (originalPath: ${originalImagePath || 'none'})`);
97-
98-
if (!GRAYSCALE_IMAGES || !imageUrl) {
99-
printDebug(`⚠️ [cyAdapter] Skipping grayscale: feature disabled or no image`);
100-
return Promise.resolve(imageUrl);
101-
}
102-
103-
// Skip SVG placeholders and test icons
104-
if (imageUrl === TEST_ICON_SVG || imageUrl.includes('data:image/svg+xml')) {
105-
printDebug(`⚠️ [cyAdapter] Skipping grayscale for SVG placeholder`);
106-
return Promise.resolve(imageUrl);
107-
}
108-
109-
// Only process real image data URLs - skip filenames and non-image data
110-
if (!imageUrl.startsWith('data:image/') || imageUrl.startsWith('data:image/svg+xml')) {
111-
printDebug(`⚠️ [cyAdapter] Skipping grayscale for non-image data URL: "${imageUrl?.substring(0, 50)}..."`);
112-
return Promise.resolve(imageUrl);
113-
}
114-
115-
// Use original image path as cache key if available, otherwise fall back to image URL
116-
const cacheKey = originalImagePath || imageUrl;
117-
118-
if (grayscaleCache.has(cacheKey)) {
119-
const cached = grayscaleCache.get(cacheKey);
120-
printDebug(`💾 [cyAdapter] Found cached grayscale result for key: "${originalImagePath ? 'path-based' : 'url-based'}"`);
121-
// If it's a string (completed conversion), return it
122-
if (typeof cached === 'string') {
123-
return Promise.resolve(cached);
124-
}
125-
// If it's a promise (in progress), return the promise
126-
return cached;
127-
}
128-
129-
printDebug(`🔄 [cyAdapter] Starting new grayscale conversion with cache key: "${cacheKey?.substring(0, 50)}..."`);
130-
// Start conversion in background, but don't wait for it
131-
const conversionPromise = convertImageToGrayscale(imageUrl)
132-
.then(grayscaleUrl => {
133-
grayscaleCache.set(cacheKey, grayscaleUrl);
134-
pendingConversions.delete(imageUrl);
135-
// Save to localStorage when conversion completes
136-
saveGrayscaleCacheToStorage();
137-
return grayscaleUrl;
138-
})
139-
.catch(error => {
140-
console.warn('Failed to convert image to grayscale:', error);
141-
grayscaleCache.set(cacheKey, imageUrl); // Cache the original to avoid retrying
142-
pendingConversions.delete(imageUrl);
143-
// Save to localStorage even on failure to avoid retrying
144-
saveGrayscaleCacheToStorage();
145-
return imageUrl;
146-
});
147-
148-
pendingConversions.add(imageUrl);
149-
grayscaleCache.set(cacheKey, conversionPromise);
150-
return conversionPromise;
151-
}
152-
153-
// Check if there are pending conversions that might benefit from an update
154-
export function hasPendingGrayscaleConversions() {
155-
return pendingConversions.size > 0;
156-
}
157-
158-
// Update only image data for nodes that have completed grayscale conversion
159-
export function updateCompletedGrayscaleImages(cy, graph) {
160-
if (!GRAYSCALE_IMAGES || pendingConversions.size === 0) return false;
161-
162-
let updated = false;
163-
const g = deserializeGraph(graph);
164-
165-
g.nodes.forEach(n => {
166-
const imageUrl = n.imageUrl;
167-
if (imageUrl && imageUrl !== TEST_ICON_SVG && grayscaleCache.has(imageUrl)) {
168-
const cached = grayscaleCache.get(imageUrl);
169-
if (typeof cached === 'string' && !pendingConversions.has(imageUrl)) {
170-
// Conversion completed, update the node
171-
const cyNode = cy.getElementById(n.id);
172-
if (cyNode.length > 0 && cyNode.data('imageUrl') !== cached) {
173-
cyNode.data('imageUrl', cached);
174-
updated = true;
175-
}
176-
}
177-
}
178-
});
179-
180-
return updated;
181-
}
182-
18350
/**
18451
* Update overlays from a notes object and visibility flag.
18552
* IMPORTANT: While a node is being dragged, we only reposition overlays
18653
* (cheap) instead of re-ensuring/recreating them (expensive). This stops
187-
* badges from flying or snapping.
54+
* badges from "flying" or snapping.
18855
*/
18956
export function updateOverlays(cy, notes, showNoteCountOverlay, visited = null, mode = 'editing') {
19057
if (!cy || cy.destroyed()) return;
@@ -280,9 +147,9 @@ export function buildElementsFromDomain(graph, options = {}) {
280147
}
281148
}
282149

283-
if (GRAYSCALE_IMAGES && loadedImageUrl && (loadedImageUrl.startsWith('data:image/png;') || loadedImageUrl.startsWith('data:image/jpeg;') || loadedImageUrl.startsWith('data:image/jpg;') || loadedImageUrl.startsWith('data:image/webp;'))) {
150+
if (GRAYSCALE_IMAGES && shouldProcessForGrayscale(loadedImageUrl, TEST_ICON_SVG)) {
284151
printDebug(`🎨 [cyAdapter] Starting grayscale (post-display) for: ${originalImageUrl}`);
285-
preprocessImageToGrayscale(loadedImageUrl, originalImageUrl).then(grayscaleUrl => {
152+
preprocessImageToGrayscale(loadedImageUrl, originalImageUrl, TEST_ICON_SVG).then(grayscaleUrl => {
286153
printDebug(`✅ [cyAdapter] Grayscale conversion complete for: ${originalImageUrl}`);
287154
if (onImageLoaded && typeof onImageLoaded === 'function') {
288155
try {
@@ -313,25 +180,19 @@ export function buildElementsFromDomain(graph, options = {}) {
313180
}
314181

315182
// Apply grayscale to images that are already data URLs (cached images) - but not SVGs
316-
if (GRAYSCALE_IMAGES && imageUrl &&
317-
(imageUrl.startsWith('data:image/png;') ||
318-
imageUrl.startsWith('data:image/jpeg;') ||
319-
imageUrl.startsWith('data:image/jpg;') ||
320-
imageUrl.startsWith('data:image/webp;')) &&
321-
imageUrl !== TEST_ICON_SVG) {
322-
183+
if (GRAYSCALE_IMAGES && shouldProcessForGrayscale(imageUrl, TEST_ICON_SVG)) {
323184
const originalPath = n.imageUrl; // The original filename
324-
let cached = grayscaleCache.get(originalPath);
325-
if (!cached || typeof cached !== 'string') {
326-
cached = grayscaleCache.get(imageUrl); // Fallback to data URL key
185+
let cached = getCachedGrayscaleImage(originalPath);
186+
if (!cached) {
187+
cached = getCachedGrayscaleImage(imageUrl); // Fallback to data URL key
327188
}
328189

329-
if (cached && typeof cached === 'string') {
190+
if (cached) {
330191
printDebug(`✅ [cyAdapter] Using cached grayscale for data URL (key: ${originalPath ? 'path-based' : 'url-based'})`);
331192
imageUrl = cached; // already grayscale
332193
} else {
333194
printDebug(`🔄 [cyAdapter] Queueing grayscale conversion (will update later) for cached data URL`);
334-
preprocessImageToGrayscale(imageUrl, originalPath); // background; node shows color first
195+
preprocessImageToGrayscale(imageUrl, originalPath, TEST_ICON_SVG); // background; node shows color first
335196
}
336197
}
337198

@@ -724,17 +585,9 @@ export function syncElements(cy, graph, options = {}) {
724585
if (!dataUrl) return;
725586

726587
let finalUrl = dataUrl;
727-
if (
728-
GRAYSCALE_IMAGES &&
729-
typeof dataUrl === 'string' &&
730-
!dataUrl.includes('data:image/svg+xml') &&
731-
(dataUrl.startsWith('data:image/png;') ||
732-
dataUrl.startsWith('data:image/jpeg;') ||
733-
dataUrl.startsWith('data:image/jpg;') ||
734-
dataUrl.startsWith('data:image/webp;'))
735-
) {
588+
if (GRAYSCALE_IMAGES && shouldProcessForGrayscale(dataUrl, TEST_ICON_SVG)) {
736589
try {
737-
finalUrl = await preprocessImageToGrayscale(dataUrl, orig);
590+
finalUrl = await preprocessImageToGrayscale(dataUrl, orig, TEST_ICON_SVG);
738591
} catch {
739592
finalUrl = dataUrl;
740593
}
@@ -918,9 +771,6 @@ export function wireEvents(cy, handlers = {}, mode = 'editing') {
918771
}
919772
container.addEventListener('touchmove', debouncedTouchMoveHandler, { passive: true });
920773

921-
cy.on('mouseover', 'node.entry-parent, edge', (evt) => { evt.cy.container().style.cursor = 'pointer'; });
922-
cy.on('mouseout', 'node.entry-parent, edge', (evt) => { evt.cy.container().style.cursor = 'default'; });
923-
924774
return () => {
925775
printDebug('🧹 [cyAdapter] Removing event listeners');
926776
cy.removeListener('*');
@@ -933,22 +783,14 @@ export function wireEvents(cy, handlers = {}, mode = 'editing') {
933783
};
934784
}
935785

936-
export function clearGrayscaleCache() {
937-
const entriesBeforeClear = grayscaleCache.size;
938-
const pendingBeforeClear = pendingConversions.size;
939-
940-
grayscaleCache.clear();
941-
pendingConversions.clear();
942-
943-
printDebug(`🧹 [cyAdapter] Cleared grayscale cache: ${entriesBeforeClear} entries, ${pendingBeforeClear} pending conversions`);
944-
945-
try {
946-
localStorage.removeItem(GRAYSCALE_CACHE_KEY);
947-
printDebug(`✅ [cyAdapter] Removed grayscale cache from localStorage`);
948-
} catch (e) {
949-
console.warn('Failed to clear grayscale cache from localStorage:', e);
950-
}
951-
}
786+
// Re-export clear function from grayscale utils
787+
export const clearGrayscaleCache = clearGrayscaleCacheUtil;
788+
789+
// Re-export hasPendingGrayscaleConversions for backward compatibility
790+
export { hasPendingGrayscaleConversions } from '../utils/grayscaleUtils.js';
791+
792+
// Re-export updateCompletedGrayscaleImages for backward compatibility
793+
export { updateCompletedGrayscaleImages } from '../utils/grayscaleUtils.js';
952794

953795
// Force immediate image update for a specific node
954796
export async function forceNodeImageUpdate(nodeId, imagePath, mapName, cdnBaseUrl, immediateImageUrl = null) {
@@ -978,7 +820,7 @@ export async function forceNodeImageUpdate(nodeId, imagePath, mapName, cdnBaseUr
978820

979821
let finalImageUrl = imageUrl;
980822
if (GRAYSCALE_IMAGES && !imageUrl.includes('data:image/svg+xml')) {
981-
finalImageUrl = await preprocessImageToGrayscale(imageUrl, imagePath);
823+
finalImageUrl = await preprocessImageToGrayscale(imageUrl, imagePath, TEST_ICON_SVG);
982824
printDebug(`🎨 [cyAdapter] Applied grayscale for immediate update`);
983825
}
984826

@@ -1013,4 +855,4 @@ export function debugPrintEntireGraph(cy) {
1013855
}));
1014856
console.log("Cytoscape Nodes:", nodes);
1015857
console.log("Cytoscape Edges:", edges);
1016-
}
858+
}

0 commit comments

Comments
 (0)