Skip to content

Commit

Permalink
feat: add neuroglancer url demo (#19)
Browse files Browse the repository at this point in the history
* add neuroglancer url demo

* update readme

* export functions, add contact sheet and image series

* try to fix up URL creation

* remove useless stuff

* contact sheet works, woo!

* sliders are replaced

* some comments
  • Loading branch information
TheMooseman authored Jun 12, 2024
1 parent 07e43f1 commit 78753f6
Show file tree
Hide file tree
Showing 13 changed files with 1,213 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"printWidth": 120,
"trailingComma": "es5",
"tabWidth": 4,
"singleQuote": true,
"semi": true,
"singleAttributePerLine": true
}
12 changes: 12 additions & 0 deletions apps/neuroglancer-url/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## Neuroglancer URL Generator

This is a demo for generating Neuroglancer URLs like the python POC [here](https://github.com/AllenInstitute/ome_zarr_converter/blob/main/src/ome_zarr_converter/neuroglancer/utils.py).

### How to build / run
1. `pnpm build` from the root directory of this project
2. `pnpm install` in this directory
3. `pnpm run demo` in this directory - this will produce `dst/neuroglancer-url.html`
4. navigate to `dst/neuroglancer-url.html` and open it in your browser

### The Code
-- TBD --
42 changes: 42 additions & 0 deletions apps/neuroglancer-url/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@alleninstitute/neuroglancer-url-demo",
"version": "0.0.1",
"contributors": [
{
"name": "Lane Sawyer",
"email": "[email protected]"
},
{
"name": "James Gerstenberger",
"email": "[email protected]"
},
{
"name": "Noah Shepard",
"email": "[email protected]"
},
{
"name": "Skyler Moosman",
"email": "[email protected]"
},
{
"name": "Su Li",
"email": "[email protected]"
}
],
"license": "TBD",
"type": "module",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
"demo": "esbuild src/demo.ts --bundle --outfile=dst/demo.js && cp public/* dst/",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"esbuild": "^0.19.12",
"typescript": "^5.3.3"
},
"dependencies": {
}
}
35 changes: 35 additions & 0 deletions apps/neuroglancer-url/public/demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<body style="width: 100vw; height: 100vh; overflow-x: hidden; display: flex; flex-direction: column">
<div style="width: 70%; height: 100%; display: flex; flex-direction: row">
<div
id="urlArgEl"
style="padding: 5px"
></div>
<div style="display: flex; flex-direction: column">
<div style="display: flex; flex-direction: row">
<button
id="urlBtn"
style="width: 100px; height: 20px"
>
Make URL
</button>
<button
id="copyBtn"
style="width: 100px; height: 20px"
>
Copy
</button>
</div>
<p
id="goodUrl"
style="max-width: 500px; text-wrap: nowrap; overflow: hidden; text-overflow: ellipsis"
></p>
</div>
</div>
<div id="ngUrls"><h3>NG URLS</h3></div>
<div id="contacts"><h3>Contact Sheets</h3></div>
<div id="imageSeries"><h3>Image Series</h3></div>
</body>
</html>
<script src="demo.js"></script>
125 changes: 125 additions & 0 deletions apps/neuroglancer-url/src/contact-sheet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { neuroglancerUrl, finalizeConfig, getImageLayer, getShaderCode, urlFromConfig } from './utils';

export function getContactSheetUrl(
srcUrl: string,
imgName: string,
omeZarrShape: number[],
xMm: number,
yMm: number,
zMm: number,
redMin: number,
redMax: number,
greenMin: number,
greenMax: number,
blueMin: number,
blueMax: number,
crossSectionScale: number = 0.5
): string {
const config = getContactSheetConfig(
srcUrl,
imgName,
omeZarrShape,
xMm,
yMm,
zMm,
redMin,
redMax,
greenMin,
greenMax,
blueMin,
blueMax,
crossSectionScale
);

console.log(config);
return urlFromConfig(neuroglancerUrl, config);
}

function getContactSheetConfig(
srcUrl: string,
imgName: string,
omeZarrShape: number[],
xMm: number,
yMm: number,
zMm: number,
redMin: number,
redMax: number,
greenMin: number,
greenMax: number,
blueMin: number,
blueMax: number,
crossSectionScale: number = 0.5
): Record<string, any> {
const [shaderCode, shaderControls] = getShaderCode(redMin, redMax, greenMin, greenMax, blueMin, blueMax);

const source = createSourceGrid(srcUrl, omeZarrShape, xMm, yMm, zMm);

const imgLayer = getImageLayer(imgName, source, shaderCode, shaderControls);

const config = finalizeConfig(imgLayer, imgName, xMm, yMm, zMm);

config['layout'] = 'xy';
config['velocity'] = { z: { velocity: -10, atBoundary: 'reverse' } };

const nz = omeZarrShape[omeZarrShape.length - 3];
const sqrtNz = Math.round(Math.sqrt(nz));
const ny = omeZarrShape[omeZarrShape.length - 2];
const nx = omeZarrShape[omeZarrShape.length - 1];
config['position'] = [(nx * sqrtNz) / 2, (ny * sqrtNz) / 2, -(nz - 1)];
config['crossSectionScale'] = crossSectionScale;

return config;
}

