diff --git a/package.json b/package.json index 565e69fd..0012a882 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "lucide-react": "^0.562.0", "next": "^16.2.7", "next-themes": "^0.4.6", + "proj4": "^2.20.9", "radix-ui": "1.4.3", "react": "^19.0.0", "react-day-picker": "^9.14.0", diff --git a/src/GlobalStates/GlobalStore.ts b/src/GlobalStates/GlobalStore.ts index d80a5e65..0e8bcd0c 100644 --- a/src/GlobalStates/GlobalStore.ts +++ b/src/GlobalStates/GlobalStore.ts @@ -21,6 +21,7 @@ type StoreState = { shape: THREE.Vector3; valueScales: { maxVal: number; minVal: number }; colormap: THREE.DataTexture; + remapTexture: THREE.DataTexture | undefined; colormapName: string; flipColormap: boolean; timeSeries: Record>; @@ -31,6 +32,9 @@ type StoreState = { dimNames: string[]; dimUnits: string[]; dimCoords: Record; + axisDimArrays: number[][]; + axisDimNames: string[]; + axisDimUnits: string[]; plotDim: number; flipY:boolean; initStore:string; @@ -69,6 +73,9 @@ type StoreState = { setDimUnits: (dimUnits: string[]) => void; setDimCoords: (dimCoords?: Record) => void; updateDimCoords: (newDims: Record) => void; + setAxisDimArrays: (axisDimArrays: number[][]) => void; + setAxisDimNames: (axisDimNames: string[]) => void; + setAxisDimUnits: (axisDimUnits: string[]) => void; setPlotDim: (plotDim: number) => void; setFlipY: (flipY:boolean) => void; setInitStore: (initStore:string) => void; @@ -96,6 +103,7 @@ export const useGlobalStore = create((set, get) => ({ shape: new THREE.Vector3(2, 2, 2), valueScales: { maxVal: 1, minVal: -1 }, colormap: GetColorMapTexture(), + remapTexture: undefined, colormapName: "Spectral", flipColormap: false, timeSeries: {}, @@ -105,6 +113,9 @@ export const useGlobalStore = create((set, get) => ({ dimArrays: [[0], [0], [0]], dimNames: ["Default"], dimUnits: ["Default"], + axisDimArrays: [[0], [0], [0]], + axisDimNames: ["Default"], + axisDimUnits: ["Default"], dimCoords: {}, plotDim: 0, flipY: false, @@ -164,6 +175,9 @@ export const useGlobalStore = create((set, get) => ({ set({ dimCoords: limitedDimCoords }); }, + setAxisDimArrays: (axisDimArrays) => set({ axisDimArrays }), + setAxisDimNames: (axisDimNames) => set({ axisDimNames }), + setAxisDimUnits: (axisDimUnits) => set({ axisDimUnits }), setPlotDim: (plotDim) => set({ plotDim }), setFlipY: (flipY) => set({ flipY }), setInitStore: (initStore) => set({ initStore }), diff --git a/src/GlobalStates/PlotStore.ts b/src/GlobalStates/PlotStore.ts index 39219a62..def20607 100644 --- a/src/GlobalStates/PlotStore.ts +++ b/src/GlobalStates/PlotStore.ts @@ -65,6 +65,8 @@ type PlotState ={ cameraPosition: THREE.Vector3; disablePointScale: boolean; camera: THREE.Camera | undefined; + nativeCRS: string | undefined; + destCRS: string | undefined; setQuality: (quality: number) => void; setTimeScale: (timeScale : number) =>void; @@ -189,6 +191,8 @@ export const usePlotStore = create((set, get) => ({ cameraPosition: new THREE.Vector3(0, 0, 5), disablePointScale: false, camera: undefined, + nativeCRS: undefined, + destCRS: undefined, setVTransferRange: (vTransferRange) => set({ vTransferRange }), setVTransferScale: (vTransferScale) => set({ vTransferScale }), diff --git a/src/app/globals.css b/src/app/globals.css index 1c4604b8..f0b90a9e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -162,6 +162,16 @@ .no-spinner { -moz-appearance: textfield; /* Firefox */ } + + .no-scrollbar { + -ms-overflow-style: none; /* IE + Edge */ + scrollbar-width: none; /* Firefox */ + } + + .no-scrollbar::-webkit-scrollbar { + display: none; /* Chrome, Safari */ + } + } /* Height fix for mobile browser UI */ diff --git a/src/components/LandingHome.tsx b/src/components/LandingHome.tsx index f5ba7108..384cf981 100644 --- a/src/components/LandingHome.tsx +++ b/src/components/LandingHome.tsx @@ -12,6 +12,7 @@ import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { useZarrStore } from '@/GlobalStates/ZarrStore'; import { useShallow } from 'zustand/shallow'; import { loadNetCDF, NETCDF_EXT_REGEX } from '@/utils/loadNetCDF'; +import { usePlotStore } from '@/GlobalStates/PlotStore'; async function sendPing() { const url = "https://www.bgc-jena.mpg.de/~jpoehls/browzarr/visitor_logger.php"; @@ -86,8 +87,11 @@ export function LandingHome() { ); setCurrentStore(newStore); // Clear after use - useZarrStore.getState().setIcechunkOptions(null); - useZarrStore.getState().setFetchOptions(null); + const {remapTexture} = useGlobalStore.getState(); + if (remapTexture) remapTexture.dispose(); + useZarrStore.setState({icechunkOptions: null, fetchOptions:null}); + useGlobalStore.setState({remapTexture: undefined }); + usePlotStore.setState({nativeCRS:undefined, destCRS:undefined}); }, [initStore, fetchKey, setCurrentStore, setUseNC, setZSlice, setYSlice, setXSlice, storeFromURL, setOpenVariables, setStoreFromURL]); useEffect(() => { diff --git a/src/components/plots/AxisLines.tsx b/src/components/plots/AxisLines.tsx index 5c997051..37894681 100644 --- a/src/components/plots/AxisLines.tsx +++ b/src/components/plots/AxisLines.tsx @@ -33,13 +33,13 @@ const AXIS_CONSTANTS = { }; const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, flipDown: boolean}) =>{ - const {dimArrays, dimNames, dimUnits, dataShape, revY, is4D} = useGlobalStore(useShallow(state => ({ - dimArrays: state.dimArrays, - dimNames: state.dimNames, - dimUnits: state.dimUnits, + const {axisDimArrays, axisDimNames, axisDimUnits, dataShape, revY, shape} = useGlobalStore(useShallow(state => ({ + axisDimArrays: state.axisDimArrays, + axisDimNames: state.axisDimNames, + axisDimUnits: state.axisDimUnits, dataShape: state.dataShape, revY: state.flipY, - is4D: state.is4D + shape: state.shape }))) const {xRange, yRange, zRange, plotType, timeScale, animProg, zSlice, ySlice, xSlice, coarsen} = usePlotStore(useShallow(state => ({ @@ -55,19 +55,18 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli }))) const {xIdx, yIdx, zIdx} = useAxisIndices() - const dimSlices = useMemo(()=> { let slices = [ - dimArrays[zIdx]?.slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined) ?? [], - revY ? dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined).reverse() ?? [] : dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined) ?? [], - dimArrays[xIdx]?.slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined) ?? [], + axisDimArrays[zIdx]?.slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined) ?? [], + revY ? axisDimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined).reverse() ?? [] : axisDimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined) ?? [], + axisDimArrays[xIdx]?.slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined) ?? [], ] if (coarsen) { const {kernelDepth, kernelSize} = useZarrStore.getState() slices = slices.map((val, idx) => coarsenFlatArray(val, (idx === 0 ? kernelDepth : kernelSize))) } return slices - },[revY, dimArrays, zSlice, ySlice, xSlice, coarsen, xIdx, yIdx, zIdx]) + },[revY, axisDimArrays, zSlice, ySlice, xSlice, coarsen, xIdx, yIdx, zIdx]) const dimLengths = dimSlices.map(val => val.length) @@ -75,13 +74,12 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli const [yResolution, setYResolution] = useState(AXIS_CONSTANTS.INITIAL_RESOLUTION) const [zResolution, setZResolution] = useState(AXIS_CONSTANTS.INITIAL_RESOLUTION) - const isPC = useMemo(()=>plotType == 'point-cloud',[plotType]) const globalScale = isPC ? dataShape[2]/AXIS_CONSTANTS.PC_GLOBAL_SCALE_DIVISOR : 1 - const depthRatio = useMemo(()=>dataShape[0]/dataShape[2]*timeScale,[dataShape, timeScale]); - const shapeRatio = useMemo(()=>dataShape[1]/dataShape[2], [dataShape]) - const timeRatio = Math.max(dataShape[0]/dataShape[2], 2); + const depthRatio = useMemo(()=>shape.z/shape.x*timeScale,[shape, timeScale]); + const shapeRatio = useMemo(()=>shape.y/shape.x, [shape]) + const timeRatio = Math.max(shape.z/shape.x, 2); const secondaryColor = useCSSVariable('--text-plot') //replace with needed variable const colorHex = useMemo(()=>{ @@ -116,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[xIdx]?.length || 0) * AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR / 2 + 0.1) * globalScale, [dimNames, globalScale, xIdx]); - const yTitleOffset = useMemo(() => ((dimNames[yIdx]?.length || 0) * AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR / 2 + 0.1) * globalScale, [dimNames, globalScale, yIdx]); - const zTitleOffset = useMemo(() => ((dimNames[zIdx]?.length || 0) * AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR / 2 + 0.1) * globalScale, [dimNames, globalScale, zIdx]); + const xTitleOffset = useMemo(() => ((axisDimNames[xIdx]?.length || 0) * AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR / 2 + 0.1) * globalScale, [axisDimNames, globalScale, xIdx]); + const yTitleOffset = useMemo(() => ((axisDimNames[yIdx]?.length || 0) * AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR / 2 + 0.1) * globalScale, [axisDimNames, globalScale, yIdx]); + const zTitleOffset = useMemo(() => ((axisDimNames[zIdx]?.length || 0) * AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR / 2 + 0.1) * globalScale, [axisDimNames, globalScale, zIdx]); return ( @@ -142,7 +140,7 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli material-depthTest={false} rotation={[-Math.PI/2, 0, flipX ? Math.PI : 0]} position={[0, 0, flipX ? -AXIS_CONSTANTS.TICK_LENGTH_FACTOR*globalScale : AXIS_CONSTANTS.TICK_LENGTH_FACTOR*globalScale]} - >{parseLoc(dimSlices[2]?.[Math.floor((dimLengths[2]-1)*idx*xValDelta)] || 0,dimUnits[xIdx])} + >{parseLoc(dimSlices[2]?.[Math.floor((dimLengths[2]-1)*idx*xValDelta)] || 0,axisDimUnits[xIdx])} ))} @@ -153,7 +151,7 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli fontSize={AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR*globalScale} color={colorHex} material-depthTest={false} - >{dimNames[xIdx]} + >{axisDimNames[xIdx]} {xResolution < AXIS_CONSTANTS.MAX_RESOLUTION && {parseLoc(dimSlices[0]?.[(Math.floor((dimLengths[0]-1)*idx*zValDelta)+Math.floor(dimLengths[0]*animProg))%dimLengths[0]] || 0,dimUnits[zIdx])} + >{parseLoc(dimSlices[0]?.[(Math.floor((dimLengths[0]-1)*idx*zValDelta)+Math.floor(dimLengths[0]*animProg))%dimLengths[0]] || 0,axisDimUnits[zIdx])} ))} @@ -217,7 +215,7 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli fontSize={AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR*globalScale} color={colorHex} material-depthTest={false} - >{dimNames[zIdx]} + >{axisDimNames[zIdx]} {zResolution < AXIS_CONSTANTS.MAX_RESOLUTION && @@ -272,7 +270,7 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli material-depthTest={false} rotation={[0, flipX ? Math.PI : 0, 0]} position={[flipY ? -0.07*globalScale : 0.07*globalScale, 0, 0]} - >{parseLoc(dimSlices[1]?.[Math.floor((dimLengths[1]-1)*idx*yValDelta)] || 0,dimUnits[yIdx])} + >{parseLoc(dimSlices[1]?.[Math.floor((dimLengths[1]-1)*idx*yValDelta)] || 0,axisDimUnits[yIdx])} ))} @@ -285,7 +283,7 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli material-depthTest={false} rotation={[0, 0, Math.PI / 2]} > - {dimNames[yIdx]} + {axisDimNames[yIdx]} @@ -344,10 +342,10 @@ const FLAT_AXIS_CONSTANTS = { }; const FlatAxis = () =>{ - const {dimArrays, dimNames, dimUnits, flipY} = useGlobalStore(useShallow(state => ({ - dimArrays: state.dimArrays, - dimNames: state.dimNames, - dimUnits: state.dimUnits, + const {axisDimArrays, axisDimNames, axisDimUnits, flipY} = useGlobalStore(useShallow(state => ({ + axisDimArrays: state.axisDimArrays, + axisDimNames: state.axisDimNames, + axisDimUnits: state.axisDimUnits, flipY: state.flipY, }))) @@ -367,23 +365,23 @@ const FlatAxis = () =>{ axis: state.axis }))) - const originallyFlat = dimArrays.length == 2; + const originallyFlat = axisDimArrays.length == 2; const slices = originallyFlat ? [ySlice, xSlice] : [zSlice, ySlice, xSlice] const {xIdx, yIdx, zIdx} = useAxisIndices() const dimSlices = useMemo(()=> { return originallyFlat ? [ - flipY ? dimArrays[yIdx]?.slice().reverse() ?? [] : dimArrays[yIdx] ?? [], - dimArrays[xIdx] ?? [] + flipY ? axisDimArrays[yIdx]?.slice().reverse() ?? [] : axisDimArrays[yIdx] ?? [], + axisDimArrays[xIdx] ?? [] ] : [ - dimArrays[zIdx]?.slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined) ?? [], - flipY ? dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined).reverse() ?? [] : dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined) ?? [], - dimArrays[xIdx]?.slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined) ?? [], + axisDimArrays[zIdx]?.slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined) ?? [], + flipY ? axisDimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined).reverse() ?? [] : axisDimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined) ?? [], + axisDimArrays[xIdx]?.slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined) ?? [], ] - },[dimArrays, flipY, originallyFlat, xSlice, ySlice, zSlice, xIdx, yIdx, zIdx]) + },[axisDimArrays, flipY, originallyFlat, xSlice, ySlice, zSlice, xIdx, yIdx, zIdx]) const dimLengths = useMemo(()=>{ if (analysisMode && !originallyFlat){ @@ -406,13 +404,13 @@ const FlatAxis = () =>{ if (originallyFlat) { return { axisArrays: dimSlices, - axisUnits: [dimUnits[yIdx], dimUnits[xIdx]], - axisNames: [dimNames[yIdx], dimNames[xIdx]], + axisUnits: [axisDimUnits[yIdx], axisDimUnits[xIdx]], + axisNames: [axisDimNames[yIdx], axisDimNames[xIdx]], }; } - const baseUnits = [dimUnits[zIdx], dimUnits[yIdx], dimUnits[xIdx]]; - const baseNames = [dimNames[zIdx], dimNames[yIdx], dimNames[xIdx]]; + const baseUnits = [axisDimUnits[zIdx], axisDimUnits[yIdx], axisDimUnits[xIdx]]; + const baseNames = [axisDimNames[zIdx], axisDimNames[yIdx], axisDimNames[xIdx]]; if (analysisMode) { return { @@ -427,9 +425,8 @@ const FlatAxis = () =>{ axisNames: baseNames, }; } - }, [analysisMode, dimArrays, dimUnits, dimNames, dimSlices, originallyFlat, axis, xIdx, yIdx, zIdx]); + }, [analysisMode, axisDimArrays, axisDimUnits, axisDimNames, dimSlices, originallyFlat, axis, xIdx, yIdx, zIdx]); - const shapeRatio = useMemo(()=>{ if(analysisMode && axis == 2){ return dimLengths[heightIdx]/dimLengths[widthIdx] diff --git a/src/components/plots/DataCube.tsx b/src/components/plots/DataCube.tsx index 035f7122..efb10aec 100644 --- a/src/components/plots/DataCube.tsx +++ b/src/components/plots/DataCube.tsx @@ -17,11 +17,12 @@ interface DataCubeProps { export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { const volTexture = usePaddedTextures(propVolTexture); - const {shape, colormap, flipY, textureArrayDepths} = useGlobalStore(useShallow(state=>({ + const {shape, colormap, flipY, textureArrayDepths, remapTexture} = useGlobalStore(useShallow(state=>({ shape:state.shape, colormap:state.colormap, flipY:state.flipY, - textureArrayDepths: state.textureArrayDepths + textureArrayDepths: state.textureArrayDepths, + remapTexture: state.remapTexture }))) //We have to useShallow when returning an object instead of a state. I don't fully know the logic yet const { valueRange, xRange, yRange, zRange, quality, useOrtho, @@ -54,6 +55,7 @@ export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { maskValue: {value: maskValue }, textureDepths: {value: new THREE.Vector3(textureArrayDepths[2], textureArrayDepths[1], textureArrayDepths[0])}, cmap:{value: colormap}, + remapTexture: { value: remapTexture}, cOffset:{value: cOffset}, cScale: {value: cScale}, threshold: {value: new THREE.Vector2(valueRange[0],valueRange[1])}, @@ -73,7 +75,8 @@ export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { }, defines: { USE_VORIGIN: 1, - USE_VDIRECTION: 1 + USE_VDIRECTION: 1, + REPROJECT: remapTexture ? true : false }, vertexShader: useOrtho ? orthoVertex : vertexShader, fragmentShader: useFragOpt ? fragOpt : fragmentShader, @@ -81,7 +84,8 @@ export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { blending: THREE.NormalBlending, depthWrite: false, side: useOrtho ? THREE.FrontSide : THREE.BackSide, - }),[useFragOpt, useOrtho, volTexture]); + }),[useFragOpt, useOrtho, volTexture, remapTexture]); + const geometry = useMemo(() => new THREE.BoxGeometry(shape.x, shape.y, shape.z), [shape]); useEffect(() => { diff --git a/src/components/plots/FlatBlocks.tsx b/src/components/plots/FlatBlocks.tsx index 83696fd0..b972eeae 100644 --- a/src/components/plots/FlatBlocks.tsx +++ b/src/components/plots/FlatBlocks.tsx @@ -5,23 +5,25 @@ import { usePlotStore } from '@/GlobalStates/PlotStore'; import { useErrorStore } from '@/GlobalStates/ErrorStore'; import { useShallow } from 'zustand/shallow' import * as THREE from 'three' -import { sphereBlocksFrag } from '../textures/shaders' +import { flatBlocksVert, sphereBlocksFrag } from '../textures/shaders' import { invalidate } from '@react-three/fiber' import { deg2rad } from '@/utils/HelperFuncs' import { useCoordBounds } from '@/hooks/useCoordBounds' -import { GetVert } from '../textures/GetVert'; import { usePaddedTextures } from '@/hooks/usePaddedTextures'; +import { useAxisIndices } from '@/hooks'; const FlatBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] | THREE.DataTexture[] | null}) => { const textures = usePaddedTextures(propTextures); const {colormap, isFlat, valueScales, flipY, - dataShape, textureArrayDepths} = useGlobalStore(useShallow(state=>({ + dataShape, textureArrayDepths, axisDimArrays, remapTexture} = useGlobalStore(useShallow(state=>({ colormap: state.colormap, isFlat: state.isFlat, valueScales: state.valueScales, flipY: state.flipY, dataShape: state.dataShape, - textureArrayDepths: state.textureArrayDepths + textureArrayDepths: state.textureArrayDepths, + axisDimArrays: state.axisDimArrays, + remapTexture: state.remapTexture }))) const { animProg, cOffset, cScale, nanColor, nanTransparency, displacement, fillValue, valueRange, offsetNegatives, rotateFlat, maskTexture, maskValue, } = usePlotStore(useShallow(state=> ({ @@ -34,16 +36,16 @@ const FlatBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] const {analysisMode, axis} = useAnalysisStore(useShallow(state => ({ analysisMode: state.analysisMode, axis:state.axis }))) + const {xIdx, yIdx} = useAxisIndices() const {width, height} = useMemo(()=>{ - if (dataShape.length == 2){ - return {width: dataShape[1], height: dataShape[0]} - } else if (analysisMode){ + if (analysisMode){ const thisShape = dataShape.filter((_val, idx) => idx != axis) return {width: thisShape[1], height: thisShape[0]} } else { - return {width: dataShape[2], height: dataShape[1]} + return {width: axisDimArrays[xIdx].length, height: axisDimArrays[yIdx].length} } - },[analysisMode, axis, dataShape]) + },[analysisMode, axis, dataShape, axisDimArrays]) + const rotateMap = analysisMode && axis == 2; const count = useMemo(()=>{ const count = width * height; @@ -84,6 +86,7 @@ const FlatBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] glslVersion: THREE.GLSL3, uniforms: { map: { value: textures }, + remapTexture: { value: remapTexture }, maskTexture: {value: maskTexture}, maskValue: {value: maskValue}, threshold: {value: new THREE.Vector2(valueRange[0],valueRange[1])}, @@ -101,14 +104,18 @@ const FlatBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] displacement: {value: displacement}, fillValue: {value: fillValue?? NaN}, }, - vertexShader: GetVert("flatBlocksVert", isFlat), + defines:{ + IS_FLAT: isFlat, + REPROJECT: remapTexture ? true: false + }, + vertexShader: flatBlocksVert, fragmentShader: sphereBlocksFrag, blending: THREE.NoBlending, depthWrite:true, depthTest:true, }) return shader - },[width, height, isFlat]) + },[width, height, isFlat, remapTexture]) useEffect(()=>{ if (shaderMaterial){ diff --git a/src/components/plots/FlatMap.tsx b/src/components/plots/FlatMap.tsx index 11422432..16b549d2 100644 --- a/src/components/plots/FlatMap.tsx +++ b/src/components/plots/FlatMap.tsx @@ -12,7 +12,7 @@ import { ThreeEvent } from '@react-three/fiber'; import { coarsenFlatArray, GetCurrentArray, GetTimeSeries, parseUVCoords, deg2rad } from '@/utils/HelperFuncs'; import { evaluate_cmap } from 'js-colormaps-es'; import { useCoordBounds } from '@/hooks/useCoordBounds'; -import { GetFrag } from '../textures'; +import { flatFrag } from '../textures/shaders'; import { SquareMeshes } from './TransectMeshes'; import { usePaddedTextures } from '@/hooks/usePaddedTextures'; import { useAxisIndices } from '@/hooks'; @@ -27,13 +27,14 @@ const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataT const textures = usePaddedTextures(propTextures); const {setLoc, setShowInfo, val, coords} = infoSetters; const {flipY, colormap, dimArrays, dimNames, dimUnits, - isFlat, dataShape, textureArrayDepths, strides, + isFlat, dataShape, textureArrayDepths, strides, remapTexture, shape, setPlotDim,updateDimCoords, updateTimeSeries} = useGlobalStore(useShallow(state => ({ flipY: state.flipY, colormap: state.colormap, dimArrays: state.dimArrays, strides: state.strides, dimNames:state.dimNames, dimUnits: state.dimUnits, isFlat: state.isFlat, dataShape: state.dataShape, textureArrayDepths: state.textureArrayDepths, + remapTexture:state.remapTexture, shape: state.shape, setPlotDim:state.setPlotDim, updateDimCoords:state.updateDimCoords, updateTimeSeries: state.updateTimeSeries @@ -78,17 +79,17 @@ const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataT if (coarsen) slices = slices.map((val, idx) => coarsenFlatArray(val, (idx === 0 && slices.length > 2 ? kernelDepth : kernelSize))) return slices } ,[dimArrays, zSlice, ySlice, xSlice, coarsen, kernelDepth, kernelSize, xIdx, yIdx, zIdx]) - const shapeRatio = useMemo(()=> { if (dataShape.length == 2){ - return dataShape[0]/dataShape[1] + return shape.y/shape.x } else if (analysisMode){ const thisShape = dataShape.filter((_val, idx) => idx != axis) return thisShape[0]/thisShape[1] } else { - return dataShape[1]/dataShape[2] + return shape.y/shape.x } - }, [axis, analysisMode] ) + }, [axis, shape, dataShape, analysisMode] ) + const geometry = useMemo(()=>new THREE.PlaneGeometry(2,2*shapeRatio),[shapeRatio]) const infoRef = useRef(false) const lastUV = useRef(new THREE.Vector2(0,0)) @@ -184,6 +185,7 @@ const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataT cScale: {value: cScale}, cOffset: {value: cOffset}, map : {value: textures}, + remapTexture: { value: remapTexture}, maskTexture: {value: maskTexture}, maskValue: {value: maskValue}, threshold: {value: new THREE.Vector2(valueRange[0],valueRange[1])}, @@ -196,10 +198,14 @@ const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataT nanAlpha: {value: 1 - nanTransparency}, fillValue: {value: fillValue?? NaN}, }, + defines:{ + IS_FLAT: isFlat, + REPROJECT: remapTexture ? true: false + }, vertexShader: vertShader, - fragmentShader: GetFrag("flatFrag", isFlat), + fragmentShader: flatFrag, side: THREE.DoubleSide, - }),[isFlat, textures]) + }),[isFlat, remapTexture, textures]) useEffect(()=>{ if(shaderMaterial){ diff --git a/src/components/plots/Sphere.tsx b/src/components/plots/Sphere.tsx index a77e4844..26a7ae06 100644 --- a/src/components/plots/Sphere.tsx +++ b/src/components/plots/Sphere.tsx @@ -7,10 +7,10 @@ import { useShallow } from 'zustand/shallow' import { parseUVCoords, GetTimeSeries, GetCurrentArray, deg2rad } from '@/utils/HelperFuncs'; import { evaluate_cmap } from 'js-colormaps-es'; import { useCoordBounds } from '@/hooks/useCoordBounds' -import { GetFrag, GetVert } from '../textures'; import { SquareMeshes } from './TransectMeshes'; import { usePaddedTextures } from '@/hooks/usePaddedTextures'; import { useAxisIndices } from '@/hooks'; +import { sphereVertex, sphereFrag } from '@/components/textures/shaders' function XYZtoRemap(xyz : THREE.Vector3, latBounds: number[], lonBounds : number[]){ const lon = Math.atan2(xyz.z,xyz.x) const lat = Math.asin(xyz.y); @@ -101,8 +101,11 @@ export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture displacement: {value: sphereDisplacement}, fillValue: {value: NaN}, }, - vertexShader: GetVert('sphereVertex', isFlat), - fragmentShader: GetFrag('sphereFrag', isFlat), + defines:{ + IS_FLAT: isFlat + }, + vertexShader: sphereVertex, + fragmentShader: sphereFrag, blending: THREE.NormalBlending, side:THREE.FrontSide, transparent: true, diff --git a/src/components/plots/SphereBlocks.tsx b/src/components/plots/SphereBlocks.tsx index ef9dad6c..1305169f 100644 --- a/src/components/plots/SphereBlocks.tsx +++ b/src/components/plots/SphereBlocks.tsx @@ -4,11 +4,10 @@ import { usePlotStore } from '@/GlobalStates/PlotStore'; import { useErrorStore } from '@/GlobalStates/ErrorStore'; import { useShallow } from 'zustand/shallow' import * as THREE from 'three' -import { sphereBlocksFrag } from '../textures/shaders' +import { sphereBlocksFrag, sphereBlocksVert } from '../textures/shaders' import { invalidate } from '@react-three/fiber' import { deg2rad } from '@/utils/HelperFuncs' import { useCoordBounds } from '@/hooks/useCoordBounds' -import { GetVert } from '../textures'; import { usePaddedTextures } from '@/hooks/usePaddedTextures'; const SphereBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] | THREE.DataTexture[] | null}) => { const textures = usePaddedTextures(propTextures); @@ -98,7 +97,10 @@ const SphereBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[ displacement: {value: sphereDisplacement}, fillValue: {value: fillValue?? NaN}, }, - vertexShader: GetVert("sphereBlocksVert", isFlat), + defines:{ + IS_FLAT: isFlat + }, + vertexShader: sphereBlocksVert, fragmentShader: sphereBlocksFrag, blending:THREE.NoBlending, depthWrite:true, diff --git a/src/components/textures/GetFrag.ts b/src/components/textures/GetFrag.ts deleted file mode 100644 index 2dcf0b6a..00000000 --- a/src/components/textures/GetFrag.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { sphereFrag, flatFrag } from "./shaders" - -const shaders ={ - sphereFrag, - flatFrag -} - -export const GetFrag = (shader:keyof typeof shaders, isFlat: boolean) => { - const frag = shaders[shader] - const prefix = isFlat ? "#define IS_FLAT\n" : ""; - const output = prefix + frag; - return ( - output - ) -} \ No newline at end of file diff --git a/src/components/textures/GetVert.ts b/src/components/textures/GetVert.ts deleted file mode 100644 index 64c96464..00000000 --- a/src/components/textures/GetVert.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { flatBlocksVert, sphereVertex, sphereBlocksVert } from "@/components/textures/shaders" - -const shaders ={ - flatBlocksVert, - sphereVertex, - sphereBlocksVert -} - -export const GetVert = (shader:keyof typeof shaders, isFlat: boolean) => { - const vert = shaders[shader as keyof typeof shaders] - const prefix = isFlat ? "#define IS_FLAT\n" : ""; - const output = prefix + vert; - return ( - output - ) -} - - diff --git a/src/components/textures/ProjectionTexture.ts b/src/components/textures/ProjectionTexture.ts new file mode 100644 index 00000000..ff372a14 --- /dev/null +++ b/src/components/textures/ProjectionTexture.ts @@ -0,0 +1,249 @@ +import { useGlobalStore } from '@/GlobalStates/GlobalStore'; +import { usePlotStore } from '@/GlobalStates/PlotStore'; +import { ArrayMinMax, linspace } from '@/utils/HelperFuncs'; +import { useErrorStore } from '@/GlobalStates/ErrorStore'; +import * as THREE from 'three'; + +import proj4 from 'proj4'; +import { getAxisIndices } from '@/hooks/useAxisIndices'; +import { useZarrStore } from '@/GlobalStates/ZarrStore'; + +export function checkProjString(projString: string){ + const {setError} = useErrorStore.getState() + try{ + const proj = proj4(projString) + return true + } catch{ + setError('badProj') + return false + } +} + +export function resetProjection(){ + const {dimArrays, dimNames, dimUnits, shape, remapTexture} = useGlobalStore.getState() + if (remapTexture) remapTexture.dispose() + const {xSlice, ySlice} = useZarrStore.getState() + const {xIdx, yIdx} = getAxisIndices() + + const xLength = dimArrays[xIdx].length; + const yLength = dimArrays[yIdx].length; + const aspectRatio = xLength/yLength; + const newShape = new THREE.Vector3().copy(shape) + newShape.y = 2/aspectRatio; + + + useGlobalStore.setState({ + axisDimArrays: dimArrays, + axisDimUnits: dimUnits, + axisDimNames: dimNames, + shape: newShape, + remapTexture: undefined + }) + usePlotStore.setState({ + xSlice, + ySlice + }) + +} + +function normalizeArray(array: Array){ + const len = array.length; + let min = Infinity; + let max = -Infinity; + for (let i = 0; i < len; i++){ + const v = array[i]; + if (v < min) min = v; + if (v > max) max = v; + } + const range = max - min; + const scaler = range === 0 ? 0 : 1 / range; + const out = new Float32Array(len); + for (let i = 0; i < array.length; i++){ + out[i] = (array[i]-min)* scaler; + } + return out; +} + +function isUniformStep(array: Array): boolean { + const len = array.length; + if (len < 3) return true; // any 0–2 element array trivially qualifies + + const step = array[1] - array[0]; + + for (let i = 2; i < len; i++) { + if (array[i] - array[i - 1] !== step) { + return false; + } + } + return true; +} + +function createRemapTexture(xArray: Array, yArray: Array) { + const width = xArray.length; + const height = yArray.length; + + const normX = normalizeArray(xArray); + const normY = normalizeArray(yArray); + + const data = new Float32Array(width * height * 2); + let ptr = 0; + for (let j = 0; j < height; j++) { + const y = normY[j]; + for (let i = 0; i < width; i++) { + const x = normX[i]; + data[ptr++] = x; + data[ptr++] = y; + } + } + + const texture = new THREE.DataTexture( + data, + width, + height, + THREE.RGFormat, + THREE.FloatType + ); + + texture.needsUpdate = true; + texture.magFilter = THREE.LinearFilter; + texture.minFilter = THREE.LinearFilter; + texture.wrapS = THREE.ClampToEdgeWrapping; + texture.wrapT = THREE.ClampToEdgeWrapping; + + return texture; +} + +export function SetReprojectionTexture(dimArrays: Array[]){ + const dimCount = dimArrays.length; + const {xIdx, yIdx} = getAxisIndices() + const xArray = dimArrays[xIdx]; + const yArray = dimArrays[yIdx]; + const isRegular = isUniformStep(xArray) && isUniformStep(yArray) + if (isRegular) return; + //Dispose of remaptexture if you use this function + const remapTexture = createRemapTexture(xArray, yArray); + useGlobalStore.setState({remapTexture}); +} + +export function reproject(resolution: number = 256){ + const {nativeCRS, destCRS} = usePlotStore.getState() + if (!nativeCRS || !destCRS) return; // This shouldn't trigger as the button will be disabled for this same condition + if (!checkProjString(destCRS)) return; // nativeCRS will already be checked when the user sets it, so we don't need to check it here + + const {dimArrays, remapTexture } = useGlobalStore.getState() + if (remapTexture) remapTexture.dispose(); + + const {xIdx, yIdx} = getAxisIndices() + const xArray = dimArrays[xIdx]; + const yArray = dimArrays[yIdx]; + const width = xArray.length; + const height = yArray.length; + + + const [xMin, xMax] = ArrayMinMax(xArray); + const [yMin, yMax] = ArrayMinMax(yArray); + // We need the border points as the min/max of the old CRS won't always be the min/max of the new CRS + const boundaryPoints: [number, number][] = []; + + + for (let i = 0; i < width; i++) { + boundaryPoints.push([xArray[i], yArray[0]]); + } + for (let i = 0; i < width; i++) { + boundaryPoints.push([xArray[i], yArray[height - 1]]); + } + for (let j = 0; j < height; j++) { + boundaryPoints.push([xArray[0], yArray[j]]); + } + for (let j = 0; j < height; j++) { + boundaryPoints.push([xArray[width - 1], yArray[j]]); + } + + const proj = proj4(nativeCRS, destCRS); + let [minX, minY] = [Infinity, Infinity]; + let [maxX, maxY] = [-Infinity, -Infinity]; + + // Get min/max of new CRS for new Axis' + for (const [lon, lat] of boundaryPoints) { + const [px, py] = proj.forward([lon, lat]); + minX = Math.min(minX, px); maxX = Math.max(maxX, px); + minY = Math.min(minY, py); maxY = Math.max(maxY, py); + } + + const aspectRatio = Math.abs(maxX - minX)/ Math.abs(maxY - minY); + function safeInverse(proj: any, xy: [number, number], tol = 1e-6) { + //This function checks if the coordinates are valid and returns 0 or 1 based on conditions + const [lon, lat] = proj.inverse(xy); + if (!isFinite(lon) || !isFinite(lat)) return [lon, lat, 0]; + const [xCheck, yCheck] = proj.forward([lon, lat]); + if (Math.abs(xCheck - xy[0]) > tol * Math.max(1, Math.abs(xy[0])) || + Math.abs(yCheck - xy[1]) > tol * Math.max(1, Math.abs(xy[1]))) { + return [lon, lat, 0]; + } + return [lon, lat, 1]; + } + + // ---- Construct new CRS axis' ----// + const targetWidth = Math.ceil(resolution*aspectRatio); + const targetHeight = resolution; + const xTicks = linspace(minX, maxX, targetWidth); + const yTicks = linspace(minY, maxY, targetHeight); + + // ---- Create Rpojection Texture ----// + const data = new Uint16Array(targetWidth * targetHeight * 4); + for (let j = 0; j < targetHeight; j++) { + for (let i = 0; i < targetWidth; i++) { + const [lon, lat, valid] = safeInverse(proj, [xTicks[i], yTicks[j]]); + const u = (lon - xMin) / (xMax - xMin); + const v = (lat - yMin) / (yMax - yMin); + const idx = (j * targetWidth + i) * 4; + data[idx] = THREE.DataUtils.toHalfFloat(u); + data[idx + 1] = THREE.DataUtils.toHalfFloat(v); + data[idx + 2] = THREE.DataUtils.toHalfFloat(valid); + } + } + const texture = new THREE.DataTexture( + data, + targetWidth, + targetHeight, + THREE.RGBAFormat, // Must be RGBA as HalfFloat RGB is not supported + THREE.HalfFloatType + ); + texture.needsUpdate = true; + texture.magFilter = THREE.LinearFilter; + texture.minFilter = THREE.LinearFilter; + texture.wrapS = THREE.ClampToEdgeWrapping; + texture.wrapT = THREE.ClampToEdgeWrapping; + + useGlobalStore.setState({remapTexture: texture}) + + // ---- Update Axis and Shape information ----// + const crsCheck = proj4(destCRS); + const {axisDimArrays, axisDimUnits, axisDimNames, shape} = useGlobalStore.getState() + const newAxisDimArrays = [...axisDimArrays]; + newAxisDimArrays[xIdx] = xTicks; + newAxisDimArrays[yIdx] = yTicks; + + const newAxisDimUnits = [...axisDimUnits]; + //@ts-ignore At this point these are all valid + newAxisDimUnits[xIdx] = crsCheck.oProj.units; + //@ts-ignore At this point these are all valid + newAxisDimUnits[yIdx] = crsCheck.oProj.units; + + const newAxisDimNames = [...axisDimNames]; + newAxisDimNames[xIdx] = 'X'; + newAxisDimNames[yIdx] = 'Y'; + const newShape = new THREE.Vector3().copy(shape) + newShape.y = 2/aspectRatio; + useGlobalStore.setState({ + axisDimArrays: newAxisDimArrays, + axisDimUnits: newAxisDimUnits, + axisDimNames: newAxisDimNames, + shape: newShape + }) + usePlotStore.setState({ + xSlice: [0, null], + ySlice: [0, null] + }) + +} \ No newline at end of file diff --git a/src/components/textures/index.ts b/src/components/textures/index.ts index cc3d8d94..d9d0cda0 100644 --- a/src/components/textures/index.ts +++ b/src/components/textures/index.ts @@ -6,6 +6,4 @@ export { colormaps, ArrayToTexture, CreateTexture -} -export {GetFrag} from './GetFrag' -export {GetVert} from './GetVert' \ No newline at end of file +} \ No newline at end of file diff --git a/src/components/textures/shaders/flatBlocksVert.glsl b/src/components/textures/shaders/flatBlocksVert.glsl index c0fbe437..75341dc8 100644 --- a/src/components/textures/shaders/flatBlocksVert.glsl +++ b/src/components/textures/shaders/flatBlocksVert.glsl @@ -3,11 +3,11 @@ attribute vec2 instanceUV; #ifdef IS_FLAT - uniform sampler2D map[14]; + uniform sampler2D map[12]; #else - uniform sampler3D map[14]; + uniform sampler3D map[12]; #endif - +uniform sampler2D remapTexture; uniform sampler2D maskTexture; uniform vec3 textureDepths; @@ -61,8 +61,6 @@ float sample1( else if (index == 9) return texture(map[9], p).r; else if (index == 10) return texture(map[10], p).r; else if (index == 11) return texture(map[11], p).r; - else if (index == 12) return texture(map[12], p).r; - else if (index == 13) return texture(map[13], p).r; else return 0.0; } @@ -81,6 +79,14 @@ void main() { int zStepSize = int(textureDepths.y) * int(textureDepths.x); int yStepSize = int(textureDepths.x); vec3 texCoord = vec3(instanceUV, animateProg); + #ifdef REPROJECT + vec3 remap = texture(remapTexture,instanceUV).rgb; + texCoord.xy = remap.rg; + if (remap.b < 0.5){ + gl_Position = vec4(2.0, 2.0, 2.0, 1.0); + return; + } + #endif #ifdef IS_FLAT ivec2 idx = clamp(ivec2(instanceUV * textureDepths.xy), ivec2(0), ivec2(textureDepths.xy) - 1); int textureIdx = idx.y * yStepSize + idx.x; diff --git a/src/components/textures/shaders/flatFrag.glsl b/src/components/textures/shaders/flatFrag.glsl index ba49450b..1ac1b15e 100644 --- a/src/components/textures/shaders/flatFrag.glsl +++ b/src/components/textures/shaders/flatFrag.glsl @@ -1,12 +1,13 @@ //This is for Flat Textures but with 3D textures to sample from i,e; animation #ifdef IS_FLAT - uniform sampler2D map[14]; + uniform sampler2D map[12]; #else - uniform sampler3D map[14]; + uniform sampler3D map[12]; #endif uniform sampler2D maskTexture; uniform sampler2D cmap; +uniform sampler2D remapTexture; uniform vec3 textureDepths; @@ -58,8 +59,8 @@ float sample1( else if (index == 9) return texture(map[9], p).r; else if (index == 10) return texture(map[10], p).r; else if (index == 11) return texture(map[11], p).r; - else if (index == 12) return texture(map[12], p).r; - else if (index == 13) return texture(map[13], p).r; + // else if (index == 12) return texture(map[12], p).r; + // else if (index == 13) return texture(map[13], p).r; else return 0.0; } @@ -78,12 +79,22 @@ void main() { int yStepSize = int(textureDepths.x); #ifdef IS_FLAT vec2 texCoord = vUv; + #ifdef REPROJECT + vec3 remap = texture(remapTexture,texCoord.xy).rgb; + texCoord.xy = remap.rg; + if (remap.b < 0.5) discard; + #endif texCoord.xy = clamp(texCoord.xy, vec2(0.0), 1. - vec2(epsilon)); // This prevent the very edges from looping around and causing line artifacts ivec2 idx = clamp(ivec2(texCoord * textureDepths.xy), ivec2(0), ivec2(textureDepths.xy) - 1); int textureIdx = idx.y * yStepSize + idx.x; vec2 localCoord = texCoord * (textureDepths.xy); // Scale up #else vec3 texCoord = vec3(vUv, animateProg); + #ifdef REPROJECT + vec3 remap = texture(remapTexture,texCoord.xy).rgb; + texCoord.xy = remap.rg; + if (remap.b < 0.5) discard; + #endif texCoord.xy = clamp(texCoord.xy, vec2(0.0), 1. - vec2(epsilon)); // This prevent the very edges from looping around and causing line artifacts ivec3 idx = clamp(ivec3(texCoord * textureDepths), ivec3(0), ivec3(textureDepths) - 1); int textureIdx = idx.z * zStepSize + idx.y * yStepSize + idx.x; @@ -101,4 +112,7 @@ void main() { float sampLoc = isNaN ? strength: (strength)*cScale; sampLoc = isNaN ? strength : min(sampLoc+cOffset,0.995); Color = isNaN ? vec4(nanColor, nanAlpha) : vec4(texture2D(cmap, vec2(sampLoc, 0.5)).rgb, 1.); + // float check = float(texture(remapTexture,texCoord.xy).g >= 0.); + // Color = vec4(check, 0., 0. , 1.); + // Color = vec4(texture(remapTexture,texCoord.xy).rg, 0. , 1.); } \ No newline at end of file diff --git a/src/components/textures/shaders/fragmentOpt.glsl b/src/components/textures/shaders/fragmentOpt.glsl index d9ba71c0..da95a2a0 100644 --- a/src/components/textures/shaders/fragmentOpt.glsl +++ b/src/components/textures/shaders/fragmentOpt.glsl @@ -8,7 +8,7 @@ in vec3 vDirection; out vec4 color; -uniform sampler3D map[14]; // We are limited to 16 textures. Cmap counts as one. 15 is weird so we use 14. +uniform sampler3D map[12]; // We are limited to 16 textures. Cmap counts as one. 15 is weird so we use 14. uniform sampler2D maskTexture; uniform sampler2D cmap; uniform vec3 textureDepths; @@ -74,8 +74,6 @@ float sample1(vec3 p, int index) { // Shader doesn't support dynamic indexing so else if (index == 9) return texture(map[9], p).r; else if (index == 10) return texture(map[10], p).r; else if (index == 11) return texture(map[11], p).r; - else if (index == 12) return texture(map[12], p).r; - else if (index == 13) return texture(map[13], p).r; else return 0.0; } diff --git a/src/components/textures/shaders/sphereBlocksVert.glsl b/src/components/textures/shaders/sphereBlocksVert.glsl index 6ab91c5b..963adc7e 100644 --- a/src/components/textures/shaders/sphereBlocksVert.glsl +++ b/src/components/textures/shaders/sphereBlocksVert.glsl @@ -3,9 +3,9 @@ attribute vec2 instanceUV; #ifdef IS_FLAT - uniform sampler2D map[14]; + uniform sampler2D map[12]; #else - uniform sampler3D map[14]; + uniform sampler3D map[12]; #endif uniform sampler2D maskTexture; uniform vec3 textureDepths; @@ -75,8 +75,6 @@ float sample1( else if (index == 9) return texture(map[9], p).r; else if (index == 10) return texture(map[10], p).r; else if (index == 11) return texture(map[11], p).r; - else if (index == 12) return texture(map[12], p).r; - else if (index == 13) return texture(map[13], p).r; else return 0.0; } diff --git a/src/components/textures/shaders/sphereFrag.glsl b/src/components/textures/shaders/sphereFrag.glsl index 55d78d84..4face350 100644 --- a/src/components/textures/shaders/sphereFrag.glsl +++ b/src/components/textures/shaders/sphereFrag.glsl @@ -4,9 +4,9 @@ out vec4 color; in vec3 aPosition; #ifdef IS_FLAT - uniform sampler2D map[14]; + uniform sampler2D map[12]; #else - uniform sampler3D map[14]; + uniform sampler3D map[12]; #endif uniform sampler2D maskTexture; @@ -69,8 +69,6 @@ float sample1( else if (index == 9) return texture(map[9], p).r; else if (index == 10) return texture(map[10], p).r; else if (index == 11) return texture(map[11], p).r; - else if (index == 12) return texture(map[12], p).r; - else if (index == 13) return texture(map[13], p).r; else return 0.0; } diff --git a/src/components/textures/shaders/sphereVertex.glsl b/src/components/textures/shaders/sphereVertex.glsl index 94a72759..abafb755 100644 --- a/src/components/textures/shaders/sphereVertex.glsl +++ b/src/components/textures/shaders/sphereVertex.glsl @@ -1,9 +1,9 @@ // by Jeran Poehls #ifdef IS_FLAT - uniform sampler2D map[14]; + uniform sampler2D map[12]; #else - uniform sampler3D map[14]; + uniform sampler3D map[12]; #endif uniform vec3 textureDepths; @@ -44,8 +44,6 @@ float sample1( else if (index == 9) return texture(map[9], p).r; else if (index == 10) return texture(map[10], p).r; else if (index == 11) return texture(map[11], p).r; - else if (index == 12) return texture(map[12], p).r; - else if (index == 13) return texture(map[13], p).r; else return 0.0; } diff --git a/src/components/textures/shaders/volFragment.glsl b/src/components/textures/shaders/volFragment.glsl index fa128c0d..112489d6 100644 --- a/src/components/textures/shaders/volFragment.glsl +++ b/src/components/textures/shaders/volFragment.glsl @@ -7,9 +7,10 @@ in vec3 vDirection; out vec4 color; -uniform sampler3D map[14]; // We are limited to 16 textures. Cmap counts as one. 15 is weird so we use 14. +uniform sampler3D map[12]; // We are limited to 16 textures. Cmap counts as one. 15 is weird so we use 14. uniform sampler2D maskTexture; uniform sampler2D cmap; +uniform sampler2D remapTexture; uniform vec3 textureDepths; uniform float cOffset; @@ -74,8 +75,6 @@ float sample1(vec3 p, int index) { // Shader doesn't support dynamic indexing so else if (index == 9) return texture(map[9], p).r; else if (index == 10) return texture(map[10], p).r; else if (index == 11) return texture(map[11], p).r; - else if (index == 12) return texture(map[12], p).r; - else if (index == 13) return texture(map[13], p).r; else return 0.0; } @@ -112,6 +111,11 @@ void main() { continue; } vec3 texCoord = p / scale + 0.5; + #ifdef REPROJECT + vec3 remap = texture2D(remapTexture, texCoord.xy).rgb; + texCoord.xy = remap.rg; + if (remap.b < 0.5) {continue;} + #endif if (maskValue != 0){ vec2 newV = texCoord.xy; diff --git a/src/components/ui/Elements/ErrorList.ts b/src/components/ui/Elements/ErrorList.ts index 40e33424..ec79b35a 100644 --- a/src/components/ui/Elements/ErrorList.ts +++ b/src/components/ui/Elements/ErrorList.ts @@ -26,5 +26,9 @@ export const ErrorList = { largeArray :{ title: "Requested too Much Memory", description: "Browzarr cannot request that much data in one call. This is a fixable issue, open an issue on GitHub so I am motivated to implement it." + }, + badProj :{ + title: "Unrecognized Projection String", + description: "The projection string provided is not recognized. Please check the string and try again." } } \ No newline at end of file diff --git a/src/components/ui/Elements/Reprojection.tsx b/src/components/ui/Elements/Reprojection.tsx new file mode 100644 index 00000000..3253b81f --- /dev/null +++ b/src/components/ui/Elements/Reprojection.tsx @@ -0,0 +1,119 @@ +import { usePlotStore } from '@/GlobalStates/PlotStore' +import React, { useRef, useState } from 'react' +import { useShallow } from 'zustand/shallow' +import { Hider } from '../Widgets/Hider' +import { ChevronDown } from 'lucide-react' +import { Input } from '../input' +import { Button } from '../button' +import { checkProjString, reproject, resetProjection } from '@/components/textures/ProjectionTexture' +import { TbReplace } from "react-icons/tb"; +import { RxReset } from "react-icons/rx"; + +export const Reprojection = () => { + const {destCRS, nativeCRS} = usePlotStore(useShallow(state => ({ + destCRS: state.destCRS, + nativeCRS: state.nativeCRS + }))) + const [showRepro, setShowRepro] = useState(false) + const [changeNativeCRS, setChangeNativeCRS] = useState(false) + + const tempCRS = useRef('') + const repRes = useRef(256) + + function handleNativeCRS(){ + const valid = checkProjString(tempCRS.current) + if (valid){ + usePlotStore.setState({ nativeCRS: tempCRS.current }) + setChangeNativeCRS(false) + } + } + return ( +
+ + + +
+
+
+ {nativeCRS + ? `Native CRS: ${nativeCRS}` + : "No CRS detected"} +
+
+ + +
+ +
+ +
+ (tempCRS.current = e.target.value)} + placeholder={`${nativeCRS ? "Update" : "Enter" } native CRS`} + /> + +
+
+
+
+

Target Projection

+

Resolution

+ usePlotStore.setState({ destCRS: e.target.value })} + placeholder="Target projection" + /> + { const val = parseInt(e.target.value); repRes.current = isNaN(val) ? 256 : val; }} + placeholder="Resolution" + /> +
+ +
+
+
+
+ ) +} + + diff --git a/src/components/ui/MainPanel/AdjustPlot.tsx b/src/components/ui/MainPanel/AdjustPlot.tsx index 4233513e..1e3a3b86 100644 --- a/src/components/ui/MainPanel/AdjustPlot.tsx +++ b/src/components/ui/MainPanel/AdjustPlot.tsx @@ -20,6 +20,8 @@ import { BsFillQuestionCircleFill } from "react-icons/bs"; import { ChevronDown } from 'lucide-react'; import {Select, SelectTrigger, SelectContent, SelectItem, SelectValue} from '@/components/ui' import { RiCloseLargeLine } from "react-icons/ri"; +import { reproject } from '@/components/textures/ProjectionTexture'; +import { Reprojection } from '../Elements/Reprojection'; function DeNorm(val : number, min : number, max : number){ const range = max-min; @@ -501,12 +503,12 @@ const SpatialExtent = () =>{ } const GlobalOptions = () =>{ - const {valueRange, showBorders, borderColor, nanColor, nanTransparency, plotType, interpPixels, fillValue, useBorderTexture, + const {valueRange, showBorders, borderColor, nanColor, nanTransparency, plotType, interpPixels, fillValue, useBorderTexture, nativeCRS, setValueRange, setShowBorders, setBorderColor, setNanColor, setNanTransparency, setInterpPixels, setFillValue} = usePlotStore(useShallow(state => ({ showBorders: state.showBorders, borderColor: state.borderColor, nanColor: state.nanColor, nanTransparency: state.nanTransparency, plotType: state.plotType, interpPixels: state.interpPixels, - fillValue: state.fillValue, useBorderTexture:state.useBorderTexture, + fillValue: state.fillValue, useBorderTexture:state.useBorderTexture, nativeCRS: state.nativeCRS, valueRange: state.valueRange, setValueRange: state.setValueRange, setShowBorders: state.setShowBorders, setBorderColor: state.setBorderColor, setNanColor: state.setNanColor, setNanTransparency: state.setNanTransparency, @@ -521,8 +523,10 @@ const GlobalOptions = () =>{ }))) const [thisFillVal, setThisFillValue] = useState(denormalize(fillValue, valueScales.minVal, valueScales.maxVal)) const [showMasks, setShowMasks] = useState(false) + const [showRepro, setShowRepro] = useState(false) const masks = ["None", "Land", "Water"] const isPC = plotType == 'point-cloud' + return (
@@ -737,7 +741,7 @@ const AdjustPlot = () => { Close settings -
+
{ {plotType === 'sphere' && } {(plotType === 'volume' || plotType === 'point-cloud') && } {plotType === 'flat' && } + {['flat','volume'].includes(plotType) && }
diff --git a/src/hooks/useAxisIndices.tsx b/src/hooks/useAxisIndices.tsx index 44329b84..9a72ab21 100644 --- a/src/hooks/useAxisIndices.tsx +++ b/src/hooks/useAxisIndices.tsx @@ -4,7 +4,6 @@ import React from 'react' import { useShallow } from 'zustand/shallow' - export const useAxisIndices = () => { const { axisMapping } = useZarrStore(useShallow(state => ({ axisMapping: state.axisMapping @@ -22,4 +21,14 @@ export const useAxisIndices = () => { } } - +export const getAxisIndices = () => { + const { axisMapping } = useZarrStore.getState() + const { dimArrays } = useGlobalStore.getState() + const shapeLength = dimArrays.length; + const xIdx = (axisMapping.x >= 0 && axisMapping.x < shapeLength) ? axisMapping.x : Math.max(0, shapeLength - 1); + const yIdx = (axisMapping.y >= 0 && axisMapping.y < shapeLength) ? axisMapping.y : Math.max(0, shapeLength - 2); + const zIdx = (axisMapping.z >= 0 && axisMapping.z < shapeLength) ? axisMapping.z : Math.max(0, shapeLength - 3); + return { + xIdx, yIdx, zIdx + } +} diff --git a/src/hooks/useDataFetcher.tsx b/src/hooks/useDataFetcher.tsx index 8fb2944d..3c89a9f6 100644 --- a/src/hooks/useDataFetcher.tsx +++ b/src/hooks/useDataFetcher.tsx @@ -8,6 +8,7 @@ import { ParseExtent, GetDimInfo } from '@/utils/HelperFuncs'; import { GetAttributes } from '@/components/zarr/ZarrLoaderLRU'; import { GetArray } from '@/components/zarr/GetArray'; import { ArrayToTexture } from '@/components/textures'; +import { SetReprojectionTexture } from '@/components/textures/ProjectionTexture'; export const useDataFetcher = () => { const { @@ -126,12 +127,14 @@ export const useDataFetcher = () => { setDimArrays(dimArrays); setDimNames(dimNames); setDimUnits(dimUnits); + useGlobalStore.setState({axisDimArrays: dimArrays, axisDimNames: dimNames, axisDimUnits: dimUnits}); const targetDim = dimArrays.length > 2 ? dimArrays[1] : dimArrays[0]; const shouldFlip = targetDim[1] < targetDim[0]; setFlipY(shouldFlip); ParseExtent(dimUnits, dimArrays); + SetReprojectionTexture(dimArrays); }); } else { diff --git a/src/hooks/usePaddedTextures.ts b/src/hooks/usePaddedTextures.ts index f410d725..a3554865 100644 --- a/src/hooks/usePaddedTextures.ts +++ b/src/hooks/usePaddedTextures.ts @@ -3,7 +3,7 @@ import * as THREE from 'three'; type TextureArray = THREE.Data3DTexture[] | THREE.DataTexture[] | null | undefined; -export const usePaddedTextures = (textures: TextureArray, length: number = 14) => { +export const usePaddedTextures = (textures: TextureArray, length: number = 12) => { return useMemo(() => { return Array.from({ length }, (_, idx) => textures?.[idx] ?? textures?.[0]); }, [textures, length]);