Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Loading from the landing page, viewer page routing #212

Merged
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
cc58736
Merge branch 'main' into feature/loading-from-landing-page
ShrimpCryptid Apr 10, 2024
12da14d
feat: Added react-router-dom
ShrimpCryptid Apr 11, 2024
f3e48cd
feat: Loading viewer from button click on landing page
ShrimpCryptid Apr 11, 2024
9a4f105
fix: Load first three channels of the first dataset by default
ShrimpCryptid Apr 11, 2024
c874c55
fix: Fixed dev server not recognizing subpaths
ShrimpCryptid Apr 11, 2024
466d5b7
fix: Fixed crash caused by bad size check in Toolbar
ShrimpCryptid Apr 11, 2024
0f1f8e1
fix: Linting, video banner color
ShrimpCryptid Apr 11, 2024
b883707
refactor: Renamed `landing-page` folder to `website`
ShrimpCryptid Apr 11, 2024
8d18a12
Merge branch 'feature/landing-page-styling' into feature/loading-from…
ShrimpCryptid Apr 11, 2024
746f473
refactor: Moved URL handling code to its own utils file
ShrimpCryptid Apr 11, 2024
1a16c02
feat: Viewer retrieves URL parameters
ShrimpCryptid Apr 11, 2024
675591f
feat: Landing page redirects to viewer if params are received
ShrimpCryptid Apr 11, 2024
c68dff3
refactor: Code cleanup + linting
ShrimpCryptid Apr 11, 2024
b167967
refactor: Added default app props, typing
ShrimpCryptid Apr 11, 2024
e76894c
refactor: Removed unused defaults in url_utils
ShrimpCryptid Apr 11, 2024
9003180
feat: Add default error page
ShrimpCryptid Apr 11, 2024
5c4f77f
feat: Added hash routing on public deployments
ShrimpCryptid Apr 11, 2024
b72d82d
fix: Fixed URL search params when using hash-based routing
ShrimpCryptid Apr 11, 2024
33e4990
fix: Linting
ShrimpCryptid Apr 11, 2024
4bf2218
refactor: Code cleanup
ShrimpCryptid Apr 12, 2024
5b56362
feat: Improved error page messaging; link styling
ShrimpCryptid Apr 12, 2024
2172864
feat: Updated reporting link with icon, link styling when focused
ShrimpCryptid Apr 12, 2024
c15caf1
refactor: Code cleanup
ShrimpCryptid Apr 12, 2024
e2cb168
refactor: Added Open Sans font files
ShrimpCryptid Apr 12, 2024
b239dd2
refactor: Added open sans 500
ShrimpCryptid Apr 12, 2024
8cb2ad9
refactor: Switched SVG files to only use basic Latin alphabet to redu…
ShrimpCryptid Apr 12, 2024
3224aa9
refactor: Removed merriweather sans and overpass font files
ShrimpCryptid Apr 12, 2024
16ed045
Merge pull request #211 from allen-cell-animated/feature/landing-page…
ShrimpCryptid Apr 15, 2024
b6c00f0
refactor: Moved `website` directory out of `src`
ShrimpCryptid Apr 17, 2024
68a2c40
doc: Removed stray comment
ShrimpCryptid Apr 17, 2024
18e9fe3
feat: Added props to AppWrapper for viewer settings and args
ShrimpCryptid Apr 17, 2024
d8deb01
Merge branch 'main' into feature/embedded-fonts
ShrimpCryptid Apr 17, 2024
a1b9774
Merge pull request #214 from allen-cell-animated/feature/embedded-fonts
ShrimpCryptid Apr 17, 2024
e06bb53
refactor: Sort imports
ShrimpCryptid Apr 23, 2024
d0e8862
refactor: Replaced useMemo with useEffect, added missing router type
ShrimpCryptid Apr 23, 2024
8747501
Merge branch 'main' into feature/loading-from-landing-page
ShrimpCryptid Apr 23, 2024
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
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
274 changes: 29 additions & 245 deletions public/index.tsx
Original file line number Diff line number Diff line change
@@ -1,262 +1,46 @@
import React from "react";
import ReactDOM from "react-dom";
import { createBrowserRouter, createHashRouter, RouterProvider } from "react-router-dom";

import "antd/dist/antd.less";

// Components
import AppWrapper from "../src/website/components/AppWrapper";
import LandingPage from "../src/website/components/LandingPage";
import ErrorPage from "../src/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";
Comment on lines -10 to -12
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the parsing logic below was moved to src/website/utils/url_utils.tsx.


// 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 = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleted

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good to delete yes, but wondering how we can get test data like this into the viewer during dev. These settings are basically how we want the variance dataset to be presented in the viewer.

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 {
Copy link
Contributor Author

@ShrimpCryptid ShrimpCryptid Apr 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleted; using useSearchParams API now instead

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],
};
Comment on lines -101 to -122
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleted


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;
ShrimpCryptid marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code was throwing an error due to the various refs being undefined on startup.

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
Loading