Skip to content

Commit

Permalink
Merge pull request #223 from allen-cell-animated/feature/landing-page…
Browse files Browse the repository at this point in the history
…-styling

- Adds `react-router-dom` and routes for the landing page (at root URL, `/`) and viewer (`/viewer`).
- The landing page can now redirect to the viewer when a Load button is clicked, and passes the app arguments as state through the navigation API.
- Renamed the `landing-page` directory to `website`, since it now includes several website-related components.
- Adds a new `AppWrapper` component, which wraps `ImageViewerApp`.
  - Handles retrieving app arguments from URL or from routing.
- Moves the URL parsing logic to `url_utils.tsx`.
  • Loading branch information
ShrimpCryptid authored Apr 23, 2024
2 parents 3810a8d + 3eb2d87 commit 5179a2a
Show file tree
Hide file tree
Showing 16 changed files with 482 additions and 309 deletions.
39 changes: 39 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"lodash": "^4.17.20",
"nouislider-react": "^3.4.0",
"react-color": "^2.19.3",
"react-router-dom": "^6.22.3",
"styled-components": "^6.1.8"
},
"peerDependencies": {
Expand Down
275 changes: 30 additions & 245 deletions public/index.tsx
Original file line number Diff line number Diff line change
@@ -1,262 +1,47 @@
import React from "react";
import ReactDOM from "react-dom";
import { createBrowserRouter, createHashRouter, RouterProvider } from "react-router-dom";
import { Router } from "@remix-run/router";

import "antd/dist/antd.less";

// Components
import AppWrapper from "../website/components/AppWrapper";
import LandingPage from "../website/components/LandingPage";
import ErrorPage from "../website/components/ErrorPage";
import StyleProvider from "../src/aics-image-viewer/components/StyleProvider";
import "../src/aics-image-viewer/assets/styles/typography.css";
import "./App.css";

import { ImageViewerApp, RenderMode, ViewerChannelSettings, ViewMode } from "../src";
import FirebaseRequest, { DatasetMetaData } from "./firebase";
import { AppProps, GlobalViewerSettings } from "../src/aics-image-viewer/components/App/types";

// vars filled at build time using webpack DefinePlugin
console.log(`website-3d-cell-viewer ${WEBSITE3DCELLVIEWER_BUILD_ENVIRONMENT} build`);
console.log(`website-3d-cell-viewer Version ${WEBSITE3DCELLVIEWER_VERSION}`);
console.log(`volume-viewer Version ${VOLUMEVIEWER_VERSION}`);

export const VIEWER_3D_SETTINGS: ViewerChannelSettings = {
groups: [
{
name: "Observed channels",
channels: [
{ name: "Membrane", match: ["(CMDRP)"], color: "E2CDB3", enabled: true, lut: ["p50", "p98"] },
{
name: "Labeled structure",
match: ["(EGFP)|(RFPT)"],
color: "6FBA11",
enabled: true,
lut: ["p50", "p98"],
},
{ name: "DNA", match: ["(H3342)"], color: "8DA3C0", enabled: true, lut: ["p50", "p98"] },
{ name: "Bright field", match: ["(100)|(Bright)"], color: "F5F1CB", enabled: false, lut: ["p50", "p98"] },
],
},
{
name: "Segmentation channels",
channels: [
{
name: "Labeled structure",
match: ["(SEG_STRUCT)"],
color: "E0E3D1",
enabled: false,
lut: ["p50", "p98"],
},
{ name: "Membrane", match: ["(SEG_Memb)"], color: "DD9BF5", enabled: false, lut: ["p50", "p98"] },
{ name: "DNA", match: ["(SEG_DNA)"], color: "E3F4F5", enabled: false, lut: ["p50", "p98"] },
],
},
{
name: "Contour channels",
channels: [
{ name: "Membrane", match: ["(CON_Memb)"], color: "FF6200", enabled: false, lut: ["p50", "p98"] },
{ name: "DNA", match: ["(CON_DNA)"], color: "F7DB78", enabled: false, lut: ["p50", "p98"] },
],
},
],
// must be the true channel name in the volume data
maskChannelName: "SEG_Memb",
};

type ParamKeys = "mask" | "ch" | "luts" | "colors" | "url" | "file" | "dataset" | "id" | "view";
type Params = { [_ in ParamKeys]?: string };

function parseQueryString(): Params {
const pairs = location.search.slice(1).split("&");
const result = {};
pairs.forEach((pairString) => {
const pair = pairString.split("=");
result[pair[0]] = decodeURIComponent(pair[1] || "");
});
return JSON.parse(JSON.stringify(result));
}
const params = parseQueryString();

const decodeURL = (url: string): string => {
const decodedUrl = decodeURIComponent(url);
return decodedUrl.endsWith("/") ? decodedUrl.slice(0, -1) : decodedUrl;
};

/** Try to parse a `string` as a list of 2 or more URLs. Returns `undefined` if the string is not a valid URL list. */
const tryDecodeURLList = (url: string, delim = ","): string[] | undefined => {
if (!url.includes(delim)) {
return undefined;
}

const urls = url.split(delim).map((u) => decodeURL(u));

// Verify that all urls are valid
for (const u of urls) {
try {
new URL(u);
} catch (_e) {
return undefined;
}
}

return urls;
};

const BASE_URL = "https://s3-us-west-2.amazonaws.com/bisque.allencell.org/v1.4.0/Cell-Viewer_Thumbnails/";
const args: Omit<AppProps, "appHeight" | "canvasMargin"> = {
cellId: "2025",
imageUrl: BASE_URL + "AICS-22/AICS-22_8319_2025_atlas.json",
parentImageUrl: BASE_URL + "AICS-22/AICS-22_8319_atlas.json",
parentImageDownloadHref: "https://files.allencell.org/api/2.0/file/download?collection=cellviewer-1-4/?id=F8319",
imageDownloadHref: "https://files.allencell.org/api/2.0/file/download?collection=cellviewer-1-4/?id=C2025",
viewerChannelSettings: VIEWER_3D_SETTINGS,
};
const viewerSettings: Partial<GlobalViewerSettings> = {
showAxes: false,
showBoundingBox: false,
autorotate: false,
viewMode: ViewMode.threeD,
renderMode: RenderMode.volumetric,
maskAlpha: 50,
brightness: 70,
density: 50,
levels: [0, 128, 255] as [number, number, number],
backgroundColor: [0, 0, 0] as [number, number, number],
boundingBoxColor: [255, 255, 255] as [number, number, number],
};

async function loadDataset(dataset: string, id: string) {
const db = new FirebaseRequest();

const datasets = await db.getAvailableDatasets();

let datasetMeta: DatasetMetaData | undefined = undefined;
for (const d of datasets) {
const innerDatasets = d.datasets!;
const names = Object.keys(innerDatasets);
const matchingName = names.find((name) => name === dataset);
if (matchingName) {
datasetMeta = innerDatasets[matchingName];
break;
}
}
if (datasetMeta === undefined) {
console.error(`No matching dataset: ${dataset}`);
return;
}

const datasetData = await db.selectDataset(datasetMeta.manifest!);
const baseUrl = datasetData.volumeViewerDataRoot + "/";
args.imageDownloadHref = datasetData.downloadRoot + "/" + id;
// args.fovDownloadHref = datasetData.downloadRoot + "/" + id;

const fileInfo = await db.getFileInfoByCellId(id);
args.imageUrl = baseUrl + fileInfo!.volumeviewerPath;
args.parentImageUrl = baseUrl + fileInfo!.fovVolumeviewerPath;
runApp();

// only now do we have all the data needed
}

if (params) {
if (params.mask) {
viewerSettings.maskAlpha = parseInt(params.mask, 10);
}
if (params.view) {
const mapping = {
"3D": ViewMode.threeD,
Z: ViewMode.xy,
Y: ViewMode.xz,
X: ViewMode.yz,
};
const allowedViews = Object.keys(mapping);
if (!allowedViews.includes(params.view)) {
params.view = "3D";
}
viewerSettings.viewMode = mapping[params.view];
}
if (params.ch) {
// ?ch=1,2
// ?luts=0,255,0,255
// ?colors=ff0000,00ff00
const initialChannelSettings: ViewerChannelSettings = {
groups: [{ name: "Channels", channels: [] }],
};
const ch = initialChannelSettings.groups[0].channels;

const channelsOn = params.ch.split(",").map((numstr) => parseInt(numstr, 10));
for (let i = 0; i < channelsOn.length; ++i) {
ch.push({ match: channelsOn[i], enabled: true });
}
// look for luts or color
if (params.luts) {
const luts = params.luts.split(",");
if (luts.length !== ch.length * 2) {
console.log("ILL-FORMED QUERYSTRING: luts must have a min/max for each ch");
}
for (let i = 0; i < ch.length; ++i) {
ch[i]["lut"] = [luts[i * 2], luts[i * 2 + 1]];
}
}
if (params.colors) {
const colors = params.colors.split(",");
if (colors.length !== ch.length) {
console.log("ILL-FORMED QUERYSTRING: if colors specified, must have a color for each ch");
}
for (let i = 0; i < ch.length; ++i) {
ch[i]["color"] = colors[i];
}
}
args.viewerChannelSettings = initialChannelSettings;
}
if (params.url) {
const imageUrls = tryDecodeURLList(params.url) ?? decodeURL(params.url);
const firstUrl = Array.isArray(imageUrls) ? imageUrls[0] : imageUrls;

args.cellId = "1";
args.imageUrl = imageUrls;
// this is invalid for zarr?
args.imageDownloadHref = firstUrl;
args.parentImageUrl = "";
args.parentImageDownloadHref = "";
// if json, then use the CFE settings for now.
// (See VIEWER_3D_SETTINGS)
// otherwise turn the first 3 channels on and group them
if (!firstUrl.endsWith("json") && !params.ch) {
args.viewerChannelSettings = {
groups: [
// first 3 channels on by default!
{
name: "Channels",
channels: [
{ match: [0, 1, 2], enabled: true },
{ match: "(.+)", enabled: false },
],
},
],
};
}
runApp();
} else if (params.file) {
// quick way to load a atlas.json from a special directory.
//
// ?file=relative-path-to-atlas-on-isilon
args.cellId = "1";
const baseUrl = "http://dev-aics-dtp-001.corp.alleninstitute.org/dan-data/";
args.imageUrl = baseUrl + params.file;
args.parentImageUrl = baseUrl + params.file;
args.parentImageDownloadHref = "";
args.imageDownloadHref = "";
runApp();
} else if (params.dataset && params.id) {
// ?dataset=aics_hipsc_v2020.1&id=232265
loadDataset(params.dataset, params.id);
} else {
runApp();
}
const routes = [
{
path: "/",
element: <LandingPage />,
errorElement: <ErrorPage />,
},
{
path: "viewer",
element: <AppWrapper />,
},
];

let router: Router;
if (WEBSITE3DCELLVIEWER_BUILD_ENVIRONMENT === "dev") {
router = createBrowserRouter(routes);
} else {
runApp();
// Production mode.
// TODO: Use createBrowserRouter when building to S3.
router = createHashRouter(routes);
}

function runApp() {
ReactDOM.render(
<ImageViewerApp {...args} appHeight="100vh" canvasMargin="0 0 0 0" viewerSettings={viewerSettings} />,
document.getElementById("cell-viewer")
);
}
ReactDOM.render(
<StyleProvider>
<RouterProvider router={router} />
</StyleProvider>,
document.getElementById("cell-viewer")
);
8 changes: 8 additions & 0 deletions src/aics-image-viewer/components/StyleProvider/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@
font-weight: 400;
}

a {
color: var(--color-text-link);
&:focus,
&:focus-visible {
text-decoration: underline;
}
}

& *::selection {
/**
* Override Ant + Less styling, since it uses a very light purple that's
Expand Down
9 changes: 6 additions & 3 deletions src/aics-image-viewer/components/Toolbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,12 @@ export default class Toolbar extends React.Component<ToolbarProps, ToolbarState>

checkSize = debounce((): void => {
const { leftRef, centerRef, rightRef, barRef } = this;
const leftRect = leftRef.current!.getBoundingClientRect();
const centerRect = centerRef.current!.getBoundingClientRect();
const rightRect = rightRef.current!.getBoundingClientRect();
if (!leftRef.current || !centerRef.current || !rightRef.current || !barRef.current) {
return;
}
const leftRect = leftRef.current.getBoundingClientRect();
const centerRect = centerRef.current.getBoundingClientRect();
const rightRect = rightRef.current.getBoundingClientRect();

// when calculating width required to leave scroll mode, add a bit of extra width to ensure that triggers
// for entering and leaving scroll mode never overlap (causing toolbar to rapidly switch when resizing)
Expand Down
Loading

0 comments on commit 5179a2a

Please sign in to comment.