diff --git a/src/GlobalStates/PlotStore.ts b/src/GlobalStates/PlotStore.ts index 5f0e34f7..39219a62 100644 --- a/src/GlobalStates/PlotStore.ts +++ b/src/GlobalStates/PlotStore.ts @@ -64,6 +64,7 @@ type PlotState ={ maskValue: number; cameraPosition: THREE.Vector3; disablePointScale: boolean; + camera: THREE.Camera | undefined; setQuality: (quality: number) => void; setTimeScale: (timeScale : number) =>void; @@ -119,8 +120,10 @@ type PlotState ={ setUseOrtho: (useOrtho: boolean) => void; setFillValue: (fillValue: number | undefined) => void; setCameraPosition: (cameraPosition: THREE.Vector3) => void; + } + export const usePlotStore = create((set, get) => ({ plotType: "volume", pointSize: 5, @@ -185,6 +188,7 @@ export const usePlotStore = create((set, get) => ({ borderWidth: 0.05, cameraPosition: new THREE.Vector3(0, 0, 5), disablePointScale: false, + camera: undefined, setVTransferRange: (vTransferRange) => set({ vTransferRange }), setVTransferScale: (vTransferScale) => set({ vTransferScale }), @@ -242,4 +246,5 @@ export const usePlotStore = create((set, get) => ({ setUseOrtho: (useOrtho) => set({ useOrtho }), setFillValue: (fillValue) => set({ fillValue }), setCameraPosition: (cameraPosition) => set({ cameraPosition }), + })) \ No newline at end of file diff --git a/src/GlobalStates/ZarrStore.ts b/src/GlobalStates/ZarrStore.ts index 00755ee3..423796df 100644 --- a/src/GlobalStates/ZarrStore.ts +++ b/src/GlobalStates/ZarrStore.ts @@ -23,6 +23,7 @@ type ZarrState = { fetchOptions: FetchStoreOptions | null; abortController: AbortController | null; fetchKey: number; + blobKey: string | undefined; // The key for the stored File blob for a local NC setZSlice: (zSlice: [number , number | null]) => void; setYSlice: (ySlice: [number , number | null]) => void; @@ -63,6 +64,7 @@ export const useZarrStore = create((set, get) => ({ fetchOptions: null, abortController: null, fetchKey: 0, + blobKey: undefined, setZSlice: (zSlice) => set({ zSlice }), setYSlice: (ySlice) => set({ ySlice }), diff --git a/src/components/StoreInitializer.tsx b/src/components/StoreInitializer.tsx index 199308a4..3cf178fb 100644 --- a/src/components/StoreInitializer.tsx +++ b/src/components/StoreInitializer.tsx @@ -3,8 +3,12 @@ import { useEffect, Suspense } from "react"; import { useSearchParams } from "next/navigation"; import { useGlobalStore } from "@/GlobalStates/GlobalStore"; import { useZarrStore } from '@/GlobalStates/ZarrStore'; +import { usePlotStore } from "@/GlobalStates/PlotStore"; import { useShallow } from 'zustand/shallow'; import { isRemoteStore } from '@/utils/isRemoteStore'; +import { loadNetCDF } from "@/utils/loadNetCDF"; +import { loadFile } from "@/utils/IndexDB"; +import { LoadLocalZarr } from "./ui/MainPanel/LocalZarr"; function StoreInitializerInner() { const searchParams = useSearchParams(); @@ -17,11 +21,40 @@ function StoreInitializerInner() { useEffect(() => { const store = searchParams.get("store"); + let data = searchParams.get("data") + if (data){ + try{ + const fullObj = JSON.parse(data); + if (fullObj.zarrState.blobKey){ // If NC local must load file beforehand + const blobKey = fullObj.zarrState.blobKey + const isNC = fullObj.zarrState.useNC + loadFile(blobKey).then(cache =>{ + if (!isNC){ + LoadLocalZarr(cache?.blob as File[]) + } else { + //@ts-ignore cache is what we want + const file = cache.blob as File + loadNetCDF(file, file.name).then(() => { + useZarrStore.setState(fullObj.zarrState); + useGlobalStore.setState(fullObj.globalState); + usePlotStore.setState(fullObj.plotState); + }) + } + }) + } else { + useZarrStore.setState(fullObj.zarrState) + useGlobalStore.setState(fullObj.globalState) + usePlotStore.setState(fullObj.plotState) + } + } catch { + console.error('Something Failed :/') + } + } if (!store) { setStoreFromURL(false); return; } - + const isNC = searchParams.get("format") === "nc"; setUseNC(isNC); setFetchNC(isNC); diff --git a/src/components/plots/AxisLines.tsx b/src/components/plots/AxisLines.tsx index b21196bb..86d9a67d 100644 --- a/src/components/plots/AxisLines.tsx +++ b/src/components/plots/AxisLines.tsx @@ -114,9 +114,9 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli const zDimScale = zResolution/(zResolution-1) const zValDelta = 1/(zResolution-1) - const xTitleOffset = useMemo(() => (dimNames[shapeLength - 1].length * AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR / 2 + 0.1) * globalScale, [dimNames, globalScale]); - const yTitleOffset = useMemo(() => (dimNames[shapeLength - 2].length * AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR / 2 + 0.1) * globalScale, [dimNames, globalScale]); - const zTitleOffset = useMemo(() => (dimNames[shapeLength - 3].length * AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR / 2 + 0.1) * globalScale, [dimNames, globalScale]); + const xTitleOffset = useMemo(() => (dimNames[shapeLength - 1]?.length * AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR / 2 + 0.1) * globalScale, [dimNames, globalScale]); + const yTitleOffset = useMemo(() => (dimNames[shapeLength - 2]?.length * AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR / 2 + 0.1) * globalScale, [dimNames, globalScale]); + const zTitleOffset = useMemo(() => (dimNames[shapeLength - 3]?.length * AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR / 2 + 0.1) * globalScale, [dimNames, globalScale]); return ( diff --git a/src/components/plots/Plot.tsx b/src/components/plots/Plot.tsx index d54e6ab3..344d8ef9 100644 --- a/src/components/plots/Plot.tsx +++ b/src/components/plots/Plot.tsx @@ -39,6 +39,7 @@ const Orbiter = ({isFlat} : {isFlat : boolean}) =>{ const hasMounted = useRef(false); const cameraRef = useRef(null) const {set, camera, size} = useThree() + // Reset Camera Position and Target useEffect(()=>{ if (!hasMounted.current) { @@ -81,6 +82,7 @@ const Orbiter = ({isFlat} : {isFlat : boolean}) =>{ } },[resetCamera, isFlat]) + // ---- Switch from Perspective to Orthographic ---- // useEffect(()=>{ if (hasMounted.current){ let newCamera; @@ -117,6 +119,7 @@ const Orbiter = ({isFlat} : {isFlat : boolean}) =>{ } },[useOrtho]) + // ---- Move camera to position ---- // useEffect(()=>{ const cam = cameraRef.current const controls = orbitRef.current @@ -136,6 +139,14 @@ const Orbiter = ({isFlat} : {isFlat : boolean}) =>{ } },[cameraPosition]) + // ---- Camera Ref for state saves ---- // + useEffect(()=>{ + usePlotStore.setState({camera}) + return () => { + usePlotStore.setState({camera: undefined}) + } + },[camera]) + return ( {})} setOpenVariables={onOpenDescription} - setInitStore={setInitStore} /> )} diff --git a/src/components/ui/MainPanel/LocalNetCDF.tsx b/src/components/ui/MainPanel/LocalNetCDF.tsx index fe16b3cc..fa9ee7d9 100644 --- a/src/components/ui/MainPanel/LocalNetCDF.tsx +++ b/src/components/ui/MainPanel/LocalNetCDF.tsx @@ -3,13 +3,14 @@ import React, {ChangeEvent, useState} from 'react' import { Input } from '../input' import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { loadNetCDF, NETCDF_EXT_REGEX } from '@/utils/loadNetCDF'; - +import { saveFile } from '@/utils/IndexDB'; import { Alert, AlertDescription, AlertTitle, } from '@/components/ui/alert'; import { isMobile } from '../MobileUIHider'; +import { useZarrStore } from '@/GlobalStates/ZarrStore'; interface LocalNCType { setOpenVariables: (open: boolean) => void; @@ -20,23 +21,26 @@ const LocalNetCDF = ({ setOpenVariables}:LocalNCType) => { // const {ncModule} = useZarrStore.getState() const [ncError, setError] = useState(null); - const handleFileSelect = async (event: ChangeEvent) => { - setError(null); - const files = event.target.files; - if (!files || files.length === 0) { setStatus(null); return; } - const file = files[0]; - if (!NETCDF_EXT_REGEX.test(file.name)) { - setError('Please select a valid NetCDF (.nc, .netcdf, .nc3, .nc4) file.'); - return; - } - try { - await loadNetCDF(file, file.name); - setOpenVariables(true) - - } catch (e) { - setError(`Failed to load file: ${e instanceof Error ? e.message : String(e)}`); - } - }; + const handleFileSelect = async (event: ChangeEvent) => { + setError(null); + const files = event.target.files; + if (!files || files.length === 0) { setStatus(null); return; } + const file = files[0]; + if (!NETCDF_EXT_REGEX.test(file.name)) { + setError('Please select a valid NetCDF (.nc, .netcdf, .nc3, .nc4) file.'); + return; + } + try { + await loadNetCDF(file, file.name); + const blobKey = `local_${file.name}` + await saveFile(file, blobKey) + useZarrStore.setState({blobKey}) + console.log('Set Key?') + setOpenVariables(true) + } catch (e) { + setError(`Failed to load file: ${e instanceof Error ? e.message : String(e)}`); + } + }; return (
diff --git a/src/components/ui/MainPanel/LocalZarr.tsx b/src/components/ui/MainPanel/LocalZarr.tsx index f5404046..216ecbd9 100644 --- a/src/components/ui/MainPanel/LocalZarr.tsx +++ b/src/components/ui/MainPanel/LocalZarr.tsx @@ -5,25 +5,16 @@ import { useZarrStore } from '@/GlobalStates/ZarrStore'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { Input } from '../input'; import ZarrParser from '@/components/zarr/ZarrParser'; +import { saveFile } from '@/utils/IndexDB'; interface LocalZarrType { setShowLocal: React.Dispatch>; setOpenVariables: (open: boolean) => void; - setInitStore: (store: string) => void; } -const LocalZarr = ({setShowLocal, setOpenVariables, setInitStore}:LocalZarrType) => { - const setCurrentStore = useZarrStore(state => state.setCurrentStore) - const {setStatus} = useGlobalStore.getState() - const handleFileSelect = async (event: ChangeEvent) => { - const files = event.target.files; - if (!files || files.length === 0) { - setStatus(null) - return; - } - // Create a Map to hold the Zarr store data +export async function LoadLocalZarr(files:File[]){ + // Create a Map to hold the Zarr store data const fileMap = new Map(); - // The base directory name will be the first part of the relative path const baseDir = files[0].webkitRelativePath.split('/')[0]; @@ -35,7 +26,6 @@ const LocalZarr = ({setShowLocal, setOpenVariables, setInitStore}:LocalZarrType) fileMap.set('/' + relativePath, file); // Zarrita looks for a leading slash before variables. Need to add it back } } - // Create a custom zarrita store from the Map const customStore: zarr.AsyncReadable = { async get(key: string): Promise { @@ -48,37 +38,52 @@ const LocalZarr = ({setShowLocal, setOpenVariables, setInitStore}:LocalZarrType) // Open the Zarr store using the custom store let store = await zarr.withMaybeConsolidatedMetadata(customStore); if (!('contents' in store)){ + console.log("Trying to parse?") // Metadata is missing. We will need to parse variables here. store = await ZarrParser(files, customStore) } const gs = await zarr.open(store, {kind: 'group'}); - setCurrentStore(gs); - setShowLocal(false); - setOpenVariables(true); - setInitStore(`local_${baseDir}`) - setStatus(null) - useZarrStore.setState({ useNC: false}) + const blobKey = `local_${baseDir}` + useGlobalStore.setState({initStore:blobKey}) + useZarrStore.setState({ useNC: false, blobKey, currentStore:gs}) } catch (error) { - setStatus(null) + if (error instanceof Error) { console.log(`Error opening Zarr store: ${error.message}`); } else { console.log('An unknown error occurred when opening the Zarr store.'); } } - }; - return ( -
- -
- ) +} + +const LocalZarr = ({setShowLocal, setOpenVariables}:LocalZarrType) => { + const {setStatus} = useGlobalStore.getState() + const handleFileSelect = async (event: ChangeEvent) => { + const files = event.target.files; + if (!files || files.length === 0) { + setStatus(null) + return; + } + const baseDir = files[0].webkitRelativePath.split('/')[0]; + LoadLocalZarr(Array.from(files)).then(()=>{ + saveFile(Array.from(files), `local_${baseDir}`) + setShowLocal(false); + setOpenVariables(true); + setStatus(null) + }) + }; + return ( +
+ +
+ ) } export default LocalZarr diff --git a/src/components/ui/NavBar/Navbar.tsx b/src/components/ui/NavBar/Navbar.tsx index aa8ffb23..fe1efee4 100644 --- a/src/components/ui/NavBar/Navbar.tsx +++ b/src/components/ui/NavBar/Navbar.tsx @@ -19,6 +19,7 @@ import { import { cn } from "@/lib/utils"; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; +import { ParameterExport } from "./ParameterExport"; import { Orthographic, Perspective } from "../Elements/Icons"; import PerformanceMode from "./PerformanceMode"; @@ -108,6 +109,7 @@ const Navbar = React.memo(function Navbar() { +
diff --git a/src/components/ui/NavBar/ParameterExport.tsx b/src/components/ui/NavBar/ParameterExport.tsx new file mode 100644 index 00000000..446206fc --- /dev/null +++ b/src/components/ui/NavBar/ParameterExport.tsx @@ -0,0 +1,156 @@ +import React, { useState } from 'react' +import { useGlobalStore } from '@/GlobalStates/GlobalStore' +import { usePlotStore } from '@/GlobalStates/PlotStore' +import { useZarrStore } from '@/GlobalStates/ZarrStore' +import { BiExport } from "react-icons/bi"; +import { FiCopy } from "react-icons/fi"; +import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover" +import { Button } from '../button'; + +function pick(obj: Record, keys: string[]) { + return Object.fromEntries( + keys.map((k) => [k, obj[k]]) + ) +} +const globalValues = [ + 'initStore', + 'storeFromURL', + 'variable', +] + +const plotValues = [ + 'plotType', + 'pointSize', + 'scalePoints', + 'scaleIntensity', + 'timeScale', + 'valueRange', + 'xRange', + 'yRange', + 'zRange', + 'showPoints', + 'linePointSize', + 'lineWidth', + 'lineColor', + 'pointColor', + 'useLineColor', + 'lineResolution', + 'cOffset', + 'cScale', + 'useFragOpt', + 'useCustomColor', + 'useCustomPointColor', + 'transparency', + 'nanTransparency', + 'nanColor', + 'showBorders', + 'borderColor', + 'lonExtent', + 'latExtent', + 'originalExtent', + 'lonResolution', + 'latResolution', + 'colorIdx', + 'vTransferRange', + 'vTransferScale', + 'sphereResolution', + 'displacement', + 'displaceSurface', + 'offsetNegatives', + 'zSlice', + 'ySlice', + 'xSlice', + 'interpPixels', + 'useOrtho', + 'rotateFlat', + 'fillValue', + 'coarsen', + 'kernel', + 'useBorderTexture', + 'maskValue', + 'borderWidth', + 'cameraPosition', + 'disablePointScale', +] + +const zarrValues = [ + 'zSlice', + 'ySlice', + 'xSlice', + 'compress', + 'useNC', // This one is more static and so toggling switch doesn't break all other logic + 'fetchNC', + 'coarsen', + 'kernelSize', + 'kernelDepth', + 'icechunkOptions', + 'fetchOptions', + 'fetchKey', + 'blobKey' +] + + +export const ParameterExport = () => { + const [copied, setCopied] = useState(false); + + function generateURL(){ + const {camera} = usePlotStore.getState() + usePlotStore.setState({cameraPosition:camera?.position}) // Set Camera position first to copy visual state + const fullObj = { + globalState: pick(useGlobalStore.getState(), globalValues), + plotState: pick(usePlotStore.getState(), plotValues), + zarrState: pick(useZarrStore.getState(), zarrValues), + } + const jString = JSON.stringify(fullObj, (_, v) => typeof v === 'bigint' ? v.toString() : v) + const params = `https://browzarr.io/latest/?data=${encodeURIComponent(jString)}` + return params + } + + const copyToClipboard = async () => { + const url = generateURL() + await navigator.clipboard.writeText(url); + setCopied(true); + setTimeout(() => setCopied(false), 1500); //Use for a pop-up that fades away + }; + + return ( + + + + + +
+ +
+ Copied! +
+
+
+
+ ) +} + + diff --git a/src/components/zarr/ZarrParser.ts b/src/components/zarr/ZarrParser.ts index 74b47475..a8cdef0d 100644 --- a/src/components/zarr/ZarrParser.ts +++ b/src/components/zarr/ZarrParser.ts @@ -11,6 +11,7 @@ function is_v3(meta: any) { return "zarr_format" in meta && meta.zarr_format === 3; } async function ZarrParser(files: any, store: any){ + console.log(store) const fileCount = files.length; const vars = [] const metadata: { [key: string]: any } = {} @@ -26,6 +27,7 @@ async function ZarrParser(files: any, store: any){ } for (const variable of vars){ const decoded = await store.get(variable) + console.log(decoded) metadata[variable.slice(1)] = jsonDecodeObject(decoded) } const v2_meta = {metadata, zarr_consolidated_format: 1} diff --git a/src/utils/IndexDB.ts b/src/utils/IndexDB.ts new file mode 100644 index 00000000..e48ad0ca --- /dev/null +++ b/src/utils/IndexDB.ts @@ -0,0 +1,32 @@ +const DB_NAME = 'browzarr-files'; +const STORE = 'blobs'; + + +export function openDB(): Promise { + return new Promise((res, rej) => { + const req = indexedDB.open(DB_NAME, 1); + req.onupgradeneeded = () => req.result.createObjectStore(STORE); + req.onsuccess = () => res(req.result); + req.onerror = () => rej(req.error); + }); +} + +export async function saveFile(blob: any, key: string): Promise { + const db = await openDB(); + return new Promise((res, rej) => { + const tx = db.transaction(STORE, 'readwrite'); + tx.objectStore(STORE).put({ blob, key }, key); + tx.oncomplete = () => res(key); + tx.onerror = () => rej(tx.error); + }); +} + +export async function loadFile(key: string): Promise<{ blob: any; name: string } | null> { + const db = await openDB(); + return new Promise((res, rej) => { + const tx = db.transaction(STORE, 'readonly'); + const req = tx.objectStore(STORE).get(key); + req.onsuccess = () => res(req.result ?? null); + req.onerror = () => rej(req.error); + }); +} \ No newline at end of file