4646import java .util .Collections ;
4747import java .util .function .Consumer ;
4848import java .util .function .Function ;
49+ import java .util .function .Supplier ;
4950
5051import com .google .gson .JsonSyntaxException ;
5152
5253import net .imglib2 .img .Img ;
5354import net .imglib2 .img .display .imagej .ImageJFunctions ;
5455import net .imglib2 .util .Cast ;
5556
56- import bdv .util .BdvFunctions ;
5757import ij .IJ ;
5858import sc .fiji .ome .zarr .pyramid .Pyramidal5DImageDataImpl ;
59+ import sc .fiji .ome .zarr .pyramid .PyramidalBdv ;
5960import sc .fiji .ome .zarr .pyramid .exceptions .MultiImageDatasetException ;
6061import sc .fiji .ome .zarr .pyramid .exceptions .NoMatchingResolutionException ;
62+ import sc .fiji .ome .zarr .pyramid .exceptions .NonExistingResolutionLevelException ;
6163import sc .fiji .ome .zarr .pyramid .exceptions .NotAMultiscaleImageException ;
6264import sc .fiji .ome .zarr .pyramid .exceptions .NotASingleScaleImageException ;
6365import sc .fiji .ome .zarr .pyramid .PyramidalDataset ;
6668import sc .fiji .ome .zarr .open .options .ZarrOpenBehavior ;
6769import sc .fiji .ome .zarr .open .options .ZarrReaderBackend ;
6870import sc .fiji .ome .zarr .ui .DnDActionChooser ;
71+ import sc .fiji .ome .zarr .util .PyramidalService ;
6972import sc .fiji .ome .zarr .util .BdvUtils ;
7073import sc .fiji .ome .zarr .util .ScriptUtils ;
7174
@@ -83,6 +86,8 @@ public class ZarrOpenActions
8386
8487 private final ZarrOpeningSettings settings ;
8588
89+ private Pyramidal5DImageDataImpl < ?, ? > cachedPyramid ;
90+
8691 /**
8792 * Loads {@link ZarrOpeningSettings} from {@code context} and opens
8893 * {@code inputUri} via the action selected by the user's configured
@@ -166,23 +171,131 @@ public void openViewerDialog()
166171
167172 public Object openIJWithImage ()
168173 {
169- return openImage (
170- pyramidalDataset -> {
171- context .getService ( UIService .class ).show ( pyramidalDataset );
172- return null ;
173- },
174- singleScaleImage -> ImageJFunctions .show ( Cast .unchecked ( singleScaleImage ) ),
175- "ImageJ"
176- );
174+ try
175+ {
176+ return openPyramidImage (
177+ () -> {
178+ final PyramidalDataset dataset = getPyramid ().asPyramidalDataset ();
179+ context .getService ( UIService .class ).show ( dataset );
180+ logger .info ( "Opened dataset in ImageJ: {}" , inputUri );
181+ return null ;
182+ },
183+ singleScaleImage -> ImageJFunctions .show ( Cast .unchecked ( singleScaleImage ) ) );
184+ }
185+ catch ( NoMatchingResolutionException e )
186+ {
187+ showNonMatchingResolutionError ( e );
188+ }
189+ return null ;
190+ }
191+
192+ /**
193+ * Opens the resolution level at {@code resolutionLevel} index as a new ImageJ dataset.
194+ * Index 0 is the highest resolution; each increment is the next coarser level.
195+ * <p>
196+ * Multiple calls — whether at the same or different level indices — each produce a separate
197+ * ImageJ {@code Dataset} (and a separate window), but all of them are backed by the same
198+ * {@link sc.fiji.ome.zarr.pyramid.Pyramidal5DImageData} object. The underlying
199+ * cached cell images and volatile images in
200+ * {@link sc.fiji.ome.zarr.pyramid.backend.PyramidContents} are the single source of truth
201+ * and are never loaded more than once per resolution level.
202+ *
203+ * @param resolutionLevel 0-based index into the resolution pyramid
204+ */
205+ public Object openIJWithImage ( final int resolutionLevel )
206+ {
207+ try
208+ {
209+ return openPyramidImage (
210+ () -> {
211+ final PyramidalDataset dataset = getPyramid ().asPyramidalDataset ( resolutionLevel );
212+ context .getService ( UIService .class ).show ( dataset );
213+ logger .info ( "Opened dataset at resolution level {} in ImageJ: {}" , resolutionLevel , inputUri );
214+ return null ;
215+ },
216+ singleScaleImage -> ImageJFunctions .show ( Cast .unchecked ( singleScaleImage ) ) );
217+ }
218+ catch ( NonExistingResolutionLevelException e )
219+ {
220+ showNonExistingResolutionLevelError ( e );
221+ }
222+ return null ;
177223 }
178224
179225 public Object openBDVWithImage ()
180226 {
181- return openImage (
182- BdvUtils ::showBdvAndRegisterDataset ,
183- singleScaleImage -> BdvFunctions .show ( singleScaleImage , "Image" ),
184- "BigDataViewer"
185- );
227+ try
228+ {
229+ return openPyramidImage (
230+ () -> {
231+ final PyramidalBdv < ? > dataset = new PyramidalBdv <>( getPyramid () );
232+ final PyramidalService pyramidalService = context .getService ( PyramidalService .class );
233+ final Object result = BdvUtils .showBdvAndRegisterDataset ( dataset , pyramidalService );
234+ logger .info ( "Opened dataset in BigDataViewer: {}" , inputUri );
235+ return result ;
236+ },
237+ singleScaleImage -> null );
238+ }
239+ catch ( NoMatchingResolutionException e )
240+ {
241+ showNonMatchingResolutionError ( e );
242+ }
243+ return null ;
244+ }
245+
246+ private Pyramidal5DImageDataImpl < ?, ? > getPyramid ()
247+ {
248+ if ( cachedPyramid == null )
249+ {
250+ final Integer preferredWidth ;
251+ if ( settings == null || settings .getOpenBehavior ().equals ( ZarrOpenBehavior .IMAGEJ_HIGHEST_RESOLUTION ) )
252+ preferredWidth = null ;
253+ else
254+ preferredWidth = settings .getPreferredMaxWidth ();
255+
256+ final ZarrReaderBackend backend = settings == null
257+ ? ZarrOpeningSettings .DEFAULT_READER_BACKEND
258+ : settings .getReaderBackend ();
259+ switch ( backend )
260+ {
261+ case ZARR_JAVA :
262+ {
263+ @ SuppressWarnings ( { "rawtypes" , "unchecked" } )
264+ final Pyramidal5DImageDataImpl < ?, ? > zarrJavaData =
265+ new Pyramidal5DImageDataImpl ( context , new ZarrJavaPyramidBackend ( inputUri ) );
266+ cachedPyramid = zarrJavaData ;
267+ break ;
268+ }
269+ case N5 :
270+ default :
271+ cachedPyramid = new Pyramidal5DImageDataImpl <>( context , inputUri , preferredWidth );
272+ break ;
273+ }
274+ }
275+ return cachedPyramid ;
276+ }
277+
278+ private Object openPyramidImage ( final Supplier < Object > multiScaleOpener , final Function < Img < ? >, Object > singleScaleOpener )
279+ {
280+ try
281+ {
282+ return multiScaleOpener .get ();
283+ }
284+ catch ( MultiImageDatasetException e )
285+ {
286+ showMultiImageNotSupported ( e );
287+ }
288+ catch ( NotAMultiscaleImageException e )
289+ {
290+ logger .warn ( "Not a multiscale image: {}" , e .getMessage () );
291+ showSingleScaleNotSupported ();
292+ // TODO: openSingleScaleImage( singleScaleOpener ) when single-scale support is added
293+ }
294+ catch ( IllegalArgumentException | JsonSyntaxException e )
295+ {
296+ showNonZarrError ( e );
297+ }
298+ return null ;
186299 }
187300
188301 private void showMultiImageNotSupported ( final MultiImageDatasetException e )
@@ -220,83 +333,33 @@ private void showNonMatchingResolutionError( final Exception e )
220333 logger .warn ( "Not opening dataset: {}. Error message: {}" , inputUri , e .getMessage () );
221334 }
222335
223- Object openImage ( final Function < PyramidalDataset < ? >, Object > multiScaleImageOpener ,
224- final Function < Img < ? >, Object > singleScaleImageOpener ,
225- final String message )
336+ private void showNonExistingResolutionLevelError ( final NonExistingResolutionLevelException e )
337+ {
338+ errorHandler .accept ( "Could not open resolution level from dataset: " + inputUri + "\n \n " + e .getMessage () );
339+ logger .warn ( "Could not open resolution level: {}. Error message: {}" , inputUri , e .getMessage () );
340+ }
341+
342+ Object openImage ( final Function < PyramidalDataset , Object > multiScaleImageOpener ,
343+ final Function < Img < ? >, Object > singleScaleImageOpener )
226344 {
227345 try
228346 {
229- Object result = openMultiScaleImage ( multiScaleImageOpener );
230- logger .info ( "Opened dataset in {}: {}" , message , inputUri );
231- return result ;
232- }
233- catch ( MultiImageDatasetException e )
234- {
235- logger .warn ( "Tried to open a multi image dataset: {}" , e .getMessage () );
236- showMultiImageNotSupported ( e );
237- }
238- catch ( NotAMultiscaleImageException e )
239- {
240- logger .warn ( "Not a multiscale image: {}" , e .getMessage () );
241- logger .info ( "Try opening as single-scale image instead." );
242- try
243- {
244- showSingleScaleNotSupported ();
245- //Object result = openSingleScaleImage( singleScaleImageOpener ); // currently not supported
246- //logger.info( "Opened single scale image in {}: {}", message, inputUri );
247- return null ;
248- }
249- catch ( NotASingleScaleImageException ex )
250- {
251- showSingleScaleError ( ex );
252- }
347+ return openPyramidImage (
348+ () -> {
349+ final PyramidalDataset dataset = getPyramid ().asPyramidalDataset ();
350+ final Object result = multiScaleImageOpener .apply ( dataset );
351+ logger .info ( "Opened dataset: {}" , inputUri );
352+ return result ;
353+ },
354+ singleScaleImageOpener );
253355 }
254356 catch ( NoMatchingResolutionException e )
255357 {
256358 showNonMatchingResolutionError ( e );
257359 }
258- catch ( IllegalArgumentException | JsonSyntaxException e )
259- {
260- showNonZarrError ( e );
261- }
262360 return null ;
263361 }
264362
265- private Object openMultiScaleImage ( final Function < PyramidalDataset < ? >, Object > multiScaleImageOpener )
266- throws NotAMultiscaleImageException , NoMatchingResolutionException
267- {
268- final Integer preferredWidth ;
269- if ( settings == null || settings .getOpenBehavior ().equals ( ZarrOpenBehavior .IMAGEJ_HIGHEST_RESOLUTION ) )
270- preferredWidth = null ;
271- else
272- preferredWidth = settings .getPreferredMaxWidth ();
273-
274- final ZarrReaderBackend backend = settings == null
275- ? ZarrOpeningSettings .DEFAULT_READER_BACKEND
276- : settings .getReaderBackend ();
277-
278- final Pyramidal5DImageDataImpl < ?, ? > data ;
279- switch ( backend )
280- {
281- case ZARR_JAVA :
282- {
283- @ SuppressWarnings ( { "rawtypes" , "unchecked" } )
284- final Pyramidal5DImageDataImpl < ?, ? > zarrJavaData =
285- new Pyramidal5DImageDataImpl ( context , new ZarrJavaPyramidBackend ( inputUri ) );
286- data = zarrJavaData ;
287- break ;
288- }
289- case N5 :
290- default :
291- data = new Pyramidal5DImageDataImpl <>( context , inputUri , preferredWidth );
292- break ;
293- }
294-
295- final Object result = multiScaleImageOpener .apply ( data .asPyramidalDataset () );
296- logger .info ( "Opened multiscale image with {} backend: {}" , backend , inputUri );
297- return result ;
298- }
299-
300363 private Object openSingleScaleImage ( final Function < Img < ? >, Object > singleScaleImageOpener ) throws NotASingleScaleImageException
301364 {
302365 N5Reader reader = new N5Factory ().openReader ( inputUri .toString () );
0 commit comments