function createSourceGrid(srcUrl: string, omeZarrShape: number[], xMm: number, yMm: number, zMm: number): any[] {
const nz = omeZarrShape[omeZarrShape.length - 3];
const ny = omeZarrShape[omeZarrShape.length - 2];
const nx = omeZarrShape[omeZarrShape.length - 1];
const sqrtNz = Math.ceil(Math.sqrt(nz));
const izToDxDy: { [key: number]: [number, number] } = {};
let dx = 0;
let dy = 0;
for (let iz = 0; iz < nz; iz++) {
izToDxDy[iz] = [dx, dy];
dx++;
if (dx >= sqrtNz) {
dy++;
dx = 0;
}
}

const dimensions = {
'c^': [1, ''],
z: [0.001 * zMm, 'm'],
y: [0.001 * yMm, 'm'],
x: [0.001 * xMm, 'm'],
};

const srcList = [];
for (let iz = 0; iz < nz; iz++) {
const src = { url: srcUrl };
const [dx, dy] = izToDxDy[iz];
const matrix = [
[1, 0, 0, 0, 0],
[0, -1, 0, 0, -1 * iz],
[0, 0, 1, 0, dy * ny],
[0, 0, 0, 1, dx * nx],
];
// @ts-expect-error
src['transform'] = {
outputDimensions: dimensions,
matrix: matrix,
};

// @ts-expect-error
src['subsources'] = {
default: true,
};
// @ts-expect-error
src['enableDefaultSubsources'] = false;

srcList.push(src);
}

return srcList;
}
349 changes: 349 additions & 0 deletions apps/neuroglancer-url/src/data.ts

Large diffs are not rendered by default.

145 changes: 145 additions & 0 deletions apps/neuroglancer-url/src/demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import type { ImageSeriesData, NGData, RegularData } from './types';
import { getNeuroglancerUrl } from './utils';
import { data } from './data';
import { getContactSheetUrl } from './contact-sheet';
import { makeContactSheetElems, makeImageSeriesElems, makeNgUrlElems } from './url-elem-maker';

interface NeuroglancerUrl {
srcUrl: string;
imgName: string;
xMm: number;
yMm: number;
zMm: number;
redMin: number;
redMax: number;
greenMin: number;
greenMax: number;
blueMin: number;
blueMax: number;
crossSectionScale?: number;
layout?: string;
}

const jsonFormat = {
input: {
srcUrl: '',
imgName: '',
xMm: 0,
yMm: 0,
zMm: 0,
redMin: 0,
redMax: 255,
greenMin: 0,
greenMax: 255,
blueMin: 0,
blueMax: 255,
crossSectionScale: 50.0,
layout: '4panel',
},
output: 'neuroglancer.com/urlstuff',
};

const defaultNeuroglancerUrl: NeuroglancerUrl = {
srcUrl: '',
imgName: '',
xMm: 0,
yMm: 0,
zMm: 0,
redMin: 0,
redMax: 255,
greenMin: 0,
greenMax: 255,
blueMin: 0,
blueMax: 255,
crossSectionScale: 50.0,
layout: '4panel',
};
function demoTime() {
let imageSeries: ImageSeriesData[] = [];
let contactSheets: RegularData[] = [];
let ngURLs: RegularData[] = [];

data.forEach((item) => {
switch (item.function) {
case 'get_image_series_grid_url':
imageSeries.push(item as ImageSeriesData);
break;
case 'get_contact_sheet_url':
contactSheets.push(item as RegularData);
break;
case 'get_neuroglancer_url':
ngURLs.push(item as RegularData);
break;
}
});

const argEl = document.getElementById('urlArgEl');
const btnEl = document.getElementById('urlBtn');
const outUrl = document.getElementById('goodUrl');
const copyBtn = document.getElementById('copyBtn');
const ngUrlParent = document.getElementById('ngUrls');
const contactParent = document.getElementById('contacts');
const imageParent = document.getElementById('imageSeries');
if (argEl && btnEl && outUrl && copyBtn && ngUrlParent && contactParent && imageParent) {
const fields = document.createElement('ol');
const fieldItems = Object.entries(defaultNeuroglancerUrl).map(([name, val]) => {
const listEl = document.createElement('li');
const pEl = document.createElement('p');
const text = document.createElement('input');
text.type = 'text';
text.value = val;
// @ts-expect-error
text.onchange = (e) => (defaultNeuroglancerUrl[name] = val);

pEl.innerText = name;
listEl.id = name;

listEl.appendChild(pEl);
listEl.appendChild(text);
fields.appendChild(listEl);
});
argEl.appendChild(fields);
btnEl.addEventListener('click', (e) => {
const {
srcUrl,
imgName,
xMm,
yMm,
zMm,
redMin,
redMax,
greenMin,
greenMax,
blueMin,
blueMax,
crossSectionScale,
layout,
} = defaultNeuroglancerUrl;
outUrl.innerText = getNeuroglancerUrl(
srcUrl,
imgName,
xMm,
yMm,
zMm,
redMin,
redMax,
greenMin,
greenMax,
blueMin,
blueMax,
crossSectionScale,
layout
);
});
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(outUrl.innerText);
});

// These URLs are example URLs that acted as test cases for my formatting
makeNgUrlElems(ngURLs, ngUrlParent);
makeContactSheetElems(contactSheets, contactParent);
makeImageSeriesElems(imageSeries, imageParent);
}
}

demoTime();
Loading

0 comments on commit 78753f6

Please sign in to comment.