Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo } from 'react';
import React, { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { Box, Typography, useMediaQuery } from '@mui/material';
import { useTheme } from '@mui/material/styles';
Expand All @@ -8,6 +8,7 @@ const NEUROGLASS_URL = import.meta.env.NEUROGLASS_URL ?? '';

export default function NeuroglassViewer() {
const [debouncedSrc, setDebouncedSrc] = useState('');
const [iframeSrc, setIframeSrc] = useState('');

const allLoadedInstances = useSelector(state => state.instances?.allLoadedInstances);
const focusedInstance = useSelector(state => state.instances?.focusedInstance);
Expand All @@ -16,16 +17,35 @@ export default function NeuroglassViewer() {
const theme = useTheme();
const isMobile = !useMediaQuery(theme.breakpoints.up('lg'));

// Rebuilds whenever instances, focused item, layout preference, or viewport size changes.
const iframeSrc = useMemo(() => {
const layout = resolveNeuroglassLayout(neuroglassView, isMobile);
const state = buildNeuroglassState(
allLoadedInstances,
focusedInstance?.metadata?.Id,
layout,
);
if (!state || !NEUROGLASS_URL) return '';
return `${NEUROGLASS_URL}/embed#!${encodeURIComponent(JSON.stringify(state))}`;
useEffect(() => {
let cancelled = false;

async function buildSrc() {
const layout = resolveNeuroglassLayout(neuroglassView, isMobile);

const state = await buildNeuroglassState(
allLoadedInstances,
focusedInstance?.metadata?.Id,
layout,
);

if (cancelled) return;

if (!state || !NEUROGLASS_URL) {
setIframeSrc('');
return;
}

setIframeSrc(
`${NEUROGLASS_URL}/embed#!${encodeURIComponent(JSON.stringify(state))}`
);
}

buildSrc();

return () => {
cancelled = true;
Comment thread
jrmartin marked this conversation as resolved.
Outdated
};
}, [allLoadedInstances, focusedInstance?.metadata?.Id, neuroglassView, isMobile]);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,88 @@ const SHARED_VIEWPORT = {
projectionScale: 1024,
};

function buildNeuroglassLayerUrl(protocol, baseUrl, instanceId) {
const path = instanceId;
if (protocol === 'neuroglancer-precomputed' || protocol === 'n5') {
// GCS / S3 require Neuroglancer's pipe notation
return `${baseUrl}/${path}/|${protocol}:`;
}
// HTTP fileservers use a precomputed:// prefix
return `precomputed://${baseUrl}/${path}`;
}

// Datasource configuration for Datasource
export const NEUROGLASS_DATASOURCE = {
protocol: import.meta.env.NEUROGLASS_DATA_PROTOCOL,
baseUrl: import.meta.env.NEUROGLASS_DATA_BASE_URL,
buildUrl(instanceId) {
return buildNeuroglassLayerUrl(
async buildUrl(instanceId) {
let instancePath = instanceId.replace(
/^VFB_(\d{4})([a-zA-Z0-9]+)$/i,
"VFB/i/$1/$2/"
);
const layerURL = await buildNeuroglassLayerUrl(
this.protocol,
this.baseUrl,
instanceId,
instancePath,
);
console.log(`[NEUROGLASS_DATASOURCE] Built URL for instance ${instanceId}: ${layerURL}`);
Comment thread
jrmartin marked this conversation as resolved.
Outdated
return layerURL;
},
};

async function buildNeuroglassLayerUrl(protocol, baseUrl, instanceId) {
let path = instanceId;

// Check if the protocol is 'neuroglancer-precomputed' or 'n5'
Comment thread
jrmartin marked this conversation as resolved.
Outdated
if (protocol === 'neuroglancer-precomputed') {
const url = `${baseUrl}/${path}`;
try {
// Fetch the folder contents of the URL
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch URL: ${url}`);
}

// Check if the response is HTML
const contentType = response.headers.get('Content-Type');
if (contentType && contentType.includes('text/html')) {
// Parse the HTML response
const html = await response.text();
const folderNames = extractFolderNamesFromHtml(html);

// Find the folder starting with VFB_
const vfbFolder = folderNames.find((name) => name.startsWith('VFB_'));

if (vfbFolder) {
// Check if the VFB_ folder contains a 'neuroglancer' folder
Comment thread
jrmartin marked this conversation as resolved.
Outdated
const vfbFolderUrl = `${url}/${vfbFolder}`;
const vfbResponse = await fetch(vfbFolderUrl);
if (!vfbResponse.ok) {
throw new Error(`Failed to fetch VFB folder: ${vfbFolderUrl}`);
}

const vfbHtml = await vfbResponse.text();
const vfbFolderNames = extractFolderNamesFromHtml(vfbHtml);
const neuroglancerFolder = vfbFolderNames.find(
(name) => name === 'neuroglancer/'
);

if (neuroglancerFolder) {
return `${vfbFolderUrl}/${neuroglancerFolder}/|${protocol}:`;
}
Comment thread
jrmartin marked this conversation as resolved.
Outdated
}
} else {
throw new Error(`Unexpected Content-Type: ${contentType}`);
}
} catch (error) {
console.error(`[buildNeuroglassLayerUrl] Error: ${error.message}`);
}
} else if (protocol === 'gs' || protocol === 'n5') {
return `${baseUrl}/${path}/|${protocol}:`;
}

return `precomputed://${baseUrl}/${path}`;
Comment thread
jrmartin marked this conversation as resolved.
Outdated
Comment thread
jrmartin marked this conversation as resolved.
Outdated
}

function extractFolderNamesFromHtml(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const links = doc.querySelectorAll('a'); // Select all <a> tags
const folderNames = Array.from(links)
.map((link) => link.textContent.trim()) // Extract text content
.filter((name) => name && !name.startsWith('..')); // Exclude parent directory links
return folderNames;
}

// Fixed GLSL shader template : Only shaderControls.color and layer.opacity change per instance.
export const LAYER_SHADER = [
'#uicontrol invlerp contrast',
Expand Down Expand Up @@ -117,10 +176,12 @@ function normalizeContrast(inst) {
}

// Per-instance layer builder: converts a VFB instance into a Neuroglancer layer config.
function buildSingleInstanceLayer(inst) {
async function buildSingleInstanceLayer(inst) {
const source = await NEUROGLASS_DATASOURCE.buildUrl(inst.metadata.Id);

const layer = {
type: 'image',
source: NEUROGLASS_DATASOURCE.buildUrl(inst.metadata.Id),
source: source,
tab: 'rendering',
opacity: alphaToOpacity(inst.color?.a),
blend: 'additive',
Expand All @@ -134,22 +195,27 @@ function buildSingleInstanceLayer(inst) {
name: inst.metadata.Id,
};

console.log(`[buildSingleInstanceLayer] Built layer for instance ${inst.metadata.Id}:`, layer);
Comment thread
jrmartin marked this conversation as resolved.
Outdated

if (inst.visibleMesh === false) layer.visible = false;

return layer;
}
// Main state builder: converts loaded VFB instances + UI state into a Neuroglass viewer state object.
export function buildNeuroglassState(allLoadedInstances, focusedInstanceId, layout) {

export async function buildNeuroglassState(allLoadedInstances, focusedInstanceId, layout) {
const instances = allLoadedInstances || [];
const layers = instances
.filter(inst => {
if (!inst?.metadata?.Id) {
console.warn(`[buildNeuroglassState] Instance missing metadata ID:`, inst);
return false;
}
return true;
})
.map(inst => buildSingleInstanceLayer(inst));

const layers = await Promise.all(
instances
.filter(inst => {
if (!inst?.metadata?.Id) {
console.warn(`[buildNeuroglassState] Instance missing metadata ID:`, inst);
return false;
}
return true;
})
.map(inst => buildSingleInstanceLayer(inst))
);
Comment thread
jrmartin marked this conversation as resolved.

if (layers.length === 0) return null;

Expand Down
Loading