diff --git a/BDV N5 format.md b/BDV N5 format.md new file mode 100644 index 0000000000000000000000000000000000000000..2c4cf4b2ffcc22767b7c06fd972b4a245e77547d --- /dev/null +++ b/BDV N5 format.md @@ -0,0 +1,57 @@ +# BigDataViewer N5 format + +The BigDataViewer N5 back-end (`format="bdv.n5"`) requires a N5 dataset with a specific group hierarchy and attributes. + +Multi-channel (-angle, -tile, -illumination) time-series as +multi-resolution 3D stacks are organized in the following N5 hierarchy: + +``` +\ +├── setup0 +│ ├── attributes.json {"downsamplingFactors":[[1,1,1],[2,2,2]],"dataType":"uint8"} +│ ├── timepoint0 +│ │ ├── s0 +│ │ │ ├── attributes.json {"dataType":"uint8","compression":{"type":"bzip2","blockSize":9},"blockSize":[16,16,16],"dimensions":[400,400,25]} +│ │ │ ┊ +│ │ │ +│ │ ├── s1 +│ │ ┊ +│ │ +│ ├── timepoint1 +│ ┊ +│ +├── setup1 +┊ +``` + +Each 3D stack is stored as a dataset with path formatted as `/setup%d/timepoint%d/s%d`. +Here, `setup` number is flattened integer ID of channel, angle, tile, illumination, etc., +`timepoint` is the integer ID of the time point, and `s` is the scale level of the multi-resolution pyramid. + +## setup attributes +Each `setup` group has attributes +``` +"downsamplingFactors" : [[1,1,1], [2,2,1], ...] +"dataType":"uint8" +``` +that specify downsampling scheme and datatype, which are the same for all timepoints. +`downsamplingFactors` specifies power-of-two downscaling factors for each scale level (with respect to full resolution `s0`, which always has `[1,1,1]`). +`dataType` is one of {uint8, uint16, uint32, uint64, int8, int16, int32, int64, float32, float64}. + +> TODO: Additional metadata (for example identifying `setup3` as channel 3, tile 124, etc.) could be replicated from the XML. +The idea would be that parts of a dataset can be used independent of BDV, without the XML. +We should agree on standard attributes for this. + +## timepoint attributes +`timepoint` has no mandatory attributes. + +For compatibility with Paintera, when exporting to N5 we put the `"multiScale" : true`, and `"resolution" : [x,y,z]` attributes. + +## scale level attributes +`s0`, `s1`, etc. have the mandatory N5 dataset attributes. + +For compatibility with Paintera, when exporting to N5 we put the `"downsamplingFactors": [x,y,z]` attribute. + +> TODO: Additional metadata (for example scaled resolution and affine transform) could be replicated from the XML. +The idea would be that an individual stack can be used independent of BDV, without the XML. +We should agree on standard attributes for this. diff --git a/LICENSE.txt b/LICENSE.txt index 5f511dfe6d816a873ca85ecd7fcb2df05eeddbc1..74eeded88037f7410a7d90acc14fc8d221f7c10b 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,5 +1,4 @@ -Copyright (c) 2012 - 2016, Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, -Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic +Copyright (c) 2012 - 2020, BigDataViewer developers. All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/pom.xml b/pom.xml index 3bcf22161bb8852cd056600496f0306a6ebff91e..b3fcafcc73d3d80e9061bb1bc1157e5b242ab1a6 100644 --- a/pom.xml +++ b/pom.xml @@ -5,13 +5,13 @@ <parent> <groupId>org.scijava</groupId> <artifactId>pom-scijava</artifactId> - <version>27.0.1</version> + <version>29.2.1</version> <relativePath /> </parent> <groupId>sc.fiji</groupId> <artifactId>bigdataviewer-core</artifactId> - <version>7.0.1-SNAPSHOT</version> + <version>10.0.3-SNAPSHOT</version> <name>BigDataViewer Core</name> <description>BigDataViewer core classes with minimal dependencies.</description> @@ -134,12 +134,9 @@ <package-name>bdv</package-name> <license.licenseName>bsd_2</license.licenseName> <license.copyrightOwners>BigDataViewer developers.</license.copyrightOwners> - <enforcer.skip>true</enforcer.skip> <!-- NB: Deploy releases to the SciJava Maven repository. --> <releaseProfiles>deploy-to-scijava</releaseProfiles> - - <imglib2-cache.version>1.0.0-beta-12</imglib2-cache.version> </properties> <repositories> @@ -162,10 +159,6 @@ <groupId>net.imglib2</groupId> <artifactId>imglib2-cache</artifactId> </dependency> - <dependency> - <groupId>net.imglib2</groupId> - <artifactId>imglib2-ui</artifactId> - </dependency> <dependency> <groupId>net.imglib2</groupId> <artifactId>imglib2-algorithm</artifactId> @@ -193,8 +186,20 @@ <dependency> <groupId>org.scijava</groupId> <artifactId>scijava-listeners</artifactId> - <version>1.0.0-beta-2</version> </dependency> + <dependency> + <groupId>org.janelia.saalfeldlab</groupId> + <artifactId>n5-imglib2</artifactId> + </dependency> + <dependency> + <groupId>org.janelia.saalfeldlab</groupId> + <artifactId>n5</artifactId> + </dependency> + <dependency> + <groupId>com.miglayout</groupId> + <artifactId>miglayout-swing</artifactId> + </dependency> + <!-- test dependencies --> <dependency> <groupId>junit</groupId> diff --git a/src/main/java/bdv/AbstractCachedViewerSetupImgLoader.java b/src/main/java/bdv/AbstractCachedViewerSetupImgLoader.java index 10a0fd1049bcae0e8d44af7d6825aa2475fbfdb9..4858bf872f429b9da1b6e504ab840edf0a0e11ff 100644 --- a/src/main/java/bdv/AbstractCachedViewerSetupImgLoader.java +++ b/src/main/java/bdv/AbstractCachedViewerSetupImgLoader.java @@ -1,3 +1,31 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package bdv; import bdv.img.cache.CacheArrayLoader; @@ -16,7 +44,7 @@ import net.imglib2.type.NativeType; /** * Abstract {@link ViewerSetupImgLoader} with a VolatileGlobalCellCache. * - * @author Stephan Saalfeld <saalfelds@janelia.hhmi.org> + * @author Stephan Saalfeld */ abstract public class AbstractCachedViewerSetupImgLoader< T extends NativeType< T > , V extends Volatile< T > & NativeType< V >, A extends VolatileAccess > extends AbstractViewerSetupImgLoader< T, V > diff --git a/src/main/java/bdv/AbstractSpimSource.java b/src/main/java/bdv/AbstractSpimSource.java index 7c5f7cd3a689bbdf377d20fe9771ef0ff066e098..2660b932d3a2823f22c3b60fdd0d64d2a951d726 100644 --- a/src/main/java/bdv/AbstractSpimSource.java +++ b/src/main/java/bdv/AbstractSpimSource.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/AbstractViewerSetupImgLoader.java b/src/main/java/bdv/AbstractViewerSetupImgLoader.java index 46550d3fa07cbb495eb7d671514208a3f42c909c..af7ce24dc5069c54528c11fe6eab895c7e93c0dc 100644 --- a/src/main/java/bdv/AbstractViewerSetupImgLoader.java +++ b/src/main/java/bdv/AbstractViewerSetupImgLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/BehaviourTransformEventHandler3D.java b/src/main/java/bdv/BehaviourTransformEventHandler3D.java deleted file mode 100644 index e0f363409e9c7eeb84bd0b7580c2d190a17d616d..0000000000000000000000000000000000000000 --- a/src/main/java/bdv/BehaviourTransformEventHandler3D.java +++ /dev/null @@ -1,498 +0,0 @@ -/*- - * #%L - * BigDataViewer core classes with minimal dependencies - * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic - * %% - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * #L% - */ -package bdv; - -import org.scijava.ui.behaviour.Behaviour; -import org.scijava.ui.behaviour.ClickBehaviour; -import org.scijava.ui.behaviour.DragBehaviour; -import org.scijava.ui.behaviour.ScrollBehaviour; -import org.scijava.ui.behaviour.io.InputTriggerConfig; -import org.scijava.ui.behaviour.util.Behaviours; -import org.scijava.ui.behaviour.util.TriggerBehaviourBindings; - -import net.imglib2.realtransform.AffineTransform3D; -import net.imglib2.ui.TransformEventHandler; -import net.imglib2.ui.TransformEventHandlerFactory; -import net.imglib2.ui.TransformListener; - -/** - * A {@link TransformEventHandler} that changes an {@link AffineTransform3D} - * through a set of {@link Behaviour}s. - * - * @author Stephan Saalfeld - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> - */ -public class BehaviourTransformEventHandler3D implements BehaviourTransformEventHandler< AffineTransform3D > -{ - public static TransformEventHandlerFactory< AffineTransform3D > factory() - { - return new BehaviourTransformEventHandler3DFactory(); - } - - public static class BehaviourTransformEventHandler3DFactory implements BehaviourTransformEventHandlerFactory< AffineTransform3D > - { - private InputTriggerConfig config = new InputTriggerConfig(); - - @Override - public void setConfig( final InputTriggerConfig config ) - { - this.config = config; - } - - @Override - public BehaviourTransformEventHandler3D create( final TransformListener< AffineTransform3D > transformListener ) - { - return new BehaviourTransformEventHandler3D( transformListener, config ); - } - } - - /** - * Current source to screen transform. - */ - protected final AffineTransform3D affine = new AffineTransform3D(); - - /** - * Whom to notify when the {@link #affine current transform} is changed. - */ - private TransformListener< AffineTransform3D > listener; - - /** - * Copy of {@link #affine current transform} when mouse dragging started. - */ - final protected AffineTransform3D affineDragStart = new AffineTransform3D(); - - /** - * Coordinates where mouse dragging started. - */ - protected double oX, oY; - - /** - * Current rotation axis for rotating with keyboard, indexed {@code x->0, y->1, - * z->2}. - */ - protected int axis = 0; - - /** - * The screen size of the canvas (the component displaying the image and - * generating mouse events). - */ - protected int canvasW = 1, canvasH = 1; - - /** - * Screen coordinates to keep centered while zooming or rotating with the - * keyboard. These are set to <em>(canvasW/2, canvasH/2)</em> - */ - protected int centerX = 0, centerY = 0; - - protected final Behaviours behaviours; - - public BehaviourTransformEventHandler3D( final TransformListener< AffineTransform3D > listener, final InputTriggerConfig config ) - { - this.listener = listener; - - final String DRAG_TRANSLATE = "drag translate"; - final String ZOOM_NORMAL = "scroll zoom"; - final String SELECT_AXIS_X = "axis x"; - final String SELECT_AXIS_Y = "axis y"; - final String SELECT_AXIS_Z = "axis z"; - - final double[] speed = { 1.0, 10.0, 0.1 }; - final String[] SPEED_NAME = { "", " fast", " slow" }; - final String[] speedMod = { "", "shift ", "ctrl " }; - - final String DRAG_ROTATE = "drag rotate"; - final String SCROLL_Z = "scroll browse z"; - final String ROTATE_LEFT = "rotate left"; - final String ROTATE_RIGHT = "rotate right"; - final String KEY_ZOOM_IN = "zoom in"; - final String KEY_ZOOM_OUT = "zoom out"; - final String KEY_FORWARD_Z = "forward z"; - final String KEY_BACKWARD_Z = "backward z"; - - behaviours = new Behaviours( config, "bdv" ); - - behaviours.behaviour( new TranslateXY(), DRAG_TRANSLATE, "button2", "button3" ); - behaviours.behaviour( new Zoom( speed[ 0 ] ), ZOOM_NORMAL, "meta scroll", "ctrl shift scroll" ); - behaviours.behaviour( new SelectRotationAxis( 0 ), SELECT_AXIS_X, "X" ); - behaviours.behaviour( new SelectRotationAxis( 1 ), SELECT_AXIS_Y, "Y" ); - behaviours.behaviour( new SelectRotationAxis( 2 ), SELECT_AXIS_Z, "Z" ); - - for ( int s = 0; s < 3; ++s ) - { - behaviours.behaviour( new Rotate( speed[ s ] ), DRAG_ROTATE + SPEED_NAME[ s ], speedMod[ s ] + "button1" ); - behaviours.behaviour( new TranslateZ( speed[ s ] ), SCROLL_Z + SPEED_NAME[ s ], speedMod[ s ] + "scroll" ); - behaviours.behaviour( new KeyRotate( speed[ s ] ), ROTATE_LEFT + SPEED_NAME[ s ], speedMod[ s ] + "LEFT" ); - behaviours.behaviour( new KeyRotate( -speed[ s ] ), ROTATE_RIGHT + SPEED_NAME[ s ], speedMod[ s ] + "RIGHT" ); - behaviours.behaviour( new KeyZoom( speed[ s ] ), KEY_ZOOM_IN + SPEED_NAME[ s ], speedMod[ s ] + "UP" ); - behaviours.behaviour( new KeyZoom( -speed[ s ] ), KEY_ZOOM_OUT + SPEED_NAME[ s ], speedMod[ s ] + "DOWN" ); - behaviours.behaviour( new KeyTranslateZ( speed[ s ] ), KEY_FORWARD_Z + SPEED_NAME[ s ], speedMod[ s ] + "COMMA" ); - behaviours.behaviour( new KeyTranslateZ( -speed[ s ] ), KEY_BACKWARD_Z + SPEED_NAME[ s ], speedMod[ s ] + "PERIOD" ); - } - } - - @Override - public void install( final TriggerBehaviourBindings bindings ) - { - behaviours.install( bindings, "transform" ); - } - - @Override - public AffineTransform3D getTransform() - { - synchronized ( affine ) - { - return affine.copy(); - } - } - - @Override - public void setTransform( final AffineTransform3D transform ) - { - synchronized ( affine ) - { - affine.set( transform ); - } - } - - @Override - public void setCanvasSize( final int width, final int height, final boolean updateTransform ) - { - if ( width == 0 || height == 0 ) { - // NB: We are probably in some intermediate layout scenario. - // Attempting to trigger a transform update with 0 size will result - // in the exception "Matrix is singular" from imglib2-realtrasform. - return; - } - if ( updateTransform ) - { - synchronized ( affine ) - { - affine.set( affine.get( 0, 3 ) - canvasW / 2, 0, 3 ); - affine.set( affine.get( 1, 3 ) - canvasH / 2, 1, 3 ); - affine.scale( ( double ) width / canvasW ); - affine.set( affine.get( 0, 3 ) + width / 2, 0, 3 ); - affine.set( affine.get( 1, 3 ) + height / 2, 1, 3 ); - notifyListener(); - } - } - canvasW = width; - canvasH = height; - centerX = width / 2; - centerY = height / 2; - } - - @Override - public void setTransformListener( final TransformListener< AffineTransform3D > transformListener ) - { - listener = transformListener; - } - - @Override - public String getHelpString() - { - return helpString; - } - - /** - * notifies {@link #listener} that the current transform changed. - */ - private void notifyListener() - { - if ( listener != null ) - listener.transformChanged( affine ); - } - - /** - * One step of rotation (radian). - */ - final protected static double step = Math.PI / 180; - - final protected static String NL = System.getProperty( "line.separator" ); - - final protected static String helpString = - "Mouse control:" + NL + " " + NL + - "Pan and tilt the volume by left-click and dragging the image in the canvas, " + NL + - "move the volume by middle-or-right-click and dragging the image in the canvas, " + NL + - "browse alongside the z-axis using the mouse-wheel, and" + NL + - "zoom in and out using the mouse-wheel holding CTRL+SHIFT or META." + NL + " " + NL + - "Key control:" + NL + " " + NL + - "X - Select x-axis as rotation axis." + NL + - "Y - Select y-axis as rotation axis." + NL + - "Z - Select z-axis as rotation axis." + NL + - "CURSOR LEFT - Rotate clockwise around the choosen rotation axis." + NL + - "CURSOR RIGHT - Rotate counter-clockwise around the choosen rotation axis." + NL + - "CURSOR UP - Zoom in." + NL + - "CURSOR DOWN - Zoom out." + NL + - "./> - Forward alongside z-axis." + NL + - ",/< - Backward alongside z-axis." + NL + - "SHIFT - Rotate and browse 10x faster." + NL + - "CTRL - Rotate and browse 10x slower."; - - private void scale( final double s, final double x, final double y ) - { - // center shift - affine.set( affine.get( 0, 3 ) - x, 0, 3 ); - affine.set( affine.get( 1, 3 ) - y, 1, 3 ); - - // scale - affine.scale( s ); - - // center un-shift - affine.set( affine.get( 0, 3 ) + x, 0, 3 ); - affine.set( affine.get( 1, 3 ) + y, 1, 3 ); - } - - /** - * Rotate by d radians around axis. Keep screen coordinates ( - * {@link #centerX}, {@link #centerY}) fixed. - */ - private void rotate( final int axis, final double d ) - { - // center shift - affine.set( affine.get( 0, 3 ) - centerX, 0, 3 ); - affine.set( affine.get( 1, 3 ) - centerY, 1, 3 ); - - // rotate - affine.rotate( axis, d ); - - // center un-shift - affine.set( affine.get( 0, 3 ) + centerX, 0, 3 ); - affine.set( affine.get( 1, 3 ) + centerY, 1, 3 ); - } - - private class Rotate implements DragBehaviour - { - private final double speed; - - public Rotate( final double speed ) - { - this.speed = speed; - } - - @Override - public void init( final int x, final int y ) - { - synchronized ( affine ) - { - oX = x; - oY = y; - affineDragStart.set( affine ); - } - } - - @Override - public void drag( final int x, final int y ) - { - synchronized ( affine ) - { - final double dX = oX - x; - final double dY = oY - y; - - affine.set( affineDragStart ); - - // center shift - affine.set( affine.get( 0, 3 ) - oX, 0, 3 ); - affine.set( affine.get( 1, 3 ) - oY, 1, 3 ); - - final double v = step * speed; - affine.rotate( 0, -dY * v ); - affine.rotate( 1, dX * v ); - - // center un-shift - affine.set( affine.get( 0, 3 ) + oX, 0, 3 ); - affine.set( affine.get( 1, 3 ) + oY, 1, 3 ); - notifyListener(); - } - } - - @Override - public void end( final int x, final int y ) - {} - } - - private class TranslateXY implements DragBehaviour - { - @Override - public void init( final int x, final int y ) - { - synchronized ( affine ) - { - oX = x; - oY = y; - affineDragStart.set( affine ); - } - } - - @Override - public void drag( final int x, final int y ) - { - synchronized ( affine ) - { - final double dX = oX - x; - final double dY = oY - y; - - affine.set( affineDragStart ); - affine.set( affine.get( 0, 3 ) - dX, 0, 3 ); - affine.set( affine.get( 1, 3 ) - dY, 1, 3 ); - notifyListener(); - } - } - - @Override - public void end( final int x, final int y ) - {} - } - - private class TranslateZ implements ScrollBehaviour - { - private final double speed; - - public TranslateZ( final double speed ) - { - this.speed = speed; - } - - @Override - public void scroll( final double wheelRotation, final boolean isHorizontal, final int x, final int y ) - { - synchronized ( affine ) - { - final double dZ = speed * -wheelRotation; - // TODO (optionally) correct for zoom - affine.set( affine.get( 2, 3 ) - dZ, 2, 3 ); - notifyListener(); - } - } - } - - private class Zoom implements ScrollBehaviour - { - private final double speed; - - public Zoom( final double speed ) - { - this.speed = speed; - } - - @Override - public void scroll( final double wheelRotation, final boolean isHorizontal, final int x, final int y ) - { - synchronized ( affine ) - { - final double s = speed * wheelRotation; - final double dScale = 1.0 + 0.05; - if ( s > 0 ) - scale( 1.0 / dScale, x, y ); - else - scale( dScale, x, y ); - notifyListener(); - } - } - } - - private class SelectRotationAxis implements ClickBehaviour - { - private final int axis; - - public SelectRotationAxis( final int axis ) - { - this.axis = axis; - } - - @Override - public void click( final int x, final int y ) - { - BehaviourTransformEventHandler3D.this.axis = axis; - } - } - - private class KeyRotate implements ClickBehaviour - { - private final double speed; - - public KeyRotate( final double speed ) - { - this.speed = speed; - } - - @Override - public void click( final int x, final int y ) - { - synchronized ( affine ) - { - rotate( axis, step * speed ); - notifyListener(); - } - } - } - - private class KeyZoom implements ClickBehaviour - { - private final double dScale; - - public KeyZoom( final double speed ) - { - if ( speed > 0 ) - dScale = 1.0 + 0.1 * speed; - else - dScale = 1.0 / ( 1.0 - 0.1 * speed ); - } - - @Override - public void click( final int x, final int y ) - { - synchronized ( affine ) - { - scale( dScale, centerX, centerY ); - notifyListener(); - } - } - } - - private class KeyTranslateZ implements ClickBehaviour - { - private final double speed; - - public KeyTranslateZ( final double speed ) - { - this.speed = speed; - } - - @Override - public void click( final int x, final int y ) - { - synchronized ( affine ) - { - affine.set( affine.get( 2, 3 ) + speed, 2, 3 ); - notifyListener(); - } - } - } -} diff --git a/src/main/java/bdv/BigDataViewer.java b/src/main/java/bdv/BigDataViewer.java index e1f6a331043dccd8f8eb5a0db74af91b6d29fc45..2756ebff4497eb0a76a7500a94c50651bc83e13d 100644 --- a/src/main/java/bdv/BigDataViewer.java +++ b/src/main/java/bdv/BigDataViewer.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -90,358 +89,390 @@ public class BigDataViewer { protected final Bookmarks bookmarks; - protected final BrightnessDialog brightnessDialog; - protected final CropDialog cropDialog; protected final RecordMovieDialog movieDialog; - protected final RecordMaxProjectionDialog movieMaxProjectDialog; - - protected final VisibilityAndGroupingDialog activeSourcesDialog; - - protected final HelpDialog helpDialog; - - protected final ManualTransformationEditor manualTransformationEditor; - - protected final BookmarksEditor bookmarkEditor; - - protected final JFileChooser fileChooser; - - protected File proposedSettingsFile; - - public void toggleManualTransformation() { - manualTransformationEditor.toggle(); - } - - public void initSetBookmark() { - bookmarkEditor.initSetBookmark(); - } - - public void initGoToBookmark() { - bookmarkEditor.initGoToBookmark(); - } - - public void initGoToBookmarkRotation() { - bookmarkEditor.initGoToBookmarkRotation(); - } - - private static String createSetupName(final BasicViewSetup setup) { - if (setup.hasName()) - return setup.getName(); - - String name = ""; - - final Angle angle = setup.getAttribute(Angle.class); - if (angle != null) - name += (name.isEmpty() ? "" : " ") + "a " + angle.getName(); - - final Channel channel = setup.getAttribute(Channel.class); - if (channel != null) - name += (name.isEmpty() ? "" : " ") + "c " + channel.getName(); - - return name; - } - - /** - * Create standard converter from the given {@code type} to ARGB: - * <ul> - * <li>For {@code RealType}s a {@link RealARGBColorConverter} is - * returned.</li> - * <li>For {@code ARGBType}s a {@link ScaledARGBConverter.ARGB} is - * returned.</li> - * <li>For {@code VolatileARGBType}s a - * {@link ScaledARGBConverter.VolatileARGB} is returned.</li> - * </ul> - */ - @SuppressWarnings("unchecked") - public static <T extends NumericType<T>> Converter<T, ARGBType> createConverterToARGB(final T type) { - if (type instanceof RealType) { - final RealType<?> t = (RealType<?>) type; - final double typeMin = Math.max(0, Math.min(t.getMinValue(), 65535)); - final double typeMax = Math.max(0, Math.min(t.getMaxValue(), 65535)); - return (Converter<T, ARGBType>) RealARGBColorConverter.create(t, typeMin, typeMax); - } else if (type instanceof ARGBType) - return (Converter<T, ARGBType>) new ScaledARGBConverter.ARGB(0, 255); - else if (type instanceof VolatileARGBType) - return (Converter<T, ARGBType>) new ScaledARGBConverter.VolatileARGB(0, 255); - else - throw new IllegalArgumentException("ImgLoader of type " + type.getClass() + " not supported."); - } - - /** - * Create a {@code ConverterSetup} for the given {@code SourceAndConverter}. - * {@link SourceAndConverter#asVolatile() Nested volatile} - * {@code SourceAndConverter} are added to the {@code ConverterSetup} if - * present. If {@code SourceAndConverter} does not comprise a - * {@code ColorConverter}, returns {@code null}. - * - * @param soc {@code SourceAndConverter} for which to create a - * {@code ConverterSetup} - * @param setupId setupId of the created {@code ConverterSetup} - * @return a new {@code ConverterSetup} or {@code null} - */ - public static ConverterSetup createConverterSetup(final SourceAndConverter<?> soc, final int setupId) { - final List<ColorConverter> converters = new ArrayList<>(); - - final Converter<?, ARGBType> c = soc.getConverter(); - if (c instanceof ColorConverter) - converters.add((ColorConverter) c); - - final SourceAndConverter<? extends Volatile<?>> vsoc = soc.asVolatile(); - if (vsoc != null) { - final Converter<?, ARGBType> vc = vsoc.getConverter(); - if (vc instanceof ColorConverter) - converters.add((ColorConverter) vc); - } - - if (converters.isEmpty()) - return null; - else - return new RealARGBColorConverterSetup(setupId, converters); - } - - /** - * Decorate source with an extra transformation, that can be edited manually - * in this viewer. {@link SourceAndConverter#asVolatile() Nested volatile} - * {@code SourceAndConverter} are wrapped as well, if present. - */ - public static <T, V extends Volatile<T>> SourceAndConverter<T> wrapWithTransformedSource(final SourceAndConverter<T> soc) { - if (soc.asVolatile() == null) - return new SourceAndConverter<>(new TransformedSource<>(soc.getSpimSource()), soc.getConverter()); - - @SuppressWarnings("unchecked") final SourceAndConverter<V> vsoc = (SourceAndConverter<V>) soc.asVolatile(); - final TransformedSource<T> ts = new TransformedSource<>(soc.getSpimSource()); - final TransformedSource<V> vts = new TransformedSource<>(vsoc.getSpimSource(), ts); - return new SourceAndConverter<>(ts, soc.getConverter(), new SourceAndConverter<>(vts, vsoc.getConverter())); - } - - private static <T extends NumericType<T>, V extends Volatile<T> & NumericType<V>> void initSetupNumericType( - final AbstractSpimData<?> spimData, - final BasicViewSetup setup, - final List<ConverterSetup> converterSetups, - final List<SourceAndConverter<?>> sources) { - final int setupId = setup.getId(); - final ViewerImgLoader imgLoader = (ViewerImgLoader) spimData.getSequenceDescription().getImgLoader(); - @SuppressWarnings("unchecked") final ViewerSetupImgLoader<T, V> setupImgLoader = - (ViewerSetupImgLoader<T, V>) imgLoader.getSetupImgLoader( - setupId); - final T type = setupImgLoader.getImageType(); - final V volatileType = setupImgLoader.getVolatileImageType(); - - if (!(type instanceof NumericType)) - throw new IllegalArgumentException("ImgLoader of type " + type.getClass() + " not supported."); - - final String setupName = createSetupName(setup); - - SourceAndConverter<V> vsoc = null; - if (volatileType != null) { - final VolatileSpimSource<V> vs = new VolatileSpimSource<>(spimData, setupId, setupName); - vsoc = new SourceAndConverter<>(vs, createConverterToARGB(volatileType)); - } - - final SpimSource<T> s = new SpimSource<>(spimData, setupId, setupName); - final SourceAndConverter<T> soc = new SourceAndConverter<>(s, createConverterToARGB(type), vsoc); - final SourceAndConverter<T> tsoc = wrapWithTransformedSource(soc); - sources.add(tsoc); - - final ConverterSetup converterSetup = createConverterSetup(tsoc, setupId); - if (converterSetup != null) - converterSetups.add(converterSetup); - } - - public static void initSetups( - final AbstractSpimData<?> spimData, - final List<ConverterSetup> converterSetups, - final List<SourceAndConverter<?>> sources) { - for (final BasicViewSetup setup : spimData.getSequenceDescription().getViewSetupsOrdered()) - initSetupNumericType(spimData, setup, converterSetups, sources); - } - - /** - * @param converterSetups list of {@link ConverterSetup} that control min/max and color - * of sources. - * @param sources list of pairs of source of some type and converter from that - * type to ARGB. - * @param spimData may be null. The {@link AbstractSpimData} of the dataset (if - * there is one). If it exists, it is used to set up a "Crop" - * dialog. - * @param numTimepoints the number of timepoints in the dataset. - * @param cache handle to cache. This is used to control io timing. - * @param windowTitle title of the viewer window. - * @param progressWriter a {@link ProgressWriter} to which BDV may report progress - * (currently only used in the "Record Movie" dialog). - * @param options optional parameters. - */ - public BigDataViewer( - final ArrayList<ConverterSetup> converterSetups, - final ArrayList<SourceAndConverter<?>> sources, - final AbstractSpimData<?> spimData, - final int numTimepoints, - final CacheControl cache, - final String windowTitle, - final ProgressWriter progressWriter, - final ViewerOptions options) { - final InputTriggerConfig inputTriggerConfig = getInputTriggerConfig(options); - if (options.values.getTransformEventHandlerFactory() instanceof BehaviourTransformEventHandlerFactory) - ((BehaviourTransformEventHandlerFactory<?>) options.values.getTransformEventHandlerFactory()).setConfig( - inputTriggerConfig); - - viewerFrame = new ViewerFrame(sources, numTimepoints, cache, options); - if (windowTitle != null) - viewerFrame.setTitle(windowTitle); - viewer = viewerFrame.getViewerPanel(); - - for (final ConverterSetup cs : converterSetups) - cs.setViewer(viewer); - - manualTransformation = new ManualTransformation(viewer); - manualTransformationEditor = new ManualTransformationEditor(viewer, viewerFrame.getKeybindings()); - - bookmarks = new Bookmarks(); - bookmarkEditor = new BookmarksEditor(viewer, viewerFrame.getKeybindings(), bookmarks); - - setupAssignments = new SetupAssignments(converterSetups, 0, 65535); - if (setupAssignments.getMinMaxGroups().size() > 0) { - final MinMaxGroup group = setupAssignments.getMinMaxGroups().get(0); - for (final ConverterSetup setup : setupAssignments.getConverterSetups()) - setupAssignments.moveSetupToGroup(setup, group); - } - - brightnessDialog = new BrightnessDialog(viewerFrame, setupAssignments); - - if (spimData != null) - viewer.getSourceInfoOverlayRenderer().setTimePointsOrdered(spimData.getSequenceDescription().getTimePoints().getTimePointsOrdered()); - - cropDialog = (spimData == null) ? null : new CropDialog(viewerFrame, viewer, spimData.getSequenceDescription()); - - movieDialog = new RecordMovieDialog(viewerFrame, viewer, progressWriter); - // this is just to get updates of window size: - viewer.getDisplay().addOverlayRenderer(movieDialog); - - movieMaxProjectDialog = new RecordMaxProjectionDialog(viewerFrame, viewer, progressWriter); - // this is just to get updates of window size: - viewer.getDisplay().addOverlayRenderer(movieMaxProjectDialog); - - activeSourcesDialog = new VisibilityAndGroupingDialog(viewerFrame, viewer.getVisibilityAndGrouping()); - - helpDialog = new HelpDialog(viewerFrame); - - fileChooser = new JFileChooser(); - fileChooser.setFileFilter(new FileFilter() { - @Override - public String getDescription() { - return "xml files"; - } - - @Override - public boolean accept(final File f) { - if (f.isDirectory()) - return true; - if (f.isFile()) { - final String s = f.getName(); - final int i = s.lastIndexOf('.'); - if (i > 0 && i < s.length() - 1) { - final String ext = s.substring(i + 1).toLowerCase(); - return ext.equals("xml"); - } - } - return false; - } - }); - - NavigationActions.installActionBindings(viewerFrame.getKeybindings(), viewer, inputTriggerConfig); - BigDataViewerActions.installActionBindings(viewerFrame.getKeybindings(), this, inputTriggerConfig); - - final JMenuBar menubar = new JMenuBar(); - JMenu menu = new JMenu("File"); - menubar.add(menu); - - final ActionMap actionMap = viewerFrame.getKeybindings().getConcatenatedActionMap(); - final JMenuItem miLoadSettings = new JMenuItem(actionMap.get(BigDataViewerActions.LOAD_SETTINGS)); - miLoadSettings.setText("Load settings"); - menu.add(miLoadSettings); - - final JMenuItem miSaveSettings = new JMenuItem(actionMap.get(BigDataViewerActions.SAVE_SETTINGS)); - miSaveSettings.setText("Save settings"); - menu.add(miSaveSettings); - - menu = new JMenu("Settings"); - menubar.add(menu); - - final JMenuItem miBrightness = new JMenuItem(actionMap.get(BigDataViewerActions.BRIGHTNESS_SETTINGS)); - miBrightness.setText("Brightness & Color"); - menu.add(miBrightness); - - final JMenuItem miVisibility = new JMenuItem(actionMap.get(BigDataViewerActions.VISIBILITY_AND_GROUPING)); - miVisibility.setText("Visibility & Grouping"); - menu.add(miVisibility); - - menu = new JMenu("Tools"); - menubar.add(menu); - - if (cropDialog != null) { - final JMenuItem miCrop = new JMenuItem(actionMap.get(BigDataViewerActions.CROP)); - miCrop.setText("Crop"); - menu.add(miCrop); - } - - final JMenuItem miMovie = new JMenuItem(actionMap.get(BigDataViewerActions.RECORD_MOVIE)); - miMovie.setText("Record Movie"); - menu.add(miMovie); - - final JMenuItem miMaxProjectMovie = - new JMenuItem(actionMap.get(BigDataViewerActions.RECORD_MAX_PROJECTION_MOVIE)); - miMaxProjectMovie.setText("Record Max-Projection Movie"); - menu.add(miMaxProjectMovie); - - final JMenuItem miManualTransform = new JMenuItem(actionMap.get(BigDataViewerActions.MANUAL_TRANSFORM)); - miManualTransform.setText("Manual Transform"); - menu.add(miManualTransform); - - menu = new JMenu("Help"); - menubar.add(menu); - - final JMenuItem miHelp = new JMenuItem(actionMap.get(BigDataViewerActions.SHOW_HELP)); - miHelp.setText("Show Help"); - menu.add(miHelp); - - viewerFrame.setJMenuBar(menubar); - } - - public static BigDataViewer open(final AbstractSpimData<?> spimData, - final String windowTitle, - final ProgressWriter progressWriter, - final ViewerOptions options) { - if (WrapBasicImgLoader.wrapImgLoaderIfNecessary(spimData)) { - System.err.println( - "WARNING:\nOpening <SpimData> dataset that is not suited for interactive browsing.\nConsider " + - "resaving as HDF5 for better performance."); - } - - final ArrayList<ConverterSetup> converterSetups = new ArrayList<>(); - final ArrayList<SourceAndConverter<?>> sources = new ArrayList<>(); - initSetups(spimData, converterSetups, sources); - - final AbstractSequenceDescription<?, ?, ?> seq = spimData.getSequenceDescription(); - final int numTimepoints = seq.getTimePoints().size(); - final CacheControl cache = ((ViewerImgLoader) seq.getImgLoader()).getCacheControl(); - - final BigDataViewer bdv = new BigDataViewer(converterSetups, - sources, - spimData, - numTimepoints, - cache, - windowTitle, - progressWriter, - options); - - WrapBasicImgLoader.removeWrapperIfPresent(spimData); - - bdv.viewerFrame.setVisible(true); - InitializeViewerState.initTransform(bdv.viewer); - return bdv; - } + protected final BrightnessDialog brightnessDialog; + + protected final RecordMaxProjectionDialog movieMaxProjectDialog; + + protected final VisibilityAndGroupingDialog activeSourcesDialog; + + protected final HelpDialog helpDialog; + + protected final ManualTransformationEditor manualTransformationEditor; + + protected final BookmarksEditor bookmarkEditor; + + protected final JFileChooser fileChooser; + + protected File proposedSettingsFile; + + public void toggleManualTransformation() + { + manualTransformationEditor.toggle(); + } + + public void initSetBookmark() + { + bookmarkEditor.initSetBookmark(); + } + + public void initGoToBookmark() + { + bookmarkEditor.initGoToBookmark(); + } + + public void initGoToBookmarkRotation() + { + bookmarkEditor.initGoToBookmarkRotation(); + } + + private static String createSetupName( final BasicViewSetup setup ) + { + if ( setup.hasName() ) + return setup.getName(); + + String name = ""; + + final Angle angle = setup.getAttribute( Angle.class ); + if ( angle != null ) + name += ( name.isEmpty() ? "" : " " ) + "a " + angle.getName(); + + final Channel channel = setup.getAttribute( Channel.class ); + if ( channel != null ) + name += ( name.isEmpty() ? "" : " " ) + "c " + channel.getName(); + + return name; + } + + /** + * Create standard converter from the given {@code type} to ARGB: + * <ul> + * <li>For {@code RealType}s a {@link RealARGBColorConverter} is + * returned.</li> + * <li>For {@code ARGBType}s a {@link ScaledARGBConverter.ARGB} is + * returned.</li> + * <li>For {@code VolatileARGBType}s a + * {@link ScaledARGBConverter.VolatileARGB} is returned.</li> + * </ul> + */ + @SuppressWarnings( "unchecked" ) + public static < T extends NumericType< T > > Converter< T, ARGBType > createConverterToARGB( final T type ) + { + if ( type instanceof RealType ) + { + final RealType< ? > t = ( RealType< ? > ) type; + final double typeMin = Math.max( 0, Math.min( t.getMinValue(), 65535 ) ); + final double typeMax = Math.max( 0, Math.min( t.getMaxValue(), 65535 ) ); + return ( Converter< T, ARGBType > ) RealARGBColorConverter.create( t, typeMin, typeMax ); + } + else if ( type instanceof ARGBType ) + return ( Converter< T, ARGBType > ) new ScaledARGBConverter.ARGB( 0, 255 ); + else if ( type instanceof VolatileARGBType ) + return ( Converter< T, ARGBType > ) new ScaledARGBConverter.VolatileARGB( 0, 255 ); + else + throw new IllegalArgumentException( "ImgLoader of type " + type.getClass() + " not supported." ); + } + + /** + * Create a {@code ConverterSetup} for the given {@code SourceAndConverter}. + * {@link SourceAndConverter#asVolatile() Nested volatile} + * {@code SourceAndConverter} are added to the {@code ConverterSetup} if + * present. If {@code SourceAndConverter} does not comprise a + * {@code ColorConverter}, returns {@code null}. + * + * @param soc + * {@code SourceAndConverter} for which to create a + * {@code ConverterSetup} + * @param setupId + * setupId of the created {@code ConverterSetup} + * @return a new {@code ConverterSetup} or {@code null} + */ + public static ConverterSetup createConverterSetup( final SourceAndConverter< ? > soc, final int setupId ) + { + final List< ColorConverter > converters = new ArrayList<>(); + + final Converter< ?, ARGBType > c = soc.getConverter(); + if ( c instanceof ColorConverter ) + converters.add( ( ColorConverter ) c ); + + final SourceAndConverter< ? extends Volatile< ? > > vsoc = soc.asVolatile(); + if ( vsoc != null ) + { + final Converter< ?, ARGBType > vc = vsoc.getConverter(); + if ( vc instanceof ColorConverter ) + converters.add( ( ColorConverter ) vc ); + } + + if ( converters.isEmpty() ) + return null; + else + return new RealARGBColorConverterSetup( setupId, converters ); + } + + /** + * Decorate source with an extra transformation, that can be edited manually + * in this viewer. {@link SourceAndConverter#asVolatile() Nested volatile} + * {@code SourceAndConverter} are wrapped as well, if present. + */ + public static < T, V extends Volatile< T > > SourceAndConverter< T > wrapWithTransformedSource( final SourceAndConverter< T > soc ) + { + if ( soc.asVolatile() == null ) + return new SourceAndConverter<>( new TransformedSource<>( soc.getSpimSource() ), soc.getConverter() ); + + @SuppressWarnings( "unchecked" ) + final SourceAndConverter< V > vsoc = ( SourceAndConverter< V > ) soc.asVolatile(); + final TransformedSource< T > ts = new TransformedSource<>( soc.getSpimSource() ); + final TransformedSource< V > vts = new TransformedSource<>( vsoc.getSpimSource(), ts ); + return new SourceAndConverter<>( ts, soc.getConverter(), new SourceAndConverter<>( vts, vsoc.getConverter() ) ); + } + + private static < T extends NumericType< T >, V extends Volatile< T > & NumericType< V > > void initSetupNumericType( + final AbstractSpimData< ? > spimData, + final BasicViewSetup setup, + final List< ConverterSetup > converterSetups, + final List< SourceAndConverter< ? > > sources ) + { + final int setupId = setup.getId(); + final ViewerImgLoader imgLoader = ( ViewerImgLoader ) spimData.getSequenceDescription().getImgLoader(); + @SuppressWarnings( "unchecked" ) + final ViewerSetupImgLoader< T, V > setupImgLoader = ( ViewerSetupImgLoader< T, V > ) imgLoader.getSetupImgLoader( setupId ); + final T type = setupImgLoader.getImageType(); + final V volatileType = setupImgLoader.getVolatileImageType(); + + if ( ! ( type instanceof NumericType ) ) + throw new IllegalArgumentException( "ImgLoader of type " + type.getClass() + " not supported." ); + + final String setupName = createSetupName( setup ); + + SourceAndConverter< V > vsoc = null; + if ( volatileType != null ) + { + final VolatileSpimSource< V > vs = new VolatileSpimSource<>( spimData, setupId, setupName ); + vsoc = new SourceAndConverter<>( vs, createConverterToARGB( volatileType ) ); + } + + final SpimSource< T > s = new SpimSource<>( spimData, setupId, setupName ); + final SourceAndConverter< T > soc = new SourceAndConverter<>( s, createConverterToARGB( type ), vsoc ); + final SourceAndConverter< T > tsoc = wrapWithTransformedSource( soc ); + sources.add( tsoc ); + + final ConverterSetup converterSetup = createConverterSetup( tsoc, setupId ); + if ( converterSetup != null ) + converterSetups.add( converterSetup ); + } + + public static void initSetups( + final AbstractSpimData< ? > spimData, + final List< ConverterSetup > converterSetups, + final List< SourceAndConverter< ? > > sources ) + { + for ( final BasicViewSetup setup : spimData.getSequenceDescription().getViewSetupsOrdered() ) + initSetupNumericType( spimData, setup, converterSetups, sources ); + } + + /** + * + * @param converterSetups + * list of {@link ConverterSetup} that control min/max and color + * of sources. + * @param sources + * list of pairs of source of some type and converter from that + * type to ARGB. + * @param spimData + * may be null. The {@link AbstractSpimData} of the dataset (if + * there is one). If it exists, it is used to set up a "Crop" + * dialog. + * @param numTimepoints + * the number of timepoints in the dataset. + * @param cache + * handle to cache. This is used to control io timing. + * @param windowTitle + * title of the viewer window. + * @param progressWriter + * a {@link ProgressWriter} to which BDV may report progress + * (currently only used in the "Record Movie" dialog). + * @param options + * optional parameters. + */ + public BigDataViewer( + final ArrayList< ConverterSetup > converterSetups, + final ArrayList< SourceAndConverter< ? > > sources, + final AbstractSpimData< ? > spimData, + final int numTimepoints, + final CacheControl cache, + final String windowTitle, + final ProgressWriter progressWriter, + final ViewerOptions options ) + { + final InputTriggerConfig inputTriggerConfig = getInputTriggerConfig( options ); + + viewerFrame = new ViewerFrame( sources, numTimepoints, cache, options.inputTriggerConfig( inputTriggerConfig ) ); + if ( windowTitle != null ) + viewerFrame.setTitle( windowTitle ); + viewer = viewerFrame.getViewerPanel(); + +// final ConverterSetup.SetupChangeListener requestRepaint = s -> viewer.requestRepaint(); +// for ( final ConverterSetup cs : converterSetups ) +// cs.setupChangeListeners().add( requestRepaint ); + + manualTransformation = new ManualTransformation( viewer ); + manualTransformationEditor = new ManualTransformationEditor( viewer, viewerFrame.getKeybindings() ); + + bookmarks = new Bookmarks(); + bookmarkEditor = new BookmarksEditor( viewer, viewerFrame.getKeybindings(), bookmarks ); + + final ConverterSetups setups = viewerFrame.getConverterSetups(); + if ( converterSetups.size() != sources.size() ) + System.err.println( "WARNING! Constructing BigDataViewer, with converterSetups.size() that is not the same as sources.size()." ); + final int numSetups = Math.min( converterSetups.size(), sources.size() ); + for ( int i = 0; i < numSetups; ++i ) + { + final SourceAndConverter< ? > source = sources.get( i ); + final ConverterSetup setup = converterSetups.get( i ); + if ( setup != null ) + setups.put( source, setup ); + } + + setupAssignments = new SetupAssignments( converterSetups, 0, 65535 ); + if ( setupAssignments.getMinMaxGroups().size() > 0 ) + { + final MinMaxGroup group = setupAssignments.getMinMaxGroups().get( 0 ); + for ( final ConverterSetup setup : setupAssignments.getConverterSetups() ) + setupAssignments.moveSetupToGroup( setup, group ); + } + + brightnessDialog = new BrightnessDialog( viewerFrame, setupAssignments ); + + if (spimData != null ) + viewer.getSourceInfoOverlayRenderer().setTimePointsOrdered( spimData.getSequenceDescription().getTimePoints().getTimePointsOrdered() ); + + cropDialog = ( spimData == null ) ? null : new CropDialog( viewerFrame, viewer, spimData.getSequenceDescription() ); + + movieDialog = new RecordMovieDialog( viewerFrame, viewer, progressWriter ); + // this is just to get updates of window size: + viewer.getDisplay().overlays().add( movieDialog ); + + movieMaxProjectDialog = new RecordMaxProjectionDialog( viewerFrame, viewer, progressWriter ); + // this is just to get updates of window size: + viewer.getDisplay().overlays().add( movieMaxProjectDialog ); + + activeSourcesDialog = new VisibilityAndGroupingDialog( viewerFrame, viewer.state() ); + + helpDialog = new HelpDialog( viewerFrame ); + + fileChooser = new JFileChooser(); + fileChooser.setFileFilter( new FileFilter() + { + @Override + public String getDescription() + { + return "xml files"; + } + + @Override + public boolean accept( final File f ) + { + if ( f.isDirectory() ) + return true; + if ( f.isFile() ) + { + final String s = f.getName(); + final int i = s.lastIndexOf( '.' ); + if ( i > 0 && i < s.length() - 1 ) + { + final String ext = s.substring( i + 1 ).toLowerCase(); + return ext.equals( "xml" ); + } + } + return false; + } + } ); + + NavigationActions.installActionBindings( viewerFrame.getKeybindings(), viewer, inputTriggerConfig ); + BigDataViewerActions.installActionBindings( viewerFrame.getKeybindings(), this, inputTriggerConfig ); + + final JMenuBar menubar = new JMenuBar(); + JMenu menu = new JMenu( "File" ); + menubar.add( menu ); + + final ActionMap actionMap = viewerFrame.getKeybindings().getConcatenatedActionMap(); + final JMenuItem miLoadSettings = new JMenuItem( actionMap.get( BigDataViewerActions.LOAD_SETTINGS ) ); + miLoadSettings.setText( "Load settings" ); + menu.add( miLoadSettings ); + + final JMenuItem miSaveSettings = new JMenuItem( actionMap.get( BigDataViewerActions.SAVE_SETTINGS ) ); + miSaveSettings.setText( "Save settings" ); + menu.add( miSaveSettings ); + + menu = new JMenu( "Settings" ); + menubar.add( menu ); + + final JMenuItem miBrightness = new JMenuItem( actionMap.get( BigDataViewerActions.BRIGHTNESS_SETTINGS ) ); + miBrightness.setText( "Brightness & Color" ); + menu.add( miBrightness ); + + final JMenuItem miVisibility = new JMenuItem( actionMap.get( BigDataViewerActions.VISIBILITY_AND_GROUPING ) ); + miVisibility.setText( "Visibility & Grouping" ); + menu.add( miVisibility ); + + menu = new JMenu( "Tools" ); + menubar.add( menu ); + + if ( cropDialog != null ) + { + final JMenuItem miCrop = new JMenuItem( actionMap.get( BigDataViewerActions.CROP ) ); + miCrop.setText( "Crop" ); + menu.add( miCrop ); + } + + final JMenuItem miMovie = new JMenuItem( actionMap.get( BigDataViewerActions.RECORD_MOVIE ) ); + miMovie.setText( "Record Movie" ); + menu.add( miMovie ); + + final JMenuItem miMaxProjectMovie = new JMenuItem( actionMap.get( BigDataViewerActions.RECORD_MAX_PROJECTION_MOVIE ) ); + miMaxProjectMovie.setText( "Record Max-Projection Movie" ); + menu.add( miMaxProjectMovie ); + + final JMenuItem miManualTransform = new JMenuItem( actionMap.get( BigDataViewerActions.MANUAL_TRANSFORM ) ); + miManualTransform.setText( "Manual Transform" ); + menu.add( miManualTransform ); + + menu = new JMenu( "Help" ); + menubar.add( menu ); + + final JMenuItem miHelp = new JMenuItem( actionMap.get( BigDataViewerActions.SHOW_HELP ) ); + miHelp.setText( "Show Help" ); + menu.add( miHelp ); + + viewerFrame.setJMenuBar( menubar ); + } + + public static BigDataViewer open( final AbstractSpimData< ? > spimData, final String windowTitle, final ProgressWriter progressWriter, final ViewerOptions options ) + { + if ( WrapBasicImgLoader.wrapImgLoaderIfNecessary( spimData ) ) + { + System.err.println( "WARNING:\nOpening <SpimData> dataset that is not suited for interactive browsing.\nConsider resaving as HDF5 for better performance." ); + } + + final ArrayList< ConverterSetup > converterSetups = new ArrayList<>(); + final ArrayList< SourceAndConverter< ? > > sources = new ArrayList<>(); + initSetups( spimData, converterSetups, sources ); + + final AbstractSequenceDescription< ?, ?, ? > seq = spimData.getSequenceDescription(); + final int numTimepoints = seq.getTimePoints().size(); + final CacheControl cache = ( ( ViewerImgLoader ) seq.getImgLoader() ).getCacheControl(); + + final BigDataViewer bdv = new BigDataViewer( converterSetups, sources, spimData, numTimepoints, cache, windowTitle, progressWriter, options ); + + WrapBasicImgLoader.removeWrapperIfPresent( spimData ); + + bdv.viewerFrame.setVisible( true ); + InitializeViewerState.initTransform( bdv.viewer ); + return bdv; + } public static BigDataViewer open(final String xmlFilename, final String windowTitle, @@ -454,169 +485,227 @@ public class BigDataViewer { final SpimDataMinimal spimData = xmlIoSpimDataMinimal.load(xmlFilename); final BigDataViewer bdv = open(spimData, windowTitle, progressWriter, options); if (!bdv.tryLoadSettings(xmlFilename)) - InitializeViewerState.initBrightness(0.001, 0.999, bdv.viewer, bdv.setupAssignments); - return bdv; - } - - public static BigDataViewer open( - final ArrayList<ConverterSetup> converterSetups, - final ArrayList<SourceAndConverter<?>> sources, - final int numTimepoints, - final CacheControl cache, - final String windowTitle, - final ProgressWriter progressWriter, - final ViewerOptions options) { - final BigDataViewer bdv = new BigDataViewer(converterSetups, - sources, - null, - numTimepoints, - cache, - windowTitle, - progressWriter, - options); - bdv.viewerFrame.setVisible(true); - InitializeViewerState.initTransform(bdv.viewer); + InitializeViewerState.initBrightness( 0.001, 0.999, bdv.viewerFrame ); return bdv; } - public ViewerPanel getViewer() { - return viewer; - } - - public ViewerFrame getViewerFrame() { - return viewerFrame; - } - - public SetupAssignments getSetupAssignments() { - return setupAssignments; - } - - public ManualTransformationEditor getManualTransformEditor() { - return manualTransformationEditor; - } - - public boolean tryLoadSettings(final String xmlFilename) { - proposedSettingsFile = null; - if (xmlFilename.startsWith("http://")) { - // load settings.xml from the BigDataServer - final String settings = xmlFilename + "settings"; - { - try { - loadSettings(settings); - return true; - } catch (final FileNotFoundException e) { - } catch (final Exception e) { - e.printStackTrace(); - } - } - } else if (xmlFilename.endsWith(".xml")) { - final String settings = xmlFilename.substring(0, - xmlFilename.length() - ".xml".length()) + ".settings" + - ".xml"; - proposedSettingsFile = new File(settings); - if (proposedSettingsFile.isFile()) { - try { - loadSettings(settings); - return true; - } catch (final Exception e) { - e.printStackTrace(); - } - } - } - return false; - } - - public void saveSettings() { - fileChooser.setSelectedFile(proposedSettingsFile); - final int returnVal = fileChooser.showSaveDialog(null); - if (returnVal == JFileChooser.APPROVE_OPTION) { - proposedSettingsFile = fileChooser.getSelectedFile(); - try { - saveSettings(proposedSettingsFile.getCanonicalPath()); - } catch (final IOException e) { - e.printStackTrace(); - } - } - } - - public void saveSettings(final String xmlFilename) throws IOException { - final Element root = new Element("Settings"); - root.addContent(viewer.stateToXml()); - root.addContent(setupAssignments.toXml()); - root.addContent(manualTransformation.toXml()); - root.addContent(bookmarks.toXml()); - final Document doc = new Document(root); - final XMLOutputter xout = new XMLOutputter(Format.getPrettyFormat()); - xout.output(doc, new FileWriter(xmlFilename)); - } - - /** - * If {@code options} doesn't define a {@link InputTriggerConfig}, try to - * load it from files in this order: - * <ol> - * <li>"bdvkeyconfig.yaml" in the current directory. - * <li>".bdv/bdvkeyconfig.yaml" in the user's home directory. - * <li>legacy "bigdataviewer.keys.properties" in current directory (will be - * also written to "bdvkeyconfig.yaml"). - * </ol> - * - * @param options - * @return - */ - public static InputTriggerConfig getInputTriggerConfig(final ViewerOptions options) { - InputTriggerConfig conf = options.values.getInputTriggerConfig(); - - // try "bdvkeyconfig.yaml" in current directory - if (conf == null && new File("bdvkeyconfig.yaml").isFile()) { - try { - conf = new InputTriggerConfig(YamlConfigIO.read("bdvkeyconfig.yaml")); - } catch (final IOException e) { - } - } - - // try "~/.bdv/bdvkeyconfig.yaml" - if (conf == null) { - final String fn = System.getProperty("user.home") + "/.bdv/bdvkeyconfig.yaml"; - if (new File(fn).isFile()) { - try { - conf = new InputTriggerConfig(YamlConfigIO.read(fn)); - } catch (final IOException e) { - } - } - } - - if (conf == null) { - conf = new InputTriggerConfig(); - } - - return conf; - } - - public void loadSettings() { - fileChooser.setSelectedFile(proposedSettingsFile); - final int returnVal = fileChooser.showOpenDialog(null); - if (returnVal == JFileChooser.APPROVE_OPTION) { - proposedSettingsFile = fileChooser.getSelectedFile(); - try { - loadSettings(proposedSettingsFile.getCanonicalPath()); - } catch (final Exception e) { - e.printStackTrace(); - } - } - } - - public void loadSettings(final String xmlFilename) throws IOException, JDOMException { - final SAXBuilder sax = new SAXBuilder(); - final Document doc = sax.build(xmlFilename); - final Element root = doc.getRootElement(); - viewer.stateFromXml(root); - setupAssignments.restoreFromXml(root); - manualTransformation.restoreFromXml(root); - bookmarks.restoreFromXml(root); - activeSourcesDialog.update(); - viewer.requestRepaint(); - } - + public static BigDataViewer open( final String xmlFilename, final String windowTitle, final ProgressWriter progressWriter, final ViewerOptions options ) throws SpimDataException + { + final SpimDataMinimal spimData = new XmlIoSpimDataMinimal().load( xmlFilename ); + final BigDataViewer bdv = open( spimData, windowTitle, progressWriter, options ); + if ( !bdv.tryLoadSettings( xmlFilename ) ) + InitializeViewerState.initBrightness( 0.001, 0.999, bdv.viewerFrame ); + return bdv; + } + + public static BigDataViewer open( + final ArrayList< ConverterSetup > converterSetups, + final ArrayList< SourceAndConverter< ? > > sources, + final int numTimepoints, + final CacheControl cache, + final String windowTitle, + final ProgressWriter progressWriter, + final ViewerOptions options ) + { + final BigDataViewer bdv = new BigDataViewer( converterSetups, sources, null, numTimepoints, cache, windowTitle, progressWriter, options ); + bdv.viewerFrame.setVisible( true ); + InitializeViewerState.initTransform( bdv.viewer ); + return bdv; + } + + public ViewerPanel getViewer() + { + return viewer; + } + + public ViewerFrame getViewerFrame() + { + return viewerFrame; + } + + public ConverterSetups getConverterSetups() + { + return viewerFrame.getConverterSetups(); + } + + /** + * @deprecated Instead {@code getViewer().state()} returns the {@link ViewerState} that can be modified directly. + */ + @Deprecated + public SetupAssignments getSetupAssignments() + { + return setupAssignments; + } + + public ManualTransformationEditor getManualTransformEditor() + { + return manualTransformationEditor; + } + + public boolean tryLoadSettings( final String xmlFilename ) + { + proposedSettingsFile = null; + if( xmlFilename.startsWith( "http://" ) ) + { + // load settings.xml from the BigDataServer + final String settings = xmlFilename + "settings"; + { + try + { + loadSettings( settings ); + return true; + } + catch ( final FileNotFoundException e ) + {} + catch ( final Exception e ) + { + e.printStackTrace(); + } + } + } + else if ( xmlFilename.endsWith( ".xml" ) ) + { + final String settings = xmlFilename.substring( 0, xmlFilename.length() - ".xml".length() ) + ".settings" + ".xml"; + proposedSettingsFile = new File( settings ); + if ( proposedSettingsFile.isFile() ) + { + try + { + loadSettings( settings ); + return true; + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + } + } + return false; + } + + public void saveSettings() + { + fileChooser.setSelectedFile( proposedSettingsFile ); + final int returnVal = fileChooser.showSaveDialog( null ); + if ( returnVal == JFileChooser.APPROVE_OPTION ) + { + proposedSettingsFile = fileChooser.getSelectedFile(); + try + { + saveSettings( proposedSettingsFile.getCanonicalPath() ); + } + catch ( final IOException e ) + { + e.printStackTrace(); + } + } + } + + public void saveSettings( final String xmlFilename ) throws IOException + { + final Element root = new Element( "Settings" ); + root.addContent( viewer.stateToXml() ); + root.addContent( setupAssignments.toXml() ); + root.addContent( manualTransformation.toXml() ); + root.addContent( bookmarks.toXml() ); + final Document doc = new Document( root ); + final XMLOutputter xout = new XMLOutputter( Format.getPrettyFormat() ); + xout.output( doc, new FileWriter( xmlFilename ) ); + } + + /** + * If {@code options} doesn't define a {@link InputTriggerConfig}, try to + * load it from files in this order: + * <ol> + * <li>"bdvkeyconfig.yaml" in the current directory. + * <li>".bdv/bdvkeyconfig.yaml" in the user's home directory. + * <li>legacy "bigdataviewer.keys.properties" in current directory (will be + * also written to "bdvkeyconfig.yaml"). + * </ol> + * + * @param options + * @return + */ + public static InputTriggerConfig getInputTriggerConfig( final ViewerOptions options ) + { + InputTriggerConfig conf = options.values.getInputTriggerConfig(); + + // try "bdvkeyconfig.yaml" in current directory + if ( conf == null && new File( "bdvkeyconfig.yaml" ).isFile() ) + { + try + { + conf = new InputTriggerConfig( YamlConfigIO.read( "bdvkeyconfig.yaml" ) ); + } + catch ( final IOException e ) + {} + } + + // try "~/.bdv/bdvkeyconfig.yaml" + if ( conf == null ) + { + final String fn = System.getProperty( "user.home" ) + "/.bdv/bdvkeyconfig.yaml"; + if ( new File( fn ).isFile() ) + { + try + { + conf = new InputTriggerConfig( YamlConfigIO.read( fn ) ); + } + catch ( final IOException e ) + {} + } + } + + if ( conf == null ) + { + conf = new InputTriggerConfig(); + } + + return conf; + } + + public void loadSettings() + { + fileChooser.setSelectedFile( proposedSettingsFile ); + final int returnVal = fileChooser.showOpenDialog( null ); + if ( returnVal == JFileChooser.APPROVE_OPTION ) + { + proposedSettingsFile = fileChooser.getSelectedFile(); + try + { + loadSettings( proposedSettingsFile.getCanonicalPath() ); + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + } + } + + public void loadSettings( final String xmlFilename ) throws IOException, JDOMException + { + final SAXBuilder sax = new SAXBuilder(); + final Document doc = sax.build( xmlFilename ); + final Element root = doc.getRootElement(); + viewer.stateFromXml( root ); + setupAssignments.restoreFromXml( root ); + manualTransformation.restoreFromXml( root ); + bookmarks.restoreFromXml( root ); + activeSourcesDialog.update(); + viewer.requestRepaint(); + } + + public void expandAndFocusCardPanel() + { + viewerFrame.getSplitPanel().setCollapsed( false ); + viewerFrame.getSplitPanel().getRightComponent().requestFocusInWindow(); + } + + public void collapseCardPanel() + { + viewerFrame.getSplitPanel().setCollapsed( true ); + viewer.requestFocusInWindow(); + } public static void main(final String[] args) { if (args.length < 1) { diff --git a/src/main/java/bdv/BigDataViewerActions.java b/src/main/java/bdv/BigDataViewerActions.java index 8119212a473f1bbdb232686c507acd8ddd112a5d..e4ce925e3a64990cb8a75f7fbab7c53ecc579480 100644 --- a/src/main/java/bdv/BigDataViewerActions.java +++ b/src/main/java/bdv/BigDataViewerActions.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -57,6 +56,8 @@ public class BigDataViewerActions extends Actions public static final String MANUAL_TRANSFORM = "toggle manual transformation"; public static final String SAVE_SETTINGS = "save settings"; public static final String LOAD_SETTINGS = "load settings"; + public static final String EXPAND_CARDS = "expand and focus cards panel"; + public static final String COLLAPSE_CARDS = "collapse cards panel"; public static final String RECORD_MOVIE = "record movie"; public static final String RECORD_MAX_PROJECTION_MOVIE = "record max projection movie"; public static final String SET_BOOKMARK = "set bookmark"; @@ -72,6 +73,8 @@ public class BigDataViewerActions extends Actions static final String[] RECORD_MOVIE_KEYS = new String[] { "F10" }; static final String[] SAVE_SETTINGS_KEYS = new String[] { "F11" }; static final String[] LOAD_SETTINGS_KEYS = new String[] { "F12" }; + public static final String[] EXPAND_CARDS_KEYS = new String[] { "P" }; + public static final String[] COLLAPSE_CARDS_KEYS = new String[] { "shift P", "shift ESCAPE" }; static final String[] GO_TO_BOOKMARK_KEYS = new String[] { "B" }; static final String[] GO_TO_BOOKMARK_ROTATION_KEYS = new String[] { "O" }; static final String[] SET_BOOKMARK_KEYS = new String[] { "shift B" }; @@ -104,6 +107,8 @@ public class BigDataViewerActions extends Actions actions.manualTransform( bdv.manualTransformationEditor ); actions.runnableAction( bdv::loadSettings, LOAD_SETTINGS, LOAD_SETTINGS_KEYS ); actions.runnableAction( bdv::saveSettings, SAVE_SETTINGS, SAVE_SETTINGS_KEYS ); + actions.runnableAction( bdv::expandAndFocusCardPanel, EXPAND_CARDS, EXPAND_CARDS_KEYS ); + actions.runnableAction( bdv::collapseCardPanel, COLLAPSE_CARDS, COLLAPSE_CARDS_KEYS ); actions.install( inputActionBindings, "bdv" ); } @@ -119,11 +124,13 @@ public class BigDataViewerActions extends Actions new ToggleDialogAction( name, dialog ).put( getActionMap() ); } + @Deprecated public void dialog( final BrightnessDialog brightnessDialog ) { toggleDialogAction( brightnessDialog, BRIGHTNESS_SETTINGS, BRIGHTNESS_SETTINGS_KEYS ); } + @Deprecated public void dialog( final VisibilityAndGroupingDialog visibilityAndGroupingDialog ) { toggleDialogAction( visibilityAndGroupingDialog, VISIBILITY_AND_GROUPING, VISIBILITY_AND_GROUPING_KEYS ); diff --git a/src/main/java/bdv/SpimSource.java b/src/main/java/bdv/SpimSource.java index adfbcf79eb7a0cf693e0f813901c4ba773fcf9fc..2f2cb71a6722b13e131c4096767659ef797309e6 100644 --- a/src/main/java/bdv/SpimSource.java +++ b/src/main/java/bdv/SpimSource.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/TransformEventHandler.java b/src/main/java/bdv/TransformEventHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..a2ee23f5b8ca0688c449e19498444957ab39d06e --- /dev/null +++ b/src/main/java/bdv/TransformEventHandler.java @@ -0,0 +1,69 @@ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv; + +import org.scijava.ui.behaviour.util.Behaviours; + +/** + * Change a transformation in response to user input (mouse events, key events, etc.) + * + * The {@link TransformEventHandler} receives notifications about changes of the + * canvas size (it may react for example by changing the scale of the + * transformation accordingly). + * + * @author Tobias Pietzsch + */ +public interface TransformEventHandler +{ + /** + * Install transformation behaviours into the specified {@code behaviours} contrainer. + */ + void install( Behaviours behaviours ); + + /** + * This is called, when the screen size of the canvas (the component + * displaying the image and generating mouse events) changes. This can be + * used to determine screen coordinates to keep fixed while zooming or + * rotating with the keyboard, e.g., set these to + * <em>(width/2, height/2)</em>. It can also be used to update the current + * source-to-screen transform, e.g., to change the zoom along with the + * canvas size. + * + * @param width + * the new canvas width. + * @param height + * the new canvas height. + * @param updateTransform + * whether the current source-to-screen transform should be + * updated. This will be <code>false</code> for the initial + * update of a new {@link TransformEventHandler} and + * <code>true</code> on subsequent calls. + */ + void setCanvasSize( int width, int height, boolean updateTransform ); +} diff --git a/src/main/java/bdv/TransformEventHandler2D.java b/src/main/java/bdv/TransformEventHandler2D.java new file mode 100644 index 0000000000000000000000000000000000000000..667ed0d6388e661b1fdf592c2dc072c09d3b58b1 --- /dev/null +++ b/src/main/java/bdv/TransformEventHandler2D.java @@ -0,0 +1,463 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv; + +import net.imglib2.realtransform.AffineTransform3D; +import org.scijava.ui.behaviour.Behaviour; +import org.scijava.ui.behaviour.ClickBehaviour; +import org.scijava.ui.behaviour.DragBehaviour; +import org.scijava.ui.behaviour.ScrollBehaviour; +import org.scijava.ui.behaviour.util.Behaviours; + +/** + * A {@link TransformEventHandler} that changes an {@link AffineTransform3D} + * through a set of {@link Behaviour}s. + * + * @author Tobias Pietzsch + */ +public class TransformEventHandler2D implements TransformEventHandler +{ + // -- behaviour names -- + + public static final String DRAG_TRANSLATE = "2d drag translate"; + public static final String DRAG_ROTATE = "2d drag rotate"; + + public static final String ZOOM_NORMAL = "2d scroll zoom"; + public static final String SCROLL_TRANSLATE = "2d scroll translate"; + public static final String SCROLL_ROTATE = "2d scroll rotate"; + public static final String ROTATE_LEFT = "2d rotate left"; + public static final String ROTATE_RIGHT = "2d rotate right"; + public static final String KEY_ZOOM_IN = "2d zoom in"; + public static final String KEY_ZOOM_OUT = "2d zoom out"; + + public static final String ZOOM_FAST = "2d scroll zoom fast"; + public static final String SCROLL_TRANSLATE_FAST = "2d scroll translate fast"; + public static final String SCROLL_ROTATE_FAST = "2d scroll rotate fast"; + public static final String ROTATE_LEFT_FAST = "2d rotate left fast"; + public static final String ROTATE_RIGHT_FAST = "2d rotate right fast"; + public static final String KEY_ZOOM_IN_FAST = "2d zoom in fast"; + public static final String KEY_ZOOM_OUT_FAST = "2d zoom out fast"; + + public static final String ZOOM_SLOW = "2d scroll zoom slow"; + public static final String SCROLL_TRANSLATE_SLOW = "2d scroll translate slow"; + public static final String SCROLL_ROTATE_SLOW = "2d scroll rotate slow"; + public static final String ROTATE_LEFT_SLOW = "2d rotate left slow"; + public static final String ROTATE_RIGHT_SLOW = "2d rotate right slow"; + public static final String KEY_ZOOM_IN_SLOW = "2d zoom in slow"; + public static final String KEY_ZOOM_OUT_SLOW = "2d zoom out slow"; + + // -- default shortcuts -- + + private static final String[] DRAG_TRANSLATE_KEYS = new String[] { "button2", "button3" }; + private static final String[] DRAG_ROTATE_KEYS = new String[] { "button1" }; + + private static final String[] ZOOM_NORMAL_KEYS = new String[] { "scroll", "meta scroll", "ctrl shift scroll" }; + private static final String[] SCROLL_TRANSLATE_KEYS = new String[] { "not mapped" }; + private static final String[] SCROLL_ROTATE_KEYS = new String[] { "not mapped" }; + private static final String[] ROTATE_LEFT_KEYS = new String[] { "LEFT" }; + private static final String[] ROTATE_RIGHT_KEYS = new String[] { "RIGHT" }; + private static final String[] KEY_ZOOM_IN_KEYS = new String[] { "UP" }; + private static final String[] KEY_ZOOM_OUT_KEYS = new String[] { "DOWN" }; + + private static final String[] ZOOM_FAST_KEYS = new String[] { "shift scroll" }; + private static final String[] SCROLL_TRANSLATE_FAST_KEYS = new String[] { "not mapped" }; + private static final String[] SCROLL_ROTATE_FAST_KEYS = new String[] { "not mapped" }; + private static final String[] ROTATE_LEFT_FAST_KEYS = new String[] { "shift LEFT" }; + private static final String[] ROTATE_RIGHT_FAST_KEYS = new String[] { "shift RIGHT" }; + private static final String[] KEY_ZOOM_IN_FAST_KEYS = new String[] { "shift UP" }; + private static final String[] KEY_ZOOM_OUT_FAST_KEYS = new String[] { "shift DOWN" }; + + private static final String[] ZOOM_SLOW_KEYS = new String[] { "ctrl scroll" }; + private static final String[] SCROLL_TRANSLATE_SLOW_KEYS = new String[] { "not mapped" }; + private static final String[] SCROLL_ROTATE_SLOW_KEYS = new String[] { "not mapped" }; + private static final String[] ROTATE_LEFT_SLOW_KEYS = new String[] { "ctrl LEFT" }; + private static final String[] ROTATE_RIGHT_SLOW_KEYS = new String[] { "ctrl RIGHT" }; + private static final String[] KEY_ZOOM_IN_SLOW_KEYS = new String[] { "ctrl UP" }; + private static final String[] KEY_ZOOM_OUT_SLOW_KEYS = new String[] { "ctrl DOWN" }; + + // -- behaviours -- + + private final DragTranslate dragTranslate; + private final DragRotate dragRotate; + private final Zoom zoom; + private final Zoom zoomFast; + private final Zoom zoomSlow; + private final ScrollTranslate scrollTranslate; + private final ScrollTranslate scrollTranslateFast; + private final ScrollTranslate scrollTranslateSlow; + private final ScrollRotate scrollRotate; + private final ScrollRotate scrollRotateFast; + private final ScrollRotate scrollRotateSlow; + private final KeyRotate keyRotateLeft; + private final KeyRotate keyRotateLeftFast; + private final KeyRotate keyRotateLeftSlow; + private final KeyRotate keyRotateRight; + private final KeyRotate keyRotateRightFast; + private final KeyRotate keyRotateRightSlow; + private final KeyZoom keyZoomIn; + private final KeyZoom keyZoomInFast; + private final KeyZoom keyZoomInSlow; + private final KeyZoom keyZoomOut; + private final KeyZoom keyZoomOutFast; + private final KeyZoom keyZoomOutSlow; + + private static final double[] speed = { 1.0, 10.0, 0.1 }; + + /** + * Copy of transform when mouse dragging started. + */ + private final AffineTransform3D affineDragStart = new AffineTransform3D(); + + /** + * Current transform during mouse dragging. + */ + private final AffineTransform3D affineDragCurrent = new AffineTransform3D(); + + /** + * Coordinates where mouse dragging started. + */ + private double oX, oY; + + /** + * The screen size of the canvas (the component displaying the image and + * generating mouse events). + */ + private int canvasW = 1, canvasH = 1; + + /** + * Screen coordinates to keep centered while zooming or rotating with the + * keyboard. These are set to <em>(canvasW/2, canvasH/2)</em> + */ + private int centerX = 0, centerY = 0; + + private final TransformState transform; + + public TransformEventHandler2D( final TransformState transform ) + { + this.transform = transform; + + dragTranslate = new DragTranslate(); + dragRotate = new DragRotate(); + + scrollTranslate = new ScrollTranslate( speed[ 0 ] ); + scrollTranslateFast = new ScrollTranslate( speed[ 1 ] ); + scrollTranslateSlow = new ScrollTranslate( speed[ 2 ] ); + + zoom = new Zoom( speed[ 0 ] ); + zoomFast = new Zoom( speed[ 1 ] ); + zoomSlow = new Zoom( speed[ 2 ] ); + + scrollRotate = new ScrollRotate( 2 * speed[ 0 ] ); + scrollRotateFast = new ScrollRotate( 2 * speed[ 1 ] ); + scrollRotateSlow = new ScrollRotate( 2 * speed[ 2 ] ); + + keyRotateLeft = new KeyRotate( speed[ 0 ] ); + keyRotateLeftFast = new KeyRotate( speed[ 1 ] ); + keyRotateLeftSlow = new KeyRotate( speed[ 2 ] ); + keyRotateRight = new KeyRotate( -speed[ 0 ] ); + keyRotateRightFast = new KeyRotate( -speed[ 1 ] ); + keyRotateRightSlow = new KeyRotate( -speed[ 2 ] ); + + keyZoomIn = new KeyZoom( speed[ 0 ] ); + keyZoomInFast = new KeyZoom( speed[ 1 ] ); + keyZoomInSlow = new KeyZoom( speed[ 2 ] ); + keyZoomOut = new KeyZoom( -speed[ 0 ] ); + keyZoomOutFast = new KeyZoom( -speed[ 1 ] ); + keyZoomOutSlow = new KeyZoom( -speed[ 2 ] ); + } + + @Override + public void install( final Behaviours behaviours ) + { + behaviours.behaviour( dragTranslate, DRAG_TRANSLATE, DRAG_TRANSLATE_KEYS ); + behaviours.behaviour( dragRotate, DRAG_ROTATE, DRAG_ROTATE_KEYS ); + + behaviours.behaviour( scrollTranslate, SCROLL_TRANSLATE, SCROLL_TRANSLATE_KEYS ); + behaviours.behaviour( scrollTranslateFast, SCROLL_TRANSLATE_FAST, SCROLL_TRANSLATE_FAST_KEYS ); + behaviours.behaviour( scrollTranslateSlow, SCROLL_TRANSLATE_SLOW, SCROLL_TRANSLATE_SLOW_KEYS ); + + behaviours.behaviour( zoom, ZOOM_NORMAL, ZOOM_NORMAL_KEYS ); + behaviours.behaviour( zoomFast, ZOOM_FAST, ZOOM_FAST_KEYS ); + behaviours.behaviour( zoomSlow, ZOOM_SLOW, ZOOM_SLOW_KEYS ); + + behaviours.behaviour( scrollRotate, SCROLL_ROTATE, SCROLL_ROTATE_KEYS ); + behaviours.behaviour( scrollRotateFast, SCROLL_ROTATE_FAST, SCROLL_ROTATE_FAST_KEYS ); + behaviours.behaviour( scrollRotateSlow, SCROLL_ROTATE_SLOW, SCROLL_ROTATE_SLOW_KEYS ); + + behaviours.behaviour( keyRotateLeft, ROTATE_LEFT, ROTATE_LEFT_KEYS ); + behaviours.behaviour( keyRotateLeftFast, ROTATE_LEFT_FAST, ROTATE_LEFT_FAST_KEYS ); + behaviours.behaviour( keyRotateLeftSlow, ROTATE_LEFT_SLOW, ROTATE_LEFT_SLOW_KEYS ); + behaviours.behaviour( keyRotateRight, ROTATE_RIGHT, ROTATE_RIGHT_KEYS ); + behaviours.behaviour( keyRotateRightFast, ROTATE_RIGHT_FAST, ROTATE_RIGHT_FAST_KEYS ); + behaviours.behaviour( keyRotateRightSlow, ROTATE_RIGHT_SLOW, ROTATE_RIGHT_SLOW_KEYS ); + + behaviours.behaviour( keyZoomIn, KEY_ZOOM_IN, KEY_ZOOM_IN_KEYS ); + behaviours.behaviour( keyZoomInFast, KEY_ZOOM_IN_FAST, KEY_ZOOM_IN_FAST_KEYS ); + behaviours.behaviour( keyZoomInSlow, KEY_ZOOM_IN_SLOW, KEY_ZOOM_IN_SLOW_KEYS ); + behaviours.behaviour( keyZoomOut, KEY_ZOOM_OUT, KEY_ZOOM_OUT_KEYS ); + behaviours.behaviour( keyZoomOutFast, KEY_ZOOM_OUT_FAST, KEY_ZOOM_OUT_FAST_KEYS ); + behaviours.behaviour( keyZoomOutSlow, KEY_ZOOM_OUT_SLOW, KEY_ZOOM_OUT_SLOW_KEYS ); + } + + @Override + public void setCanvasSize( final int width, final int height, final boolean updateTransform ) + { + if ( width == 0 || height == 0 ) { + // NB: We are probably in some intermediate layout scenario. + // Attempting to trigger a transform update with 0 size will result + // in the exception "Matrix is singular" from imglib2-realtrasform. + return; + } + if ( updateTransform ) + { + final AffineTransform3D affine = transform.get(); + affine.set( affine.get( 0, 3 ) - canvasW / 2, 0, 3 ); + affine.set( affine.get( 1, 3 ) - canvasH / 2, 1, 3 ); + affine.scale( ( double ) width / canvasW ); + affine.set( affine.get( 0, 3 ) + width / 2, 0, 3 ); + affine.set( affine.get( 1, 3 ) + height / 2, 1, 3 ); + transform.set( affine ); + } + canvasW = width; + canvasH = height; + centerX = width / 2; + centerY = height / 2; + } + + /** + * One step of rotation (radian). + */ + final private static double step = Math.PI / 180; + + private void scale( final double s, final double x, final double y ) + { + final AffineTransform3D affine = transform.get(); + + // center shift + affine.set( affine.get( 0, 3 ) - x, 0, 3 ); + affine.set( affine.get( 1, 3 ) - y, 1, 3 ); + + // scale + affine.scale( s ); + + // center un-shift + affine.set( affine.get( 0, 3 ) + x, 0, 3 ); + affine.set( affine.get( 1, 3 ) + y, 1, 3 ); + + transform.set( affine ); + } + + /** + * Rotate by d radians around Z axis. Keep screen coordinates {@code (centerX, centerY)} fixed. + */ + private void rotate( final AffineTransform3D affine, final double d ) + { + // center shift + affine.set( affine.get( 0, 3 ) - centerX, 0, 3 ); + affine.set( affine.get( 1, 3 ) - centerY, 1, 3 ); + + // rotate + affine.rotate( 2, d ); + + // center un-shift + affine.set( affine.get( 0, 3 ) + centerX, 0, 3 ); + affine.set( affine.get( 1, 3 ) + centerY, 1, 3 ); + } + + private class DragRotate implements DragBehaviour + { + @Override + public void init( final int x, final int y ) + { + oX = x; + oY = y; + transform.get( affineDragStart ); + } + + @Override + public void drag( final int x, final int y ) + { + final double dX = x - centerX; + final double dY = y - centerY; + final double odX = oX - centerX; + final double odY = oY - centerY; + final double theta = Math.atan2( dY, dX ) - Math.atan2( odY, odX ); + + affineDragCurrent.set( affineDragStart ); + rotate( affineDragCurrent, theta ); + transform.set( affineDragCurrent ); + } + + @Override + public void end( final int x, final int y ) + {} + } + + private class ScrollRotate implements ScrollBehaviour + { + private final double speed; + + public ScrollRotate( final double speed ) + { + this.speed = speed; + } + + @Override + public void scroll( final double wheelRotation, final boolean isHorizontal, final int x, final int y ) + { + final AffineTransform3D affine = transform.get(); + + final double theta = speed * wheelRotation * Math.PI / 180.0; + + // center shift + affine.set( affine.get( 0, 3 ) - x, 0, 3 ); + affine.set( affine.get( 1, 3 ) - y, 1, 3 ); + + affine.rotate( 2, theta ); + + // center un-shift + affine.set( affine.get( 0, 3 ) + x, 0, 3 ); + affine.set( affine.get( 1, 3 ) + y, 1, 3 ); + + transform.set( affine ); + } + } + + private class DragTranslate implements DragBehaviour + { + @Override + public void init( final int x, final int y ) + { + oX = x; + oY = y; + transform.get( affineDragStart ); + } + + @Override + public void drag( final int x, final int y ) + { + final double dX = oX - x; + final double dY = oY - y; + + affineDragCurrent.set( affineDragStart ); + affineDragCurrent.set( affineDragCurrent.get( 0, 3 ) - dX, 0, 3 ); + affineDragCurrent.set( affineDragCurrent.get( 1, 3 ) - dY, 1, 3 ); + + transform.set( affineDragCurrent ); + } + + @Override + public void end( final int x, final int y ) + {} + } + + private class ScrollTranslate implements ScrollBehaviour + { + + private final double speed; + + public ScrollTranslate( final double speed ) + { + this.speed = speed; + } + + @Override + public void scroll( final double wheelRotation, final boolean isHorizontal, final int x, final int y ) + { + final AffineTransform3D affine = transform.get(); + + final double d = -wheelRotation * 10 * speed; + if ( isHorizontal ) + affine.translate( d, 0, 0 ); + else + affine.translate( 0, d, 0 ); + + transform.set( affine ); + } + } + + private class Zoom implements ScrollBehaviour + { + + private final double speed; + + public Zoom( final double speed ) + { + this.speed = speed; + } + + @Override + public void scroll( final double wheelRotation, final boolean isHorizontal, final int x, final int y ) + { + final double s = speed * wheelRotation; + final double dScale = 1.0 + 0.05 * Math.abs( s ); + if ( s > 0 ) + scale( 1.0 / dScale, x, y ); + else + scale( dScale, x, y ); + } + } + + private class KeyRotate implements ClickBehaviour + { + private final double speed; + + public KeyRotate( final double speed ) + { + this.speed = speed; + } + + @Override + public void click( final int x, final int y ) + { + final AffineTransform3D affine = transform.get(); + rotate( affine, step * speed ); + transform.set( affine ); + } + } + + private class KeyZoom implements ClickBehaviour + { + private final double dScale; + + public KeyZoom( final double speed ) + { + if ( speed > 0 ) + dScale = 1.0 + 0.1 * speed; + else + dScale = 1.0 / ( 1.0 - 0.1 * speed ); + } + + @Override + public void click( final int x, final int y ) + { + scale( dScale, centerX, centerY ); + } + } +} diff --git a/src/main/java/bdv/TransformEventHandler3D.java b/src/main/java/bdv/TransformEventHandler3D.java new file mode 100644 index 0000000000000000000000000000000000000000..9424e9509e703438ae07efbd83f2ae6990a8a21e --- /dev/null +++ b/src/main/java/bdv/TransformEventHandler3D.java @@ -0,0 +1,515 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv; + +import net.imglib2.realtransform.AffineTransform3D; +import org.scijava.ui.behaviour.Behaviour; +import org.scijava.ui.behaviour.ClickBehaviour; +import org.scijava.ui.behaviour.DragBehaviour; +import org.scijava.ui.behaviour.ScrollBehaviour; +import org.scijava.ui.behaviour.util.Behaviours; + +/** + * A {@link TransformEventHandler} that changes an {@link AffineTransform3D} + * through a set of {@link Behaviour}s. + * + * @author Stephan Saalfeld + * @author Tobias Pietzsch + */ +public class TransformEventHandler3D implements TransformEventHandler +{ + // -- behaviour names -- + + public static final String DRAG_TRANSLATE = "drag translate"; + public static final String ZOOM_NORMAL = "scroll zoom"; + public static final String SELECT_AXIS_X = "axis x"; + public static final String SELECT_AXIS_Y = "axis y"; + public static final String SELECT_AXIS_Z = "axis z"; + + public static final String DRAG_ROTATE = "drag rotate"; + public static final String SCROLL_Z = "scroll browse z"; + public static final String ROTATE_LEFT = "rotate left"; + public static final String ROTATE_RIGHT = "rotate right"; + public static final String KEY_ZOOM_IN = "zoom in"; + public static final String KEY_ZOOM_OUT = "zoom out"; + public static final String KEY_FORWARD_Z = "forward z"; + public static final String KEY_BACKWARD_Z = "backward z"; + + public static final String DRAG_ROTATE_FAST = "drag rotate fast"; + public static final String SCROLL_Z_FAST = "scroll browse z fast"; + public static final String ROTATE_LEFT_FAST = "rotate left fast"; + public static final String ROTATE_RIGHT_FAST = "rotate right fast"; + public static final String KEY_ZOOM_IN_FAST = "zoom in fast"; + public static final String KEY_ZOOM_OUT_FAST = "zoom out fast"; + public static final String KEY_FORWARD_Z_FAST = "forward z fast"; + public static final String KEY_BACKWARD_Z_FAST = "backward z fast"; + + public static final String DRAG_ROTATE_SLOW = "drag rotate slow"; + public static final String SCROLL_Z_SLOW = "scroll browse z slow"; + public static final String ROTATE_LEFT_SLOW = "rotate left slow"; + public static final String ROTATE_RIGHT_SLOW = "rotate right slow"; + public static final String KEY_ZOOM_IN_SLOW = "zoom in slow"; + public static final String KEY_ZOOM_OUT_SLOW = "zoom out slow"; + public static final String KEY_FORWARD_Z_SLOW = "forward z slow"; + public static final String KEY_BACKWARD_Z_SLOW = "backward z slow"; + + // -- default shortcuts -- + + private static final String[] DRAG_TRANSLATE_KEYS = new String[] { "button2", "button3" }; + private static final String[] ZOOM_NORMAL_KEYS = new String[] { "meta scroll", "ctrl shift scroll" }; + private static final String[] SELECT_AXIS_X_KEYS = new String[] { "X" }; + private static final String[] SELECT_AXIS_Y_KEYS = new String[] { "Y" }; + private static final String[] SELECT_AXIS_Z_KEYS = new String[] { "Z" }; + + private static final String[] DRAG_ROTATE_KEYS = new String[] { "button1" }; + private static final String[] SCROLL_Z_KEYS = new String[] { "scroll" }; + private static final String[] ROTATE_LEFT_KEYS = new String[] { "LEFT" }; + private static final String[] ROTATE_RIGHT_KEYS = new String[] { "RIGHT" }; + private static final String[] KEY_ZOOM_IN_KEYS = new String[] { "UP" }; + private static final String[] KEY_ZOOM_OUT_KEYS = new String[] { "DOWN" }; + private static final String[] KEY_FORWARD_Z_KEYS = new String[] { "COMMA" }; + private static final String[] KEY_BACKWARD_Z_KEYS = new String[] { "PERIOD" }; + + private static final String[] DRAG_ROTATE_FAST_KEYS = new String[] { "shift button1" }; + private static final String[] SCROLL_Z_FAST_KEYS = new String[] { "shift scroll" }; + private static final String[] ROTATE_LEFT_FAST_KEYS = new String[] { "shift LEFT" }; + private static final String[] ROTATE_RIGHT_FAST_KEYS = new String[] { "shift RIGHT" }; + private static final String[] KEY_ZOOM_IN_FAST_KEYS = new String[] { "shift UP" }; + private static final String[] KEY_ZOOM_OUT_FAST_KEYS = new String[] { "shift DOWN" }; + private static final String[] KEY_FORWARD_Z_FAST_KEYS = new String[] { "shift COMMA" }; + private static final String[] KEY_BACKWARD_Z_FAST_KEYS = new String[] { "shift PERIOD" }; + + private static final String[] DRAG_ROTATE_SLOW_KEYS = new String[] { "ctrl button1" }; + private static final String[] SCROLL_Z_SLOW_KEYS = new String[] { "ctrl scroll" }; + private static final String[] ROTATE_LEFT_SLOW_KEYS = new String[] { "ctrl LEFT" }; + private static final String[] ROTATE_RIGHT_SLOW_KEYS = new String[] { "ctrl RIGHT" }; + private static final String[] KEY_ZOOM_IN_SLOW_KEYS = new String[] { "ctrl UP" }; + private static final String[] KEY_ZOOM_OUT_SLOW_KEYS = new String[] { "ctrl DOWN" }; + private static final String[] KEY_FORWARD_Z_SLOW_KEYS = new String[] { "ctrl COMMA" }; + private static final String[] KEY_BACKWARD_Z_SLOW_KEYS = new String[] { "ctrl PERIOD" }; + + // -- behaviours -- + + private final TranslateXY dragTranslate; + private final Zoom zoom; + private final SelectRotationAxis selectRotationAxisX; + private final SelectRotationAxis selectRotationAxisY; + private final SelectRotationAxis selectRotationAxisZ; + private final Rotate dragRotate; + private final Rotate dragRotateFast; + private final Rotate dragRotateSlow; + private final TranslateZ translateZ; + private final TranslateZ translateZFast; + private final TranslateZ translateZSlow; + private final KeyRotate rotateLeft; + private final KeyRotate rotateLeftFast; + private final KeyRotate rotateLeftSlow; + private final KeyRotate rotateRight; + private final KeyRotate rotateRightFast; + private final KeyRotate rotateRightSlow; + private final KeyZoom keyZoomIn; + private final KeyZoom keyZoomInFast; + private final KeyZoom keyZoomInSlow; + private final KeyZoom keyZoomOut; + private final KeyZoom keyZoomOutFast; + private final KeyZoom keyZoomOutSlow; + private final KeyTranslateZ keyForwardZ; + private final KeyTranslateZ keyForwardZFast; + private final KeyTranslateZ keyForwardZSlow; + private final KeyTranslateZ keyBackwardZ; + private final KeyTranslateZ keyBackwardZFast; + private final KeyTranslateZ keyBackwardZSlow; + + private static final double[] speed = { 1.0, 10.0, 0.1 }; + + /** + * Copy of transform when mouse dragging started. + */ + private final AffineTransform3D affineDragStart = new AffineTransform3D(); + + /** + * Current transform during mouse dragging. + */ + private final AffineTransform3D affineDragCurrent = new AffineTransform3D(); + + /** + * Coordinates where mouse dragging started. + */ + private double oX, oY; + + /** + * Current rotation axis for rotating with keyboard, indexed {@code x->0, y->1, + * z->2}. + */ + private int axis = 0; + + /** + * The screen size of the canvas (the component displaying the image and + * generating mouse events). + */ + private int canvasW = 1, canvasH = 1; + + /** + * Screen coordinates to keep centered while zooming or rotating with the + * keyboard. These are set to <em>(canvasW/2, canvasH/2)</em> + */ + private int centerX = 0, centerY = 0; + + private final TransformState transform; + + public TransformEventHandler3D( final TransformState transform ) + { + this.transform = transform; + + dragTranslate = new TranslateXY(); + zoom = new Zoom(); + selectRotationAxisX = new SelectRotationAxis( 0 ); + selectRotationAxisY = new SelectRotationAxis( 1 ); + selectRotationAxisZ = new SelectRotationAxis( 2 ); + + dragRotate = new Rotate( speed[ 0 ] ); + dragRotateFast = new Rotate( speed[ 1 ] ); + dragRotateSlow = new Rotate( speed[ 2 ] ); + + translateZ = new TranslateZ( speed[ 0 ] ); + translateZFast = new TranslateZ( speed[ 1 ] ); + translateZSlow = new TranslateZ( speed[ 2 ] ); + + rotateLeft = new KeyRotate( speed[ 0 ] ); + rotateLeftFast = new KeyRotate( speed[ 1 ] ); + rotateLeftSlow = new KeyRotate( speed[ 2 ] ); + rotateRight = new KeyRotate( -speed[ 0 ] ); + rotateRightFast = new KeyRotate( -speed[ 1 ] ); + rotateRightSlow = new KeyRotate( -speed[ 2 ] ); + + keyZoomIn = new KeyZoom( speed[ 0 ] ); + keyZoomInFast = new KeyZoom( speed[ 1 ] ); + keyZoomInSlow = new KeyZoom( speed[ 2 ] ); + keyZoomOut = new KeyZoom( -speed[ 0 ] ); + keyZoomOutFast = new KeyZoom( -speed[ 1 ] ); + keyZoomOutSlow = new KeyZoom( -speed[ 2 ] ); + + keyForwardZ = new KeyTranslateZ( speed[ 0 ] ); + keyForwardZFast = new KeyTranslateZ( speed[ 1 ] ); + keyForwardZSlow = new KeyTranslateZ( speed[ 2 ] ); + keyBackwardZ = new KeyTranslateZ( -speed[ 0 ] ); + keyBackwardZFast = new KeyTranslateZ( -speed[ 1 ] ); + keyBackwardZSlow = new KeyTranslateZ( -speed[ 2 ] ); + } + + @Override + public void install( final Behaviours behaviours ) + { + behaviours.behaviour( dragTranslate, DRAG_TRANSLATE, DRAG_TRANSLATE_KEYS ); + behaviours.behaviour( zoom, ZOOM_NORMAL, ZOOM_NORMAL_KEYS ); + + behaviours.behaviour( selectRotationAxisX, SELECT_AXIS_X, SELECT_AXIS_X_KEYS ); + behaviours.behaviour( selectRotationAxisY, SELECT_AXIS_Y, SELECT_AXIS_Y_KEYS ); + behaviours.behaviour( selectRotationAxisZ, SELECT_AXIS_Z, SELECT_AXIS_Z_KEYS ); + + behaviours.behaviour( dragRotate, DRAG_ROTATE, DRAG_ROTATE_KEYS ); + behaviours.behaviour( dragRotateFast, DRAG_ROTATE_FAST, DRAG_ROTATE_FAST_KEYS ); + behaviours.behaviour( dragRotateSlow, DRAG_ROTATE_SLOW, DRAG_ROTATE_SLOW_KEYS ); + + behaviours.behaviour( translateZ, SCROLL_Z, SCROLL_Z_KEYS ); + behaviours.behaviour( translateZFast, SCROLL_Z_FAST, SCROLL_Z_FAST_KEYS ); + behaviours.behaviour( translateZSlow, SCROLL_Z_SLOW, SCROLL_Z_SLOW_KEYS ); + + behaviours.behaviour( rotateLeft, ROTATE_LEFT, ROTATE_LEFT_KEYS ); + behaviours.behaviour( rotateLeftFast, ROTATE_LEFT_FAST, ROTATE_LEFT_FAST_KEYS ); + behaviours.behaviour( rotateLeftSlow, ROTATE_LEFT_SLOW, ROTATE_LEFT_SLOW_KEYS ); + behaviours.behaviour( rotateRight, ROTATE_RIGHT, ROTATE_RIGHT_KEYS ); + behaviours.behaviour( rotateRightFast, ROTATE_RIGHT_FAST, ROTATE_RIGHT_FAST_KEYS ); + behaviours.behaviour( rotateRightSlow, ROTATE_RIGHT_SLOW, ROTATE_RIGHT_SLOW_KEYS ); + + behaviours.behaviour( keyZoomIn, KEY_ZOOM_IN, KEY_ZOOM_IN_KEYS ); + behaviours.behaviour( keyZoomInFast, KEY_ZOOM_IN_FAST, KEY_ZOOM_IN_FAST_KEYS ); + behaviours.behaviour( keyZoomInSlow, KEY_ZOOM_IN_SLOW, KEY_ZOOM_IN_SLOW_KEYS ); + behaviours.behaviour( keyZoomOut, KEY_ZOOM_OUT, KEY_ZOOM_OUT_KEYS ); + behaviours.behaviour( keyZoomOutFast, KEY_ZOOM_OUT_FAST, KEY_ZOOM_OUT_FAST_KEYS ); + behaviours.behaviour( keyZoomOutSlow, KEY_ZOOM_OUT_SLOW, KEY_ZOOM_OUT_SLOW_KEYS ); + + behaviours.behaviour( keyForwardZ, KEY_FORWARD_Z, KEY_FORWARD_Z_KEYS ); + behaviours.behaviour( keyForwardZFast, KEY_FORWARD_Z_SLOW, KEY_FORWARD_Z_SLOW_KEYS ); + behaviours.behaviour( keyForwardZSlow, KEY_FORWARD_Z_FAST, KEY_FORWARD_Z_FAST_KEYS ); + behaviours.behaviour( keyBackwardZ, KEY_BACKWARD_Z, KEY_BACKWARD_Z_KEYS ); + behaviours.behaviour( keyBackwardZFast, KEY_BACKWARD_Z_FAST, KEY_BACKWARD_Z_FAST_KEYS ); + behaviours.behaviour( keyBackwardZSlow, KEY_BACKWARD_Z_SLOW, KEY_BACKWARD_Z_SLOW_KEYS ); + } + + @Override + public void setCanvasSize( final int width, final int height, final boolean updateTransform ) + { + if ( width == 0 || height == 0 ) { + // NB: We are probably in some intermediate layout scenario. + // Attempting to trigger a transform update with 0 size will result + // in the exception "Matrix is singular" from imglib2-realtrasform. + return; + } + if ( updateTransform ) + { + final AffineTransform3D affine = transform.get(); + affine.set( affine.get( 0, 3 ) - canvasW / 2, 0, 3 ); + affine.set( affine.get( 1, 3 ) - canvasH / 2, 1, 3 ); + affine.scale( ( double ) width / canvasW ); + affine.set( affine.get( 0, 3 ) + width / 2, 0, 3 ); + affine.set( affine.get( 1, 3 ) + height / 2, 1, 3 ); + transform.set( affine ); + } + canvasW = width; + canvasH = height; + centerX = width / 2; + centerY = height / 2; + } + + /** + * One step of rotation (radian). + */ + final private static double step = Math.PI / 180; + + private void scale( final double s, final double x, final double y ) + { + final AffineTransform3D affine = transform.get(); + + // center shift + affine.set( affine.get( 0, 3 ) - x, 0, 3 ); + affine.set( affine.get( 1, 3 ) - y, 1, 3 ); + + // scale + affine.scale( s ); + + // center un-shift + affine.set( affine.get( 0, 3 ) + x, 0, 3 ); + affine.set( affine.get( 1, 3 ) + y, 1, 3 ); + + transform.set( affine ); + } + + /** + * Rotate by d radians around axis. Keep screen coordinates ( + * {@link #centerX}, {@link #centerY}) fixed. + */ + private void rotate( final int axis, final double d ) + { + final AffineTransform3D affine = transform.get(); + + // center shift + affine.set( affine.get( 0, 3 ) - centerX, 0, 3 ); + affine.set( affine.get( 1, 3 ) - centerY, 1, 3 ); + + // rotate + affine.rotate( axis, d ); + + // center un-shift + affine.set( affine.get( 0, 3 ) + centerX, 0, 3 ); + affine.set( affine.get( 1, 3 ) + centerY, 1, 3 ); + + transform.set( affine ); + } + + private class Rotate implements DragBehaviour + { + private final double speed; + + public Rotate( final double speed ) + { + this.speed = speed; + } + + @Override + public void init( final int x, final int y ) + { + oX = x; + oY = y; + transform.get( affineDragStart ); + } + + @Override + public void drag( final int x, final int y ) + { + final double dX = oX - x; + final double dY = oY - y; + + affineDragCurrent.set( affineDragStart ); + + // center shift + affineDragCurrent.set( affineDragCurrent.get( 0, 3 ) - oX, 0, 3 ); + affineDragCurrent.set( affineDragCurrent.get( 1, 3 ) - oY, 1, 3 ); + + final double v = step * speed; + affineDragCurrent.rotate( 0, -dY * v ); + affineDragCurrent.rotate( 1, dX * v ); + + // center un-shift + affineDragCurrent.set( affineDragCurrent.get( 0, 3 ) + oX, 0, 3 ); + affineDragCurrent.set( affineDragCurrent.get( 1, 3 ) + oY, 1, 3 ); + + transform.set( affineDragCurrent ); + } + + @Override + public void end( final int x, final int y ) + {} + } + + private class TranslateXY implements DragBehaviour + { + @Override + public void init( final int x, final int y ) + { + oX = x; + oY = y; + transform.get( affineDragStart ); + } + + @Override + public void drag( final int x, final int y ) + { + final double dX = oX - x; + final double dY = oY - y; + + affineDragCurrent.set( affineDragStart ); + affineDragCurrent.set( affineDragCurrent.get( 0, 3 ) - dX, 0, 3 ); + affineDragCurrent.set( affineDragCurrent.get( 1, 3 ) - dY, 1, 3 ); + + transform.set( affineDragCurrent ); + } + + @Override + public void end( final int x, final int y ) + {} + } + + private class TranslateZ implements ScrollBehaviour + { + private final double speed; + + public TranslateZ( final double speed ) + { + this.speed = speed; + } + + @Override + public void scroll( final double wheelRotation, final boolean isHorizontal, final int x, final int y ) + { + final AffineTransform3D affine = transform.get(); + + final double dZ = speed * -wheelRotation; + // TODO (optionally) correct for zoom + affine.set( affine.get( 2, 3 ) - dZ, 2, 3 ); + + transform.set( affine ); + } + } + + private class Zoom implements ScrollBehaviour + { + private final double speed = 1.0; + + @Override + public void scroll( final double wheelRotation, final boolean isHorizontal, final int x, final int y ) + { + final double s = speed * wheelRotation; + final double dScale = 1.0 + 0.05; + if ( s > 0 ) + scale( 1.0 / dScale, x, y ); + else + scale( dScale, x, y ); + } + } + + private class SelectRotationAxis implements ClickBehaviour + { + private final int axis; + + public SelectRotationAxis( final int axis ) + { + this.axis = axis; + } + + @Override + public void click( final int x, final int y ) + { + TransformEventHandler3D.this.axis = axis; + } + } + + private class KeyRotate implements ClickBehaviour + { + private final double speed; + + public KeyRotate( final double speed ) + { + this.speed = speed; + } + + @Override + public void click( final int x, final int y ) + { + rotate( axis, step * speed ); + } + } + + private class KeyZoom implements ClickBehaviour + { + private final double dScale; + + public KeyZoom( final double speed ) + { + if ( speed > 0 ) + dScale = 1.0 + 0.1 * speed; + else + dScale = 1.0 / ( 1.0 - 0.1 * speed ); + } + + @Override + public void click( final int x, final int y ) + { + scale( dScale, centerX, centerY ); + } + } + + private class KeyTranslateZ implements ClickBehaviour + { + private final double speed; + + public KeyTranslateZ( final double speed ) + { + this.speed = speed; + } + + @Override + public void click( final int x, final int y ) + { + final AffineTransform3D affine = transform.get(); + affine.set( affine.get( 2, 3 ) + speed, 2, 3 ); + transform.set( affine ); + } + } +} diff --git a/src/main/java/bdv/BehaviourTransformEventHandler.java b/src/main/java/bdv/TransformEventHandlerFactory.java similarity index 73% rename from src/main/java/bdv/BehaviourTransformEventHandler.java rename to src/main/java/bdv/TransformEventHandlerFactory.java index 4abf1c09d7200011dfd9f2f8da3a3b3393ff2d43..3e03753687f1c2a3b869390361668af9ccb8f8a5 100644 --- a/src/main/java/bdv/BehaviourTransformEventHandler.java +++ b/src/main/java/bdv/TransformEventHandlerFactory.java @@ -1,9 +1,8 @@ -/*- +/* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,11 +28,15 @@ */ package bdv; -import org.scijava.ui.behaviour.util.TriggerBehaviourBindings; - -import net.imglib2.ui.TransformEventHandler; - -public interface BehaviourTransformEventHandler< A > extends TransformEventHandler< A > +/** + * Factory for {@code TransformEventHandler}. + * + * @author Tobias Pietzsch + */ +public interface TransformEventHandlerFactory { - public void install( final TriggerBehaviourBindings bindings ); + /** + * Create a new {@code TransformEventHandler}. + */ + TransformEventHandler create( TransformState transformState ); } diff --git a/src/main/java/bdv/viewer/render/TransformAwareRenderTarget.java b/src/main/java/bdv/TransformState.java similarity index 57% rename from src/main/java/bdv/viewer/render/TransformAwareRenderTarget.java rename to src/main/java/bdv/TransformState.java index ac381770164bc3513896f71b170ee2fb886931b9..a73ae03f59fda1ae7401350356e3ba91b231db51 100644 --- a/src/main/java/bdv/viewer/render/TransformAwareRenderTarget.java +++ b/src/main/java/bdv/TransformState.java @@ -1,9 +1,8 @@ -/* +/*- * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -27,28 +26,53 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package bdv.viewer.render; - -import java.awt.image.BufferedImage; +package bdv; +import java.util.function.Consumer; import net.imglib2.realtransform.AffineTransform3D; -import net.imglib2.ui.RenderTarget; -import net.imglib2.ui.TransformListener; -public interface TransformAwareRenderTarget extends RenderTarget +public interface TransformState { /** - * Set the {@link BufferedImage} that is to be drawn on the canvas, and the - * transform with which this image was created. + * Get the current transform. * - * @param img - * image to draw (may be null). + * @param transform + * is set to the current transform */ - public BufferedImage setBufferedImageAndTransform( final BufferedImage img, final AffineTransform3D transform ); + void get( AffineTransform3D transform ); - public void addTransformListener( final TransformListener< AffineTransform3D > listener ); + /** + * Get the current transform. + * + * @return a copy of the current transform + */ + default AffineTransform3D get() + { + final AffineTransform3D transform = new AffineTransform3D(); + get( transform ); + return transform; + } + + /** + * Set the transform. + */ + void set( AffineTransform3D transform ); - public void addTransformListener( final TransformListener< AffineTransform3D > listener, final int index ); + static TransformState from( Consumer< AffineTransform3D > get, Consumer< AffineTransform3D > set ) + { + return new TransformState() + { + @Override + public void get( final AffineTransform3D transform ) + { + get.accept( transform ); + } - public void removeTransformListener( final TransformListener< AffineTransform3D > listener ); + @Override + public void set( final AffineTransform3D transform ) + { + set.accept( transform ); + } + }; + } } diff --git a/src/main/java/bdv/ViewerImgLoader.java b/src/main/java/bdv/ViewerImgLoader.java index dc6f76eef0d4cdb386d7d1b7daadb793e6d228dc..e398116b5160b8b078efe2ed5efeb0186cca8825 100644 --- a/src/main/java/bdv/ViewerImgLoader.java +++ b/src/main/java/bdv/ViewerImgLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -35,7 +34,7 @@ import mpicbg.spim.data.generic.sequence.BasicMultiResolutionImgLoader; public interface ViewerImgLoader extends BasicMultiResolutionImgLoader { @Override - public ViewerSetupImgLoader< ?, ? > getSetupImgLoader( final int setupId ); + ViewerSetupImgLoader< ?, ? > getSetupImgLoader( final int setupId ); - public CacheControl getCacheControl(); + CacheControl getCacheControl(); } diff --git a/src/main/java/bdv/ViewerSetupImgLoader.java b/src/main/java/bdv/ViewerSetupImgLoader.java index 3566132262bef633ee683fbe628e6f3ca5057e35..193749c992552636bd52b11caf92402a96b3a163 100644 --- a/src/main/java/bdv/ViewerSetupImgLoader.java +++ b/src/main/java/bdv/ViewerSetupImgLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -36,7 +35,7 @@ import net.imglib2.Volatile; public interface ViewerSetupImgLoader< T, V extends Volatile< T > > extends BasicMultiResolutionSetupImgLoader< T > { - public RandomAccessibleInterval< V > getVolatileImage( final int timepointId, final int level, ImgLoaderHint... hints ); + RandomAccessibleInterval< V > getVolatileImage( final int timepointId, final int level, ImgLoaderHint... hints ); - public V getVolatileImageType(); + V getVolatileImageType(); } diff --git a/src/main/java/bdv/VolatileSpimSource.java b/src/main/java/bdv/VolatileSpimSource.java index 4b7a245ad4e97da7ab0de68d162a4a1a727d8a18..9abd82c30405b1b66ec5759b1fdf70e3334c3d51 100644 --- a/src/main/java/bdv/VolatileSpimSource.java +++ b/src/main/java/bdv/VolatileSpimSource.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE diff --git a/src/main/java/bdv/cache/CacheControl.java b/src/main/java/bdv/cache/CacheControl.java index 0a6ee06dadcbfa1043b5d9aad392578d27e2767c..a4b96910dcb34ad527c5cfb57ab2ed629bfaa734 100644 --- a/src/main/java/bdv/cache/CacheControl.java +++ b/src/main/java/bdv/cache/CacheControl.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -40,7 +39,7 @@ import bdv.img.cache.VolatileGlobalCellCache; * {@link VolatileGlobalCellCache}, these can be simply implemented to do * nothing. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public interface CacheControl { @@ -54,12 +53,12 @@ public interface CacheControl * previously enqueued requests to be enqueued again for the new frame. * </ul> */ - public void prepareNextFrame(); + void prepareNextFrame(); /** * {@link CacheControl} that does nothing. */ - public static class Dummy implements CacheControl + class Dummy implements CacheControl { @Override public void prepareNextFrame() @@ -70,7 +69,7 @@ public interface CacheControl * {@link CacheControl} backed by a set of {@link CacheControl}s. * {@link #prepareNextFrame()} forwards to all of them. */ - public static class CacheControls implements CacheControl + class CacheControls implements CacheControl { private final CopyOnWriteArrayList< CacheControl > cacheControls = new CopyOnWriteArrayList<>(); @@ -94,6 +93,11 @@ public interface CacheControl cacheControls.remove( cacheControl ); } + public synchronized void clear() + { + cacheControls.clear(); + } + @Override public void prepareNextFrame() { diff --git a/src/main/java/bdv/export/CopyBlock.java b/src/main/java/bdv/export/CopyBlock.java new file mode 100644 index 0000000000000000000000000000000000000000..2171fc21cce4cc4b18b829bbaed92b1a1a2c78dc --- /dev/null +++ b/src/main/java/bdv/export/CopyBlock.java @@ -0,0 +1,147 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.export; + +import java.util.Arrays; +import net.imglib2.RandomAccess; +import net.imglib2.loops.ClassCopyProvider; +import net.imglib2.type.numeric.RealType; + +public interface CopyBlock< T extends RealType< T > > +{ + void copyBlock( final RandomAccess< T > in, final RandomAccess< T > out, final int[] dimensions ); + + static < T extends RealType< T > > CopyBlock< T > create( + final int numDimensions, + final Class< ? > pixelTypeClass, + final Class< ? > inAccessClass ) + { + return CopyBlockInstances.create( numDimensions, pixelTypeClass, inAccessClass ); + } +} + +class CopyBlockInstances +{ + @SuppressWarnings( "rawtypes" ) + private static ClassCopyProvider< CopyBlock > provider; + + @SuppressWarnings( "unchecked" ) + public static < T extends RealType< T > > CopyBlock< T > create( + final int numDimensions, + final Class< ? > pixelTypeClass, + final Class< ? > inAccessClass ) + { + if ( provider == null ) + { + synchronized ( CopyBlockInstances.class ) + { + if ( provider == null ) + provider = new ClassCopyProvider<>( Imp.class, CopyBlock.class, int.class ); + } + } + + Object key = Arrays.asList( numDimensions, pixelTypeClass, inAccessClass ); + return provider.newInstanceForKey( key, numDimensions ); + } + + public static class Imp< T extends RealType< T > > implements CopyBlock< T > + { + private final int n; + + public Imp( final int n ) + { + if ( n < 1 || n > 3 ) + throw new IllegalArgumentException(); + + this.n = n; + } + + @Override + public void copyBlock( + final RandomAccess< T > in, + final RandomAccess< T > out, + final int[] dimensions ) + { + if ( n == 3 ) + copyBlock3D( out, dimensions[ 0 ], dimensions[ 1 ], dimensions[ 2 ], in ); + else if ( n == 2 ) + copyBlock2D( out, dimensions[ 0 ], dimensions[ 1 ], in ); + else + copyBlock1D( out, dimensions[ 0 ], in ); + } + + private void copyBlock3D( + final RandomAccess< T > out, + final int sx, // size of output image + final int sy, + final int sz, + final RandomAccess< T > in ) + { + for ( int z = 0; z < sz; ++z ) + { + copyBlock2D( out, sx, sy, in ); + out.fwd( 2 ); + in.fwd( 2 ); + } + out.move( -sz, 2 ); + in.move( -sz, 2 ); + } + + private void copyBlock2D( + final RandomAccess< T > out, + final int sx, // size of output image + final int sy, + final RandomAccess< T > in ) + { + for ( int y = 0; y < sy; ++y ) + { + copyBlock1D( out, sx, in ); + out.fwd( 1 ); + in.fwd( 1 ); + } + out.move( -sy, 1 ); + in.move( -sy, 1 ); + } + + private void copyBlock1D( + final RandomAccess< T > out, + final int sx, // size of output image + final RandomAccess< T > in ) + { + for ( int x = 0; x < sx; ++x ) + { + out.get().set( in.get() ); + out.fwd( 0 ); + in.fwd( 0 ); + } + out.move( -sx, 0 ); + in.move( -sx, 0 ); + } + } +} diff --git a/src/main/java/bdv/export/Downsample.java b/src/main/java/bdv/export/Downsample.java index 5d270db0f34e1d5d7c6c39eeefdea81e9fb29894..8ddc2dda3bc74d34e7fe7a484feb46ceb40d2392 100644 --- a/src/main/java/bdv/export/Downsample.java +++ b/src/main/java/bdv/export/Downsample.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -44,6 +43,9 @@ import net.imglib2.view.Views; public class Downsample { + /** + * TODO: Revise. This is probably not very efficient + */ public static < T extends RealType< T > > void downsample( final RandomAccessible< T > input, final RandomAccessibleInterval< T > output, final int[] factor ) { assert input.numDimensions() == output.numDimensions(); diff --git a/src/main/java/bdv/export/DownsampleBlock.java b/src/main/java/bdv/export/DownsampleBlock.java new file mode 100644 index 0000000000000000000000000000000000000000..ce07e3e8e7952244b65ce5876deef25d5816d407 --- /dev/null +++ b/src/main/java/bdv/export/DownsampleBlock.java @@ -0,0 +1,250 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.export; + +import java.util.Arrays; +import net.imglib2.Cursor; +import net.imglib2.RandomAccess; +import net.imglib2.img.array.ArrayImgs; +import net.imglib2.loops.ClassCopyProvider; +import net.imglib2.type.numeric.RealType; +import net.imglib2.type.numeric.real.DoubleType; +import net.imglib2.util.Intervals; + +public interface DownsampleBlock< T extends RealType< T > > +{ + void downsampleBlock( final RandomAccess< T > in, final Cursor< T > out, final int[] dimensions ); + + static < T extends RealType< T > > DownsampleBlock< T > create( + final int[] blockDimensions, + final int[] downsamplingFactors, + final Class< ? > pixelTypeClass, + final Class< ? > inAccessClass ) + { + return DownsampleBlockInstances.create( blockDimensions, downsamplingFactors, pixelTypeClass, inAccessClass ); + } +} + +class DownsampleBlockInstances +{ + @SuppressWarnings( "rawtypes" ) + private static ClassCopyProvider< DownsampleBlock > provider; + + @SuppressWarnings( "unchecked" ) + public static < T extends RealType< T > > DownsampleBlock< T > create( + final int[] blockDimensions, + final int[] downsamplingFactors, + final Class< ? > pixelTypeClass, + final Class< ? > inAccessClass ) + { + if ( provider == null ) + { + synchronized ( DownsampleBlockInstances.class ) + { + if ( provider == null ) + provider = new ClassCopyProvider<>( Imp.class, DownsampleBlock.class, int[].class, int[].class ); + } + } + + final int numDimensions = blockDimensions.length; + + Object key = Arrays.asList( numDimensions, pixelTypeClass, inAccessClass ); + return provider.newInstanceForKey( key, blockDimensions, downsamplingFactors ); + } + + public static class Imp< T extends RealType< T > > implements DownsampleBlock< T > + { + private final int n; + + private final int[] downsamplingFactors; + + private final double scale; + + private final double[] accumulator; + + private final RandomAccess< DoubleType > acc; + + public Imp( + final int[] blockDimensions, + final int[] downsamplingFactors ) + { + n = blockDimensions.length; + if ( n < 1 || n > 3 ) + throw new IllegalArgumentException(); + + this.downsamplingFactors = downsamplingFactors; + scale = 1.0 / Intervals.numElements( downsamplingFactors ); + + accumulator = new double[ ( int ) Intervals.numElements( blockDimensions ) ]; + + final long[] dims = new long[ n ]; + Arrays.setAll( dims, d -> blockDimensions[ d ] ); + acc = ArrayImgs.doubles( accumulator, dims ).randomAccess(); + } + + @Override + public void downsampleBlock( + final RandomAccess< T > in, + final Cursor< T > out, // must be flat iteration order + final int[] dimensions ) + { + clearAccumulator(); + + if ( n == 3 ) + { + downsampleBlock3D( acc, dimensions[ 0 ], dimensions[ 1 ], dimensions[ 2 ], in ); + writeOutput3D( out, dimensions[ 0 ], dimensions[ 1 ], dimensions[ 2 ], acc ); + } + else if ( n == 2 ) + { + downsampleBlock2D( acc, dimensions[ 0 ], dimensions[ 1 ], in ); + writeOutput2D( out, dimensions[ 0 ], dimensions[ 1 ], acc ); + } + else + { + downsampleBlock1D( acc, dimensions[ 0 ], in ); + writeOutput1D( out, dimensions[ 0 ], acc ); + } + } + + private void clearAccumulator() + { + Arrays.fill( accumulator, 0, accumulator.length, 0 ); + } + + private void downsampleBlock3D( + final RandomAccess< DoubleType > acc, + final int asx, // size of output (resp accumulator) image + final int asy, + final int asz, + final RandomAccess< T > in ) + { + final int bsz = downsamplingFactors[ 2 ]; + final int sz = asz * bsz; + for ( int z = 0, bz = 0; z < sz; ++z ) + { + downsampleBlock2D( acc, asx, asy, in ); + in.fwd( 2 ); + if ( ++bz == bsz ) + { + bz = 0; + acc.fwd( 2 ); + } + } + in.move( -sz, 2 ); + acc.move( -asz, 2 ); + } + + private void downsampleBlock2D( + final RandomAccess< DoubleType > acc, + final int asx, // size of output (resp accumulator) image + final int asy, + final RandomAccess< T > in ) + { + final int bsy = downsamplingFactors[ 1 ]; + final int sy = asy * bsy; + for ( int y = 0, by = 0; y < sy; ++y ) + { + downsampleBlock1D( acc, asx, in ); + in.fwd( 1 ); + if ( ++by == bsy ) + { + by = 0; + acc.fwd( 1 ); + } + } + in.move( -sy, 1 ); + acc.move( -asy, 1 ); + } + + private void downsampleBlock1D( + final RandomAccess< DoubleType > acc, + final int asx, // size of output (resp accumulator) image + final RandomAccess< T > in ) + { + final int bsx = downsamplingFactors[ 0 ]; + final int sx = asx * bsx; + for ( int x = 0, bx = 0; x < sx; ++x ) + { + acc.get().set( acc.get().get() + in.get().getRealDouble() ); + in.fwd( 0 ); + if ( ++bx == bsx ) + { + bx = 0; + acc.fwd( 0 ); + } + } + in.move( -sx, 0 ); + acc.move( -asx, 0 ); + } + + private void writeOutput3D( + final Cursor< T > out, // must be flat iteration order + final int asx, // size of output (resp accumulator) image + final int asy, + final int asz, + final RandomAccess< DoubleType > acc ) + { + for ( int z = 0; z < asz; ++z ) + { + writeOutput2D( out, asx, asy, acc ); + acc.fwd( 2 ); + } + acc.move( -asz, 2 ); + } + + private void writeOutput2D( + final Cursor< T > out, // must be flat iteration order + final int asx, // size of output (resp accumulator) image + final int asy, + final RandomAccess< DoubleType > acc ) + { + for ( int y = 0; y < asy; ++y ) + { + writeOutput1D( out, asx, acc ); + acc.fwd( 1 ); + } + acc.move( -asy, 1 ); + } + + private void writeOutput1D( + final Cursor< T > out, // must be flat iteration order + final int asx, // size of output (resp accumulator) image + final RandomAccess< DoubleType > acc ) + { + final double scale = this.scale; + for ( int x = 0; x < asx; ++x ) + { + out.next().setReal( acc.get().get() * scale ); + acc.fwd( 0 ); + } + acc.move( -asx, 0 ); + } + } +} diff --git a/src/main/java/bdv/export/ExportMipmapInfo.java b/src/main/java/bdv/export/ExportMipmapInfo.java index 5a258314272bf30a51892d8233358775b3b0125b..c146fe4aa8da16ce3d1cb7509417fc9e91c940c2 100644 --- a/src/main/java/bdv/export/ExportMipmapInfo.java +++ b/src/main/java/bdv/export/ExportMipmapInfo.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/export/ExportScalePyramid.java b/src/main/java/bdv/export/ExportScalePyramid.java new file mode 100644 index 0000000000000000000000000000000000000000..52c85d67682f9d755b1851ade8e1a4667116911e --- /dev/null +++ b/src/main/java/bdv/export/ExportScalePyramid.java @@ -0,0 +1,441 @@ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.export; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; + +import net.imglib2.FinalInterval; +import net.imglib2.RandomAccess; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.cache.img.SingleCellArrayImg; +import net.imglib2.img.basictypeaccess.ArrayDataAccessFactory; +import net.imglib2.img.basictypeaccess.array.ArrayDataAccess; +import net.imglib2.img.cell.CellGrid; +import net.imglib2.type.NativeType; +import net.imglib2.type.NativeTypeFactory; +import net.imglib2.type.numeric.RealType; +import net.imglib2.util.Cast; +import net.imglib2.util.Intervals; +import net.imglib2.view.Views; + +/** + * Write an image to a chunked mipmap representation. + */ +public class ExportScalePyramid +{ + /** + * A heuristic to decide for a given resolution level whether the source + * pixels should be taken from the original image or read from a previously + * written resolution level in the output dataset. + */ + public interface LoopbackHeuristic + { + /** + * @return {@code true} if source pixels should be read back from + * dataset. {@code false} if source pixels should be taken from + * original image. + */ + boolean decide( + final RandomAccessibleInterval< ? > originalImg, + final int[] factorsToOriginalImg, + final int previousLevel, + final int[] factorsToPreviousLevel, + final int[] chunkSize ); + } + + /** + * Simple heuristic: use loopback image loader if saving 8 times or more on + * number of pixel access with respect to the original image. + */ + public static class DefaultLoopbackHeuristic implements LoopbackHeuristic + { + @Override + public boolean decide( final RandomAccessibleInterval< ? > originalImg, final int[] factorsToOriginalImg, final int previousLevel, final int[] factorsToPreviousLevel, final int[] chunkSize ) + { + if ( previousLevel < 0 ) + return false; + + if ( Intervals.numElements( factorsToOriginalImg ) / Intervals.numElements( factorsToPreviousLevel ) >= 8 ) + return true; + + return false; + } + } + + /** + * Callback that is called after each "plane of blocks" is written, giving + * the opportunity to clear caches, etc. + */ + public interface AfterEachPlane + { + /** + * Called after a "plane of blocks" is written. + * + * @param usedLoopBack + * {@code true}, if source was previously written resolution + * level in the output dataset. {@code false}, if source was + * the original image. + */ + void afterEachPlane( final boolean usedLoopBack ); + } + + /** + * A block to be written. See {@link DatasetIO#writeBlock(Object, Block) + * DatasetIO.writeBlock()}. + */ + public static class Block< T extends NativeType< T > > + { + final SingleCellArrayImg< T, ? > data; + final int[] size; + final long[] position; + + Block( final SingleCellArrayImg< T, ? > data, final int[] size, final long[] position ) + { + this.data = data; + this.size = size; + this.position = position; + } + + public SingleCellArrayImg< T, ? > getData() + { + return data; + } + + public int[] getSize() + { + return size; + } + + public long[] getGridPosition() + { + return position; + } + } + + /** + * Writing and reading back data for each resolution level. + * + * @param <D> + * Dataset handle + * @param <T> + * Pixel type + */ + public interface DatasetIO< D, T extends NativeType< T > > + { + /** + * Create a dataset for the image of the given resolution {@code level}. + * + * @return a handle to the dataset. + */ + D createDataset( + final int level, + final long[] dimensions, + final int[] blockSize ) throws IOException; + + /** + * Write the given {@code dataBlock} to the {@code dataset}. + */ + void writeBlock( + final D dataset, + final Block< T > dataBlock ) throws IOException; + + /** + * Blocks until all pending data was written to {@code dataset}. + */ + void flush( D dataset ) throws IOException; + + /** + * Opens a dataset that was already written as a + * {@code RaπdomAccessibleInterval}. + */ + default RandomAccessibleInterval< T > getImage( final int level ) throws IOException + { + return null; + } + } + + /** + * Write an image to a chunked mipmap representation. + * + * @param img + * the image to be written. + * @param type + * instance of the pixel type of the image. + * @param mipmapInfo + * contains for each mipmap level of the setup, the subsampling + * factors and block sizes. + * @param io + * writer for image blocks. + * @param executorService + * ExecutorService where block-creator tasks are submitted. + * @param numThreads + * How many block-creator tasks to run in parallel. (This many + * tasks are submitted to the @code + * @param loopbackHeuristic + * heuristic to decide whether to create each resolution level by + * reading pixels from the original image or by reading back a + * finer resolution level already written to the hdf5. may be + * null (in this case always use the original image). + * @param afterEachPlane + * this is called after each "plane of blocks" is written, giving + * the opportunity to clear caches, etc. may be null. + * @param progressWriter + * completion ratio and status output will be directed here. may + * be null. + * + * @param <T> + * Pixel type + * @param <D> + * Dataset handle + * + * @throws IOException + */ + public static < T extends RealType< T > & NativeType< T >, D > void writeScalePyramid( + final RandomAccessibleInterval< T > img, + final T type, + final ExportMipmapInfo mipmapInfo, + final DatasetIO< D, T > io, + final ExecutorService executorService, + final int numThreads, + final LoopbackHeuristic loopbackHeuristic, + final AfterEachPlane afterEachPlane, + ProgressWriter progressWriter ) throws IOException + { + final BlockCreator< T > blockCreator = BlockCreator.forType( type ); + + if ( progressWriter == null ) + progressWriter = new ProgressWriterNull(); + + // for progressWriter + final int numTasks = mipmapInfo.getNumLevels(); + int numCompletedTasks = 0; + progressWriter.setProgress( 0.0 ); + + // write image data for all views to the HDF5 file + final int n = 3; // TODO checkNumDimensions( img.numDimensions() ); + final long[] dimensions = new long[ n ]; + + final int[][] resolutions = mipmapInfo.getExportResolutions(); + final int[][] subdivisions = mipmapInfo.getSubdivisions(); + final int numLevels = mipmapInfo.getNumLevels(); + + for ( int level = 0; level < numLevels; ++level ) + { + progressWriter.out().println( "writing level " + level ); + + boolean useLoopBack = false; + int[] factorsToPreviousLevel = null; + RandomAccessibleInterval< T > loopbackImg = null; + if ( loopbackHeuristic != null ) + { + // Are downsampling factors a multiple of a level that we have + // already written? + int previousLevel = -1; + A: + for ( int l = level - 1; l >= 0; --l ) + { + final int[] f = new int[ n ]; + for ( int d = 0; d < n; ++d ) + { + f[ d ] = resolutions[ level ][ d ] / resolutions[ l ][ d ]; + if ( f[ d ] * resolutions[ l ][ d ] != resolutions[ level ][ d ] ) + continue A; + } + factorsToPreviousLevel = f; + previousLevel = l; + break; + } + // Now, if previousLevel >= 0 we can use loopback ImgLoader on + // previousLevel and downsample with factorsToPreviousLevel. + // + // whether it makes sense to actually do so is determined by a + // heuristic based on the following considerations: + // * if downsampling a lot over original image, the cost of + // reading images back from hdf5 outweighs the cost of + // accessing and averaging original pixels. + // * original image may already be cached (for example when + // exporting an ImageJ virtual stack. To compute blocks + // that downsample a lot in Z, many planes of the virtual + // stack need to be accessed leading to cache thrashing if + // individual planes are very large. + + if ( previousLevel >= 0 ) + useLoopBack = loopbackHeuristic.decide( img, resolutions[ level ], previousLevel, factorsToPreviousLevel, subdivisions[ level ] ); + + if ( useLoopBack ) + loopbackImg = io.getImage( previousLevel ); + + if ( loopbackImg == null ) + useLoopBack = false; + } + + final RandomAccessibleInterval< T > sourceImg; + final int[] factor; + if ( useLoopBack ) + { + sourceImg = loopbackImg; + factor = factorsToPreviousLevel; + } + else + { + sourceImg = img; + factor = resolutions[ level ]; + } + + sourceImg.dimensions( dimensions ); + + final long size = Intervals.numElements( factor ); + final boolean fullResolution = size == 1; + if ( !fullResolution ) + { + for ( int d = 0; d < n; ++d ) + dimensions[ d ] = Math.max( dimensions[ d ] / factor[ d ], 1 ); + } + + final long[] minRequiredInput = new long[ n ]; + final long[] maxRequiredInput = new long[ n ]; + sourceImg.min( minRequiredInput ); + for ( int d = 0; d < n; ++d ) + maxRequiredInput[ d ] = minRequiredInput[ d ] + dimensions[ d ] * factor[ d ] - 1; + + // TODO: pass OutOfBoundsFactory + final RandomAccessibleInterval< T > extendedImg = Views.interval( Views.extendBorder( sourceImg ), new FinalInterval( minRequiredInput, maxRequiredInput ) ); + + final int[] cellDimensions = subdivisions[ level ]; + final D dataset = io.createDataset( level, dimensions, cellDimensions ); + + final ProgressWriter subProgressWriter = new SubTaskProgressWriter( + progressWriter, ( double ) numCompletedTasks / numTasks, + ( double ) ( numCompletedTasks + 1 ) / numTasks ); + // generate one "plane" of cells after the other to avoid cache thrashing when exporting from virtual stacks + final CellGrid grid = new CellGrid( dimensions, cellDimensions ); + final long[] numCells = grid.getGridDimensions(); + final long numBlocksPerPlane = numElements( numCells, 0, 2 ); + final long numPlanes = numElements( numCells, 2, n ); + for ( int plane = 0; plane < numPlanes; ++plane ) + { + final long planeBaseIndex = numBlocksPerPlane * plane; + final AtomicInteger nextCellInPlane = new AtomicInteger(); + final List< Callable< Void > > tasks = new ArrayList<>(); + for ( int threadNum = 0; threadNum < numThreads; ++threadNum ) + { + tasks.add( () -> { + final long[] currentCellMin = new long[ n ]; + final int[] currentCellDim = new int[ n ]; + final long[] currentCellPos = new long[ n ]; + final long[] blockMin = new long[ n ]; + final RandomAccess< T > in = extendedImg.randomAccess(); + + final Class< ? extends RealType > kl1 = type.getClass(); + final Class< ? extends RandomAccess > kl2 = in.getClass(); + final CopyBlock< T > copyBlock = fullResolution ? CopyBlock.create( n, kl1, kl2 ) : null; + final DownsampleBlock< T > downsampleBlock = fullResolution ? null : DownsampleBlock.create( cellDimensions, factor, kl1, kl2 ); + + for ( int i = nextCellInPlane.getAndIncrement(); i < numBlocksPerPlane; i = nextCellInPlane.getAndIncrement() ) + { + final long index = planeBaseIndex + i; + + grid.getCellDimensions( index, currentCellMin, currentCellDim ); + grid.getCellGridPositionFlat( index, currentCellPos ); + final Block< T > block = blockCreator.create( currentCellDim, currentCellMin, currentCellPos ); + + if ( fullResolution ) + { + final RandomAccess< T > out = block.getData().randomAccess(); + in.setPosition( currentCellMin ); + out.setPosition( currentCellMin ); + copyBlock.copyBlock( in, out, currentCellDim ); + } + else + { + for ( int d = 0; d < n; ++d ) + blockMin[ d ] = currentCellMin[ d ] * factor[ d ]; + in.setPosition( blockMin ); + downsampleBlock.downsampleBlock( in, block.getData().cursor(), currentCellDim ); + } + + io.writeBlock( dataset, block ); + } + return null; + } ); + } + try + { + final List< Future< Void > > futures = executorService.invokeAll( tasks ); + for ( final Future< Void > future : futures ) + future.get(); + } + catch ( final InterruptedException | ExecutionException e ) + { + // TODO... + e.printStackTrace(); + throw new IOException( e ); + } + if ( afterEachPlane != null ) + afterEachPlane.afterEachPlane( useLoopBack ); + + subProgressWriter.setProgress( ( double ) plane / numPlanes ); + } + io.flush( dataset ); + progressWriter.setProgress( ( double ) ++numCompletedTasks / numTasks ); + } + } + + private static long numElements( final long[] size, final int mind, final int maxd ) + { + long numElements = 1; + for ( int d = mind; d < maxd; ++d ) + numElements *= size[ d ]; + return numElements; + } + + private interface BlockCreator< T extends NativeType< T > > + { + Block< T > create( final int[] blockSize, final long[] blockMin, final long[] gridPosition ); + + static < T extends NativeType< T > & RealType< T >, A extends ArrayDataAccess< A > > BlockCreator< T > forType( final T type ) + { + final A accessFactory = Cast.unchecked( ArrayDataAccessFactory.get( type ) ); + final NativeTypeFactory< T, A > nativeTypeFactory = Cast.unchecked( type.getNativeTypeFactory() ); + return ( blockSize, blockMin, gridPosition ) -> { + final A data = accessFactory.createArray( ( int ) Intervals.numElements( blockSize ) ); + final SingleCellArrayImg< T, A > img = new SingleCellArrayImg<>( blockSize, blockMin, data, null ); + img.setLinkedType( nativeTypeFactory.createLinkedType( img ) ); + return new Block<>( img, blockSize, gridPosition ); + }; + } + } +} diff --git a/src/main/java/bdv/export/HDF5Access.java b/src/main/java/bdv/export/HDF5Access.java index 5c31cf13fb017b9e8aa0bceb7321412ed697ba6d..5cff238cfff3f75f56e6803365135aa2d49c0c1f 100644 --- a/src/main/java/bdv/export/HDF5Access.java +++ b/src/main/java/bdv/export/HDF5Access.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/export/HDF5AccessHack.java b/src/main/java/bdv/export/HDF5AccessHack.java index 4e187bf8ea6156184b30c82ae455e97860c8bc0e..6cdc0ee7c0ca3a6b2ddc84a8c2576cfa5c50501a 100644 --- a/src/main/java/bdv/export/HDF5AccessHack.java +++ b/src/main/java/bdv/export/HDF5AccessHack.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/export/Hdf5BlockWriterThread.java b/src/main/java/bdv/export/Hdf5BlockWriterThread.java index b0cedc6eca21ae0dc64e0d11c0986c09e7b881ff..a799c401178b784857f1c861244dd7e26f88188e 100644 --- a/src/main/java/bdv/export/Hdf5BlockWriterThread.java +++ b/src/main/java/bdv/export/Hdf5BlockWriterThread.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -42,9 +41,9 @@ class Hdf5BlockWriterThread extends Thread implements IHDF5Access { private final IHDF5Access hdf5Access; - private static interface Hdf5Task + private interface Hdf5Task { - public void run( final IHDF5Access hdf5Access ); + void run( final IHDF5Access hdf5Access ); } private final BlockingQueue< Hdf5BlockWriterThread.Hdf5Task > queue; @@ -163,6 +162,7 @@ class Hdf5BlockWriterThread extends Thread implements IHDF5Access public void closeDataset() { put( new CloseDatasetTask() ); + waitUntilEmpty(); } private boolean put( final Hdf5BlockWriterThread.Hdf5Task task ) diff --git a/src/main/java/bdv/export/IHDF5Access.java b/src/main/java/bdv/export/IHDF5Access.java index 4c8b65bcf41ba4a429847b8b0381a141ad38b8eb..ac6dd19c401b8a2751430c07e7aaf02fc4418734 100644 --- a/src/main/java/bdv/export/IHDF5Access.java +++ b/src/main/java/bdv/export/IHDF5Access.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -34,16 +33,16 @@ import ch.systemsx.cisd.hdf5.IHDF5Writer; interface IHDF5Access { - public void writeMipmapDescription( final int setupIdPartition, final ExportMipmapInfo mipmapInfo ); + void writeMipmapDescription( final int setupIdPartition, final ExportMipmapInfo mipmapInfo ); - public void createAndOpenDataset( final String path, long[] dimensions, int[] cellDimensions, HDF5IntStorageFeatures features ); + void createAndOpenDataset( final String path, long[] dimensions, int[] cellDimensions, HDF5IntStorageFeatures features ); - public void writeBlockWithOffset( final short[] data, final long[] blockDimensions, final long[] offset ); + void writeBlockWithOffset( final short[] data, final long[] blockDimensions, final long[] offset ); - public void closeDataset(); + void closeDataset(); - public void close(); + void close(); // this is for sharing with Hdf5ImageLoader for loopback loader when exporting - public IHDF5Writer getIHDF5Writer(); + IHDF5Writer getIHDF5Writer(); } diff --git a/src/main/java/bdv/export/ProgressWriter.java b/src/main/java/bdv/export/ProgressWriter.java index 9172db18f2aef6adf9e14c0ee4010d9841da636a..f8ec44c2aa261d5f5da96013e8f72b0fdd992de5 100644 --- a/src/main/java/bdv/export/ProgressWriter.java +++ b/src/main/java/bdv/export/ProgressWriter.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -33,9 +32,9 @@ import java.io.PrintStream; public interface ProgressWriter { - public PrintStream out(); + PrintStream out(); - public PrintStream err(); + PrintStream err(); - public void setProgress( double completionRatio ); + void setProgress( double completionRatio ); } diff --git a/src/main/java/bdv/export/ProgressWriterConsole.java b/src/main/java/bdv/export/ProgressWriterConsole.java index d5cd56a990ac5ce6666caf16c14e11f5ce320876..a480b08a91b0ac18d3beaff88a293c7a014a5775 100644 --- a/src/main/java/bdv/export/ProgressWriterConsole.java +++ b/src/main/java/bdv/export/ProgressWriterConsole.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/export/ProgressWriterNull.java b/src/main/java/bdv/export/ProgressWriterNull.java new file mode 100644 index 0000000000000000000000000000000000000000..3337ff2eb1e734c96cf00c8c87a23dddcb420bf9 --- /dev/null +++ b/src/main/java/bdv/export/ProgressWriterNull.java @@ -0,0 +1,70 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.export; + +import java.io.OutputStream; +import java.io.PrintStream; + +public class ProgressWriterNull implements ProgressWriter +{ + private final PrintStream blackhole; + + public ProgressWriterNull() + { + blackhole = new PrintStream( new OutputStream() { + @Override + public void write( final int b ) + {} + + @Override + public void write( final byte[] b ) + {} + + @Override + public void write( final byte[] b, final int off, final int len ) + {} + } ); + } + + @Override + public PrintStream out() + { + return blackhole; + } + + @Override + public PrintStream err() + { + return blackhole; + } + + @Override + public void setProgress( final double completionRatio ) + {} +} diff --git a/src/main/java/bdv/export/ProposeMipmaps.java b/src/main/java/bdv/export/ProposeMipmaps.java index e71afb9214aae57521830c3a959f43d66d0610d3..0fe75311240bd5d8fafe557c21670ad8672b3f52 100644 --- a/src/main/java/bdv/export/ProposeMipmaps.java +++ b/src/main/java/bdv/export/ProposeMipmaps.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -30,6 +29,7 @@ package bdv.export; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -43,11 +43,13 @@ import mpicbg.spim.data.sequence.VoxelDimensions; * * <p> * Choice of proposed chunksize is not based on any hard benchmark data - * currently. Chunksize is set as either 16x16x16 or 32x32x4 depending on which - * one is closer to isotropic. It is very likely that more efficient choices can - * be found by manual tuning, depending on hardware and use case. + * currently. Chunk sizes are proposed such that chunks have power-of-two side + * lengths, are roughly square in world space, and contain close to (but not + * more than) a specified number of elements (4096 by default). It is very + * likely that more efficient choices can be found by manual tuning, depending + * on hardware and use case. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class ProposeMipmaps { @@ -69,12 +71,28 @@ public class ProposeMipmaps /** * Propose number of mipmap levels as well subsampling factors and chunk * size for each level, based on the image and voxel size of the given - * setup. + * setup. Chunks contain close to (but not more than) 4096 elements. * * @param setup * @return proposed mipmap settings */ public static ExportMipmapInfo proposeMipmaps( final BasicViewSetup setup ) + { + return proposeMipmaps( setup, 4096 ); + } + + /** + * Propose number of mipmap levels as well subsampling factors and chunk + * size for each level, based on the image and voxel size of the given + * setup. Chunk sizes are proposed such that chunks have power-of-two side + * lengths, are roughly square in world space, and contain close to (but not + * more than) {@code maxNumElements}. + * + * @param setup + * @param maxNumElements + * @return proposed mipmap settings + */ + public static ExportMipmapInfo proposeMipmaps( final BasicViewSetup setup, final int maxNumElements ) { final VoxelDimensions voxelSize = setup.getVoxelSize(); final double[] voxelScale = new double[ 3 ]; @@ -88,7 +106,7 @@ public class ProposeMipmaps final ArrayList< int[] > subdivisions = new ArrayList<>(); // for ( int level = 0;; ++level ) - while( true ) + while ( true ) { resolutions.add( res.clone() ); @@ -102,10 +120,7 @@ public class ProposeMipmaps dmax = d; } } - if ( ( 4 * vmax / 32 ) > ( 1 / vmax ) ) - subdivisions.add( subdiv_32_32_4[ dmax ] ); - else - subdivisions.add( subdiv_16_16_16 ); + subdivisions.add( suggestPoTBlockSize( voxelScale, maxNumElements ) ); setup.getSize().dimensions( size ); long maxSize = 0; @@ -175,7 +190,75 @@ public class ProposeMipmaps size[ d ] /= minVoxelDim; } - private static int[] subdiv_16_16_16 = new int[] { 16, 16, 16 }; + /** + * Propose block size such that + * <ol> + * <li>each dimension is power-of-two,</li> + * <li>number of elements is as big as possible, but not larger than + * {@code maxNumElements}</li> + * <li>and the block (scaled by the {@code voxelSize}) is as close to square + * as possible given constraints 1 and 2.</li> + * </ol> + */ + public static int[] suggestPoTBlockSize( final double[] voxelSize, final int maxNumElements ) + { + final int n = voxelSize.length; + final double[] bias = new double[ n ]; + Arrays.setAll( bias, d -> 0.01 * ( n - d ) ); + return suggestPoTBlockSize( voxelSize, maxNumElements, bias ); + } - private static int[][] subdiv_32_32_4 = new int[][] { { 4, 32, 32 }, { 32, 4, 32 }, { 32, 32, 4 } }; + /** + * Propose block size such that + * <ol> + * <li>each dimension is power-of-two,</li> + * <li>number of elements is as big as possible, but not larger than + * {@code maxNumElements}</li> + * <li>and the block (scaled by the {@code voxelSize}) is as close to square + * as possible given constraints 1 and 2.</li> + * </ol> + * + * Determination works by finding real PoT for each dimension, then rounding + * down, and increasing one by one the PoT for dimensions until going over + * maxNumElements. Dimensions are ordered by decreasing fractional remainder + * of real PoT plus some per-dimension bias (usually set such that X is + * enlarged before Y before Z...) + */ + private static int[] suggestPoTBlockSize( final double[] voxelSize, final int maxNumElements, final double[] bias ) + { + final int n = voxelSize.length; + final double[] shape = new double[ n ]; + double shapeVol = 1; + for ( int d = 0; d < n; ++d ) + { + shape[ d ] = 1 / voxelSize[ d ]; + shapeVol *= shape[ d ]; + } + final double m = Math.pow( maxNumElements / shapeVol, 1. / n ); + final double sumNumBits = Math.log( maxNumElements ) / Math.log( 2 ); + final double[] numBits = new double[ n ]; + Arrays.setAll( numBits, d -> Math.log( m * shape[ d ] ) / Math.log( 2 ) ); + final int[] intNumBits = new int[ n ]; + Arrays.setAll( intNumBits, d -> Math.max( 0, ( int ) numBits[ d ] ) ); + for ( int sumIntNumBits = Arrays.stream( intNumBits ).sum(); sumIntNumBits + 1 <= sumNumBits; ++sumIntNumBits ) + { + double maxDiff = 0; + int maxDiffDim = 0; + for ( int d = 0; d < n; ++d ) + { + final double diff = numBits[ d ] - intNumBits[ d ] + bias[ d ]; + if ( diff > maxDiff ) + { + maxDiff = diff; + maxDiffDim = d; + } + } + ++intNumBits[ maxDiffDim ]; + } + + final int[] blockSize = new int[ n ]; + for ( int d = 0; d < n; ++d ) + blockSize[ d ] = 1 << intNumBits[ d ]; + return blockSize; + } } diff --git a/src/main/java/bdv/export/SubTaskProgressWriter.java b/src/main/java/bdv/export/SubTaskProgressWriter.java index b3b0d3681c6d0f00b8cb8d2848b46548236c7dd9..7dc850d9263a85430ffe720026bb09642ead1b1b 100644 --- a/src/main/java/bdv/export/SubTaskProgressWriter.java +++ b/src/main/java/bdv/export/SubTaskProgressWriter.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/export/WriteSequenceToHdf5.java b/src/main/java/bdv/export/WriteSequenceToHdf5.java index a47599c1d18589b7aae471ec1b84072deeb8e721..a2294dfe97274bcf97ead885c67221c5fccdfbc4 100644 --- a/src/main/java/bdv/export/WriteSequenceToHdf5.java +++ b/src/main/java/bdv/export/WriteSequenceToHdf5.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -29,17 +28,10 @@ */ package bdv.export; -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.CountDownLatch; - -import bdv.export.WriteSequenceToHdf5.AfterEachPlane; -import bdv.export.WriteSequenceToHdf5.LoopbackHeuristic; +import bdv.export.ExportScalePyramid.AfterEachPlane; +import bdv.export.ExportScalePyramid.Block; +import bdv.export.ExportScalePyramid.DatasetIO; +import bdv.export.ExportScalePyramid.LoopbackHeuristic; import bdv.img.hdf5.Hdf5ImageLoader; import bdv.img.hdf5.Partition; import bdv.img.hdf5.Util; @@ -48,6 +40,15 @@ import ch.systemsx.cisd.hdf5.HDF5Factory; import ch.systemsx.cisd.hdf5.HDF5IntStorageFeatures; import ch.systemsx.cisd.hdf5.IHDF5Reader; import ch.systemsx.cisd.hdf5.IHDF5Writer; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import mpicbg.spim.data.XmlHelpers; import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription; import mpicbg.spim.data.generic.sequence.BasicImgLoader; @@ -56,19 +57,13 @@ import mpicbg.spim.data.generic.sequence.BasicViewSetup; import mpicbg.spim.data.sequence.TimePoint; import mpicbg.spim.data.sequence.TimePoints; import mpicbg.spim.data.sequence.ViewId; -import net.imglib2.Cursor; import net.imglib2.Dimensions; -import net.imglib2.FinalInterval; -import net.imglib2.RandomAccess; import net.imglib2.RandomAccessibleInterval; -import net.imglib2.img.array.ArrayImg; -import net.imglib2.img.array.ArrayImgs; -import net.imglib2.img.basictypeaccess.array.ShortArray; +import net.imglib2.cache.img.SingleCellArrayImg; import net.imglib2.img.cell.CellImg; -import net.imglib2.iterator.LocalizingIntervalIterator; -import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.integer.UnsignedShortType; -import net.imglib2.view.Views; +import net.imglib2.util.Cast; +import net.imglib2.util.Intervals; /** * Create a hdf5 files containing image data from all views and all timepoints @@ -96,7 +91,7 @@ import net.imglib2.view.Views; * A data-set can be stored in a single hdf5 file or split across several hdf5 * "partitions" with one master hdf5 linking into the partitions. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class WriteSequenceToHdf5 { @@ -400,8 +395,7 @@ public class WriteSequenceToHdf5 writerQueue.start(); // start CellCreatorThreads - final CellCreatorThread[] cellCreatorThreads = createAndStartCellCreatorThreads( numCellCreatorThreads ); - + final ExecutorService executorService = Executors.newFixedThreadPool( numCellCreatorThreads ); try { // calculate number of tasks for progressWriter @@ -456,13 +450,13 @@ public class WriteSequenceToHdf5 writeViewToHdf5PartitionFile( img, timepointIdPartition, setupIdPartition, mipmapInfo, false, - deflate, writerQueue, cellCreatorThreads, loopbackHeuristic, afterEachPlane, subProgressWriter ); + deflate, writerQueue, executorService, numCellCreatorThreads, loopbackHeuristic, afterEachPlane, subProgressWriter ); } } } finally { - stopCellCreatorThreads( cellCreatorThreads ); + executorService.shutdown(); } } finally { @@ -534,15 +528,15 @@ public class WriteSequenceToHdf5 try { writerQueue.start(); - final CellCreatorThread[] cellCreatorThreads = createAndStartCellCreatorThreads( numCellCreatorThreads ); + final ExecutorService executorService = Executors.newFixedThreadPool( numCellCreatorThreads ); try { // write the image - writeViewToHdf5PartitionFile( img, timepointIdPartition, setupIdPartition, mipmapInfo, writeMipmapInfo, deflate, writerQueue, cellCreatorThreads, loopbackHeuristic, afterEachPlane, progressWriter ); + writeViewToHdf5PartitionFile( img, timepointIdPartition, setupIdPartition, mipmapInfo, writeMipmapInfo, deflate, writerQueue, executorService, numCellCreatorThreads, loopbackHeuristic, afterEachPlane, progressWriter ); } finally { - stopCellCreatorThreads( cellCreatorThreads ); + executorService.shutdown(); } } finally @@ -569,6 +563,57 @@ public class WriteSequenceToHdf5 } } + /* + TODO: + This approximately implements DatasetIO in terms of IHDFAccess. + It works with how DatasetIO is currently used, + but for general usage, IHDF5Access must be revised. + */ + static class HDF5DatasetIO implements DatasetIO< Object, UnsignedShortType > + { + private final IHDF5Access writerQueue; + private final ViewId viewIdPartition; + private final HDF5IntStorageFeatures storage; + private final LoopBackImageLoader loopback; + + public HDF5DatasetIO( final IHDF5Access writerQueue, final ViewId viewIdPartition, final HDF5IntStorageFeatures storage, final LoopBackImageLoader loopback ) + { + this.writerQueue = writerQueue; + this.viewIdPartition = viewIdPartition; + this.storage = storage; + this.loopback = loopback; + } + + @Override + public Object createDataset( final int level, final long[] dimensions, final int[] blockSize ) + { + final String path = Util.getCellsPath( viewIdPartition, level ); + writerQueue.createAndOpenDataset( path, dimensions.clone(), blockSize.clone(), storage ); + return null; + } + + @Override + public void writeBlock( final Object dataset, final Block< UnsignedShortType > dataBlock ) + { + final SingleCellArrayImg< UnsignedShortType, ? > img = dataBlock.getData(); + final long[] blockDimensions = Intervals.dimensionsAsLongArray( img ); + final long[] offset = Intervals.minAsLongArray( img ); + writerQueue.writeBlockWithOffset( Cast.unchecked( img.getStorageArray() ), blockDimensions, offset ); + } + + @Override + public void flush( final Object dataset ) + { + writerQueue.closeDataset(); + } + + @Override + public RandomAccessibleInterval< UnsignedShortType > getImage( final int level ) + { + return loopback.getSetupImgLoader( viewIdPartition.getViewSetupId() ).getImage( viewIdPartition.getTimePointId(), level ); + } + } + /** * Write a single view to a hdf5 partition file, in a chunked, mipmaped * representation. Note that the specified view must not already exist in @@ -594,9 +639,10 @@ public class WriteSequenceToHdf5 * whether to compress the data with the HDF5 DEFLATE filter. * @param writerQueue * block writing tasks are enqueued here. - * @param cellCreatorThreads - * threads used for creating (possibly down-sampled) blocks of + * @param executorService + * executor used for creating (possibly down-sampled) blocks of * the view to be written. + * @param numThreads * @param loopbackHeuristic * heuristic to decide whether to create each resolution level by * reading pixels from the original image or by reading back a @@ -616,22 +662,13 @@ public class WriteSequenceToHdf5 final ExportMipmapInfo mipmapInfo, final boolean writeMipmapInfo, final boolean deflate, - final Hdf5BlockWriterThread writerQueue, - final CellCreatorThread[] cellCreatorThreads, + final IHDF5Access writerQueue, + final ExecutorService executorService, // TODO + final int numThreads, // TODO final LoopbackHeuristic loopbackHeuristic, final AfterEachPlane afterEachPlane, ProgressWriter progressWriter ) { - final HDF5IntStorageFeatures storage = deflate ? HDF5IntStorageFeatures.INT_AUTO_SCALING_DEFLATE : HDF5IntStorageFeatures.INT_AUTO_SCALING; - - if ( progressWriter == null ) - progressWriter = new ProgressWriterConsole(); - - // for progressWriter - final int numTasks = mipmapInfo.getNumLevels(); - int numCompletedTasks = 0; - progressWriter.setProgress( 0.0 ); - // write Mipmap descriptions if ( writeMipmapInfo ) writerQueue.writeMipmapDescription( setupIdPartition, mipmapInfo ); @@ -640,359 +677,31 @@ public class WriteSequenceToHdf5 // h5 for generating low-resolution versions. final LoopBackImageLoader loopback = ( loopbackHeuristic == null ) ? null : LoopBackImageLoader.create( writerQueue.getIHDF5Writer(), timepointIdPartition, setupIdPartition, img ); - // write image data for all views to the HDF5 file - final int n = 3; - final long[] dimensions = new long[ n ]; - - final int[][] resolutions = mipmapInfo.getExportResolutions(); - final int[][] subdivisions = mipmapInfo.getSubdivisions(); - final int numLevels = mipmapInfo.getNumLevels(); - - for ( int level = 0; level < numLevels; ++level ) - { - progressWriter.out().println( "writing level " + level ); - - final RandomAccessibleInterval< UnsignedShortType > sourceImg; - final int[] factor; - final boolean useLoopBack; - if ( loopbackHeuristic == null ) - { - sourceImg = img; - factor = resolutions[ level ]; - useLoopBack = false; - } - else - { - // Are downsampling factors a multiple of a level that we have - // already written? - int[] factorsToPreviousLevel = null; - int previousLevel = -1; - A: for ( int l = level - 1; l >= 0; --l ) - { - final int[] f = new int[ n ]; - for ( int d = 0; d < n; ++d ) - { - f[ d ] = resolutions[ level ][ d ] / resolutions[ l ][ d ]; - if ( f[ d ] * resolutions[ l ][ d ] != resolutions[ level ][ d ] ) - continue A; - } - factorsToPreviousLevel = f; - previousLevel = l; - break; - } - // Now, if previousLevel >= 0 we can use loopback ImgLoader on - // previousLevel and downsample with factorsToPreviousLevel. - // - // whether it makes sense to actually do so is determined by a - // heuristic based on the following considerations: - // * if downsampling a lot over original image, the cost of - // reading images back from hdf5 outweighs the cost of - // accessing and averaging original pixels. - // * original image may already be cached (for example when - // exporting an ImageJ virtual stack. To compute blocks - // that downsample a lot in Z, many planes of the virtual - // stack need to be accessed leading to cache thrashing if - // individual planes are very large. - - useLoopBack = loopbackHeuristic.decide( img, resolutions[ level ], previousLevel, factorsToPreviousLevel, subdivisions[ level ] ); - if ( useLoopBack ) - { - sourceImg = loopback.getSetupImgLoader( setupIdPartition ).getImage( timepointIdPartition, previousLevel ); - factor = factorsToPreviousLevel; - } - else - { - sourceImg = img; - factor = resolutions[ level ]; - } - } - - sourceImg.dimensions( dimensions ); - final boolean fullResolution = ( factor[ 0 ] == 1 && factor[ 1 ] == 1 && factor[ 2 ] == 1 ); - long size = 1; - if ( !fullResolution ) - { - for ( int d = 0; d < n; ++d ) - { - dimensions[ d ] = Math.max( dimensions[ d ] / factor[ d ], 1 ); - size *= factor[ d ]; - } - } - final double scale = 1.0 / size; - - final long[] minRequiredInput = new long[ n ]; - final long[] maxRequiredInput = new long[ n ]; - sourceImg.min( minRequiredInput ); - for ( int d = 0; d < n; ++d ) - maxRequiredInput[ d ] = minRequiredInput[ d ] + dimensions[ d ] * factor[ d ] - 1; - final RandomAccessibleInterval< UnsignedShortType > extendedImg = Views.interval( Views.extendBorder( sourceImg ), new FinalInterval( minRequiredInput, maxRequiredInput ) ); - - final int[] cellDimensions = subdivisions[ level ]; - final ViewId viewIdPartition = new ViewId( timepointIdPartition, setupIdPartition ); - final String path = Util.getCellsPath( viewIdPartition, level ); - writerQueue.createAndOpenDataset( path, dimensions.clone(), cellDimensions.clone(), storage ); - - final long[] numCells = new long[ n ]; - final int[] borderSize = new int[ n ]; - final long[] minCell = new long[ n ]; - final long[] maxCell = new long[ n ]; - for ( int d = 0; d < n; ++d ) - { - numCells[ d ] = ( dimensions[ d ] - 1 ) / cellDimensions[ d ] + 1; - maxCell[ d ] = numCells[ d ] - 1; - borderSize[ d ] = ( int ) ( dimensions[ d ] - ( numCells[ d ] - 1 ) * cellDimensions[ d ] ); - } - - ProgressWriter subProgressWriter = new SubTaskProgressWriter( - progressWriter, (double) numCompletedTasks / numTasks, - (double) (numCompletedTasks + 1) / numTasks); - // generate one "plane" of cells after the other to avoid cache thrashing when exporting from virtual stacks - for ( int lastDimCell = 0; lastDimCell < numCells[ n - 1 ]; ++lastDimCell ) - { - minCell[ n - 1 ] = lastDimCell; - maxCell[ n - 1 ] = lastDimCell; - final LocalizingIntervalIterator i = new LocalizingIntervalIterator( minCell, maxCell ); - - final int numThreads = cellCreatorThreads.length; - final CountDownLatch doneSignal = new CountDownLatch( numThreads ); - for ( int threadNum = 0; threadNum < numThreads; ++threadNum ) - { - cellCreatorThreads[ threadNum ].run( new Runnable() - { - @Override - public void run() - { - final double[] accumulator = fullResolution ? null : new double[ cellDimensions[ 0 ] * cellDimensions[ 1 ] * cellDimensions[ 2 ] ]; - final long[] currentCellMin = new long[ n ]; - final long[] currentCellMax = new long[ n ]; - final long[] currentCellDim = new long[ n ]; - final long[] currentCellPos = new long[ n ]; - final long[] blockMin = new long[ n ]; - final RandomAccess< UnsignedShortType > in = extendedImg.randomAccess(); - while ( true ) - { - synchronized ( i ) - { - if ( !i.hasNext() ) - break; - i.fwd(); - i.localize( currentCellPos ); - } - for ( int d = 0; d < n; ++d ) - { - currentCellMin[ d ] = currentCellPos[ d ] * cellDimensions[ d ]; - blockMin[ d ] = currentCellMin[ d ] * factor[ d ]; - final boolean isBorderCellInThisDim = ( currentCellPos[ d ] + 1 == numCells[ d ] ); - currentCellDim[ d ] = isBorderCellInThisDim ? borderSize[ d ] : cellDimensions[ d ]; - currentCellMax[ d ] = currentCellMin[ d ] + currentCellDim[ d ] - 1; - } - - final ArrayImg< UnsignedShortType, ? > cell = ArrayImgs.unsignedShorts( currentCellDim ); - if ( fullResolution ) - copyBlock( cell.randomAccess(), currentCellDim, in, blockMin ); - else - downsampleBlock( cell.cursor(), accumulator, currentCellDim, in, blockMin, factor, scale ); - - writerQueue.writeBlockWithOffset( ( ( ShortArray ) cell.update( null ) ).getCurrentStorageArray(), currentCellDim.clone(), currentCellMin.clone() ); - } - doneSignal.countDown(); - } - } ); - } - try - { - doneSignal.await(); - } - catch ( final InterruptedException e ) - { - e.printStackTrace(); - } - if ( afterEachPlane != null ) - afterEachPlane.afterEachPlane( useLoopBack ); - - subProgressWriter.setProgress((double) lastDimCell / numCells[n - 1]); - } - writerQueue.closeDataset(); - progressWriter.setProgress( ( double ) ++numCompletedTasks / numTasks ); - } - if ( loopback != null ) - loopback.close(); - } - - /** - * A heuristic to decide for a given resolution level whether the source - * pixels should be taken from the original image or read from a previously - * written resolution level in the hdf5 file. - */ - public interface LoopbackHeuristic - { - /** - * @return {@code true} if source pixels should be read back from hdf5 - * file. {@code false} if source pixels should be taken from - * original image. - */ - public boolean decide( - final RandomAccessibleInterval< ? > originalImg, - final int[] factorsToOriginalImg, - final int previousLevel, - final int[] factorsToPreviousLevel, - final int[] chunkSize ); - } - - public interface AfterEachPlane - { - public void afterEachPlane( final boolean usedLoopBack ); - } - - /** - * Simple heuristic: use loopback image loader if saving 8 times or more on - * number of pixel access with respect to the original image. - * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> - */ - public static class DefaultLoopbackHeuristic implements LoopbackHeuristic - { - @Override - public boolean decide( final RandomAccessibleInterval< ? > originalImg, final int[] factorsToOriginalImg, final int previousLevel, final int[] factorsToPreviousLevel, final int[] chunkSize ) - { - if ( previousLevel < 0 ) - return false; - - if ( numElements( factorsToOriginalImg ) / numElements( factorsToPreviousLevel ) >= 8 ) - return true; - - return false; - } - } - - public static int numElements( final int[] size ) - { - int numElements = size[ 0 ]; - for ( int d = 1; d < size.length; ++d ) - numElements *= size[ d ]; - return numElements; - } - - public static CellCreatorThread[] createAndStartCellCreatorThreads( final int numThreads ) - { - final CellCreatorThread[] cellCreatorThreads = new CellCreatorThread[ numThreads ]; - for ( int threadNum = 0; threadNum < numThreads; ++threadNum ) - { - cellCreatorThreads[ threadNum ] = new CellCreatorThread(); - cellCreatorThreads[ threadNum ].setName( "CellCreatorThread " + threadNum ); - cellCreatorThreads[ threadNum ].start(); - } - return cellCreatorThreads; - } - - public static void stopCellCreatorThreads( final CellCreatorThread[] cellCreatorThreads ) - { - for ( final CellCreatorThread thread : cellCreatorThreads ) - thread.interrupt(); - } - - public static class CellCreatorThread extends Thread - { - private Runnable currentTask = null; - - public synchronized void run( final Runnable task ) - { - currentTask = task; - notify(); - } - - @Override - public void run() - { - while ( !isInterrupted() ) - { - synchronized ( this ) - { - try - { - if ( currentTask == null ) - wait(); - else - { - currentTask.run(); - currentTask = null; - } - } - catch ( final InterruptedException e ) - { - break; - } - } - } - - } - } + final DatasetIO< Object, UnsignedShortType > io = new HDF5DatasetIO( + writerQueue, + new ViewId( timepointIdPartition, setupIdPartition ), + deflate ? HDF5IntStorageFeatures.INT_AUTO_SCALING_DEFLATE : HDF5IntStorageFeatures.INT_AUTO_SCALING, + loopback ); - private static < T extends RealType< T > > void copyBlock( final RandomAccess< T > out, final long[] outDim, final RandomAccess< T > in, final long[] blockMin ) - { - in.setPosition( blockMin ); - for ( out.setPosition( 0, 2 ); out.getLongPosition( 2 ) < outDim[ 2 ]; out.fwd( 2 ) ) + try { - for ( out.setPosition( 0, 1 ); out.getLongPosition( 1 ) < outDim[ 1 ]; out.fwd( 1 ) ) - { - for ( out.setPosition( 0, 0 ); out.getLongPosition( 0 ) < outDim[ 0 ]; out.fwd( 0 ), in.fwd( 0 ) ) - { - out.get().set( in.get() ); - } - in.setPosition( blockMin[ 0 ], 0 ); - in.fwd( 1 ); - } - in.setPosition( blockMin[ 1 ], 1 ); - in.fwd( 2 ); + ExportScalePyramid.writeScalePyramid( + img, + new UnsignedShortType(), + mipmapInfo, + io, + executorService, + numThreads, + loopbackHeuristic, + afterEachPlane, + progressWriter ); } - } - - private static < T extends RealType< T > > void downsampleBlock( final Cursor< T > out, final double[] accumulator, final long[] outDim, final RandomAccess< UnsignedShortType > randomAccess, final long[] blockMin, final int[] blockSize, final double scale ) - { - final int numBlockPixels = ( int ) ( outDim[ 0 ] * outDim[ 1 ] * outDim[ 2 ] ); - Arrays.fill( accumulator, 0, numBlockPixels, 0 ); - - randomAccess.setPosition( blockMin ); - - final int ox = ( int ) outDim[ 0 ]; - final int oy = ( int ) outDim[ 1 ]; - final int oz = ( int ) outDim[ 2 ]; - - final int sx = ox * blockSize[ 0 ]; - final int sy = oy * blockSize[ 1 ]; - final int sz = oz * blockSize[ 2 ]; - - int i = 0; - for ( int z = 0, bz = 0; z < sz; ++z ) + catch ( IOException e ) { - for ( int y = 0, by = 0; y < sy; ++y ) - { - for ( int x = 0, bx = 0; x < sx; ++x ) - { - accumulator[ i ] += randomAccess.get().getRealDouble(); - randomAccess.fwd( 0 ); - if ( ++bx == blockSize[ 0 ] ) - { - bx = 0; - ++i; - } - } - randomAccess.move( -sx, 0 ); - randomAccess.fwd( 1 ); - if ( ++by == blockSize[ 1 ] ) - by = 0; - else - i -= ox; - } - randomAccess.move( -sy, 1 ); - randomAccess.fwd( 2 ); - if ( ++bz == blockSize[ 2 ] ) - bz = 0; - else - i -= ox * oy; + e.printStackTrace(); } - for ( int j = 0; j < numBlockPixels; ++j ) - out.next().setReal( accumulator[ j ] * scale ); + if ( loopback != null ) + loopback.close(); } } diff --git a/src/main/java/bdv/export/n5/WriteSequenceToN5.java b/src/main/java/bdv/export/n5/WriteSequenceToN5.java new file mode 100644 index 0000000000000000000000000000000000000000..9de577c7c4275ec4ec957b5a5d2b1ce457b56a1f --- /dev/null +++ b/src/main/java/bdv/export/n5/WriteSequenceToN5.java @@ -0,0 +1,371 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.export.n5; + +import bdv.export.ExportMipmapInfo; +import bdv.export.ExportScalePyramid; +import bdv.export.ProgressWriter; +import bdv.export.ProgressWriterNull; +import bdv.export.SubTaskProgressWriter; +import bdv.export.ExportScalePyramid.AfterEachPlane; +import bdv.export.ExportScalePyramid.LoopbackHeuristic; +import bdv.img.cache.SimpleCacheArrayLoader; +import bdv.img.n5.N5ImageLoader; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Function; +import java.util.stream.Collectors; +import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription; +import mpicbg.spim.data.generic.sequence.BasicImgLoader; +import mpicbg.spim.data.generic.sequence.BasicSetupImgLoader; +import mpicbg.spim.data.generic.sequence.BasicViewSetup; +import mpicbg.spim.data.sequence.TimePoint; +import mpicbg.spim.data.sequence.ViewId; +import mpicbg.spim.data.sequence.VoxelDimensions; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.cache.img.ReadOnlyCachedCellImgFactory; +import net.imglib2.img.cell.Cell; +import net.imglib2.img.cell.CellGrid; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.RealType; +import net.imglib2.util.Cast; +import org.janelia.saalfeldlab.n5.ByteArrayDataBlock; +import org.janelia.saalfeldlab.n5.Compression; +import org.janelia.saalfeldlab.n5.DataBlock; +import org.janelia.saalfeldlab.n5.DataType; +import org.janelia.saalfeldlab.n5.DatasetAttributes; +import org.janelia.saalfeldlab.n5.DoubleArrayDataBlock; +import org.janelia.saalfeldlab.n5.FloatArrayDataBlock; +import org.janelia.saalfeldlab.n5.IntArrayDataBlock; +import org.janelia.saalfeldlab.n5.LongArrayDataBlock; +import org.janelia.saalfeldlab.n5.N5FSWriter; +import org.janelia.saalfeldlab.n5.N5Writer; +import org.janelia.saalfeldlab.n5.ShortArrayDataBlock; +import org.janelia.saalfeldlab.n5.imglib2.N5Utils; + +import static bdv.img.n5.BdvN5Format.DATA_TYPE_KEY; +import static bdv.img.n5.BdvN5Format.DOWNSAMPLING_FACTORS_KEY; +import static bdv.img.n5.BdvN5Format.getPathName; +import static net.imglib2.cache.img.ReadOnlyCachedCellImgOptions.options; + +/** + * @author Tobias Pietzsch + * @author John Bogovic + */ +public class WriteSequenceToN5 +{ + private static final String MULTI_SCALE_KEY = "multiScale"; + private static final String RESOLUTION_KEY = "resolution"; + + /** + * Create a n5 group containing image data from all views and all + * timepoints in a chunked, mipmaped representation. + * + * @param seq + * description of the sequence to be stored as hdf5. (The + * {@link AbstractSequenceDescription} contains the number of + * setups and timepoints as well as an {@link BasicImgLoader} + * that provides the image data, Registration information is not + * needed here, that will go into the accompanying xml). + * @param perSetupMipmapInfo + * this maps from setup {@link BasicViewSetup#getId() id} to + * {@link ExportMipmapInfo} for that setup. The + * {@link ExportMipmapInfo} contains for each mipmap level, the + * subsampling factors and subdivision block sizes. + * @param compression + * n5 compression scheme. + * @param n5File + * n5 root. + * @param loopbackHeuristic + * heuristic to decide whether to create each resolution level by + * reading pixels from the original image or by reading back a + * finer resolution level already written to the hdf5. may be + * null (in this case always use the original image). + * @param afterEachPlane + * this is called after each "plane of chunks" is written, giving + * the opportunity to clear caches, etc. + * @param numCellCreatorThreads + * The number of threads that will be instantiated to generate + * cell data. Must be at least 1. (In addition the cell creator + * threads there is one writer thread that saves the generated + * data to HDF5.) + * @param progressWriter + * completion ratio and status output will be directed here. + */ + public static void writeN5File( + final AbstractSequenceDescription< ?, ?, ? > seq, + final Map< Integer, ExportMipmapInfo > perSetupMipmapInfo, + final Compression compression, + final File n5File, + final LoopbackHeuristic loopbackHeuristic, + final AfterEachPlane afterEachPlane, + final int numCellCreatorThreads, + ProgressWriter progressWriter ) throws IOException + { + if ( progressWriter == null ) + progressWriter = new ProgressWriterNull(); + progressWriter.setProgress( 0 ); + + final BasicImgLoader imgLoader = seq.getImgLoader(); + + for ( final BasicViewSetup setup : seq.getViewSetupsOrdered() ) + { + final Object type = imgLoader.getSetupImgLoader( setup.getId() ).getImageType(); + if ( !( type instanceof RealType && + type instanceof NativeType && + N5Utils.dataType( Cast.unchecked( type ) ) != null ) ) + throw new IllegalArgumentException( "Unsupported pixel type: " + type.getClass().getSimpleName() ); + } + + final List< Integer > timepointIds = seq.getTimePoints().getTimePointsOrdered().stream() + .map( TimePoint::getId ) + .collect( Collectors.toList() ); + final List< Integer > setupIds = seq.getViewSetupsOrdered().stream() + .map( BasicViewSetup::getId ) + .collect( Collectors.toList() ); + + N5Writer n5 = new N5FSWriter( n5File.getAbsolutePath() ); + + // write Mipmap descriptions + for ( final int setupId : setupIds ) + { + final String pathName = getPathName( setupId ); + final int[][] downsamplingFactors = perSetupMipmapInfo.get( setupId ).getExportResolutions(); + final DataType dataType = N5Utils.dataType( Cast.unchecked( imgLoader.getSetupImgLoader( setupId ).getImageType() ) ); + n5.createGroup( pathName ); + n5.setAttribute( pathName, DOWNSAMPLING_FACTORS_KEY, downsamplingFactors ); + n5.setAttribute( pathName, DATA_TYPE_KEY, dataType ); + } + + + // calculate number of tasks for progressWriter + int numTasks = 0; // first task is for writing mipmap descriptions etc... + for ( final int timepointIdSequence : timepointIds ) + for ( final int setupIdSequence : setupIds ) + if ( seq.getViewDescriptions().get( new ViewId( timepointIdSequence, setupIdSequence ) ).isPresent() ) + numTasks++; + int numCompletedTasks = 0; + + final ExecutorService executorService = Executors.newFixedThreadPool( numCellCreatorThreads ); + try + { + // write image data for all views + final int numTimepoints = timepointIds.size(); + int timepointIndex = 0; + for ( final int timepointId : timepointIds ) + { + progressWriter.out().printf( "proccessing timepoint %d / %d\n", ++timepointIndex, numTimepoints ); + + // assemble the viewsetups that are present in this timepoint + final ArrayList< Integer > setupsTimePoint = new ArrayList<>(); + for ( final int setupId : setupIds ) + if ( seq.getViewDescriptions().get( new ViewId( timepointId, setupId ) ).isPresent() ) + setupsTimePoint.add( setupId ); + + final int numSetups = setupsTimePoint.size(); + int setupIndex = 0; + for ( final int setupId : setupsTimePoint ) + { + progressWriter.out().printf( "proccessing setup %d / %d\n", ++setupIndex, numSetups ); + + final ExportMipmapInfo mipmapInfo = perSetupMipmapInfo.get( setupId ); + final double startCompletionRatio = ( double ) numCompletedTasks++ / numTasks; + final double endCompletionRatio = ( double ) numCompletedTasks / numTasks; + final ProgressWriter subProgressWriter = new SubTaskProgressWriter( progressWriter, startCompletionRatio, endCompletionRatio ); + writeScalePyramid( + n5, compression, + imgLoader, setupId, timepointId, mipmapInfo, + executorService, numCellCreatorThreads, + loopbackHeuristic, afterEachPlane, subProgressWriter ); + + + // additional attributes for paintera compatibility + final String pathName = getPathName( setupId, timepointId ); + n5.createGroup( pathName ); + n5.setAttribute( pathName, MULTI_SCALE_KEY, true ); + final VoxelDimensions voxelSize = seq.getViewSetups().get( setupId ).getVoxelSize(); + if ( voxelSize != null ) + { + final double[] resolution = new double[ voxelSize.numDimensions() ]; + voxelSize.dimensions( resolution ); + n5.setAttribute( pathName, RESOLUTION_KEY, resolution ); + } + final int[][] downsamplingFactors = perSetupMipmapInfo.get( setupId ).getExportResolutions(); + for( int l = 0; l < downsamplingFactors.length; ++l ) + n5.setAttribute( getPathName( setupId, timepointId, l ), DOWNSAMPLING_FACTORS_KEY, downsamplingFactors[ l ] ); + } + } + } + finally + { + executorService.shutdown(); + } + + progressWriter.setProgress( 1.0 ); + } + + static < T extends RealType< T > & NativeType< T > > void writeScalePyramid( + final N5Writer n5, + final Compression compression, + final BasicImgLoader imgLoader, + final int setupId, + final int timepointId, + final ExportMipmapInfo mipmapInfo, + final ExecutorService executorService, + final int numThreads, + final LoopbackHeuristic loopbackHeuristic, + final AfterEachPlane afterEachPlane, + ProgressWriter progressWriter ) throws IOException + { + final BasicSetupImgLoader< T > setupImgLoader = Cast.unchecked( imgLoader.getSetupImgLoader( setupId ) ); + final RandomAccessibleInterval< T > img = setupImgLoader.getImage( timepointId ); + final T type = setupImgLoader.getImageType(); + final N5DatasetIO< T > io = new N5DatasetIO<>( n5, compression, setupId, timepointId, type ); + ExportScalePyramid.writeScalePyramid( + img, type, mipmapInfo, io, + executorService, numThreads, + loopbackHeuristic, afterEachPlane, progressWriter ); + } + + static class N5Dataset + { + final String pathName; + final DatasetAttributes attributes; + + public N5Dataset( final String pathName, final DatasetAttributes attributes ) + { + this.pathName = pathName; + this.attributes = attributes; + } + } + + static class N5DatasetIO< T extends RealType< T > & NativeType< T > > implements ExportScalePyramid.DatasetIO< N5Dataset, T > + { + private final N5Writer n5; + private final Compression compression; + private final int setupId; + private final int timepointId; + private final DataType dataType; + private final T type; + private final Function< ExportScalePyramid.Block< T >, DataBlock< ? > > getDataBlock; + + public N5DatasetIO( final N5Writer n5, final Compression compression, final int setupId, final int timepointId, final T type ) + { + this.n5 = n5; + this.compression = compression; + this.setupId = setupId; + this.timepointId = timepointId; + this.dataType = N5Utils.dataType( type ); + this.type = type; + + switch ( dataType ) + { + case UINT8: + getDataBlock = b -> new ByteArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) ); + break; + case UINT16: + getDataBlock = b -> new ShortArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) ); + break; + case UINT32: + getDataBlock = b -> new IntArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) ); + break; + case UINT64: + getDataBlock = b -> new LongArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) ); + break; + case INT8: + getDataBlock = b -> new ByteArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) ); + break; + case INT16: + getDataBlock = b -> new ShortArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) ); + break; + case INT32: + getDataBlock = b -> new IntArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) ); + break; + case INT64: + getDataBlock = b -> new LongArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) ); + break; + case FLOAT32: + getDataBlock = b -> new FloatArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) ); + break; + case FLOAT64: + getDataBlock = b -> new DoubleArrayDataBlock( b.getSize(), b.getGridPosition(), Cast.unchecked( b.getData().getStorageArray() ) ); + break; + default: + throw new IllegalArgumentException(); + } + } + + @Override + public N5Dataset createDataset( final int level, final long[] dimensions, final int[] blockSize ) throws IOException + { + final String pathName = getPathName( setupId, timepointId, level ); + n5.createDataset( pathName, dimensions, blockSize, dataType, compression ); + final DatasetAttributes attributes = n5.getDatasetAttributes( pathName ); + return new N5Dataset( pathName, attributes ); + } + + @Override + public void writeBlock( final N5Dataset dataset, final ExportScalePyramid.Block< T > dataBlock ) throws IOException + { + n5.writeBlock( dataset.pathName, dataset.attributes, getDataBlock.apply( dataBlock ) ); + } + + @Override + public void flush( final N5Dataset dataset ) + {} + + @Override + public RandomAccessibleInterval< T > getImage( final int level ) throws IOException + { + final String pathName = getPathName( setupId, timepointId, level ); + final DatasetAttributes attributes = n5.getDatasetAttributes( pathName ); + final long[] dimensions = attributes.getDimensions(); + final int[] cellDimensions = attributes.getBlockSize(); + final CellGrid grid = new CellGrid( dimensions, cellDimensions ); + final SimpleCacheArrayLoader< ? > cacheArrayLoader = N5ImageLoader.createCacheArrayLoader( n5, pathName ); + return new ReadOnlyCachedCellImgFactory().createWithCacheLoader( + dimensions, type, + key -> { + final int n = grid.numDimensions(); + final long[] cellMin = new long[ n ]; + final int[] cellDims = new int[ n ]; + final long[] cellGridPosition = new long[ n ]; + grid.getCellDimensions( key, cellMin, cellDims ); + grid.getCellGridPositionFlat( key, cellGridPosition ); + return new Cell<>( cellDims, cellMin, cacheArrayLoader.loadArray( cellGridPosition ) ); + }, + options().cellDimensions( cellDimensions ) ); + } + } +} diff --git a/src/main/java/bdv/img/cache/CacheArrayLoader.java b/src/main/java/bdv/img/cache/CacheArrayLoader.java index 800ff355b8e4116ade49aee3ffee61d59790799c..e78905e038e86e28d5981a030dd264015f8d292a 100644 --- a/src/main/java/bdv/img/cache/CacheArrayLoader.java +++ b/src/main/java/bdv/img/cache/CacheArrayLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -30,7 +29,6 @@ package bdv.img.cache; import bdv.ViewerImgLoader; -import bdv.img.catmaid.CatmaidImageLoader; import net.imglib2.img.basictypeaccess.volatiles.VolatileAccess; import net.imglib2.img.basictypeaccess.volatiles.VolatileArrayDataAccess; import net.imglib2.img.basictypeaccess.volatiles.array.DirtyVolatileByteArray; @@ -58,7 +56,7 @@ import net.imglib2.type.NativeType; * type of access to cell data, currently always a * {@link VolatileAccess}. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public interface CacheArrayLoader< A > { @@ -69,7 +67,7 @@ public interface CacheArrayLoader< A > * * @return number of bytes required to store one element. */ - public default int getBytesPerElement() + default int getBytesPerElement() { return 1; } @@ -103,7 +101,7 @@ public interface CacheArrayLoader< A > * * @return an {@link EmptyArrayCreator} for {@code A} or null. */ - public default EmptyArrayCreator< A > getEmptyArrayCreator() + default EmptyArrayCreator< A > getEmptyArrayCreator() { return null; } @@ -128,10 +126,10 @@ public interface CacheArrayLoader< A > * back-end. You do not need to be able to load blocks of arbitrary sizes * and offsets here -- just the ones that you will use from the images * returned by your {@link ViewerImgLoader}. For an example, look at - * {@link CatmaidImageLoader}. There, the blockDimensions are defined in the + * {@code CatmaidImageLoader}. There, the blockDimensions are defined in the * constructor, according to the tile size of the data set. These * blockDimensions are then used for every image that the - * {@link CatmaidImageLoader} provides. Therefore, all calls to + * {@code CatmaidImageLoader} provides. Therefore, all calls to * {@link #loadArray(int, int, int, int[], long[])} will have predictable * {@code dimensions} (corresponding to tile size of the data set) and * {@code min} offsets (multiples of the tile size). @@ -156,5 +154,6 @@ public interface CacheArrayLoader< A > * the min coordinate of the block in the stack (in voxels). * @return loaded cell data. */ - public A loadArray( final int timepoint, final int setup, final int level, int[] dimensions, long[] min ) throws InterruptedException; + // TODO: It would make more sense to throw IOException here. Declare both IOException and InterruptedException. Throw IOException in bdv-core implementations. + A loadArray( final int timepoint, final int setup, final int level, int[] dimensions, long[] min ) throws InterruptedException; } diff --git a/src/main/java/bdv/img/cache/CreateInvalidVolatileCell.java b/src/main/java/bdv/img/cache/CreateInvalidVolatileCell.java index 30ea961909ccbf37fd3d55a47063a802c254ba1d..58280eee92048e1b247f913669ba49dd951186f3 100644 --- a/src/main/java/bdv/img/cache/CreateInvalidVolatileCell.java +++ b/src/main/java/bdv/img/cache/CreateInvalidVolatileCell.java @@ -1,3 +1,31 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package bdv.img.cache; import net.imglib2.cache.volatiles.CreateInvalid; diff --git a/src/main/java/bdv/img/cache/DefaultEmptyArrayCreator.java b/src/main/java/bdv/img/cache/DefaultEmptyArrayCreator.java index 1ea403e0537b95c05d4e6b3a496eec1a56e62e34..0cf9dbc4507f89c4eb978c7eb77878daa6d89170 100644 --- a/src/main/java/bdv/img/cache/DefaultEmptyArrayCreator.java +++ b/src/main/java/bdv/img/cache/DefaultEmptyArrayCreator.java @@ -1,3 +1,31 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package bdv.img.cache; import net.imglib2.img.basictypeaccess.volatiles.VolatileArrayDataAccess; diff --git a/src/main/java/bdv/img/cache/EmptyArrayCreator.java b/src/main/java/bdv/img/cache/EmptyArrayCreator.java index 208c9ada795864438f082d098360b2b9751c4e58..cb200ead32891cfaac413326cf6e25bde5795fa2 100644 --- a/src/main/java/bdv/img/cache/EmptyArrayCreator.java +++ b/src/main/java/bdv/img/cache/EmptyArrayCreator.java @@ -1,3 +1,31 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package bdv.img.cache; import net.imglib2.img.basictypeaccess.AccessFlags; @@ -21,9 +49,9 @@ import net.imglib2.type.PrimitiveType; */ public interface EmptyArrayCreator< A > { - public A getEmptyArray( final long numEntities ); + A getEmptyArray( final long numEntities ); - public static < A extends VolatileArrayDataAccess< A > > EmptyArrayCreator< A > get( + static < A extends VolatileArrayDataAccess< A > > EmptyArrayCreator< A > get( final PrimitiveType primitiveType, final boolean dirty ) { diff --git a/src/main/java/bdv/img/cache/SimpleCacheArrayLoader.java b/src/main/java/bdv/img/cache/SimpleCacheArrayLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..30922c3babc41a838c97945b7624d16493e777f1 --- /dev/null +++ b/src/main/java/bdv/img/cache/SimpleCacheArrayLoader.java @@ -0,0 +1,130 @@ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.img.cache; + +import java.io.IOException; + +import net.imglib2.img.basictypeaccess.volatiles.VolatileAccess; +import net.imglib2.img.basictypeaccess.volatiles.VolatileArrayDataAccess; +import net.imglib2.img.basictypeaccess.volatiles.array.DirtyVolatileByteArray; +import net.imglib2.img.basictypeaccess.volatiles.array.DirtyVolatileCharArray; +import net.imglib2.img.basictypeaccess.volatiles.array.DirtyVolatileDoubleArray; +import net.imglib2.img.basictypeaccess.volatiles.array.DirtyVolatileFloatArray; +import net.imglib2.img.basictypeaccess.volatiles.array.DirtyVolatileIntArray; +import net.imglib2.img.basictypeaccess.volatiles.array.DirtyVolatileLongArray; +import net.imglib2.img.basictypeaccess.volatiles.array.DirtyVolatileShortArray; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileByteArray; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileCharArray; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileDoubleArray; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileFloatArray; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileIntArray; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileLongArray; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileShortArray; +import net.imglib2.img.cell.CellGrid; +import net.imglib2.type.NativeType; + +/** + * Provider of volatile {@link net.imglib2.img.cell.Cell} data. This is + * implemented by data back-ends to the {@link VolatileGlobalCellCache}. + * <p> + * {@code SimpleCacheArrayLoader} is supposed to load data one specific image. + * {@code loadArray()} will not get information about which timepoint, + * resolution level, etc a requested block belongs to, and also the appropriate + * block size is supposed to be known. + * <p> + * This is in contrast to {@link CacheArrayLoader}, where all information to + * identify a particular block in a whole dataset is provided. Whether it makes + * more sense to implement {@code CacheArrayLoader} or + * {@code SimpleCacheArrayLoader} depends on the particular back-end. + * + * @param <A> + * type of access to cell data, currently always a + * {@link VolatileAccess}. + * + * @author Tobias Pietzsch + */ +public interface SimpleCacheArrayLoader< A > +{ + /** + * Implementing classes must override this if {@code A} is not a standard + * {@link VolatileArrayDataAccess} type. The default implementation returns + * {@code null}, which will let + * {@link CreateInvalidVolatileCell#get(CellGrid, NativeType, boolean) + * CreateInvalidVolatileCell.get(...)} try to figure out the appropriate + * {@link DefaultEmptyArrayCreator}. + * <p> + * Default access types are + * </p> + * <ul> + * <li>{@link DirtyVolatileByteArray}</li> + * <li>{@link VolatileByteArray}</li> + * <li>{@link DirtyVolatileCharArray}</li> + * <li>{@link VolatileCharArray}</li> + * <li>{@link DirtyVolatileDoubleArray}</li> + * <li>{@link VolatileDoubleArray}</li> + * <li>{@link DirtyVolatileFloatArray}</li> + * <li>{@link VolatileFloatArray}</li> + * <li>{@link DirtyVolatileIntArray}</li> + * <li>{@link VolatileIntArray}</li> + * <li>{@link DirtyVolatileLongArray}</li> + * <li>{@link VolatileLongArray}</li> + * <li>{@link DirtyVolatileShortArray}</li> + * <li>{@link VolatileShortArray}</li> + * </ul> + * + * @return an {@link EmptyArrayCreator} for {@code A} or null. + */ + default EmptyArrayCreator< A > getEmptyArrayCreator() + { + return null; + } + + /** + * Load cell data into memory. This method blocks until data is successfully + * loaded. If it completes normally, the returned data is always valid. If + * anything goes wrong, an {@link IOException} is thrown. + * <p> + * {@code SimpleCacheArrayLoader} is supposed to load data one specific + * image. {@code loadArray()} will not get information about which + * timepoint, resolution level, etc a requested block belongs to. Also the + * appropriate block size is supposed to be known to the + * {@code SimpleCacheArrayLoader}. + * <p> + * This is in contrast to + * {@link CacheArrayLoader#loadArray(int, int, int, int[], long[])}, where + * all information to identify a particular block in a whole dataset is + * provided. + * + * @param gridPosition + * the coordinate of the cell in the cell grid. + * + * @return loaded cell data. + */ + A loadArray( long[] gridPosition ) throws IOException; +} diff --git a/src/main/java/bdv/img/cache/VolatileCachedCellImg.java b/src/main/java/bdv/img/cache/VolatileCachedCellImg.java index 81e6c2973ba022683a7458c99cacc4cca9072b05..798fdffc5a5c7d3c5537a697611bb68344b73d37 100644 --- a/src/main/java/bdv/img/cache/VolatileCachedCellImg.java +++ b/src/main/java/bdv/img/cache/VolatileCachedCellImg.java @@ -1,3 +1,31 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package bdv.img.cache; import java.util.function.Function; diff --git a/src/main/java/bdv/img/cache/VolatileGlobalCellCache.java b/src/main/java/bdv/img/cache/VolatileGlobalCellCache.java index 63eddd7de0821d191c1f699c805c0a59256793a4..b39668a256545aedea602f29bd0c633a1ec80bb8 100644 --- a/src/main/java/bdv/img/cache/VolatileGlobalCellCache.java +++ b/src/main/java/bdv/img/cache/VolatileGlobalCellCache.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -202,22 +201,63 @@ public class VolatileGlobalCellCache implements CacheControl final CacheArrayLoader< A > cacheArrayLoader, final T type ) { - final CacheLoader< Long, Cell< ? > > loader = new CacheLoader< Long, Cell< ? > >() - { - @Override - public Cell< A > get( final Long key ) throws Exception - { - final int n = grid.numDimensions(); - final long[] cellMin = new long[ n ]; - final int[] cellDims = new int[ n ]; - grid.getCellDimensions( key, cellMin, cellDims ); - return new Cell<>( - cellDims, - cellMin, - cacheArrayLoader.loadArray( timepoint, setup, level, cellDims, cellMin ) ); - } + final CacheLoader< Long, Cell< ? > > loader = key -> { + final int n = grid.numDimensions(); + final long[] cellMin = new long[ n ]; + final int[] cellDims = new int[ n ]; + grid.getCellDimensions( key, cellMin, cellDims ); + return new Cell<>( + cellDims, + cellMin, + cacheArrayLoader.loadArray( timepoint, setup, level, cellDims, cellMin ) ); + }; + return createImg( grid, timepoint, setup, level, cacheHints, loader, cacheArrayLoader.getEmptyArrayCreator(), type ); + } + + /** + * Create a {@link VolatileCachedCellImg} backed by this {@link VolatileGlobalCellCache}, + * using the provided {@link SimpleCacheArrayLoader} to load data. + * + * @param grid + * @param timepoint + * @param setup + * @param level + * @param cacheHints + * @param cacheArrayLoader + * @param type + * @return + */ + public < T extends NativeType< T >, A > VolatileCachedCellImg< T, A > createImg( + final CellGrid grid, + final int timepoint, + final int setup, + final int level, + final CacheHints cacheHints, + final SimpleCacheArrayLoader< A > cacheArrayLoader, + final T type ) + { + final CacheLoader< Long, Cell< ? > > loader = key -> { + final int n = grid.numDimensions(); + final long[] cellMin = new long[ n ]; + final int[] cellDims = new int[ n ]; + final long[] cellGridPosition = new long[ n ]; + grid.getCellDimensions( key, cellMin, cellDims ); + grid.getCellGridPositionFlat( key, cellGridPosition ); + return new Cell<>( cellDims, cellMin, cacheArrayLoader.loadArray( cellGridPosition ) ); }; + return createImg( grid, timepoint, setup, level, cacheHints, loader, cacheArrayLoader.getEmptyArrayCreator(), type ); + } + private < T extends NativeType< T >, A > VolatileCachedCellImg< T, A > createImg( + final CellGrid grid, + final int timepoint, + final int setup, + final int level, + final CacheHints cacheHints, + final CacheLoader< Long, Cell< ? > > loader, + final EmptyArrayCreator< A > emptyArrayCreator, // optional, can be null + final T type ) + { final KeyBimap< Long, Key > bimap = KeyBimap.build( index -> new Key( timepoint, setup, level, index ), key -> ( key.timepoint == timepoint && key.setup == setup && key.level == level ) @@ -228,14 +268,13 @@ public class VolatileGlobalCellCache implements CacheControl .mapKeys( bimap ) .withLoader( loader ); - final EmptyArrayCreator< A > emptyArrayCreator = cacheArrayLoader.getEmptyArrayCreator(); final CreateInvalidVolatileCell< ? > createInvalid = ( emptyArrayCreator == null ) ? CreateInvalidVolatileCell.get( grid, type, false ) : new CreateInvalidVolatileCell<>( grid, type.getEntitiesPerPixel(), emptyArrayCreator ); final UncheckedVolatileCache< Long, Cell< ? > > vcache = new WeakRefVolatileCache<>( cache, queue, createInvalid ) - .unchecked(); + .unchecked(); @SuppressWarnings( "unchecked" ) final VolatileCachedCellImg< T, A > img = new VolatileCachedCellImg<>( grid, type, cacheHints, diff --git a/src/main/java/bdv/img/catmaid/CatmaidImageLoader.java b/src/main/java/bdv/img/catmaid/CatmaidImageLoader.java index bae9af7f9b6e9e5f4fe858b914cc2ac36b185b72..532484294b62ad13342aa4c2b066947f2c17322d 100644 --- a/src/main/java/bdv/img/catmaid/CatmaidImageLoader.java +++ b/src/main/java/bdv/img/catmaid/CatmaidImageLoader.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE diff --git a/src/main/java/bdv/img/catmaid/CatmaidVolatileIntArrayLoader.java b/src/main/java/bdv/img/catmaid/CatmaidVolatileIntArrayLoader.java index da890bf370ec7bae1e3fa4e3e163a4f2d4c1695f..5d381bc2bada6674870c52fefff120efbae2cb9e 100644 --- a/src/main/java/bdv/img/catmaid/CatmaidVolatileIntArrayLoader.java +++ b/src/main/java/bdv/img/catmaid/CatmaidVolatileIntArrayLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/catmaid/XmlIoCatmaidImageLoader.java b/src/main/java/bdv/img/catmaid/XmlIoCatmaidImageLoader.java index 22ec0231db4fe6c0ad49df01e17657e83c087846..bd5343fad8a8bda47516830824831b39d1169ecc 100644 --- a/src/main/java/bdv/img/catmaid/XmlIoCatmaidImageLoader.java +++ b/src/main/java/bdv/img/catmaid/XmlIoCatmaidImageLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/hdf5/DimsAndExistence.java b/src/main/java/bdv/img/hdf5/DimsAndExistence.java index f84e74c64ae92c113a8ec8f42b6d78cfa76a8c79..7939223367408e04aef0192e0794deb4c6a64240 100644 --- a/src/main/java/bdv/img/hdf5/DimsAndExistence.java +++ b/src/main/java/bdv/img/hdf5/DimsAndExistence.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -33,7 +32,7 @@ package bdv.img.hdf5; * The dimensions of an image and a flag indicating whether that image * exists (can be loaded) * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class DimsAndExistence { diff --git a/src/main/java/bdv/img/hdf5/HDF5Access.java b/src/main/java/bdv/img/hdf5/HDF5Access.java index b3344a0e75257f9542aa541792fc3675bf715ec4..d379a9be1c3d4ed34fcfaa43ed7d43a92abe593a 100644 --- a/src/main/java/bdv/img/hdf5/HDF5Access.java +++ b/src/main/java/bdv/img/hdf5/HDF5Access.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/hdf5/HDF5AccessHack.java b/src/main/java/bdv/img/hdf5/HDF5AccessHack.java index 427b9f61235596e4545d6120133b9019da3bb020..8d437950cbd9b325fa326e6dceea85cdab4d2994 100644 --- a/src/main/java/bdv/img/hdf5/HDF5AccessHack.java +++ b/src/main/java/bdv/img/hdf5/HDF5AccessHack.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -58,7 +57,7 @@ import ch.systemsx.cisd.hdf5.IHDF5Reader; * The HDF5 fileId is extracted from a jhdf5 HDF5Reader using reflection to * avoid having to do everything ourselves. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ class HDF5AccessHack implements IHDF5Access { diff --git a/src/main/java/bdv/img/hdf5/Hdf5ImageLoader.java b/src/main/java/bdv/img/hdf5/Hdf5ImageLoader.java index 4c54c99ca89384b2669a9f5901f88bd42ee1b406..c376732f3be6fc076aa08b92c00d86e2a7c4fa36 100644 --- a/src/main/java/bdv/img/hdf5/Hdf5ImageLoader.java +++ b/src/main/java/bdv/img/hdf5/Hdf5ImageLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -38,8 +37,6 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import bdv.AbstractViewerSetupImgLoader; import bdv.ViewerImgLoader; @@ -63,17 +60,13 @@ import net.imglib2.Cursor; import net.imglib2.Dimensions; import net.imglib2.FinalDimensions; import net.imglib2.FinalInterval; -import net.imglib2.IterableInterval; -import net.imglib2.RandomAccess; import net.imglib2.RandomAccessibleInterval; import net.imglib2.cache.queue.BlockingFetchQueues; import net.imglib2.cache.queue.FetcherThreads; import net.imglib2.cache.volatiles.CacheHints; import net.imglib2.cache.volatiles.LoadingStrategy; import net.imglib2.img.Img; -import net.imglib2.img.ImgFactory; import net.imglib2.img.NativeImg; -import net.imglib2.img.array.ArrayImgFactory; import net.imglib2.img.array.ArrayImgs; import net.imglib2.img.basictypeaccess.array.ShortArray; import net.imglib2.img.cell.Cell; @@ -83,7 +76,6 @@ import net.imglib2.img.cell.CellImgFactory; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.type.NativeType; import net.imglib2.type.numeric.integer.UnsignedShortType; -import net.imglib2.type.numeric.real.FloatType; import net.imglib2.type.volatiles.VolatileUnsignedShortType; import net.imglib2.util.Intervals; import net.imglib2.view.Views; @@ -363,27 +355,6 @@ public class Hdf5ImageLoader implements ViewerImgLoader, MultiResolutionImgLoade } } - /** - * normalize img to 0...1 - */ - protected static void normalize( final IterableInterval< FloatType > img ) - { - float currentMax = img.firstElement().get(); - float currentMin = currentMax; - for ( final FloatType t : img ) - { - final float f = t.get(); - if ( f > currentMax ) - currentMax = f; - else if ( f < currentMin ) - currentMin = f; - } - - final float scale = ( float ) ( 1.0 / ( currentMax - currentMin ) ); - for ( final FloatType t : img ) - t.set( ( t.get() - currentMin ) * scale ); - } - @Override public SetupImgLoader getSetupImgLoader( final int setupId ) { @@ -516,9 +487,7 @@ public class Hdf5ImageLoader implements ViewerImgLoader, MultiResolutionImgLoade } /** - * (Almost) create a {@link CellImg} backed by the cache. - * The created image needs a {@link NativeImg#setLinkedType(net.imglib2.type.Type) linked type} before it can be used. - * The type should be either {@link UnsignedShortType} and {@link VolatileUnsignedShortType}. + * Create a {@link CellImg} backed by the cache. */ protected < T extends NativeType< T > > RandomAccessibleInterval< T > prepareCachedImage( final int timepointId, final int level, final LoadingStrategy loadingStrategy, final T type ) { @@ -555,118 +524,6 @@ public class Hdf5ImageLoader implements ViewerImgLoader, MultiResolutionImgLoade return Views.interval( new ConstantRandomAccessible<>( constant, 3 ), new FinalInterval( d ) ); } - @Override - public RandomAccessibleInterval< FloatType > getFloatImage( final int timepointId, final boolean normalize, final ImgLoaderHint... hints ) - { - return getFloatImage( timepointId, 0, normalize, hints ); - } - - @Override - public RandomAccessibleInterval< FloatType > getFloatImage( final int timepointId, final int level, final boolean normalize, final ImgLoaderHint... hints ) - { - final RandomAccessibleInterval< UnsignedShortType > ushortImg = getImage( timepointId, level, hints ); - - // copy unsigned short img to float img - - // create float img - final FloatType f = new FloatType(); - final ImgFactory< FloatType > imgFactory; - if ( Intervals.numElements( ushortImg ) <= Integer.MAX_VALUE ) - { - imgFactory = new ArrayImgFactory<>( f ); - } - else - { - final long[] dimsLong = new long[ ushortImg.numDimensions() ]; - ushortImg.dimensions( dimsLong ); - final int[] cellDimensions = computeCellDimensions( - dimsLong, - mipmapInfo.getSubdivisions()[ level ] ); - imgFactory = new CellImgFactory<>( f, cellDimensions ); - } - final Img< FloatType > floatImg = imgFactory.create( ushortImg ); - - // set up executor service - final int numProcessors = Runtime.getRuntime().availableProcessors(); - final ExecutorService taskExecutor = Executors.newFixedThreadPool( numProcessors ); - final ArrayList< Callable< Void > > tasks = new ArrayList<>(); - - // set up all tasks - final int numPortions = numProcessors * 2; - final long threadChunkSize = floatImg.size() / numPortions; - final long threadChunkMod = floatImg.size() % numPortions; - - for ( int portionID = 0; portionID < numPortions; ++portionID ) - { - // move to the starting position of the current thread - final long startPosition = portionID * threadChunkSize; - - // the last thread may has to run longer if the number of pixels cannot be divided by the number of threads - final long loopSize = ( portionID == numPortions - 1 ) ? threadChunkSize + threadChunkMod : threadChunkSize; - - if ( Views.iterable( ushortImg ).iterationOrder().equals( floatImg.iterationOrder() ) ) - { - tasks.add( new Callable< Void >() - { - @Override - public Void call() throws Exception - { - final Cursor< UnsignedShortType > in = Views.iterable( ushortImg ).cursor(); - final Cursor< FloatType > out = floatImg.cursor(); - - in.jumpFwd( startPosition ); - out.jumpFwd( startPosition ); - - for ( long j = 0; j < loopSize; ++j ) - out.next().set( in.next().getRealFloat() ); - - return null; - } - } ); - } - else - { - tasks.add( new Callable< Void >() - { - @Override - public Void call() throws Exception - { - final Cursor< UnsignedShortType > in = Views.iterable( ushortImg ).localizingCursor(); - final RandomAccess< FloatType > out = floatImg.randomAccess(); - - in.jumpFwd( startPosition ); - - for ( long j = 0; j < loopSize; ++j ) - { - final UnsignedShortType vin = in.next(); - out.setPosition( in ); - out.get().set( vin.getRealFloat() ); - } - - return null; - } - } ); - } - } - - try - { - // invokeAll() returns when all tasks are complete - taskExecutor.invokeAll( tasks ); - taskExecutor.shutdown(); - } - catch ( final InterruptedException e ) - { - return null; - } - - if ( normalize ) - // normalize the image to 0...1 - normalize( floatImg ); - - return floatImg; - } - public MipmapInfo getMipmapInfo() { return mipmapInfo; @@ -690,12 +547,6 @@ public class Hdf5ImageLoader implements ViewerImgLoader, MultiResolutionImgLoade return mipmapInfo.getNumLevels(); } - @Override - public Dimensions getImageSize( final int timepointId ) - { - return getImageSize( timepointId, 0 ); - } - @Override public Dimensions getImageSize( final int timepointId, final int level ) { diff --git a/src/main/java/bdv/img/hdf5/Hdf5VolatileShortArrayLoader.java b/src/main/java/bdv/img/hdf5/Hdf5VolatileShortArrayLoader.java index 41c47dfb1b873d3f945a63e575d236568a4d2ad3..c1c56aa33ef830ef2a91b905882bebe74b95184f 100644 --- a/src/main/java/bdv/img/hdf5/Hdf5VolatileShortArrayLoader.java +++ b/src/main/java/bdv/img/hdf5/Hdf5VolatileShortArrayLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/hdf5/IHDF5Access.java b/src/main/java/bdv/img/hdf5/IHDF5Access.java index 1c5a5d2a1afdc998cfe22512f44d3a1314e17324..f0f07a7fb415611cfa21df5ef989306afb384852 100644 --- a/src/main/java/bdv/img/hdf5/IHDF5Access.java +++ b/src/main/java/bdv/img/hdf5/IHDF5Access.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -31,17 +30,17 @@ package bdv.img.hdf5; interface IHDF5Access { - public DimsAndExistence getDimsAndExistence( final ViewLevelId id ); + DimsAndExistence getDimsAndExistence( final ViewLevelId id ); - public short[] readShortMDArrayBlockWithOffset( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min ) throws InterruptedException; + short[] readShortMDArrayBlockWithOffset( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min ) throws InterruptedException; - public short[] readShortMDArrayBlockWithOffset( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min, final short[] dataBlock ) throws InterruptedException; + short[] readShortMDArrayBlockWithOffset( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min, final short[] dataBlock ) throws InterruptedException; - public float[] readShortMDArrayBlockWithOffsetAsFloat( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min ) throws InterruptedException; + float[] readShortMDArrayBlockWithOffsetAsFloat( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min ) throws InterruptedException; - public float[] readShortMDArrayBlockWithOffsetAsFloat( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min, final float[] dataBlock ) throws InterruptedException; + float[] readShortMDArrayBlockWithOffsetAsFloat( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min, final float[] dataBlock ) throws InterruptedException; - public void closeAllDataSets(); + void closeAllDataSets(); - public void close(); + void close(); } diff --git a/src/main/java/bdv/img/hdf5/MipmapInfo.java b/src/main/java/bdv/img/hdf5/MipmapInfo.java index 23816589bdff639d4066971618f192a69fc801cd..baef04981b17368cb303a20365c171e923eefe6f 100644 --- a/src/main/java/bdv/img/hdf5/MipmapInfo.java +++ b/src/main/java/bdv/img/hdf5/MipmapInfo.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -37,8 +36,9 @@ import net.imglib2.realtransform.AffineTransform3D; * Contains for each mipmap level, the subsampling factors and subdivision * block sizes. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ +// TODO Move to bdv.img package public class MipmapInfo { /** diff --git a/src/main/java/bdv/img/hdf5/Partition.java b/src/main/java/bdv/img/hdf5/Partition.java index f46a2f754f3e9318c599f114bf346d7801255db9..2c7e0716d5e849245b5d5ea2ef9ef7e4e9e461fb 100644 --- a/src/main/java/bdv/img/hdf5/Partition.java +++ b/src/main/java/bdv/img/hdf5/Partition.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -45,7 +44,7 @@ import mpicbg.spim.data.sequence.ViewId; * created using the Partition information (without looking at the constituent * files). * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class Partition { diff --git a/src/main/java/bdv/img/hdf5/Util.java b/src/main/java/bdv/img/hdf5/Util.java index 7da106bc3d5758f2563047e22cfb87cc43632e53..663d09e182201d97d282be06819a667b6f61659c 100644 --- a/src/main/java/bdv/img/hdf5/Util.java +++ b/src/main/java/bdv/img/hdf5/Util.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/hdf5/ViewLevelId.java b/src/main/java/bdv/img/hdf5/ViewLevelId.java index 98a8e1d3dd6fd75c6d6418ee5eac25a939942329..ace8491d2d40e351572019777281bb55869d681a 100644 --- a/src/main/java/bdv/img/hdf5/ViewLevelId.java +++ b/src/main/java/bdv/img/hdf5/ViewLevelId.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/hdf5/XmlIoHdf5ImageLoader.java b/src/main/java/bdv/img/hdf5/XmlIoHdf5ImageLoader.java index 720f150338023cf5c1fc280428c846c465f189be..e211e5986258faaa60dd6fce4efa96e6c8245111 100644 --- a/src/main/java/bdv/img/hdf5/XmlIoHdf5ImageLoader.java +++ b/src/main/java/bdv/img/hdf5/XmlIoHdf5ImageLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/imaris/DataTypes.java b/src/main/java/bdv/img/imaris/DataTypes.java index 239cee7d37e000efed3efd8dd1d99ad95a77a9d1..3f35908ba229a4e328d27aff829dec0f9c10e7b4 100644 --- a/src/main/java/bdv/img/imaris/DataTypes.java +++ b/src/main/java/bdv/img/imaris/DataTypes.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -45,16 +44,16 @@ import net.imglib2.type.volatiles.VolatileUnsignedShortType; class DataTypes { - static interface DataType< + interface DataType< T extends NativeType< T >, V extends Volatile< T > & NativeType< V > , A extends VolatileAccess > { - public T getType(); + T getType(); - public V getVolatileType(); + V getVolatileType(); - public CacheArrayLoader< A > createArrayLoader( final IHDF5Access hdf5Access ); + CacheArrayLoader< A > createArrayLoader( final IHDF5Access hdf5Access ); } static DataType< UnsignedByteType, VolatileUnsignedByteType, VolatileByteArray > UnsignedByte = diff --git a/src/main/java/bdv/img/imaris/HDF5AccessHack.java b/src/main/java/bdv/img/imaris/HDF5AccessHack.java index 22e54dc8042c010e0785cd16189cfc165954b070..bd855276f087e87389f046da494103ad11b3f883 100644 --- a/src/main/java/bdv/img/imaris/HDF5AccessHack.java +++ b/src/main/java/bdv/img/imaris/HDF5AccessHack.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/imaris/IHDF5Access.java b/src/main/java/bdv/img/imaris/IHDF5Access.java index 9f8f994595feea9e67ddfab431e30da641f42cc7..b58f34f54cad04397499d4c3906840d85e993279 100644 --- a/src/main/java/bdv/img/imaris/IHDF5Access.java +++ b/src/main/java/bdv/img/imaris/IHDF5Access.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -34,21 +33,21 @@ import bdv.img.hdf5.ViewLevelId; public interface IHDF5Access { - public DimsAndExistence getDimsAndExistence( final ViewLevelId id ); + DimsAndExistence getDimsAndExistence( final ViewLevelId id ); - public byte[] readByteMDArrayBlockWithOffset( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min ) throws InterruptedException; + byte[] readByteMDArrayBlockWithOffset( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min ) throws InterruptedException; - public byte[] readByteMDArrayBlockWithOffset( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min, final byte[] dataBlock ) throws InterruptedException; + byte[] readByteMDArrayBlockWithOffset( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min, final byte[] dataBlock ) throws InterruptedException; - public short[] readShortMDArrayBlockWithOffset( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min ) throws InterruptedException; + short[] readShortMDArrayBlockWithOffset( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min ) throws InterruptedException; - public short[] readShortMDArrayBlockWithOffset( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min, final short[] dataBlock ) throws InterruptedException; + short[] readShortMDArrayBlockWithOffset( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min, final short[] dataBlock ) throws InterruptedException; - public float[] readFloatMDArrayBlockWithOffset( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min ) throws InterruptedException; + float[] readFloatMDArrayBlockWithOffset( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min ) throws InterruptedException; - public float[] readFloatMDArrayBlockWithOffset( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min, final float[] dataBlock ) throws InterruptedException; + float[] readFloatMDArrayBlockWithOffset( final int timepoint, final int setup, final int level, final int[] dimensions, final long[] min, final float[] dataBlock ) throws InterruptedException; - public String readImarisAttributeString( final String objectPath, final String attributeName ); + String readImarisAttributeString( final String objectPath, final String attributeName ); - public String readImarisAttributeString( final String objectPath, final String attributeName, final String defaultValue ); + String readImarisAttributeString( final String objectPath, final String attributeName, final String defaultValue ); } diff --git a/src/main/java/bdv/img/imaris/Imaris.java b/src/main/java/bdv/img/imaris/Imaris.java index d2187532e632f4c398f17ed3eab61168d6af456b..7e3ce052bcc233f841571eae366f11f0ec6c925c 100644 --- a/src/main/java/bdv/img/imaris/Imaris.java +++ b/src/main/java/bdv/img/imaris/Imaris.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/imaris/ImarisImageLoader.java b/src/main/java/bdv/img/imaris/ImarisImageLoader.java index 5a3c425f4c90f0eb3ee26d651aa12e87b54a4ec3..6fa4509ca96cc4af95c84c31b94d590bfff2762e 100644 --- a/src/main/java/bdv/img/imaris/ImarisImageLoader.java +++ b/src/main/java/bdv/img/imaris/ImarisImageLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/imaris/ImarisVolatileByteArrayLoader.java b/src/main/java/bdv/img/imaris/ImarisVolatileByteArrayLoader.java index a1f5bdf9d615a73ae85b7cc4c3e3e829f35c51cd..e58f9fa3958648f6481a56fa490156e448ff2066 100644 --- a/src/main/java/bdv/img/imaris/ImarisVolatileByteArrayLoader.java +++ b/src/main/java/bdv/img/imaris/ImarisVolatileByteArrayLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/imaris/ImarisVolatileFloatArrayLoader.java b/src/main/java/bdv/img/imaris/ImarisVolatileFloatArrayLoader.java index 69068c2ddeb3c177d4d7d6d9dda6bbb3a4fe44c2..11dc4b12ef95d3c6c8cf104a19b1f1a53d1a6b83 100644 --- a/src/main/java/bdv/img/imaris/ImarisVolatileFloatArrayLoader.java +++ b/src/main/java/bdv/img/imaris/ImarisVolatileFloatArrayLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/imaris/ImarisVolatileShortArrayLoader.java b/src/main/java/bdv/img/imaris/ImarisVolatileShortArrayLoader.java index 77cec55f26ab21d69d1bd33f0f1b2c98de715095..0eecf007ab0d2faf8d0a5fc2a451b44e1eac308f 100644 --- a/src/main/java/bdv/img/imaris/ImarisVolatileShortArrayLoader.java +++ b/src/main/java/bdv/img/imaris/ImarisVolatileShortArrayLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/n5/BdvN5Format.java b/src/main/java/bdv/img/n5/BdvN5Format.java new file mode 100644 index 0000000000000000000000000000000000000000..145f1ef1a60f78420e57dba032ba96cbd6809bba --- /dev/null +++ b/src/main/java/bdv/img/n5/BdvN5Format.java @@ -0,0 +1,50 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.img.n5; + +public class BdvN5Format +{ + public static final String DOWNSAMPLING_FACTORS_KEY = "downsamplingFactors"; + public static final String DATA_TYPE_KEY = "dataType"; + + public static String getPathName( final int setupId ) + { + return String.format( "setup%d", setupId ); + } + + public static String getPathName( final int setupId, final int timepointId ) + { + return String.format( "setup%d/timepoint%d", setupId, timepointId ); + } + + public static String getPathName( final int setupId, final int timepointId, final int level ) + { + return String.format( "setup%d/timepoint%d/s%d", setupId, timepointId, level ); + } +} diff --git a/src/main/java/bdv/img/n5/N5ImageLoader.java b/src/main/java/bdv/img/n5/N5ImageLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..e3d6db5a979ef47db9ae185ee31b338c9a2ee7cc --- /dev/null +++ b/src/main/java/bdv/img/n5/N5ImageLoader.java @@ -0,0 +1,418 @@ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.img.n5; + +import bdv.AbstractViewerSetupImgLoader; +import bdv.ViewerImgLoader; +import bdv.cache.CacheControl; +import bdv.img.cache.SimpleCacheArrayLoader; +import bdv.img.cache.VolatileGlobalCellCache; +import bdv.util.ConstantRandomAccessible; +import bdv.util.MipmapTransforms; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.function.Function; +import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription; +import mpicbg.spim.data.generic.sequence.BasicViewSetup; +import mpicbg.spim.data.generic.sequence.ImgLoaderHint; +import mpicbg.spim.data.sequence.MultiResolutionImgLoader; +import mpicbg.spim.data.sequence.MultiResolutionSetupImgLoader; +import mpicbg.spim.data.sequence.VoxelDimensions; +import net.imglib2.Dimensions; +import net.imglib2.FinalDimensions; +import net.imglib2.FinalInterval; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.Volatile; +import net.imglib2.cache.queue.BlockingFetchQueues; +import net.imglib2.cache.queue.FetcherThreads; +import net.imglib2.cache.volatiles.CacheHints; +import net.imglib2.cache.volatiles.LoadingStrategy; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileByteArray; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileDoubleArray; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileFloatArray; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileIntArray; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileLongArray; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileShortArray; +import net.imglib2.img.cell.CellGrid; +import net.imglib2.img.cell.CellImg; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.integer.ByteType; +import net.imglib2.type.numeric.integer.IntType; +import net.imglib2.type.numeric.integer.LongType; +import net.imglib2.type.numeric.integer.ShortType; +import net.imglib2.type.numeric.integer.UnsignedByteType; +import net.imglib2.type.numeric.integer.UnsignedIntType; +import net.imglib2.type.numeric.integer.UnsignedLongType; +import net.imglib2.type.numeric.integer.UnsignedShortType; +import net.imglib2.type.numeric.real.DoubleType; +import net.imglib2.type.numeric.real.FloatType; +import net.imglib2.type.volatiles.VolatileByteType; +import net.imglib2.type.volatiles.VolatileDoubleType; +import net.imglib2.type.volatiles.VolatileFloatType; +import net.imglib2.type.volatiles.VolatileIntType; +import net.imglib2.type.volatiles.VolatileLongType; +import net.imglib2.type.volatiles.VolatileShortType; +import net.imglib2.type.volatiles.VolatileUnsignedByteType; +import net.imglib2.type.volatiles.VolatileUnsignedIntType; +import net.imglib2.type.volatiles.VolatileUnsignedLongType; +import net.imglib2.type.volatiles.VolatileUnsignedShortType; +import net.imglib2.util.Cast; +import net.imglib2.view.Views; +import org.janelia.saalfeldlab.n5.*; + +import static bdv.img.n5.BdvN5Format.DATA_TYPE_KEY; +import static bdv.img.n5.BdvN5Format.DOWNSAMPLING_FACTORS_KEY; +import static bdv.img.n5.BdvN5Format.getPathName; + +public class N5ImageLoader implements ViewerImgLoader, MultiResolutionImgLoader +{ + private final File n5File; + + // TODO: it would be good if this would not be needed + // find available setups from the n5 + private final AbstractSequenceDescription< ?, ?, ? > seq; + + /** + * Maps setup id to {@link SetupImgLoader}. + */ + private final Map< Integer, SetupImgLoader > setupImgLoaders = new HashMap<>(); + + public N5ImageLoader( final File n5File, final AbstractSequenceDescription< ?, ?, ? > sequenceDescription ) + { + this.n5File = n5File; + this.seq = sequenceDescription; + } + + public File getN5File() + { + return n5File; + } + + private volatile boolean isOpen = false; + private FetcherThreads fetchers; + private VolatileGlobalCellCache cache; + private N5Reader n5; + + private void open() + { + if ( !isOpen ) + { + synchronized ( this ) + { + if ( isOpen ) + return; + + try + { + this.n5 = new N5FSReader( n5File.getAbsolutePath() ); + + int maxNumLevels = 0; + final List< ? extends BasicViewSetup > setups = seq.getViewSetupsOrdered(); + for ( final BasicViewSetup setup : setups ) + { + final int setupId = setup.getId(); + final SetupImgLoader setupImgLoader = createSetupImgLoader( setupId ); + setupImgLoaders.put( setupId, setupImgLoader ); + maxNumLevels = Math.max( maxNumLevels, setupImgLoader.numMipmapLevels() ); + } + + final int numFetcherThreads = Math.max( 1, Runtime.getRuntime().availableProcessors() ); + final BlockingFetchQueues< Callable< ? > > queue = new BlockingFetchQueues<>( maxNumLevels, numFetcherThreads ); + fetchers = new FetcherThreads( queue, numFetcherThreads ); + cache = new VolatileGlobalCellCache( queue ); + } + catch ( IOException e ) + { + throw new RuntimeException( e ); + } + + isOpen = true; + } + } + } + + /** + * Clear the cache. Images that were obtained from + * this loader before {@link #close()} will stop working. Requesting images + * after {@link #close()} will cause the n5 to be reopened (with a + * new cache). + */ + public void close() + { + if ( isOpen ) + { + synchronized ( this ) + { + if ( !isOpen ) + return; + fetchers.shutdown(); + cache.clearCache(); + isOpen = false; + } + } + } + + @Override + public SetupImgLoader getSetupImgLoader( final int setupId ) + { + open(); + return setupImgLoaders.get( setupId ); + } + + private < T extends NativeType< T >, V extends Volatile< T > & NativeType< V > > SetupImgLoader< T, V > createSetupImgLoader( final int setupId ) throws IOException + { + final String pathName = getPathName( setupId ); + final DataType dataType = n5.getAttribute( pathName, DATA_TYPE_KEY, DataType.class ); + switch ( dataType ) + { + case UINT8: + return Cast.unchecked( new SetupImgLoader<>( setupId, new UnsignedByteType(), new VolatileUnsignedByteType() ) ); + case UINT16: + return Cast.unchecked( new SetupImgLoader<>( setupId, new UnsignedShortType(), new VolatileUnsignedShortType() ) ); + case UINT32: + return Cast.unchecked( new SetupImgLoader<>( setupId, new UnsignedIntType(), new VolatileUnsignedIntType() ) ); + case UINT64: + return Cast.unchecked( new SetupImgLoader<>( setupId, new UnsignedLongType(), new VolatileUnsignedLongType() ) ); + case INT8: + return Cast.unchecked( new SetupImgLoader<>( setupId, new ByteType(), new VolatileByteType() ) ); + case INT16: + return Cast.unchecked( new SetupImgLoader<>( setupId, new ShortType(), new VolatileShortType() ) ); + case INT32: + return Cast.unchecked( new SetupImgLoader<>( setupId, new IntType(), new VolatileIntType() ) ); + case INT64: + return Cast.unchecked( new SetupImgLoader<>( setupId, new LongType(), new VolatileLongType() ) ); + case FLOAT32: + return Cast.unchecked( new SetupImgLoader<>( setupId, new FloatType(), new VolatileFloatType() ) ); + case FLOAT64: + return Cast.unchecked( new SetupImgLoader<>( setupId, new DoubleType(), new VolatileDoubleType() ) ); + } + return null; + } + + @Override + public CacheControl getCacheControl() + { + open(); + return cache; + } + + public class SetupImgLoader< T extends NativeType< T >, V extends Volatile< T > & NativeType< V > > + extends AbstractViewerSetupImgLoader< T, V > + implements MultiResolutionSetupImgLoader< T > + { + private final int setupId; + + private final double[][] mipmapResolutions; + + private final AffineTransform3D[] mipmapTransforms; + + public SetupImgLoader( final int setupId, final T type, final V volatileType ) throws IOException + { + super( type, volatileType ); + this.setupId = setupId; + final String pathName = getPathName( setupId ); + mipmapResolutions = n5.getAttribute( pathName, DOWNSAMPLING_FACTORS_KEY, double[][].class ); + mipmapTransforms = new AffineTransform3D[ mipmapResolutions.length ]; + for ( int level = 0; level < mipmapResolutions.length; level++ ) + mipmapTransforms[ level ] = MipmapTransforms.getMipmapTransformDefault( mipmapResolutions[ level ] ); + } + + @Override + public RandomAccessibleInterval< V > getVolatileImage( final int timepointId, final int level, final ImgLoaderHint... hints ) + { + return prepareCachedImage( timepointId, level, LoadingStrategy.BUDGETED, volatileType ); + } + + @Override + public RandomAccessibleInterval< T > getImage( final int timepointId, final int level, final ImgLoaderHint... hints ) + { + return prepareCachedImage( timepointId, level, LoadingStrategy.BLOCKING, type ); + } + + @Override + public Dimensions getImageSize( final int timepointId, final int level ) + { + try + { + final String pathName = getPathName( setupId, timepointId, level ); + final DatasetAttributes attributes = n5.getDatasetAttributes( pathName ); + return new FinalDimensions( attributes.getDimensions() ); + } + catch( Exception e ) + { + return null; + } + } + + @Override + public double[][] getMipmapResolutions() + { + return mipmapResolutions; + } + + @Override + public AffineTransform3D[] getMipmapTransforms() + { + return mipmapTransforms; + } + + @Override + public int numMipmapLevels() + { + return mipmapResolutions.length; + } + + @Override + public VoxelDimensions getVoxelSize( final int timepointId ) + { + return null; + } + + /** + * Create a {@link CellImg} backed by the cache. + */ + private < T extends NativeType< T > > RandomAccessibleInterval< T > prepareCachedImage( final int timepointId, final int level, final LoadingStrategy loadingStrategy, final T type ) + { + try + { + final String pathName = getPathName( setupId, timepointId, level ); + final DatasetAttributes attributes = n5.getDatasetAttributes( pathName ); + final long[] dimensions = attributes.getDimensions(); + final int[] cellDimensions = attributes.getBlockSize(); + final CellGrid grid = new CellGrid( dimensions, cellDimensions ); + + final int priority = numMipmapLevels() - 1 - level; + final CacheHints cacheHints = new CacheHints( loadingStrategy, priority, false ); + + final SimpleCacheArrayLoader< ? > loader = createCacheArrayLoader( n5, pathName ); + return cache.createImg( grid, timepointId, setupId, level, cacheHints, loader, type ); + } + catch ( IOException e ) + { + System.err.println( String.format( + "image data for timepoint %d setup %d level %d could not be found.", + timepointId, setupId, level ) ); + return Views.interval( + new ConstantRandomAccessible<>( type.createVariable(), 3 ), + new FinalInterval( 1, 1, 1 ) ); + } + } + } + + private static class N5CacheArrayLoader< A > implements SimpleCacheArrayLoader< A > + { + private final N5Reader n5; + private final String pathName; + private final DatasetAttributes attributes; + private final Function< DataBlock< ? >, A > createArray; + + N5CacheArrayLoader( final N5Reader n5, final String pathName, final DatasetAttributes attributes, final Function< DataBlock< ? >, A > createArray ) + { + this.n5 = n5; + this.pathName = pathName; + this.attributes = attributes; + this.createArray = createArray; + } + + @Override + public A loadArray( final long[] gridPosition ) throws IOException + { + final DataBlock< ? > dataBlock = n5.readBlock( pathName, attributes, gridPosition ); + + if ( dataBlock == null ) + return createEmptyArray( gridPosition ); + else + return createArray.apply( dataBlock ); + } + + private A createEmptyArray( long[] gridPosition ) + { + final int[] blockSize = attributes.getBlockSize(); + final int n = blockSize[ 0 ] * blockSize[ 1 ] * blockSize[ 2 ]; + switch ( attributes.getDataType() ) + { + case UINT8: + case INT8: + return createArray.apply( new ByteArrayDataBlock( blockSize, gridPosition, new byte[ n ] ) ); + case UINT16: + case INT16: + return createArray.apply( new ShortArrayDataBlock( blockSize, gridPosition, new short[ n ] ) ); + case UINT32: + case INT32: + return createArray.apply( new IntArrayDataBlock( blockSize, gridPosition, new int[ n ] ) ); + case UINT64: + case INT64: + return createArray.apply( new LongArrayDataBlock( blockSize, gridPosition, new long[ n ] ) ); + case FLOAT32: + return createArray.apply( new FloatArrayDataBlock( blockSize, gridPosition, new float[ n ] ) ); + case FLOAT64: + return createArray.apply( new DoubleArrayDataBlock( blockSize, gridPosition, new double[ n ] ) ); + default: + throw new UnsupportedOperationException("Data type not supported: " + attributes.getDataType()); + } + } + } + + public static SimpleCacheArrayLoader< ? > createCacheArrayLoader( final N5Reader n5, final String pathName ) throws IOException + { + final DatasetAttributes attributes = n5.getDatasetAttributes( pathName ); + switch ( attributes.getDataType() ) + { + case UINT8: + case INT8: + return new N5CacheArrayLoader<>( n5, pathName, attributes, + dataBlock -> new VolatileByteArray( Cast.unchecked( dataBlock.getData() ), true ) ); + case UINT16: + case INT16: + return new N5CacheArrayLoader<>( n5, pathName, attributes, + dataBlock -> new VolatileShortArray( Cast.unchecked( dataBlock.getData() ), true ) ); + case UINT32: + case INT32: + return new N5CacheArrayLoader<>( n5, pathName, attributes, + dataBlock -> new VolatileIntArray( Cast.unchecked( dataBlock.getData() ), true ) ); + case UINT64: + case INT64: + return new N5CacheArrayLoader<>( n5, pathName, attributes, + dataBlock -> new VolatileLongArray( Cast.unchecked( dataBlock.getData() ), true ) ); + case FLOAT32: + return new N5CacheArrayLoader<>( n5, pathName, attributes, + dataBlock -> new VolatileFloatArray( Cast.unchecked( dataBlock.getData() ), true ) ); + case FLOAT64: + return new N5CacheArrayLoader<>( n5, pathName, attributes, + dataBlock -> new VolatileDoubleArray( Cast.unchecked( dataBlock.getData() ), true ) ); + default: + throw new IllegalArgumentException(); + } + } +} diff --git a/src/main/java/bdv/img/n5/XmlIoN5ImageLoader.java b/src/main/java/bdv/img/n5/XmlIoN5ImageLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..aa95e6bef6be169fdec38f6d2bdac6588fff9e09 --- /dev/null +++ b/src/main/java/bdv/img/n5/XmlIoN5ImageLoader.java @@ -0,0 +1,61 @@ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.img.n5; + +import java.io.File; +import mpicbg.spim.data.XmlHelpers; +import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription; +import mpicbg.spim.data.generic.sequence.ImgLoaderIo; +import mpicbg.spim.data.generic.sequence.XmlIoBasicImgLoader; +import org.jdom2.Element; + +import static mpicbg.spim.data.XmlHelpers.loadPath; +import static mpicbg.spim.data.XmlKeys.IMGLOADER_FORMAT_ATTRIBUTE_NAME; + +@ImgLoaderIo( format = "bdv.n5", type = N5ImageLoader.class ) +public class XmlIoN5ImageLoader implements XmlIoBasicImgLoader< N5ImageLoader > +{ + @Override + public Element toXml( final N5ImageLoader imgLoader, final File basePath ) + { + final Element elem = new Element( "ImageLoader" ); + elem.setAttribute( IMGLOADER_FORMAT_ATTRIBUTE_NAME, "bdv.n5" ); + elem.setAttribute( "version", "1.0" ); + elem.addContent( XmlHelpers.pathElement( "n5", imgLoader.getN5File(), basePath ) ); + return elem; + } + + @Override + public N5ImageLoader fromXml( final Element elem, final File basePath, final AbstractSequenceDescription< ?, ?, ? > sequenceDescription ) + { +// final String version = elem.getAttributeValue( "version" ); + final File path = loadPath( elem, "n5", basePath ); + return new N5ImageLoader( path, sequenceDescription ); + } +} diff --git a/src/main/java/bdv/img/openconnectome/OpenConnectomeDataset.java b/src/main/java/bdv/img/openconnectome/OpenConnectomeDataset.java index dc51630af63c9ae74d3c3718738669f5433c8935..a4b7f95109b6e8187717d4ef4772ca6c71fbd327 100644 --- a/src/main/java/bdv/img/openconnectome/OpenConnectomeDataset.java +++ b/src/main/java/bdv/img/openconnectome/OpenConnectomeDataset.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -38,7 +37,7 @@ import java.util.HashMap; * * <a href="http://openconnecto.me/ocp/ca/bock11/info/">http://openconnecto.me/ocp/ca/bock11/info/</a> * - * @author Stephan Saalfeld <saalfelds@janelia.hhmi.org> + * @author Stephan Saalfeld */ public class OpenConnectomeDataset implements Serializable { diff --git a/src/main/java/bdv/img/openconnectome/OpenConnectomeImageLoader.java b/src/main/java/bdv/img/openconnectome/OpenConnectomeImageLoader.java index 263c83fcccced7347a83aded2e58275d1355813b..f4449a0e144c7325d675a3965a8b85a9a2cb33b4 100644 --- a/src/main/java/bdv/img/openconnectome/OpenConnectomeImageLoader.java +++ b/src/main/java/bdv/img/openconnectome/OpenConnectomeImageLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/openconnectome/OpenConnectomeProject.java b/src/main/java/bdv/img/openconnectome/OpenConnectomeProject.java index d462d926df85024495c2e87302c6256448adba40..ab77eae8e9cccf428969e1d33db7f423c02449dc 100644 --- a/src/main/java/bdv/img/openconnectome/OpenConnectomeProject.java +++ b/src/main/java/bdv/img/openconnectome/OpenConnectomeProject.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -36,7 +35,7 @@ import java.io.Serializable; * * {@linkplain } * - * @author Stephan Saalfeld <saalfelds@janelia.hhmi.org> + * @author Stephan Saalfeld */ public class OpenConnectomeProject implements Serializable { diff --git a/src/main/java/bdv/img/openconnectome/OpenConnectomeTokenInfo.java b/src/main/java/bdv/img/openconnectome/OpenConnectomeTokenInfo.java index 7b6cad957f2540b1158de2a5a3a7a1b65af77de1..876c92ea111a9a7d38ff3235ed4f2fe374925b29 100644 --- a/src/main/java/bdv/img/openconnectome/OpenConnectomeTokenInfo.java +++ b/src/main/java/bdv/img/openconnectome/OpenConnectomeTokenInfo.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/openconnectome/OpenConnectomeVolatileArrayLoader.java b/src/main/java/bdv/img/openconnectome/OpenConnectomeVolatileArrayLoader.java index 1d9214fe4244fa43a9ad982e2da533045c99c927..47ea4879e67e2198c6be52dfb263bdb30d166d33 100644 --- a/src/main/java/bdv/img/openconnectome/OpenConnectomeVolatileArrayLoader.java +++ b/src/main/java/bdv/img/openconnectome/OpenConnectomeVolatileArrayLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/openconnectome/XmlIoOpenConnectomeImageLoader.java b/src/main/java/bdv/img/openconnectome/XmlIoOpenConnectomeImageLoader.java index b04e312e20c56ccd9663053b0e80e345cff3b4a3..8f4ed67656fb56cf8102fbe82aa01c344ae5b0f9 100644 --- a/src/main/java/bdv/img/openconnectome/XmlIoOpenConnectomeImageLoader.java +++ b/src/main/java/bdv/img/openconnectome/XmlIoOpenConnectomeImageLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/remote/AffineTransform3DJsonSerializer.java b/src/main/java/bdv/img/remote/AffineTransform3DJsonSerializer.java index 452e88cb83c8ed989c47300b14116b41439d7686..0b0f0dcdc2c6ad7edb3fd90ea00ca1e560e2521a 100644 --- a/src/main/java/bdv/img/remote/AffineTransform3DJsonSerializer.java +++ b/src/main/java/bdv/img/remote/AffineTransform3DJsonSerializer.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/remote/RemoteImageLoader.java b/src/main/java/bdv/img/remote/RemoteImageLoader.java index 949c7cf330d729a023a3ed5d57a3e0b7a37d84e8..01835d7790c86af1b55466e22a035fd41c4964cc 100644 --- a/src/main/java/bdv/img/remote/RemoteImageLoader.java +++ b/src/main/java/bdv/img/remote/RemoteImageLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/remote/RemoteImageLoaderMetaData.java b/src/main/java/bdv/img/remote/RemoteImageLoaderMetaData.java index cc0f1e38219b2f3c8d2e8bf5e729960dc42e4e08..b08fe14d48cc1dfbab91dc8681176cc0724935d8 100644 --- a/src/main/java/bdv/img/remote/RemoteImageLoaderMetaData.java +++ b/src/main/java/bdv/img/remote/RemoteImageLoaderMetaData.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/remote/RemoteVolatileShortArrayLoader.java b/src/main/java/bdv/img/remote/RemoteVolatileShortArrayLoader.java index 7a85c86b23b11a1237f711982f5f250d28b757ba..8d6dbb9edc3455974f7719ef15e3d1ce5c5dd81c 100644 --- a/src/main/java/bdv/img/remote/RemoteVolatileShortArrayLoader.java +++ b/src/main/java/bdv/img/remote/RemoteVolatileShortArrayLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/img/remote/XmlIoRemoteImageLoader.java b/src/main/java/bdv/img/remote/XmlIoRemoteImageLoader.java index 6b9b8a03b3687b30b4f0345429dfe455298770e0..fd53dc5a2cea9b3b0afa2fbb09d3b323cc2b1979 100644 --- a/src/main/java/bdv/img/remote/XmlIoRemoteImageLoader.java +++ b/src/main/java/bdv/img/remote/XmlIoRemoteImageLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/spimdata/SequenceDescriptionMinimal.java b/src/main/java/bdv/spimdata/SequenceDescriptionMinimal.java index de5c21db173f8ee35b19169a2c38b112bcb528d0..d75c37bd3c8d5960707bda70973daf611b3aedb7 100644 --- a/src/main/java/bdv/spimdata/SequenceDescriptionMinimal.java +++ b/src/main/java/bdv/spimdata/SequenceDescriptionMinimal.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/spimdata/SpimDataMinimal.java b/src/main/java/bdv/spimdata/SpimDataMinimal.java index bbd41f29ce0053e1e8debc9ef791d503dc5a3eb3..1c2829c2464750b17a9409d75b2823b941e2887f 100644 --- a/src/main/java/bdv/spimdata/SpimDataMinimal.java +++ b/src/main/java/bdv/spimdata/SpimDataMinimal.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/spimdata/WrapBasicImgLoader.java b/src/main/java/bdv/spimdata/WrapBasicImgLoader.java index 6b7802192e1c50414903b52ff441354dbfd88abf..522bc8ce1b45cc8aff2bdf378e163eb17113d91a 100644 --- a/src/main/java/bdv/spimdata/WrapBasicImgLoader.java +++ b/src/main/java/bdv/spimdata/WrapBasicImgLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/spimdata/XmlIoSpimDataMinimal.java b/src/main/java/bdv/spimdata/XmlIoSpimDataMinimal.java index 2988d6d25513031886122e82e3ee78b85b9780ac..2d5daf3cb1bde2824b930c8d4f23f52438f5b5d9 100644 --- a/src/main/java/bdv/spimdata/XmlIoSpimDataMinimal.java +++ b/src/main/java/bdv/spimdata/XmlIoSpimDataMinimal.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/spimdata/legacy/AbstractLegacyViewerImgLoader.java b/src/main/java/bdv/spimdata/legacy/AbstractLegacyViewerImgLoader.java index b529b98f6d2fb2ceb0aa8358e52bb979d01e01f2..2c6520dc00f103789eb59d0c74e426d0e571a2e1 100644 --- a/src/main/java/bdv/spimdata/legacy/AbstractLegacyViewerImgLoader.java +++ b/src/main/java/bdv/spimdata/legacy/AbstractLegacyViewerImgLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/spimdata/legacy/LegacyViewerImgLoader.java b/src/main/java/bdv/spimdata/legacy/LegacyViewerImgLoader.java index 1d8558bf6357487fb87e49edb24da5a3c2a0a194..35fe12b30dc2a14fdf01024e3a6f9972cc9fba26 100644 --- a/src/main/java/bdv/spimdata/legacy/LegacyViewerImgLoader.java +++ b/src/main/java/bdv/spimdata/legacy/LegacyViewerImgLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -39,17 +38,17 @@ import net.imglib2.realtransform.AffineTransform3D; //@Deprecated public interface LegacyViewerImgLoader< T, V extends Volatile< T > > extends LegacyBasicImgLoader< T > { - public RandomAccessibleInterval< T > getImage( final ViewId view, final int level ); + RandomAccessibleInterval< T > getImage( final ViewId view, final int level ); - public RandomAccessibleInterval< V > getVolatileImage( final ViewId view, final int level ); + RandomAccessibleInterval< V > getVolatileImage( final ViewId view, final int level ); - public V getVolatileImageType(); + V getVolatileImageType(); - public double[][] getMipmapResolutions( final int setupId ); + double[][] getMipmapResolutions( final int setupId ); - public AffineTransform3D[] getMipmapTransforms( final int setupId ); + AffineTransform3D[] getMipmapTransforms( final int setupId ); - public int numMipmapLevels( final int setupId ); + int numMipmapLevels( final int setupId ); - public CacheControl getCache(); + CacheControl getCache(); } diff --git a/src/main/java/bdv/spimdata/legacy/LegacyViewerImgLoaderExtWrapper.java b/src/main/java/bdv/spimdata/legacy/LegacyViewerImgLoaderExtWrapper.java index af4d90e388fb05a53830ba3bec09fc80326d9997..3ab43ba168276a914d0afcea200be9ad780a202f 100644 --- a/src/main/java/bdv/spimdata/legacy/LegacyViewerImgLoaderExtWrapper.java +++ b/src/main/java/bdv/spimdata/legacy/LegacyViewerImgLoaderExtWrapper.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/spimdata/legacy/LegacyViewerImgLoaderWrapper.java b/src/main/java/bdv/spimdata/legacy/LegacyViewerImgLoaderWrapper.java index bec5d3202cfd23a0c92ed17b1625ab205147ea17..998158f7f31a501bb53ee4e11666a00cd619fc0c 100644 --- a/src/main/java/bdv/spimdata/legacy/LegacyViewerImgLoaderWrapper.java +++ b/src/main/java/bdv/spimdata/legacy/LegacyViewerImgLoaderWrapper.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/spimdata/legacy/XmlIoSpimDataMinimalLegacy.java b/src/main/java/bdv/spimdata/legacy/XmlIoSpimDataMinimalLegacy.java index a59c57b3d11f2b6225474398994294219a909c36..c9365a9d48571292cc5fcfb50d6edf264c4ad912 100644 --- a/src/main/java/bdv/spimdata/legacy/XmlIoSpimDataMinimalLegacy.java +++ b/src/main/java/bdv/spimdata/legacy/XmlIoSpimDataMinimalLegacy.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/spimdata/tools/ChangeAttributeId.java b/src/main/java/bdv/spimdata/tools/ChangeAttributeId.java index 75f6d94cc3fb222b52542e4e6dbf13465925c980..ad983b332f1de5adc000b51a93b641ee77dadba5 100644 --- a/src/main/java/bdv/spimdata/tools/ChangeAttributeId.java +++ b/src/main/java/bdv/spimdata/tools/ChangeAttributeId.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/spimdata/tools/ChangeViewSetupId.java b/src/main/java/bdv/spimdata/tools/ChangeViewSetupId.java index 263f573035700e5ee7d018c571d32a1fe8e1cc8a..fa135b3808d961cb8f52210a1c29641c5f1072a4 100644 --- a/src/main/java/bdv/spimdata/tools/ChangeViewSetupId.java +++ b/src/main/java/bdv/spimdata/tools/ChangeViewSetupId.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/spimdata/tools/MergeExample.java b/src/main/java/bdv/spimdata/tools/MergeExample.java index 8e3788cde4c0f296ed15201db85603bf97f54a24..96d8d713606de59ea0ea5635313c4443fc6b1bbb 100644 --- a/src/main/java/bdv/spimdata/tools/MergeExample.java +++ b/src/main/java/bdv/spimdata/tools/MergeExample.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -55,7 +54,7 @@ import mpicbg.spim.data.sequence.TimePoints; import mpicbg.spim.data.sequence.ViewId; /** - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class MergeExample { diff --git a/src/main/java/bdv/spimdata/tools/MergePartitionList.java b/src/main/java/bdv/spimdata/tools/MergePartitionList.java index 5ba2717b502845fd117b13d936bd95773110e901..1721bf0b52f413b62a481cd3d1f9cf9018bc6dd2 100644 --- a/src/main/java/bdv/spimdata/tools/MergePartitionList.java +++ b/src/main/java/bdv/spimdata/tools/MergePartitionList.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/spimdata/tools/MergeTools.java b/src/main/java/bdv/spimdata/tools/MergeTools.java index 2222c471f69edfea3f6fe4f6d09e06adac986b3f..4a8add0f41e3cfcb28bb07aa954a6b123cb786db 100644 --- a/src/main/java/bdv/spimdata/tools/MergeTools.java +++ b/src/main/java/bdv/spimdata/tools/MergeTools.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/tools/HelpDialog.java b/src/main/java/bdv/tools/HelpDialog.java index 87658494ea239366c39bbbef4d203391c5139c46..61edcb7ed41fdb417c49f216def1d1f7fa9b5a6b 100644 --- a/src/main/java/bdv/tools/HelpDialog.java +++ b/src/main/java/bdv/tools/HelpDialog.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,6 +28,8 @@ */ package bdv.tools; +import bdv.util.DelayedPackDialog; + import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Frame; @@ -43,14 +44,12 @@ import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.InputMap; import javax.swing.JComponent; -import javax.swing.JDialog; import javax.swing.JEditorPane; import javax.swing.JScrollPane; import javax.swing.KeyStroke; import javax.swing.ScrollPaneConstants; -import javax.swing.WindowConstants; -public class HelpDialog extends JDialog +public class HelpDialog extends DelayedPackDialog { private static final long serialVersionUID = 1L; @@ -101,7 +100,6 @@ public class HelpDialog extends JDialog am.put( hideKey, hideAction ); pack(); - setDefaultCloseOperation( WindowConstants.HIDE_ON_CLOSE ); } catch ( final IOException e ) { diff --git a/src/main/java/bdv/tools/InitializeViewerState.java b/src/main/java/bdv/tools/InitializeViewerState.java index 36aa6b2a4d7b2df0f62fb0c796f5a3c82f0a2662..02f1cb8562cf248382b35c69a9527ee660bc6be7 100644 --- a/src/main/java/bdv/tools/InitializeViewerState.java +++ b/src/main/java/bdv/tools/InitializeViewerState.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,6 +28,10 @@ */ package bdv.tools; +import bdv.tools.brightness.ConverterSetup; +import bdv.util.Bounds; +import bdv.viewer.ConverterSetups; +import bdv.viewer.ViewerFrame; import java.awt.Dimension; import net.imglib2.Interval; @@ -37,15 +40,20 @@ import net.imglib2.histogram.DiscreteFrequencyDistribution; import net.imglib2.histogram.Histogram1d; import net.imglib2.histogram.Real1dBinMapper; import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.RealType; +import net.imglib2.type.numeric.integer.UnsignedByteType; import net.imglib2.type.numeric.integer.UnsignedShortType; import net.imglib2.util.LinAlgHelpers; +import net.imglib2.view.IntervalView; import net.imglib2.view.Views; + import bdv.tools.brightness.MinMaxGroup; import bdv.tools.brightness.SetupAssignments; import bdv.util.Affine3DHelpers; import bdv.viewer.Source; +import bdv.viewer.SourceAndConverter; import bdv.viewer.ViewerPanel; -import bdv.viewer.state.ViewerState; +import bdv.viewer.ViewerState; public class InitializeViewerState { @@ -68,9 +76,8 @@ public class InitializeViewerState public static void initTransform( final ViewerPanel viewer ) { final Dimension dim = viewer.getDisplay().getSize(); - final ViewerState state = viewer.getState(); - final AffineTransform3D viewerTransform = initTransform( dim.width, dim.height, false, state ); - viewer.setCurrentViewerTransform( viewerTransform ); + final AffineTransform3D viewerTransform = initTransform( dim.width, dim.height, false, viewer.state().snapshot() ); + viewer.state().setViewerTransform( viewerTransform ); } /** @@ -93,13 +100,17 @@ public class InitializeViewerState */ public static AffineTransform3D initTransform( final int viewerWidth, final int viewerHeight, final boolean zoomedIn, final ViewerState state ) { + final AffineTransform3D viewerTransform = new AffineTransform3D(); final double cX = viewerWidth / 2.0; final double cY = viewerHeight / 2.0; - final Source< ? > source = state.getSources().get( state.getCurrentSource() ).getSpimSource(); + final SourceAndConverter< ? > current = state.getCurrentSource(); + if ( current == null ) + return viewerTransform; + final Source< ? > source = current.getSpimSource(); final int timepoint = state.getCurrentTimepoint(); if ( !source.isPresent( timepoint ) ) - return new AffineTransform3D(); + return viewerTransform; final AffineTransform3D sourceTransform = new AffineTransform3D(); source.getSourceTransform( timepoint, 0, sourceTransform ); @@ -133,7 +144,6 @@ public class InitializeViewerState LinAlgHelpers.scale( translation, -1, translation ); LinAlgHelpers.setCol( 3, translation, m ); - final AffineTransform3D viewerTransform = new AffineTransform3D(); viewerTransform.set( m ); // scale @@ -157,51 +167,97 @@ public class InitializeViewerState return viewerTransform; } - public static void initBrightness( final double cumulativeMinCutoff, final double cumulativeMaxCutoff, final ViewerPanel viewer, final SetupAssignments setupAssignments ) + public static void initBrightness( final double cumulativeMinCutoff, final double cumulativeMaxCutoff, final ViewerFrame viewerFrame ) + { + initBrightness( cumulativeMinCutoff, cumulativeMaxCutoff, viewerFrame.getViewerPanel().state().snapshot(), viewerFrame.getConverterSetups() ); + } + + public static void initBrightness( final double cumulativeMinCutoff, final double cumulativeMaxCutoff, final ViewerState state, final ConverterSetups converterSetups ) { - initBrightness( cumulativeMinCutoff, cumulativeMaxCutoff, viewer.getState(), setupAssignments ); + final SourceAndConverter< ? > current = state.getCurrentSource(); + if ( current == null ) + return; + final Source< ? > source = current.getSpimSource(); + final int timepoint = state.getCurrentTimepoint(); + final Bounds bounds = estimateSourceRange( source, timepoint, cumulativeMinCutoff, cumulativeMaxCutoff ); + for ( SourceAndConverter< ? > s : state.getSources() ) + { + final ConverterSetup setup = converterSetups.getConverterSetup( s ); + setup.setDisplayRange( bounds.getMinBound(), bounds.getMaxBound() ); + } } /** - * TODO - * * @param cumulativeMinCutoff + * fraction of pixels that are allowed to be saturated at the lower end of the range. * @param cumulativeMaxCutoff - * @param state - * @param setupAssignments + * fraction of pixels that are allowed to be saturated at the upper end of the range. */ - public static void initBrightness( final double cumulativeMinCutoff, final double cumulativeMaxCutoff, final ViewerState state, final SetupAssignments setupAssignments ) + public static Bounds estimateSourceRange( final Source< ? > source, final int timepoint, final double cumulativeMinCutoff, final double cumulativeMaxCutoff ) { - final Source< ? > source = state.getSources().get( state.getCurrentSource() ).getSpimSource(); - final int timepoint = state.getCurrentTimepoint(); - if ( !source.isPresent( timepoint ) ) - return; - if ( !UnsignedShortType.class.isInstance( source.getType() ) ) - return; - @SuppressWarnings( "unchecked" ) - final RandomAccessibleInterval< UnsignedShortType > img = ( RandomAccessibleInterval< UnsignedShortType > ) source.getSource( timepoint, source.getNumMipmapLevels() - 1 ); - final long z = ( img.min( 2 ) + img.max( 2 ) + 1 ) / 2; - - final int numBins = 6535; - final Histogram1d< UnsignedShortType > histogram = new Histogram1d<>( Views.iterable( Views.hyperSlice( img, 2, z ) ), new Real1dBinMapper< UnsignedShortType >( 0, 65535, numBins, false ) ); - final DiscreteFrequencyDistribution dfd = histogram.dfd(); - final long[] bin = new long[] { 0 }; - double cumulative = 0; - int i = 0; - for ( ; i < numBins && cumulative < cumulativeMinCutoff; ++i ) - { - bin[ 0 ] = i; - cumulative += dfd.relativeFrequency( bin ); - } - final int min = i * 65535 / numBins; - for ( ; i < numBins && cumulative < cumulativeMaxCutoff; ++i ) + final Object type = source.getType(); + if ( type instanceof UnsignedShortType && source.isPresent( timepoint ) ) { - bin[ 0 ] = i; - cumulative += dfd.relativeFrequency( bin ); + @SuppressWarnings( "unchecked" ) + final RandomAccessibleInterval< UnsignedShortType > img = ( RandomAccessibleInterval< UnsignedShortType > ) source.getSource( timepoint, source.getNumMipmapLevels() - 1 ); + final long z = ( img.min( 2 ) + img.max( 2 ) + 1 ) / 2; + + final int numBins = 6535; + final Histogram1d< ? > histogram = new Histogram1d<>( Views.hyperSlice( img, 2, z ), new Real1dBinMapper<>( 0, 65535, numBins, false ) ); + final DiscreteFrequencyDistribution dfd = histogram.dfd(); + final long[] bin = new long[] { 0 }; + double cumulative = 0; + int i = 0; + for ( ; i < numBins && cumulative < cumulativeMinCutoff; ++i ) + { + bin[ 0 ] = i; + cumulative += dfd.relativeFrequency( bin ); + } + final int min = i * 65535 / numBins; + for ( ; i < numBins && cumulative < cumulativeMaxCutoff; ++i ) + { + bin[ 0 ] = i; + cumulative += dfd.relativeFrequency( bin ); + } + final int max = i * 65535 / numBins; + return new Bounds( min, max ); } - final int max = i * 65535 / numBins; + else if ( type instanceof UnsignedByteType ) + return new Bounds( 0, 255 ); + else + return new Bounds( 0, 65535 ); + } + + @Deprecated + public static AffineTransform3D initTransform( final int viewerWidth, final int viewerHeight, final boolean zoomedIn, final bdv.viewer.state.ViewerState state ) + { + return initTransform( viewerWidth, viewerHeight, zoomedIn, state.getState() ); + } + + @Deprecated + public static void initBrightness( final double cumulativeMinCutoff, final double cumulativeMaxCutoff, final bdv.viewer.state.ViewerState state, final SetupAssignments setupAssignments ) + { + initBrightness( cumulativeMinCutoff, cumulativeMaxCutoff, state.getState(), setupAssignments ); + } + + @Deprecated + public static void initBrightness( final double cumulativeMinCutoff, final double cumulativeMaxCutoff, final ViewerPanel viewer, final SetupAssignments setupAssignments ) + { + initBrightness( cumulativeMinCutoff, cumulativeMaxCutoff, viewer.state().snapshot(), setupAssignments ); + } + + @Deprecated + public static void initBrightness( final double cumulativeMinCutoff, final double cumulativeMaxCutoff, final ViewerState state, final SetupAssignments setupAssignments ) + { + final SourceAndConverter< ? > current = state.getCurrentSource(); + if ( current == null ) + return; + + final Source< ? > source = current.getSpimSource(); + final int timepoint = state.getCurrentTimepoint(); + final Bounds bounds = estimateSourceRange( source, timepoint, cumulativeMinCutoff, cumulativeMaxCutoff ); final MinMaxGroup minmax = setupAssignments.getMinMaxGroups().get( 0 ); - minmax.getMinBoundedValue().setCurrentValue( min ); - minmax.getMaxBoundedValue().setCurrentValue( max ); + minmax.getMinBoundedValue().setCurrentValue( bounds.getMinBound() ); + minmax.getMaxBoundedValue().setCurrentValue( bounds.getMaxBound() ); } } diff --git a/src/main/java/bdv/tools/RecordMaxProjectionDialog.java b/src/main/java/bdv/tools/RecordMaxProjectionDialog.java index 205d8e99bd3b78a6ec8e1ddfee59dfe067357ae7..3ec543035640a7494229a28b3e862ba33cc40d46 100644 --- a/src/main/java/bdv/tools/RecordMaxProjectionDialog.java +++ b/src/main/java/bdv/tools/RecordMaxProjectionDialog.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,6 +28,16 @@ */ package bdv.tools; +import bdv.cache.CacheControl; +import bdv.export.ProgressWriter; +import bdv.util.DelayedPackDialog; +import bdv.util.Prefs; +import bdv.viewer.BasicViewerState; +import bdv.viewer.ViewerPanel; +import bdv.viewer.ViewerState; +import bdv.viewer.overlay.ScaleBarOverlayRenderer; +import bdv.viewer.render.awt.BufferedImageRenderResult; +import bdv.viewer.render.MultiResolutionRenderer; import java.awt.BorderLayout; import java.awt.Frame; import java.awt.Graphics; @@ -40,7 +49,6 @@ import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.io.File; import java.io.IOException; - import javax.imageio.ImageIO; import javax.swing.AbstractAction; import javax.swing.Action; @@ -49,7 +57,6 @@ import javax.swing.BoxLayout; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JComponent; -import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JPanel; @@ -57,29 +64,19 @@ import javax.swing.JSpinner; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SpinnerNumberModel; -import javax.swing.WindowConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; - import net.imglib2.Cursor; import net.imglib2.display.screenimage.awt.ARGBScreenImage; import net.imglib2.img.Img; import net.imglib2.img.array.ArrayImgs; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.type.numeric.ARGBType; -import net.imglib2.ui.OverlayRenderer; -import net.imglib2.ui.PainterThread; -import net.imglib2.ui.RenderTarget; +import bdv.viewer.OverlayRenderer; +import bdv.viewer.render.RenderTarget; import net.imglib2.util.LinAlgHelpers; -import bdv.cache.CacheControl; -import bdv.export.ProgressWriter; -import bdv.util.Prefs; -import bdv.viewer.ViewerPanel; -import bdv.viewer.overlay.ScaleBarOverlayRenderer; -import bdv.viewer.render.MultiResolutionRenderer; -import bdv.viewer.state.ViewerState; -public class RecordMaxProjectionDialog extends JDialog implements OverlayRenderer +public class RecordMaxProjectionDialog extends DelayedPackDialog implements OverlayRenderer { private static final long serialVersionUID = 1L; @@ -107,7 +104,7 @@ public class RecordMaxProjectionDialog extends JDialog implements OverlayRendere { super( owner, "record max projection movie", false ); this.viewer = viewer; - maxTimepoint = viewer.getState().getNumTimepoints() - 1; + maxTimepoint = viewer.state().getNumTimepoints() - 1; this.progressWriter = progressWriter; final JPanel boxes = new JPanel(); @@ -277,7 +274,6 @@ public class RecordMaxProjectionDialog extends JDialog implements OverlayRendere am.put( hideKey, hideAction ); pack(); - setDefaultCloseOperation( WindowConstants.HIDE_ON_CLOSE ); } /** @@ -285,7 +281,7 @@ public class RecordMaxProjectionDialog extends JDialog implements OverlayRendere */ public void recordMovie( final int width, final int height, final int minTimepointIndex, final int maxTimepointIndex, final double stepSize, final int numSteps, final File dir ) throws IOException { - final ViewerState renderState = viewer.getState(); + final ViewerState renderState = new BasicViewerState( viewer.state().snapshot() ); final int canvasW = viewer.getDisplay().getWidth(); final int canvasH = viewer.getDisplay().getHeight(); @@ -314,14 +310,11 @@ public class RecordMaxProjectionDialog extends JDialog implements OverlayRendere final ScaleBarOverlayRenderer scalebar = Prefs.showScaleBarInMovie() ? new ScaleBarOverlayRenderer() : null; - class MyTarget implements RenderTarget + class MyTarget implements RenderTarget< BufferedImageRenderResult > { - final ARGBScreenImage accumulated; + final ARGBScreenImage accumulated = new ARGBScreenImage( width, height ); - public MyTarget() - { - accumulated = new ARGBScreenImage( width, height ); - } + final BufferedImageRenderResult renderResult = new BufferedImageRenderResult(); public void clear() { @@ -330,8 +323,21 @@ public class RecordMaxProjectionDialog extends JDialog implements OverlayRendere } @Override - public BufferedImage setBufferedImage( final BufferedImage bufferedImage ) + public BufferedImageRenderResult getReusableRenderResult() + { + return renderResult; + } + + @Override + public BufferedImageRenderResult createRenderResult() + { + return new BufferedImageRenderResult(); + } + + @Override + public void setRenderResult( final BufferedImageRenderResult renderResult ) { + final BufferedImage bufferedImage = renderResult.getBufferedImage(); final Img< ARGBType > argbs = ArrayImgs.argbs( ( ( DataBufferInt ) bufferedImage.getData().getDataBuffer() ).getData(), width, height ); final Cursor< ARGBType > c = argbs.cursor(); for ( final ARGBType acc : accumulated ) @@ -344,7 +350,6 @@ public class RecordMaxProjectionDialog extends JDialog implements OverlayRendere Math.max( ARGBType.blue( in ), ARGBType.blue( current ) ), Math.max( ARGBType.alpha( in ), ARGBType.alpha( current ) ) ) ); } - return null; } @Override @@ -361,7 +366,7 @@ public class RecordMaxProjectionDialog extends JDialog implements OverlayRendere } final MyTarget target = new MyTarget(); final MultiResolutionRenderer renderer = new MultiResolutionRenderer( - target, new PainterThread( null ), new double[] { 1 }, 0, false, 1, null, false, + target, () -> {}, new double[] { 1 }, 0, 1, null, false, viewer.getOptionValues().getAccumulateProjectorFactory(), new CacheControl.Dummy() ); progressWriter.setProgress( 0 ); for ( int timepoint = minTimepointIndex; timepoint <= maxTimepointIndex; ++timepoint ) diff --git a/src/main/java/bdv/tools/RecordMovieDialog.java b/src/main/java/bdv/tools/RecordMovieDialog.java index 4f6b47aa4a167017c6bdd830d71efb3337101008..cf35bcbd141638108ea1cde6d74bec026dd63084 100644 --- a/src/main/java/bdv/tools/RecordMovieDialog.java +++ b/src/main/java/bdv/tools/RecordMovieDialog.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,6 +28,16 @@ */ package bdv.tools; +import bdv.cache.CacheControl; +import bdv.export.ProgressWriter; +import bdv.util.DelayedPackDialog; +import bdv.util.Prefs; +import bdv.viewer.BasicViewerState; +import bdv.viewer.ViewerPanel; +import bdv.viewer.ViewerState; +import bdv.viewer.overlay.ScaleBarOverlayRenderer; +import bdv.viewer.render.awt.BufferedImageRenderResult; +import bdv.viewer.render.MultiResolutionRenderer; import java.awt.BorderLayout; import java.awt.Frame; import java.awt.Graphics; @@ -39,7 +48,6 @@ import java.awt.event.KeyEvent; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; - import javax.imageio.ImageIO; import javax.swing.AbstractAction; import javax.swing.Action; @@ -48,7 +56,6 @@ import javax.swing.BoxLayout; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JComponent; -import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JPanel; @@ -56,23 +63,13 @@ import javax.swing.JSpinner; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SpinnerNumberModel; -import javax.swing.WindowConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; - -import bdv.cache.CacheControl; -import bdv.export.ProgressWriter; -import bdv.util.Prefs; -import bdv.viewer.ViewerPanel; -import bdv.viewer.overlay.ScaleBarOverlayRenderer; -import bdv.viewer.render.MultiResolutionRenderer; -import bdv.viewer.state.ViewerState; import net.imglib2.realtransform.AffineTransform3D; -import net.imglib2.ui.OverlayRenderer; -import net.imglib2.ui.PainterThread; -import net.imglib2.ui.RenderTarget; +import bdv.viewer.OverlayRenderer; +import bdv.viewer.render.RenderTarget; -public class RecordMovieDialog extends JDialog implements OverlayRenderer +public class RecordMovieDialog extends DelayedPackDialog implements OverlayRenderer { private static final long serialVersionUID = 1L; @@ -96,7 +93,7 @@ public class RecordMovieDialog extends JDialog implements OverlayRenderer { super( owner, "record movie", false ); this.viewer = viewer; - maxTimepoint = viewer.getState().getNumTimepoints() - 1; + maxTimepoint = viewer.state().getNumTimepoints() - 1; this.progressWriter = progressWriter; final JPanel boxes = new JPanel(); @@ -250,12 +247,11 @@ public class RecordMovieDialog extends JDialog implements OverlayRenderer am.put( hideKey, hideAction ); pack(); - setDefaultCloseOperation( WindowConstants.HIDE_ON_CLOSE ); } public void recordMovie( final int width, final int height, final int minTimepointIndex, final int maxTimepointIndex, final File dir ) throws IOException { - final ViewerState renderState = viewer.getState(); + final ViewerState renderState = new BasicViewerState( viewer.state().snapshot() ); final int canvasW = viewer.getDisplay().getWidth(); final int canvasH = viewer.getDisplay().getHeight(); @@ -270,17 +266,26 @@ public class RecordMovieDialog extends JDialog implements OverlayRenderer final ScaleBarOverlayRenderer scalebar = Prefs.showScaleBarInMovie() ? new ScaleBarOverlayRenderer() : null; - class MyTarget implements RenderTarget + class MyTarget implements RenderTarget< BufferedImageRenderResult > { - BufferedImage bi; + final BufferedImageRenderResult renderResult = new BufferedImageRenderResult(); + + @Override + public BufferedImageRenderResult getReusableRenderResult() + { + return renderResult; + } @Override - public BufferedImage setBufferedImage( final BufferedImage bufferedImage ) + public BufferedImageRenderResult createRenderResult() { - bi = bufferedImage; - return null; + return new BufferedImageRenderResult(); } + @Override + public void setRenderResult( final BufferedImageRenderResult renderResult ) + {} + @Override public int getWidth() { @@ -295,7 +300,7 @@ public class RecordMovieDialog extends JDialog implements OverlayRenderer } final MyTarget target = new MyTarget(); final MultiResolutionRenderer renderer = new MultiResolutionRenderer( - target, new PainterThread( null ), new double[] { 1 }, 0, false, 1, null, false, + target, () -> {}, new double[] { 1 }, 0, 1, null, false, viewer.getOptionValues().getAccumulateProjectorFactory(), new CacheControl.Dummy() ); progressWriter.setProgress( 0 ); for ( int timepoint = minTimepointIndex; timepoint <= maxTimepointIndex; ++timepoint ) @@ -304,15 +309,16 @@ public class RecordMovieDialog extends JDialog implements OverlayRenderer renderer.requestRepaint(); renderer.paint( renderState ); + final BufferedImage bi = target.renderResult.getBufferedImage(); if ( Prefs.showScaleBarInMovie() ) { - final Graphics2D g2 = target.bi.createGraphics(); + final Graphics2D g2 = bi.createGraphics(); g2.setClip( 0, 0, width, height ); scalebar.setViewerState( renderState ); scalebar.paint( g2 ); } - ImageIO.write( target.bi, "png", new File( String.format( "%s/img-%03d.png", dir, timepoint ) ) ); + ImageIO.write( bi, "png", new File( String.format( "%s/img-%03d.png", dir, timepoint ) ) ); progressWriter.setProgress( ( double ) (timepoint - minTimepointIndex + 1) / (maxTimepointIndex - minTimepointIndex + 1) ); } } diff --git a/src/main/java/bdv/tools/ToggleDialogAction.java b/src/main/java/bdv/tools/ToggleDialogAction.java index 09fb113a8e263325bfe1d48a711502abf659f9a5..d2ce237e87b783855c30c32c5087be0406494806 100644 --- a/src/main/java/bdv/tools/ToggleDialogAction.java +++ b/src/main/java/bdv/tools/ToggleDialogAction.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/tools/VisibilityAndGroupingDialog.java b/src/main/java/bdv/tools/VisibilityAndGroupingDialog.java index d6326b403bae9a782be8d1b04c8c823099ff34fb..b1da27e26f79a5bd9cfe80c2da58c2047b44cbee 100644 --- a/src/main/java/bdv/tools/VisibilityAndGroupingDialog.java +++ b/src/main/java/bdv/tools/VisibilityAndGroupingDialog.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,16 +28,13 @@ */ package bdv.tools; -import static bdv.viewer.VisibilityAndGrouping.Event.CURRENT_GROUP_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.CURRENT_SOURCE_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.DISPLAY_MODE_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.GROUP_ACTIVITY_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.GROUP_NAME_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.NUM_SOURCES_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.SOURCE_ACTVITY_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.SOURCE_TO_GROUP_ASSIGNMENT_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.VISIBILITY_CHANGED; - +import bdv.util.DelayedPackDialog; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.ViewerState; +import bdv.viewer.VisibilityAndGrouping; +import bdv.viewer.SourceGroup; +import bdv.viewer.ViewerStateChange; +import bdv.viewer.ViewerStateChangeListener; import java.awt.BorderLayout; import java.awt.Frame; import java.awt.GridBagConstraints; @@ -46,11 +42,17 @@ import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Window; import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; import java.awt.event.KeyEvent; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; - +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; @@ -60,22 +62,17 @@ import javax.swing.ButtonGroup; import javax.swing.InputMap; import javax.swing.JCheckBox; import javax.swing.JComponent; -import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; -import javax.swing.WindowConstants; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; -import bdv.viewer.VisibilityAndGrouping; -import bdv.viewer.VisibilityAndGrouping.Event; -import bdv.viewer.state.SourceGroup; - -public class VisibilityAndGroupingDialog extends JDialog +@Deprecated +public class VisibilityAndGroupingDialog extends DelayedPackDialog { private static final long serialVersionUID = 1L; @@ -86,11 +83,15 @@ public class VisibilityAndGroupingDialog extends JDialog private final ModePanel modePanel; public VisibilityAndGroupingDialog( final Frame owner, final VisibilityAndGrouping visibilityAndGrouping ) + { + this( owner, visibilityAndGrouping.getState() ); + } + + public VisibilityAndGroupingDialog( final Frame owner, final ViewerState state ) { super( owner, "visibility and grouping", false ); - visibilityPanel = new VisibilityPanel( visibilityAndGrouping ); - visibilityAndGrouping.addUpdateListener( visibilityPanel ); + visibilityPanel = new VisibilityPanel( state, this::isVisible ); visibilityPanel.setBorder( BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder( 4, 2, 4, 2 ), BorderFactory.createCompoundBorder( @@ -99,8 +100,7 @@ public class VisibilityAndGroupingDialog extends JDialog "visibility" ), BorderFactory.createEmptyBorder( 2, 2, 2, 2 ) ) ) ); - groupingPanel = new GroupingPanel( visibilityAndGrouping ); - visibilityAndGrouping.addUpdateListener( groupingPanel ); + groupingPanel = new GroupingPanel( state, this::isVisible ); groupingPanel.setBorder( BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder( 4, 2, 4, 2 ), BorderFactory.createCompoundBorder( @@ -109,15 +109,14 @@ public class VisibilityAndGroupingDialog extends JDialog "grouping" ), BorderFactory.createEmptyBorder( 2, 2, 2, 2 ) ) ) ); - modePanel = new ModePanel( visibilityAndGrouping ); - visibilityAndGrouping.addUpdateListener( modePanel ); + modePanel = new ModePanel( state, this::isVisible ); final JPanel content = new JPanel(); content.setLayout( new BoxLayout( content, BoxLayout.PAGE_AXIS ) ); content.add( visibilityPanel ); content.add( groupingPanel ); content.add( modePanel ); - getContentPane().add( content, BorderLayout.NORTH ); + add( content, BorderLayout.NORTH ); final ActionMap am = getRootPane().getActionMap(); final InputMap im = getRootPane().getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); @@ -135,8 +134,18 @@ public class VisibilityAndGroupingDialog extends JDialog im.put( KeyStroke.getKeyStroke( KeyEvent.VK_ESCAPE, 0 ), hideKey ); am.put( hideKey, hideAction ); + addComponentListener( new ComponentAdapter() + { + @Override + public void componentShown( final ComponentEvent e ) + { + visibilityPanel.shown(); + groupingPanel.shown(); + modePanel.shown(); + } + } ); + pack(); - setDefaultCloseOperation( WindowConstants.HIDE_ON_CLOSE ); } public void update() @@ -146,174 +155,206 @@ public class VisibilityAndGroupingDialog extends JDialog modePanel.update(); } - public static class VisibilityPanel extends JPanel implements VisibilityAndGrouping.UpdateListener + public static class VisibilityPanel extends JPanel implements ViewerStateChangeListener { private static final long serialVersionUID = 1L; - private final VisibilityAndGrouping visibility; + private final ViewerState state; + + private final Map< SourceAndConverter< ? >, JRadioButton > currentButtonsMap = new HashMap<>(); - private final ArrayList< JRadioButton > currentButtons; + private final ArrayList< Consumer< Set< SourceAndConverter< ? > > > > updateActiveBoxes = new ArrayList<>(); - private final ArrayList< JCheckBox > fusedBoxes; + private final ArrayList< Consumer< Set< SourceAndConverter< ? > > > > updateVisibleBoxes = new ArrayList<>(); - private final ArrayList< JCheckBox > visibleBoxes; + private final BooleanSupplier isVisible; - public VisibilityPanel( final VisibilityAndGrouping visibilityAndGrouping ) + public VisibilityPanel( final ViewerState state, BooleanSupplier isVisible ) { super( new GridBagLayout() ); - this.visibility = visibilityAndGrouping; - currentButtons = new ArrayList<>(); - fusedBoxes = new ArrayList<>(); - visibleBoxes = new ArrayList<>(); - recreateContent(); - update(); + this.state = state; + this.isVisible = isVisible; + state.changeListeners().add( this ); } - protected void recreateContent() + private void shown() { - removeAll(); - currentButtons.clear(); - fusedBoxes.clear(); - visibleBoxes.clear(); + if ( recreateContentPending.getAndSet( false ) ) + recreateContentNow(); + if ( updatePending.getAndSet( false ) ) + updateNow(); + } - final int numSources = visibility.numSources(); - final GridBagConstraints c = new GridBagConstraints(); - c.insets = new Insets( 0, 5, 0, 5 ); + private final AtomicBoolean recreateContentPending = new AtomicBoolean( true ); - // source names - c.gridx = 0; - c.gridy = 0; - add( new JLabel( "source" ), c ); - c.anchor = GridBagConstraints.LINE_END; - c.gridy = GridBagConstraints.RELATIVE; - for ( int i = 0; i < numSources; ++i ) - add( new JLabel( visibility.getSources().get( i ).getSpimSource().getName() ), c ); - - // "current" radio-buttons - c.anchor = GridBagConstraints.CENTER; - c.gridx = 1; - c.gridy = 0; - add( new JLabel( "current" ), c ); - c.gridy = GridBagConstraints.RELATIVE; - final ButtonGroup currentButtonGroup = new ButtonGroup(); - for ( int i = 0; i < numSources; ++i ) + private void recreateContent() + { + if ( isVisible.getAsBoolean() ) { - final JRadioButton b = new JRadioButton(); - final int sourceIndex = i; - b.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( final ActionEvent e ) - { - if ( b.isSelected() ) - visibility.setCurrentSource( sourceIndex ); - } - } ); - currentButtons.add( b ); - currentButtonGroup.add( b ); - add( b, c ); + recreateContentNow(); + recreateContentPending.set( false ); } + else + recreateContentPending.set( true ); + } - // "active in fused" check-boxes - c.gridx = 2; - c.gridy = 0; - add( new JLabel( "active in fused" ), c ); - c.gridy = GridBagConstraints.RELATIVE; - for ( int i = 0; i < numSources; ++i ) + private void recreateContentNow() + { + synchronized ( state ) { - final JCheckBox b = new JCheckBox(); - final int sourceIndex = i; - b.addActionListener( new ActionListener() + removeAll(); + currentButtonsMap.clear(); + updateActiveBoxes.clear(); + updateVisibleBoxes.clear(); + + final List< SourceAndConverter< ? > > sources = state.getSources(); + + final GridBagConstraints c = new GridBagConstraints(); + c.insets = new Insets( 0, 5, 0, 5 ); + + // source names + c.gridx = 0; + c.gridy = 0; + add( new JLabel( "source" ), c ); + c.anchor = GridBagConstraints.LINE_END; + c.gridy = GridBagConstraints.RELATIVE; + for ( final SourceAndConverter< ? > source : sources ) + add( new JLabel( source.getSpimSource().getName() ), c ); + + // "current" radio-buttons + c.anchor = GridBagConstraints.CENTER; + c.gridx = 1; + c.gridy = 0; + add( new JLabel( "current" ), c ); + c.gridy = GridBagConstraints.RELATIVE; + final ButtonGroup currentButtonGroup = new ButtonGroup(); + for ( final SourceAndConverter< ? > source : sources ) { - @Override - public void actionPerformed( final ActionEvent e ) - { - visibility.setSourceActive( sourceIndex, b.isSelected() ); - } - } ); - fusedBoxes.add( b ); - add( b, c ); + final JRadioButton b = new JRadioButton(); + b.addActionListener( e -> { + if ( b.isSelected() ) + state.setCurrentSource( source ); + } ); + currentButtonsMap.put( source, b ); + currentButtonGroup.add( b ); + add( b, c ); + } + + // "active in fused" check-boxes + c.gridx = 2; + c.gridy = 0; + add( new JLabel( "active in fused" ), c ); + c.gridy = GridBagConstraints.RELATIVE; + for ( final SourceAndConverter< ? > source : sources ) + { + final JCheckBox b = new JCheckBox(); + b.addActionListener( e -> state.setSourceActive( source, b.isSelected() ) ); + updateActiveBoxes.add( active -> b.setSelected( active.contains( source ) ) ); + add( b, c ); + } + + // "currently visible" check-boxes + c.gridx = 3; + c.gridy = 0; + add( new JLabel( "visible" ), c ); + c.gridy = GridBagConstraints.RELATIVE; + for ( final SourceAndConverter< ? > source : sources ) + { + final JCheckBox b = new JCheckBox(); + updateVisibleBoxes.add( visible -> b.setSelected( visible.contains( source ) ) ); + b.setEnabled( false ); + add( b, c ); + } + + invalidate(); + final Window frame = SwingUtilities.getWindowAncestor( this ); + if ( frame != null ) + frame.pack(); + + update(); } + } - // "currently visible" check-boxes - c.gridx = 3; - c.gridy = 0; - add( new JLabel( "visible" ), c ); - c.gridy = GridBagConstraints.RELATIVE; - for ( int i = 0; i < numSources; ++i ) + private final AtomicBoolean updatePending = new AtomicBoolean( true ); + + private void update() + { + if ( isVisible.getAsBoolean() ) { - final JCheckBox b = new JCheckBox(); - visibleBoxes.add( b ); - b.setEnabled( false ); - add( b, c ); + updateNow(); + updatePending.set( false ); } - - invalidate(); - final Window frame = SwingUtilities.getWindowAncestor( this ); - if ( frame != null ) - frame.pack(); + else + updatePending.set( true ); } - protected void update() + private void updateNow() { - synchronized ( visibility ) - { - final int numSources = visibility.numSources(); - if ( currentButtons.size() != numSources ) - recreateContent(); + final SourceAndConverter< ? > currentSource = state.getCurrentSource(); + if ( currentSource == null ) + currentButtonsMap.values().forEach( b -> b.setSelected( false ) ); + else + currentButtonsMap.get( currentSource ).setSelected( true ); - if ( numSources > 0 ) - { - currentButtons.get( visibility.getCurrentSource() ).setSelected( true ); - for ( int i = 0; i < numSources; ++i ) - { - fusedBoxes.get( i ).setSelected( visibility.isSourceActive( i ) ); - visibleBoxes.get( i ).setSelected( visibility.isSourceVisible( i ) ); - } - } - } + final Set< SourceAndConverter< ? > > activeSources = state.getActiveSources(); + updateActiveBoxes.forEach( c -> c.accept( activeSources ) ); + + final Set< SourceAndConverter< ? > > visibleSources = state.getVisibleSources(); + updateVisibleBoxes.forEach( c -> c.accept( visibleSources ) ); } @Override - public void visibilityChanged( final Event e ) + public void viewerStateChanged( final ViewerStateChange change ) { - synchronized ( visibility ) - { - if ( currentButtons.size() != visibility.numSources() ) - recreateContent(); + final AtomicBoolean pendingUpdate = new AtomicBoolean(); + final AtomicBoolean pendingRecreate = new AtomicBoolean(); - switch ( e.id ) - { - case CURRENT_SOURCE_CHANGED: - case SOURCE_ACTVITY_CHANGED: - case VISIBILITY_CHANGED: - case NUM_SOURCES_CHANGED: - update(); - break; - } + switch ( change ) + { + case CURRENT_SOURCE_CHANGED: + case SOURCE_ACTIVITY_CHANGED: + case VISIBILITY_CHANGED: + SwingUtilities.invokeLater( this::update ); + break; + case NUM_SOURCES_CHANGED: + SwingUtilities.invokeLater( this::recreateContent ); + break; } } } - public static class ModePanel extends JPanel implements VisibilityAndGrouping.UpdateListener + public static class ModePanel extends JPanel implements ViewerStateChangeListener { private static final long serialVersionUID = 1L; - private final VisibilityAndGrouping visibility; + private final ViewerState state; private JCheckBox groupingBox; private JCheckBox fusedModeBox; - public ModePanel( final VisibilityAndGrouping visibilityAndGrouping ) + private final BooleanSupplier isVisible; + + public ModePanel( final ViewerState state, BooleanSupplier isVisible ) { super( new GridBagLayout() ); - this.visibility = visibilityAndGrouping; - recreateContent(); - update(); + this.state = state; + this.isVisible = isVisible; + state.changeListeners().add( this ); + synchronized ( state ) + { + recreateContent(); + update(); + } + } + + private void shown() + { + if ( updatePending.getAndSet( false ) ) + updateNow(); } - protected void recreateContent() + private void recreateContent() { final GridBagConstraints c = new GridBagConstraints(); c.insets = new Insets( 0, 5, 0, 5 ); @@ -321,12 +362,10 @@ public class VisibilityAndGroupingDialog extends JDialog c.gridwidth = 1; c.anchor = GridBagConstraints.LINE_START; groupingBox = new JCheckBox(); - groupingBox.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( final ActionEvent e ) + groupingBox.addActionListener( e -> { + synchronized ( state ) { - visibility.setGroupingEnabled( groupingBox.isSelected() ); + state.setDisplayMode( state.getDisplayMode().withGrouping( groupingBox.isSelected() ) ); } } ); c.gridx = 0; @@ -336,12 +375,10 @@ public class VisibilityAndGroupingDialog extends JDialog add( new JLabel("enable grouping"), c ); fusedModeBox = new JCheckBox(); - fusedModeBox.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( final ActionEvent e ) + fusedModeBox.addActionListener( e -> { + synchronized ( state ) { - visibility.setFusedEnabled( fusedModeBox.isSelected() ); + state.setDisplayMode( state.getDisplayMode().withFused( fusedModeBox.isSelected() ) ); } } ); c.gridx = 0; @@ -351,280 +388,334 @@ public class VisibilityAndGroupingDialog extends JDialog add( new JLabel("enable fused mode"), c ); } - protected void update() + private final AtomicBoolean updatePending = new AtomicBoolean( true ); + + private void update() { - synchronized ( visibility ) + if ( isVisible.getAsBoolean() ) { - groupingBox.setSelected( visibility.isGroupingEnabled() ); - fusedModeBox.setSelected( visibility.isFusedEnabled() ); + updateNow(); + updatePending.set( false ); } + else + updatePending.set( true ); + } + + private void updateNow() + { + groupingBox.setSelected( state.getDisplayMode().hasGrouping() ); + fusedModeBox.setSelected( state.getDisplayMode().hasFused() ); } @Override - public void visibilityChanged( final Event e ) + public void viewerStateChanged( final ViewerStateChange change ) { - synchronized ( visibility ) - { - switch ( e.id ) - { - case DISPLAY_MODE_CHANGED: - groupingBox.setSelected( visibility.isGroupingEnabled() ); - fusedModeBox.setSelected( visibility.isFusedEnabled() ); - break; - } - } + if ( change == ViewerStateChange.DISPLAY_MODE_CHANGED ) + SwingUtilities.invokeLater( this::update ); } } - public static class GroupingPanel extends JPanel implements VisibilityAndGrouping.UpdateListener + public static class GroupingPanel extends JPanel implements ViewerStateChangeListener { private static final long serialVersionUID = 1L; - private final VisibilityAndGrouping visibility; + private final ArrayList< Runnable > updateNames = new ArrayList<>(); - private final ArrayList< JTextField > nameFields; + private final Map< SourceGroup, JRadioButton > currentButtonsMap = new HashMap<>(); - private final ArrayList< JRadioButton > currentButtons; + private final ArrayList< Consumer< Set< SourceGroup > > > updateActiveBoxes = new ArrayList<>(); - private final ArrayList< JCheckBox > fusedBoxes; + private final ArrayList< Runnable > updateAssignBoxes = new ArrayList<>(); - private final ArrayList< JCheckBox > assignBoxes; + private final ViewerState state; - private int numSources; + private final BooleanSupplier isVisible; - private int numGroups; - - public GroupingPanel( final VisibilityAndGrouping visibilityAndGrouping ) + public GroupingPanel( final ViewerState state, BooleanSupplier isVisible ) { super( new GridBagLayout() ); - this.visibility = visibilityAndGrouping; - nameFields = new ArrayList<>(); - currentButtons = new ArrayList<>(); - fusedBoxes = new ArrayList<>(); - assignBoxes = new ArrayList<>(); - numSources = visibilityAndGrouping.numSources(); - numGroups = visibilityAndGrouping.numGroups(); - recreateContent(); - update(); + this.state = state; + this.isVisible = isVisible; + state.changeListeners().add( this ); } - protected void recreateContent() + private void shown() { - removeAll(); - nameFields.clear(); - currentButtons.clear(); - fusedBoxes.clear(); - assignBoxes.clear(); - - numSources = visibility.numSources(); - numGroups = visibility.numGroups(); + if ( recreateContentPending.getAndSet( false ) ) + recreateContentNow(); + if ( updateCurrentGroupPending.getAndSet( false ) ) + updateCurrentGroupNow(); + if ( updateGroupNamesPending.getAndSet( false ) ) + updateGroupNamesNow(); + if ( updateGroupActivityPending.getAndSet( false ) ) + updateGroupActivityNow(); + if ( updateGroupAssignmentsPending.getAndSet( false ) ) + updateGroupAssignmentsNow(); + } - final GridBagConstraints c = new GridBagConstraints(); - c.insets = new Insets( 0, 5, 0, 5 ); + private final AtomicBoolean recreateContentPending = new AtomicBoolean( true ); - final List< SourceGroup > groups = visibility.getSourceGroups(); + private void recreateContent() + { + if ( isVisible.getAsBoolean() ) + { + recreateContentNow(); + recreateContentPending.set( false ); + } + else + recreateContentPending.set( true ); + } - // source shortcuts - // TODO: shortcut "names" should not be hard-coded here! - c.gridx = 0; - c.gridy = 0; - add( new JLabel( "shortcut" ), c ); - c.anchor = GridBagConstraints.LINE_END; - c.gridy = GridBagConstraints.RELATIVE; - final int nShortcuts = Math.min( numGroups, 10 ); - for ( int i = 0; i < nShortcuts; ++i ) - add( new JLabel( Integer.toString( i == 10 ? 0 : i + 1 ) ), c ); - - // source names - c.gridx = 1; - c.gridy = 0; - c.anchor = GridBagConstraints.CENTER; - add( new JLabel( "group name" ), c ); - c.anchor = GridBagConstraints.LINE_END; - c.gridy = GridBagConstraints.RELATIVE; - for ( int g = 0; g < numGroups; ++g ) + private void recreateContentNow() + { + synchronized ( state ) { - final JTextField tf = new JTextField( groups.get( g ).getName(), 10 ); - final int groupIndex = g; - tf.getDocument().addDocumentListener( new DocumentListener() + removeAll(); + updateNames.clear(); + currentButtonsMap.clear(); + updateActiveBoxes.clear(); + updateAssignBoxes.clear(); + + final GridBagConstraints c = new GridBagConstraints(); + c.insets = new Insets( 0, 5, 0, 5 ); + + final List< SourceAndConverter< ? > > sources = state.getSources(); + final List< SourceGroup > groups = state.getGroups(); + + // source shortcuts + // TODO: shortcut "names" should not be hard-coded here! + c.gridx = 0; + c.gridy = 0; + add( new JLabel( "shortcut" ), c ); + c.anchor = GridBagConstraints.LINE_END; + c.gridy = GridBagConstraints.RELATIVE; + final int nShortcuts = Math.min( groups.size(), 10 ); + for ( int i = 0; i < nShortcuts; ++i ) + add( new JLabel( Integer.toString( i == 10 ? 0 : i + 1 ) ), c ); + + // source names + c.gridx = 1; + c.gridy = 0; + c.anchor = GridBagConstraints.CENTER; + add( new JLabel( "group name" ), c ); + c.anchor = GridBagConstraints.LINE_END; + c.gridy = GridBagConstraints.RELATIVE; + for ( final SourceGroup group : groups ) { - private void doit() + final JTextField tf = new JTextField( state.getGroupName( group ), 10 ); + tf.getDocument().addDocumentListener( new DocumentListener() { - visibility.setGroupName( groupIndex, tf.getText() ); - } + private void doit() + { + state.setGroupName( group, tf.getText() ); + } - @Override - public void removeUpdate( final DocumentEvent e ) - { - doit(); - } + @Override + public void removeUpdate( final DocumentEvent e ) + { + doit(); + } - @Override - public void insertUpdate( final DocumentEvent e ) - { - doit(); - } + @Override + public void insertUpdate( final DocumentEvent e ) + { + doit(); + } - @Override - public void changedUpdate( final DocumentEvent e ) - { - doit(); - } - } ); - nameFields.add( tf ); - add( tf, c ); - } + @Override + public void changedUpdate( final DocumentEvent e ) + { + doit(); + } + } ); + updateNames.add( () -> { + final String name = state.getGroupName( group ); + if ( !tf.getText().equals( name ) ) + { + tf.setText( name ); + } + } ); + add( tf, c ); + } - // "current" radio-buttons - c.anchor = GridBagConstraints.CENTER; - c.gridx = 2; - c.gridy = 0; - add( new JLabel( "current" ), c ); - c.gridy = GridBagConstraints.RELATIVE; - final ButtonGroup currentButtonGroup = new ButtonGroup(); - for ( int g = 0; g < numGroups; ++g ) - { - final JRadioButton b = new JRadioButton(); - final int groupIndex = g; - b.addActionListener( new ActionListener() + // "current" radio-buttons + c.anchor = GridBagConstraints.CENTER; + c.gridx = 2; + c.gridy = 0; + add( new JLabel( "current" ), c ); + c.gridy = GridBagConstraints.RELATIVE; + final ButtonGroup currentButtonGroup = new ButtonGroup(); + for ( final SourceGroup group : groups ) { - @Override - public void actionPerformed( final ActionEvent e ) - { + final JRadioButton b = new JRadioButton(); + b.addActionListener( e -> { if ( b.isSelected() ) - visibility.setCurrentGroup( groupIndex ); - } - } ); - currentButtons.add( b ); - currentButtonGroup.add( b ); - add( b, c ); - } + state.setCurrentGroup( group ); + } ); + currentButtonsMap.put( group, b ); + currentButtonGroup.add( b ); + add( b, c ); + } - // "active in fused" check-boxes - c.gridx = 3; - c.gridy = 0; - c.anchor = GridBagConstraints.CENTER; - add( new JLabel( "active in fused" ), c ); - c.gridy = GridBagConstraints.RELATIVE; - for ( int g = 0; g < numGroups; ++g ) - { - final JCheckBox b = new JCheckBox(); - final int groupIndex = g; - b.addActionListener( new ActionListener() + // "active in fused" check-boxes + c.gridx = 3; + c.gridy = 0; + c.anchor = GridBagConstraints.CENTER; + add( new JLabel( "active in fused" ), c ); + c.gridy = GridBagConstraints.RELATIVE; + for ( final SourceGroup group : groups ) { - @Override - public void actionPerformed( final ActionEvent e ) - { - visibility.setGroupActive( groupIndex, b.isSelected() ); - } - } ); - fusedBoxes.add( b ); - add( b, c ); - } + final JCheckBox b = new JCheckBox(); + b.addActionListener( e -> state.setGroupActive( group, b.isSelected() ) ); + updateActiveBoxes.add( active -> b.setSelected( active.contains( group ) ) ); + add( b, c ); + } - // setup-to-group assignments - c.gridx = 4; - c.gridy = 0; - c.gridwidth = numSources; - c.anchor = GridBagConstraints.CENTER; - add( new JLabel( "assigned sources" ), c ); - c.gridwidth = 1; - c.anchor = GridBagConstraints.LINE_END; - for ( int s = 0; s < numSources; ++s ) - { - final int sourceIndex = s; - c.gridx = sourceIndex + 4; - for ( int g = 0; g < numGroups; ++g ) + // setup-to-group assignments + c.gridx = 4; + c.gridy = 0; + c.gridwidth = sources.size(); + c.anchor = GridBagConstraints.CENTER; + add( new JLabel( "assigned sources" ), c ); + c.gridwidth = 1; + c.anchor = GridBagConstraints.LINE_END; + for ( final SourceAndConverter< ? > source : sources ) { - final int groupIndex = g; - c.gridy = g + 1; - final JCheckBox b = new JCheckBox(); - b.addActionListener( new ActionListener() + c.gridy = 1; + for ( final SourceGroup group : groups ) { - @Override - public void actionPerformed( final ActionEvent e ) - { + final JCheckBox b = new JCheckBox(); + b.addActionListener( e -> { if ( b.isSelected() ) - visibility.addSourceToGroup( sourceIndex, groupIndex ); + state.addSourceToGroup( source, group ); else - visibility.removeSourceFromGroup( sourceIndex, groupIndex ); - } - } ); - assignBoxes.add( b ); - add( b, c ); + state.removeSourceFromGroup( source, group ); + } ); + updateAssignBoxes.add( () -> { + b.setSelected( state.getSourcesInGroup( group ).contains( source ) ); + } ); + add( b, c ); + c.gridy++; + } + c.gridx++; } + + invalidate(); + final Window frame = SwingUtilities.getWindowAncestor( this ); + if ( frame != null ) + frame.pack(); + + update(); + } + } + + private void update() + { + updateGroupNames(); + updateCurrentGroup(); + updateGroupActivity(); + updateGroupAssignments(); + } + + private final AtomicBoolean updateGroupNamesPending = new AtomicBoolean( true ); + + private void updateGroupNames() + { + if ( isVisible.getAsBoolean() ) + { + updateGroupNamesNow(); + updateGroupNamesPending.set( false ); } + else + updateGroupNamesPending.set( true ); + } - invalidate(); - final Window frame = SwingUtilities.getWindowAncestor( this ); - if ( frame != null ) - frame.pack(); + private void updateGroupNamesNow() + { + updateNames.forEach( Runnable::run ); } - protected void update() + private final AtomicBoolean updateGroupAssignmentsPending = new AtomicBoolean( true ); + + private void updateGroupAssignments() { - synchronized ( visibility ) + if ( isVisible.getAsBoolean() ) { - if ( visibility.numSources() != numSources || visibility.numGroups() != numGroups ) - recreateContent(); + updateGroupAssignmentsNow(); + updateGroupAssignmentsPending.set( false ); + } + else + updateGroupAssignmentsPending.set( true ); + } - if ( numGroups > 0 ) - currentButtons.get( visibility.getCurrentGroup() ).setSelected( true ); + private void updateGroupAssignmentsNow() + { + updateAssignBoxes.forEach( Runnable::run ); + } - for ( int g = 0; g < numGroups; ++g ) - fusedBoxes.get( g ).setSelected( visibility.isGroupActive( g ) ); + private final AtomicBoolean updateGroupActivityPending = new AtomicBoolean( true ); - updateGroupNames(); - updateGroupAssignments(); + private void updateGroupActivity() + { + if ( isVisible.getAsBoolean() ) + { + updateGroupActivityNow(); + updateGroupActivityPending.set( false ); } + else + updateGroupActivityPending.set( true ); + } + + private void updateGroupActivityNow() + { + final Set< SourceGroup > activeGroups = state.getActiveGroups(); + updateActiveBoxes.forEach( c -> c.accept( activeGroups ) ); } - protected void updateGroupNames() + private final AtomicBoolean updateCurrentGroupPending = new AtomicBoolean( true ); + + private void updateCurrentGroup() { - final List< SourceGroup > groups = visibility.getSourceGroups(); - for ( int i = 0; i < numGroups; ++i ) + if ( isVisible.getAsBoolean() ) { - final JTextField tf = nameFields.get( i ); - final String name = groups.get( i ).getName(); - if ( ! tf.getText().equals( name ) ) - tf.setText( name ); + updateCurrentGroupNow(); + updateCurrentGroupPending.set( false ); } + else + updateCurrentGroupPending.set( true ); } - protected void updateGroupAssignments() + private void updateCurrentGroupNow() { - final List< SourceGroup > groups = visibility.getSourceGroups(); - for ( int s = 0; s < numSources; ++s ) - for ( int g = 0; g < numGroups; ++g ) - assignBoxes.get( s * numGroups + g ).setSelected( groups.get( g ).getSourceIds().contains( s ) ); + final SourceGroup currentGroup = state.getCurrentGroup(); + if ( currentGroup == null ) + currentButtonsMap.values().forEach( b -> b.setSelected( false ) ); + else + currentButtonsMap.get( currentGroup ).setSelected( true ); } @Override - public void visibilityChanged( final Event e ) + public void viewerStateChanged( final ViewerStateChange change ) { - synchronized ( visibility ) + switch( change ) { - if ( visibility.numSources() != numSources || visibility.numGroups() != numGroups ) - update(); - - switch ( e.id ) - { - case CURRENT_GROUP_CHANGED: - currentButtons.get( visibility.getCurrentGroup() ).setSelected( true ); - break; - case GROUP_ACTIVITY_CHANGED: - for ( int g = 0; g < numGroups; ++g ) - fusedBoxes.get( g ).setSelected( visibility.isGroupActive( g ) ); - break; - case SOURCE_TO_GROUP_ASSIGNMENT_CHANGED: - updateGroupAssignments(); - break; - case GROUP_NAME_CHANGED: - updateGroupNames(); - break; - } + case CURRENT_GROUP_CHANGED: + SwingUtilities.invokeLater( this::updateCurrentGroup ); + break; + case GROUP_ACTIVITY_CHANGED: + SwingUtilities.invokeLater( this::updateGroupActivity ); + break; + case SOURCE_TO_GROUP_ASSIGNMENT_CHANGED: + SwingUtilities.invokeLater( this::updateGroupAssignments ); + break; + case GROUP_NAME_CHANGED: + SwingUtilities.invokeLater( this::updateGroupNames ); + break; + case NUM_GROUPS_CHANGED: + case NUM_SOURCES_CHANGED: + SwingUtilities.invokeLater( this::recreateContent ); + break; } } } - } diff --git a/src/main/java/bdv/tools/bookmarks/BookmarkTextOverlayAnimator.java b/src/main/java/bdv/tools/bookmarks/BookmarkTextOverlayAnimator.java index 53824318d77c605cafc2414cd9c0847c07c97537..539290a733ac0536ad87b7303dc15899c6875123 100644 --- a/src/main/java/bdv/tools/bookmarks/BookmarkTextOverlayAnimator.java +++ b/src/main/java/bdv/tools/bookmarks/BookmarkTextOverlayAnimator.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -44,7 +43,7 @@ import bdv.viewer.animate.OverlayAnimator; * Draw one line of text in the center or bottom right of the display. Text is * fading in and out. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class BookmarkTextOverlayAnimator implements OverlayAnimator { diff --git a/src/main/java/bdv/tools/bookmarks/Bookmarks.java b/src/main/java/bdv/tools/bookmarks/Bookmarks.java index 45e197354479544ba6b15dbcbf14024df84bbfb8..13da5c87fb5c1b78bbe69895f0eddaf2a988d527 100644 --- a/src/main/java/bdv/tools/bookmarks/Bookmarks.java +++ b/src/main/java/bdv/tools/bookmarks/Bookmarks.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/tools/bookmarks/BookmarksEditor.java b/src/main/java/bdv/tools/bookmarks/BookmarksEditor.java index 577ecec07ca9f2bd690b4065778a9db7cb9ad31e..59a8b5539ce29483476845686b299f50998694a1 100644 --- a/src/main/java/bdv/tools/bookmarks/BookmarksEditor.java +++ b/src/main/java/bdv/tools/bookmarks/BookmarksEditor.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -117,7 +116,7 @@ public class BookmarksEditor case SET: { final AffineTransform3D t = new AffineTransform3D(); - viewer.getState().getViewerTransform( t ); + viewer.state().getViewerTransform( t ); final double cX = viewer.getDisplay().getWidth() / 2.0; final double cY = viewer.getDisplay().getHeight() / 2.0; t.set( t.get( 0, 3 ) - cX, 0, 3 ); @@ -133,7 +132,7 @@ public class BookmarksEditor if ( t != null ) { final AffineTransform3D c = new AffineTransform3D(); - viewer.getState().getViewerTransform( c ); + viewer.state().getViewerTransform( c ); final double cX = viewer.getDisplay().getWidth() / 2.0; final double cY = viewer.getDisplay().getHeight() / 2.0; c.set( c.get( 0, 3 ) - cX, 0, 3 ); @@ -149,7 +148,7 @@ public class BookmarksEditor if ( t != null ) { final AffineTransform3D c = new AffineTransform3D(); - viewer.getState().getViewerTransform( c ); + viewer.state().getViewerTransform( c ); final Point p = new Point( 2 ); viewer.getMouseCoordinates( p ); final double[] qTarget = new double[ 4 ]; diff --git a/src/main/java/bdv/tools/boundingbox/AbstractTransformedBoxModel.java b/src/main/java/bdv/tools/boundingbox/AbstractTransformedBoxModel.java index 9bdbdb19902999843f3bba70a9970bffbb42a14f..e1e807c3c29a6b47a2a635ef8cf7a06f34a78909 100644 --- a/src/main/java/bdv/tools/boundingbox/AbstractTransformedBoxModel.java +++ b/src/main/java/bdv/tools/boundingbox/AbstractTransformedBoxModel.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE diff --git a/src/main/java/bdv/tools/boundingbox/AbstractTransformedBoxSelectionDialog.java b/src/main/java/bdv/tools/boundingbox/AbstractTransformedBoxSelectionDialog.java index 0f782d7381996bee1cf815a0e38844cfccef65ff..4840454f73f954de600e525a076db04f7d8e08c7 100644 --- a/src/main/java/bdv/tools/boundingbox/AbstractTransformedBoxSelectionDialog.java +++ b/src/main/java/bdv/tools/boundingbox/AbstractTransformedBoxSelectionDialog.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -44,6 +43,7 @@ import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JPanel; +import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import bdv.tools.boundingbox.BoxSelectionOptions.TimepointSelection; @@ -118,7 +118,7 @@ public abstract class AbstractTransformedBoxSelectionDialog< R > extends JDialog synchronized ( monitor ) { result = null; - setVisible( true ); + SwingUtilities.invokeLater( () -> setVisible( true ) ); while( true ) { try @@ -160,7 +160,7 @@ public abstract class AbstractTransformedBoxSelectionDialog< R > extends JDialog mode = options.values.getTimepointSelection(); if ( mode == SINGLE || mode == RANGE ) { - final int tmaxViewer = viewer.getState().getNumTimepoints() - 1; + final int tmaxViewer = viewer.state().getNumTimepoints() - 1; final int tmin = Math.max( 0, Math.min( tmaxViewer, options.values.getRangeMinTimepoint() ) ); final int tmax = Math.max( tmin, Math.min( tmaxViewer, options.values.getRangeMaxTimepoint() ) );; diff --git a/src/main/java/bdv/tools/boundingbox/BoundingBoxDialog.java b/src/main/java/bdv/tools/boundingbox/BoundingBoxDialog.java index 3243cd5cdcc20041f72ddc0361ab9abaf8b689bd..52002dbe20e47434401d8ce7812f984b5524ddc9 100644 --- a/src/main/java/bdv/tools/boundingbox/BoundingBoxDialog.java +++ b/src/main/java/bdv/tools/boundingbox/BoundingBoxDialog.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -52,6 +51,7 @@ import net.imglib2.type.numeric.ARGBType; import net.imglib2.type.numeric.integer.UnsignedShortType; import bdv.tools.boundingbox.BoxSelectionPanel.Box; +import bdv.tools.brightness.ConverterSetup.SetupChangeListener; import bdv.tools.brightness.RealARGBColorConverterSetup; import bdv.tools.brightness.SetupAssignments; import bdv.tools.transformation.TransformedSource; @@ -134,7 +134,8 @@ public class BoundingBoxDialog extends JDialog // create a ConverterSetup (can be used by the brightness dialog to adjust the converter settings) boxConverterSetup = new RealARGBColorConverterSetup( boxSetupId, converter ); - boxConverterSetup.setViewer( viewer ); + final SetupChangeListener requestRepaint = s -> viewer.requestRepaint(); + boxConverterSetup.setupChangeListeners().add( requestRepaint ); // create a SourceAndConverter (can be added to the viewer for display) final TransformedSource< UnsignedShortType > ts = new TransformedSource<>( boxSource ); @@ -186,7 +187,7 @@ public class BoundingBoxDialog extends JDialog { viewer.addSource( boxSourceAndConverter ); setupAssignments.addSetup( boxConverterSetup ); - boxConverterSetup.setViewer( viewer ); + boxConverterSetup.setupChangeListeners().add( requestRepaint ); final int bbSourceIndex = viewer.getState().numSources() - 1; final VisibilityAndGrouping vg = viewer.getVisibilityAndGrouping(); @@ -201,8 +202,8 @@ public class BoundingBoxDialog extends JDialog } if ( showBoxOverlay ) { - viewer.getDisplay().addOverlayRenderer( boxOverlay ); - viewer.addRenderTransformListener( boxOverlay ); + viewer.getDisplay().overlays().add( boxOverlay ); + viewer.renderTransformListeners().add( boxOverlay ); } } @@ -213,10 +214,11 @@ public class BoundingBoxDialog extends JDialog { viewer.removeSource( boxSourceAndConverter.getSpimSource() ); setupAssignments.removeSetup( boxConverterSetup ); + boxConverterSetup.setupChangeListeners().remove( requestRepaint ); } if ( showBoxOverlay ) { - viewer.getDisplay().removeOverlayRenderer( boxOverlay ); + viewer.getDisplay().overlays().remove( boxOverlay ); viewer.removeTransformListener( boxOverlay ); } } diff --git a/src/main/java/bdv/tools/boundingbox/BoundingBoxUtil.java b/src/main/java/bdv/tools/boundingbox/BoundingBoxUtil.java index 62b60894b6f9aac8f5bba98dba98d31dfddb5872..8f808dce7f371a6311053d451e8f2284db8164da 100644 --- a/src/main/java/bdv/tools/boundingbox/BoundingBoxUtil.java +++ b/src/main/java/bdv/tools/boundingbox/BoundingBoxUtil.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,6 +28,7 @@ */ package bdv.tools.boundingbox; +import bdv.viewer.SourceAndConverter; import java.util.ArrayList; import java.util.Collection; @@ -40,7 +40,7 @@ import net.imglib2.RealPoint; import net.imglib2.realtransform.AffineTransform3D; import bdv.viewer.Source; import bdv.viewer.state.SourceState; -import bdv.viewer.state.ViewerState; +import bdv.viewer.ViewerState; public class BoundingBoxUtil { @@ -52,7 +52,7 @@ public class BoundingBoxUtil public static Interval getSourcesBoundingBox( final ViewerState state, final int minTimepointIndex, final int maxTimepointIndex ) { final ArrayList< Source< ? > > sources = new ArrayList<>(); - for ( final SourceState< ? > source : state.getSources() ) + for ( final SourceAndConverter< ? > source : state.getSources() ) sources.add( source.getSpimSource() ); return getSourcesBoundingBox( sources, minTimepointIndex, maxTimepointIndex ); } @@ -107,7 +107,7 @@ public class BoundingBoxUtil public static RealInterval getSourcesBoundingBoxReal( final ViewerState state, final int minTimepointIndex, final int maxTimepointIndex ) { final ArrayList< Source< ? > > sources = new ArrayList<>(); - for ( final SourceState< ? > source : state.getSources() ) + for ( final SourceAndConverter< ? > source : state.getSources() ) sources.add( source.getSpimSource() ); return getSourcesBoundingBoxReal( sources, minTimepointIndex, maxTimepointIndex ); } @@ -185,4 +185,28 @@ public class BoundingBoxUtil } return new FinalRealInterval( bbMin, bbMax ); } + + @Deprecated + public static Interval getSourcesBoundingBox( final bdv.viewer.state.ViewerState state ) + { + return getSourcesBoundingBox( state.getState() ); + } + + @Deprecated + public static Interval getSourcesBoundingBox( final bdv.viewer.state.ViewerState state, final int minTimepointIndex, final int maxTimepointIndex ) + { + return getSourcesBoundingBox( state.getState(), minTimepointIndex, maxTimepointIndex ); + } + + @Deprecated + public static RealInterval getSourcesBoundingBoxReal( final bdv.viewer.state.ViewerState state ) + { + return getSourcesBoundingBoxReal( state.getState() ); + } + + @Deprecated + public static RealInterval getSourcesBoundingBoxReal( final bdv.viewer.state.ViewerState state, final int minTimepointIndex, final int maxTimepointIndex ) + { + return getSourcesBoundingBoxReal( state.getState(), minTimepointIndex, maxTimepointIndex ); + } } diff --git a/src/main/java/bdv/tools/boundingbox/BoxDisplayModePanel.java b/src/main/java/bdv/tools/boundingbox/BoxDisplayModePanel.java index c912169dc066a5e46ac673ba1226f09e0b489cc0..f8715e21d969a0602915794b3d31e3b3ff207733 100644 --- a/src/main/java/bdv/tools/boundingbox/BoxDisplayModePanel.java +++ b/src/main/java/bdv/tools/boundingbox/BoxDisplayModePanel.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE diff --git a/src/main/java/bdv/tools/boundingbox/BoxRealRandomAccessible.java b/src/main/java/bdv/tools/boundingbox/BoxRealRandomAccessible.java index 9c248b4cc3bed02ff90dd21b409ec23c57a56508..f1aa7aaa355f747d58af0f5d98bc791e0873d4fe 100644 --- a/src/main/java/bdv/tools/boundingbox/BoxRealRandomAccessible.java +++ b/src/main/java/bdv/tools/boundingbox/BoxRealRandomAccessible.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/tools/boundingbox/BoxSelectionOptions.java b/src/main/java/bdv/tools/boundingbox/BoxSelectionOptions.java index b6486543091181ea9f4a86b82d9a5b1c8f00e815..0dc0edc20f43f872be9f61f22f3573a97288856f 100644 --- a/src/main/java/bdv/tools/boundingbox/BoxSelectionOptions.java +++ b/src/main/java/bdv/tools/boundingbox/BoxSelectionOptions.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE diff --git a/src/main/java/bdv/tools/boundingbox/BoxSelectionPanel.java b/src/main/java/bdv/tools/boundingbox/BoxSelectionPanel.java index f45628c0b85ca68d068cc42494c704773fbb5896..ff70e0e411830609e7c86b238e24587a67506155 100644 --- a/src/main/java/bdv/tools/boundingbox/BoxSelectionPanel.java +++ b/src/main/java/bdv/tools/boundingbox/BoxSelectionPanel.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE diff --git a/src/main/java/bdv/tools/boundingbox/DragBoxCornerBehaviour.java b/src/main/java/bdv/tools/boundingbox/DragBoxCornerBehaviour.java index 713d2ab3b0e5fedd5bdd447a3bf3788a2f8aad80..edba768256acdcd08c755b7da4fd1a131dfae456 100644 --- a/src/main/java/bdv/tools/boundingbox/DragBoxCornerBehaviour.java +++ b/src/main/java/bdv/tools/boundingbox/DragBoxCornerBehaviour.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE diff --git a/src/main/java/bdv/tools/boundingbox/IntervalCorners.java b/src/main/java/bdv/tools/boundingbox/IntervalCorners.java index 269c8aeb92c1a09552fe9bff7c46f1c13d528e8c..5a2990078aea578d82cfc3846650e252d5cc499b 100644 --- a/src/main/java/bdv/tools/boundingbox/IntervalCorners.java +++ b/src/main/java/bdv/tools/boundingbox/IntervalCorners.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE diff --git a/src/main/java/bdv/tools/boundingbox/RealBoxSelectionPanel.java b/src/main/java/bdv/tools/boundingbox/RealBoxSelectionPanel.java index a3b298dba7f6d1f7f828cc2997934f3b06de630a..9c5bce63024ef74e73bcbc88a5c4ff1721313eba 100644 --- a/src/main/java/bdv/tools/boundingbox/RealBoxSelectionPanel.java +++ b/src/main/java/bdv/tools/boundingbox/RealBoxSelectionPanel.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE diff --git a/src/main/java/bdv/tools/boundingbox/RenderBoxHelper.java b/src/main/java/bdv/tools/boundingbox/RenderBoxHelper.java index 4871c60e8b1118da7873378cc5a0e8084536ed8e..8fb8aa2eacad57537be9c45439a25e16c860be13 100644 --- a/src/main/java/bdv/tools/boundingbox/RenderBoxHelper.java +++ b/src/main/java/bdv/tools/boundingbox/RenderBoxHelper.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE diff --git a/src/main/java/bdv/tools/boundingbox/TransformedBox.java b/src/main/java/bdv/tools/boundingbox/TransformedBox.java index 75e1a145166e4705ef807061c39b3c5e4e505557..a7dbdf503b4cbc92007b3229014d99ba60883c22 100644 --- a/src/main/java/bdv/tools/boundingbox/TransformedBox.java +++ b/src/main/java/bdv/tools/boundingbox/TransformedBox.java @@ -1,3 +1,31 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package bdv.tools.boundingbox; import net.imglib2.RealInterval; diff --git a/src/main/java/bdv/tools/boundingbox/TransformedBoxEditor.java b/src/main/java/bdv/tools/boundingbox/TransformedBoxEditor.java index 6380939cd2e205782c9b978239097b56ad335d85..0dcc495436eb7f00bbde294d0ff1e84565ac02dc 100644 --- a/src/main/java/bdv/tools/boundingbox/TransformedBoxEditor.java +++ b/src/main/java/bdv/tools/boundingbox/TransformedBoxEditor.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -31,6 +30,7 @@ package bdv.tools.boundingbox; import static bdv.tools.boundingbox.TransformedBoxOverlay.BoxDisplayMode.FULL; +import bdv.viewer.ConverterSetups; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -45,7 +45,6 @@ import org.scijava.ui.behaviour.util.Behaviours; import org.scijava.ui.behaviour.util.TriggerBehaviourBindings; import bdv.tools.boundingbox.TransformedBoxOverlay.BoxDisplayMode; -import bdv.tools.brightness.SetupAssignments; import bdv.viewer.ViewerPanel; /** @@ -94,17 +93,19 @@ public class TransformedBoxEditor public TransformedBoxEditor( final InputTriggerConfig keyconf, final ViewerPanel viewer, - final SetupAssignments setupAssignments, + final ConverterSetups converterSetups, + final int setupId, final TriggerBehaviourBindings triggerbindings, final AbstractTransformedBoxModel model ) { - this( keyconf, viewer, setupAssignments, triggerbindings, model, "selection", BoxSourceType.PLACEHOLDER ); + this( keyconf, viewer, converterSetups, setupId, triggerbindings, model, "selection", BoxSourceType.PLACEHOLDER ); } public TransformedBoxEditor( final InputTriggerConfig keyconf, final ViewerPanel viewer, - final SetupAssignments setupAssignments, + final ConverterSetups converterSetups, + final int setupId, final TriggerBehaviourBindings triggerbindings, final AbstractTransformedBoxModel model, final String boxSourceName, @@ -129,7 +130,7 @@ public class TransformedBoxEditor switch ( boxSourceType ) { case PLACEHOLDER: - boxSource = new TransformedBoxOverlaySource( boxSourceName, boxOverlay, model, viewer, setupAssignments ); + boxSource = new TransformedBoxOverlaySource( boxSourceName, boxOverlay, model, viewer, converterSetups, setupId ); break; case NONE: default: @@ -154,8 +155,8 @@ public class TransformedBoxEditor public void install() { - viewer.getDisplay().addOverlayRenderer( boxOverlay ); - viewer.addRenderTransformListener( boxOverlay ); + viewer.getDisplay().overlays().add( boxOverlay ); + viewer.renderTransformListeners().add( boxOverlay ); viewer.getDisplay().addHandler( boxOverlay.getCornerHighlighter() ); refreshBlockMap(); @@ -167,7 +168,7 @@ public class TransformedBoxEditor public void uninstall() { - viewer.getDisplay().removeOverlayRenderer( boxOverlay ); + viewer.getDisplay().overlays().remove( boxOverlay ); viewer.removeTransformListener( boxOverlay ); viewer.getDisplay().removeHandler( boxOverlay.getCornerHighlighter() ); diff --git a/src/main/java/bdv/tools/boundingbox/TransformedBoxModel.java b/src/main/java/bdv/tools/boundingbox/TransformedBoxModel.java index c1e110b820fed0bfd3fa72a14085e8749d8f6bab..9043f072c30e9119cf3ee6460e29d68401c69fa9 100644 --- a/src/main/java/bdv/tools/boundingbox/TransformedBoxModel.java +++ b/src/main/java/bdv/tools/boundingbox/TransformedBoxModel.java @@ -1,3 +1,31 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package bdv.tools.boundingbox; import bdv.util.ModifiableInterval; diff --git a/src/main/java/bdv/tools/boundingbox/TransformedBoxOverlay.java b/src/main/java/bdv/tools/boundingbox/TransformedBoxOverlay.java index bae13dcd90043d8073231dd3714c6aeaebfd4913..7439703cad1b91241acfb288388da9e8d0c20e4c 100644 --- a/src/main/java/bdv/tools/boundingbox/TransformedBoxOverlay.java +++ b/src/main/java/bdv/tools/boundingbox/TransformedBoxOverlay.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -46,8 +45,8 @@ import java.awt.geom.GeneralPath; import net.imglib2.Interval; import net.imglib2.RealInterval; import net.imglib2.realtransform.AffineTransform3D; -import net.imglib2.ui.OverlayRenderer; -import net.imglib2.ui.TransformListener; +import bdv.viewer.OverlayRenderer; +import bdv.viewer.TransformListener; import org.scijava.listeners.ChangeListener; import org.scijava.listeners.ListenableVar; diff --git a/src/main/java/bdv/tools/boundingbox/TransformedBoxOverlaySource.java b/src/main/java/bdv/tools/boundingbox/TransformedBoxOverlaySource.java index 699fbe783aa320ceb60923164eb96cb21b9ad5dd..a5209e9cbc5478a686a87cb58bd9ca30da558313 100644 --- a/src/main/java/bdv/tools/boundingbox/TransformedBoxOverlaySource.java +++ b/src/main/java/bdv/tools/boundingbox/TransformedBoxOverlaySource.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -29,22 +28,19 @@ */ package bdv.tools.boundingbox; -import bdv.tools.brightness.SetupAssignments; +import bdv.util.Bounds; import bdv.util.PlaceHolderConverterSetup; +import bdv.viewer.ConverterSetups; import bdv.viewer.DisplayMode; -import bdv.viewer.Source; import bdv.viewer.SourceAndConverter; import bdv.viewer.ViewerPanel; -import bdv.viewer.VisibilityAndGrouping; -import bdv.viewer.VisibilityAndGrouping.Event; -import bdv.viewer.state.SourceGroup; -import bdv.viewer.state.SourceState; -import bdv.viewer.state.ViewerState; +import bdv.viewer.ViewerState; +import bdv.viewer.ViewerStateChange; +import bdv.viewer.ViewerStateChangeListener; import java.awt.Color; import net.imglib2.type.numeric.ARGBType; -import static bdv.viewer.VisibilityAndGrouping.Event.SOURCE_ACTVITY_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.VISIBILITY_CHANGED; +import static bdv.viewer.ViewerStateChange.VISIBILITY_CHANGED; /** * A BDV source (and converter etc) representing a {@code TransformedBox}. @@ -61,13 +57,11 @@ public class TransformedBoxOverlaySource private final PlaceHolderConverterSetup boxConverterSetup; - private final Source< Void > boxSource; - private final SourceAndConverter< Void > boxSourceAndConverter; private final ViewerPanel viewer; - private final SetupAssignments setupAssignments; + private final ConverterSetups setups; private boolean isVisible; @@ -76,85 +70,61 @@ public class TransformedBoxOverlaySource final TransformedBoxOverlay boxOverlay, final TransformedBox bbSource, final ViewerPanel viewer, - final SetupAssignments setupAssignments ) + final ConverterSetups converterSetups, + final int setupId ) { this.boxOverlay = boxOverlay; this.viewer = viewer; - this.setupAssignments = setupAssignments; + this.setups = converterSetups; - final int setupId = SetupAssignments.getUnusedSetupId( setupAssignments ); boxConverterSetup = new PlaceHolderConverterSetup( setupId, 0, 128, new ARGBType( 0x00994499) ); - boxConverterSetup.setViewer( this::repaint ); - boxSource = new TransformedBoxPlaceHolderSource( name, bbSource ); - boxSourceAndConverter = new SourceAndConverter<>( boxSource, ( input, output ) -> output.set( 0 ) ); + boxConverterSetup.setupChangeListeners().add( s -> this.repaint() ); + boxSourceAndConverter = new SourceAndConverter<>( + new TransformedBoxPlaceHolderSource( name, bbSource ), + ( input, output ) -> output.set( 0 ) ); } public void addToViewer() { - final VisibilityAndGrouping vg = viewer.getVisibilityAndGrouping(); - if ( vg.getDisplayMode() != DisplayMode.FUSED ) + final ViewerState state = viewer.state(); + synchronized ( state ) { - final int numSources = vg.numSources(); - for ( int i = 0; i < numSources; ++i ) - vg.setSourceActive( i, vg.isSourceVisible( i ) ); - vg.setDisplayMode( DisplayMode.FUSED ); + if ( state.getDisplayMode() != DisplayMode.FUSED ) + { + for ( SourceAndConverter< ? > source : state.getSources() ) + state.setSourceActive( source, state.isSourceVisible( source ) ); + state.setDisplayMode( DisplayMode.FUSED ); + } + + state.addSource( boxSourceAndConverter ); + state.changeListeners().add( viewerStateChangeListener ); + state.setSourceActive( boxSourceAndConverter, true ); + state.setCurrentSource( boxSourceAndConverter ); + + isVisible = state.isSourceVisible( boxSourceAndConverter ); } - viewer.addSource( boxSourceAndConverter ); - vg.addUpdateListener( visibilityChanged ); - vg.setSourceActive( boxSource, true ); - vg.setCurrentSource( boxSource ); + setups.put( boxSourceAndConverter, boxConverterSetup ); + setups.getBounds().setBounds( boxConverterSetup, new Bounds( 0, 255 ) ); - setupAssignments.addSetup( boxConverterSetup ); - setupAssignments.getMinMaxGroup( boxConverterSetup ).setRange( 0, 255 ); - - isVisible = isVisible(); repaint(); } public void removeFromViewer() { - final VisibilityAndGrouping vg = viewer.getVisibilityAndGrouping(); - vg.removeUpdateListener( visibilityChanged ); - viewer.removeSource( boxSource ); - setupAssignments.removeSetup( boxConverterSetup ); - } - - private boolean isVisible() - { - final ViewerState state = viewer.getState(); - int sourceIndex = 0; - for ( final SourceState< ? > s : state.getSources() ) - if ( s.getSpimSource() == boxSource ) - break; - else - ++sourceIndex; - switch ( state.getDisplayMode() ) - { - case SINGLE: - return ( sourceIndex == state.getCurrentSource() ); - case GROUP: - return state.getSourceGroups().get( state.getCurrentGroup() ).getSourceIds().contains( sourceIndex ); - case FUSED: - return state.getSources().get( sourceIndex ).isActive(); - case FUSEDGROUP: - default: - for ( final SourceGroup group : state.getSourceGroups() ) - if ( group.isActive() && group.getSourceIds().contains( sourceIndex ) ) - return true; - } - return false; + viewer.state().changeListeners().remove( viewerStateChangeListener ); + viewer.state().removeSource( boxSourceAndConverter ); } - private final VisibilityAndGrouping.UpdateListener visibilityChanged = this::visibilityChanged; + private final ViewerStateChangeListener viewerStateChangeListener = this::viewerStateChanged; - private void visibilityChanged( final Event e ) + private void viewerStateChanged( final ViewerStateChange change ) { - if ( e.id == VISIBILITY_CHANGED || e.id == SOURCE_ACTVITY_CHANGED ) + if ( change == VISIBILITY_CHANGED ) { final boolean wasVisible = isVisible; - isVisible = isVisible(); + isVisible = viewer.state().isSourceVisible( boxSourceAndConverter ); if ( wasVisible != isVisible ) repaint(); } @@ -162,6 +132,7 @@ public class TransformedBoxOverlaySource private void repaint() { + System.out.println( "TransformedBoxOverlaySource.repaint" ); boxOverlay.fillIntersection( isVisible ); if ( isVisible ) { diff --git a/src/main/java/bdv/tools/boundingbox/TransformedBoxPlaceHolderSource.java b/src/main/java/bdv/tools/boundingbox/TransformedBoxPlaceHolderSource.java index abdd3ba701e60584634816a8ce66d8333ae069e0..fb7c43086f836bd0ce308f4f63acd48d2c9af851 100644 --- a/src/main/java/bdv/tools/boundingbox/TransformedBoxPlaceHolderSource.java +++ b/src/main/java/bdv/tools/boundingbox/TransformedBoxPlaceHolderSource.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE diff --git a/src/main/java/bdv/tools/boundingbox/TransformedBoxSelectionDialog.java b/src/main/java/bdv/tools/boundingbox/TransformedBoxSelectionDialog.java index cee31ad5a0d848710cb6d125cce508d0294b4c99..f170378c8a3e613fe419396643e78f2eac56045f 100644 --- a/src/main/java/bdv/tools/boundingbox/TransformedBoxSelectionDialog.java +++ b/src/main/java/bdv/tools/boundingbox/TransformedBoxSelectionDialog.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -31,6 +30,7 @@ package bdv.tools.boundingbox; import static bdv.tools.boundingbox.BoxSelectionOptions.TimepointSelection.NONE; +import bdv.viewer.ConverterSetups; import java.awt.BorderLayout; import java.awt.Font; import java.awt.GridBagConstraints; @@ -52,7 +52,6 @@ import net.imglib2.realtransform.AffineTransform3D; import org.scijava.ui.behaviour.io.InputTriggerConfig; import org.scijava.ui.behaviour.util.TriggerBehaviourBindings; -import bdv.tools.brightness.SetupAssignments; import bdv.viewer.ViewerPanel; /** @@ -79,7 +78,8 @@ public class TransformedBoxSelectionDialog extends AbstractTransformedBoxSelecti public TransformedBoxSelectionDialog( final ViewerPanel viewer, - final SetupAssignments setupAssignments, + final ConverterSetups converterSetups, + final int setupId, final InputTriggerConfig keyConfig, final TriggerBehaviourBindings triggerbindings, final AffineTransform3D boxTransform, @@ -106,7 +106,8 @@ public class TransformedBoxSelectionDialog extends AbstractTransformedBoxSelecti boxEditor = new TransformedBoxEditor( keyConfig, viewer, - setupAssignments, + converterSetups, + setupId, triggerbindings, model ); boxEditor.setPerspective( 1, 1000 ); // TODO expose, initialize from rangeInterval diff --git a/src/main/java/bdv/tools/boundingbox/TransformedRealBoxModel.java b/src/main/java/bdv/tools/boundingbox/TransformedRealBoxModel.java index 0c42a47f5e440796a73d524863c980736b84ef44..18deaea54b0c4d2e68331829bab9546676373933 100644 --- a/src/main/java/bdv/tools/boundingbox/TransformedRealBoxModel.java +++ b/src/main/java/bdv/tools/boundingbox/TransformedRealBoxModel.java @@ -1,3 +1,31 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package bdv.tools.boundingbox; import bdv.util.ModifiableRealInterval; diff --git a/src/main/java/bdv/tools/boundingbox/TransformedRealBoxSelectionDialog.java b/src/main/java/bdv/tools/boundingbox/TransformedRealBoxSelectionDialog.java index b4c707de59c9aeb457f9d639d37cb6463a2f4011..7bfc7e765b7c06d3ce91a6977f4170df905660c6 100644 --- a/src/main/java/bdv/tools/boundingbox/TransformedRealBoxSelectionDialog.java +++ b/src/main/java/bdv/tools/boundingbox/TransformedRealBoxSelectionDialog.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -31,6 +30,7 @@ package bdv.tools.boundingbox; import static bdv.tools.boundingbox.BoxSelectionOptions.TimepointSelection.NONE; +import bdv.viewer.ConverterSetups; import java.awt.BorderLayout; import java.awt.Font; import java.awt.GridBagConstraints; @@ -55,7 +55,6 @@ import net.imglib2.util.Intervals; import org.scijava.ui.behaviour.io.InputTriggerConfig; import org.scijava.ui.behaviour.util.TriggerBehaviourBindings; -import bdv.tools.brightness.SetupAssignments; import bdv.viewer.ViewerPanel; /** @@ -82,7 +81,8 @@ public class TransformedRealBoxSelectionDialog extends AbstractTransformedBoxSel public TransformedRealBoxSelectionDialog( final ViewerPanel viewer, - final SetupAssignments setupAssignments, + final ConverterSetups converterSetups, + final int setupId, final InputTriggerConfig keyConfig, final TriggerBehaviourBindings triggerbindings, final AffineTransform3D boxTransform, @@ -109,7 +109,8 @@ public class TransformedRealBoxSelectionDialog extends AbstractTransformedBoxSel boxEditor = new TransformedBoxEditor( keyConfig, viewer, - setupAssignments, + converterSetups, + setupId, triggerbindings, model ); boxEditor.setPerspective( 1, 1000 ); // TODO expose, initialize from rangeInterval diff --git a/src/main/java/bdv/tools/brightness/BrightnessDialog.java b/src/main/java/bdv/tools/brightness/BrightnessDialog.java index 34b60ebdb603371c0b09900d14b5c934d0e2811d..906694afeaff3fe09901b30ef20d60a980abadf1 100644 --- a/src/main/java/bdv/tools/brightness/BrightnessDialog.java +++ b/src/main/java/bdv/tools/brightness/BrightnessDialog.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -31,13 +30,9 @@ package bdv.tools.brightness; import java.awt.BorderLayout; import java.awt.Color; -import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Frame; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.RenderingHints; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -47,12 +42,12 @@ import java.awt.event.KeyEvent; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.BoxLayout; -import javax.swing.Icon; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JCheckBox; @@ -65,11 +60,11 @@ import javax.swing.JSpinner; import javax.swing.KeyStroke; import javax.swing.SpinnerNumberModel; import javax.swing.SwingUtilities; -import javax.swing.WindowConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import bdv.util.InvokeOnEDT; +import bdv.util.DelayedPackDialog; import mpicbg.spim.data.generic.sequence.BasicViewSetup; import net.imglib2.type.numeric.ARGBType; @@ -77,9 +72,10 @@ import net.imglib2.type.numeric.ARGBType; /** * Adjust brightness and colors for individual (or groups of) {@link BasicViewSetup setups}. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ -public class BrightnessDialog extends JDialog +@Deprecated +public class BrightnessDialog extends DelayedPackDialog { public BrightnessDialog( final Frame owner, final SetupAssignments setupAssignments ) { @@ -108,6 +104,8 @@ public class BrightnessDialog extends JDialog im.put( KeyStroke.getKeyStroke( KeyEvent.VK_ESCAPE, 0 ), hideKey ); am.put( hideKey, hideAction ); + final AtomicBoolean recreateContentPending = new AtomicBoolean(); + setupAssignments.setUpdateListener( new SetupAssignments.UpdateListener() { @Override @@ -116,8 +114,17 @@ public class BrightnessDialog extends JDialog try { InvokeOnEDT.invokeAndWait( () -> { - colorsPanel.recreateContent(); - minMaxPanels.recreateContent(); + if ( isVisible() ) + { + System.out.println( "colorsPanel.recreateContent()" ); + colorsPanel.recreateContent(); + minMaxPanels.recreateContent(); + recreateContentPending.set( false ); + } + else + { + recreateContentPending.set( true ); + } } ); } catch ( InvocationTargetException | InterruptedException e ) @@ -127,44 +134,20 @@ public class BrightnessDialog extends JDialog } } ); - pack(); - setDefaultCloseOperation( WindowConstants.HIDE_ON_CLOSE ); - } - - /** - * Adapted from http://stackoverflow.com/a/3072979/230513 - */ - private static class ColorIcon implements Icon - { - private final int size = 16; - - private final Color color; - - public ColorIcon( final Color color ) - { - this.color = color; - } - - @Override - public void paintIcon( final Component c, final Graphics g, final int x, final int y ) + addComponentListener( new ComponentAdapter() { - final Graphics2D g2d = ( Graphics2D ) g; - g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON ); - g2d.setColor( color ); - g2d.fillOval( x, y, size, size ); - } - - @Override - public int getIconWidth() - { - return size; - } + @Override + public void componentShown( final ComponentEvent e ) + { + if ( recreateContentPending.getAndSet( false ) ) + { + colorsPanel.recreateContent(); + minMaxPanels.recreateContent(); + } + } + } ); - @Override - public int getIconHeight() - { - return size; - } + pack(); } public static class ColorsPanel extends JPanel @@ -196,27 +179,22 @@ public class BrightnessDialog extends JDialog for ( final ConverterSetup setup : setupAssignments.getConverterSetups() ) { final JButton button = new JButton( new ColorIcon( getColor( setup ) ) ); - button.addActionListener( new ActionListener() - { - @Override - public void actionPerformed( final ActionEvent e ) + button.addActionListener( e -> { + colorChooser.setColor( getColor( setup ) ); + final JDialog d = JColorChooser.createDialog( button, "Choose a color", true, colorChooser, new ActionListener() { - colorChooser.setColor( getColor( setup ) ); - final JDialog d = JColorChooser.createDialog( button, "Choose a color", true, colorChooser, new ActionListener() + @Override + public void actionPerformed( final ActionEvent arg0 ) { - @Override - public void actionPerformed( final ActionEvent arg0 ) + final Color c = colorChooser.getColor(); + if (c != null) { - final Color c = colorChooser.getColor(); - if (c != null) - { - button.setIcon( new ColorIcon( c ) ); - setColor( setup, c ); - } + button.setIcon( new ColorIcon( c ) ); + setColor( setup, c ); } - }, null ); - d.setVisible( true ); - } + } + }, null ); + d.setVisible( true ); } ); button.setEnabled( setup.supportsColor() ); buttons.add( button ); @@ -237,7 +215,7 @@ public class BrightnessDialog extends JDialog return new Color( value ); } else - return new Color ( 0xFFBBBBBB ); + return null; } private static void setColor( final ConverterSetup setup, final Color color ) diff --git a/src/main/java/bdv/tools/brightness/ColorIcon.java b/src/main/java/bdv/tools/brightness/ColorIcon.java new file mode 100644 index 0000000000000000000000000000000000000000..271153cdf92f9d2bae846eb5f4461987d02aad31 --- /dev/null +++ b/src/main/java/bdv/tools/brightness/ColorIcon.java @@ -0,0 +1,144 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.tools.brightness; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import javax.swing.Icon; + +/** + * Adapted from http://stackoverflow.com/a/3072979/230513 + */ +public class ColorIcon implements Icon +{ + private final int width; + + private final int height; + + private final boolean drawAsCircle; + + private final int arcWidth; + + private final int arcHeight; + + private final boolean drawOutline; + + private final Color color; + + private final int size; // == min(width, height) + + private final int ox; + + private final int oy; + + public ColorIcon( final Color color ) + { + this( color, 16, 16, true ); + } + + public ColorIcon( final Color color, final int width, final int height, final boolean drawAsCircle ) + { + this( color, width, height, drawAsCircle, 3, 3, false ); + } + + public ColorIcon( final Color color, final int width, final int height, final int arcWidth, final int arcHeight ) + { + this( color, width, height, false, arcWidth, arcHeight, false ); + } + + public ColorIcon( final Color color, final int width, final int height, final int arcWidth, final int arcHeight, final boolean drawOutline ) + { + this( color, width, height, false, arcWidth, arcHeight, drawOutline ); + } + + private ColorIcon( final Color color, final int width, final int height, final boolean drawAsCircle, final int arcWidth, final int arcHeight, final boolean drawOutline ) + { + this.color = color; + this.width = width; + this.height = height; + this.drawAsCircle = drawAsCircle; + this.arcWidth = arcWidth; + this.arcHeight = arcHeight; + this.drawOutline = drawOutline; + + size = Math.min( width, height ); + ox = ( width - size ) / 2; + oy = ( height - size ) / 2; + } + + @Override + public void paintIcon( final Component c, final Graphics g, final int x, final int y ) + { + final Graphics2D g2d = ( Graphics2D ) g; + g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON ); + final int x0 = x + ox; + final int y0 = y + oy; + if ( color == null ) + { + g2d.setColor( new Color( 0xffbcbc ) ); + g2d.fillArc( x0, y0, size, size, 0, 120 ); + g2d.setColor( new Color( 0xbcffbc ) ); + g2d.fillArc( x0, y0, size, size, 120, 120 ); + g2d.setColor( new Color( 0xbcbcff ) ); + g2d.fillArc( x0, y0, size, size, 240, 120 ); + } + else + { + g2d.setColor( color ); + if ( drawAsCircle ) + g2d.fillOval( x0, y0, size, size ); + else + g2d.fillRoundRect( x, y, width, height, arcWidth, arcHeight ); + + if ( drawOutline ) + { + g2d.setColor( c.isFocusOwner() ? new Color( 0x8FC4F9 ) : Color.gray ); + if ( drawAsCircle ) + g2d.drawOval( x0, y0, size, size ); + else + g2d.drawRoundRect( x, y, width, height, arcWidth, arcHeight ); + } + } + } + + @Override + public int getIconWidth() + { + return width; + } + + @Override + public int getIconHeight() + { + return height; + } +} diff --git a/src/main/java/bdv/tools/brightness/ConverterSetup.java b/src/main/java/bdv/tools/brightness/ConverterSetup.java index e79ab42d25f0fbf9076039ffbba5029f031adead..b928d220540461bfc80f5097d545b243d0681fe9 100644 --- a/src/main/java/bdv/tools/brightness/ConverterSetup.java +++ b/src/main/java/bdv/tools/brightness/ConverterSetup.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,43 +28,61 @@ */ package bdv.tools.brightness; +import net.imglib2.type.numeric.ARGBType; + +import org.scijava.listeners.Listeners; + import bdv.viewer.RequestRepaint; import mpicbg.spim.data.generic.sequence.BasicViewSetup; -import net.imglib2.type.numeric.ARGBType; /** * Modify the range and color of the converter for a source. Because each source * can have its own converter, this is used to adjust brightness, contrast, and * color of individual sources. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public interface ConverterSetup { + /** + * {@link SetupChangeListener}s are notified about changes to a + * {@code ConverterSetup}. + */ + interface SetupChangeListener + { + void setupParametersChanged( ConverterSetup setup ); + } + + /** + * {@code SetupChangeListener}s can be added/removed here, and will be + * notified about changes to this {@code ConverterSetup}. + */ + Listeners< SetupChangeListener > setupChangeListeners(); + /** * Get the id of the {@link BasicViewSetup} this converter acts on. * * @return the id of the {@link BasicViewSetup} this converter acts on. */ - public int getSetupId(); + int getSetupId(); /** * Set the range of source values that is mapped to the full range of the * target type. Source values outside of the specified range are clamped. * * @param min - * source value to map to minimum of the target range. + * source value to map to minimum of the target range. * @param max - * source value to map to maximum of the target range. + * source value to map to maximum of the target range. */ - public void setDisplayRange( double min, double max ); + void setDisplayRange( double min, double max ); /** * Set the color for this converter. */ - public void setColor( final ARGBType color ); + void setColor( ARGBType color ); - public boolean supportsColor(); + boolean supportsColor(); /** * Get the (largest) source value that is mapped to the minimum of the @@ -73,7 +90,7 @@ public interface ConverterSetup * * @return source value that is mapped to the minimum of the target range. */ - public double getDisplayRangeMin(); + double getDisplayRangeMin(); /** * Get the (smallest) source value that is mapped to the maximum of the @@ -81,20 +98,26 @@ public interface ConverterSetup * * @return source value that is mapped to the maximum of the target range. */ - public double getDisplayRangeMax(); + double getDisplayRangeMax(); /** * Get the color for this converter. * * @return the color for this converter. */ - public ARGBType getColor(); + ARGBType getColor(); /** + * Deprecated: Use "{@code setupChangeListeners().add( s -> viewer.requestRepaint() )}" instead. + * <p> * Set the {@link RequestRepaint} that should be notified if * {@link ConverterSetup} settings change. * * @param viewer */ - public void setViewer( final RequestRepaint viewer ); + @Deprecated + default void setViewer( RequestRepaint viewer ) + { + setupChangeListeners().add( s -> viewer.requestRepaint() ); + } } diff --git a/src/main/java/bdv/tools/brightness/MinMaxGroup.java b/src/main/java/bdv/tools/brightness/MinMaxGroup.java index 8f9b8c11fc57630301fb5b4b6c81f5b1f3964a08..de3ce938bcef80be980c049ca50eca8ad1017596 100644 --- a/src/main/java/bdv/tools/brightness/MinMaxGroup.java +++ b/src/main/java/bdv/tools/brightness/MinMaxGroup.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -50,7 +49,7 @@ import bdv.util.BoundedValueDouble; * An {@link UpdateListener} (usually a GUI component) can be notified about * changes. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class MinMaxGroup extends BoundedIntervalDouble { @@ -62,7 +61,7 @@ public class MinMaxGroup extends BoundedIntervalDouble public interface UpdateListener { - public void update(); + void update(); } private UpdateListener updateListener; diff --git a/src/main/java/bdv/tools/brightness/RealARGBColorConverterSetup.java b/src/main/java/bdv/tools/brightness/RealARGBColorConverterSetup.java index f79066c3a0c6514642592aafc6b13fff4dd5e3a0..ea6e0849f71992cf71508439598780c7c9aa93bb 100644 --- a/src/main/java/bdv/tools/brightness/RealARGBColorConverterSetup.java +++ b/src/main/java/bdv/tools/brightness/RealARGBColorConverterSetup.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -32,49 +31,75 @@ package bdv.tools.brightness; import java.util.Arrays; import java.util.List; -import bdv.viewer.RequestRepaint; import net.imglib2.display.ColorConverter; import net.imglib2.type.numeric.ARGBType; +import org.scijava.listeners.Listeners; + public class RealARGBColorConverterSetup implements ConverterSetup { - protected final int id; + private final int id; - protected final List< ColorConverter > converters; + private final List< ColorConverter > converters; - protected RequestRepaint viewer; + private final Listeners.List< SetupChangeListener > listeners; - public RealARGBColorConverterSetup( final int setupId, final ColorConverter ... converters ) + public RealARGBColorConverterSetup( final int setupId, final ColorConverter... converters ) { - this( setupId, Arrays.< ColorConverter >asList( converters ) ); + this( setupId, Arrays.asList( converters ) ); } public RealARGBColorConverterSetup( final int setupId, final List< ColorConverter > converters ) { this.id = setupId; this.converters = converters; - this.viewer = null; + this.listeners = new Listeners.SynchronizedList<>(); + } + + @Override + public Listeners< SetupChangeListener > setupChangeListeners() + { + return listeners; } @Override public void setDisplayRange( final double min, final double max ) { + boolean changed = false; for ( final ColorConverter converter : converters ) { - converter.setMin( min ); - converter.setMax( max ); + if ( converter.getMin() != min ) + { + converter.setMin( min ); + changed = true; + } + if ( converter.getMax() != max ) + { + converter.setMax( max ); + changed = true; + } } - if ( viewer != null ) - viewer.requestRepaint(); + if ( changed ) + listeners.list.forEach( l -> l.setupParametersChanged( this ) ); } @Override public void setColor( final ARGBType color ) { + if ( !supportsColor() ) + return; + + boolean changed = false; for ( final ColorConverter converter : converters ) - converter.setColor( color ); - if ( viewer != null ) - viewer.requestRepaint(); + { + if ( converter.getColor().get() != color.get() ) + { + converter.setColor( color ); + changed = true; + } + } + if ( changed ) + listeners.list.forEach( l -> l.setupParametersChanged( this ) ); } @Override @@ -106,10 +131,4 @@ public class RealARGBColorConverterSetup implements ConverterSetup { return converters.get( 0 ).getColor(); } - - @Override - public void setViewer( final RequestRepaint viewer ) - { - this.viewer = viewer; - } } diff --git a/src/main/java/bdv/tools/brightness/SetupAssignments.java b/src/main/java/bdv/tools/brightness/SetupAssignments.java index 83c3357ecc0770f3e1b29233e7d811d1fbf8a371..6e343899b210bcfd58540b622b84f6d5e462063f 100644 --- a/src/main/java/bdv/tools/brightness/SetupAssignments.java +++ b/src/main/java/bdv/tools/brightness/SetupAssignments.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -50,8 +49,9 @@ import mpicbg.spim.data.XmlHelpers; * <li>No group is empty.</li> * </ol> * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ +@Deprecated public class SetupAssignments { /** @@ -83,7 +83,7 @@ public class SetupAssignments public interface UpdateListener { - public void update(); + void update(); } private UpdateListener updateListener; diff --git a/src/main/java/bdv/tools/brightness/SliderPanel.java b/src/main/java/bdv/tools/brightness/SliderPanel.java index f91f7db0e05052d482e5cb9154741f87c279ae37..5043425cbca27619039247227eea0be7e928ea59 100644 --- a/src/main/java/bdv/tools/brightness/SliderPanel.java +++ b/src/main/java/bdv/tools/brightness/SliderPanel.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/tools/brightness/SliderPanelDouble.java b/src/main/java/bdv/tools/brightness/SliderPanelDouble.java index 821e78202f7325556a8d7ec6d0a1f0a9cf59f63c..463413dfc42396afa97aaa7a9b6fb52426ef6186 100644 --- a/src/main/java/bdv/tools/brightness/SliderPanelDouble.java +++ b/src/main/java/bdv/tools/brightness/SliderPanelDouble.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -71,9 +70,9 @@ public class SliderPanelDouble extends JPanel implements BoundedValueDouble.Upda private RangeListener rangeListener; - public static interface RangeListener + public interface RangeListener { - public void rangeChanged(); + void rangeChanged(); } /** diff --git a/src/main/java/bdv/tools/crop/CropDialog.java b/src/main/java/bdv/tools/crop/CropDialog.java index 30dc1217d3e811653f8882908cb85e7f76bd89ea..25a2c0c334fdf6ba9e9f3d82a8ff2c8c28050990 100644 --- a/src/main/java/bdv/tools/crop/CropDialog.java +++ b/src/main/java/bdv/tools/crop/CropDialog.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,6 +28,8 @@ */ package bdv.tools.crop; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.ViewerState; import java.awt.BorderLayout; import java.awt.Frame; import java.awt.event.ActionEvent; @@ -71,7 +72,6 @@ import bdv.spimdata.XmlIoSpimDataMinimal; import bdv.tools.transformation.TransformedSource; import bdv.viewer.Source; import bdv.viewer.ViewerPanel; -import bdv.viewer.state.SourceState; import mpicbg.spim.data.SpimDataException; import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription; import mpicbg.spim.data.generic.sequence.BasicViewSetup; @@ -101,7 +101,7 @@ public class CropDialog extends JDialog { if ( b ) { - final int tp = viewer.getState().getCurrentTimepoint(); + final int tp = viewer.state().getCurrentTimepoint(); spinnerMinTimepoint.setValue( tp ); spinnerMaxTimepoint.setValue( tp ); } @@ -291,9 +291,6 @@ public class CropDialog extends JDialog */ public void cropGlobal( final int minTimepointIndex, final int maxTimepointIndex, final File hdf5File, final File xmlFile ) throws SpimDataException { - final AffineTransform3D globalToCropTransform = new AffineTransform3D(); - viewer.getState().getViewerTransform( globalToCropTransform ); - final int w = viewer.getDisplay().getWidth(); final int h = viewer.getDisplay().getHeight(); final int d = Math.min( w, h ); @@ -322,30 +319,37 @@ public class CropDialog extends JDialog // This is needed because the CropImgLoader is asked for (timepointId, // setupId) pair and needs to retrieve from corresponding source. final HashMap< Integer, Integer > setupIdToSourceIndex = new HashMap<>(); - for( final SourceState< ? > s : viewer.getState().getSources() ) + + final AffineTransform3D globalToCropTransform = new AffineTransform3D(); + final ViewerState state = viewer.state(); + synchronized ( state ) { - Source< ? > source = s.getSpimSource(); - sources.add( source ); + state.getViewerTransform( globalToCropTransform ); + for ( SourceAndConverter< ? > s : state.getSources() ) + { + Source< ? > source = s.getSpimSource(); + sources.add( source ); - // try to find the BasicViewSetup for the source - final BasicViewSetup setup; + // try to find the BasicViewSetup for the source + final BasicViewSetup setup; - // strip TransformedSource wrapper - while ( source instanceof TransformedSource ) - source = ( ( TransformedSource< ? > ) source ).getWrappedSource(); + // strip TransformedSource wrapper + while ( source instanceof TransformedSource ) + source = ( ( TransformedSource< ? > ) source ).getWrappedSource(); - if ( source instanceof AbstractSpimSource ) - { - final int setupId = ( ( AbstractSpimSource< ? > ) source ).getSetupId(); - setup = sequenceDescription.getViewSetups().get( setupId ); - } - else - { - final int setupId = nextSetupIndex++; - setup = new BasicViewSetup( setupId, Integer.toString( setupId ), null, null ); + if ( source instanceof AbstractSpimSource ) + { + final int setupId = ( ( AbstractSpimSource< ? > ) source ).getSetupId(); + setup = sequenceDescription.getViewSetups().get( setupId ); + } + else + { + final int setupId = nextSetupIndex++; + setup = new BasicViewSetup( setupId, Integer.toString( setupId ), null, null ); + } + cropSetups.put( setup.getId(), setup ); + setupIdToSourceIndex.put( setup.getId(), sources.size() - 1 ); } - cropSetups.put( setup.getId(), setup ); - setupIdToSourceIndex.put( setup.getId(), sources.size() - 1 ); } // Map from timepoint id to timepoint index (in the list of timepoints diff --git a/src/main/java/bdv/tools/crop/CropImgLoader.java b/src/main/java/bdv/tools/crop/CropImgLoader.java index e24fb30ab2ada4a6408a3e0758b24a17d2bf1ce6..afa3d502821788bbda3cd8f8781661669a9ba82b 100644 --- a/src/main/java/bdv/tools/crop/CropImgLoader.java +++ b/src/main/java/bdv/tools/crop/CropImgLoader.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -57,7 +56,7 @@ import bdv.viewer.Source; * This {@link ImgLoader} provides views and transformations into a cropped * region of a data-set (provided by list of {@link Source Sources}). * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class CropImgLoader implements BasicImgLoader { diff --git a/src/main/java/bdv/tools/transformation/ManualSourceTransforms.java b/src/main/java/bdv/tools/transformation/ManualSourceTransforms.java index 59db360a575a19fc238a729310cdfcdb7a80654d..b6fe5c4940073c26f46a1857ccc2320b865ad2f8 100644 --- a/src/main/java/bdv/tools/transformation/ManualSourceTransforms.java +++ b/src/main/java/bdv/tools/transformation/ManualSourceTransforms.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/tools/transformation/ManualTransformActiveListener.java b/src/main/java/bdv/tools/transformation/ManualTransformActiveListener.java index 552665133848c7e76e98748ba031a2ebd9ddebe8..9087a7cd41499d66c167afff16847b837331586a 100644 --- a/src/main/java/bdv/tools/transformation/ManualTransformActiveListener.java +++ b/src/main/java/bdv/tools/transformation/ManualTransformActiveListener.java @@ -1,9 +1,8 @@ /*- * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -31,5 +30,5 @@ package bdv.tools.transformation; public interface ManualTransformActiveListener { - public void manualTransformActiveChanged( final boolean acitve ); + void manualTransformActiveChanged( final boolean active ); } diff --git a/src/main/java/bdv/tools/transformation/ManualTransformation.java b/src/main/java/bdv/tools/transformation/ManualTransformation.java index 70e209b7bd8e76a49c86e1aeb2f1d2345892210f..ff726ff99e74397b0928720196613732e958525e 100644 --- a/src/main/java/bdv/tools/transformation/ManualTransformation.java +++ b/src/main/java/bdv/tools/transformation/ManualTransformation.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,6 +28,8 @@ */ package bdv.tools.transformation; +import bdv.viewer.SynchronizedViewerState; +import bdv.viewer.ViewerState; import java.util.ArrayList; import java.util.List; @@ -88,14 +89,23 @@ public class ManualTransformation private ArrayList< TransformedSource< ? > > getTransformedSources() { + final List< ? extends SourceAndConverter< ? > > sourceList; + if ( sources != null ) + sourceList = sources; + else + { + final ViewerState state = viewer.state(); + synchronized ( state ) + { + sourceList = new ArrayList<>( state.getSources() ); + } + } + final ArrayList< TransformedSource< ? > > list = new ArrayList<>(); - final List< ? extends SourceAndConverter< ? > > sourceList = ( sources != null ) - ? sources - : viewer.getState().getSources(); for ( final SourceAndConverter< ? > soc : sourceList ) { final Source< ? > source = soc.getSpimSource(); - if ( TransformedSource.class.isInstance( source ) ) + if ( source instanceof TransformedSource ) list.add( (TransformedSource< ? > ) source ); } return list; diff --git a/src/main/java/bdv/tools/transformation/ManualTransformationEditor.java b/src/main/java/bdv/tools/transformation/ManualTransformationEditor.java index 3575d87bd693e957fd30d7c795b7189339a3f0b6..d0f9146ba36c40c5a1f6b7d5be222052e1331973 100644 --- a/src/main/java/bdv/tools/transformation/ManualTransformationEditor.java +++ b/src/main/java/bdv/tools/transformation/ManualTransformationEditor.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,27 +28,21 @@ */ package bdv.tools.transformation; -import java.awt.event.ActionEvent; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.ViewerPanel; +import bdv.viewer.ViewerState; import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.InputMap; import javax.swing.KeyStroke; - -import org.scijava.ui.behaviour.util.InputActionBindings; - -import bdv.viewer.Source; -import bdv.viewer.ViewerPanel; -import bdv.viewer.state.SourceGroup; -import bdv.viewer.state.ViewerState; import net.imglib2.realtransform.AffineTransform3D; -import net.imglib2.ui.TransformListener; - +import bdv.viewer.TransformListener; +import org.scijava.listeners.Listeners; +import org.scijava.ui.behaviour.util.InputActionBindings; +import org.scijava.ui.behaviour.util.RunnableAction; // TODO: what happens when the current source, display mode, etc is changed while the editor is active? deactivate? public class ManualTransformationEditor implements TransformListener< AffineTransform3D > @@ -72,7 +65,7 @@ public class ManualTransformationEditor implements TransformListener< AffineTran private final InputMap inputMap; - protected final CopyOnWriteArrayList< ManualTransformActiveListener > manualTransformActiveListeners; + private final Listeners.List< ManualTransformActiveListener > manualTransformActiveListeners; public ManualTransformationEditor( final ViewerPanel viewer, final InputActionBindings inputActionBindings ) { @@ -82,30 +75,12 @@ public class ManualTransformationEditor implements TransformListener< AffineTran liveTransform = new AffineTransform3D(); sourcesToModify = new ArrayList<>(); sourcesToFix = new ArrayList<>(); - manualTransformActiveListeners = new CopyOnWriteArrayList<>(); + manualTransformActiveListeners = new Listeners.SynchronizedList<>(); final KeyStroke abortKey = KeyStroke.getKeyStroke( KeyEvent.VK_ESCAPE, 0 ); - final Action abortAction = new AbstractAction( "abort manual transformation" ) - { - @Override - public void actionPerformed( final ActionEvent e ) - { - abort(); - } - - private static final long serialVersionUID = 1L; - }; + final Action abortAction = new RunnableAction( "abort manual transformation", this::abort ); final KeyStroke resetKey = KeyStroke.getKeyStroke( KeyEvent.VK_R, 0 ); - final Action resetAction = new AbstractAction( "reset manual transformation" ) - { - @Override - public void actionPerformed( final ActionEvent e ) - { - reset(); - } - - private static final long serialVersionUID = 1L; - }; + final Action resetAction = new RunnableAction( "reset manual transformation", this::reset ); actionMap = new ActionMap(); inputMap = new InputMap(); actionMap.put( "abort manual transformation", abortAction ); @@ -122,9 +97,10 @@ public class ManualTransformationEditor implements TransformListener< AffineTran final AffineTransform3D identity = new AffineTransform3D(); for ( final TransformedSource< ? > source : sourcesToModify ) source.setIncrementalTransform( identity ); - viewer.setCurrentViewerTransform( frozenTransform ); + viewer.state().setViewerTransform( frozenTransform ); viewer.showMessage( "aborted manual transform" ); active = false; + manualTransformActiveListeners.list.forEach( l -> l.manualTransformActiveChanged( active ) ); } } @@ -142,28 +118,28 @@ public class ManualTransformationEditor implements TransformListener< AffineTran { source.setIncrementalTransform( identity ); } - viewer.setCurrentViewerTransform( frozenTransform ); + viewer.state().setViewerTransform( frozenTransform ); viewer.showMessage( "reset manual transform" ); } } public synchronized void setActive( final boolean a ) { - if ( this.active == a ) { return; } + if ( this.active == a ) + return; + if ( a ) { - active = a; // Enter manual edit mode - final ViewerState state = viewer.getState(); - final List< Integer > indices = new ArrayList<>(); + final ViewerState state = viewer.state().snapshot(); + final List< SourceAndConverter< ? > > currentSources = new ArrayList<>(); switch ( state.getDisplayMode() ) { case FUSED: - indices.add( state.getCurrentSource() ); + currentSources.add( state.getCurrentSource() ); break; case FUSEDGROUP: - final SourceGroup group = state.getSourceGroups().get( state.getCurrentGroup() ); - indices.addAll( group.getSourceIds() ); + currentSources.addAll( state.getSourcesInGroup( state.getCurrentGroup() ) ); break; default: viewer.showMessage( "Can only do manual transformation when in FUSED mode." ); @@ -172,16 +148,14 @@ public class ManualTransformationEditor implements TransformListener< AffineTran state.getViewerTransform( frozenTransform ); sourcesToModify.clear(); sourcesToFix.clear(); - final int numSources = state.numSources(); - for ( int i = 0; i < numSources; ++i ) + for ( SourceAndConverter< ? > source : state.getSources() ) { - final Source< ? > source = state.getSources().get( i ).getSpimSource(); - if ( TransformedSource.class.isInstance( source ) ) + if ( source.getSpimSource() instanceof TransformedSource ) { - if ( indices.contains( i ) ) - sourcesToModify.add( ( bdv.tools.transformation.TransformedSource< ? > ) source ); + if ( currentSources.contains( source ) ) + sourcesToModify.add( ( TransformedSource< ? > ) source.getSpimSource() ); else - sourcesToFix.add( ( bdv.tools.transformation.TransformedSource< ? > ) source ); + sourcesToFix.add( ( TransformedSource< ? > ) source.getSpimSource() ); } } active = true; @@ -190,7 +164,8 @@ public class ManualTransformationEditor implements TransformListener< AffineTran viewer.showMessage( "starting manual transform" ); } else - { // Exit manual edit mode. + { + // Exit manual edit mode. active = false; viewer.removeTransformListener( this ); bindings.removeInputMap( "manual transform" ); @@ -206,13 +181,10 @@ public class ManualTransformationEditor implements TransformListener< AffineTran tmp.identity(); for ( final TransformedSource< ? > source : sourcesToFix ) source.setIncrementalTransform( tmp ); - viewer.setCurrentViewerTransform( frozenTransform ); + viewer.state().setViewerTransform( frozenTransform ); viewer.showMessage( "fixed manual transform" ); } - for ( final ManualTransformActiveListener l : manualTransformActiveListeners ) - { - l.manualTransformActiveChanged( active ); - } + manualTransformActiveListeners.list.forEach( l -> l.manualTransformActiveChanged( active ) ); } public synchronized void toggle() @@ -232,13 +204,8 @@ public class ManualTransformationEditor implements TransformListener< AffineTran source.setIncrementalTransform( liveTransform.inverse() ); } - public void addManualTransformActiveListener( final ManualTransformActiveListener l ) - { - manualTransformActiveListeners.add( l ); - } - - public void removeManualTransformActiveListener( final ManualTransformActiveListener l ) + public Listeners< ManualTransformActiveListener > manualTransformActiveListeners() { - manualTransformActiveListeners.remove( l ); + return manualTransformActiveListeners; } } diff --git a/src/main/java/bdv/tools/transformation/TransformedSource.java b/src/main/java/bdv/tools/transformation/TransformedSource.java index 1f76eb0f76b5cbc41577f8cc43a7c01a0fc84d58..0d84ea5c9ae15ecdf14e3dc585f006b18bdaae5a 100644 --- a/src/main/java/bdv/tools/transformation/TransformedSource.java +++ b/src/main/java/bdv/tools/transformation/TransformedSource.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -122,6 +121,12 @@ public class TransformedSource< T > implements Source< T >, MipmapOrdering this.composed = new AffineTransform3D(); } + @Override + public boolean doBoundingBoxCulling() + { + return source.doBoundingBoxCulling(); + } + /* * EXTRA TRANSFORMATION methods */ diff --git a/src/main/java/bdv/tools/transformation/XmlIoTransformedSources.java b/src/main/java/bdv/tools/transformation/XmlIoTransformedSources.java index 881ca14c3530455f847918f71787887bc090abd8..201b0dc330b279b8a0a6e7ee4068addba9cc7056 100644 --- a/src/main/java/bdv/tools/transformation/XmlIoTransformedSources.java +++ b/src/main/java/bdv/tools/transformation/XmlIoTransformedSources.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/ui/BdvDefaultCards.java b/src/main/java/bdv/ui/BdvDefaultCards.java new file mode 100644 index 0000000000000000000000000000000000000000..403dd1fcf45d9d2751da5347a4c0a3332ca04301 --- /dev/null +++ b/src/main/java/bdv/ui/BdvDefaultCards.java @@ -0,0 +1,198 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui; + +import bdv.ui.convertersetupeditor.ConverterSetupEditPanel; +import bdv.ui.sourcegrouptree.SourceGroupTree; +import bdv.ui.sourcetable.SourceTable; +import bdv.ui.viewermodepanel.DisplaySettingsPanel; +import bdv.viewer.ConverterSetups; +import bdv.viewer.SynchronizedViewerState; +import bdv.viewer.ViewerPanel; +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.KeyboardFocusManager; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.lang.ref.WeakReference; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.border.EmptyBorder; +import javax.swing.tree.TreeSelectionModel; + +/** + * Default cards added to the card panel. + * <ul> + * <li>Display Modes</li> + * <li>SourceTable with ConverterSetup editor</li> + * <li>SourceGroupTree with ConverterSetup editor</li> + * </ul> + * + * @author Tobias Pietzsch + */ +public class BdvDefaultCards +{ + public static final String DEFAULT_SOURCES_CARD = "default bdv sources card"; + + public static final String DEFAULT_SOURCEGROUPS_CARD = "default bdv groups card"; + + public static final String DEFAULT_VIEWERMODES_CARD = "default bdv viewer modes card"; + + public static void setup( final CardPanel cards, final ViewerPanel viewer, final ConverterSetups converterSetups ) + { + final SynchronizedViewerState state = viewer.state(); + + // -- Sources table -- + final SourceTable table = new SourceTable( state, converterSetups, viewer.getOptionValues().getInputTriggerConfig() ); + table.setPreferredScrollableViewportSize( new Dimension( 300, 200 ) ); + table.setFillsViewportHeight( true ); + table.setDragEnabled( true ); + final ConverterSetupEditPanel editPanelTable = new ConverterSetupEditPanel( table, converterSetups ); + final JPanel tablePanel = new JPanel( new BorderLayout() ); + final JScrollPane scrollPaneTable = new JScrollPane( table ); + scrollPaneTable.addMouseWheelListener( new MouseWheelScrollListener( scrollPaneTable ) ); + scrollPaneTable.setBorder( new EmptyBorder( 0, 0, 0, 0 ) ); + tablePanel.add( scrollPaneTable, BorderLayout.CENTER ); + tablePanel.add( editPanelTable, BorderLayout.SOUTH ); + tablePanel.setPreferredSize( new Dimension( 300, 245 ) ); + + // -- Groups tree -- + final SourceGroupTree tree = new SourceGroupTree( state, viewer.getOptionValues().getInputTriggerConfig() ); +// tree.setPreferredSize( new Dimension( 300, 200 ) ); + tree.setVisibleRowCount( 10 ); + tree.setEditable( true ); + tree.setSelectionRow( 0 ); + tree.setRootVisible( false ); + tree.setShowsRootHandles( true ); + tree.setExpandsSelectedPaths( true ); + tree.getSelectionModel().setSelectionMode( TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION ); + final ConverterSetupEditPanel editPanelTree = new ConverterSetupEditPanel( tree, converterSetups ); + final JPanel treePanel = new JPanel( new BorderLayout() ); + final JScrollPane scrollPaneTree = new JScrollPane( tree ); + scrollPaneTree.addMouseWheelListener( new MouseWheelScrollListener( scrollPaneTree ) ); + scrollPaneTree.setBorder( new EmptyBorder( 0, 0, 0, 0 ) ); + treePanel.add( scrollPaneTree, BorderLayout.CENTER ); + treePanel.add( editPanelTree, BorderLayout.SOUTH ); + treePanel.setPreferredSize( new Dimension( 300, 225 ) ); + + new FocusListener( tablePanel, table, treePanel, tree ); + + cards.addCard( DEFAULT_VIEWERMODES_CARD, "Display Modes", new DisplaySettingsPanel( viewer.state() ), true, new Insets( 0, 4, 4, 0 ) ); + cards.addCard( DEFAULT_SOURCES_CARD, "Sources", tablePanel, true, new Insets( 0, 4, 0, 0 ) ); + cards.addCard( DEFAULT_SOURCEGROUPS_CARD, "Groups", treePanel, true, new Insets( 0, 4, 0, 0 ) ); + } + + private static class FocusListener implements PropertyChangeListener + { + private final KeyboardFocusManager keyboardFocusManager; + + private final WeakReference< JPanel > tablePanel; + private final WeakReference< SourceTable > table; + private final WeakReference< JPanel > treePanel; + private final WeakReference< SourceGroupTree > tree; + + static final int MAX_DEPTH = 8; + boolean tableFocused; + boolean treeFocused; + + FocusListener( final JPanel tablePanel, final SourceTable table, final JPanel treePanel, final SourceGroupTree tree ) + { + this.tablePanel = new WeakReference<>( tablePanel ); + this.table = new WeakReference<>( table ); + this.treePanel = new WeakReference<>( treePanel ); + this.tree = new WeakReference<>( tree ); + + keyboardFocusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager(); + keyboardFocusManager.addPropertyChangeListener( "focusOwner", this ); + } + + void focusTable( final boolean focus ) + { + if ( focus != tableFocused ) + { + tableFocused = focus; + final SourceTable table = this.table.get(); + if ( table != null ) + table.setSelectionBackground( focus ); + } + } + + void focusTree( final boolean focus ) + { + if ( focus != treeFocused ) + { + treeFocused = focus; + final SourceGroupTree tree = this.tree.get(); + if ( tree != null ) + tree.setSelectionBackground( focus ); + } + } + + @Override + public void propertyChange( final PropertyChangeEvent evt ) + { + final JPanel tablePanel = this.tablePanel.get(); + final JPanel treePanel = this.treePanel.get(); + if ( tablePanel == null && treePanel == null ) + { + keyboardFocusManager.removePropertyChangeListener( "focusOwner", this ); + return; + } + + if ( evt.getNewValue() instanceof JComponent ) + { + final JComponent component = ( JComponent ) evt.getNewValue(); + for ( int i = 0; i < MAX_DEPTH; ++i ) + { + final Container parent = component.getParent(); + if ( !( parent instanceof JComponent ) ) + break; + + if ( component == treePanel ) + { + focusTable( false ); + focusTree( true ); + return; + } + else if ( component == tablePanel ) + { + focusTable( true ); + focusTree( false ); + return; + } + } + focusTable( false ); + focusTree( false ); + } + } + } +} diff --git a/src/main/java/bdv/ui/CardPanel.java b/src/main/java/bdv/ui/CardPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..7c7c8312450a787ad68d0167641253a4881c67ad --- /dev/null +++ b/src/main/java/bdv/ui/CardPanel.java @@ -0,0 +1,685 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Insets; +import java.awt.LayoutManager; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.geom.AffineTransform; +import java.awt.geom.Path2D; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.swing.Icon; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JToggleButton; +import javax.swing.Scrollable; +import javax.swing.border.EmptyBorder; +import net.miginfocom.swing.MigLayout; + +/** + * CardPanel handles components in named {@code Card}s which can be expanded or collapsed. + * + * @author Tim-Oliver Buchholz, MPI-CBG CSBD, Dresden + * @author Tobias Pietzsch + */ +public class CardPanel +{ + /** + * Color scheme. + */ + private final static Color DEFAULT_CARD_BACKGROUND = Color.white; + + private final static Color DEFAULT_HEADER_BACKGROUND = new Color( 0xcccccc ); + + private final static Color DEFAULT_HEADER_FOREGROUND = new Color( 0x202020 ); + + private Color headerBackground = DEFAULT_HEADER_BACKGROUND; + + private Color headerForeground = DEFAULT_HEADER_FOREGROUND; + + private final Map< Object, Card > cards = new HashMap<>(); + + private final List< Card > cardList = new ArrayList<>(); + + private final Container container; + + /** + * Empty card panel. + */ + public CardPanel() + { + container = new Container( new MigLayout( "fillx, ins 0", "[grow]", "[]0[]" ) ); + container.setBackground( DEFAULT_CARD_BACKGROUND ); + } + + public JComponent getComponent() + { + return container; + } + + /** + * Add a new {@link JComponent} to the card panel. + * + * @param title + * title of the new card. Is shown in the card header. + * This is also used as the key of the new card. + * If a card with this key already exists, no new card will be added! + * @param component + * body of the new card + * @param expanded + * state of the card + * @param insets + * insets of card body + * + * @return if a new card was successfully added + */ + public boolean addCard( final String title, final JComponent component, final boolean expanded, final Insets insets ) + { + return addCard( title, title, component, expanded, insets ); + } + + /** + * Add a new {@link JComponent} to the card panel. + * + * @param key + * key of the new card. + * If a card with this key already exists, no new card will be added! + * @param title + * title of the new card. Is shown in the card header. + * @param component + * body of the new card + * @param expanded + * state of the card + * @param insets + * insets of card body + * + * @return if a new card was successfully added + */ + public synchronized boolean addCard( final Object key, final String title, final JComponent component, final boolean expanded, final Insets insets ) + { + if ( key == null || title == null || component == null ) + throw new NullPointerException(); + + if ( cards.containsKey( key ) ) + return false; + + final Card card = new Card( key, title, component, expanded, insets ); + cards.put( key, card ); + if ( !cardList.isEmpty() ) + cardList.get( cardList.size() - 1 ).setIsLastCard( false ); + cardList.add( card ); + container.add( card, "growx, wrap" ); + container.revalidate(); + return true; + } + + public boolean addCard( final String title, final JComponent component, final boolean expanded ) + { + return addCard( title, title, component, expanded, null ); + } + + public boolean addCard( final Object key, final String title, final JComponent component, final boolean expanded ) + { + return addCard( key, title, component, expanded, null ); + } + + /** + * Remove a card. If the card does not exist, nothing happens. + * + * @param key + * of the card + */ + public synchronized void removeCard( final Object key ) + { + final Card card = cards.remove( key ); + if ( card != null ) + { + cardList.remove( card ); + final Card lastCard = cardList.get( cardList.size() - 1 ); + lastCard.setIsLastCard( true ); + container.remove( card ); + container.revalidate(); + } + } + + /** + * TODO + * + * @param key + * + * @return + */ + public synchronized int indexOf( final Object key ) + { + if ( cards.containsKey( key ) ) + for ( int i = 0; i < cardList.size(); i++ ) + if ( cardList.get( i ).getKey().equals( key ) ) + return i; + return -1; + } + + /** + * Get expanded state of a card. + * + * @param key + * of the card + * + * @return open state + */ + public synchronized boolean isCardExpanded( final Object key ) + { + final Card card = cards.get( key ); + return card != null && card.isExpanded(); + } + + /** + * Set open state of a card + * + * @param key + * of the card + * @param expanded + * new state + */ + public synchronized void setCardExpanded( final Object key, final boolean expanded ) + { + final Card card = cards.get( key ); + if ( card != null && card.isExpanded() != expanded ) + card.setExpanded( expanded ); + } + + /** + * Set the card background color + */ + public void setCardBackground( final Color bg ) + { + container.setBackground( bg ); + for ( Card card : cardList ) + { + card.setBackground( bg ); + card.componentPanel.setBackground( bg ); + } + } + + public Color getCardBackground() + { + return container.getBackground(); + } + + /** + * Set the header background color + */ + public void setHeaderBackground( final Color bg ) + { + headerBackground = bg; + for ( Card card : cardList ) + { + card.headerPanel.setBackground( bg ); + card.terminalResizePanel.setBackground( bg ); + } + } + + public Color getHeaderBackground() + { + return headerBackground; + } + + /** + * Set the header foreground color + */ + public void setHeaderForeground( final Color fg ) + { + headerForeground = fg; + for ( Card card : cardList ) + card.headerPanel.setForeground( fg ); + } + + public Color getHeaderForeground() + { + return headerForeground; + } + + // ================================================================= + + private static final Icon collapsedIcon = new CardCollapseIcon( 10, 10, true, false ); + private static final Icon collapsedMouseOverIcon = new CardCollapseIcon( 10, 10, true, true ); + private static final Icon expandedIcon = new CardCollapseIcon( 10, 10, false, false ); + private static final Icon expandedMouseOverIcon = new CardCollapseIcon( 10, 10, false, true ); + + private static final int RESIZE_HANDLE_HEIGHT = 10; + + private static class TerminalResizePanel extends JPanel + { + public TerminalResizePanel( final JComponent component ) + { + UIUtils.setMinimumHeight( this, 5 ); + UIUtils.setPreferredHeight( this, 5 ); + + final ResizeMouseHandler resizeHandler = new ResizeMouseHandler( this, new Resizable() + { + @Override + public boolean showResizeHandle() + { + return true; + } + + @Override + public JComponent getComponent() + { + return component; + } + } ); + addMouseListener( resizeHandler ); + addMouseMotionListener( resizeHandler ); + } + } + + private class Card extends JPanel + { + private final JPanel componentPanel; + + private final HeaderPanel headerPanel; + + private final TerminalResizePanel terminalResizePanel; + + /** + * Card key. + */ + private final Object key; + + private boolean isLastCard = true; + + public Card( final Object key, final String title, final JComponent component, final boolean open, final Insets insets ) + { + this.key = key; + + this.setLayout( new MigLayout( "fillx, ins 0, hidemode 3", "[grow]", "[]0lp![]" ) ); + this.setBackground( getCardBackground() ); + + final String ins = insets == null ? "ins 4 4 4 0" : String.format( "ins %d %d %d %d", insets.top, insets.left, insets.bottom, insets.right ); + componentPanel = new JPanel( new MigLayout( "fillx, hidemode 3, " + ins, "[grow]", "[grow]0lp![]" ) ); + componentPanel.setBackground( getCardBackground() ); + componentPanel.add( component, "grow, wrap" ); + + terminalResizePanel = new TerminalResizePanel( componentPanel ); + terminalResizePanel.setBackground( getHeaderBackground() ); + + headerPanel = new HeaderPanel( title ); + headerPanel.setBackground( getHeaderBackground() ); + headerPanel.setForeground( getHeaderForeground() ); + + this.add( headerPanel, "growx, wrap" ); + this.add( componentPanel, "growx, wrap" ); + this.add( terminalResizePanel, "growx" ); + this.setExpanded( open ); + } + + private class HeaderPanel extends JPanel + { + private final JPanel labelPanel; + + private final JLabel label; + + @Override + public void setBackground( final Color bg ) + { + super.setBackground( bg ); + if ( labelPanel != null ) + labelPanel.setBackground( bg ); + } + + @Override + public void setForeground( final Color fg ) + { + super.setForeground( fg ); + if ( label != null ) + label.setForeground( fg ); + } + + public HeaderPanel( final String title ) + { + super( new MigLayout( "fillx, aligny center, ins 0 0 0 0", "[][grow]", "" ) ); + UIUtils.setPreferredWidth( this, 100 ); + + // Holds the name with insets. + labelPanel = new JPanel( new MigLayout( "fillx, ins 0 4 0 4", "[grow]", "" ) ); + label = new JLabel( title ); + labelPanel.add( label ); + + final JToggleButton collapseButton = new JToggleButton(); + collapseButton.setIcon( expandedIcon ); + collapseButton.setRolloverIcon( expandedMouseOverIcon ); + collapseButton.setSelectedIcon( collapsedIcon ); + collapseButton.setRolloverSelectedIcon( collapsedMouseOverIcon ); + collapseButton.setFocusable( false ); + collapseButton.setBorder( new EmptyBorder( 5, 5, 5, 5 ) ); + collapseButton.setBorderPainted( false ); + collapseButton.setFocusPainted( false ); + collapseButton.setContentAreaFilled( false ); + + collapseButton.addActionListener( e -> setExpanded( !collapseButton.isSelected() ) ); + + componentPanel.addComponentListener( new ComponentAdapter() + { + @Override + public void componentShown( final ComponentEvent e ) + { + collapseButton.setSelected( false ); + } + + @Override + public void componentHidden( final ComponentEvent e ) + { + collapseButton.setSelected( true ); + } + } ); + + final ResizeMouseHandler resizeHandler = new ResizeMouseHandler( this, new Resizable() + { + @Override + public boolean showResizeHandle() + { + return isCardAboveExpanded(); + } + + @Override + public JComponent getComponent() + { + return getContentOfCardAbove(); + } + } ); + addMouseListener( resizeHandler ); + addMouseMotionListener( resizeHandler ); + + add( collapseButton ); + add( labelPanel, "growx" ); + } + } + + /** + * Get the key of this card. Keys are unique within one CardPanel. + */ + public Object getKey() + { + return key; + } + + public boolean isExpanded() + { + return componentPanel.isVisible(); + } + + public void setExpanded( final boolean open ) + { + componentPanel.setVisible( open ); + terminalResizePanel.setVisible( open && isLastCard ); + componentPanel.revalidate(); + } + + public void setIsLastCard( final boolean isLastCard ) + { + this.isLastCard = isLastCard; + terminalResizePanel.setVisible( componentPanel.isVisible() && isLastCard ); + terminalResizePanel.revalidate(); + } + + @Override + public boolean equals( final Object obj ) + { + return obj instanceof Card && this.key.equals( ( ( Card ) obj ).key ); + } + + @Override + public int hashCode() + { + return key.hashCode(); + } + + private JComponent getContentOfCardAbove() + { + synchronized ( CardPanel.this ) + { + final int i = cardList.indexOf( this ); + if ( i <= 0 ) + return null; + + final JPanel component = cardList.get( i - 1 ).componentPanel; + return component.isVisible() ? component : null; + } + } + + private boolean isCardAboveExpanded() + { + synchronized ( CardPanel.this ) + { + final int i = cardList.indexOf( this ); + if ( i <= 0 ) + return false; + + return cardList.get( i - 1 ).isExpanded(); + } + } + } + + // ================================================================= + + private interface Resizable + { + boolean showResizeHandle(); + + JComponent getComponent(); + } + + private static class ResizeMouseHandler extends MouseAdapter + { + private final JComponent attachedComponent; + + private final Resizable resizable; + + private int oy; + + private int oheight; + + private int minheight; + + private int maxheight; + + private JComponent resizeComponent; + + public ResizeMouseHandler( final JComponent attachedComponent, final Resizable resizable ) + { + this.attachedComponent = attachedComponent; + this.resizable = resizable; + } + + @Override + public void mousePressed( final MouseEvent e ) + { + if ( e.getY() < RESIZE_HANDLE_HEIGHT ) + resizeComponent = resizable.getComponent(); + else + resizeComponent = null; + + if ( resizeComponent != null ) + { + oheight = resizeComponent.getHeight(); + minheight = resizeComponent.getMinimumSize().height; + maxheight = resizeComponent.getMaximumSize().height; + UIUtils.setPreferredHeight( resizeComponent, oheight ); + oy = e.getYOnScreen(); + } + } + + @Override + public void mouseMoved( final MouseEvent e ) + { + if ( e.getY() < RESIZE_HANDLE_HEIGHT && resizable.showResizeHandle() ) + attachedComponent.setCursor( Cursor.getPredefinedCursor( Cursor.N_RESIZE_CURSOR ) ); + else + attachedComponent.setCursor( Cursor.getPredefinedCursor( Cursor.DEFAULT_CURSOR ) ); + } + + @Override + public void mouseExited( final MouseEvent e ) + { + attachedComponent.setCursor( Cursor.getPredefinedCursor( Cursor.DEFAULT_CURSOR ) ); + } + + @Override + public void mouseDragged( final MouseEvent e ) + { + if ( resizeComponent != null ) + { + final int height = oheight + e.getYOnScreen() - oy; + UIUtils.setPreferredHeight( resizeComponent, Math.min( maxheight, Math.max( minheight, height ) ) ); + resizeComponent.revalidate(); + } + } + } + + // ================================================================= + + /** + * Scroll-savvy JPanel that tracks viewport width + */ + private static class Container extends JPanel implements Scrollable + { + public Container( final LayoutManager layout ) + { + super( layout ); + } + + @Override + public Dimension getPreferredScrollableViewportSize() + { + return getPreferredSize(); + } + + @Override + public int getScrollableUnitIncrement( final Rectangle visibleRect, final int orientation, final int direction ) + { + return 16; + } + + @Override + public int getScrollableBlockIncrement( final Rectangle visibleRect, final int orientation, final int direction ) + { + return 10; + } + + @Override + public boolean getScrollableTracksViewportWidth() + { + return true; + } + + @Override + public boolean getScrollableTracksViewportHeight() + { + return false; + } + } + + // ================================================================= + + private static class CardCollapseIcon implements Icon + { + private static final Color mouseOverColor = new Color(0x606060 ); + + private static final Color color = new Color( 0x808080 ); + + private final int width; + + private final int height; + + private final boolean collapsed; + + private final boolean mouseOver; + + public CardCollapseIcon( final int width, final int height, final boolean collapsed, final boolean mouseOver ) + { + this.width = width; + this.height = height; + this.collapsed = collapsed; + this.mouseOver = mouseOver; + } + + @Override + public void paintIcon( final Component c, final Graphics g, final int x, final int y ) + { + final Graphics2D g2d = ( Graphics2D ) g; + g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON ); + + final Path2D.Double path = new Path2D.Double(); + if ( collapsed ) + { + path.moveTo( 0.05, 0.05 ); + path.lineTo( 0.85, 0.5 ); + path.lineTo( 0.05, 0.95 ); + } + else + { + path.moveTo( 0.05, 0.05 ); + path.lineTo( 0.5, 0.85 ); + path.lineTo( 0.95, 0.05 ); + } + path.transform( AffineTransform.getScaleInstance( width, height ) ); + path.transform( AffineTransform.getTranslateInstance( x, y ) ); + + g2d.setColor( mouseOver ? mouseOverColor : color ); + g2d.fill( path ); + } + + @Override + public int getIconWidth() + { + return width; + } + + @Override + public int getIconHeight() + { + return height; + } + } +} diff --git a/src/main/java/bdv/ui/MouseWheelScrollListener.java b/src/main/java/bdv/ui/MouseWheelScrollListener.java new file mode 100644 index 0000000000000000000000000000000000000000..fa6b880c392826e9b6b0542f477022e09ebf17cd --- /dev/null +++ b/src/main/java/bdv/ui/MouseWheelScrollListener.java @@ -0,0 +1,98 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package bdv.ui; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; + +/** + * Passes mouse wheel events to the parent component if this component + * cannot scroll further in the given direction. + * <p> + * This behavior is a little better than Swing's default behavior but + * still worse than the behavior of Google Chrome, which remembers the + * currently scrolling component and sticks to it until a timeout happens. + * + * @see <a href="https://stackoverflow.com/a/53687022">Stack Overflow</a> + */ +final class MouseWheelScrollListener implements MouseWheelListener +{ + + private final JScrollPane pane; + + private int previousValue; + + private boolean parentSearched = false; + + private Component parent = null; + + public MouseWheelScrollListener( JScrollPane pane ) + { + this.pane = pane; + previousValue = pane.getVerticalScrollBar().getValue(); + } + + public void mouseWheelMoved( MouseWheelEvent e ) + { + + if ( !parentSearched ) + { + if ( !searchParentScrollPane() ) + return; + } + if ( parent == null ) + return; + JScrollBar bar = pane.getVerticalScrollBar(); + int limit = e.getWheelRotation() < 0 ? 0 : bar.getMaximum() - bar.getVisibleAmount(); + if ( previousValue == limit && bar.getValue() == limit ) + { + parent.dispatchEvent( SwingUtilities.convertMouseEvent( pane, e, parent ) ); + } + previousValue = bar.getValue(); + } + + private boolean searchParentScrollPane() + { + parentSearched = true; + Component parent = pane.getParent(); + while ( !( parent instanceof JScrollPane ) ) + { + if ( parent == null ) + { + return false; + } + parent = parent.getParent(); + } + this.parent = parent; + return true; + } +} diff --git a/src/main/java/bdv/ui/SourcesTransferable.java b/src/main/java/bdv/ui/SourcesTransferable.java new file mode 100644 index 0000000000000000000000000000000000000000..596e9e8b48541f46a55e80ecf48855214e5863af --- /dev/null +++ b/src/main/java/bdv/ui/SourcesTransferable.java @@ -0,0 +1,89 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui; + +import bdv.viewer.SourceAndConverter; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +/** + * Drag and Drop {@code Transferable} for a collection of sources. + */ +public class SourcesTransferable implements Transferable +{ + public static final DataFlavor flavor = new DataFlavor( SourceList.class, DataFlavor.javaJVMLocalObjectMimeType ); + + static final DataFlavor[] transferDataFlavors = { flavor }; + + public static class SourceList + { + private final ArrayList< SourceAndConverter< ? > > sources; + + public List< SourceAndConverter< ? > > getSources() + { + return sources; + } + + SourceList( final Collection< SourceAndConverter< ? > > sources ) + { + this.sources = new ArrayList<>( sources ); + } + } + + private final SourceList sources; + + public SourcesTransferable( final Collection< SourceAndConverter< ? > > sources ) + { + this.sources = new SourceList( sources ); + } + + @Override + public DataFlavor[] getTransferDataFlavors() + { + return transferDataFlavors; + } + + @Override + public boolean isDataFlavorSupported( final DataFlavor flavor ) + { + return Objects.equals( flavor, this.flavor ); + } + + @Override + public Object getTransferData( final DataFlavor flavor ) throws UnsupportedFlavorException, IOException + { + return sources; + } +} diff --git a/src/main/java/bdv/ui/UIUtils.java b/src/main/java/bdv/ui/UIUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..696668013b20a972726421100c3e8fc99ac63440 --- /dev/null +++ b/src/main/java/bdv/ui/UIUtils.java @@ -0,0 +1,85 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; + +/** + * AWT/Swing helpers. + * + * @author Tobias Pietzsch + */ +public class UIUtils +{ + /** + * Mix colors {@code c1} and {@code c2} by ratios {@code c1Weight} and {@code (1-c1Weight)}, respectively. + */ + public static Color mix( final Color c1, final Color c2, final double c1Weight ) + { + final double c2Weight = 1.0 - c1Weight; + return new Color( + ( int ) ( c1.getRed() * c1Weight + c2.getRed() * c2Weight ), + ( int ) ( c1.getGreen() * c1Weight + c2.getGreen() * c2Weight ), + ( int ) ( c1.getBlue() * c1Weight + c2.getBlue() * c2Weight ) ); + } + + /** + * Set the preferred width of a component (leaving the preferred height untouched). + */ + public static void setPreferredWidth( final Component component, final int preferredWidth ) + { + component.setPreferredSize( new Dimension( preferredWidth, component.getPreferredSize().height ) ); + } + + /** + * Set the preferred height of a component (leaving the preferred width untouched). + */ + public static void setPreferredHeight( final Component component, final int preferredHeight ) + { + component.setPreferredSize( new Dimension( component.getPreferredSize().width, preferredHeight ) ); + } + + /** + * Set the minimum width of a component (leaving the minimum height untouched). + */ + public static void setMinimumWidth( final Component component, final int minimumWidth ) + { + component.setMinimumSize( new Dimension( minimumWidth, component.getMinimumSize().height ) ); + } + + /** + * Set the minimum height of a component (leaving the minimum width untouched). + */ + public static void setMinimumHeight( final Component component, final int minimumHeight ) + { + component.setMinimumSize( new Dimension( component.getMinimumSize().width, minimumHeight ) ); + } +} diff --git a/src/main/java/bdv/ui/convertersetupeditor/BoundedRangeEditor.java b/src/main/java/bdv/ui/convertersetupeditor/BoundedRangeEditor.java new file mode 100644 index 0000000000000000000000000000000000000000..e7ddc89beed218525e6d35625fe4917c537a0d38 --- /dev/null +++ b/src/main/java/bdv/ui/convertersetupeditor/BoundedRangeEditor.java @@ -0,0 +1,232 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.convertersetupeditor; + +import bdv.tools.brightness.ConverterSetup; +import bdv.viewer.ConverterSetupBounds; +import bdv.viewer.ConverterSetups; +import bdv.ui.sourcetable.SourceTable; +import bdv.util.BoundedRange; +import bdv.util.Bounds; +import bdv.ui.sourcegrouptree.SourceGroupTree; +import bdv.ui.UIUtils; +import java.awt.Color; +import java.util.List; +import java.util.function.Supplier; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.SwingUtilities; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; + +class BoundedRangeEditor +{ + private final Supplier< List< ConverterSetup > > selectedConverterSetups; + + private final BoundedRangePanel rangePanel; + + private final ConverterSetupBounds converterSetupBounds; + + private final Color equalColor; + + private final Color notEqualColor; + + public BoundedRangeEditor( + final SourceTable table, + final ConverterSetups converterSetups, + final BoundedRangePanel rangePanel, + final ConverterSetupBounds converterSetupBounds ) + { + this( table::getSelectedConverterSetups, converterSetups, rangePanel, converterSetupBounds ); + table.getSelectionModel().addListSelectionListener( e -> updateSelection() ); + } + + public BoundedRangeEditor( + final SourceGroupTree tree, + final ConverterSetups converterSetups, + final BoundedRangePanel rangePanel, + final ConverterSetupBounds converterSetupBounds ) + { + this( + () -> converterSetups.getConverterSetups( tree.getSelectedSources() ), + converterSetups, rangePanel, converterSetupBounds ); + tree.getSelectionModel().addTreeSelectionListener( e -> updateSelection() ); + tree.getModel().addTreeModelListener( new TreeModelListener() + { + @Override + public void treeNodesChanged( final TreeModelEvent e ) + { + updateSelection(); + } + + @Override + public void treeNodesInserted( final TreeModelEvent e ) + { + updateSelection(); + } + + @Override + public void treeNodesRemoved( final TreeModelEvent e ) + { + updateSelection(); + } + + @Override + public void treeStructureChanged( final TreeModelEvent e ) + { + updateSelection(); + } + } ); + } + + private BoundedRangeEditor( + final Supplier< List< ConverterSetup > > selectedConverterSetups, + final ConverterSetups converterSetups, + final BoundedRangePanel rangePanel, final ConverterSetupBounds converterSetupBounds ) + { + this.selectedConverterSetups = selectedConverterSetups; + this.rangePanel = rangePanel; + this.converterSetupBounds = converterSetupBounds; + + rangePanel.changeListeners().add( this::updateConverterSetupRanges ); + + converterSetups.listeners().add( s -> updateRangePanel() ); + + equalColor = rangePanel.getBackground(); + notEqualColor = UIUtils.mix( equalColor, Color.red, 0.9 ); + + final JPopupMenu menu = new JPopupMenu(); + menu.add( runnableItem( "set bounds ...", rangePanel::setBoundsDialog ) ); + menu.add( setBoundsItem( "set bounds 0..1", 0, 1 ) ); + menu.add( setBoundsItem( "set bounds 0..255", 0, 255 ) ); + menu.add( setBoundsItem( "set bounds 0..65535", 0, 65535 ) ); + menu.add( runnableItem( "shrink bounds to selection", rangePanel::shrinkBoundsToRange ) ); + rangePanel.setPopup( () -> menu ); + + updateRangePanel(); + } + + private JMenuItem setBoundsItem( final String text, final double min, final double max ) + { + final JMenuItem item = new JMenuItem( text ); + item.addActionListener( e -> setBounds( new Bounds( min, max ) ) ); + return item; + } + + private JMenuItem runnableItem( final String text, final Runnable action ) + { + final JMenuItem item = new JMenuItem( text ); + item.addActionListener( e -> action.run() ); + return item; + } + + private boolean blockUpdates = false; + + private List< ConverterSetup > converterSetups; + + private synchronized void setBounds( final Bounds bounds ) + { + if ( converterSetups == null || converterSetups.isEmpty() ) + return; + + for ( final ConverterSetup converterSetup : converterSetups ) + { + converterSetupBounds.setBounds( converterSetup, bounds ); + } + + updateRangePanel(); + } + + private synchronized void updateConverterSetupRanges() + { + if ( blockUpdates || converterSetups == null || converterSetups.isEmpty() ) + return; + + final BoundedRange range = rangePanel.getRange(); + + for ( final ConverterSetup converterSetup : converterSetups ) + { + converterSetup.setDisplayRange( range.getMin(), range.getMax() ); + converterSetupBounds.setBounds( converterSetup, range.getBounds() ); + } + + updateRangePanel(); + } + + private synchronized void updateSelection() + { + converterSetups = selectedConverterSetups.get(); + updateRangePanel(); + } + + private synchronized void updateRangePanel() + { + if ( converterSetups == null || converterSetups.isEmpty() ) + { + SwingUtilities.invokeLater( () -> { + rangePanel.setEnabled( false ); + rangePanel.setBackground( equalColor ); + } ); + } + else + { + BoundedRange range = null; + boolean allRangesEqual = true; + for ( final ConverterSetup converterSetup : converterSetups ) + { + final Bounds bounds = converterSetupBounds.getBounds( converterSetup ); + final double minBound = bounds.getMinBound(); + final double maxBound = bounds.getMaxBound(); + final double min = converterSetup.getDisplayRangeMin(); + final double max = converterSetup.getDisplayRangeMax(); + + final BoundedRange converterSetupRange = new BoundedRange( minBound, maxBound, min, max ); + if ( range == null ) + range = converterSetupRange; + else + { + allRangesEqual &= range.equals( converterSetupRange ); + range = range.join( converterSetupRange ); + } + } + final BoundedRange finalRange = range; + final Color bg = allRangesEqual ? equalColor : notEqualColor; + SwingUtilities.invokeLater( () -> { + synchronized ( BoundedRangeEditor.this ) + { + blockUpdates = true; + rangePanel.setEnabled( true ); + rangePanel.setRange( finalRange ); + rangePanel.setBackground( bg ); + blockUpdates = false; + } + } ); + } + } +} diff --git a/src/main/java/bdv/ui/convertersetupeditor/BoundedRangePanel.java b/src/main/java/bdv/ui/convertersetupeditor/BoundedRangePanel.java new file mode 100644 index 0000000000000000000000000000000000000000..b4e49fa2699b89f1fb446ecfdfdc20f7888db995 --- /dev/null +++ b/src/main/java/bdv/ui/convertersetupeditor/BoundedRangePanel.java @@ -0,0 +1,412 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.convertersetupeditor; + +import java.awt.Color; +import java.awt.Font; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.text.DecimalFormat; +import java.util.Objects; +import java.util.function.Supplier; + +import javax.swing.JFormattedTextField; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.event.ChangeEvent; +import javax.swing.text.DefaultFormatterFactory; +import javax.swing.text.NumberFormatter; + +import net.miginfocom.swing.MigLayout; + +import org.scijava.listeners.Listeners; + +import bdv.ui.UIUtils; +import bdv.ui.rangeslider.RangeSlider; +import bdv.util.BoundedRange; + +/** + * A {@code JPanel} with a range slider, min/max spinners, and a range bounds + * display (for setting {@code ConverterSetup} display range). + * + * @author Tobias Pietzsch + */ +class BoundedRangePanel extends JPanel +{ + private Supplier< JPopupMenu > popup; + + public interface ChangeListener + { + void boundedRangeChanged(); + } + + private BoundedRange range; + + /** + * The range slider. + */ + private final RangeSlider rangeSlider; + + /** + * Range slider number of steps. + */ + private static final int SLIDER_LENGTH = 10000; + + /** + * The minimum spinner. + */ + private final JSpinner minSpinner; + + /** + * The maximum spinner. + */ + private final JSpinner maxSpinner; + + private final JLabel upperBoundLabel; + + private final JLabel lowerBoundLabel; + + private final Listeners.List< ChangeListener > listeners = new Listeners.SynchronizedList<>(); + + public BoundedRangePanel() + { + this( new BoundedRange( 0, 1, 0, 0.5 ) ); + } + + public BoundedRangePanel( final BoundedRange range ) + { + setLayout( new MigLayout( "ins 5 5 5 10, fillx, filly, hidemode 3", "[][grow][][]", "[]0[]" ) ); + + minSpinner = new JSpinner( new SpinnerNumberModel( 0.0, 0.0, 1.0, 1.0 ) ); + maxSpinner = new JSpinner( new SpinnerNumberModel( 1.0, 0.0, 1.0, 1.0 ) ); + rangeSlider = new RangeSlider( 0, SLIDER_LENGTH ); + upperBoundLabel = new JLabel(); + lowerBoundLabel = new JLabel(); + + setupMinSpinner(); + setupMaxSpinner(); + setupRangeSlider(); + setupBoundLabels(); + setupPopupMenu(); + + this.add( minSpinner, "sy 2" ); + this.add( rangeSlider, "growx, sy 2" ); + this.add( maxSpinner, "sy 2" ); + this.add( upperBoundLabel, "right, wrap" ); + this.add( lowerBoundLabel, "right" ); + + setRange( range ); + } + + @Override + public void setEnabled( final boolean enabled ) + { + super.setEnabled( enabled ); + if ( minSpinner != null ) + minSpinner.setEnabled( enabled ); + if ( rangeSlider != null ) + rangeSlider.setEnabled( enabled ); + if ( maxSpinner != null ) + maxSpinner.setEnabled( enabled ); + if ( upperBoundLabel != null ) + upperBoundLabel.setEnabled( enabled ); + if ( lowerBoundLabel != null ) + lowerBoundLabel.setEnabled( enabled ); + } + + @Override + public void setBackground( final Color bg ) + { + super.setBackground( bg ); + if ( minSpinner != null ) + minSpinner.setBackground( bg ); + if ( rangeSlider != null ) + rangeSlider.setBackground( bg ); + if ( maxSpinner != null ) + maxSpinner.setBackground( bg ); + if ( upperBoundLabel != null ) + upperBoundLabel.setBackground( bg ); + if ( lowerBoundLabel != null ) + lowerBoundLabel.setBackground( bg ); + } + + private static class UnboundedNumberEditor extends JSpinner.NumberEditor + { + public UnboundedNumberEditor( final JSpinner spinner ) + { + super( spinner ); + final JFormattedTextField ftf = getTextField(); + final DecimalFormat format = ( DecimalFormat ) ( ( NumberFormatter ) ftf.getFormatter() ).getFormat(); + final NumberFormatter formatter = new NumberFormatter( format ); + formatter.setValueClass( spinner.getValue().getClass() ); + final DefaultFormatterFactory factory = new DefaultFormatterFactory( formatter ); + ftf.setFormatterFactory( factory ); + } + } + + private void setupMinSpinner() + { + UIUtils.setPreferredWidth( minSpinner, 70 ); + + minSpinner.addChangeListener( e -> { + final double value = ( Double ) minSpinner.getValue(); + if ( value != range.getMin() ) + updateRange( range.withMin( value ) ); + } ); + + minSpinner.setEditor( new UnboundedNumberEditor( minSpinner ) ); + } + + private void setupMaxSpinner() + { + UIUtils.setPreferredWidth( maxSpinner, 70 ); + + maxSpinner.addChangeListener( e -> { + final double value = ( Double ) maxSpinner.getValue(); + if ( value != range.getMax() ) + updateRange( range.withMax( value ) ); + } ); + + maxSpinner.setEditor( new UnboundedNumberEditor( maxSpinner ) ); + } + + private void setupRangeSlider() + { + UIUtils.setPreferredWidth( rangeSlider, 50 ); + rangeSlider.setRange( 0, SLIDER_LENGTH ); + rangeSlider.setFocusable( false ); + + rangeSlider.addChangeListener( e -> { + updateRange( range.withMin( posToValue( rangeSlider.getValue() ) ).withMax( posToValue( rangeSlider.getUpperValue() ) ) ); + } ); + + rangeSlider.addComponentListener( new ComponentAdapter() + { + @Override + public void componentResized( final ComponentEvent e ) + { + updateNumberFormat(); + } + } ); + } + + private void setupBoundLabels() + { + final Font font = upperBoundLabel.getFont().deriveFont( 10f ); + upperBoundLabel.setFont( font ); + lowerBoundLabel.setFont( font ); + upperBoundLabel.setBorder( null ); + lowerBoundLabel.setBorder( null ); + } + + private void setupPopupMenu() + { + final MouseListener ml = new MouseAdapter() + { + @Override + public void mousePressed( final MouseEvent e ) + { + if ( e.isPopupTrigger() || + ( e.getButton() == MouseEvent.BUTTON1 && e.getX() > upperBoundLabel.getX() ) ) + doPop( e ); + } + + @Override + public void mouseReleased( final MouseEvent e ) + { + if ( e.isPopupTrigger() ) + doPop( e ); + } + + private void doPop( final MouseEvent e ) + { + if ( isEnabled() && popup != null ) + { + final JPopupMenu menu = popup.get(); + if ( menu != null ) + menu.show( e.getComponent(), e.getX(), e.getY() ); + } + } + }; + this.addMouseListener( ml ); + rangeSlider.addMouseListener( ml ); + } + + /** + * Convert range-slider position to value. + * + * @param pos + * of range-slider + */ + private double posToValue( final int pos ) + { + final double dmin = range.getMinBound(); + final double dmax = range.getMaxBound(); + return ( pos * ( dmax - dmin ) / SLIDER_LENGTH ) + dmin; + } + + /** + * Convert value to range-slider position. + */ + private int valueToPos( final double value ) + { + final double dmin = range.getMinBound(); + final double dmax = range.getMaxBound(); + return ( int ) Math.round( ( value - dmin ) * SLIDER_LENGTH / ( dmax - dmin ) ); + } + + private synchronized void updateNumberFormat() + { +// if ( userDefinedNumberFormat ) +// return; + + final int sw = rangeSlider.getWidth(); + if ( sw > 0 ) + { + final double vrange = range.getMaxBound() - range.getMinBound(); + final int digits = ( int ) Math.ceil( Math.log10( sw / vrange ) ); + + blockUpdates = true; + + JSpinner.NumberEditor numberEditor = ( ( JSpinner.NumberEditor ) minSpinner.getEditor() ); + numberEditor.getFormat().setMaximumFractionDigits( digits ); + numberEditor.stateChanged( new ChangeEvent( minSpinner ) ); + + numberEditor = ( ( JSpinner.NumberEditor ) maxSpinner.getEditor() ); + numberEditor.getFormat().setMaximumFractionDigits( digits ); + numberEditor.stateChanged( new ChangeEvent( maxSpinner ) ); + + blockUpdates = false; + } + } + + private synchronized void updateRange( final BoundedRange newRange ) + { + if ( !blockUpdates ) + setRange( newRange ); + } + + private boolean blockUpdates = false; + + public synchronized void setRange( final BoundedRange range ) + { + if ( Objects.equals( this.range, range ) ) + return; + + this.range = range; + + blockUpdates = true; + + final double minBound = range.getMinBound(); + final double maxBound = range.getMaxBound(); + + final SpinnerNumberModel minSpinnerModel = ( SpinnerNumberModel ) minSpinner.getModel(); + minSpinnerModel.setMinimum( minBound ); + minSpinnerModel.setMaximum( maxBound ); + minSpinnerModel.setValue( range.getMin() ); + + final SpinnerNumberModel maxSpinnerModel = ( SpinnerNumberModel ) maxSpinner.getModel(); + maxSpinnerModel.setMinimum( minBound ); + maxSpinnerModel.setMaximum( maxBound ); + maxSpinnerModel.setValue( range.getMax() ); + + rangeSlider.setRange( valueToPos( range.getMin() ), valueToPos( range.getMax() ) ); + + final double frac = Math.max( + Math.abs( Math.round( minBound ) - minBound ), + Math.abs( Math.round( maxBound ) - maxBound ) ); + final String format = frac > 0.005 ? "%.2f" : "%.0f"; + upperBoundLabel.setText( String.format( format, maxBound ) ); + lowerBoundLabel.setText( String.format( format, minBound ) ); + this.invalidate(); + + blockUpdates = false; + + listeners.list.forEach( ChangeListener::boundedRangeChanged ); + } + + public BoundedRange getRange() + { + return range; + } + + public Listeners< ChangeListener > changeListeners() + { + return listeners; + } + + public void setPopup( final Supplier< JPopupMenu > popup ) + { + this.popup = popup; + } + + public void shrinkBoundsToRange() + { + updateRange( range.withMinBound( range.getMin() ).withMaxBound( range.getMax() ) ); + } + + public void setBoundsDialog() + { + final JPanel panel = new JPanel( new MigLayout( "fillx", "[][grow]", "" ) ); + final JSpinner minSpinner = new JSpinner( new SpinnerNumberModel( 0.0, 0.0, 1.0, 1.0 ) ); + final JSpinner maxSpinner = new JSpinner( new SpinnerNumberModel( 0.0, 0.0, 1.0, 1.0 ) ); + minSpinner.setEditor( new UnboundedNumberEditor( minSpinner ) ); + maxSpinner.setEditor( new UnboundedNumberEditor( maxSpinner ) ); + minSpinner.setValue( range.getMinBound() ); + maxSpinner.setValue( range.getMaxBound() ); + minSpinner.addChangeListener( e -> { + final double value = ( Double ) minSpinner.getValue(); + if ( value > ( Double ) maxSpinner.getValue() ) + maxSpinner.setValue( value ); + } ); + maxSpinner.addChangeListener( e -> { + final double value = ( Double ) maxSpinner.getValue(); + if ( value < ( Double ) minSpinner.getValue() ) + minSpinner.setValue( value ); + } ); + panel.add( "right", new JLabel( "min" ) ); + panel.add( "growx, wrap", minSpinner ); + panel.add( "right", new JLabel( "max" ) ); + panel.add( "growx", maxSpinner ); + final int result = JOptionPane.showConfirmDialog( null, panel, "Set Bounds", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE ); + if ( result == JOptionPane.YES_OPTION ) + { + final double min = ( Double ) minSpinner.getValue(); + final double max = ( Double ) maxSpinner.getValue(); + updateRange( range.withMinBound( min ).withMaxBound( max ) ); + } + } +} diff --git a/src/main/java/bdv/ui/convertersetupeditor/ColorEditor.java b/src/main/java/bdv/ui/convertersetupeditor/ColorEditor.java new file mode 100644 index 0000000000000000000000000000000000000000..21c608418d3a5a098ecd6faeb890bf9915eee9ba --- /dev/null +++ b/src/main/java/bdv/ui/convertersetupeditor/ColorEditor.java @@ -0,0 +1,179 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.convertersetupeditor; + +import bdv.tools.brightness.ConverterSetup; +import bdv.viewer.ConverterSetups; +import bdv.ui.sourcetable.SourceTable; +import bdv.ui.sourcegrouptree.SourceGroupTree; +import bdv.ui.UIUtils; +import java.awt.Color; +import java.util.List; +import java.util.function.Supplier; +import javax.swing.SwingUtilities; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import net.imglib2.type.numeric.ARGBType; + +class ColorEditor +{ + private final Supplier< List< ConverterSetup > > selectedConverterSetups; + + private final ColorPanel colorPanel; + + private final Color equalColor; + + private final Color notEqualColor; + + public ColorEditor( + final SourceTable table, + final ConverterSetups converterSetups, + final ColorPanel colorPanel ) + { + this( table::getSelectedConverterSetups, converterSetups, colorPanel ); + table.getSelectionModel().addListSelectionListener( e -> updateSelection() ); + } + + public ColorEditor( + final SourceGroupTree tree, + final ConverterSetups converterSetups, + final ColorPanel colorPanel ) + { + this( + () -> converterSetups.getConverterSetups( tree.getSelectedSources() ), + converterSetups, colorPanel ); + tree.getSelectionModel().addTreeSelectionListener( e -> updateSelection() ); + tree.getModel().addTreeModelListener( new TreeModelListener() + { + @Override + public void treeNodesChanged( final TreeModelEvent e ) + { + updateSelection(); + } + + @Override + public void treeNodesInserted( final TreeModelEvent e ) + { + updateSelection(); + } + + @Override + public void treeNodesRemoved( final TreeModelEvent e ) + { + updateSelection(); + } + + @Override + public void treeStructureChanged( final TreeModelEvent e ) + { + updateSelection(); + } + } ); + } + + private ColorEditor( + final Supplier< List< ConverterSetup > > selectedConverterSetups, + final ConverterSetups converterSetups, + final ColorPanel colorPanel ) + { + this.selectedConverterSetups = selectedConverterSetups; + this.colorPanel = colorPanel; + + colorPanel.changeListeners().add( this::updateConverterSetupColors ); + converterSetups.listeners().add( s -> updateColorPanel() ); + + equalColor = colorPanel.getBackground(); + notEqualColor = UIUtils.mix( equalColor, Color.red, 0.9 ); + } + + private boolean blockUpdates = false; + + private List< ConverterSetup > converterSetups; + + private synchronized void updateConverterSetupColors() + { + if ( blockUpdates || converterSetups == null || converterSetups.isEmpty() ) + return; + + ARGBType color = colorPanel.getColor(); + + for ( final ConverterSetup converterSetup : converterSetups ) + { + if ( converterSetup.supportsColor() ) + converterSetup.setColor( color ); + } + + updateColorPanel(); + } + + private synchronized void updateSelection() + { + converterSetups = selectedConverterSetups.get(); + updateColorPanel(); + } + + private synchronized void updateColorPanel() + { + if ( converterSetups == null || converterSetups.isEmpty() ) + { + SwingUtilities.invokeLater( () -> { + colorPanel.setEnabled( false ); + colorPanel.setColor( null ); + colorPanel.setBackground( equalColor ); + } ); + } + else + { + ARGBType color = null; + boolean allColorsEqual = true; + for ( final ConverterSetup converterSetup : converterSetups ) + { + if ( converterSetup.supportsColor() ) + { + if ( color == null ) + color = converterSetup.getColor(); + else + allColorsEqual &= color.equals( converterSetup.getColor() ); + } + } + final ARGBType finalColor = color; + final Color bg = allColorsEqual ? equalColor : notEqualColor; + SwingUtilities.invokeLater( () -> { + synchronized ( ColorEditor.this ) + { + blockUpdates = true; + colorPanel.setEnabled( finalColor != null ); + colorPanel.setColor( finalColor ); + colorPanel.setBackground( bg ); + blockUpdates = false; + } + } ); + } + } +} diff --git a/src/main/java/bdv/ui/convertersetupeditor/ColorPanel.java b/src/main/java/bdv/ui/convertersetupeditor/ColorPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..9b7297ae29085260b53400e8331b4ff0b94f0bfc --- /dev/null +++ b/src/main/java/bdv/ui/convertersetupeditor/ColorPanel.java @@ -0,0 +1,115 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.convertersetupeditor; + +import java.awt.Color; + +import java.awt.Dimension; +import javax.swing.JButton; +import javax.swing.JColorChooser; +import javax.swing.JPanel; + +import net.imglib2.type.numeric.ARGBType; +import net.miginfocom.swing.MigLayout; + +import org.scijava.listeners.Listeners; + +import bdv.tools.brightness.ColorIcon; + +/** + * A {@code JPanel} with a color button (for setting {@code ConverterSetup} + * colors). + * + * @author Tobias Pietzsch + */ +class ColorPanel extends JPanel +{ + private final JButton colorButton; + + private final ARGBType color = new ARGBType(); + + public interface ChangeListener + { + void colorChanged(); + } + + private final Listeners.List< ChangeListener > listeners = new Listeners.SynchronizedList<>(); + + public ColorPanel() + { + setLayout( new MigLayout( "ins 0, fillx, filly, hidemode 3", "[grow]", "" ) ); + colorButton = new JButton(); + this.add( colorButton, "center" ); + + colorButton.addActionListener( e -> chooseColor() ); + + colorButton.setBorderPainted( false ); + colorButton.setFocusPainted( false ); + colorButton.setContentAreaFilled( false ); + colorButton.setMinimumSize( new Dimension( 46, 42 ) ); + colorButton.setPreferredSize( new Dimension( 46, 42 ) ); + setColor( null ); + } + + @Override + public void setEnabled( final boolean enabled ) + { + super.setEnabled( enabled ); + if ( colorButton != null ) + colorButton.setEnabled( enabled ); + } + + private void chooseColor() + { + final Color newColor = JColorChooser.showDialog( null, "Set Source Color", new Color( color.get() ) ); + if ( newColor == null ) + return; + setColor( new ARGBType( newColor.getRGB() | 0xff000000 ) ); + listeners.list.forEach( ChangeListener::colorChanged ); + }; + + public Listeners< ChangeListener > changeListeners() + { + return listeners; + } + + public synchronized void setColor( final ARGBType color ) + { + if ( color == null ) + this.color.set( 0xffaaaaaa ); + else + this.color.set( color ); + colorButton.setIcon( new ColorIcon( new Color( this.color.get() ), 30, 30, 10, 10, true ) ); + } + + public ARGBType getColor() + { + return color.copy(); + } +} diff --git a/src/main/java/bdv/ui/convertersetupeditor/ConverterSetupEditPanel.java b/src/main/java/bdv/ui/convertersetupeditor/ConverterSetupEditPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..b2b2cb9cca076a92385e740b3d2ff89281bd787a --- /dev/null +++ b/src/main/java/bdv/ui/convertersetupeditor/ConverterSetupEditPanel.java @@ -0,0 +1,82 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.convertersetupeditor; + +import bdv.viewer.ConverterSetupBounds; +import javax.swing.JPanel; + +import net.miginfocom.swing.MigLayout; + +import bdv.viewer.ConverterSetups; +import bdv.ui.sourcetable.SourceTable; +import bdv.ui.sourcegrouptree.SourceGroupTree; + +/** + * A {@code JPanel} containing a {@link ColorPanel} and a + * {@link BoundedRangePanel}. It can be constructed for a {@link SourceTable} or + * a {@link SourceGroupTree}, and will be set up to edit the selected + * source/groups respectively. + * + * @author Tobias Pietzsch + */ +public class ConverterSetupEditPanel extends JPanel +{ + private final ColorPanel colorPanel; + + private final BoundedRangePanel rangePanel; + + public ConverterSetupEditPanel( + final SourceGroupTree tree, + final ConverterSetups converterSetups ) + { + this(); + new BoundedRangeEditor( tree, converterSetups, rangePanel, converterSetups.getBounds() ); + new ColorEditor( tree, converterSetups, colorPanel ); + } + + public ConverterSetupEditPanel( + final SourceTable table, + final ConverterSetups converterSetups ) + { + this(); + new BoundedRangeEditor( table, converterSetups, rangePanel, converterSetups.getBounds() ); + new ColorEditor( table, converterSetups, colorPanel ); + } + + public ConverterSetupEditPanel() + { + super( new MigLayout( "ins 0, fillx, hidemode 3", "[]0[grow]", "" ) ); + colorPanel = new ColorPanel(); + rangePanel = new BoundedRangePanel(); + + ( ( MigLayout ) rangePanel.getLayout() ).setLayoutConstraints( "ins 5 5 5 10, fillx, filly, hidemode 3" ); + add( colorPanel, "growy" ); + add( rangePanel, "grow" ); + } +} diff --git a/src/main/java/bdv/ui/rangeslider/RangeSlider.java b/src/main/java/bdv/ui/rangeslider/RangeSlider.java new file mode 100644 index 0000000000000000000000000000000000000000..68fae9ebc158d0b6525aed41fdc8194ff8a04e49 --- /dev/null +++ b/src/main/java/bdv/ui/rangeslider/RangeSlider.java @@ -0,0 +1,184 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.rangeslider; + +import javax.swing.JSlider; + +/** + * An extension of JSlider to select a range of values using two thumb controls. + * The thumb controls are used to select the lower and upper value of a range + * with predetermined minimum and maximum values. + * <p> + * Note that RangeSlider makes use of the default BoundedRangeModel, which + * supports an inner range defined by a value and an extent. The upper value + * returned by RangeSlider is simply the lower value plus the extent. + * <p> + * This is copied from https://github.com/ernieyu/Swing-range-slider with the + * following changes: + * <ul> + * <li>The slider thumbs push each other. That is, the upper thumb can be + * dragged below the lower thumb, and it will drag the lower thumb with it, and + * vice versa</li> + * <li>The {@link #setRange(int, int)} method was added, which sets both the + * lower and upper values simultaneously.</li> + * </ul> + * <p> + * + * <pre> + * ============================================================================= + * + * The MIT License Copyright (c) 2010 Ernest Yu. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * </pre> + */ +public class RangeSlider extends JSlider +{ + private static final long serialVersionUID = 1L; + + /** + * Constructs a RangeSlider with default minimum and maximum values of 0 and + * 100. + */ + public RangeSlider() + { + initSlider(); + } + + /** + * Constructs a RangeSlider with the specified default minimum and maximum + * values. + */ + public RangeSlider( final int min, final int max ) + { + super( min, max ); + initSlider(); + } + + /** + * Initializes the slider by setting default properties. + */ + private void initSlider() + { + setOrientation( HORIZONTAL ); + } + + /** + * Overrides the superclass method to install the UI delegate to draw two + * thumbs. + */ + @Override + public void updateUI() + { + setUI( new RangeSliderUI( this ) ); + // Update UI for slider labels. This must be called after updating the + // UI of the slider. Refer to JSlider.updateUI(). + updateLabelUIs(); + } + + /** + * Returns the lower value in the range. + */ + @Override + public int getValue() + { + return super.getValue(); + } + + /** + * Sets the lower value in the range. + */ + @Override + public void setValue( final int value ) + { + final int oldValue = getValue(); + if ( oldValue == value ) + { return; } + + // Compute new value and extent to maintain upper value. + final int oldExtent = getExtent(); + final int newValue = Math.min( Math.max( getMinimum(), value ), oldValue + oldExtent ); + final int newExtent = oldExtent + oldValue - newValue; + + // Set new value and extent, and fire a single change event. + getModel().setRangeProperties( newValue, newExtent, getMinimum(), + getMaximum(), getValueIsAdjusting() ); + } + + /** + * Returns the upper value in the range. + */ + public int getUpperValue() + { + return getValue() + getExtent(); + } + + /** + * Sets the upper value in the range. + */ + public void setUpperValue( final int value ) + { + // Compute new extent. + final int lowerValue = getValue(); + final int newExtent = Math.min( Math.max( 0, value - lowerValue ), getMaximum() - lowerValue ); + + // Set extent to set upper value. + setExtent( newExtent ); + } + + /** + * Sets both the lower and upper values in the range. + */ + public void setRange( final int lower, final int upper ) + { + final int newValue = Math.max( getMinimum(), Math.min( getMaximum(), lower ) ); + final int newUpper = Math.max( newValue, Math.min( getMaximum(), upper ) ); + final int newExtent = newUpper - newValue; + + // Set new value and extent, and fire a single change event. + getModel().setRangeProperties( newValue, newExtent, getMinimum(), + getMaximum(), getValueIsAdjusting() ); + } +} diff --git a/src/main/java/bdv/ui/rangeslider/RangeSliderUI.java b/src/main/java/bdv/ui/rangeslider/RangeSliderUI.java new file mode 100644 index 0000000000000000000000000000000000000000..626b2bbfe68e1ee13c771f476df2ce21bc3463bb --- /dev/null +++ b/src/main/java/bdv/ui/rangeslider/RangeSliderUI.java @@ -0,0 +1,752 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.rangeslider; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.event.MouseEvent; +import java.awt.geom.Ellipse2D; + +import javax.swing.JComponent; +import javax.swing.JSlider; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.plaf.basic.BasicSliderUI; + +/** + * UI delegate for the RangeSlider component. RangeSliderUI paints two thumbs, + * one for the lower value and one for the upper value. + * <p> + * This is copied from https://github.com/ernieyu/Swing-range-slider with the + * following changes: + * <ul> + * <li>The slider thumbs push each other. That is, the upper thumb can be + * dragged below the lower thumb, and it will drag the lower thumb with it, and + * vice versa</li> + * <li>The {@link RangeSlider#setRange(int, int)} method was added, to set both + * the lower and upper values simultaneously.</li> + * </ul> + * <p> + * + * <pre> + * ============================================================================= + * + * The MIT License Copyright (c) 2010 Ernest Yu. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * </pre> + */ +class RangeSliderUI extends BasicSliderUI +{ + /** + * Color of selected range. + */ + private final Color rangeColor = Color.gray; + + /** + * Location and size of thumb for upper value. + */ + private Rectangle upperThumbRect; + + /** + * Indicator that determines whether upper thumb is selected. + */ + private boolean upperThumbSelected; + + /** + * Indicator that determines whether lower thumb is being dragged. + */ + private transient boolean lowerDragging; + + /** + * Indicator that determines whether upper thumb is being dragged. + */ + private transient boolean upperDragging; + + /** + * Constructs a RangeSliderUI for the specified slider component. + * + * @param b + * RangeSlider + */ + public RangeSliderUI( final RangeSlider b ) + { + super( b ); + } + + /** + * Installs this UI delegate on the specified component. + */ + @Override + public void installUI( final JComponent c ) + { + upperThumbRect = new Rectangle(); + super.installUI( c ); + } + + /** + * Creates a listener to handle track events in the specified slider. + */ + @Override + protected TrackListener createTrackListener( final JSlider slider ) + { + return new RangeTrackListener(); + } + + /** + * Creates a listener to handle change events in the specified slider. + */ + @Override + protected ChangeListener createChangeListener( final JSlider slider ) + { + return new ChangeHandler(); + } + + /** + * Updates the dimensions for both thumbs. + */ + @Override + protected void calculateThumbSize() + { + // Call superclass method for lower thumb size. + super.calculateThumbSize(); + + // Set upper thumb size. + upperThumbRect.setSize( thumbRect.width, thumbRect.height ); + } + + /** + * Updates the locations for both thumbs. + */ + @Override + protected void calculateThumbLocation() + { + // Call superclass method for lower thumb location. + super.calculateThumbLocation(); + + // Adjust upper value to snap to ticks if necessary. + if ( slider.getSnapToTicks() ) + { + final int upperValue = slider.getValue() + slider.getExtent(); + int snappedValue = upperValue; + final int majorTickSpacing = slider.getMajorTickSpacing(); + final int minorTickSpacing = slider.getMinorTickSpacing(); + int tickSpacing = 0; + + if ( minorTickSpacing > 0 ) + { + tickSpacing = minorTickSpacing; + } + else if ( majorTickSpacing > 0 ) + { + tickSpacing = majorTickSpacing; + } + + if ( tickSpacing != 0 ) + { + // If it's not on a tick, change the value + if ( ( upperValue - slider.getMinimum() ) % tickSpacing != 0 ) + { + final float temp = ( float ) ( upperValue - slider.getMinimum() ) / ( float ) tickSpacing; + final int whichTick = Math.round( temp ); + snappedValue = slider.getMinimum() + ( whichTick * tickSpacing ); + } + + if ( snappedValue != upperValue ) + { + slider.setExtent( snappedValue - slider.getValue() ); + } + } + } + + // Calculate upper thumb location. The thumb is centered over its + // value on the track. + if ( slider.getOrientation() == JSlider.HORIZONTAL ) + { + final int upperPosition = xPositionForValue( slider.getValue() + slider.getExtent() ); + upperThumbRect.x = upperPosition - ( upperThumbRect.width / 2 ); + upperThumbRect.y = trackRect.y; + + } + else + { + final int upperPosition = yPositionForValue( slider.getValue() + slider.getExtent() ); + upperThumbRect.x = trackRect.x; + upperThumbRect.y = upperPosition - ( upperThumbRect.height / 2 ); + } + } + + /** + * Returns the size of a thumb. + */ + @Override + protected Dimension getThumbSize() + { + return new Dimension( 12, 12 ); + } + + /** + * Paints the slider. The selected thumb is always painted on top of the + * other thumb. + */ + @Override + public void paint( final Graphics g, final JComponent c ) + { + super.paint( g, c ); + + final Rectangle clipRect = g.getClipBounds(); + if ( upperThumbSelected ) + { + // Paint lower thumb first, then upper thumb. + if ( clipRect.intersects( thumbRect ) ) + { + paintLowerThumb( g ); + } + if ( clipRect.intersects( upperThumbRect ) ) + { + paintUpperThumb( g ); + } + + } + else + { + // Paint upper thumb first, then lower thumb. + if ( clipRect.intersects( upperThumbRect ) ) + { + paintUpperThumb( g ); + } + if ( clipRect.intersects( thumbRect ) ) + { + paintLowerThumb( g ); + } + } + } + + /** + * Paints the track. + */ + @Override + public void paintTrack( final Graphics g ) + { + // Draw track. + super.paintTrack( g ); + + final Rectangle trackBounds = trackRect; + + if ( slider.getOrientation() == JSlider.HORIZONTAL ) + { + // Determine position of selected range by moving from the middle + // of one thumb to the other. + final int lowerX = thumbRect.x + ( thumbRect.width / 2 ); + final int upperX = upperThumbRect.x + ( upperThumbRect.width / 2 ); + + // Determine track position. + final int cy = ( trackBounds.height / 2 ) - 2; + + // Save color and shift position. + final Color oldColor = g.getColor(); + g.translate( trackBounds.x, trackBounds.y + cy ); + + // Draw selected range. + g.setColor( getSliderColor() ); + for ( int y = 0; y <= 2; y++ ) + { + g.drawLine( lowerX - trackBounds.x, y, upperX - trackBounds.x, y ); + } + + // Restore position and color. + g.translate( -trackBounds.x, -( trackBounds.y + cy ) ); + g.setColor( oldColor ); + + } + else + { + // Determine position of selected range by moving from the middle + // of one thumb to the other. + final int lowerY = thumbRect.x + ( thumbRect.width / 2 ); + final int upperY = upperThumbRect.x + ( upperThumbRect.width / 2 ); + + // Determine track position. + final int cx = ( trackBounds.width / 2 ) - 2; + + // Save color and shift position. + final Color oldColor = g.getColor(); + g.translate( trackBounds.x + cx, trackBounds.y ); + + // Draw selected range. + g.setColor( getSliderColor() ); + for ( int x = 0; x <= 2; x++ ) + { + g.drawLine( x, lowerY - trackBounds.y, x, upperY - trackBounds.y ); + } + + // Restore position and color. + g.translate( -( trackBounds.x + cx ), -trackBounds.y ); + g.setColor( oldColor ); + } + } + + /** + * Overrides superclass method to do nothing. Thumb painting is handled + * within the <code>paint()</code> method. + */ + @Override + public void paintThumb( final Graphics g ) + { + // Do nothing. + } + + private Color getSliderFillColor() + { + return slider.isEnabled() ? Color.lightGray : Color.white; + } + + private Color getSliderColor() + { + return slider.isEnabled() ? Color.darkGray : Color.lightGray; + } + + /** + * Paints the thumb for the lower value using the specified graphics object. + */ + private void paintLowerThumb( final Graphics g ) + { + final Rectangle knobBounds = thumbRect; + final int w = knobBounds.width; + final int h = knobBounds.height; + + // Create graphics copy. + final Graphics2D g2d = ( Graphics2D ) g.create(); + + // Create default thumb shape. + final Shape thumbShape = createThumbShape( w - 1, h - 1 ); + + // Draw thumb. + g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON ); + g2d.translate( knobBounds.x, knobBounds.y ); + + final Color sliderFillColor = getSliderFillColor(); + g2d.setColor( sliderFillColor ); + g2d.fill( thumbShape ); + + final Color sliderColor = getSliderColor(); + g2d.setColor( sliderColor ); + g2d.draw( thumbShape ); + + // Dispose graphics. + g2d.dispose(); + } + + /** + * Paints the thumb for the upper value using the specified graphics object. + */ + private void paintUpperThumb( final Graphics g ) + { + final Rectangle knobBounds = upperThumbRect; + final int w = knobBounds.width; + final int h = knobBounds.height; + + // Create graphics copy. + final Graphics2D g2d = ( Graphics2D ) g.create(); + + // Create default thumb shape. + final Shape thumbShape = createThumbShape( w - 1, h - 1 ); + + // Draw thumb. + g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON ); + g2d.translate( knobBounds.x, knobBounds.y ); + + final Color sliderFillColor = getSliderFillColor(); + g2d.setColor( sliderFillColor ); + g2d.fill( thumbShape ); + + final Color sliderColor = getSliderColor(); + g2d.setColor( sliderColor ); + g2d.draw( thumbShape ); + + // Dispose graphics. + g2d.dispose(); + } + + /** + * Returns a Shape representing a thumb. + */ + private Shape createThumbShape( final int width, final int height ) + { + // Use circular shape. + final Ellipse2D shape = new Ellipse2D.Double( 0, 0, width, height ); + return shape; + } + + /** + * Sets the location of the upper thumb, and repaints the slider. This is + * called when the upper thumb is dragged to repaint the slider. The + * <code>setThumbLocation()</code> method performs the same task for the + * lower thumb. + */ + private void setUpperThumbLocation( final int x, final int y ) + { + final Rectangle upperUnionRect = new Rectangle(); + upperUnionRect.setBounds( upperThumbRect ); + + upperThumbRect.setLocation( x, y ); + + SwingUtilities.computeUnion( upperThumbRect.x, upperThumbRect.y, upperThumbRect.width, upperThumbRect.height, upperUnionRect ); + slider.repaint( upperUnionRect.x, upperUnionRect.y, upperUnionRect.width, upperUnionRect.height ); + } + + /** + * Moves the selected thumb in the specified direction by a block increment. + * This method is called when the user presses the Page Up or Down keys. + */ + @Override + public void scrollByBlock( final int direction ) + { + synchronized ( slider ) + { + int blockIncrement = ( slider.getMaximum() - slider.getMinimum() ) / 10; + if ( blockIncrement <= 0 && slider.getMaximum() > slider.getMinimum() ) + { + blockIncrement = 1; + } + final int delta = blockIncrement * ( ( direction > 0 ) ? POSITIVE_SCROLL : NEGATIVE_SCROLL ); + + if ( upperThumbSelected ) + { + final int oldValue = ( ( RangeSlider ) slider ).getUpperValue(); + ( ( RangeSlider ) slider ).setUpperValue( oldValue + delta ); + } + else + { + final int oldValue = slider.getValue(); + slider.setValue( oldValue + delta ); + } + } + } + + /** + * Moves the selected thumb in the specified direction by a unit increment. + * This method is called when the user presses one of the arrow keys. + */ + @Override + public void scrollByUnit( final int direction ) + { + synchronized ( slider ) + { + final int delta = 1 * ( ( direction > 0 ) ? POSITIVE_SCROLL : NEGATIVE_SCROLL ); + + if ( upperThumbSelected ) + { + final int oldValue = ( ( RangeSlider ) slider ).getUpperValue(); + ( ( RangeSlider ) slider ).setUpperValue( oldValue + delta ); + } + else + { + final int oldValue = slider.getValue(); + slider.setValue( oldValue + delta ); + } + } + } + + /** + * Listener to handle model change events. This calculates the thumb + * locations and repaints the slider if the value change is not caused by + * dragging a thumb. + */ + public class ChangeHandler implements ChangeListener + { + @Override + public void stateChanged( final ChangeEvent arg0 ) + { + if ( !lowerDragging && !upperDragging ) + { + calculateThumbLocation(); + slider.repaint(); + } + } + } + + /** + * Listener to handle mouse movements in the slider track. + */ + public class RangeTrackListener extends TrackListener + { + + @Override + public void mousePressed( final MouseEvent e ) + { + if ( !slider.isEnabled() ) + { return; } + + currentMouseX = e.getX(); + currentMouseY = e.getY(); + + if ( slider.isRequestFocusEnabled() ) + { + slider.requestFocus(); + } + + // Determine which thumb is pressed. If the upper thumb is + // selected (last one dragged), then check its position first; + // otherwise check the position of the lower thumb first. + boolean lowerPressed = false; + boolean upperPressed = false; + if ( upperThumbSelected || slider.getMinimum() == slider.getValue() ) + { + if ( upperThumbRect.contains( currentMouseX, currentMouseY ) ) + { + upperPressed = true; + } + else if ( thumbRect.contains( currentMouseX, currentMouseY ) ) + { + lowerPressed = true; + } + } + else + { + if ( thumbRect.contains( currentMouseX, currentMouseY ) ) + { + lowerPressed = true; + } + else if ( upperThumbRect.contains( currentMouseX, currentMouseY ) ) + { + upperPressed = true; + } + } + + // Handle lower thumb pressed. + if ( lowerPressed ) + { + switch ( slider.getOrientation() ) + { + case JSlider.VERTICAL: + offset = currentMouseY - thumbRect.y; + break; + case JSlider.HORIZONTAL: + offset = currentMouseX - thumbRect.x; + break; + } + upperThumbSelected = false; + lowerDragging = true; + return; + } + lowerDragging = false; + + // Handle upper thumb pressed. + if ( upperPressed ) + { + switch ( slider.getOrientation() ) + { + case JSlider.VERTICAL: + offset = currentMouseY - upperThumbRect.y; + break; + case JSlider.HORIZONTAL: + offset = currentMouseX - upperThumbRect.x; + break; + } + upperThumbSelected = true; + upperDragging = true; + return; + } + upperDragging = false; + } + + @Override + public void mouseReleased( final MouseEvent e ) + { + lowerDragging = false; + upperDragging = false; + slider.setValueIsAdjusting( false ); + super.mouseReleased( e ); + } + + @Override + public void mouseDragged( final MouseEvent e ) + { + if ( !slider.isEnabled() ) + { return; } + + currentMouseX = e.getX(); + currentMouseY = e.getY(); + + if ( lowerDragging ) + { + slider.setValueIsAdjusting( true ); + moveLowerThumb(); + + } + else if ( upperDragging ) + { + slider.setValueIsAdjusting( true ); + moveUpperThumb(); + } + } + + @Override + public boolean shouldScroll( final int direction ) + { + return false; + } + + /** + * Moves the location of the lower thumb, and sets its corresponding + * value in the slider. + */ + private void moveLowerThumb() + { + int thumbMiddle = 0; + + final RangeSlider slider = ( RangeSlider ) RangeSliderUI.this.slider; + if ( slider.getOrientation() == JSlider.VERTICAL ) + { + final int halfThumbHeight = thumbRect.height / 2; + int thumbTop = currentMouseY - offset; + final int trackTop = trackRect.y; + final int trackBottom = trackRect.y + ( trackRect.height - 1 ); + + thumbTop = Math.max( thumbTop, trackTop - halfThumbHeight ); + thumbTop = Math.min( thumbTop, trackBottom - halfThumbHeight ); + + setThumbLocation( thumbRect.x, thumbTop ); + + // Update slider value. + thumbMiddle = thumbTop + halfThumbHeight; + + final int lower = valueForYPosition( thumbMiddle ); + final int upper = Math.max( lower, slider.getUpperValue() ); + + final int upperThumbTop = yPositionForValue( upper ) - halfThumbHeight; + setUpperThumbLocation( thumbRect.x, upperThumbTop ); + + slider.setRange( lower, upper ); + } + else if ( slider.getOrientation() == JSlider.HORIZONTAL ) + { + final int halfThumbWidth = thumbRect.width / 2; + int thumbLeft = currentMouseX - offset; + final int trackLeft = trackRect.x; + final int trackRight = trackRect.x + ( trackRect.width - 1 ); + + thumbLeft = Math.max( thumbLeft, trackLeft - halfThumbWidth ); + thumbLeft = Math.min( thumbLeft, trackRight - halfThumbWidth ); + + setThumbLocation( thumbLeft, thumbRect.y ); + + // Update slider range. + thumbMiddle = thumbLeft + halfThumbWidth; + final int lower = valueForXPosition( thumbMiddle ); + final int upper = Math.max( lower, slider.getUpperValue() ); + + final int upperThumbLeft = xPositionForValue( upper ) - halfThumbWidth; + setUpperThumbLocation( upperThumbLeft, thumbRect.y ); + + slider.setRange( lower, upper ); + } + } + + /** + * Moves the location of the upper thumb, and sets its corresponding + * value in the slider. + */ + private void moveUpperThumb() + { + int thumbMiddle = 0; + + final RangeSlider slider = ( RangeSlider ) RangeSliderUI.this.slider; + if ( slider.getOrientation() == JSlider.VERTICAL ) + { + final int halfThumbHeight = thumbRect.height / 2; + int thumbTop = currentMouseY - offset; + final int trackTop = trackRect.y; + final int trackBottom = trackRect.y + ( trackRect.height - 1 ); + + thumbTop = Math.max( thumbTop, trackTop - halfThumbHeight ); + thumbTop = Math.min( thumbTop, trackBottom - halfThumbHeight ); + + setUpperThumbLocation( thumbRect.x, thumbTop ); + + // Update slider extent. + thumbMiddle = thumbTop + halfThumbHeight; + final int upper = valueForYPosition( thumbMiddle ); + final int lower = Math.min( upper, slider.getValue() ); + + final int lowerThumbTop = yPositionForValue( lower ) - halfThumbHeight; + setThumbLocation( thumbRect.x, lowerThumbTop ); + + slider.setRange( lower, upper ); + } + else if ( slider.getOrientation() == JSlider.HORIZONTAL ) + { + final int halfThumbWidth = thumbRect.width / 2; + int thumbLeft = currentMouseX - offset; + final int trackLeft = trackRect.x; + final int trackRight = trackRect.x + ( trackRect.width - 1 ); + + thumbLeft = Math.max( thumbLeft, trackLeft - halfThumbWidth ); + thumbLeft = Math.min( thumbLeft, trackRight - halfThumbWidth ); + + setUpperThumbLocation( thumbLeft, thumbRect.y ); + + // Update slider range. + thumbMiddle = thumbLeft + halfThumbWidth; + final int upper = valueForXPosition( thumbMiddle ); + final int lower = Math.min( upper, slider.getValue() ); + + final int lowerThumbLeft = xPositionForValue( lower ) - halfThumbWidth; + setThumbLocation( lowerThumbLeft, thumbRect.y ); + + slider.setRange( lower, upper ); + } + } + } +} diff --git a/src/main/java/bdv/ui/sourcegrouptree/SourceGroupEditor.java b/src/main/java/bdv/ui/sourcegrouptree/SourceGroupEditor.java new file mode 100644 index 0000000000000000000000000000000000000000..c0bb8b041e24e3a272229fd1ed18d6acad869361 --- /dev/null +++ b/src/main/java/bdv/ui/sourcegrouptree/SourceGroupEditor.java @@ -0,0 +1,89 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.sourcegrouptree; + +import java.awt.Component; +import java.awt.Graphics; +import javax.swing.Icon; +import javax.swing.JTree; +import javax.swing.tree.DefaultTreeCellEditor; +import javax.swing.tree.DefaultTreeCellRenderer; + +/** + * @author Tobias Pietzsch + */ +class SourceGroupEditor extends DefaultTreeCellEditor +{ + private final SourceGroupTreeCellRenderer sourceGroupRenderer; + + public SourceGroupEditor( final JTree tree, final SourceGroupTreeCellRenderer sourceGroupRenderer ) + { + // super class uses the DefaultTreeCellRenderer for font, icons, preferred size, etc + super( tree, new DefaultTreeCellRenderer() ); + this.sourceGroupRenderer = sourceGroupRenderer; + } + + // Overridden because we don't want to start the editing timer at all + @Override + protected void startEditingTimer() + { + } + + @Override + protected void determineOffset( final JTree tree, final Object value, final boolean isSelected, final boolean expanded, final boolean leaf, final int row ) + { + editingIcon = emptyIcon; + offset = sourceGroupRenderer.determineOffset( value ); + } + + private final Icon emptyIcon = new Icon() + { + private int iconHeight = -1; + + @Override + public void paintIcon( final Component c, final Graphics g, final int x, final int y ) + { + } + + @Override + public int getIconWidth() + { + return 1; + } + + @Override + public int getIconHeight() + { + if ( iconHeight < 0 && renderer != null ) + iconHeight = renderer.getDefaultLeafIcon().getIconHeight(); + return iconHeight < 0 ? 1 : iconHeight; + } + }; + +} diff --git a/src/main/java/bdv/ui/sourcegrouptree/SourceGroupTree.java b/src/main/java/bdv/ui/sourcegrouptree/SourceGroupTree.java new file mode 100644 index 0000000000000000000000000000000000000000..0629e427f920ca31e7637731059c5d28be7ec6d5 --- /dev/null +++ b/src/main/java/bdv/ui/sourcegrouptree/SourceGroupTree.java @@ -0,0 +1,456 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.sourcegrouptree; + +import bdv.ui.SourcesTransferable; +import bdv.ui.UIUtils; +import bdv.ui.sourcegrouptree.SourceGroupTreeModel.GroupModel; +import bdv.ui.sourcegrouptree.SourceGroupTreeModel.SourceModel; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.SourceGroup; +import bdv.viewer.ViewerState; +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.datatransfer.Transferable; +import java.awt.event.InputEvent; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.swing.JComponent; +import javax.swing.JTree; +import javax.swing.SwingUtilities; +import javax.swing.TransferHandler; +import javax.swing.tree.TreePath; +import org.scijava.ui.behaviour.io.InputTriggerConfig; +import org.scijava.ui.behaviour.util.Actions; +import org.scijava.ui.behaviour.util.InputActionBindings; + +/** + * A {@code JTree} that shows the source-groups of a {@link ViewerState} and + * allows to modify activeness and currentness, to add/remove sources to/from + * groups, and to add/remove/rename groups. + * + * @author Tobias Pietzsch + */ +public class SourceGroupTree extends JTree +{ + private final ViewerState state; + + private final SourceGroupTreeModel model; + + private final SourceGroupTreeCellRenderer renderer; + + private final SourceGroupEditor editor; + + private final Color focusedSelectionBg; + private final Color unfocusedSelectionBg; + + public SourceGroupTree( final ViewerState state ) + { + this( state, new InputTriggerConfig() ); + } + + public SourceGroupTree( final ViewerState state, final InputTriggerConfig inputTriggerConfig ) + { + this.state = state; + model = new SourceGroupTreeModel( state ); + setModel( model ); + + renderer = new SourceGroupTreeCellRenderer(); + setCellRenderer( renderer ); + + editor = new SourceGroupEditor( this, renderer ); + setCellEditor( editor ); + + setOpaque( false ); + + setTransferHandler( new SourceGroupTreeTransferHandler( this, state ) ); + + this.installActions( inputTriggerConfig ); + + focusedSelectionBg = renderer.getBackgroundSelectionColor(); + unfocusedSelectionBg = UIUtils.mix( focusedSelectionBg, getBackground(), 0.8 ); + } + + public void setSelectionBackground( final boolean hasFocus ) + { + renderer.setBackgroundSelectionColor( hasFocus ? focusedSelectionBg : unfocusedSelectionBg ); + this.repaint(); + } + + /** + * Get list of sources in selected groups. + * + * TODO how should they be ordered? + */ + public List< SourceAndConverter< ? > > getSelectedSources() + { + final List< SourceAndConverter< ? > > sources = new ArrayList<>(); + for ( final SourceGroup group : getSelectedGroups() ) + for ( final SourceAndConverter< ? > source : state.getSourcesInGroup( group ) ) + if ( !sources.contains( source ) ) + sources.add( source ); + sources.sort( state.sourceOrder() ); + return sources; + } + + private void installActions( final InputTriggerConfig inputTriggerConfig ) + { + final InputActionBindings keybindings = InputActionBindings.installNewBindings( this, JComponent.WHEN_FOCUSED, false ); + final Actions actions = new Actions( inputTriggerConfig, "bdv" ); + actions.install( keybindings, "source groups tree" ); + actions.runnableAction( () -> toggleSelectedActive(), "toggle active", "A" ); + actions.runnableAction( () -> makeSelectedActive( true ), "set active", "not mapped" ); + actions.runnableAction( () -> makeSelectedActive( false ), "set inactive", "not mapped" ); + actions.runnableAction( () -> cycleSelectedCurrent(), "cycle current", "C" ); + actions.runnableAction( () -> removeSelected(), "remove sources or groups", "DELETE", "BACK_SPACE" ); + actions.runnableAction( () -> editGroupName(), "edit name", "ENTER" ); + } + + private void makeSelectedActive( final boolean active ) + { + final List< SourceGroup > selectedGroups = getSelectedGroups(); + state.setGroupsActive( selectedGroups, active ); + } + + private void toggleSelectedActive() + { + final List< SourceGroup > selectedGroups = getSelectedGroups(); + if ( !selectedGroups.isEmpty() ) + state.setGroupsActive( selectedGroups, !state.isGroupActive( selectedGroups.get( 0 ) ) ); + } + + private void cycleSelectedCurrent() + { + final List< SourceGroup > selectedGroups = getSelectedGroups(); + if ( !selectedGroups.isEmpty() ) + { + final SourceGroup current = state.getCurrentGroup(); + final int i = ( selectedGroups.indexOf( current ) + 1 ) % selectedGroups.size(); + state.setCurrentGroup( selectedGroups.get( i ) ); + } + } + + private void removeSelected() + { + final Map< SourceGroup, List< SourceAndConverter< ? > > > groupToSelectedSources = getSelectedSourcesInGroups(); + final List< SourceGroup > selectedGroups = getSelectedGroups(); + + for ( final SourceGroup group : selectedGroups ) + groupToSelectedSources.remove( group ); + groupToSelectedSources.forEach( ( group, sources ) -> state.removeSourcesFromGroup( sources, group ) ); + + state.removeGroups( selectedGroups ); + } + + private void editGroupName() + { + final TreePath path = this.getLeadSelectionPath(); + if ( path != null ) + { + final Object obj = path.getLastPathComponent(); + if ( obj instanceof GroupModel ) + startEditingAtPath( path ); + } + } + + private List< SourceGroup > getSelectedGroups() + { + final List< SourceGroup > selectedGroups = new ArrayList<>(); + final TreePath[] selectionPaths = getSelectionPaths(); + if ( selectionPaths != null ) + for ( final TreePath path : selectionPaths ) + { + final Object obj = path.getLastPathComponent(); + if ( obj instanceof GroupModel ) + selectedGroups.add( ( ( GroupModel ) obj ).getGroup() ); + } + return selectedGroups; + } + + /** + * Get all sources that are directly selected in the tree. + * + * @return map from group to list of directly selected sources under the group + */ + private Map< SourceGroup, List< SourceAndConverter< ? > > > getSelectedSourcesInGroups() + { + final Map< SourceGroup, List< SourceAndConverter< ? > > > selected = new HashMap<>(); + for ( final TreePath path : getSelectionPaths() ) + { + final Object obj = path.getLastPathComponent(); + if ( obj instanceof SourceModel ) + { + final SourceGroup group = ( ( GroupModel ) path.getParentPath().getLastPathComponent() ).getGroup(); + final SourceAndConverter< ? > source = ( ( SourceModel ) obj ).getSource(); + selected.computeIfAbsent( group, g -> new ArrayList<>() ).add( source ); + } + } + return selected; + } + + @Override + public String convertValueToText( final Object value, final boolean selected, final boolean expanded, final boolean leaf, final int row, final boolean hasFocus ) + { + if ( value instanceof GroupModel ) + { + final String groupName = ( ( GroupModel ) value ).getName(); + if ( groupName != null ) + return groupName; + } + return ""; + } + + // -- Process clicks on active and current checkboxes -- + // These clicks are consumed, because they should not cause selection changes, etc, in the tree. + + private Point pressedAt; + private boolean consumeNext = false; + private long releasedWhen = 0; + + @Override + protected void processMouseEvent( final MouseEvent e ) + { + MouseEvent modifiedMouseEvent = e; + + if ( e.getModifiers() == InputEvent.BUTTON1_MASK ) + { + if ( e.getID() == MouseEvent.MOUSE_PRESSED ) + { + pressedAt = e.getPoint(); + int x = e.getX(); + int y = e.getY(); + final TreePath path = getPathForLocation( x, y ); + + if ( path != null ) + { + final Rectangle bounds = getPathBounds( path ); + + if ( e.getX() >= bounds.getX() + bounds.getWidth() ) + { + modifiedMouseEvent = new MouseEvent( + ( Component ) e.getSource(), e.getID(), e.getWhen(), e.getModifiersEx(), + ( int ) ( bounds.getX() + bounds.getWidth() - 1 ), e.getY(), + 0, 0, e.getClickCount(), e.isPopupTrigger(), e.getButton() ); + } + + if ( path.getLastPathComponent() instanceof GroupModel ) + { + x -= bounds.getX(); + y -= bounds.getY(); + final boolean currentHit = renderer.currentHit( x, y ); + final boolean activeHit = !currentHit && renderer.activeHit( x, y ); + if ( currentHit || activeHit ) + { + if ( isPathSelected( path ) ) + { + e.consume(); + consumeNext = true; + } + } + } + } + } + else if ( e.getID() == MouseEvent.MOUSE_RELEASED ) + { + if ( consumeNext ) + { + releasedWhen = e.getWhen(); + consumeNext = false; + e.consume(); + } + + if ( pressedAt == null ) + return; + + final Point point = e.getPoint(); + if ( point.distanceSq( pressedAt ) > 2 ) + return; + + int x = e.getX(); + int y = e.getY(); + final TreePath path = getPathForLocation( x, y ); + if ( path != null && path.getLastPathComponent() instanceof GroupModel ) + { + final Rectangle bounds = getPathBounds( path ); + x -= bounds.getX(); + y -= bounds.getY(); + final boolean currentHit = renderer.currentHit( x, y ); + final boolean activeHit = !currentHit && renderer.activeHit( x, y ); + if ( currentHit || activeHit ) + { + final SourceGroup group = ( ( GroupModel ) path.getLastPathComponent() ).getGroup(); + if ( currentHit ) + state.setCurrentGroup( group ); + else + { + if ( isPathSelected( path ) ) + state.setGroupsActive( getSelectedGroups(), !state.isGroupActive( group ) ); + else + state.setGroupActive( group, !state.isGroupActive( group ) ); + } + } + } + } + else if ( e.getID() == MouseEvent.MOUSE_CLICKED ) + { + if ( e.getWhen() == releasedWhen ) + e.consume(); + } + } + + super.processMouseEvent( modifiedMouseEvent ); + } + + @Override + public TreePath getPathForLocation( final int x, final int y ) + { + final TreePath closestPath = getClosestPathForLocation( x, y ); + + if ( closestPath != null ) + { + final Rectangle pathBounds = getPathBounds( closestPath ); + + if ( pathBounds != null && + x >= pathBounds.x && + y >= pathBounds.y && y < ( pathBounds.y + pathBounds.height ) ) + return closestPath; + } + return null; + } + + @Override + public boolean isPathEditable( final TreePath path ) + { + return path != null && path.getLastPathComponent() instanceof GroupModel; + } + + @Override + public void paintComponent( final Graphics g ) + { + g.setColor( getBackground() ); + g.fillRect( 0, 0, getWidth(), getHeight() ); + final int[] rows = getSelectionRows(); + if ( rows != null ) + { + g.setColor( renderer.getBackgroundSelectionColor() ); + for ( final int i : rows ) + { + final Rectangle r = getRowBounds( i ); + g.fillRect( 0, r.y, getWidth(), r.height ); + } + } + super.paintComponent( g ); + } + + public TreePath getPathTo( final SourceGroup group ) + { + synchronized ( state ) + { + if ( state.containsGroup( group ) ) + return model.getPathTo( new GroupModel( group, state ) ); + else + return null; + } + } + + /** + * {@code TransferHandler} for transferring sources into a group of a {@code SourceGroupTree} via cut/copy/paste and drag and drop. + */ + public static class SourceGroupTreeTransferHandler extends TransferHandler + { + private final SourceGroupTree tree; + + private final ViewerState state; + + public SourceGroupTreeTransferHandler( final SourceGroupTree tree, final ViewerState state ) + { + this.tree = tree; + this.state = state; + } + + @Override + public boolean canImport( final TransferSupport support ) + { + final boolean canDrop = support.isDataFlavorSupported( SourcesTransferable.flavor ); + support.setShowDropLocation( canDrop ); + return canDrop; + } + + @Override + public boolean importData( final TransferSupport support ) + { + if ( !canImport( support ) ) + return false; + + try + { + final Transferable t = support.getTransferable(); + final List< SourceAndConverter< ? > > sources = ( ( SourcesTransferable.SourceList ) t.getTransferData( SourcesTransferable.flavor ) ).getSources(); + + final DropLocation dropLocation = support.getDropLocation(); + if ( dropLocation instanceof JTree.DropLocation ) + { + final JTree.DropLocation drop = ( JTree.DropLocation ) dropLocation; + + final TreePath path = drop.getPath(); + if ( path == null ) + { + final SourceGroup group = new SourceGroup(); + state.addGroup( group ); + state.setGroupName( group, "new group" ); + state.addSourcesToGroup( sources, group ); + SwingUtilities.invokeLater( () -> { + final TreePath path1 = tree.getPathTo( group ); + tree.expandPath( path1 ); + tree.startEditingAtPath( path1 ); + } ); + } + else + { + final GroupModel groupModel = ( GroupModel ) path.getPathComponent( 1 ); + state.addSourcesToGroup( sources, groupModel.getGroup() ); + SwingUtilities.invokeLater( () -> { + tree.expandPath( tree.getPathTo( groupModel.getGroup() ) ); + } ); + } + return true; + } + } + catch ( final Exception e ) + {} + return false; + } + } +} diff --git a/src/main/java/bdv/ui/sourcegrouptree/SourceGroupTreeCellRenderer.java b/src/main/java/bdv/ui/sourcegrouptree/SourceGroupTreeCellRenderer.java new file mode 100644 index 0000000000000000000000000000000000000000..1e5327abe3965b574bacf7959fea0a29aa6493b7 --- /dev/null +++ b/src/main/java/bdv/ui/sourcegrouptree/SourceGroupTreeCellRenderer.java @@ -0,0 +1,430 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.sourcegrouptree; + +import bdv.ui.sourcegrouptree.SourceGroupTreeModel.GroupModel; +import bdv.ui.sourcegrouptree.SourceGroupTreeModel.SourceModel; +import java.awt.Color; +import java.awt.Component; +import java.awt.ComponentOrientation; +import java.awt.Font; +import java.awt.Graphics; +import javax.swing.BoxLayout; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JTree; +import javax.swing.UIManager; +import javax.swing.border.EmptyBorder; +import javax.swing.plaf.FontUIResource; +import javax.swing.plaf.basic.BasicGraphicsUtils; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.TreeCellRenderer; + +/** + * @author Tobias Pietzsch + */ +class SourceGroupTreeCellRenderer implements TreeCellRenderer +{ + /** + * Last tree the renderer was painted in. + */ + private JTree tree; + + /** + * Is the value currently selected. + */ + private boolean selected; + + /** + * True if has focus. + */ + private boolean hasFocus; + + + // Color to use for the foreground for selected nodes. + private final Color textSelectionColor; + + // Color to use for the foreground for non-selected nodes. + private final Color textNonSelectionColor; + + // Color to use for the background when a node is selected. + private Color backgroundSelectionColor; + + // Color to use for the background when the node isn't selected. + private final Color backgroundNonSelectionColor; + + // Color to use for the focus indicator when the node has focus. + private final Color borderSelectionColor; + + // TODO + private final Color backgroundDropCellColor; + + // TODO + private final boolean fillBackground; + + // If true, a dashed line is drawn as the focus indicator. + private final boolean drawDashedFocusIndicator; + + // If drawDashedFocusIndicator is true, the following are used. + + // Background color of the tree. + private Color treeBGColor; + + // Color to draw the focus indicator in, determined from the background color. + private Color focusBGColor; + + // actual renderers + private GroupRenderer groupRenderer = new GroupRenderer(); + private SourceRenderer sourceRenderer = new SourceRenderer(); + + // fallback for debugging + private final DefaultTreeCellRenderer defaultRenderer = new DefaultTreeCellRenderer(); + + SourceGroupTreeCellRenderer() + { + textSelectionColor = getUIColor( "Tree.selectionForeground" ); + textNonSelectionColor = getUIColor( "Tree.textForeground" ); + backgroundSelectionColor = getUIColor( "Tree.selectionBackground" ); + backgroundNonSelectionColor = getUIColor( "Tree.textBackground" ); + borderSelectionColor = getUIColor( "Tree.selectionBorderColor" ); + backgroundDropCellColor = getUIColor( "Tree.dropCellBackground", backgroundSelectionColor ); + fillBackground = getUIBoolean( "Tree.rendererFillBackground", true ); + drawDashedFocusIndicator = getUIBoolean( "Tree.drawDashedFocusIndicator", false ); + } + + /** + * Sets the color to use for the background if node is selected. + */ + public void setBackgroundSelectionColor( final Color newColor ) + { + backgroundSelectionColor = newColor; + } + + /** + * Returns the color to use for the background if node is selected. + */ + public Color getBackgroundSelectionColor() + { + return backgroundSelectionColor; + } + + private static boolean getUIBoolean( String key, boolean defaultValue ) + { + final Object value = UIManager.get( key ); + if ( value instanceof Boolean ) + return ( Boolean ) value; + else + return defaultValue; + } + + private static Color getUIColor( String key ) + { + return getUIColor( key, null ); + } + + private static Color getUIColor( String key, Color defaultValue ) + { + final Object value = UIManager.get( key ); + if ( value instanceof Color ) + return ( Color ) value; + else + return defaultValue; + } + + @Override + public Component getTreeCellRendererComponent( final JTree tree, final Object value, final boolean selected, final boolean expanded, final boolean leaf, final int row, final boolean hasFocus ) + { + this.tree = tree; + this.hasFocus = hasFocus; + this.selected = selected; + + if ( value instanceof GroupModel ) + return groupRenderer.getTreeCellRendererComponent( ( GroupModel ) value ); + else if ( value instanceof SourceModel ) + return sourceRenderer.getTreeCellRendererComponent( ( SourceModel ) value ); + else + return defaultRenderer.getTreeCellRendererComponent( tree, value, selected, expanded, leaf, row, hasFocus ); + } + + public int determineOffset( final Object value ) + { + if ( value instanceof GroupModel ) + return groupRenderer.getOffset(); + else if ( value instanceof SourceModel ) + return sourceRenderer.getOffset(); + else + return 0; + } + + public boolean currentHit( final int x, final int y ) + { + return groupRenderer.currentHit( x, y ); + } + + public boolean activeHit( final int x, final int y ) + { + return groupRenderer.activeHit( x, y ); + } + + class TreeLabel extends JLabel + { + @Override + public void setFont( Font font ) + { + if ( font instanceof FontUIResource ) + font = null; + super.setFont( font ); + } + + @Override + public Font getFont() + { + Font font = super.getFont(); + if ( font == null && tree != null ) + { + font = tree.getFont(); + } + return font; + } + } + + private void paintFocus( Graphics g, int x, int y, int w, int h, Color notColor ) + { + Color bsColor = borderSelectionColor; + + if ( bsColor != null && ( selected || !drawDashedFocusIndicator ) ) + { + g.setColor( bsColor ); + g.drawRect( x, y, w - 1, h - 1 ); + } + if ( drawDashedFocusIndicator && notColor != null ) + { + if ( treeBGColor != notColor ) + { + treeBGColor = notColor; + focusBGColor = new Color( ~notColor.getRGB() ); + } + g.setColor( focusBGColor ); + BasicGraphicsUtils.drawDashedRect( g, x, y, w, h ); + } + } + + class GroupRenderer extends JPanel + { + private final TreeLabel nameLabel; + + private final JRadioButton currentRadioButton; + + private final JCheckBox activeCheckBox; + + GroupRenderer() + { + setLayout( new BoxLayout( this, BoxLayout.X_AXIS ) ); + setBorder( new EmptyBorder( 0, 0, 0, 0 ) ); + nameLabel = new TreeLabel(); +// UIUtils.setPreferredWidth( nameLabel, 100 ); + currentRadioButton = new JRadioButton(); + currentRadioButton.setBorder( new EmptyBorder( 0, 0, 0, 5 ) ); + currentRadioButton.setOpaque( false ); + activeCheckBox = new JCheckBox(); + activeCheckBox.setBorder( new EmptyBorder( 0, 0, 0, 5 ) ); + activeCheckBox.setOpaque( false ); + add( currentRadioButton ); + add( activeCheckBox ); + add( nameLabel ); + setOpaque( false ); + invalidate(); + } + + @Override + public void setFont( Font font ) + { + if ( font instanceof FontUIResource ) + font = null; + super.setFont( font ); + } + + @Override + public Font getFont() + { + Font font = super.getFont(); + if ( font == null && tree != null ) + { + font = tree.getFont(); + } + return font; + } + + public Component getTreeCellRendererComponent( final GroupModel group ) + { + nameLabel.setText( group.getName() ); + currentRadioButton.setSelected( group.isCurrent() ); + activeCheckBox.setSelected( group.isActive() ); + + final Color fg; + if ( selected ) + fg = textSelectionColor; + else + fg = textNonSelectionColor; + nameLabel.setForeground( fg ); + + final boolean enabled = tree.isEnabled(); + setEnabled( enabled ); + nameLabel.setEnabled( enabled ); + currentRadioButton.setEnabled( enabled ); + activeCheckBox.setEnabled( enabled ); + + final ComponentOrientation componentOrientation = tree.getComponentOrientation(); + setComponentOrientation( componentOrientation ); + nameLabel.setComponentOrientation( componentOrientation ); + currentRadioButton.setComponentOrientation( componentOrientation ); + activeCheckBox.setComponentOrientation( componentOrientation ); + + invalidate(); + return this; + } + + // TODO + private boolean isDropCell = false; + + /** + * Paints the value. The background is filled based on selected. + */ + @Override + public void paint( Graphics g ) + { + Color bColor; + + if ( isDropCell ) + bColor = backgroundDropCellColor; + else if ( selected ) + bColor = backgroundSelectionColor; + else + bColor = backgroundNonSelectionColor; + + if ( bColor == null ) + bColor = getBackground(); + + if ( bColor != null && fillBackground ) + { + g.setColor( bColor ); + g.fillRect( 0, 0, getWidth(), getHeight() ); + } + + if ( hasFocus ) + paintFocus( g, 0, 0, getWidth(), getHeight(), bColor ); + + super.paint( g ); + } + + public int getOffset() + { + return nameLabel.getX(); + } + + public boolean currentHit( final int x, final int y ) + { + return currentRadioButton.getBounds().contains( x, y ); + } + + public boolean activeHit( final int x, final int y ) + { + return activeCheckBox.getBounds().contains( x, y ); + } + } + + class SourceRenderer extends TreeLabel + { + SourceRenderer() + { + setBorder( new EmptyBorder( 0, 35, 0, 0 ) ); + setOpaque( false ); + } + + public Component getTreeCellRendererComponent( final SourceModel source ) + { + setText( source.getName() ); + + final Color fg; + if ( selected ) + fg = textSelectionColor; + else + fg = textNonSelectionColor; + setForeground( fg ); + + final boolean enabled = tree.isEnabled(); + setEnabled( enabled ); + + final ComponentOrientation componentOrientation = tree.getComponentOrientation(); + setComponentOrientation( componentOrientation ); + + return this; + } + + // TODO + private boolean isDropCell = false; + + /** + * Paints the value. The background is filled based on selected. + */ + @Override + public void paint( Graphics g ) + { + Color bColor; + + if ( isDropCell ) + bColor = backgroundDropCellColor; + else if ( selected ) + bColor = backgroundSelectionColor; + else + bColor = backgroundNonSelectionColor; + + if ( bColor == null ) + bColor = getBackground(); + + if ( bColor != null && fillBackground ) + { + g.setColor( bColor ); + g.fillRect( 0, 0, getWidth(), getHeight() ); + } + + if ( hasFocus ) + paintFocus( g, 0, 0, getWidth(), getHeight(), bColor ); + + super.paint( g ); + } + + public int getOffset() + { + return getX(); + } + } +} diff --git a/src/main/java/bdv/ui/sourcegrouptree/SourceGroupTreeModel.java b/src/main/java/bdv/ui/sourcegrouptree/SourceGroupTreeModel.java new file mode 100644 index 0000000000000000000000000000000000000000..a5b524ab7b8921023966b3093e2548f5712481c4 --- /dev/null +++ b/src/main/java/bdv/ui/sourcegrouptree/SourceGroupTreeModel.java @@ -0,0 +1,532 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.sourcegrouptree; + +import bdv.util.WrappedList; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.SourceGroup; +import bdv.viewer.ViewerState; +import gnu.trove.map.TObjectIntMap; +import gnu.trove.map.hash.TObjectIntHashMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import javax.swing.SwingUtilities; +import javax.swing.event.EventListenerList; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; + +import static gnu.trove.impl.Constants.DEFAULT_CAPACITY; +import static gnu.trove.impl.Constants.DEFAULT_LOAD_FACTOR; + +/** + * @author Tobias Pietzsch + */ +public class SourceGroupTreeModel implements TreeModel +{ + private final EventListenerList listenerList = new EventListenerList(); + + private final String root = "root"; + + private final Object[] rootPath = new Object[] { root }; + + private final ViewerState state; + + private StateModel model; + + public SourceGroupTreeModel( final ViewerState state ) + { + model = new StateModel( state ); + this.state = state; + state.changeListeners().add( e -> + { + switch ( e ) + { + case CURRENT_GROUP_CHANGED: + case GROUP_ACTIVITY_CHANGED: + case SOURCE_TO_GROUP_ASSIGNMENT_CHANGED: + case GROUP_NAME_CHANGED: + case NUM_GROUPS_CHANGED: + final StateModel model = new StateModel( state ); + SwingUtilities.invokeLater( () -> analyzeChanges( model ) ); + } + } ); + } + + @Override + public Object getRoot() + { + return root; + } + + @Override + public Object getChild( final Object parent, final int index ) + { + if ( parent == root ) + { + return model.getGroups().get( index ); + } + else if ( parent instanceof GroupModel ) + { + return ( ( GroupModel ) parent ).getSources().get( index ); + } + else + { + throw new IllegalArgumentException(); + } + } + + @Override + public int getChildCount( final Object parent ) + { + if ( parent == root ) + { + return model.getGroups().size(); + } + else if ( parent instanceof GroupModel ) + { + return ( ( GroupModel ) parent ).getSources().size(); + } + else + { + return 0; + } + } + + @Override + public boolean isLeaf( final Object node ) + { + if ( node == root ) + { + return model.getGroups().isEmpty(); + } + else if ( node instanceof GroupModel ) + { + return ( ( GroupModel ) node ).getSources().isEmpty(); + } + else + { + return true; + } + } + + @Override + public void valueForPathChanged( final TreePath path, final Object newValue ) + { + final Object o = path.getLastPathComponent(); + if ( o instanceof GroupModel ) + state.setGroupName( ( ( GroupModel ) o ).group, newValue.toString() ); + } + + @Override + public int getIndexOfChild( final Object parent, final Object child ) + { + if ( parent == root ) + { + return model.getGroups().indexOf( child ); + } + else if ( parent instanceof GroupModel ) + { + return ( ( GroupModel ) parent ).getSources().indexOf( child ); + } + else + { + throw new IllegalArgumentException(); + } + } + + // + // Events + // + + /** + * Adds a listener for the TreeModelEvent posted after the tree changes. + * + * @param l + * the listener to add + * + * @see #removeTreeModelListener + */ + public void addTreeModelListener( TreeModelListener l ) + { + listenerList.add( TreeModelListener.class, l ); + } + + /** + * Removes a listener previously added with <B>addTreeModelListener()</B>. + * + * @param l + * the listener to remove + * + * @see #addTreeModelListener + */ + public void removeTreeModelListener( TreeModelListener l ) + { + listenerList.remove( TreeModelListener.class, l ); + } + + private void fireTreeNodesChanged(final TreeModelEvent e) + { + final Object[] listeners = listenerList.getListenerList(); + for ( int i = listeners.length - 2; i >= 0; i -= 2 ) + if ( listeners[ i ] == TreeModelListener.class ) + ( ( TreeModelListener ) listeners[ i + 1 ] ).treeNodesChanged( e ); + } + + private void fireTreeNodesInserted(final TreeModelEvent e) + { + final Object[] listeners = listenerList.getListenerList(); + for ( int i = listeners.length - 2; i >= 0; i -= 2 ) + if ( listeners[ i ] == TreeModelListener.class ) + ( ( TreeModelListener ) listeners[ i + 1 ] ).treeNodesInserted( e ); + } + + private void fireTreeNodesRemoved(final TreeModelEvent e) + { + final Object[] listeners = listenerList.getListenerList(); + for ( int i = listeners.length - 2; i >= 0; i -= 2 ) + if ( listeners[ i ] == TreeModelListener.class ) + ( ( TreeModelListener ) listeners[ i + 1 ] ).treeNodesRemoved( e ); + } + + private void fireTreeStructureChanged(final TreeModelEvent e) + { + final Object[] listeners = listenerList.getListenerList(); + for ( int i = listeners.length - 2; i >= 0; i -= 2 ) + if ( listeners[ i ] == TreeModelListener.class ) + ( ( TreeModelListener ) listeners[ i + 1 ] ).treeStructureChanged( e ); + } + + public TreePath getPathTo( final GroupModel group ) + { + return new TreePath( new Object[] { root, group } ); + } + + private void analyzeChanges( final StateModel model ) + { + final StateModel previousModel = this.model; + this.model = model; + + // -- NUM_GROUPS_CHANGED -- + + final List< GroupModel > removedGroups = new ArrayList<>(); + for ( GroupModel group : previousModel.getGroups() ) + if ( !model.getGroups().contains( group ) ) + removedGroups.add( group ); + + final List< GroupModel > addedGroups = new ArrayList<>(); + for ( GroupModel group : model.getGroups() ) + if ( !previousModel.getGroups().contains( group ) ) + addedGroups.add( group ); + + // -- GROUP_NAME_CHANGED, CURRENT_GROUP_CHANGED, GROUP_ACTIVITY_CHANGED -- + + final List< GroupModel > changedGroups = new ArrayList<>(); + for ( GroupModel group : model.getGroups() ) + { + final GroupModel previousGroup = previousModel.getGroups().get( group ); + if ( previousGroup != null ) + { + if ( group.isCurrent() != previousGroup.isCurrent() || + group.isActive() != previousGroup.isActive() || + !Objects.equals( group.getName(), previousGroup.getName() ) ) + { + changedGroups.add( group ); + } + } + } + + // -- SOURCE_TO_GROUP_ASSIGNMENT_CHANGED -- + + final List< GroupModel > structurallyChangedGroups = new ArrayList<>(); + for ( GroupModel group : model.getGroups() ) + { + final GroupModel previousGroup = previousModel.getGroups().get( group ); + if ( previousGroup != null ) + { + final List< SourceModel > content = group.getSources(); + final List< SourceModel > previousContent = previousGroup.getSources(); + if ( !content.equals( previousContent ) ) + structurallyChangedGroups.add( group ); + } + } + + // -- create corresponding TreeModelEvents -- + + // groups added or removed + if ( !addedGroups.isEmpty() ) + { + final int[] childIndices = new int[ addedGroups.size() ]; + Arrays.setAll( childIndices, i -> model.getGroups().indexOf( addedGroups.get( i ) ) ); + final Object[] children = addedGroups.toArray( new Object[ 0 ] ); + fireTreeNodesInserted( new TreeModelEvent( this, rootPath, childIndices, children ) ); + } + else if ( !removedGroups.isEmpty() ) + { + final int[] childIndices = new int[ removedGroups.size() ]; + Arrays.setAll( childIndices, i -> previousModel.getGroups().indexOf( removedGroups.get( i ) ) ); + final Object[] children = removedGroups.toArray( new Object[ 0 ] ); + fireTreeNodesRemoved( new TreeModelEvent( this, rootPath, childIndices, children ) ); + } + + // groups that change currentness, activeness, or name + if ( !changedGroups.isEmpty() ) + { + final int[] childIndices = new int[ changedGroups.size() ]; + Arrays.setAll( childIndices, i -> model.getGroups().indexOf( changedGroups.get( i ) ) ); + final Object[] children = changedGroups.toArray( new Object[ 0 ] ); + fireTreeNodesChanged( new TreeModelEvent( this, rootPath, childIndices, children ) ); + } + + // groups that had children added or removed + if ( !structurallyChangedGroups.isEmpty() ) + { + for ( GroupModel group : structurallyChangedGroups ) + { + final Object[] path = new Object[] { root, group }; + fireTreeStructureChanged( new TreeModelEvent( this, path, null, null ) ); + } + } + } + + // + // Internal state model + // + + private static final int NO_ENTRY_VALUE = -1; + + static class StateModel + { + private final UnmodifiableGroups groups; + + public StateModel( final ViewerState state ) + { + final List< GroupModel > glist = new ArrayList<>(); + final TObjectIntMap< GroupModel > gindices = new TObjectIntHashMap<>( DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, NO_ENTRY_VALUE ); + final List< SourceGroup > sgroups = state.getGroups(); + for ( int i = 0; i < sgroups.size(); ++i ) + { + final GroupModel groupModel = new GroupModel( sgroups.get( i ), state ); + glist.add( groupModel ); + gindices.put( groupModel, i ); + } + groups = new UnmodifiableGroups( glist, gindices ); + } + + public UnmodifiableGroups getGroups() + { + return groups; + } + + static class UnmodifiableGroups extends WrappedList< GroupModel > + { + private final TObjectIntMap< GroupModel > groupIndices; + + public UnmodifiableGroups(final List< GroupModel > groups, final TObjectIntMap< GroupModel > groupIndices) + { + super( Collections.unmodifiableList( groups ) ); + this.groupIndices = groupIndices; + } + + public GroupModel get( GroupModel groupModel ) + { + final int index = groupIndices.get( groupModel ); + return index == NO_ENTRY_VALUE ? null : get( index ); + } + + @Override + public boolean contains( final Object o ) + { + return groupIndices.containsKey( o ); + } + + @Override + public boolean containsAll( final Collection< ? > c ) + { + return groupIndices.keySet().containsAll( c ); + } + + @Override + public int indexOf( final Object o ) + { + return groupIndices.get( o ); + } + + @Override + public int lastIndexOf( final Object o ) + { + return groupIndices.get( o ); + } + } + } + + static class GroupModel + { + private final String name; + private final boolean active; + private final boolean current; + + private final List< SourceModel > sources; + + private final SourceGroup group; + + public GroupModel( final SourceGroup group, final ViewerState state ) + { + name = state.getGroupName( group ); + active = state.isGroupActive( group ); + current = state.isCurrentGroup( group ); + + final List< SourceAndConverter< ? > > orderedSources = new ArrayList<>( state.getSourcesInGroup( group ) ); + orderedSources.sort( state.sourceOrder() ); + final List< SourceModel > slist = new ArrayList<>(); + final TObjectIntMap< SourceModel > sindices = new TObjectIntHashMap<>( DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, NO_ENTRY_VALUE ); + for ( int i = 0; i < orderedSources.size(); ++i ) + { + final SourceModel sourceModel = new SourceModel( orderedSources.get( i ) ); + slist.add( sourceModel ); + sindices.put( sourceModel, i ); + } + sources = new UnmodifiableSources( slist, sindices ); + + this.group = group; + } + + public String getName() + { + return name; + } + + public boolean isActive() + { + return active; + } + + public boolean isCurrent() + { + return current; + } + + public List< SourceModel > getSources() + { + return sources; + } + + public SourceGroup getGroup() + { + return group; + } + + @Override + public boolean equals( final Object o ) + { + return ( o instanceof GroupModel ) && group.equals( ( ( GroupModel ) o ).group ); + } + + @Override + public int hashCode() + { + return group.hashCode(); + } + + static class UnmodifiableSources extends WrappedList< SourceModel > + { + private final TObjectIntMap< SourceModel > sourceIndices; + + public UnmodifiableSources( final List< SourceModel > sources, final TObjectIntMap< SourceModel > sourceIndices ) + { + super( Collections.unmodifiableList( sources ) ); + this.sourceIndices = sourceIndices; + } + + @Override + public boolean contains( final Object o ) + { + return sourceIndices.containsKey( o ); + } + + @Override + public boolean containsAll( final Collection< ? > c ) + { + return sourceIndices.keySet().containsAll( c ); + } + + @Override + public int indexOf( final Object o ) + { + return sourceIndices.get( o ); + } + + @Override + public int lastIndexOf( final Object o ) + { + return sourceIndices.get( o ); + } + } + } + + static class SourceModel + { + private final String name; + + private final SourceAndConverter< ? > source; + + public SourceModel( final SourceAndConverter< ? > source ) + { + name = source.getSpimSource().getName(); + this.source = source; + } + + public String getName() + { + return name; + } + + public SourceAndConverter<?> getSource() + { + return source; + } + + @Override + public boolean equals( final Object o ) + { + return ( o instanceof SourceModel ) && source.equals( ( ( SourceModel ) o ).source ); + } + + @Override + public int hashCode() + { + return source.hashCode(); + } + } +} diff --git a/src/main/java/bdv/ui/sourcetable/ColorRenderer.java b/src/main/java/bdv/ui/sourcetable/ColorRenderer.java new file mode 100644 index 0000000000000000000000000000000000000000..30d29c8e923a4b9880f0925d39e2a8c6b6d83e0e --- /dev/null +++ b/src/main/java/bdv/ui/sourcetable/ColorRenderer.java @@ -0,0 +1,137 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.sourcetable; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import javax.swing.Icon; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.SwingConstants; +import javax.swing.border.EmptyBorder; +import javax.swing.table.TableCellRenderer; +import net.imglib2.type.numeric.ARGBType; + +/** + * @author Tobias Pietzsch + */ +class ColorRenderer extends JLabel implements TableCellRenderer +{ + private final NoColorIcon noColorIcon; + + public ColorRenderer() + { + setBorder( new EmptyBorder( 0, 0, 0, 0 ) ); + setOpaque( true ); + setHorizontalAlignment( SwingConstants.CENTER ); + noColorIcon = new NoColorIcon( 14, 14 ); + } + + @Override + public Component getTableCellRendererComponent( final JTable table, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column ) + { + if ( value == null ) + { + setBackground( isSelected ? table.getSelectionBackground() : table.getBackground() ); + setIcon( noColorIcon ); + } + else + { + setBackground( new Color( ( ( ARGBType ) value ).get() ) ); + setIcon( null ); + } + return this; + } + + private static class NoColorIcon implements Icon + { + private final int width; + private final int height; + + private final int size; // == min(width, height) + private final int ox; + private final int oy; + private final int lox0; + private final int loy0; + private final int lox1; + private final int loy1; + + private final Color color = new Color( 0xBBBBBB ); + private final BasicStroke stroke = new BasicStroke( 2 ); + + public NoColorIcon( final int width, final int height ) + { + this.width = width; + this.height = height; + + size = Math.min( width, height ); + ox = ( width - size ) / 2; + oy = ( height - size ) / 2; + lox0 = ( int ) ( ox + size * ( 0.5 * ( 1 - Math.sqrt( 0.5 ) ) ) ); + loy0 = ( int ) ( oy + size * ( 0.5 * ( 1 - Math.sqrt( 0.5 ) ) ) ); + lox1 = ( int ) ( ox + size * ( 0.5 * ( 1 + Math.sqrt( 0.5 ) ) ) ); + loy1 = ( int ) ( oy + size * ( 0.5 * ( 1 + Math.sqrt( 0.5 ) ) ) ); + } + + @Override + public void paintIcon( final Component c, final Graphics g, final int x, final int y ) + { + final Graphics2D g2d = ( Graphics2D ) g; + g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON ); + g2d.setColor( color ); + g2d.setStroke( stroke ); + + final int lx0 = x + lox0; + final int ly0 = y + loy0; + final int lx1 = x + lox1; + final int ly1 = y + loy1; + g2d.drawLine( lx0, ly0, lx1, ly1 ); + + final int x0 = x + ox; + final int y0 = y + oy; + g2d.drawOval( x0, y0, size, size ); + } + + @Override + public int getIconWidth() + { + return width; + } + + @Override + public int getIconHeight() + { + return height; + } + } +} diff --git a/src/main/java/bdv/ui/sourcetable/RadioButtonRenderer.java b/src/main/java/bdv/ui/sourcetable/RadioButtonRenderer.java new file mode 100644 index 0000000000000000000000000000000000000000..bb247a839de2757cd8ccdffa61dfbd84ba58e148 --- /dev/null +++ b/src/main/java/bdv/ui/sourcetable/RadioButtonRenderer.java @@ -0,0 +1,78 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.sourcetable; + +import java.awt.Component; +import javax.swing.JLabel; +import javax.swing.JRadioButton; +import javax.swing.JTable; +import javax.swing.UIManager; +import javax.swing.border.Border; +import javax.swing.border.EmptyBorder; +import javax.swing.table.TableCellRenderer; + +/** + * @author Tobias Pietzsch + */ +class RadioButtonRenderer extends JRadioButton implements TableCellRenderer +{ + private static final Border noFocusBorder = new EmptyBorder(1, 1, 1, 1); + + public RadioButtonRenderer() { + setHorizontalAlignment( JLabel.CENTER); + setBorderPainted( true ); + } + + @Override + public Component getTableCellRendererComponent( final JTable table, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column ) + { + if ( isSelected ) + { + setForeground( table.getSelectionForeground() ); + setBackground( table.getSelectionBackground() ); + } + else + { + setForeground( table.getForeground() ); + setBackground( table.getBackground() ); + } + setSelected( ( value != null && ( Boolean ) value ) ); + + if ( hasFocus ) + { + setBorder( UIManager.getBorder( "Table.focusCellHighlightBorder" ) ); + } + else + { + setBorder( noFocusBorder ); + } + + return this; + } +} diff --git a/src/main/java/bdv/ui/sourcetable/SourceTable.java b/src/main/java/bdv/ui/sourcetable/SourceTable.java new file mode 100644 index 0000000000000000000000000000000000000000..d29a4cbd713f838607b078cec7637279ab348d35 --- /dev/null +++ b/src/main/java/bdv/ui/sourcetable/SourceTable.java @@ -0,0 +1,324 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.sourcetable; + +import bdv.tools.brightness.ConverterSetup; +import bdv.ui.SourcesTransferable; +import bdv.ui.UIUtils; +import bdv.viewer.ConverterSetups; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.SourceToConverterSetupBimap; +import bdv.viewer.ViewerState; +import java.awt.Color; +import java.awt.Point; +import java.awt.datatransfer.Transferable; +import java.awt.event.InputEvent; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; +import javax.swing.JColorChooser; +import javax.swing.JComponent; +import javax.swing.JTable; +import javax.swing.TransferHandler; +import net.imglib2.type.numeric.ARGBType; +import org.scijava.ui.behaviour.io.InputTriggerConfig; +import org.scijava.ui.behaviour.util.Actions; +import org.scijava.ui.behaviour.util.InputActionBindings; + +import static bdv.ui.sourcetable.SourceTableModel.COLOR_COLUMN; +import static bdv.ui.sourcetable.SourceTableModel.IS_ACTIVE_COLUMN; +import static bdv.ui.sourcetable.SourceTableModel.IS_CURRENT_COLUMN; + +/** + * A {@code JTable} that shows the sources of a {@link ViewerState} and allows + * to modify activeness, currentness, as well as the color of the corresponding + * {@link ConverterSetup}. + * + * @author Tobias Pietzsch + */ +public class SourceTable extends JTable +{ + private final SourceTableModel model; + + private final ViewerState state; + + private final SourceToConverterSetupBimap converters; + + private final Color focusedSelectionBg; + private final Color unfocusedSelectionBg; + + private final Color focusedSelectionFg; + + public SourceTable( final ViewerState state, final ConverterSetups converterSetups ) + { + this( state, converterSetups, new InputTriggerConfig() ); + } + + public SourceTable( final ViewerState state, final ConverterSetups converterSetups, final InputTriggerConfig inputTriggerConfig ) + { + this.state = state; + converters = converterSetups; + model = new SourceTableModel( state, converterSetups ); + setModel( model ); + setTransferHandler( new SourceTableTransferHandler() ); + + getColumnModel().getColumn( IS_CURRENT_COLUMN ).setCellRenderer( new RadioButtonRenderer() ); + getColumnModel().getColumn( COLOR_COLUMN ).setCellRenderer( new ColorRenderer() ); + setRowHeight( 18 ); + + setShowGrid( false ); + + getColumnModel().getColumn( IS_CURRENT_COLUMN ).setMinWidth( 20 ); + getColumnModel().getColumn( IS_ACTIVE_COLUMN ).setMinWidth( 20 ); + getColumnModel().getColumn( COLOR_COLUMN ).setMinWidth( 40 ); + + this.installActions( inputTriggerConfig ); + + focusedSelectionBg = getSelectionBackground(); + focusedSelectionFg = getSelectionForeground(); + unfocusedSelectionBg = UIUtils.mix( focusedSelectionBg, getBackground(), 0.8 ); + } + + public void setSelectionBackground( final boolean hasFocus ) + { + setSelectionForeground( focusedSelectionFg ); + setSelectionBackground( hasFocus ? focusedSelectionBg : unfocusedSelectionBg ); + } + + public List< SourceAndConverter< ? > > getSelectedSources() + { + final List< SourceAndConverter< ? > > sources = new ArrayList<>(); + for ( final int row : getSelectedRows() ) + sources.add( model.getValueAt( row ).getSource() ); + return sources; + } + + public List< ConverterSetup > getSelectedConverterSetups() + { + return converters.getConverterSetups( getSelectedSources() ); + } + + private void installActions( final InputTriggerConfig inputTriggerConfig ) + { + final InputActionBindings keybindings = InputActionBindings.installNewBindings( this, JComponent.WHEN_FOCUSED, false ); + final Actions actions = new Actions( inputTriggerConfig, "bdv" ); + actions.install( keybindings, "source table" ); + actions.runnableAction( () -> toggleSelectedActive(), "toggle active", "A" ); + actions.runnableAction( () -> makeSelectedActive( true ), "set active", "not mapped" ); + actions.runnableAction( () -> makeSelectedActive( false ), "set inactive", "not mapped" ); + actions.runnableAction( () -> cycleSelectedCurrent(), "cycle current", "C" ); + } + + private void makeSelectedActive( final boolean active ) + { + final List< SourceAndConverter< ? > > selectedSources = getSelectedSources(); + state.setSourcesActive( selectedSources, active ); + } + + private void toggleSelectedActive() + { + final List< SourceAndConverter< ? > > selectedSources = getSelectedSources(); + if ( !selectedSources.isEmpty() ) + state.setSourcesActive( selectedSources, !state.isSourceActive( selectedSources.get( 0 ) ) ); + } + + private void cycleSelectedCurrent() + { + final List< SourceAndConverter< ? > > selectedSources = getSelectedSources(); + if ( !selectedSources.isEmpty() ) + { + final SourceAndConverter< ? > current = state.getCurrentSource(); + final int i = ( selectedSources.indexOf( current ) + 1 ) % selectedSources.size(); + state.setCurrentSource( selectedSources.get( i ) ); + } + } + + // -- Process clicks on active and current checkboxes -- + // These clicks are consumed, because they should not cause selection changes, etc, in the table. + + private Point pressedAt; + private boolean consumeNext = false; + private long releasedWhen = 0; + + @Override + protected void processMouseEvent( final MouseEvent e ) + { + if ( e.getModifiers() == InputEvent.BUTTON1_MASK ) + { + if ( e.getID() == MouseEvent.MOUSE_PRESSED ) + { + final Point point = e.getPoint(); + pressedAt = point; + final int vcol = columnAtPoint( point ); + final int vrow = rowAtPoint( point ); + if ( vcol >= 0 && vrow >= 0 ) + { + final int mcol = convertColumnIndexToModel( vcol ); + switch ( mcol ) + { + case IS_ACTIVE_COLUMN: + case IS_CURRENT_COLUMN: + case COLOR_COLUMN: + final int mrow = convertRowIndexToModel( vrow ); + if ( isRowSelected( mrow ) ) + { + e.consume(); + consumeNext = true; + } + } + } + } + else if ( e.getID() == MouseEvent.MOUSE_RELEASED ) + { + if ( consumeNext ) + { + releasedWhen = e.getWhen(); + consumeNext = false; + e.consume(); + } + + if ( pressedAt == null ) + return; + + final Point point = e.getPoint(); + if ( point.distanceSq( pressedAt ) > 2 ) + return; + + final int vcol = columnAtPoint( point ); + final int vrow = rowAtPoint( point ); + if ( vcol >= 0 && vrow >= 0 ) + { + final int mcol = convertColumnIndexToModel( vcol ); + switch ( mcol ) + { + case IS_ACTIVE_COLUMN: + case IS_CURRENT_COLUMN: + case COLOR_COLUMN: + final int mrow = convertRowIndexToModel( vrow ); + final SourceAndConverter< ? > source = model.getValueAt( mrow ).getSource(); + if ( mcol == IS_ACTIVE_COLUMN ) + { + if ( isRowSelected( mrow ) ) + state.setSourcesActive( getSelectedSources(), !state.isSourceActive( source ) ); + else + state.setSourceActive( source, !state.isSourceActive( source ) ); + } + else if ( mcol == IS_CURRENT_COLUMN ) + { + state.setCurrentSource( source ); + } + else // if ( mcol == COLOR_COLUMN ) + { + converters.getConverterSetup( source ); + final ConverterSetup c = converters.getConverterSetup( source ); + if ( c != null && c.supportsColor() ) + { + final Color newColor = JColorChooser.showDialog( null, "Set Source Color", new Color( c.getColor().get() ) ); + if ( newColor != null ) + { + final ARGBType color = new ARGBType( newColor.getRGB() | 0xff000000 ); + if ( isRowSelected( mrow ) ) + { + for ( final SourceAndConverter< ? > s : getSelectedSources() ) + { + final ConverterSetup cs = converters.getConverterSetup( s ); + if ( cs != null && cs.supportsColor() ) + cs.setColor( color ); + } + } + else + c.setColor( color ); + } + } + } + } + } + } + else if ( e.getID() == MouseEvent.MOUSE_CLICKED ) + { + if ( e.getWhen() == releasedWhen ) + e.consume(); + } + } + super.processMouseEvent( e ); + } + + @Override + protected void processMouseMotionEvent( final MouseEvent e ) + { + if ( consumeNext && e.getModifiers() == InputEvent.BUTTON1_MASK && e.getID() == MouseEvent.MOUSE_DRAGGED ) + e.consume(); + super.processMouseMotionEvent( e ); + } + + @Override + public SourceTableModel getModel() + { + return model; + } + + /** + * {@code TransferHandler} for transferring sources out of a + * {@code SourceTable} via cut/copy/paste and drag and drop. + */ + static class SourceTableTransferHandler extends TransferHandler + { + @Override + public int getSourceActions( final JComponent c ) + { + return TransferHandler.LINK; + } + + @Override + protected Transferable createTransferable( final JComponent c ) + { + if ( ! ( c instanceof JTable ) ) + return null; + + final JTable table = ( JTable ) c; + + if ( ! ( table.getModel() instanceof SourceTableModel ) ) + return null; + + final SourceTableModel model = ( SourceTableModel ) table.getModel(); + + final List< SourceAndConverter< ? > > sources = new ArrayList<>(); + for ( final int row : table.getSelectedRows() ) + sources.add( model.getValueAt( row ).getSource() ); + + return new SourcesTransferable( sources ); + } + + @Override + public boolean canImport( final TransferSupport support ) + { + return false; + } + } +} diff --git a/src/main/java/bdv/ui/sourcetable/SourceTableModel.java b/src/main/java/bdv/ui/sourcetable/SourceTableModel.java new file mode 100644 index 0000000000000000000000000000000000000000..ec52ec8879c3e8b5feddd4740aab47b8c7cd596a --- /dev/null +++ b/src/main/java/bdv/ui/sourcetable/SourceTableModel.java @@ -0,0 +1,355 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.sourcetable; + +import bdv.tools.brightness.ConverterSetup; +import bdv.util.WrappedList; +import bdv.viewer.ConverterSetups; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.SourceToConverterSetupBimap; +import bdv.viewer.ViewerState; +import gnu.trove.map.TObjectIntMap; +import gnu.trove.map.hash.TObjectIntHashMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import javax.swing.SwingUtilities; +import javax.swing.table.AbstractTableModel; +import net.imglib2.type.numeric.ARGBType; + +import static gnu.trove.impl.Constants.DEFAULT_CAPACITY; +import static gnu.trove.impl.Constants.DEFAULT_LOAD_FACTOR; + +/** + * @author Tobias Pietzsch + */ +public class SourceTableModel extends AbstractTableModel +{ + private final ViewerState state; + + private final SourceToConverterSetupBimap converters; + + private StateModel model; + + public static final int NAME_COLUMN = 0; + public static final int IS_CURRENT_COLUMN = 1; + public static final int IS_ACTIVE_COLUMN = 2; + public static final int COLOR_COLUMN = 3; + + public SourceTableModel( final ViewerState state, final ConverterSetups converterSetups ) + { + this.state = state; + this.converters = converterSetups; + model = new StateModel( state ); + + converterSetups.listeners().add( converterSetup -> + { + final SourceAndConverter< ? > source = converters.getSource( converterSetup ); + if ( source != null ) + { + final SourceModel sourceModel = new SourceModel( source, state ); + SwingUtilities.invokeLater( () -> { + final int row = model.getSources().indexOf( sourceModel ); + if ( row != -1 ) + fireTableRowsUpdated( row, row ); + } ); + } + } ); + + state.changeListeners().add( e -> + { + switch ( e ) + { + case CURRENT_SOURCE_CHANGED: + case SOURCE_ACTIVITY_CHANGED: + case NUM_SOURCES_CHANGED: + final StateModel model = new StateModel( state ); + SwingUtilities.invokeLater( () -> analyzeChanges( model ) ); + } + } ); + } + + @Override + public int getRowCount() + { + return model.getSources().size(); + } + + @Override + public int getColumnCount() + { + return 4; + } + + @Override + public String getColumnName( final int column ) + { + switch( column ) + { + case NAME_COLUMN: + return "name"; + case IS_ACTIVE_COLUMN: + return "active"; + case IS_CURRENT_COLUMN: + return "current"; + case COLOR_COLUMN: + return "color"; + default: + throw new IllegalArgumentException(); + } + } + + @Override + public Object getValueAt( final int rowIndex, final int columnIndex ) + { + final SourceModel source = model.getSources().get( rowIndex ); + switch( columnIndex ) + { + case NAME_COLUMN: + return source.getName(); + case IS_ACTIVE_COLUMN: + return source.isActive(); + case IS_CURRENT_COLUMN: + return source.isCurrent(); + case COLOR_COLUMN: + final ConverterSetup c = converters.getConverterSetup( source.getSource() ); + return ( c != null && c.supportsColor() ) ? c.getColor() : null; + default: + throw new IllegalArgumentException(); + } + } + + @Override + public Class< ? > getColumnClass( final int columnIndex ) + { + switch( columnIndex ) + { + case NAME_COLUMN: + return String.class; + case IS_ACTIVE_COLUMN: + case IS_CURRENT_COLUMN: + return Boolean.class; + case COLOR_COLUMN: + return ARGBType.class; + default: + throw new IllegalArgumentException(); + } + } + + @Override + public boolean isCellEditable( final int rowIndex, final int columnIndex ) + { + return columnIndex != 0; + } + + public SourceModel getValueAt( final int rowIndex ) + { + return model.getSources().get( rowIndex ); + } + + private void analyzeChanges( final StateModel model ) + { + final StateModel previousModel = this.model; + this.model = model; + + // -- NUM_SOURCES_CHANGED -- + + final List< SourceModel > removedSources = new ArrayList<>(); + for ( SourceModel source : previousModel.getSources() ) + if ( !model.getSources().contains( source ) ) + removedSources.add( source ); + + final List< SourceModel > addedSources = new ArrayList<>(); + for ( SourceModel source : model.getSources() ) + if ( !previousModel.getSources().contains( source ) ) + addedSources.add( source ); + + // -- CURRENT_SOURCE_CHANGED, SOURCE_ACTIVITY_CHANGED -- + + final List< SourceModel > changedSources = new ArrayList<>(); + for ( SourceModel source : model.getSources() ) + { + final SourceModel previousSource = previousModel.getSources().get( source ); + if ( previousSource != null ) + { + if ( source.isCurrent() != previousSource.isCurrent() || + source.isActive() != previousSource.isActive() ) + { + changedSources.add( source ); + } + } + } + + // -- create corresponding TableModelEvents -- + + // sources added or removed + if ( !addedSources.isEmpty() ) + { + final int firstRow = model.getSources().indexOf( addedSources.get( 0 ) ); + final int lastRow = model.getSources().indexOf( addedSources.get( addedSources.size() - 1 ) ); + fireTableRowsInserted( firstRow, lastRow ); + } + else if ( !removedSources.isEmpty() ) + { + final int firstRow = previousModel.getSources().indexOf( removedSources.get( 0 ) ); + final int lastRow = previousModel.getSources().indexOf( removedSources.get( removedSources.size() - 1 ) ); + fireTableRowsDeleted( firstRow, lastRow ); + } + + // sources that changed currentness or activeness + if ( !changedSources.isEmpty() ) + { + final int firstRow = model.getSources().indexOf( changedSources.get( 0 ) ); + final int lastRow = model.getSources().indexOf( changedSources.get( changedSources.size() - 1 ) ); + fireTableRowsUpdated( firstRow, lastRow ); + } + } + + // + // Internal state model + // + + private static final int NO_ENTRY_VALUE = -1; + + static class StateModel + { + private final StateModel.UnmodifiableSources sources; + + public StateModel( final ViewerState state ) + { + final List< SourceModel > slist = new ArrayList<>(); + final TObjectIntMap< SourceModel > sindices = new TObjectIntHashMap<>( DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, NO_ENTRY_VALUE ); + final List< SourceAndConverter< ? > > ssources = state.getSources(); + for ( int i = 0; i < ssources.size(); ++i ) + { + final SourceModel sourceModel = new SourceModel( ssources.get( i ), state ); + slist.add( sourceModel ); + sindices.put( sourceModel, i ); + } + sources = new StateModel.UnmodifiableSources( slist, sindices ); + } + + public StateModel.UnmodifiableSources getSources() + { + return sources; + } + + static class UnmodifiableSources extends WrappedList< SourceModel > + { + private final TObjectIntMap< SourceModel > sourceIndices; + + public UnmodifiableSources( final List< SourceModel > sources, final TObjectIntMap< SourceModel > sourceIndices ) + { + super( Collections.unmodifiableList( sources ) ); + this.sourceIndices = sourceIndices; + } + + public SourceModel get( SourceModel sourceModel ) + { + final int index = sourceIndices.get( sourceModel ); + return index == NO_ENTRY_VALUE ? null : get( index ); + } + + @Override + public boolean contains( final Object o ) + { + return sourceIndices.containsKey( o ); + } + + @Override + public boolean containsAll( final Collection< ? > c ) + { + return sourceIndices.keySet().containsAll( c ); + } + + @Override + public int indexOf( final Object o ) + { + return sourceIndices.get( o ); + } + + @Override + public int lastIndexOf( final Object o ) + { + return sourceIndices.get( o ); + } + } + } + + static class SourceModel + { + private final String name; + private final boolean active; + private final boolean current; + + private final SourceAndConverter< ? > source; + + public SourceModel( final SourceAndConverter< ? > source, final ViewerState state ) + { + name = source.getSpimSource().getName(); + active = state.isSourceActive( source ); + current = state.isCurrentSource( source ); + + this.source = source; + } + + public String getName() + { + return name; + } + + public boolean isActive() + { + return active; + } + + public boolean isCurrent() + { + return current; + } + + public SourceAndConverter<?> getSource() + { + return source; + } + + @Override + public boolean equals( final Object o ) + { + return ( o instanceof SourceModel ) && source.equals( ( ( SourceModel ) o ).source ); + } + + @Override + public int hashCode() + { + return source.hashCode(); + } + } +} diff --git a/src/main/java/bdv/ui/splitpanel/SplitPaneOneTouchExpandAnimator.java b/src/main/java/bdv/ui/splitpanel/SplitPaneOneTouchExpandAnimator.java new file mode 100644 index 0000000000000000000000000000000000000000..2438d858d5370649b6aec441016f48b7b67f0f6e --- /dev/null +++ b/src/main/java/bdv/ui/splitpanel/SplitPaneOneTouchExpandAnimator.java @@ -0,0 +1,495 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.splitpanel; + +import bdv.viewer.animate.OverlayAnimator; +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.util.function.BooleanSupplier; +import javax.swing.ImageIcon; + +import static bdv.ui.splitpanel.SplitPaneOneTouchExpandAnimator.AnimationType.SHOW_COLLAPSE; +import static bdv.ui.splitpanel.SplitPaneOneTouchExpandAnimator.AnimationType.SHOW_EXPAND; +import static bdv.ui.splitpanel.SplitPaneOneTouchExpandAnimator.AnimationType.HIDE_COLLAPSE; +import static bdv.ui.splitpanel.SplitPaneOneTouchExpandAnimator.AnimationType.HIDE_EXPAND; +import static bdv.ui.splitpanel.SplitPaneOneTouchExpandAnimator.AnimationType.NONE; + +/** + * @author Tim-Oliver Buchholz + * @author Tobias Pietzsch + */ +class SplitPaneOneTouchExpandAnimator implements OverlayAnimator +{ + private final BooleanSupplier isCollapsed; + + private final ImageIcon rightArrowIcon; + private final ImageIcon leftArrowIcon; + private final int imgw; + private final int imgh; + + private int borderWidth; + private int triggerHeight; + + private final double animationSpeed = 0.09; + private final float backgroundAlpha = 0.65f; + + private float alpha = 1.0f; + + private int viewPortWidth; + private int viewPortHeight; + + /** + * @param isCollapsed provides collapsed state to decide whether to display left-arrow of right-arrow icon + */ + public SplitPaneOneTouchExpandAnimator( final BooleanSupplier isCollapsed ) + { + this.isCollapsed = isCollapsed; + rightArrowIcon = new ImageIcon( SplitPaneOneTouchExpandAnimator.class.getResource( "rightdoublearrow_tiny.png" ) ); + leftArrowIcon = new ImageIcon( SplitPaneOneTouchExpandAnimator.class.getResource( "leftdoublearrow_tiny.png" ) ); + imgw = leftArrowIcon.getIconWidth(); + imgh = leftArrowIcon.getIconHeight(); + borderWidth = imgw + 10; + triggerHeight = imgh + 10; + } + + public enum AnimationType + { + SHOW_EXPAND, + HIDE_EXPAND, + SHOW_COLLAPSE, + HIDE_COLLAPSE, + NONE + } + + public synchronized void startAnimation( final AnimationType animationType ) + { + requestedAnimationType = animationType; + } + + private AnimationType requestedAnimationType = NONE; + + private long last_time; + + @Override + public void paint( final Graphics2D g, final long time ) + { + viewPortWidth = g.getClipBounds().width; + viewPortHeight = g.getClipBounds().height; + + if ( requestedAnimationType != NONE ) + { + if ( animator == null || animator.animationType() != requestedAnimationType ) + { + last_time = time; + + switch ( requestedAnimationType ) + { + case SHOW_EXPAND: + animator = new ShowExpandButton( animator ); + break; + case HIDE_EXPAND: + animator = new HideExpandButton( animator ); + break; + case SHOW_COLLAPSE: + animator = new ShowCollapseButton(); + break; + case HIDE_COLLAPSE: + animator = new HideCollapseButton(); + break; + } + } + requestedAnimationType = NONE; + } + + if ( animator != null ) + { + final long delta_time = time - last_time; + last_time = time; + + paintState = animator.animate( delta_time ); + if ( animator.isComplete() ) + animator = null; + } + + if ( paintState != null ) + paint( g, paintState ); + } + + void clearPaintState() + { + paintState = null; + } + + @Override + public boolean isComplete() + { + return false; + } + + @Override + public boolean requiresRepaint() + { + return animator != null; + } + + + // == TRIGGER REGION ===================================================== + + public boolean isInBorderRegion( final int x, final int y ) + { + return x > viewPortWidth - borderWidth; + } + + public boolean isInTriggerRegion( final int x, final int y ) + { + return x > viewPortWidth - borderWidth && Math.abs( viewPortHeight - 2 * y ) < triggerHeight; + } + + + // == PAINTING =========================================================== + + private static class PaintState + { + final double imgRatio; + final double bgRatio; + final double bumpRatio; + final float alpha; + + private PaintState( final double imgRatio, final double bgRatio, final double bumpRatio, final float alpha ) + { + this.imgRatio = imgRatio; + this.bgRatio = bgRatio; + this.bumpRatio = bumpRatio; + this.alpha = alpha; + } + } + + private PaintState paintState; + + private void paint( final Graphics2D g, final PaintState state ) + { + final int imgX = viewPortWidth - ( int ) ( imgw * state.imgRatio + 10 * state.bumpRatio ); + final int bgX = viewPortWidth - ( int ) ( imgw * state.bgRatio + 10 * state.bumpRatio ); + final int y = ( viewPortHeight - imgh ) / 2; + + drawBackground( g, bgX, y, state.alpha ); + drawImg( g, isCollapsed.getAsBoolean() ? leftArrowIcon : rightArrowIcon, imgX, y, state.alpha ); + } + + private void drawBackground( final Graphics2D g, final int x, final int y, final float alpha ) + { + final int width = imgw + 60; + final int height = imgh; + + g.setColor( new Color( 0.28f, 0.5f, 0.96f, Math.min( alpha, backgroundAlpha ) ) ); + g.fillRoundRect( x, y, width, height, 25, 25 ); + } + + private void drawImg( final Graphics2D g, final ImageIcon img, final int x, final int y, final float alpha ) + { + Composite oldComposite = g.getComposite(); + final AlphaComposite alcom = AlphaComposite.getInstance( AlphaComposite.SRC_OVER, alpha ); + g.setComposite( alcom ); + g.drawImage( img.getImage(), x, y, null ); + g.setComposite(oldComposite); + } + + + // == ANIMATOR HELPERS =================================================== + + /** + * Cosine shape acceleration/ deceleration curve of linear [0,1] + */ + private static double cos( final double t ) + { + return 0.5 - 0.5 * Math.cos( Math.PI * t ); + } + + // TODO: Animator implementations should maintain their own private alpha instead of using this shared method + private void updateAlpha( final boolean fadeIn, final long delta_time ) + { + if ( fadeIn ) + { + alpha += ( 1 / 250.0 ) * delta_time; + alpha = Math.min( 1, alpha ); + } + else + { + alpha -= ( 1 / 250.0 ) * delta_time; + alpha = Math.max( 0.25f, alpha ); + } + } + + + // == ANIMATORS ========================================================== + + private interface Animator + { + PaintState animate( final long delta_time ); + + AnimationType animationType(); + + boolean isComplete(); + } + + private Animator animator; + + // ======================================================================= + + private class HideCollapseButton implements Animator + { + private boolean complete = false; + + @Override + public PaintState animate( final long delta_time ) + { + // Fade-out + updateAlpha( false, delta_time ); + + complete = alpha <= 0.25; + + return new PaintState( 1, 1, 0, alpha ); + } + + @Override + public AnimationType animationType() + { + return HIDE_COLLAPSE; + } + + @Override + public boolean isComplete() + { + return complete; + } + } + + // ======================================================================= + + private class ShowCollapseButton implements Animator + { + // TODO (?) + // Slide image 10px to the left + private final double bumpAnimationSpeed = animationSpeed / 10; + + private int keyFrame = 0; + + // ratio to which the image is slid left (0 = all the way right, 1 = all the way left) + private double bumpRatio = 0; + + private boolean complete = false; + + @Override + public PaintState animate( final long delta_time ) + { + if ( keyFrame == 0 ) + { + // Slide image to the left + bumpRatio += bumpAnimationSpeed * delta_time; + bumpRatio = Math.min( 1, Math.max( 0, bumpRatio ) ); + if ( bumpRatio == 1 ) + keyFrame = 1; + + } + else if ( keyFrame == 1 ) + { + // Slide image back to the initial position + bumpRatio -= bumpAnimationSpeed * delta_time; + bumpRatio = Math.min( 1, Math.max( 0, bumpRatio ) ); + if ( bumpRatio == 0 ) + keyFrame = 2; + } + + // Fade-in + updateAlpha( true, delta_time ); + + complete = keyFrame >= 2; + + return new PaintState( 1, 1, cos( bumpRatio ), alpha ); + } + + @Override + public AnimationType animationType() + { + return SHOW_COLLAPSE; + } + + @Override + public boolean isComplete() + { + return complete; + } + } + + // ======================================================================= + + private class HideExpandButton implements Animator + { + // TODO (?) + private final double expandAnimationSpeed = animationSpeed / imgw; + + // ratio to which the image is expanded (0 = hidden, 1 = fully expanded) + private double expandRatio; + + private boolean complete = false; + + public HideExpandButton( Animator currentAnimator ) + { + if ( currentAnimator != null && !currentAnimator.isComplete() && currentAnimator instanceof ShowExpandButton ) + // if an incomplete ShowExpandButton animation is running, initialize expandRatio to match + expandRatio = ( ( ShowExpandButton ) currentAnimator ).expandRatio; + else + // otherwise start from fully expanded image + expandRatio = 1; + } + + @Override + public PaintState animate( final long delta_time ) + { + + // Speed up animation by factor 2 + expandRatio -= 2 * expandAnimationSpeed * delta_time; + expandRatio = Math.min( 1, Math.max( 0, expandRatio ) ); + + // Fade-out + updateAlpha( false, delta_time ); + + complete = expandRatio <= 0; + + final double imgRatio = cos( expandRatio ); + return new PaintState( imgRatio, imgRatio, 0, alpha ); + } + + @Override + public AnimationType animationType() + { + return HIDE_EXPAND; + } + + @Override + public boolean isComplete() + { + return complete; + } + } + + // ======================================================================= + + private class ShowExpandButton implements Animator + { + // TODO (?) + private final double expandAnimationSpeed = animationSpeed / imgw; + + private int keyFrame; + + // ratio to which the image is expanded (0 = hidden, 1 = fully expanded) + private double expandRatio; + + private boolean complete = false; + + public ShowExpandButton( Animator currentAnimator ) + { + if ( currentAnimator != null && !currentAnimator.isComplete() && currentAnimator instanceof HideExpandButton ) + // if an incomplete HideExpandButton animation is running, initialize expandRatio to match + expandRatio = ( ( HideExpandButton ) currentAnimator ).expandRatio; + else + // otherwise start from fully hidden image + expandRatio = 0; + + // initialize keyFrame based on expandRatio + if ( expandRatio > 0 ) + keyFrame = 1; + else + keyFrame = 0; + } + + @Override + public PaintState animate( final long delta_time ) + { + // Slide image in + if ( keyFrame == 0 ) + { + // Slide image in with doubled speed + expandRatio += delta_time * 2 * expandAnimationSpeed; + expandRatio = Math.min( 1, Math.max( 0, expandRatio ) ); + if ( expandRatio == 1 ) + { + keyFrame = 1; + } + } + else if ( keyFrame == 1 ) + { + // Move it half back + expandRatio -= delta_time * expandAnimationSpeed; + expandRatio = Math.min( 1, Math.max( 0, expandRatio ) ); + if ( expandRatio <= 0.5 ) + { + keyFrame = 2; + } + } + else if ( keyFrame == 2 ) + { + // And move it again full in + expandRatio += delta_time * expandAnimationSpeed; + expandRatio = Math.min( 1, Math.max( 0, expandRatio ) ); + if ( expandRatio == 1 ) + { + keyFrame = 3; + } + } + else + { + expandRatio = 1; + } + + // Fade-in + updateAlpha( true, delta_time ); + + complete = keyFrame >= 3; + + final double imgRatio = cos( expandRatio ); + final double bgRatio = keyFrame > 0 ? 1 : imgRatio; // Background should only move out and stay + return new PaintState( imgRatio, bgRatio, 0, alpha ); + } + + @Override + public AnimationType animationType() + { + return SHOW_EXPAND; + } + + @Override + public boolean isComplete() + { + return complete; + } + } +} diff --git a/src/main/java/bdv/ui/splitpanel/SplitPaneOneTouchExpandTrigger.java b/src/main/java/bdv/ui/splitpanel/SplitPaneOneTouchExpandTrigger.java new file mode 100644 index 0000000000000000000000000000000000000000..35c8fdc29cacf62b51906f44901b92bf41c3820a --- /dev/null +++ b/src/main/java/bdv/ui/splitpanel/SplitPaneOneTouchExpandTrigger.java @@ -0,0 +1,142 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.splitpanel; + +import bdv.ui.splitpanel.SplitPaneOneTouchExpandAnimator.AnimationType; +import bdv.viewer.ViewerPanel; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import static bdv.ui.splitpanel.SplitPaneOneTouchExpandAnimator.AnimationType.HIDE_COLLAPSE; +import static bdv.ui.splitpanel.SplitPaneOneTouchExpandAnimator.AnimationType.HIDE_EXPAND; +import static bdv.ui.splitpanel.SplitPaneOneTouchExpandAnimator.AnimationType.SHOW_COLLAPSE; +import static bdv.ui.splitpanel.SplitPaneOneTouchExpandAnimator.AnimationType.SHOW_EXPAND; + +class SplitPaneOneTouchExpandTrigger extends MouseAdapter +{ + private final SplitPaneOneTouchExpandAnimator animator; + + private final SplitPanel splitPanel; + + private final ViewerPanel viewer; + + public SplitPaneOneTouchExpandTrigger( final SplitPaneOneTouchExpandAnimator animator, final SplitPanel splitPanel, final ViewerPanel viewer ) + { + this.animator = animator; + this.splitPanel = splitPanel; + this.viewer = viewer; + } + + @Override + public void mouseMoved( final MouseEvent e ) + { + final int x = e.getX(); + final int y = e.getY(); + + // check whether in border region + if ( animator.isInBorderRegion( x, y ) ) + checkEnterBorderRegion(); + else + checkExitBorderRegion(); + + // check whether in trigger region + if ( animator.isInTriggerRegion( x, y ) ) + checkEnterTriggerRegion(); + else + checkExitTriggerRegion(); + } + + @Override + public void mouseExited( final MouseEvent e ) + { + checkExitBorderRegion(); + checkExitTriggerRegion(); + } + + @Override + public void mouseClicked( final MouseEvent e ) + { + if ( animator.isInTriggerRegion( e.getX(), e.getY() ) ) + { + splitPanel.setCollapsed( !splitPanel.isCollapsed() ); + viewer.requestRepaint(); + checkExitBorderRegion(); + checkExitTriggerRegion(); + } + } + + private boolean inBorderRegion = false; + private boolean inTriggerRegion = false; + + private void checkExitTriggerRegion() + { + if ( inTriggerRegion ) + { + inTriggerRegion = false; + if ( !splitPanel.isCollapsed() ) + startAnimation( HIDE_COLLAPSE ); + } + } + + private void checkEnterTriggerRegion() + { + if ( !inTriggerRegion ) + { + inTriggerRegion = true; + if ( !splitPanel.isCollapsed() ) + startAnimation( SHOW_COLLAPSE ); + } + } + + private void checkExitBorderRegion() + { + if ( inBorderRegion ) + { + inBorderRegion = false; + if ( splitPanel.isCollapsed() ) + startAnimation( HIDE_EXPAND ); + } + } + + private void checkEnterBorderRegion() + { + if ( !inBorderRegion ) + { + inBorderRegion = true; + if ( splitPanel.isCollapsed() ) + startAnimation( SHOW_EXPAND ); + } + } + + private void startAnimation( final AnimationType animationType ) + { + animator.startAnimation( animationType ); + viewer.getDisplay().repaint(); + } +} diff --git a/src/main/java/bdv/ui/splitpanel/SplitPanel.java b/src/main/java/bdv/ui/splitpanel/SplitPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..cc4b65bc69d43daafd11906491eabe1b1418f4d0 --- /dev/null +++ b/src/main/java/bdv/ui/splitpanel/SplitPanel.java @@ -0,0 +1,205 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.splitpanel; + +import bdv.ui.CardPanel; +import bdv.viewer.ViewerPanel; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import javax.swing.InputMap; +import javax.swing.JComponent; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.KeyStroke; +import javax.swing.border.Border; +import javax.swing.border.EmptyBorder; +import javax.swing.plaf.basic.BasicSplitPaneDivider; +import javax.swing.plaf.basic.BasicSplitPaneUI; +import org.scijava.ui.behaviour.io.InputTriggerConfig; +import org.scijava.ui.behaviour.util.Actions; + +/** + * A {@code JSplitPane} with a {@code ViewerPanel} on the left and a + * {@code CardPanel} on the right. Animated arrows are added to the + * {@code ViewerPanel}, such that the right ({@code CardPanel}) pane can be + * fully collapsed or exanded. The {@code CardPanel} can be also + * programmatically collapsed or exanded using {@link #setCollapsed(boolean)}. + * + * @author Tim-Oliver Buchholz + * @author Tobias Pietzsch + */ +public class SplitPanel extends JSplitPane +{ + private static final int DEFAULT_DIVIDER_SIZE = 3; + + private static final String FOCUS_VIEWER_PANEL = "focus viewer panel"; + private static final String HIDE_CARD_PANEL = "hide card panel"; + + private final JScrollPane scrollPane; + + private int width; + + private final SplitPaneOneTouchExpandAnimator oneTouchExpandAnimator; + + public SplitPanel( final ViewerPanel viewerPanel, final CardPanel cardPanel ) + { + super( JSplitPane.HORIZONTAL_SPLIT ); + + configureSplitPane(); + + final JComponent cardPanelComponent = cardPanel.getComponent(); + scrollPane = new JScrollPane( cardPanelComponent ); + scrollPane.setBorder( new EmptyBorder( 0, 0, 0, 0 ) ); + scrollPane.setHorizontalScrollBarPolicy( JScrollPane.HORIZONTAL_SCROLLBAR_NEVER ); + scrollPane.setPreferredSize( new Dimension( 800, 200 ) ); + + final InputMap inputMap = scrollPane.getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + inputMap.put( KeyStroke.getKeyStroke( "F6" ), "none" ); + + final InputTriggerConfig inputTriggerConfig = viewerPanel.getOptionValues().getInputTriggerConfig(); + final Actions actions = new Actions( inputMap, scrollPane.getActionMap(), inputTriggerConfig, "bdv" ); + actions.runnableAction( viewerPanel::requestFocusInWindow, FOCUS_VIEWER_PANEL, "ESCAPE" ); + actions.runnableAction( () -> { + setCollapsed( true ); + viewerPanel.requestFocusInWindow(); + }, HIDE_CARD_PANEL, "shift ESCAPE" ); + + setLeftComponent( viewerPanel ); + setRightComponent( null ); + setBorder( null ); + setPreferredSize( viewerPanel.getPreferredSize() ); + + super.setDividerSize( 0 ); + + oneTouchExpandAnimator = new SplitPaneOneTouchExpandAnimator( this::isCollapsed ); + viewerPanel.addOverlayAnimator( oneTouchExpandAnimator ); + + final SplitPaneOneTouchExpandTrigger oneTouchExpandTrigger = new SplitPaneOneTouchExpandTrigger( oneTouchExpandAnimator, this, viewerPanel ); + viewerPanel.getDisplay().addMouseMotionListener( oneTouchExpandTrigger ); + viewerPanel.getDisplay().addMouseListener( oneTouchExpandTrigger ); + + setDividerSize( DEFAULT_DIVIDER_SIZE ); + + addComponentListener( new ComponentAdapter() + { + @Override + public void componentResized( final ComponentEvent e ) + { + final int w = getWidth(); + if ( width > 0 ) + { + final int dl = getLastDividerLocation() + w - width; + setLastDividerLocation( Math.max( 50, dl ) ); + } + else + { + // When the component is first made visible, set LastDividerLocation to a reasonable value + setDividerLocation( w ); + setLastDividerLocation( Math.max( w / 2, w - Math.max( 200, cardPanelComponent.getPreferredSize().width ) ) ); + } + width = w; + } + } ); + } + + private void configureSplitPane() + { + this.setUI( new BasicSplitPaneUI() + { + @Override + public BasicSplitPaneDivider createDefaultDivider() + { + return new BasicSplitPaneDivider( this ) + { + private static final long serialVersionUID = 1L; + + @Override + public void paint( final Graphics g ) + { + g.setColor( Color.white ); + g.fillRect( 0, 0, getSize().width, getSize().height ); + super.paint( g ); + } + + @Override + public void setBorder( final Border border ) + { + super.setBorder( null ); + } + }; + } + } ); + this.setForeground( Color.white ); + this.setBackground( Color.white ); + this.setResizeWeight( 1.0 ); + this.setContinuousLayout( true ); + } + + // divider size set externally + private int dividerSizeWhenVisible = DEFAULT_DIVIDER_SIZE; + + @Override + public void setDividerSize( final int newSize ) + { + dividerSizeWhenVisible = newSize; + } + + /** + * Un/collapse the UI-Panel. + */ + public void setCollapsed( final boolean collapsed ) + { + if ( isCollapsed() == collapsed ) + return; + + oneTouchExpandAnimator.clearPaintState(); + if ( collapsed ) + { + setRightComponent( null ); + super.setDividerSize( 0 ); + setDividerLocation( 1.0d ); + } + else + { + setRightComponent( scrollPane ); + super.setDividerSize( dividerSizeWhenVisible ); + final int dl = getLastDividerLocation(); + final int w = getWidth(); + setDividerLocation( Math.max( Math.min ( w / 2, 50 ), Math.min( w - 50, dl ) ) ); + } + } + + public boolean isCollapsed() + { + return getRightComponent() == null; + } +} diff --git a/src/main/java/bdv/ui/viewermodepanel/DisplaySettingsPanel.java b/src/main/java/bdv/ui/viewermodepanel/DisplaySettingsPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..e938a923cde97458ec118151566858291cc462c0 --- /dev/null +++ b/src/main/java/bdv/ui/viewermodepanel/DisplaySettingsPanel.java @@ -0,0 +1,128 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.viewermodepanel; + +import bdv.viewer.DisplayMode; +import bdv.viewer.Interpolation; +import bdv.viewer.ViewerState; +import java.awt.Color; +import javax.swing.ImageIcon; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import net.miginfocom.swing.MigLayout; + +import static bdv.viewer.Interpolation.NEARESTNEIGHBOR; +import static bdv.viewer.Interpolation.NLINEAR; +import static bdv.viewer.ViewerStateChange.DISPLAY_MODE_CHANGED; +import static bdv.viewer.ViewerStateChange.INTERPOLATION_CHANGED; + +/** + * This panel adds buttons to toggle fused, grouped, and + * interpolation mode. + * + * @author Tim-Oliver Buchholz, CSBD/MPI-CBG, Dresden + * @author Tobias Pietzsch + */ +public class DisplaySettingsPanel extends JPanel +{ + private static final String SINGLE_MODE_TOOL_TIP = "<html><b>Single</b>/Fused</html>"; + private static final String FUSED_MODE_TOOL_TIP = "<html>Single/<b>Fused</b></html>"; + private static final String GROUP_MODE_TOOL_TIP = "<html>Source/<b>Group</b></html>"; + private static final String SOURCE_MODE_TOOL_TIP = "<html><b>Source</b>/Group</html>"; + private static final String NEAREST_INTERPOLATION_TOOL_TIP = "<html><b>Nearest</b>/Linear</html>"; + private static final String LINEAR_INTERPOLATION_TOOL_TIP = "<html>Nearest/<b>Linear</b></html>"; + + private final LabeledToggleButton fusion; + private final LabeledToggleButton grouping; + private final LabeledToggleButton interpolation; + + public DisplaySettingsPanel( final ViewerState state ) + { + super( new MigLayout( "ins 0, fillx, filly", "[][][]", "top" ) ); + this.setBackground( Color.white ); + + fusion = new LabeledToggleButton( + new ImageIcon( this.getClass().getResource( "single_mode.png" ) ), + new ImageIcon( this.getClass().getResource( "fusion_mode.png" ) ), + "Single", + "Fused", + SINGLE_MODE_TOOL_TIP, + FUSED_MODE_TOOL_TIP ); + grouping = new LabeledToggleButton( + new ImageIcon( this.getClass().getResource( "source_mode.png" ) ), + new ImageIcon( this.getClass().getResource( "grouping_mode.png" ) ), + "Source", + "Group", + SOURCE_MODE_TOOL_TIP, + GROUP_MODE_TOOL_TIP ); + interpolation = new LabeledToggleButton( + new ImageIcon( this.getClass().getResource( "nearest.png" ) ), + new ImageIcon( this.getClass().getResource( "linear.png" ) ), + "Nearest", + "Linear", + NEAREST_INTERPOLATION_TOOL_TIP, + LINEAR_INTERPOLATION_TOOL_TIP ); + + fusion.setSelected( state.getDisplayMode().hasFused() ); + grouping.setSelected( state.getDisplayMode().hasGrouping() ); + interpolation.setSelected( state.getInterpolation() == NLINEAR ); + + state.changeListeners().add( e -> { + if ( e == DISPLAY_MODE_CHANGED ) + { + final DisplayMode displayMode = state.getDisplayMode(); + SwingUtilities.invokeLater( () -> { + fusion.setSelected( displayMode.hasFused() ); + grouping.setSelected( displayMode.hasGrouping() ); + } ); + } + else if ( e == INTERPOLATION_CHANGED ) + { + final Interpolation interpolationMode = state.getInterpolation(); + SwingUtilities.invokeLater( () -> interpolation.setSelected( interpolationMode == NLINEAR ) ); + } + } ); + + fusion.addActionListener( e -> { + state.setDisplayMode( state.getDisplayMode().withFused( fusion.isSelected() ) ); + } ); + + grouping.addActionListener( e -> { + state.setDisplayMode( state.getDisplayMode().withGrouping( grouping.isSelected() ) ); + } ); + + interpolation.addActionListener( e -> { + state.setInterpolation( interpolation.isSelected() ? NLINEAR : NEARESTNEIGHBOR ); + } ); + + this.add( fusion ); + this.add( grouping ); + this.add( interpolation ); + } +} diff --git a/src/main/java/bdv/ui/viewermodepanel/LabeledToggleButton.java b/src/main/java/bdv/ui/viewermodepanel/LabeledToggleButton.java new file mode 100644 index 0000000000000000000000000000000000000000..1cf8b1ddc119df7eb2adb1463d9335647d5bb77d --- /dev/null +++ b/src/main/java/bdv/ui/viewermodepanel/LabeledToggleButton.java @@ -0,0 +1,70 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.viewermodepanel; + +import java.awt.Font; +import javax.swing.Icon; +import javax.swing.JLabel; + +class LabeledToggleButton extends ToggleButton +{ + private final String text; + private final String selectedText; + + private final JLabel label; + + public LabeledToggleButton( + final Icon icon, + final Icon selectedIcon, + final String text, + final String selectedText, + final String tooltipText, + final String selectedTooltipText ) + { + super( icon, selectedIcon, tooltipText, selectedTooltipText ); + this.text = text; + this.selectedText = selectedText; + + label = new JLabel( text ); + setFont( label ); + + this.add( label, "center" ); + } + + public void setSelected( final boolean selected ) + { + super.setSelected( selected ); + label.setText( selected ? selectedText : text ); + } + + private void setFont( final JLabel label ) + { + label.setFont( new Font( Font.MONOSPACED, Font.BOLD, 9 ) ); + } +} diff --git a/src/main/java/bdv/ui/viewermodepanel/ToggleButton.java b/src/main/java/bdv/ui/viewermodepanel/ToggleButton.java new file mode 100644 index 0000000000000000000000000000000000000000..042fce75405f429c587b23e0bcebb9a832aeb91a --- /dev/null +++ b/src/main/java/bdv/ui/viewermodepanel/ToggleButton.java @@ -0,0 +1,93 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.viewermodepanel; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.event.ActionListener; +import javax.swing.Icon; +import javax.swing.JPanel; +import javax.swing.JToggleButton; +import net.miginfocom.swing.MigLayout; + +class ToggleButton extends JPanel +{ + private final String tooltipText; + private final String selectedTooltipText; + + private final JToggleButton button; + + public ToggleButton( + final Icon icon, + final Icon selectedIcon, + final String tooltipText, + final String selectedTooltipText ) + { + super( new MigLayout( "ins 0, fillx, filly", "[]", "[]0lp![]" ) ); + this.tooltipText = tooltipText; + this.selectedTooltipText = selectedTooltipText; + + button = new JToggleButton( icon ); + button.setSelectedIcon( selectedIcon ); + setLook( button ); + + this.setBackground( Color.white ); + this.add( button, "growx, center, wrap" ); + } + + public void setSelected( final boolean selected ) + { + button.setSelected( selected ); + button.setToolTipText( selected ? selectedTooltipText : tooltipText ); + } + + public boolean isSelected() + { + return button.isSelected(); + } + + public void addActionListener( final ActionListener l ) + { + button.addActionListener( l ); + } + + public void removeActionListener( final ActionListener l ) + { + button.removeActionListener( l ); + } + + private void setLook( final JToggleButton button ) + { + button.setMaximumSize( new Dimension( button.getIcon().getIconWidth(), button.getIcon().getIconHeight() ) ); + button.setBackground( Color.white ); + button.setBorderPainted( false ); + button.setFocusPainted( false ); + button.setContentAreaFilled( false ); + } +} diff --git a/src/main/java/bdv/util/AWTUtils.java b/src/main/java/bdv/util/AWTUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3c1b1a4b099d09ca118175a640516b6028e97511 --- /dev/null +++ b/src/main/java/bdv/util/AWTUtils.java @@ -0,0 +1,113 @@ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.util; + +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.Transparency; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.DirectColorModel; +import java.awt.image.Raster; +import java.awt.image.SampleModel; +import java.awt.image.WritableRaster; +import net.imglib2.display.screenimage.awt.ARGBScreenImage; + +/** + * Static helper methods for setting up {@link GraphicsConfiguration} and + * {@link BufferedImage BufferedImages}. + * + * @author Tobias Pietzsch + */ +public class AWTUtils +{ + /** + * Get a {@link GraphicsConfiguration} from the default screen + * {@link GraphicsDevice} that matches the + * {@link ColorModel#getTransparency() transparency} of the given + * <code>colorModel</code>. If no matching configuration is found, the + * default configuration of the {@link GraphicsDevice} is returned. + */ + public static GraphicsConfiguration getSuitableGraphicsConfiguration( final ColorModel colorModel ) + { + final GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); + final GraphicsConfiguration defaultGc = device.getDefaultConfiguration(); + + final int transparency = colorModel.getTransparency(); + + if ( defaultGc.getColorModel( transparency ).equals( colorModel ) ) + return defaultGc; + + for ( final GraphicsConfiguration gc : device.getConfigurations() ) + if ( gc.getColorModel( transparency ).equals( colorModel ) ) + return gc; + + return defaultGc; + } + + public static final ColorModel ARGB_COLOR_MODEL = new DirectColorModel( 32, 0xff0000, 0xff00, 0xff, 0xff000000 ); + + public static final ColorModel RGB_COLOR_MODEL = new DirectColorModel( 24, 0xff0000, 0xff00, 0xff ); + + /** + * Get a {@link BufferedImage} for the given {@link ARGBScreenImage}. + * + * @param screenImage + * the image. + * @param discardAlpha + * Whether to discard the <code>screenImage</code> alpha + * components when drawing. + */ + public static BufferedImage getBufferedImage( final ARGBScreenImage screenImage, final boolean discardAlpha ) + { + final BufferedImage si = screenImage.image(); + if ( discardAlpha && ( si.getTransparency() != Transparency.OPAQUE ) ) + { + final SampleModel sampleModel = RGB_COLOR_MODEL.createCompatibleWritableRaster( 1, 1 ).getSampleModel().createCompatibleSampleModel( si.getWidth(), si.getHeight() ); + final DataBuffer dataBuffer = si.getRaster().getDataBuffer(); + final WritableRaster rgbRaster = Raster.createWritableRaster( sampleModel, dataBuffer, null ); + return new BufferedImage( RGB_COLOR_MODEL, rgbRaster, false, null ); + } + return si; + } + + /** + * Get a {@link BufferedImage} for the given {@link ARGBScreenImage}. + * Discard the <code>screenImage</code> alpha components when drawing. + * + * @param screenImage + * the image. + */ + public static BufferedImage getBufferedImage( final ARGBScreenImage screenImage ) + { + return getBufferedImage( screenImage, true ); + } +} diff --git a/src/main/java/bdv/util/Affine3DHelpers.java b/src/main/java/bdv/util/Affine3DHelpers.java index 28b572920866072b3e590cd888b2e31cff0f715a..d0aca79f3d40102c2e7f1fac33778e57432c8f1b 100644 --- a/src/main/java/bdv/util/Affine3DHelpers.java +++ b/src/main/java/bdv/util/Affine3DHelpers.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -37,7 +36,7 @@ import net.imglib2.util.LinAlgHelpers; * {@link AffineTransform3D}. Note that most of these helpers assume additional * restrictions on the affine transform. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class Affine3DHelpers { @@ -200,8 +199,21 @@ public class Affine3DHelpers } return Math.sqrt( sqSum ); } - - + + /** + * Compare two transforms for equality. + * + * @return {@code true} iff {@code t1} and {@code t2} are exactly the same + */ + public static boolean equals( AffineTransform3D t1, AffineTransform3D t2 ) + { + for ( int r = 0; r < 3; ++r ) + for ( int c = 0; c < 4; ++c ) + if ( t1.get( r, c ) != t2.get( r, c ) ) + return false; + return true; + } + /** * Pretty-print the matrix content of an affine transform. * diff --git a/src/main/java/bdv/util/BoundedInterval.java b/src/main/java/bdv/util/BoundedInterval.java index 601ae514f5fd6cb13e1fbe3db8c2498e74c4e2de..63c728aa7da996f916af4cb14141d3f871116824 100644 --- a/src/main/java/bdv/util/BoundedInterval.java +++ b/src/main/java/bdv/util/BoundedInterval.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -36,7 +35,7 @@ package bdv.util; * {@link BoundedValue#setUpdateListener(UpdateListener) minimum and maximum} * values and/or overriding the {@link #updateInterval(int, int)} method. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class BoundedInterval { diff --git a/src/main/java/bdv/util/BoundedIntervalDouble.java b/src/main/java/bdv/util/BoundedIntervalDouble.java index 0837d36962c0478228f0cad7026d7298253fce3b..1257c1cfc21764225a7e5728ca13cc2e173d618e 100644 --- a/src/main/java/bdv/util/BoundedIntervalDouble.java +++ b/src/main/java/bdv/util/BoundedIntervalDouble.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -36,7 +35,7 @@ package bdv.util; * {@link BoundedValueDouble#setUpdateListener(UpdateListener) minimum and maximum} * values and/or overriding the {@link #updateInterval(double, double)} method. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class BoundedIntervalDouble { diff --git a/src/main/java/bdv/util/BoundedRange.java b/src/main/java/bdv/util/BoundedRange.java new file mode 100644 index 0000000000000000000000000000000000000000..3f0292c69eda8a6f2078d5fef243d926e774cccd --- /dev/null +++ b/src/main/java/bdv/util/BoundedRange.java @@ -0,0 +1,173 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.util; + +/** + * A bounded range with {@code minBound <= min <= max <= maxBound}. + * <p> + * {@code BoundedRang} is immutable. + * <p> + * {@link #withMin(double)}, {@link #withMinBound(double)} etc derive a new + * {@code BoundedRange} with the given {@code min}, {@code minBound} etc, while + * maintaining the {@code minBound <= min <= max <= maxBound} property. For + * example, {@code this.withMin(x)}, will also update {@code max}, if + * {@code x > this.getMax()}. + * <p> + * {@link #join(BoundedRange) join(other)} will derive a new + * {@code BoundedRange}, with {@code min} the minimum of {@code this.getMin()} + * and {@code other.getMin()} and so on. + * + * @author Tobias Pietzsch + */ +public final class BoundedRange +{ + private final double minBound; + private final double maxBound; + private final double min; + private final double max; + + public BoundedRange( final double minBound, final double maxBound, final double min, final double max ) + { + if ( ( minBound > min ) || ( maxBound < max ) || ( min > max ) ) + throw new IllegalArgumentException(); + + this.minBound = minBound; + this.maxBound = maxBound; + this.min = min; + this.max = max; + } + + public double getMinBound() + { + return minBound; + } + + public double getMaxBound() + { + return maxBound; + } + + public Bounds getBounds() + { + return new Bounds( minBound, maxBound ); + } + + public double getMin() + { + return min; + } + + public double getMax() + { + return max; + } + + public BoundedRange withMax( final double newMax ) + { + final double newMin = Math.min( min, newMax ); + final double newMinBound = Math.min( minBound, newMin ); + final double newMaxBound = Math.max( maxBound, newMax ); + return new BoundedRange( newMinBound, newMaxBound, newMin, newMax ); + } + + public BoundedRange withMin( final double newMin ) + { + final double newMax = Math.max( max, newMin ); + final double newMinBound = Math.min( minBound, newMin ); + final double newMaxBound = Math.max( maxBound, newMax ); + return new BoundedRange( newMinBound, newMaxBound, newMin, newMax ); + } + + public BoundedRange withMaxBound( final double newMaxBound ) + { + final double newMinBound = Math.min( minBound, newMaxBound ); + final double newMin = Math.min( Math.max( min, newMinBound ), newMaxBound ); + final double newMax = Math.min( Math.max( max, newMinBound ), newMaxBound ); + return new BoundedRange( newMinBound, newMaxBound, newMin, newMax ); + } + + public BoundedRange withMinBound( final double newMinBound ) + { + final double newMaxBound = Math.max( maxBound, newMinBound ); + final double newMin = Math.min( Math.max( min, newMinBound ), newMaxBound ); + final double newMax = Math.min( Math.max( max, newMinBound ), newMaxBound ); + return new BoundedRange( newMinBound, newMaxBound, newMin, newMax ); + } + + public BoundedRange join( final BoundedRange other ) + { + final double newMinBound = Math.min( minBound, other.minBound ); + final double newMaxBound = Math.max( maxBound, other.maxBound ); + final double newMin = Math.min( min, other.min ); + final double newMax = Math.max( max, other.max ); + return new BoundedRange( newMinBound, newMaxBound, newMin, newMax ); + } + + @Override + public String toString() + { + return "BoundedRange[ (" + minBound + ") " + min + ", " + max + " (" + maxBound + ") ]"; + } + + @Override + public boolean equals( final Object o ) + { + if ( this == o ) + return true; + if ( o == null || getClass() != o.getClass() ) + return false; + + final BoundedRange that = ( BoundedRange ) o; + + if ( Double.compare( that.minBound, minBound ) != 0 ) + return false; + if ( Double.compare( that.maxBound, maxBound ) != 0 ) + return false; + if ( Double.compare( that.min, min ) != 0 ) + return false; + return Double.compare( that.max, max ) == 0; + } + + @Override + public int hashCode() + { + int result; + long temp; + temp = Double.doubleToLongBits( minBound ); + result = ( int ) ( temp ^ ( temp >>> 32 ) ); + temp = Double.doubleToLongBits( maxBound ); + result = 31 * result + ( int ) ( temp ^ ( temp >>> 32 ) ); + temp = Double.doubleToLongBits( min ); + result = 31 * result + ( int ) ( temp ^ ( temp >>> 32 ) ); + temp = Double.doubleToLongBits( max ); + result = 31 * result + ( int ) ( temp ^ ( temp >>> 32 ) ); + return result; + } +} + diff --git a/src/main/java/bdv/util/BoundedValue.java b/src/main/java/bdv/util/BoundedValue.java index cd50c6748d7138d1148d93509d6802f07cbcaf03..5ef692507f13517d940f95f35c8bd42be782e564 100644 --- a/src/main/java/bdv/util/BoundedValue.java +++ b/src/main/java/bdv/util/BoundedValue.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -34,7 +33,7 @@ package bdv.util; * {@link #setUpdateListener(UpdateListener) listener} is notified when the * value or its allowed range is changed. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class BoundedValue { @@ -46,7 +45,7 @@ public class BoundedValue public interface UpdateListener { - public void update(); + void update(); } private UpdateListener updateListener; diff --git a/src/main/java/bdv/util/BoundedValueDouble.java b/src/main/java/bdv/util/BoundedValueDouble.java index a684f97a6853b017cc0639ed57bd4be482adefd7..369072ab93ae74d94929e313bc679b485272c42f 100644 --- a/src/main/java/bdv/util/BoundedValueDouble.java +++ b/src/main/java/bdv/util/BoundedValueDouble.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -34,7 +33,7 @@ package bdv.util; * {@link #setUpdateListener(UpdateListener) listener} is notified when the * value or its allowed range is changed. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class BoundedValueDouble { @@ -46,7 +45,7 @@ public class BoundedValueDouble public interface UpdateListener { - public void update(); + void update(); } private UpdateListener updateListener; diff --git a/src/main/java/bdv/util/Bounds.java b/src/main/java/bdv/util/Bounds.java new file mode 100644 index 0000000000000000000000000000000000000000..7238046d48ef2f4c358acd63337d15a1e9a7c04a --- /dev/null +++ b/src/main/java/bdv/util/Bounds.java @@ -0,0 +1,103 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.util; + +/** + * A range with {@code minBound <= maxBound}. + * <p> + * {@link #join(Bounds) join(other)} will derive a new {@code Bounds}, with + * {@code minBound} the minimum of {@code this.getMinBound()} and + * {@code other.getMinBound()} and so on. + * + * @author Tobias Pietzsch + */ +public final class Bounds +{ + private final double minBound; + private final double maxBound; + + public Bounds( final double minBound, final double maxBound ) + { + if ( minBound > maxBound ) + throw new IllegalArgumentException(); + + this.minBound = minBound; + this.maxBound = maxBound; + } + + public double getMinBound() + { + return minBound; + } + + public double getMaxBound() + { + return maxBound; + } + + public Bounds join( final Bounds other ) + { + final double newMinBound = Math.min( minBound, other.minBound ); + final double newMaxBound = Math.max( maxBound, other.maxBound ); + return new Bounds( newMinBound, newMaxBound ); + } + + @Override + public String toString() + { + return "Bounds[ " + minBound + ", " + maxBound + " ]"; + } + + @Override + public boolean equals( final Object o ) + { + if ( this == o ) + return true; + if ( o == null || getClass() != o.getClass() ) + return false; + + final Bounds that = ( Bounds ) o; + + if ( Double.compare( that.minBound, minBound ) != 0 ) + return false; + return Double.compare( that.maxBound, maxBound ) == 0; + } + + @Override + public int hashCode() + { + int result; + long temp; + temp = Double.doubleToLongBits( minBound ); + result = ( int ) ( temp ^ ( temp >>> 32 ) ); + temp = Double.doubleToLongBits( maxBound ); + result = 31 * result + ( int ) ( temp ^ ( temp >>> 32 ) ); + return result; + } +} diff --git a/src/main/java/bdv/util/ConstantRandomAccessible.java b/src/main/java/bdv/util/ConstantRandomAccessible.java index a31de68d7a3c6d8b3bebb3d45a68ca87d2483af7..c62a71041b919c99de609a35037faca71f648bb5 100644 --- a/src/main/java/bdv/util/ConstantRandomAccessible.java +++ b/src/main/java/bdv/util/ConstantRandomAccessible.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/util/DelayedPackDialog.java b/src/main/java/bdv/util/DelayedPackDialog.java new file mode 100644 index 0000000000000000000000000000000000000000..18278a2b41eb29a36f0fdbb757188d4c7f8ebd42 --- /dev/null +++ b/src/main/java/bdv/util/DelayedPackDialog.java @@ -0,0 +1,68 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.util; + +import javax.swing.*; +import java.awt.*; + +/** + * A {@code JDialog} that delays {@code pack()} calls until the dialog is made visible. + */ +public class DelayedPackDialog extends JDialog +{ + private volatile boolean packIsPending = false; + + public DelayedPackDialog( Frame owner, String title, boolean modal ) + { + super( owner, title, modal ); + } + + @Override + public void pack() + { + if ( isVisible() ) + { + packIsPending = false; + super.pack(); + } + else + packIsPending = true; + } + + @Override + public void setVisible( boolean visible ) + { + if ( visible && packIsPending ) + { + packIsPending = false; + super.pack(); + } + super.setVisible( visible ); + } +} diff --git a/src/main/java/bdv/util/DumpInputConfig.java b/src/main/java/bdv/util/DumpInputConfig.java index 2a63818e141b00968d42ad651ffaedc53dab8e9f..2daa36bf6332f9eba2d9056b80f8c2a23e3e0aa3 100644 --- a/src/main/java/bdv/util/DumpInputConfig.java +++ b/src/main/java/bdv/util/DumpInputConfig.java @@ -1,9 +1,8 @@ /*- * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/util/IntervalBoundingBox.java b/src/main/java/bdv/util/IntervalBoundingBox.java index beabc8b433251be1af84b894198eefa01d60ecf8..d4903e01e2870c533a6a7825c33273685bb1db3b 100644 --- a/src/main/java/bdv/util/IntervalBoundingBox.java +++ b/src/main/java/bdv/util/IntervalBoundingBox.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/util/InvokeOnEDT.java b/src/main/java/bdv/util/InvokeOnEDT.java index 92f0885bbdc2d4eae1b387ae5a0377cc6a309e69..ecf5209aa8e0987dc25f65e677f3a3a560d2fabd 100644 --- a/src/main/java/bdv/util/InvokeOnEDT.java +++ b/src/main/java/bdv/util/InvokeOnEDT.java @@ -1,9 +1,8 @@ /*- * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/util/MipmapTransforms.java b/src/main/java/bdv/util/MipmapTransforms.java index df802e2cdbd54db7a16da74d951505430f96d5e0..797e92bf996470a297b45884c282e44c80b1ed7c 100644 --- a/src/main/java/bdv/util/MipmapTransforms.java +++ b/src/main/java/bdv/util/MipmapTransforms.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/util/ModifiableInterval.java b/src/main/java/bdv/util/ModifiableInterval.java index 58ee1c3f1f4ee3d1b980d48ad09df424f34c70cc..a61a12e3a88a9d53484082f126777605207bacef 100644 --- a/src/main/java/bdv/util/ModifiableInterval.java +++ b/src/main/java/bdv/util/ModifiableInterval.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/util/ModifiableRealInterval.java b/src/main/java/bdv/util/ModifiableRealInterval.java index 4deaad294ab371fd7ebbf4702d7c7b57ae4bb3f3..ace0bbfd142bd23bcfd97f25f46a1c741d61036b 100644 --- a/src/main/java/bdv/util/ModifiableRealInterval.java +++ b/src/main/java/bdv/util/ModifiableRealInterval.java @@ -1,3 +1,31 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package bdv.util; import net.imglib2.AbstractRealInterval; diff --git a/src/main/java/bdv/util/MovingAverage.java b/src/main/java/bdv/util/MovingAverage.java new file mode 100644 index 0000000000000000000000000000000000000000..f3a4d3383f54f5d896b90539746ee637fc6c8fad --- /dev/null +++ b/src/main/java/bdv/util/MovingAverage.java @@ -0,0 +1,71 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.util; + +import java.util.Arrays; + +/** + * Maintains a moving average over the last {@code width} values {@link #add + * added}. The average can be {@link #init initialized} to some value (or starts + * as 0, i.e., as if {@code width} 0 values had been added) + */ +public class MovingAverage +{ + private final double[] values; + + private final int width; + + private int index = 0; + + private double average; + + public MovingAverage( final int width ) + { + values = new double[ width ]; + this.width = width; + } + + public void init( final double initialValue ) + { + Arrays.fill( values, initialValue ); + average = initialValue; + } + + public void add( final double value ) + { + average = average + ( value - values[ index ] ) / width; + values[ index ] = value; + index = ( index + 1 ) % width; + } + + public double getAverage() + { + return average; + } +} diff --git a/src/main/java/bdv/util/PlaceHolderConverterSetup.java b/src/main/java/bdv/util/PlaceHolderConverterSetup.java index 9edc1f25f2691b81d9160fa5a855dabf64eeb881..e6a4ba1ab5bc2b5bcbbc8b831f4e8e56f4bc22e8 100644 --- a/src/main/java/bdv/util/PlaceHolderConverterSetup.java +++ b/src/main/java/bdv/util/PlaceHolderConverterSetup.java @@ -1,10 +1,36 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package bdv.util; -import java.util.ArrayList; - import bdv.tools.brightness.ConverterSetup; -import bdv.viewer.RequestRepaint; import net.imglib2.type.numeric.ARGBType; +import org.scijava.listeners.Listeners; public final class PlaceHolderConverterSetup implements ConverterSetup { @@ -18,14 +44,7 @@ public final class PlaceHolderConverterSetup implements ConverterSetup private final boolean supportsColor; - private RequestRepaint viewer; - - private final ArrayList< SetupChangeListener > listeners; - - public interface SetupChangeListener - { - void setupParametersChanged(); - } + private final Listeners.List< SetupChangeListener > listeners; public PlaceHolderConverterSetup( final int setupId, @@ -49,8 +68,13 @@ public final class PlaceHolderConverterSetup implements ConverterSetup if ( color != null ) this.color.set( color ); this.supportsColor = color != null; - this.viewer = null; - this.listeners = new ArrayList<>(); + this.listeners = new Listeners.SynchronizedList<>(); + } + + @Override + public Listeners< SetupChangeListener > setupChangeListeners() + { + return listeners; } @Override @@ -62,15 +86,12 @@ public final class PlaceHolderConverterSetup implements ConverterSetup @Override public void setDisplayRange( final double min, final double max ) { + if ( this.min == min && this.max == max ) + return; + this.min = min; this.max = max; - synchronized ( listeners ) - { - for ( final SetupChangeListener l : listeners ) - l.setupParametersChanged(); - } - if ( viewer != null ) - viewer.requestRepaint(); + listeners.list.forEach( l -> l.setupParametersChanged( this ) ); } @Override @@ -81,14 +102,11 @@ public final class PlaceHolderConverterSetup implements ConverterSetup public void setColor( final int rgb ) { - this.color.set( rgb ); - synchronized ( listeners ) - { - for ( final SetupChangeListener l : listeners ) - l.setupParametersChanged(); - } - if ( viewer != null ) - viewer.requestRepaint(); + if ( !supportsColor() || color.get() == rgb ) + return; + + color.set( rgb ); + listeners.list.forEach( l -> l.setupParametersChanged( this ) ); } @Override @@ -114,48 +132,4 @@ public final class PlaceHolderConverterSetup implements ConverterSetup { return color; } - - @Override - public void setViewer( final RequestRepaint viewer ) - { - this.viewer = viewer; - } - - /** - * Registers a SetupChangeListener, that will be notified when the display - * range or the color of this {@link ConverterSetup} changes. - * - * @param listener - * the listener to register. - * @return {@code true} if the listener was successfully registered. - * {@code false} if it was already registered. - */ - public boolean addSetupChangeListener( final SetupChangeListener listener ) - { - synchronized( listeners ) - { - if ( !listeners.contains( listener ) ) - { - listeners.add( listener ); - return true; - } - return false; - } - } - - /** - * Removes the specified listener. - * - * @param listener - * the listener to remove. - * @return {@code true} if the listener was present in the listeners of - * this model and was successfully removed. - */ - public boolean removeSetupChangeListener( final SetupChangeListener listener ) - { - synchronized( listeners ) - { - return listeners.remove( listener ); - } - } } diff --git a/src/main/java/bdv/util/Prefs.java b/src/main/java/bdv/util/Prefs.java index 4b24a64643b56be5479f7cabed4e6ab5ce5c0a01..323321d5e642d97ea8524b539e2769adc4afca74 100644 --- a/src/main/java/bdv/util/Prefs.java +++ b/src/main/java/bdv/util/Prefs.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/util/PrintSequenceMipmapInfo.java b/src/main/java/bdv/util/PrintSequenceMipmapInfo.java index 1c842a513a36b0a0da7d38d5eb7ebd2409799be0..68182e348b884e0b50a5d8269eaf5a1e3be57551 100644 --- a/src/main/java/bdv/util/PrintSequenceMipmapInfo.java +++ b/src/main/java/bdv/util/PrintSequenceMipmapInfo.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/util/RealRandomAccessibleSource.java b/src/main/java/bdv/util/RealRandomAccessibleSource.java index cc4496fbd68ee5ebfa9ba096def4433fec0edda6..0fef59da4c0c8702baaffcf3d90c46cebce5446d 100644 --- a/src/main/java/bdv/util/RealRandomAccessibleSource.java +++ b/src/main/java/bdv/util/RealRandomAccessibleSource.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -46,7 +45,7 @@ import net.imglib2.view.Views; * * @param <T> * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public abstract class RealRandomAccessibleSource< T extends Type< T > > implements Source< T > { @@ -58,17 +57,25 @@ public abstract class RealRandomAccessibleSource< T extends Type< T > > implemen protected final VoxelDimensions voxelDimensions; + protected final boolean doBoundingBoxIntersectionCheck; + public RealRandomAccessibleSource( final RealRandomAccessible< T > accessible, final T type, final String name ) { - this( accessible, type, name, null ); + this( accessible, type, name, null, false ); } public RealRandomAccessibleSource( final RealRandomAccessible< T > accessible, final T type, final String name, final VoxelDimensions voxelDimensions ) + { + this( accessible, type, name, voxelDimensions, false ); + } + + public RealRandomAccessibleSource( final RealRandomAccessible< T > accessible, final T type, final String name, final VoxelDimensions voxelDimensions, final boolean doBoundingBoxIntersectionCheck ) { this.accessible = accessible; this.type = type.createVariable(); this.name = name; this.voxelDimensions = voxelDimensions; + this.doBoundingBoxIntersectionCheck = doBoundingBoxIntersectionCheck; } @Override @@ -126,4 +133,10 @@ public abstract class RealRandomAccessibleSource< T extends Type< T > > implemen { return 1; } + + @Override + public boolean doBoundingBoxCulling() + { + return doBoundingBoxIntersectionCheck; + } } diff --git a/src/main/java/bdv/util/TripleBuffer.java b/src/main/java/bdv/util/TripleBuffer.java new file mode 100644 index 0000000000000000000000000000000000000000..28cc129b27520d18bf9e7eec2fa6ce10912030f1 --- /dev/null +++ b/src/main/java/bdv/util/TripleBuffer.java @@ -0,0 +1,160 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.util; + +import java.util.function.Supplier; + +/** + * Class that provides a triple-buffer-algorithm. For communication between a + * painter thread and a display. + * + * @param <T> + * Type of the buffer. + * + * @author Matthias Arzt + * @author Tobias Pietzsch + * @see <a href="https://en.wikipedia.org/wiki/Multiple_buffering">Wikipedia Multiple Buffering</a> + */ +public class TripleBuffer< T > +{ + private T writable; + private T readable; + private T exchange; + + private boolean pending = false; + + private final Supplier< T > factory; + + /** + * Creates a triple buffer. + * + * @param factory + * Factory method, that is used to create the three buffers. + */ + public TripleBuffer( Supplier< T > factory ) + { + this.factory = factory; + } + + public TripleBuffer() + { + this( () -> null ); + } + + /** + * Returns a buffer that can be used in the painter thread for rendering. + * The returned buffer might be a previously {@link #doneWriting submitted} + * buffer that is not used by the display thread anymore. + * + * @return buffer that can be used for rendering, may be {@code null}. + */ + public synchronized T getWritableBuffer() + { + if ( writable == null ) + writable = factory.get(); + return writable; + } + + /** + * This method should be called by the painter thread, to signal that the rendering is completed. + * Assumes that the buffer used for rendering was returned by the last call to {@link #getWritableBuffer()} + */ + public synchronized void doneWriting() + { + doneWriting( writable ); + } + + /** + * This method should be called by the painter thread, to submit a completed + * buffer. This buffer will be supplied to the display thread with the next + * {@link #getReadableBuffer} (unless another {@link #doneWriting} happens + * in between). + */ + public synchronized void doneWriting( final T value ) + { + writable = exchange; + exchange = value; + pending = true; + } + + /** + * This method should be called in the display thread. To get the latest + * buffer that was completely rendered. + */ + public synchronized ReadableBuffer< T > getReadableBuffer() + { + final boolean updated = pending; + if ( pending ) + { + final T tmp = exchange; + exchange = readable; + readable = tmp; + pending = false; + } + return new ReadableBuffer<>( readable, updated ); + } + + /** + * Returned from {@link #getReadableBuffer}. + * A buffer and a flag indicating whether the buffer is different than the one + * returned from the previous {@link #getReadableBuffer} + */ + public static class ReadableBuffer< T > + { + private final T buffer; + + private final boolean updated; + + ReadableBuffer( final T buffer, final boolean updated ) + { + this.buffer = buffer; + this.updated = updated; + } + + public T getBuffer() + { + return buffer; + } + + public boolean isUpdated() + { + return updated; + } + } + + /** + * Free all buffers. + */ + public synchronized void clear() + { + writable = null; + readable = null; + exchange = null; + } +} diff --git a/src/main/java/bdv/util/WrappedList.java b/src/main/java/bdv/util/WrappedList.java new file mode 100644 index 0000000000000000000000000000000000000000..01ae7cae297cc9de319c885be81d009a206d3951 --- /dev/null +++ b/src/main/java/bdv/util/WrappedList.java @@ -0,0 +1,205 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.util; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +/** + * A {@link List} wrapper that forwards all calls to another {@link List}. + * + * @author Tobias Pietzsch + */ +public class WrappedList< E > implements List< E > +{ + private final List< E > list; + + public WrappedList( final List< E > list ) + { + this.list = list; + } + + @Override + public int size() + { + return list.size(); + } + + @Override + public boolean isEmpty() + { + return list.isEmpty(); + } + + @Override + public boolean contains( final Object o ) + { + return list.contains( o ); + } + + @Override + public Iterator< E > iterator() + { + return list.iterator(); + } + + @Override + public Object[] toArray() + { + return list.toArray(); + } + + @Override + public < T > T[] toArray( final T[] a ) + { + return list.toArray( a ); + } + + @Override + public boolean add( final E e ) + { + return list.add( e ); + } + + @Override + public boolean remove( final Object o ) + { + return list.remove( o ); + } + + @Override + public boolean containsAll( final Collection< ? > c ) + { + return list.containsAll( c ); + } + + @Override + public boolean addAll( final Collection< ? extends E > c ) + { + return list.addAll( c ); + } + + @Override + public boolean addAll( final int index, final Collection< ? extends E > c ) + { + return list.addAll( index, c ); + } + + @Override + public boolean removeAll( final Collection< ? > c ) + { + return list.removeAll( c ); + } + + @Override + public boolean retainAll( final Collection< ? > c ) + { + return list.retainAll( c ); + } + + @Override + public void clear() + { + list.clear(); + } + + @Override + public E get( final int index ) + { + return list.get( index ); + } + + @Override + public E set( final int index, final E element ) + { + return list.set( index, element ); + } + + @Override + public void add( final int index, final E element ) + { + list.add( index, element ); + } + + @Override + public E remove( final int index ) + { + return list.remove( index ); + } + + @Override + public int indexOf( final Object o ) + { + return list.indexOf( o ); + } + + @Override + public int lastIndexOf( final Object o ) + { + return list.lastIndexOf( o ); + } + + @Override + public ListIterator< E > listIterator() + { + return list.listIterator(); + } + + @Override + public ListIterator< E > listIterator( final int index ) + { + return list.listIterator( index ); + } + + @Override + public List< E > subList( final int fromIndex, final int toIndex ) + { + return list.subList( fromIndex, toIndex ); + } + + @Override + public String toString() + { + return list.toString(); + } + + @Override + public int hashCode() + { + return list.hashCode(); + } + + @Override + public boolean equals( final Object obj ) + { + return list.equals( obj ); + } +} diff --git a/src/main/java/bdv/viewer/BasicViewerState.java b/src/main/java/bdv/viewer/BasicViewerState.java new file mode 100644 index 0000000000000000000000000000000000000000..9fdf6efd7ff20b7a16eacf32a92aa5da472fe411 --- /dev/null +++ b/src/main/java/bdv/viewer/BasicViewerState.java @@ -0,0 +1,1663 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer; + +import bdv.util.Affine3DHelpers; +import bdv.util.WrappedList; +import gnu.trove.map.TObjectIntMap; +import gnu.trove.map.hash.TObjectIntHashMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import net.imglib2.realtransform.AffineTransform3D; +import org.scijava.listeners.Listeners; + +import static bdv.viewer.ViewerStateChange.CURRENT_GROUP_CHANGED; +import static bdv.viewer.ViewerStateChange.CURRENT_SOURCE_CHANGED; +import static bdv.viewer.ViewerStateChange.CURRENT_TIMEPOINT_CHANGED; +import static bdv.viewer.ViewerStateChange.DISPLAY_MODE_CHANGED; +import static bdv.viewer.ViewerStateChange.GROUP_ACTIVITY_CHANGED; +import static bdv.viewer.ViewerStateChange.GROUP_NAME_CHANGED; +import static bdv.viewer.ViewerStateChange.INTERPOLATION_CHANGED; +import static bdv.viewer.ViewerStateChange.NUM_GROUPS_CHANGED; +import static bdv.viewer.ViewerStateChange.NUM_SOURCES_CHANGED; +import static bdv.viewer.ViewerStateChange.NUM_TIMEPOINTS_CHANGED; +import static bdv.viewer.ViewerStateChange.SOURCE_ACTIVITY_CHANGED; +import static bdv.viewer.ViewerStateChange.SOURCE_TO_GROUP_ASSIGNMENT_CHANGED; +import static bdv.viewer.ViewerStateChange.VIEWER_TRANSFORM_CHANGED; +import static bdv.viewer.ViewerStateChange.VISIBILITY_CHANGED; +import static gnu.trove.impl.Constants.DEFAULT_CAPACITY; +import static gnu.trove.impl.Constants.DEFAULT_LOAD_FACTOR; + +/** + * Maintains the BigDataViewer state and implements {@link ViewerState} to + * expose query and modification methods. {@code ViewerStateChangeListener}s can + * be registered and will be notified about various {@link ViewerStateChange + * state changes}. + * <p> + * <em>This class is not thread-safe.</em> + * </p> + * + * @author Tobias Pietzsch + */ +public class BasicViewerState implements ViewerState +{ + private final Listeners.List< ViewerStateChangeListener > listeners; + + /** + * The current number of available timepoints. + */ + private int numTimepoints; + + /** + * Which timepoint (index) is currently shown. + */ + private int currentTimepoint; + + /** + * Transforms global coordinates to viewer coordinates. + */ + private final AffineTransform3D viewerTransform; + + /** + * The current interpolation method. + */ + private Interpolation interpolation; + + /** + * The current display mode. + */ + private DisplayMode displayMode; + + // -- sources -- + + private final List< SourceAndConverter< ? > > sources; + + private final List< SourceAndConverter< ? > > unmodifiableSources; + + private final Set< SourceAndConverter< ? > > activeSources; + + private final Set< SourceAndConverter< ? > > unmodifiableActiveSources; + + private SourceAndConverter< ? > currentSource; + + private final TObjectIntMap< SourceAndConverter< ? > > sourceIndices; + + private final Set< SourceAndConverter< ? > > previousVisibleSources; + + // -- groups -- + + private final List< SourceGroup > groups; + + private final List< SourceGroup > unmodifiableGroups; + + private final Map< SourceGroup, GroupData > groupData; + + private final Set< SourceGroup > activeGroups; + + private final Set< SourceGroup > unmodifiableActiveGroups; + + private SourceGroup currentGroup; + + private final TObjectIntMap< SourceGroup > groupIndices; + + private static final int NO_ENTRY_VALUE = -1; + + /** + * Create an empty state without any sources or groups. Interpolation is + * initialized as {@code Interpolation.NEARESTNEIGHBOR}. Display mode is + * initialized as {@code DisplayMode.SINGLE}. + */ + public BasicViewerState() + { + listeners = new Listeners.List<>(); + numTimepoints = 0; + currentTimepoint = 0; + viewerTransform = new AffineTransform3D(); + interpolation = Interpolation.NEARESTNEIGHBOR; + displayMode = DisplayMode.SINGLE; + sources = new ArrayList<>(); + unmodifiableSources = new UnmodifiableSources(); + activeSources = new HashSet<>(); + unmodifiableActiveSources = Collections.unmodifiableSet( activeSources ); + sourceIndices = new TObjectIntHashMap<>( DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, NO_ENTRY_VALUE ); + previousVisibleSources = new HashSet<>(); + groups = new ArrayList<>(); + unmodifiableGroups = new UnmodifiableGroups(); + groupData = new HashMap<>(); + activeGroups = new HashSet<>(); + unmodifiableActiveGroups = Collections.unmodifiableSet( activeGroups ); + groupIndices = new TObjectIntHashMap<>( DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, NO_ENTRY_VALUE ); + } + + /** + * Create a copy of the given {@code ViewerState} (except for (@link + * #changeListeners()}, which are not copied). + */ + public BasicViewerState( final ViewerState other ) + { + listeners = new Listeners.List<>(); + + numTimepoints = other.getNumTimepoints(); + currentTimepoint = other.getCurrentTimepoint(); + viewerTransform = other.getViewerTransform(); + interpolation = other.getInterpolation(); + displayMode = other.getDisplayMode(); + + sources = new ArrayList<>( other.getSources() ); + unmodifiableSources = new UnmodifiableSources(); + activeSources = new HashSet<>( other.getActiveSources() ); + unmodifiableActiveSources = Collections.unmodifiableSet( activeSources ); + currentSource = other.getCurrentSource(); + sourceIndices = new TObjectIntHashMap<>( DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, NO_ENTRY_VALUE ); + for ( int i = 0; i < sources.size(); ++i ) + sourceIndices.put( sources.get( i ), i ); + previousVisibleSources = new HashSet<>( other.getVisibleSources() ); + + groups = new ArrayList<>( other.getGroups() ); + unmodifiableGroups = new UnmodifiableGroups(); + groupData = new HashMap<>(); + other.getGroups().forEach( group -> { + final GroupData data = new GroupData(); + data.name = other.getGroupName( group ); + data.sources.addAll( other.getSourcesInGroup( group ) ); + groupData.put( group, data ); + } ); + activeGroups = new HashSet<>( other.getActiveGroups() ); + unmodifiableActiveGroups = Collections.unmodifiableSet( activeGroups ); + currentGroup = other.getCurrentGroup(); + groupIndices = new TObjectIntHashMap<>( DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, NO_ENTRY_VALUE ); + for ( int i = 0; i < groups.size(); ++i ) + groupIndices.put( groups.get( i ), i ); + } + + /** + * Set this {@code ViewerState} to {@code other}. + * <em>No {@code ViewerStateChange} events are fired.</em> + */ + public void set( final ViewerState other ) + { + numTimepoints = other.getNumTimepoints(); + currentTimepoint = other.getCurrentTimepoint(); + viewerTransform.set( other.getViewerTransform() ); + interpolation = other.getInterpolation(); + displayMode = other.getDisplayMode(); + + sources.clear(); + sources.addAll( other.getSources() ); + activeSources.clear(); + activeSources.addAll( other.getActiveSources() ); + currentSource = other.getCurrentSource(); + sourceIndices.clear(); + for ( int i = 0; i < sources.size(); ++i ) + sourceIndices.put( sources.get( i ), i ); + previousVisibleSources.clear(); + previousVisibleSources.addAll( other.getVisibleSources() ); + + groups.clear(); + groups.addAll( other.getGroups() ); + groupData.clear(); + other.getGroups().forEach( group -> { + final GroupData data = new GroupData(); + data.name = other.getGroupName( group ); + data.sources.addAll( other.getSourcesInGroup( group ) ); + groupData.put( group, data ); + } ); + activeGroups.clear(); + activeGroups.addAll( other.getActiveGroups() ); + currentGroup = other.getCurrentGroup(); + groupIndices.clear(); + for ( int i = 0; i < groups.size(); ++i ) + groupIndices.put( groups.get( i ), i ); + } + + /** + * {@code ViewerStateChangeListener}s can be added/removed here. + */ + @Override + public Listeners< ViewerStateChangeListener > changeListeners() + { + return listeners; + } + + /** + * Get a snapshot of this ViewerState. + * + * @return unmodifiable copy of the current state + */ + @Override + public ViewerState snapshot() + { + return new UnmodifiableViewerState( new BasicViewerState( this ) ); + } + + @Override + public Interpolation getInterpolation() + { + return interpolation; + } + + @Override + public void setInterpolation( final Interpolation i ) + { + if ( interpolation != i ) + { + interpolation = i; + notifyListeners( INTERPOLATION_CHANGED ); + } + } + + @Override + public DisplayMode getDisplayMode() + { + return displayMode; + } + + @Override + public void setDisplayMode( final DisplayMode mode ) + { + if ( displayMode != mode ) + { + displayMode = mode; + notifyListeners( DISPLAY_MODE_CHANGED ); + checkVisibilityChanged(); + } + } + + @Override + public int getNumTimepoints() + { + return numTimepoints; + } + + @Override + public void setNumTimepoints( final int n ) + { + if ( n < 1 ) + throw new IllegalArgumentException("numTimepoints must be >= 1"); + + if ( numTimepoints != n ) + { + numTimepoints = n; + notifyListeners( NUM_TIMEPOINTS_CHANGED ); + + if ( currentTimepoint > n - 1 ) + setCurrentTimepoint( n - 1 ); + } + } + + @Override + public int getCurrentTimepoint() + { + return currentTimepoint; + } + + @Override + public void setCurrentTimepoint( final int t ) + { + if ( t >= numTimepoints || t < 0 ) + throw new IllegalArgumentException( "currentTimepoint must be < numTimepoints and >= 0" ); + + if ( currentTimepoint != t ) + { + currentTimepoint = t; + notifyListeners( CURRENT_TIMEPOINT_CHANGED ); + } + } + + @Override + public void getViewerTransform( final AffineTransform3D t ) + { + t.set( viewerTransform ); + } + + @Override + public void setViewerTransform( final AffineTransform3D t ) + { + if ( !Affine3DHelpers.equals( viewerTransform, t ) ) + { + viewerTransform.set( t ); + notifyListeners( VIEWER_TRANSFORM_CHANGED ); + } + } + + // -------------------- + // -- sources -- + // -------------------- + + /** + * Get the list of sources. The returned {@code List} reflects changes to + * the viewer state. It is unmodifiable and not thread-safe. + * + * @return the list of sources + */ + @Override + public List< SourceAndConverter< ? > > getSources() + { + return unmodifiableSources; + } + + /** + * Get the current source. (May return {@code null} if there is no current + * source) + * + * @return the current source + */ + @Override + public SourceAndConverter< ? > getCurrentSource() + { + return currentSource; + } + + /** + * Returns {@code true} if {@code source} is the current source. Equivalent + * to {@code (getCurrentSource() == source)}. + * + * @param source + * the source. Passing {@code null} checks whether no source is current. + * @return {@code true} if {@code source} is the current source + */ + @Override + public boolean isCurrentSource( final SourceAndConverter< ? > source ) + { + return Objects.equals( source, currentSource ); + } + + /** + * Make {@code source} the current source. Returns {@code true}, if current + * source changes as a result of the call. Returns {@code false}, if + * {@code source} is already the current source. + * + * @param source + * the source to make current. Passing {@code null} clears the current + * source. + * + * @return {@code true}, if current source changed as a result of the call + * + * @throws IllegalArgumentException + * if {@code source} is not contained in the state (and not + * {@code null}). + */ + @Override + public boolean setCurrentSource( final SourceAndConverter< ? > source ) + { + checkSourcePresentAllowNull( source ); + + final boolean modified = !Objects.equals( currentSource, source ); + currentSource = source; + if ( modified ) + { + notifyListeners( CURRENT_SOURCE_CHANGED ); + checkVisibilityChanged(); + } + return modified; + } + + /** + * Get the set of active sources. The returned {@code Set} reflects changes + * to the viewer state. It is unmodifiable and not thread-safe. + * + * @return the set of active sources + */ + @Override + public Set< SourceAndConverter< ? > > getActiveSources() + { + return unmodifiableActiveSources; + } + + /** + * Check whether the given {@code source} is active. + * + * @return {@code true}, if {@code source} is active + * + * @throws NullPointerException + * if {@code source == null} + * @throws IllegalArgumentException + * if {@code source} is not contained in the state (and not + * {@code null}). + */ + @Override + public boolean isSourceActive( final SourceAndConverter< ? > source ) + { + checkSourcePresent( source ); + + return activeSources.contains( source ); + } + + /** + * Set {@code source} active or inactive. + * <p> + * Returns {@code true}, if source activity changes as a result of the call. + * Returns {@code false}, if {@code source} is already in the desired + * {@code active} state. + * + * @return {@code true}, if source activity changed as a result of the call + * + * @throws NullPointerException + * if {@code source == null} + * @throws IllegalArgumentException + * if {@code source} is not contained in the state (and not + * {@code null}). + */ + @Override + public boolean setSourceActive( final SourceAndConverter< ? > source, final boolean active ) + { + checkSourcePresent( source ); + + final boolean modified = active ? activeSources.add( source ) : activeSources.remove( source ); + if ( modified ) + { + notifyListeners( SOURCE_ACTIVITY_CHANGED ); + checkVisibilityChanged(); + } + return modified; + } + + /** + * Set all sources in {@code collection} active or inactive. + * <p> + * Returns {@code true}, if source activity changes as a result of the call. + * Returns {@code false}, if all sources were already in the desired + * {@code active} state. + * + * @return {@code true}, if source activity changed as a result of the call + * + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + * @throws IllegalArgumentException + * if any element of {@code collection} is not contained in the state. + */ + @Override + public boolean setSourcesActive( final Collection< ? extends SourceAndConverter< ? > > collection, final boolean active ) + { + checkSourcesPresent( collection ); + + final boolean modified = active ? activeSources.addAll( collection ) : activeSources.removeAll( collection ); + if ( modified ) + { + notifyListeners( SOURCE_ACTIVITY_CHANGED ); + checkVisibilityChanged(); + } + return modified; + } + + /** + * Check whether the given {@code source} is visible. + * <p> + * Whether a source is visible depends on the {@link #getDisplayMode() + * display mode}: + * <ul> + * <li>In {@code DisplayMode.SINGLE} only the current source is visible.</li> + * <li>In {@code DisplayMode.GROUP} the sources in the current group are visible.</li> + * <li>In {@code DisplayMode.FUSED} all active sources are visible.</li> + * <li>In {@code DisplayMode.FUSEDROUP} the sources in all active groups are visible.</li> + * </ul> + * + * @return {@code true}, if {@code source} is visible + * + * @throws NullPointerException + * if {@code source == null} + * @throws IllegalArgumentException + * if {@code source} is not contained in the state (and not + * {@code null}). + */ + @Override + public boolean isSourceVisible( final SourceAndConverter< ? > source ) + { + checkSourcePresent( source ); + + switch ( displayMode ) + { + case SINGLE: + default: + return Objects.equals( currentSource, source ); + case GROUP: + return currentGroup != null && groupData.get( currentGroup ).sources.contains( source ); + case FUSED: + return isSourceActive( source ); + case FUSEDGROUP: + for ( final SourceGroup group : activeGroups ) + if ( groupData.get( group ).sources.contains( source ) ) + return true; + return false; + } + } + + /** + * Check whether the given {@code source} is both visible and provides image + * data for the current timepoint. + * <p> + * Whether a source is visible depends on the {@link #getDisplayMode() + * display mode}: + * <ul> + * <li>In {@code DisplayMode.SINGLE} only the current source is visible.</li> + * <li>In {@code DisplayMode.GROUP} the sources in the current group are visible.</li> + * <li>In {@code DisplayMode.FUSED} all active sources are visible.</li> + * <li>In {@code DisplayMode.FUSEDROUP} the sources in all active groups are visible.</li> + * </ul> + * Additionally, the source must be {@link bdv.viewer.Source#isPresent(int) + * present}, i.e., provide image data for the {@link #getCurrentTimepoint() + * current timepoint}. + * + * @return {@code true}, if {@code source} is both visible and present + * + * @throws NullPointerException + * if {@code source == null} + * @throws IllegalArgumentException + * if {@code source} is not contained in the state (and not + * {@code null}). + */ + @Override + public boolean isSourceVisibleAndPresent( final SourceAndConverter< ? > source ) + { + return isSourceVisible( source ) && source.getSpimSource().isPresent( currentTimepoint ); + } + + /** + * Get the set of visible sources. + * <p> + * The returned {@code Set} is a copy. Changes to the set will not be + * reflected in the viewer state, and vice versa. + * <p> + * Whether a source is visible depends on the {@link #getDisplayMode() + * display mode}: + * <ul> + * <li>In {@code DisplayMode.SINGLE} only the current source is visible.</li> + * <li>In {@code DisplayMode.GROUP} the sources in the current group are visible.</li> + * <li>In {@code DisplayMode.FUSED} all active sources are visible.</li> + * <li>In {@code DisplayMode.FUSEDROUP} the sources in all active groups are visible.</li> + * </ul> + * + * @return the set of visible sources + */ + @Override + public Set< SourceAndConverter< ? > > getVisibleSources() + { + final Set< SourceAndConverter< ? > > visible = new HashSet<>(); + switch ( displayMode ) + { + case SINGLE: + if ( currentSource != null ) + visible.add( currentSource ); + break; + case GROUP: + if ( currentGroup != null ) + visible.addAll( groupData.get( currentGroup ).sources ); + break; + case FUSED: + visible.addAll( activeSources ); + break; + case FUSEDGROUP: + for ( final SourceGroup group : activeGroups ) + visible.addAll( groupData.get( group ).sources ); + break; + } + return visible; + } + + /** + * Get the set of visible sources that also provide image data for the + * current timepoint. + * <p> + * The returned {@code Set} is a copy. Changes to the set will not be + * reflected in the viewer state, and vice versa. + * <p> + * Whether a source is visible depends on the {@link #getDisplayMode() + * display mode}: + * <ul> + * <li>In {@code DisplayMode.SINGLE} only the current source is visible.</li> + * <li>In {@code DisplayMode.GROUP} the sources in the current group are visible.</li> + * <li>In {@code DisplayMode.FUSED} all active sources are visible.</li> + * <li>In {@code DisplayMode.FUSEDROUP} the sources in all active groups are visible.</li> + * </ul> + * Additionally, the source must be {@link bdv.viewer.Source#isPresent(int) + * present}, i.e., provide image data for the {@link #getCurrentTimepoint() + * current timepoint}. + * + * @return the set of sources that are both visible and present + */ + @Override + public Set< SourceAndConverter< ? > > getVisibleAndPresentSources() + { + final Set< SourceAndConverter< ? > > visible = getVisibleSources(); + visible.removeIf( source -> !source.getSpimSource().isPresent( currentTimepoint ) ); + return visible; + } + + /** + * Check whether the state contains the {@code source}. + * + * @return {@code true}, if {@code source} is in the list of sources. + * + * @throws NullPointerException + * if {@code source == null} + */ + @Override + public boolean containsSource( final SourceAndConverter< ? > source ) + { + if ( source == null ) + throw new NullPointerException(); + + return sourceIndices.containsKey( source ); + } + + /** + * Add {@code source} to the state. Returns {@code true}, if the source is + * added. Returns {@code false}, if the source is already present. + * <p> + * If {@code source} is added and no other source was current, then + * {@code source} is made current + * + * @return {@code true}, if list of sources changed as a result of the call. + * + * @throws NullPointerException + * if {@code source == null} + */ + @Override + public boolean addSource( final SourceAndConverter< ? > source ) + { + if ( source == null ) + throw new NullPointerException(); + + final boolean modified = !sourceIndices.containsKey( source ); + if ( modified ) + { + final int nextIndex = sources.size(); + sources.add( source ); + sourceIndices.put( source, nextIndex ); + final boolean currentSourceChanged = ( currentSource == null ); + if ( currentSourceChanged ) + currentSource = source; + + notifyListeners( NUM_SOURCES_CHANGED ); + if ( currentSourceChanged ) + notifyListeners( CURRENT_SOURCE_CHANGED ); + checkVisibilityChanged(); + } + return modified; + } + + /** + * Add all sources in {@code collection} to the state. Returns {@code true}, + * if at least one source was added. Returns {@code false}, if all sources + * were already present. + * <p> + * If any sources are added and no other source was current, then the first + * added sources will be made current. + * + * @return {@code true}, if list of sources changed as a result of the call. + * + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + */ + @Override + public boolean addSources( final Collection< ? extends SourceAndConverter< ? > > collection ) + { + checkAllNonNull( collection ); + + boolean modified = false; + boolean currentSourceChanged = false; + for ( final SourceAndConverter< ? > source : collection ) + { + if ( sourceIndices.containsKey( source ) ) + continue; + + modified = true; + final int nextIndex = sources.size(); + sources.add( source ); + sourceIndices.put( source, nextIndex ); + if ( currentSource == null ) + { + currentSourceChanged = true; + currentSource = source; + } + } + if ( modified ) + { + notifyListeners( NUM_SOURCES_CHANGED ); + if ( currentSourceChanged ) + notifyListeners( CURRENT_SOURCE_CHANGED ); + checkVisibilityChanged(); + } + return modified; + } + + /** + * Remove {@code source} from the state. + * <p> + * Returns {@code true}, if {@code source} was removed from the state. + * Returns {@code false}, if {@code source} was not contained in state. + * <p> + * The {@code source} is also removed from any groups that contained it. If + * {@code source} was current, then the first source in the list of sources + * is made current (if it exists). + * + * @return {@code true}, if list of sources changed as a result of the call + * + * @throws NullPointerException + * if {@code source == null} + */ + @Override + public boolean removeSource( final SourceAndConverter< ? > source ) + { + if ( source == null ) + throw new NullPointerException(); + + final int removedIndex = sourceIndices.remove( source ); + final boolean modified = ( removedIndex != NO_ENTRY_VALUE ); + if ( modified ) + { + sources.remove( removedIndex ); + for ( int i = removedIndex; i < sources.size(); ++i ) + sourceIndices.put( sources.get( i ), i ); + + activeSources.remove( source ); + final boolean currentSourceChanged = source.equals( currentSource ); + if ( currentSourceChanged ) + currentSource = sources.isEmpty() ? null : sources.get( 0 ); + + boolean sourceToGroupAssignmentChanged = false; + for ( final GroupData groupData : groupData.values() ) + sourceToGroupAssignmentChanged |= groupData.sources.remove( source ); + + notifyListeners( NUM_SOURCES_CHANGED ); + if ( currentSourceChanged ) + notifyListeners( CURRENT_SOURCE_CHANGED ); + if ( sourceToGroupAssignmentChanged ) + notifyListeners( SOURCE_TO_GROUP_ASSIGNMENT_CHANGED ); + checkVisibilityChanged(); + } + return modified; + } + + /** + * Remove all sources in {@code collection} from the state. Returns + * {@code true}, if at least one source was removed. Returns {@code false}, + * if none of the sources was present. + * <p> + * Removed sources are also removed from any groups containing them. If the + * current source was removed, then the first source in the remaining list + * of sources is made current (if it exists). + * + * @return {@code true}, if list of sources changed as a result of the call. + * + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + */ + @Override + public boolean removeSources( final Collection< ? extends SourceAndConverter< ? > > collection ) + { + checkAllNonNull( collection ); + + final boolean modified = sources.removeAll( collection ); + final boolean currentSourceChanged = collection.contains( currentSource ); + + if ( modified ) + { + sourceIndices.clear(); + for ( int i = 0; i < sources.size(); ++i ) + sourceIndices.put( sources.get( i ), i ); + activeSources.removeAll( collection ); + + boolean sourceToGroupAssignmentChanged = false; + for ( final GroupData groupData : groupData.values() ) + sourceToGroupAssignmentChanged |= groupData.sources.removeAll( sources ); + + if ( currentSourceChanged ) + currentSource = sources.isEmpty() ? null : sources.get( 0 ); + + notifyListeners( NUM_SOURCES_CHANGED ); + if ( currentSourceChanged ) + notifyListeners( CURRENT_SOURCE_CHANGED ); + if ( sourceToGroupAssignmentChanged ) + notifyListeners( SOURCE_TO_GROUP_ASSIGNMENT_CHANGED ); + checkVisibilityChanged(); + } + return modified; + + } + + // TODO addSource with index + // TODO addSources with index + + /** + * Remove all sources from the state. + */ + @Override + public void clearSources() + { + if ( sources.isEmpty() ) + return; + + sources.clear(); + sourceIndices.clear(); + activeSources.clear(); + + boolean sourceToGroupAssignmentChanged = false; + for ( final GroupData groupData : groupData.values() ) + { + sourceToGroupAssignmentChanged |= !groupData.sources.isEmpty(); + groupData.sources.clear(); + } + + final boolean currentSourceChanged = ( currentSource != null ); + currentSource = null; + + notifyListeners( NUM_SOURCES_CHANGED ); + if ( currentSourceChanged ) + notifyListeners( CURRENT_SOURCE_CHANGED ); + if ( sourceToGroupAssignmentChanged ) + notifyListeners( SOURCE_TO_GROUP_ASSIGNMENT_CHANGED ); + checkVisibilityChanged(); + } + + /** + * Returns a {@link Comparator} that compares sources according to the order + * in which they occur in the sources list. (Sources that do not occur in + * the list are ordered before any source in the list). + */ + @Override + public Comparator< SourceAndConverter< ? > > sourceOrder() + { + return Comparator.comparingInt( sourceIndices::get ); + } + + // -------------------- + // -- groups -- + // -------------------- + + /** + * Get the list of groups. The returned {@code List} reflects changes to the + * viewer state. It is unmodifiable and not thread-safe. + * + * @return the list of groups + */ + @Override + public List< SourceGroup > getGroups() + { + return unmodifiableGroups; + } + + /** + * Get the current group. (May return {@code null} if there is no current + * group) + * + * @return the current group + */ + @Override + public SourceGroup getCurrentGroup() + { + return currentGroup; + } + + /** + * Returns {@code true} if {@code group} is the current group. Equivalent to + * {@code (getCurrentGroup() == group)}. + * + * @return {@code true} if {@code group} is the current group + */ + @Override + public boolean isCurrentGroup( final SourceGroup group ) + { + return Objects.equals( group, currentGroup ); + } + + /** + * Make {@code group} the current group. Returns {@code true}, if current + * group changes as a result of the call. Returns {@code false}, if + * {@code group} is already the current group. + * + * @param group + * the group to make current + * @return {@code true}, if current group changed as a result of the call. + * + * @throws IllegalArgumentException + * if {@code group} is not contained in the state (and not + * {@code null}). + */ + @Override + public boolean setCurrentGroup( final SourceGroup group ) + { + checkGroupPresentAllowNull( group ); + + final boolean modified = !Objects.equals( currentGroup, group ); + currentGroup = group; + if ( modified ) + { + notifyListeners( CURRENT_GROUP_CHANGED ); + checkVisibilityChanged(); + } + return modified; + } + + /** + * Get the set of active groups. The returned {@code Set} reflects changes + * to the viewer state. It is unmodifiable and not thread-safe. + * + * @return the set of active groups + */ + @Override + public Set< SourceGroup > getActiveGroups() + { + return unmodifiableActiveGroups; + } + + /** + * Check whether the given {@code group} is active. + * + * @return {@code true}, if {@code group} is active + * + * @throws NullPointerException + * if {@code group == null} + * @throws IllegalArgumentException + * if {@code group} is not contained in the state (and not + * {@code null}). + */ + @Override + public boolean isGroupActive( final SourceGroup group ) + { + checkGroupPresent( group ); + + return activeGroups.contains( group ); + } + + /** + * Set {@code group} active or inactive. + * <p> + * Returns {@code true}, if group activity changes as a result of the call. + * Returns {@code false}, if {@code group} is already in the desired + * {@code active} state. + * + * @return {@code true}, if group activity changed as a result of the call + * + * @throws NullPointerException + * if {@code group == null} + * @throws IllegalArgumentException + * if {@code group} is not contained in the state (and not + * {@code null}). + */ + @Override + public boolean setGroupActive( final SourceGroup group, final boolean active ) + { + checkGroupPresent( group ); + + final boolean modified = active ? activeGroups.add( group ) : activeGroups.remove( group ); + if ( modified ) + { + notifyListeners( GROUP_ACTIVITY_CHANGED ); + checkVisibilityChanged(); + } + return modified; + } + + /** + * Set all groups in {@code collection} active or inactive. + * <p> + * Returns {@code true}, if group activity changes as a result of the call. + * Returns {@code false}, if all groups were already in the desired + * {@code active} state. + * + * @return {@code true}, if group activity changed as a result of the call + * + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + * @throws IllegalArgumentException + * if any element of {@code collection} is not contained in the state. + */ + @Override + public boolean setGroupsActive( final Collection< ? extends SourceGroup > collection, final boolean active ) + { + checkGroupsPresent( collection ); + + final boolean modified = active ? activeGroups.addAll( collection ) : activeGroups.removeAll( collection ); + if ( modified ) + { + notifyListeners( GROUP_ACTIVITY_CHANGED ); + checkVisibilityChanged(); + } + return modified; + } + + /** + * Get the name of a {@code group}. + * + * @return name of the group, may be {@code null} + * + * @throws NullPointerException + * if {@code group == null} + * @throws IllegalArgumentException + * if {@code group} is not contained in the state (and not + * {@code null}). + */ + @Override + public String getGroupName( final SourceGroup group ) + { + checkGroupPresent( group ); + + return groupData.get( group ).name; + } + + /** + * Set the {@code name} of a {@code group}. + * + * @throws NullPointerException + * if {@code group == null} + * @throws IllegalArgumentException + * if {@code group} is not contained in the state (and not + * {@code null}). + */ + @Override + public void setGroupName( final SourceGroup group, final String name ) + { + checkGroupPresent( group ); + + final GroupData data = groupData.get( group ); + if ( !Objects.equals( data.name, name ) ) + { + data.name = name; + notifyListeners( GROUP_NAME_CHANGED ); + } + } + + /** + * Check whether the state contains the {@code group}. + * + * @return {@code true}, if {@code group} is in the list of groups. + * + * @throws NullPointerException + * if {@code group == null} + */ + @Override + public boolean containsGroup( final SourceGroup group ) + { + if ( group == null ) + throw new NullPointerException(); + + return groupIndices.containsKey( group ); + } + + /** + * Add {@code group} to the state. Returns {@code true}, if the group is + * added. Returns {@code false}, if the group is already present. + * <p> + * If {@code group} is added and no other group was current, then + * {@code group} is made current + * + * @return {@code true}, if list of groups changed as a result of the call. + * + * @throws NullPointerException + * if {@code group == null} + */ + @Override + public boolean addGroup( final SourceGroup group ) + { + if ( group == null ) + throw new NullPointerException(); + + final boolean modified = !groupIndices.containsKey( group ); + if ( modified ) + { + final int nextIndex = groups.size(); + groups.add( group ); + groupData.put( group, new GroupData() ); + groupIndices.put( group, nextIndex ); + final boolean currentGroupChanged = ( currentGroup == null ); + if ( currentGroupChanged ) + currentGroup = group; + + notifyListeners( NUM_GROUPS_CHANGED ); + if ( currentGroupChanged ) + notifyListeners( CURRENT_GROUP_CHANGED ); + // new group is empty, so visibility will not change + // checkVisibilityChanged(); + } + return modified; + } + + /** + * Add all groups in {@code collection} to the state. Returns {@code true}, + * if at least one group was added. Returns {@code false}, if all groups + * were already present. + * <p> + * If any groups are added and no other group was current, then the first + * added groups will be made current. + * + * @return {@code true}, if list of groups changed as a result of the call. + * + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + */ + @Override + public boolean addGroups( final Collection< ? extends SourceGroup > collection ) + { + checkAllNonNull( collection ); + + boolean modified = false; + boolean currentGroupChanged = false; + for ( final SourceGroup group : collection ) + { + if ( groupIndices.containsKey( group ) ) + continue; + + modified = true; + final int nextIndex = groups.size(); + groups.add( group ); + groupData.put( group, new GroupData() ); + groupIndices.put( group, nextIndex ); + if ( currentGroup == null ) + { + currentGroupChanged = true; + currentGroup = group; + } + } + if ( modified ) + { + notifyListeners( NUM_GROUPS_CHANGED ); + if ( currentGroupChanged ) + notifyListeners( CURRENT_GROUP_CHANGED ); + // new groups are empty, so visibility will not change + // checkVisibilityChanged(); + } + return modified; + } + + /** + * Remove {@code group} from the state. + * <p> + * Returns {@code true}, if {@code group} was removed from the state. + * Returns {@code false}, if {@code group} was not contained in state. + * <p> + * If {@code group} was current, then the first group in the list of groups + * is made current (if it exists). + * + * @return {@code true}, if list of groups changed as a result of the call + * + * @throws NullPointerException + * if {@code group == null} + */ + @Override + public boolean removeGroup( final SourceGroup group ) + { + if ( group == null ) + throw new NullPointerException(); + + final int removedIndex = groupIndices.remove( group ); + final boolean modified = ( removedIndex != NO_ENTRY_VALUE ); + if ( modified ) + { + groups.remove( group ); + for ( int i = removedIndex; i < groups.size(); ++i ) + groupIndices.put( groups.get( i ), i ); + + groupData.remove( group ); + activeGroups.remove( group ); + final boolean currentGroupChanged = group.equals( currentGroup ); + if ( currentGroupChanged ) + currentGroup = groups.isEmpty() ? null : groups.get( 0 ); + + notifyListeners( NUM_GROUPS_CHANGED ); + if ( currentGroupChanged ) + notifyListeners( CURRENT_GROUP_CHANGED ); + checkVisibilityChanged(); + } + return modified; + } + + /** + * Remove all groups in {@code collection} from the state. Returns + * {@code true}, if at least one group was removed. Returns {@code false}, + * if none of the groups was present. + * <p> + * If the current group was removed, then the first group in the remaining + * list of groups is made current (if it exists). + * + * @return {@code true}, if list of groups changed as a result of the call. + * + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + */ + @Override + public boolean removeGroups( final Collection< ? extends SourceGroup > collection ) + { + checkAllNonNull( collection ); + + final boolean modified = groups.removeAll( collection ); + final boolean currentGroupChanged = collection.contains( currentGroup ); + + if ( modified ) + { + groupIndices.clear(); + for ( int i = 0; i < groups.size(); ++i ) + groupIndices.put( groups.get( i ), i ); + groupData.keySet().removeAll( collection ); + activeGroups.removeAll( collection ); + + if ( currentGroupChanged ) + currentGroup = groups.isEmpty() ? null : groups.get( 0 ); + + notifyListeners( NUM_GROUPS_CHANGED ); + if ( currentGroupChanged ) + notifyListeners( CURRENT_GROUP_CHANGED ); + checkVisibilityChanged(); + } + return modified; + } + + /** + * Add {@code source} to {@code group}. + * <p> + * Returns {@code true}, if {@code source} was added to {@code group}. + * Returns {@code false}, if {@code source} was already contained in + * {@code group}. or either of {@code source} and {@code group} is not valid + * (not in the BDV sources/groups list). + * + * @return {@code true}, if set of sources in {@code group} changed as a + * result of the call + * + * @throws NullPointerException + * if {@code source == null} or {@code group == null} + * @throws IllegalArgumentException + * if either of {@code source} and {@code group} is not contained in the + * state (and not {@code null}). + */ + @Override + public boolean addSourceToGroup( final SourceAndConverter< ? > source, final SourceGroup group ) + { + checkSourcePresent( source ); + checkGroupPresent( group ); + + final boolean modified = groupData.get( group ).sources.add( source ); + if ( modified ) + { + notifyListeners( SOURCE_TO_GROUP_ASSIGNMENT_CHANGED ); + checkVisibilityChanged(); + } + return modified; + } + + /** + * Add all sources in {@code collection} to {@code group}. + * <p> + * Returns {@code true}, if at least one source was added to {@code group}. + * Returns {@code false}, if all sources were already contained in + * {@code group}. + * + * @return {@code true}, if set of sources in {@code group} changed as a + * result of the call + * + * @throws NullPointerException + * if {@code group == null} or {@code collection == null} or any element + * of {@code collection} is {@code null}. + * @throws IllegalArgumentException + * if {@code group} or any element of {@code collection} is is not + * contained in the state (and not {@code null}). + */ + @Override + public boolean addSourcesToGroup( final Collection< ? extends SourceAndConverter< ? > > collection, final SourceGroup group ) + { + checkSourcesPresent( collection ); + checkGroupPresent( group ); + + final boolean modified = groupData.get( group ).sources.addAll( collection ); + if ( modified ) + { + notifyListeners( SOURCE_TO_GROUP_ASSIGNMENT_CHANGED ); + checkVisibilityChanged(); + } + return modified; + } + + /** + * Remove {@code source} from {@code group}. + * <p> + * Returns {@code true}, if {@code source} was removed from {@code group}. + * Returns {@code false}, if {@code source} was not contained in + * {@code group}, + * + * @return {@code true}, if set of sources in {@code group} changed as a + * result of the call + * + * @throws NullPointerException + * if {@code source == null} or {@code group == null} + * @throws IllegalArgumentException + * if either of {@code source} and {@code group} is not contained in the + * state (and not {@code null}). + */ + @Override + public boolean removeSourceFromGroup( final SourceAndConverter< ? > source, final SourceGroup group ) + { + checkSourcePresent( source ); + checkGroupPresent( group ); + + final boolean modified = groupData.get( group ).sources.remove( source ); + if ( modified ) + { + notifyListeners( SOURCE_TO_GROUP_ASSIGNMENT_CHANGED ); + checkVisibilityChanged(); + } + + return modified; + } + + /** + * Remove all sources in {@code collection} from {@code group}. + * <p> + * Returns {@code true}, if at least one source was removed from + * {@code group}. Returns {@code false}, if none of the sources were + * contained in {@code group}. + * + * @return {@code true}, if set of sources in {@code group} changed as a + * result of the call + * + * @throws NullPointerException + * if {@code group == null} or {@code collection == null} or any element + * of {@code collection} is {@code null}. + * @throws IllegalArgumentException + * if {@code group} or any element of {@code collection} is is not + * contained in the state (and not {@code null}). + */ + @Override + public boolean removeSourcesFromGroup( final Collection< ? extends SourceAndConverter< ? > > collection, final SourceGroup group ) + { + checkSourcesPresent( collection ); + checkGroupPresent( group ); + + final boolean modified = groupData.get( group ).sources.removeAll( collection ); + if ( modified ) + { + notifyListeners( SOURCE_TO_GROUP_ASSIGNMENT_CHANGED ); + checkVisibilityChanged(); + } + + return modified; + } + + /** + * Get the set sources in {@code group}. The returned {@code Set} reflects + * changes to the viewer state. It is unmodifiable and not thread-safe. + * + * @return the set of sources in {@code group} + * + * @throws NullPointerException + * if {@code group == null} + * @throws IllegalArgumentException + * if {@code group} is not contained in the state (and not + * {@code null}). + */ + @Override + public Set< SourceAndConverter< ? > > getSourcesInGroup( final SourceGroup group ) + { + checkGroupPresent( group ); + + return groupData.get( group ).unmodifiableSources; + } + + /** + * Remove all groups from the state. + */ + @Override + public void clearGroups() + { + if ( groups.isEmpty() ) + return; + + groups.clear(); + groupIndices.clear(); + activeGroups.clear(); + + final boolean currentGroupChanged = ( currentGroup != null ); + currentGroup = null; + + notifyListeners( NUM_GROUPS_CHANGED ); + if ( currentGroupChanged ) + notifyListeners( CURRENT_GROUP_CHANGED ); + notifyListeners( SOURCE_TO_GROUP_ASSIGNMENT_CHANGED ); + checkVisibilityChanged(); + } + + /** + * Returns a {@link Comparator} that compares groups according to the order + * in which they occur in the groups list. (Groups that do not occur in the + * list are ordered before any group in the list). + */ + @Override + public Comparator< SourceGroup > groupOrder() + { + return Comparator.comparingInt( groupIndices::get ); + } + + // -------------------- + // -- helpers -- + // -------------------- + + private class GroupData + { + String name; + + final Set< SourceAndConverter< ? > > sources; + + final Set< SourceAndConverter< ? > > unmodifiableSources; + + GroupData() + { + name = null; + sources = new HashSet<>(); + unmodifiableSources = Collections.unmodifiableSet( sources ); + } + } + + private class UnmodifiableSources extends WrappedList< SourceAndConverter< ? > > + { + public UnmodifiableSources() + { + super( Collections.unmodifiableList( sources ) ); + } + + @Override + public boolean contains( final Object o ) + { + return sourceIndices.containsKey( o ); + } + + @Override + public boolean containsAll( final Collection< ? > c ) + { + return sourceIndices.keySet().containsAll( c ); + } + + @Override + public int indexOf( final Object o ) + { + return sourceIndices.get( o ); + } + + @Override + public int lastIndexOf( final Object o ) + { + return sourceIndices.get( o ); + } + } + + private class UnmodifiableGroups extends WrappedList< SourceGroup > + { + public UnmodifiableGroups() + { + super( Collections.unmodifiableList( groups ) ); + } + + @Override + public boolean contains( final Object o ) + { + return groupIndices.containsKey( o ); + } + + @Override + public boolean containsAll( final Collection< ? > c ) + { + return groupIndices.keySet().containsAll( c ); + } + + @Override + public int indexOf( final Object o ) + { + return groupIndices.get( o ); + } + + @Override + public int lastIndexOf( final Object o ) + { + return groupIndices.get( o ); + } + } + + /** + * @throws NullPointerException + * if {@code source == null} + * @throws IllegalArgumentException + * if {@code source} is not contained in the state (and not + * {@code null}). + */ + private void checkSourcePresent( final SourceAndConverter< ? > source ) + { + if ( source == null ) + throw new NullPointerException(); + if ( !sources.contains( source ) ) + throw new IllegalArgumentException(); + } + + /** + * @throws IllegalArgumentException + * if {@code source} is not contained in the state (and not + * {@code null}). + */ + private void checkSourcePresentAllowNull( final SourceAndConverter< ? > source ) + { + if ( source != null && !sources.contains( source ) ) + throw new IllegalArgumentException(); + } + + /** + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + * @throws IllegalArgumentException + * if any element of {@code collection} is not contained in the state. + */ + private void checkSourcesPresent( final Collection< ? extends SourceAndConverter< ? > > collection ) + { + if ( collection == null ) + throw new NullPointerException(); + for ( final SourceAndConverter< ? > source : collection ) + checkSourcePresent( source ); + } + + /** + * @throws NullPointerException + * if {@code group == null} + * @throws IllegalArgumentException + * if {@code group} is not contained in the state (and not + * {@code null}). + */ + private void checkGroupPresent( final SourceGroup group ) + { + if ( group == null ) + throw new NullPointerException(); + if ( !groups.contains( group ) ) + throw new IllegalArgumentException(); + } + + /** + * @throws IllegalArgumentException + * if {@code group} is not contained in the state (and not + * {@code null}). + */ + private void checkGroupPresentAllowNull( final SourceGroup group ) + { + if ( group != null && !groups.contains( group ) ) + throw new IllegalArgumentException(); + } + + /** + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + * @throws IllegalArgumentException + * if any element of {@code collection} is not contained in the state. + */ + private void checkGroupsPresent( final Collection< ? extends SourceGroup > collection ) + { + if ( collection == null ) + throw new NullPointerException(); + for ( final SourceGroup group : collection ) + checkGroupPresent( group ); + } + + /** + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + */ + private void checkAllNonNull( final Collection< ? > collection ) + { + if ( collection == null ) + throw new NullPointerException(); + for ( final Object e : collection ) + if ( e == null ) + throw new NullPointerException(); + } + + private void notifyListeners( final ViewerStateChange change ) + { + listeners.list.forEach( l -> l.viewerStateChanged( change ) ); + } + + private void checkVisibilityChanged() + { + final Set< SourceAndConverter< ? > > visible = getVisibleSources(); + if ( !visible.equals( previousVisibleSources ) ) + { + previousVisibleSources.clear(); + previousVisibleSources.addAll( visible ); + notifyListeners( VISIBILITY_CHANGED ); + } + } +} diff --git a/src/main/java/bdv/viewer/ConverterSetupBounds.java b/src/main/java/bdv/viewer/ConverterSetupBounds.java new file mode 100644 index 0000000000000000000000000000000000000000..315041971f84c51c1a1f1f02f51d8008cfecfbde --- /dev/null +++ b/src/main/java/bdv/viewer/ConverterSetupBounds.java @@ -0,0 +1,106 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer; + +import bdv.tools.brightness.ConverterSetup; +import bdv.viewer.SourceToConverterSetupBimap; +import bdv.util.BoundedRange; +import bdv.util.Bounds; +import bdv.viewer.SourceAndConverter; +import java.util.HashMap; +import java.util.Map; +import net.imglib2.type.numeric.ARGBType; +import net.imglib2.type.numeric.IntegerType; +import net.imglib2.type.volatiles.VolatileARGBType; + +/** + * Map from {@code ConverterSetup} to {@code Bounds} on the range bounds to put + * on the range slider. + * + * @author Tobias Pietzsch + */ +public class ConverterSetupBounds +{ + private final SourceToConverterSetupBimap bimap; + + private final Map< ConverterSetup, Bounds > setupToBounds = new HashMap<>(); + + ConverterSetupBounds( final SourceToConverterSetupBimap bimap ) + { + this.bimap = bimap; + } + + public Bounds getBounds( final ConverterSetup setup ) + { + return setupToBounds.compute( setup, this::getExtendedBounds ); + } + + public void setBounds( final ConverterSetup setup, final Bounds bounds ) + { + setupToBounds.put( setup, bounds ); + + final double min = setup.getDisplayRangeMin(); + final double max = setup.getDisplayRangeMax(); + final BoundedRange range = new BoundedRange( min, max, min, max ).withMinBound( bounds.getMinBound() ).withMaxBound( bounds.getMaxBound() ); + if ( range.getMin() != min || range.getMax() != max ) + setup.setDisplayRange( range.getMin(), range.getMax() ); + } + + private Bounds getDefaultBounds( final ConverterSetup setup ) + { + Bounds bounds = new Bounds( setup.getDisplayRangeMin(), setup.getDisplayRangeMax() ); + final SourceAndConverter< ? > source = bimap.getSource( setup ); + if ( source != null ) + { + final Object type = source.getSpimSource().getType(); + + if ( type instanceof ARGBType || type instanceof VolatileARGBType ) + { + bounds = bounds.join( new Bounds( 0, 255 ) ); + } + else if ( type instanceof IntegerType ) + { + final IntegerType< ? > integerType = ( IntegerType< ? > ) type; + bounds = bounds.join( new Bounds( integerType.getMinValue(), integerType.getMaxValue() ) ); + } + else + { + bounds = bounds.join( new Bounds( 0, 1 ) ); + } + } + return bounds; + } + + private Bounds getExtendedBounds( final ConverterSetup setup, Bounds bounds ) + { + if ( bounds == null ) + bounds = getDefaultBounds( setup ); + return bounds.join( new Bounds( setup.getDisplayRangeMin(), setup.getDisplayRangeMax() ) ); + } +} diff --git a/src/main/java/bdv/viewer/ConverterSetups.java b/src/main/java/bdv/viewer/ConverterSetups.java new file mode 100644 index 0000000000000000000000000000000000000000..93aaeb16650a7f54312b604b387622ec5ef5a7aa --- /dev/null +++ b/src/main/java/bdv/viewer/ConverterSetups.java @@ -0,0 +1,147 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer; + +import static bdv.viewer.ViewerStateChange.NUM_SOURCES_CHANGED; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import org.scijava.listeners.Listeners; + +import bdv.tools.brightness.ConverterSetup; +import bdv.tools.brightness.ConverterSetup.SetupChangeListener; + +/** + * Provides a mapping between {@link ConverterSetup}s and + * {@link SourceAndConverter sources}. All {@code setupParametersChanged()} + * events from ConverterSetup currently in the mapping are forwarded to + * registered {@link #listeners()}. + * <p> + * {@code ConverterSetups} listens to {@code ViewerState} changes. When a source + * is removed from the {@code ViewerState}, it is also removed from the mapping + * (as well as the corresponding {@code ConverterSetup}. + * <p> + * When <em>adding</em> sources, however, the corresponding + * {@code ConverterSetup} has to be manually registered using + * {@link #put(SourceAndConverter, ConverterSetup)}. (This is necessary to avoid + * breaking existing API.) + * + * @author Tobias Pietzsch + */ +public class ConverterSetups implements SourceToConverterSetupBimap +{ + private final Map< ConverterSetup, SourceAndConverter< ? > > setupToSource = new HashMap<>(); + + private final Map< SourceAndConverter< ? >, ConverterSetup > sourceToSetup = new HashMap<>(); + + private final ViewerState state; + + private final BasicViewerState previousState; + + private final SetupChangeListener converterSetupChangeListener; + + private final Listeners.List< SetupChangeListener > forwardedSetupChangeListeners = new Listeners.SynchronizedList<>(); + + private final ConverterSetupBounds bounds; + + public ConverterSetups( final ViewerState state ) + { + this.state = state; + previousState = new BasicViewerState( state ); + converterSetupChangeListener = setup -> forwardedSetupChangeListeners.list.forEach( l -> l.setupParametersChanged( setup ) ); + state.changeListeners().add( this::analyzeChanges ); + bounds = new ConverterSetupBounds( this ); + } + + /** + * All {@code setupParametersChanged()} events from ConverterSetup currently + * in the ViewerState are forwarded to these listeners. + */ + public Listeners< SetupChangeListener > listeners() + { + return forwardedSetupChangeListeners; + } + + @Override + public SourceAndConverter< ? > getSource( final ConverterSetup setup ) + { + return setupToSource.get( setup ); + } + + @Override + public ConverterSetup getConverterSetup( final SourceAndConverter< ? > source ) + { + return sourceToSetup.get( source ); + } + + public synchronized void put( final SourceAndConverter< ? > source, final ConverterSetup setup ) + { + final ConverterSetup previousSetup = sourceToSetup.put( source, setup ); + final SourceAndConverter< ? > previousSource = setupToSource.put( setup, source ); + setup.setupChangeListeners().add( converterSetupChangeListener ); + + // keep mapping one-to-one + if ( previousSetup != null && previousSetup != setup ) + { + setupToSource.remove( previousSetup ); + previousSetup.setupChangeListeners().remove( converterSetupChangeListener ); + } + if ( previousSource != null && previousSource != source ) + sourceToSetup.remove( previousSource ); + } + + public ConverterSetupBounds getBounds() + { + return bounds; + } + + private synchronized void analyzeChanges( final ViewerStateChange change ) + { + if ( change != NUM_SOURCES_CHANGED ) + return; + + final HashSet< SourceAndConverter< ? > > removedSources = new HashSet<>( previousState.getSources() ); + removedSources.removeAll( state.getSources() ); + + // update ConverterSetup listeners + for ( final SourceAndConverter< ? > source : removedSources ) + { + final ConverterSetup setup = sourceToSetup.remove( source ); + if ( setup != null ) + { + setupToSource.remove( setup ); + setup.setupChangeListeners().remove( converterSetupChangeListener ); + } + } + + previousState.set( state ); + } +} diff --git a/src/main/java/bdv/viewer/DisplayMode.java b/src/main/java/bdv/viewer/DisplayMode.java index 6986364dcfb0b3a828dfeafc02209d79ba9c5205..6f6f8dfd6d7e63128f06b5e79ddd1fd01ffcff2d 100644 --- a/src/main/java/bdv/viewer/DisplayMode.java +++ b/src/main/java/bdv/viewer/DisplayMode.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -55,6 +54,44 @@ public enum DisplayMode return name; } + public DisplayMode withFused( final boolean activate ) + { + switch ( this ) + { + default: + case SINGLE: + case FUSED: + return activate ? FUSED : SINGLE; + case GROUP: + case FUSEDGROUP: + return activate ? FUSEDGROUP : GROUP; + } + } + + public DisplayMode withGrouping( final boolean activate ) + { + switch ( this ) + { + default: + case SINGLE: + case GROUP: + return activate ? GROUP : SINGLE; + case FUSED: + case FUSEDGROUP: + return activate ? FUSEDGROUP : FUSED; + } + } + + public boolean hasFused() + { + return this == FUSED || this == FUSEDGROUP; + } + + public boolean hasGrouping() + { + return this == GROUP || this == FUSEDGROUP; + } + public static final int length; static diff --git a/src/main/java/bdv/viewer/InteractiveDisplayCanvas.java b/src/main/java/bdv/viewer/InteractiveDisplayCanvas.java new file mode 100644 index 0000000000000000000000000000000000000000..1efc46edff7f5c0e1789e21dcc119f073cd5e843 --- /dev/null +++ b/src/main/java/bdv/viewer/InteractiveDisplayCanvas.java @@ -0,0 +1,218 @@ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer; + +import bdv.TransformEventHandler; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelListener; +import javax.swing.JComponent; +import org.scijava.listeners.Listeners; + +/* + * A {@link JComponent} that uses {@link OverlayRenderer OverlayRenderers} + * to render a canvas displayed on screen. + * <p> + * {@code InteractiveDisplayCanvas} has a {@code TransformEventHandler} that is notified when the component size is changed. + * <p> + * {@link #addHandler}/{@link #removeHandler} provide simplified that implement {@code MouseListener}, {@code KeyListener}, etc can be + * + * @param <A> + * transform type + * + * @author Tobias Pietzsch + */ +public class InteractiveDisplayCanvas extends JComponent +{ + /** + * Mouse/Keyboard handler that manipulates the view transformation. + */ + private TransformEventHandler handler; + + /** + * To draw this component, {@link OverlayRenderer#drawOverlays} is invoked for each renderer. + */ + final private Listeners.List< OverlayRenderer > overlayRenderers; + + /** + * Create a new {@code InteractiveDisplayCanvas}. + * + * @param width + * preferred component width. + * @param height + * preferred component height. + */ + public InteractiveDisplayCanvas( final int width, final int height ) + { + super(); + setPreferredSize( new Dimension( width, height ) ); + setFocusable( true ); + + overlayRenderers = new Listeners.SynchronizedList<>( r -> r.setCanvasSize( getWidth(), getHeight() ) ); + + addComponentListener( new ComponentAdapter() + { + @Override + public void componentResized( final ComponentEvent e ) + { + final int w = getWidth(); + final int h = getHeight(); + // NB: Update of overlayRenderers needs to happen before update of handler + // Otherwise repaint might start before the render target receives the size change. + overlayRenderers.list.forEach( r -> r.setCanvasSize( w, h ) ); + if ( handler != null ) + handler.setCanvasSize( w, h, true ); + // enableEvents( AWTEvent.MOUSE_MOTION_EVENT_MASK ); + } + } ); + + addMouseListener( new MouseAdapter() + { + @Override + public void mousePressed( final MouseEvent e ) + { + requestFocusInWindow(); + } + } ); + } + + /** + * OverlayRenderers can be added/removed here. + * {@link OverlayRenderer#drawOverlays} is invoked for each renderer (in the order they were added). + */ + public Listeners< OverlayRenderer > overlays() + { + return overlayRenderers; + } + + /** + * Add new event handler. Depending on the interfaces implemented by + * <code>handler</code> calls {@link Component#addKeyListener(KeyListener)}, + * {@link Component#addMouseListener(MouseListener)}, + * {@link Component#addMouseMotionListener(MouseMotionListener)}, + * {@link Component#addMouseWheelListener(MouseWheelListener)}. + * + * @param h handler to remove + */ + public void addHandler( final Object h ) + { + if ( h instanceof KeyListener ) + addKeyListener( ( KeyListener ) h ); + + if ( h instanceof MouseMotionListener ) + addMouseMotionListener( ( MouseMotionListener ) h ); + + if ( h instanceof MouseListener ) + addMouseListener( ( MouseListener ) h ); + + if ( h instanceof MouseWheelListener ) + addMouseWheelListener( ( MouseWheelListener ) h ); + + if ( h instanceof FocusListener ) + addFocusListener( ( FocusListener ) h ); + } + + /** + * Remove an event handler. Add new event handler. Depending on the + * interfaces implemented by <code>handler</code> calls + * {@link Component#removeKeyListener(KeyListener)}, + * {@link Component#removeMouseListener(MouseListener)}, + * {@link Component#removeMouseMotionListener(MouseMotionListener)}, + * {@link Component#removeMouseWheelListener(MouseWheelListener)}. + * + * @param h handler to remove + */ + public void removeHandler( final Object h ) + { + if ( h instanceof KeyListener ) + removeKeyListener( ( KeyListener ) h ); + + if ( h instanceof MouseMotionListener ) + removeMouseMotionListener( ( MouseMotionListener ) h ); + + if ( h instanceof MouseListener ) + removeMouseListener( ( MouseListener ) h ); + + if ( h instanceof MouseWheelListener ) + removeMouseWheelListener( ( MouseWheelListener ) h ); + + if ( h instanceof FocusListener ) + removeFocusListener( ( FocusListener ) h ); + } + + /** + * Set the {@link TransformEventHandler} that will be notified when component is resized. + * + * @param transformEventHandler + * handler to use + */ + public void setTransformEventHandler( final TransformEventHandler transformEventHandler ) + { + if ( handler != null ) + removeHandler( handler ); + handler = transformEventHandler; + handler.setCanvasSize( getWidth(), getHeight(), false ); + addHandler( handler ); + } + + @Override + public void paintComponent( final Graphics g ) + { + overlayRenderers.list.forEach( r -> r.drawOverlays( g ) ); + } + + // -- deprecated API -- + + /** + * @deprecated Use {@code overlays().add(renderer)} instead + */ + @Deprecated + public void addOverlayRenderer( final OverlayRenderer renderer ) + { + overlayRenderers.add( renderer ); + } + + /** + * @deprecated Use {@code overlays().remove(renderer)} instead + */ + @Deprecated + public void removeOverlayRenderer( final OverlayRenderer renderer ) + { + overlayRenderers.remove( renderer ); + } +} diff --git a/src/main/java/bdv/viewer/Interpolation.java b/src/main/java/bdv/viewer/Interpolation.java index ab96371881c7da5e2d8e0302d68ab8f6bcd2e95a..c8719dc3962bf2caadcaf73b303af3d2077b239e 100644 --- a/src/main/java/bdv/viewer/Interpolation.java +++ b/src/main/java/bdv/viewer/Interpolation.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/viewer/InterpolationModeListener.java b/src/main/java/bdv/viewer/InterpolationModeListener.java index f754906a7fda1db00bbbf963ca6defaed18a117c..06cabdb7d8093b04e0268bacd43f4931496a65ee 100644 --- a/src/main/java/bdv/viewer/InterpolationModeListener.java +++ b/src/main/java/bdv/viewer/InterpolationModeListener.java @@ -1,9 +1,8 @@ /*- * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -31,5 +30,5 @@ package bdv.viewer; public interface InterpolationModeListener { - public void interpolationModeChanged( final Interpolation mode ); + void interpolationModeChanged( final Interpolation mode ); } diff --git a/src/main/java/bdv/viewer/NavigationActions.java b/src/main/java/bdv/viewer/NavigationActions.java index 297a11a6a34279e88e2c86d02c66bdbcbdc2c397..5146ee374a3cb6b6102303dd1d2403957fc34b47 100644 --- a/src/main/java/bdv/viewer/NavigationActions.java +++ b/src/main/java/bdv/viewer/NavigationActions.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,6 +28,8 @@ */ package bdv.viewer; +import java.util.ArrayList; +import java.util.List; import javax.swing.ActionMap; import javax.swing.InputMap; @@ -90,23 +91,31 @@ public class NavigationActions extends Actions public void modes( final ViewerPanel viewer ) { runnableAction( - () -> viewer.toggleInterpolation(), + viewer::toggleInterpolation, TOGGLE_INTERPOLATION, "I" ); runnableAction( - () -> viewer.getVisibilityAndGrouping().setFusedEnabled( !viewer.visibilityAndGrouping.isFusedEnabled() ), + () -> { + final ViewerState state = viewer.state(); + final DisplayMode mode = state.getDisplayMode(); + state.setDisplayMode( mode.withFused( !mode.hasFused() ) ); + }, TOGGLE_FUSED_MODE, "F" ); runnableAction( - () -> viewer.getVisibilityAndGrouping().setGroupingEnabled( !viewer.visibilityAndGrouping.isGroupingEnabled() ), + () -> { + final ViewerState state = viewer.state(); + final DisplayMode mode = state.getDisplayMode(); + state.setDisplayMode( mode.withGrouping( !mode.hasGrouping() ) ); + }, TOGGLE_GROUPING, "G" ); } public void time( final ViewerPanel viewer ) { runnableAction( - () -> viewer.nextTimePoint(), + viewer::nextTimePoint, NEXT_TIMEPOINT, "CLOSE_BRACKET", "M" ); runnableAction( - () -> viewer.previousTimePoint(), + viewer::previousTimePoint, PREVIOUS_TIMEPOINT, "OPEN_BRACKET", "N" ); } @@ -117,10 +126,10 @@ public class NavigationActions extends Actions { final int sourceIndex = i; runnableAction( - () -> viewer.getVisibilityAndGrouping().setCurrentGroupOrSource( sourceIndex ), + () -> setCurrentGroupOrSource( viewer, sourceIndex ), String.format( SET_CURRENT_SOURCE, i ), numkeys[ i ] ); runnableAction( - () -> viewer.getVisibilityAndGrouping().toggleActiveGroupOrSource( sourceIndex ), + () -> toggleGroupOrSourceActive( viewer, sourceIndex ), String.format( TOGGLE_SOURCE_VISIBILITY, i ), "shift " + numkeys[ i ] ); } } @@ -131,4 +140,59 @@ public class NavigationActions extends Actions alignPlaneAction( viewer, AlignPlane.ZY, "shift X" ); alignPlaneAction( viewer, AlignPlane.XZ, "shift Y", "shift A" ); } + + private static void setCurrentGroupOrSource( final ViewerPanel viewer, final int index ) + { + final ViewerState state = viewer.state(); + synchronized ( state ) + { + if ( state.getDisplayMode().hasGrouping() ) + { + final List< SourceGroup > groups = state.getGroups(); + if ( index >= 0 && index < groups.size() ) + { + final SourceGroup group = groups.get( index ); + state.setCurrentGroup( group ); + final List< SourceAndConverter< ? > > sources = new ArrayList<>( state.getSourcesInGroup( group ) ); + if ( !sources.isEmpty() ) + { + sources.sort( state.sourceOrder() ); + state.setCurrentSource( sources.get( 0 ) ); + } + } + } + else + { + final List< SourceAndConverter< ? > > sources = state.getSources(); + if ( index >= 0 && index < sources.size() ) + state.setCurrentSource( sources.get( index ) ); + } + } + } + + private static void toggleGroupOrSourceActive( final ViewerPanel viewer, final int index ) + { + final ViewerState state = viewer.state(); + synchronized ( state ) + { + if ( state.getDisplayMode().hasGrouping() ) + { + final List< SourceGroup > groups = state.getGroups(); + if ( index >= 0 && index < groups.size() ) + { + final SourceGroup group = groups.get( index ); + state.setGroupActive( group, !state.isGroupActive( group ) ); + } + } + else + { + final List< SourceAndConverter< ? > > sources = state.getSources(); + if ( index >= 0 && index < sources.size() ) + { + final SourceAndConverter< ? > source = sources.get( index ); + state.setSourceActive( source, !state.isSourceActive( source ) ); + } + } + } + } } diff --git a/src/main/java/bdv/viewer/OverlayRenderer.java b/src/main/java/bdv/viewer/OverlayRenderer.java new file mode 100644 index 0000000000000000000000000000000000000000..a815ee95cbb9b380b59265aa1a3a7770e03bcdd1 --- /dev/null +++ b/src/main/java/bdv/viewer/OverlayRenderer.java @@ -0,0 +1,58 @@ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer; + +import java.awt.Graphics; + +/** + * Draw something to a {@link Graphics} canvas and receive notifications about + * changes of the canvas size. + * + * @author Tobias Pietzsch + */ +public interface OverlayRenderer +{ + /** + * Render overlays. + */ + void drawOverlays( Graphics g ); + + /** + * This is called, when the screen size of the canvas (the component + * displaying the image and generating mouse events) changes. This can be + * used to determine scale of overlay or screen coordinates relative to the + * border. + * + * @param width + * the new canvas width. + * @param height + * the new canvas height. + */ + void setCanvasSize( int width, int height ); +} diff --git a/src/main/java/bdv/viewer/RequestRepaint.java b/src/main/java/bdv/viewer/RequestRepaint.java index b80179cb39ec3349404aa448dbd34226ee3c3a82..a6eedd483db18dbca2a31166a591f42c189c4edd 100644 --- a/src/main/java/bdv/viewer/RequestRepaint.java +++ b/src/main/java/bdv/viewer/RequestRepaint.java @@ -1,9 +1,8 @@ /*- * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -34,5 +33,5 @@ public interface RequestRepaint /** * Repaint as soon as possible. */ - public void requestRepaint(); + void requestRepaint(); } diff --git a/src/main/java/bdv/viewer/Source.java b/src/main/java/bdv/viewer/Source.java index b623521c776a229a1a720a7de8ca57b4398bf315..434ec35fc3bd430d22e8e0cff878ff865521d66c 100644 --- a/src/main/java/bdv/viewer/Source.java +++ b/src/main/java/bdv/viewer/Source.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -45,7 +44,7 @@ import net.imglib2.realtransform.AffineTransform3D; * {@link TimePoint#getId() id}. This timepoint index is an index into the * ordered list of timepoints {@link TimePoints#getTimePointsOrdered()}. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public interface Source< T > { @@ -56,7 +55,7 @@ public interface Source< T > * timepoint index * @return true, if there is data for timepoint index t. */ - public boolean isPresent( int t ); + boolean isPresent( int t ); /** * Get the 3D stack at timepoint index t. @@ -67,8 +66,24 @@ public interface Source< T > * mipmap level * @return the {@link RandomAccessibleInterval stack}. */ - public RandomAccessibleInterval< T > getSource( int t, int level ); + RandomAccessibleInterval< T > getSource( int t, int level ); + /** + * Whether this source participates in bounding box culling. + * <p> + * If {@code true}, then this source will only be rendered if its bounding + * box, i.e., the interval of {@link #getSource}, intersects the + * current screen area (when transformed to viewer coordinates). + * <p> + * If {@code false}, then this source will be always rendered (if it is + * set to be visible.) + * + * @return {@code true}, if this source participates in bounding box culling. + */ + default boolean doBoundingBoxCulling() + { + return true; + } /** * Get the 3D stack at timepoint index t, extended to infinity and interpolated. @@ -81,15 +96,7 @@ public interface Source< T > * interpolation method to use * @return the extended and interpolated {@link RandomAccessible stack}. */ - public RealRandomAccessible< T > getInterpolatedSource( final int t, final int level, final Interpolation method ); - - /* - * TODO: Consider adding the methods for getting Source with explicit - * ThreadGroup key (in case we don't want it to be the current thread's - * ThreadGroup). - */ -// public default RandomAccessibleInterval< T > getSource( int t, int level, ThreadGroup threadGroup ); -// public RealRandomAccessible< T > getInterpolatedSource( final int t, final int level, final Interpolation method, ThreadGroup threadGroup ); + RealRandomAccessible< T > getInterpolatedSource( final int t, final int level, final Interpolation method ); /** * Get the transform from the {@link #getSource(int, int) source} at the @@ -103,26 +110,26 @@ public interface Source< T > * is set to the source-to-global transform, that transforms * source coordinates into the global coordinates */ - public void getSourceTransform( int t, int level, AffineTransform3D transform ); + void getSourceTransform( int t, int level, AffineTransform3D transform ); /** * Get an instance of the pixel type. * @return instance of pixel type. */ - public T getType(); + T getType(); /** * Get the name of the source. * @return the name of the source. */ - public String getName(); + String getName(); /** * Get voxel size and unit for this source. May return null. * * @return voxel size and unit or {@code null}. */ - public VoxelDimensions getVoxelDimensions(); + VoxelDimensions getVoxelDimensions(); - public int getNumMipmapLevels(); + int getNumMipmapLevels(); } diff --git a/src/main/java/bdv/viewer/SourceAndConverter.java b/src/main/java/bdv/viewer/SourceAndConverter.java index d42825791237ec4f10cc3d5d43b85064e4216a4d..223cae00e91035bfed3197b34e66fcf518555b57 100644 --- a/src/main/java/bdv/viewer/SourceAndConverter.java +++ b/src/main/java/bdv/viewer/SourceAndConverter.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/viewer/SourceGroup.java b/src/main/java/bdv/viewer/SourceGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..0e747004597e5c272b581cfad348282d4ec8e9aa --- /dev/null +++ b/src/main/java/bdv/viewer/SourceGroup.java @@ -0,0 +1,36 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer; + +/** + * A SourceGroup handle. Identifies the same source group over time (while both + * the group's name and associated sources change). + */ +public final class SourceGroup +{} diff --git a/src/main/java/bdv/viewer/SourceToConverterSetupBimap.java b/src/main/java/bdv/viewer/SourceToConverterSetupBimap.java new file mode 100644 index 0000000000000000000000000000000000000000..48e2c0cfa070ce7f9d384abf5a806badc8729fb9 --- /dev/null +++ b/src/main/java/bdv/viewer/SourceToConverterSetupBimap.java @@ -0,0 +1,62 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer; + +import java.util.ArrayList; +import java.util.List; + +import bdv.tools.brightness.ConverterSetup; + +/** + * Associates {@code ConverterSetup}s to sources ({@code SourceAndConverter}). + * + * @author Tobias Pietzsch + */ +public interface SourceToConverterSetupBimap +{ + SourceAndConverter< ? > getSource( ConverterSetup converterSetup ); + + ConverterSetup getConverterSetup( SourceAndConverter< ? > source ); + + /** + * Returns list of all non-{@code null} ConverterSetups associated to + * {@code sources}. + */ + default List< ConverterSetup > getConverterSetups( final List< ? extends SourceAndConverter< ? > > sources ) + { + final List< ConverterSetup > converterSetups = new ArrayList<>(); + for ( final SourceAndConverter< ? > source : sources ) + { + final ConverterSetup converterSetup = getConverterSetup( source ); + if ( converterSetup != null ) + converterSetups.add( converterSetup ); + } + return converterSetups; + } +} diff --git a/src/main/java/bdv/viewer/SynchronizedViewerState.java b/src/main/java/bdv/viewer/SynchronizedViewerState.java new file mode 100644 index 0000000000000000000000000000000000000000..754d732252ff3876e7d446085dd95b72bde63339 --- /dev/null +++ b/src/main/java/bdv/viewer/SynchronizedViewerState.java @@ -0,0 +1,1102 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer; + +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +import net.imglib2.realtransform.AffineTransform3D; + +import org.scijava.listeners.Listeners; + +/** + * Maintains the BigDataViewer state and implements {@link ViewerState} to + * expose query and modification methods. {@code ViewerStateChangeListener}s can + * be registered and will be notified about various {@link ViewerStateChange + * state changes}. + * <p> + * All methods of this class are {@code synchronized}, so that every individual + * change to the viewer state is atomic. {@code IllegalArgumentException}s + * thrown by the wrapped {@code BasicViewerState} are silently swallowed, under + * the assumption that they result from concurrent changes (for example another + * thread might have removed the source that you are trying to make current). + * <p> + * To perform sequences of operations atomically explicit synchronization is + * required. In particular, this is true when using the collections returned by + * {@link #getSources()}, {@link #getActiveSources()}, {@link #getGroups()}, + * {@link #getActiveGroups()}, and {@link #getSourcesInGroup(SourceGroup)}. + * These collections are backed by the ViewerState, they reflect changes, and + * they are <em>not thread-safe</em>. It is possible to run into + * {@code ConcurrentModificationException} when iterating them, etc. + * <p> + * Example where explicit synchronization is required: + * <pre>{@code + * List<SourceGroup> groupsContainingCurrentSource; + * synchronized (state) { + * SourceAndConverter<?> currentSource = state.getCurrentSource(); + * groupsContainingCurrentSource = + * state.getGroups().stream() + * .filter(g -> state.getSourcesInGroup(g).contains(currentSource)) + * .collect(Collectors.toList()); + * }}</pre> + * <p> + * Alternatively, for read-only access, it is possible to (atomically) take an + * unmodifiable {@link #snapshot()} of the current state. + * + * @author Tobias Pietzsch + */ +public class SynchronizedViewerState implements ViewerState +{ + private static final boolean DEBUG = false; + + private final ViewerState state; + + public SynchronizedViewerState( final ViewerState state ) + { + this.state = state; + } + + @Override + public Listeners< ViewerStateChangeListener > changeListeners() + { + return state.changeListeners(); + } + + /** + * Get a snapshot of this ViewerState. + * + * @return unmodifiable copy of the current state + */ + @Override + public synchronized ViewerState snapshot() + { + return state.snapshot(); + } + + @Override + public synchronized Interpolation getInterpolation() + { + return state.getInterpolation(); + } + + @Override + public synchronized void setInterpolation( final Interpolation interpolation ) + { + state.setInterpolation( interpolation ); + } + + @Override + public synchronized DisplayMode getDisplayMode() + { + return state.getDisplayMode(); + } + + @Override + public synchronized void setDisplayMode( final DisplayMode mode ) + { + state.setDisplayMode( mode ); + } + + @Override + public synchronized int getNumTimepoints() + { + return state.getNumTimepoints(); + } + + @Override + public synchronized void setNumTimepoints( final int n ) + { + state.setNumTimepoints( n ); + } + + @Override + public synchronized int getCurrentTimepoint() + { + return state.getCurrentTimepoint(); + } + + @Override + public synchronized void setCurrentTimepoint( final int t ) + { + try + { + state.setCurrentTimepoint( t ); + } + catch ( final IllegalArgumentException e ) + { + if ( DEBUG ) + e.printStackTrace(); + } + } + + @Override + public synchronized void getViewerTransform( final AffineTransform3D transform ) + { + state.getViewerTransform( transform ); + } + + @Override + public synchronized void setViewerTransform( final AffineTransform3D transform ) + { + state.setViewerTransform( transform ); + } + + // -------------------- + // -- sources -- + // -------------------- + + /** + * Get the list of sources. The returned {@code List} reflects changes to + * the viewer state. It is unmodifiable and <em>not thread-safe</em>. + * <p> + * The returned list is also a set, there are no duplicate entries. + * + * @return the list of sources + */ + @Override + public synchronized List< SourceAndConverter< ? > > getSources() + { + return state.getSources(); + } + + /** + * Get the current source. (May return {@code null} if there is no current + * source) + * + * @return the current source + */ + @Override + public synchronized SourceAndConverter< ? > getCurrentSource() + { + return state.getCurrentSource(); + } + + /** + * Returns {@code true} if {@code source} is the current source. Equivalent + * to {@code (getCurrentSource() == source)}. + * + * @param source + * the source. Passing {@code null} checks whether no source is current. + * + * @return {@code true} if {@code source} is the current source + */ + @Override + public synchronized boolean isCurrentSource( final SourceAndConverter< ? > source ) + { + return state.isCurrentSource( source ); + } + + /** + * Make {@code source} the current source. Returns {@code true}, if current + * source changes as a result of the call. Returns {@code false}, if + * {@code source} is already the current source. + * <p> + * Also returns {@code false}, if {@code source} is not valid (for example + * because it has been removed from the state by a different thread). + * + * @param source + * the source to make current. Passing {@code null} clears the current + * source. + * + * @return {@code true}, if current source changed as a result of the call + */ + @Override + public synchronized boolean setCurrentSource( final SourceAndConverter< ? > source ) + { + try + { + return state.setCurrentSource( source ); + } + catch ( final IllegalArgumentException e ) + { + if ( DEBUG ) + e.printStackTrace(); + return false; + } + } + + /** + * Get the set of active sources. The returned {@code Set} reflects changes + * to the viewer state. It is unmodifiable and <em>not thread-safe</em>. + * + * @return the set of active sources + */ + @Override + public synchronized Set< SourceAndConverter< ? > > getActiveSources() + { + return state.getActiveSources(); + } + + /** + * Check whether the given {@code source} is active. + * + * Returns {@code true}, if {@code source} is active. Returns {@code false}, + * if {@code source} is inactive or not valid (for example because it has + * been removed from the state by a different thread). + * + * @return {@code true}, if {@code source} is active + * + * @throws NullPointerException + * if {@code source == null} + */ + @Override + public synchronized boolean isSourceActive( final SourceAndConverter< ? > source ) + { + try + { + return state.isSourceActive( source ); + } + catch ( final IllegalArgumentException e ) + { + if ( DEBUG ) + e.printStackTrace(); + return false; + } + } + + /** + * Set {@code source} active or inactive. + * <p> + * Returns {@code true}, if source activity changes as a result of the call. + * Returns {@code false}, if {@code source} is already in the desired + * {@code active} state. + * <p> + * Also returns {@code false}, if {@code source} is not valid (for example + * because it has been removed from the state by a different thread). + * + * @return {@code true}, if source activity changed as a result of the call + * + * @throws NullPointerException + * if {@code source == null} + */ + @Override + public synchronized boolean setSourceActive( final SourceAndConverter< ? > source, final boolean active ) + { + try + { + return state.setSourceActive( source, active ); + } + catch ( final IllegalArgumentException e ) + { + if ( DEBUG ) + e.printStackTrace(); + return false; + } + } + + /** + * Set all sources in {@code collection} active or inactive. + * <p> + * Returns {@code true}, if source activity changes as a result of the call. + * Returns {@code false}, if all sources were already in the desired + * {@code active} state. + * <p> + * Does nothing and returns {@code false}, if any source in + * {@code collection} is not valid (for example because it was removed from + * the state by a different thread). + * + * @return {@code true}, if source activity changed as a result of the call + * + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + */ + @Override + public synchronized boolean setSourcesActive( final Collection< ? extends SourceAndConverter< ? > > collection, final boolean active ) + { + try + { + return state.setSourcesActive( collection, active ); + } + catch ( final IllegalArgumentException e ) + { + if ( DEBUG ) + e.printStackTrace(); + return false; + } + } + + /** + * Check whether the given {@code source} is visible. + * <p> + * Returns {@code false}, if {@code source} is not valid (for example + * because it has been removed from the state by a different thread). + * <p> + * Whether a source is visible depends on the {@link #getDisplayMode() + * display mode}: + * <ul> + * <li>In {@code DisplayMode.SINGLE} only the current source is visible.</li> + * <li>In {@code DisplayMode.GROUP} the sources in the current group are visible.</li> + * <li>In {@code DisplayMode.FUSED} all active sources are visible.</li> + * <li>In {@code DisplayMode.FUSEDROUP} the sources in all active groups are visible.</li> + * </ul> + * + * @return {@code true}, if {@code source} is visible + * + * @throws NullPointerException + * if {@code source == null} + * @throws IllegalArgumentException + * if {@code source} is not contained in the state (and not + * {@code null}). + */ + @Override + public synchronized boolean isSourceVisible( final SourceAndConverter< ? > source ) + { + try + { + return state.isSourceVisible( source ); + } + catch ( final IllegalArgumentException e ) + { + if ( DEBUG ) + e.printStackTrace(); + return false; + } + } + + /** + * Check whether the given {@code source} is both visible and provides image + * data for the current timepoint. + * <p> + * Returns {@code false}, if {@code source} is not valid (for example + * because it has been removed from the state by a different thread). + * + * Whether a source is visible depends on the {@link #getDisplayMode() + * display mode}: + * <ul> + * <li>In {@code DisplayMode.SINGLE} only the current source is visible.</li> + * <li>In {@code DisplayMode.GROUP} the sources in the current group are visible.</li> + * <li>In {@code DisplayMode.FUSED} all active sources are visible.</li> + * <li>In {@code DisplayMode.FUSEDROUP} the sources in all active groups are visible.</li> + * </ul> + * Additionally, the source must be {@link bdv.viewer.Source#isPresent(int) + * present}, i.e., provide image data for the {@link #getCurrentTimepoint() + * current timepoint}. + * + * @return {@code true}, if {@code source} is both visible and present + * + * @throws NullPointerException + * if {@code source == null} + */ + @Override + public synchronized boolean isSourceVisibleAndPresent( final SourceAndConverter< ? > source ) + { + try + { + return state.isSourceVisibleAndPresent( source ); + } + catch ( final IllegalArgumentException e ) + { + if ( DEBUG ) + e.printStackTrace(); + return false; + } + } + + /** + * Get the set of visible sources. + * <p> + * The returned {@code Set} is a copy. Changes to the set will not be + * reflected in the viewer state, and vice versa. + * <p> + * Whether a source is visible depends on the {@link #getDisplayMode() + * display mode}: + * <ul> + * <li>In {@code DisplayMode.SINGLE} only the current source is visible.</li> + * <li>In {@code DisplayMode.GROUP} the sources in the current group are visible.</li> + * <li>In {@code DisplayMode.FUSED} all active sources are visible.</li> + * <li>In {@code DisplayMode.FUSEDROUP} the sources in all active groups are visible.</li> + * </ul> + * + * @return the set of visible sources + */ + @Override + public synchronized Set< SourceAndConverter< ? > > getVisibleSources() + { + return state.getVisibleSources(); + } + + /** + * Get the set of visible sources that also provide image data for the + * current timepoint. + * <p> + * The returned {@code Set} is a copy. Changes to the set will not be + * reflected in the viewer state, and vice versa. + * <p> + * Whether a source is visible depends on the {@link #getDisplayMode() + * display mode}: + * <ul> + * <li>In {@code DisplayMode.SINGLE} only the current source is visible.</li> + * <li>In {@code DisplayMode.GROUP} the sources in the current group are visible.</li> + * <li>In {@code DisplayMode.FUSED} all active sources are visible.</li> + * <li>In {@code DisplayMode.FUSEDROUP} the sources in all active groups are visible.</li> + * </ul> + * Additionally, the source must be {@link bdv.viewer.Source#isPresent(int) + * present}, i.e., provide image data for the {@link #getCurrentTimepoint() + * current timepoint}. + * + * @return the set of sources that are both visible and present + */ + @Override + public synchronized Set< SourceAndConverter< ? > > getVisibleAndPresentSources() + { + return state.getVisibleAndPresentSources(); + } + + /** + * Check whether the state contains the {@code source}. + * + * @return {@code true}, if {@code source} is in the list of sources. + * + * @throws NullPointerException + * if {@code source == null} + */ + @Override + public synchronized boolean containsSource( final SourceAndConverter< ? > source ) + { + return state.containsSource( source ); + } + + /** + * Add {@code source} to the state. Returns {@code true}, if the source is + * added. Returns {@code false}, if the source is already present. + * <p> + * If {@code source} is added and no other source was current, then + * {@code source} is made current + * + * @return {@code true}, if list of sources changed as a result of the call. + * + * @throws NullPointerException + * if {@code source == null} + */ + @Override + public synchronized boolean addSource( final SourceAndConverter< ? > source ) + { + return state.addSource( source ); + } + + /** + * Add all sources in {@code collection} to the state. Returns {@code true}, + * if at least one source was added. Returns {@code false}, if all sources + * were already present. + * <p> + * If any sources are added and no other source was current, then the first + * added sources will be made current. + * + * @return {@code true}, if list of sources changed as a result of the call. + * + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + */ + @Override + public synchronized boolean addSources( final Collection< ? extends SourceAndConverter< ? > > collection ) + { + return state.addSources( collection ); + } + + /** + * Remove {@code source} from the state. + * <p> + * Returns {@code true}, if {@code source} was removed from the state. + * Returns {@code false}, if {@code source} was not contained in state. + * <p> + * The {@code source} is also removed from any groups that contained it. If + * {@code source} was current, then the first source in the list of sources + * is made current (if it exists). + * + * @return {@code true}, if list of sources changed as a result of the call + * + * @throws NullPointerException + * if {@code source == null} + */ + @Override + public synchronized boolean removeSource( final SourceAndConverter< ? > source ) + { + return state.removeSource( source ); + } + + /** + * Remove all sources in {@code collection} from the state. Returns + * {@code true}, if at least one source was removed. Returns {@code false}, + * if none of the sources was present. + * <p> + * Removed sources are also removed from any groups containing them. If the + * current source was removed, then the first source in the remaining list + * of sources is made current (if it exists). + * + * @return {@code true}, if list of sources changed as a result of the call. + * + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + */ + @Override + public synchronized boolean removeSources( final Collection< ? extends SourceAndConverter< ? > > collection ) + { + return state.removeSources( collection ); + } + + // TODO addSource with index + // TODO addSources with index + + /** + * Remove all sources from the state. + */ + @Override + public synchronized void clearSources() + { + state.clearSources(); + } + + /** + * Returns a {@link Comparator} that compares sources according to the order + * in which they occur in the sources list. (Sources that do not occur in + * the list are ordered before any source in the list). + */ + @Override + public synchronized Comparator< SourceAndConverter< ? > > sourceOrder() + { + return state.sourceOrder(); + } + + // -------------------- + // -- groups -- + // -------------------- + + /** + * Get the list of groups. The returned {@code List} reflects changes to the + * viewer state. It is unmodifiable and <em>not thread-safe</em>. + * <p> + * The returned list is also a set, there are no duplicate entries. + * + * @return the list of groups + */ + @Override + public synchronized List< SourceGroup > getGroups() + { + return state.getGroups(); + } + + /** + * Get the current group. (May return {@code null} if there is no current + * group) + * + * @return the current group + */ + @Override + public synchronized SourceGroup getCurrentGroup() + { + return state.getCurrentGroup(); + } + + /** + * Returns {@code true} if {@code group} is the current group. Equivalent to + * {@code (getCurrentGroup() == group)}. + * + * @param group + * the group. Passing {@code null} checks whether no group is current. + * + * @return {@code true} if {@code group} is the current group + */ + @Override + public synchronized boolean isCurrentGroup( final SourceGroup group ) + { + return state.isCurrentGroup( group ); + } + + /** + * Make {@code group} the current group. Returns {@code true}, if current + * group changes as a result of the call. Returns {@code false}, if + * {@code group} is already the current group. + * <p> + * Also returns {@code false}, if {@code group} is not valid (for example + * because it has been removed from the state by a different thread). + * + * @param group + * the group to make current. Passing {@code null} clears the current + * group. + * + * @return {@code true}, if current group changed as a result of the call. + */ + @Override + public synchronized boolean setCurrentGroup( final SourceGroup group ) + { + try + { + return state.setCurrentGroup( group ); + } + catch ( final IllegalArgumentException e ) + { + if ( DEBUG ) + e.printStackTrace(); + return false; + } + } + + /** + * Get the set of active groups. The returned {@code Set} reflects changes + * to the viewer state. It is unmodifiable and not thread-safe. + * + * @return the set of active groups + */ + @Override + public synchronized Set< SourceGroup > getActiveGroups() + { + return state.getActiveGroups(); + } + + /** + * Check whether the given {@code group} is active. + * + * Returns {@code true}, if {@code group} is active. Returns {@code false}, + * if {@code group} is inactive or not valid (for example because it has + * been removed from the state by a different thread). + * + * @return {@code true}, if {@code group} is active + * + * @throws NullPointerException + * if {@code group == null} + * @throws IllegalArgumentException + * if {@code group} is not contained in the state (and not + * {@code null}). + */ + @Override + public synchronized boolean isGroupActive( final SourceGroup group ) + { + try + { + return state.isGroupActive( group ); + } + catch ( final IllegalArgumentException e ) + { + if ( DEBUG ) + e.printStackTrace(); + return false; + } + } + + /** + * Set {@code group} active or inactive. + * <p> + * Returns {@code true}, if group activity changes as a result of the call. + * Returns {@code false}, if {@code group} is already in the desired + * {@code active} state. + * <p> + * Also returns {@code false}, if {@code group} is not valid (for example + * because it has been removed from the state by a different thread). + * + * @return {@code true}, if group activity changed as a result of the call + * + * @throws NullPointerException + * if {@code group == null} + */ + @Override + public synchronized boolean setGroupActive( final SourceGroup group, final boolean active ) + { + try + { + return state.setGroupActive( group, active ); + } + catch ( final IllegalArgumentException e ) + { + if ( DEBUG ) + e.printStackTrace(); + return false; + } + } + + /** + * Set all groups in {@code collection} active or inactive. + * <p> + * Returns {@code true}, if group activity changes as a result of the call. + * Returns {@code false}, if all groups were already in the desired + * {@code active} state. + * <p> + * Does nothing and returns {@code false}, if any group in + * {@code collection} is not valid (for example because it was removed from + * the state by a different thread). + * + * @return {@code true}, if group activity changed as a result of the call + * + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + */ + @Override + public synchronized boolean setGroupsActive( final Collection< ? extends SourceGroup > collection, final boolean active ) + { + try + { + return state.setGroupsActive( collection, active ); + } + catch ( final IllegalArgumentException e ) + { + if ( DEBUG ) + e.printStackTrace(); + return false; + } + } + + /** + * Get the name of a {@code group}. + * <p> + * Returns {@code null}, if {@code group} is not valid (for example because + * it has been removed from the state by a different thread), an empty set + * is returned. + * + * @return name of the group, may be {@code null} + * + * @throws NullPointerException + * if {@code group == null} + */ + @Override + public synchronized String getGroupName( final SourceGroup group ) + { + try + { + return state.getGroupName( group ); + } + catch ( final IllegalArgumentException e ) + { + if ( DEBUG ) + e.printStackTrace(); + return null; + } + } + + /** + * Set the {@code name} of a {@code group}. + * <p> + * Does nothing, if {@code group} is not valid (for example because it has + * been removed from the state by a different thread), an empty set is + * returned. + * + * @throws NullPointerException + * if {@code group == null} + */ + @Override + public synchronized void setGroupName( final SourceGroup group, final String name ) + { + try + { + state.setGroupName( group, name ); + } + catch ( final IllegalArgumentException e ) + { + if ( DEBUG ) + e.printStackTrace(); + } + } + + /** + * Check whether the state contains the {@code group}. + * + * @return {@code true}, if {@code group} is in the list of groups. + * + * @throws NullPointerException + * if {@code group == null} + */ + @Override + public synchronized boolean containsGroup( final SourceGroup group ) + { + return state.containsGroup( group ); + } + + /** + * Add {@code group} to the state. Returns {@code true}, if the group is + * added. Returns {@code false}, if the group is already present. + * <p> + * If {@code group} is added and no other group was current, then + * {@code group} is made current + * + * @return {@code true}, if list of groups changed as a result of the call. + * + * @throws NullPointerException + * if {@code group == null} + */ + @Override + public synchronized boolean addGroup( final SourceGroup group ) + { + return state.addGroup( group ); + } + + /** + * Add all groups in {@code collection} to the state. Returns {@code true}, + * if at least one group was added. Returns {@code false}, if all groups + * were already present. + * <p> + * If any groups are added and no other group was current, then the first + * added groups will be made current. + * + * @return {@code true}, if list of groups changed as a result of the call. + * + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + */ + @Override + public synchronized boolean addGroups( final Collection< ? extends SourceGroup > collection ) + { + return state.addGroups( collection ); + } + + /** + * Remove {@code group} from the state. + * <p> + * Returns {@code true}, if {@code group} was removed from the state. + * Returns {@code false}, if {@code group} was not contained in state. + * <p> + * If {@code group} was current, then the first group in the list of groups + * is made current (if it exists). + * + * @return {@code true}, if list of groups changed as a result of the call + * + * @throws NullPointerException + * if {@code group == null} + */ + @Override + public synchronized boolean removeGroup( final SourceGroup group ) + { + return state.removeGroup( group ); + } + + /** + * Remove all groups in {@code collection} from the state. Returns + * {@code true}, if at least one group was removed. Returns {@code false}, + * if none of the groups was present. + * <p> + * If the current group was removed, then the first group in the remaining + * list of groups is made current (if it exists). + * + * @return {@code true}, if list of groups changed as a result of the call. + * + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + */ + @Override + public synchronized boolean removeGroups( final Collection< ? extends SourceGroup > collection ) + { + return state.removeGroups( collection ); + } + + /** + * Add {@code source} to {@code group}. + * <p> + * Returns {@code true}, if {@code source} was added to {@code group}. + * Returns {@code false}, if {@code source} was already contained in + * {@code group}. or either of {@code source} and {@code group} is not valid + * (not in the BDV sources/groups list). + * <p> + * Does nothing and returns {@code false}, if {@code source} or + * {@code group} are not valid (for example because they were removed from + * the state by a different thread). + * + * @return {@code true}, if set of sources in {@code group} changed as a + * result of the call + * + * @throws NullPointerException + * if {@code source == null} or {@code group == null} + */ + @Override + public synchronized boolean addSourceToGroup( final SourceAndConverter< ? > source, final SourceGroup group ) + { + try + { + return state.addSourceToGroup( source, group ); + } + catch ( final IllegalArgumentException e ) + { + if ( DEBUG ) + e.printStackTrace(); + return false; + } + } + + /** + * Add all sources in {@code collection} to {@code group}. + * <p> + * Returns {@code true}, if at least one source was added to {@code group}. + * Returns {@code false}, if all sources were already contained in + * {@code group}. + * <p> + * Does nothing and returns {@code false}, if {@code group} or any source in + * {@code collection} are not valid (for example because they were removed + * from the state by a different thread). + * + * @return {@code true}, if set of sources in {@code group} changed as a + * result of the call + * + * @throws NullPointerException + * if {@code group == null} or {@code collection == null} or any element + * of {@code collection} is {@code null}. + */ + @Override + public synchronized boolean addSourcesToGroup( final Collection< ? extends SourceAndConverter< ? > > collection, final SourceGroup group ) + { + try + { + return state.addSourcesToGroup( collection, group ); + } + catch ( final IllegalArgumentException e ) + { + if ( DEBUG ) + e.printStackTrace(); + return false; + } + } + + /** + * Remove {@code source} from {@code group}. + * <p> + * Returns {@code true}, if {@code source} was removed from {@code group}. + * Returns {@code false}, if {@code source} was not contained in + * {@code group}, + * <p> + * Does nothing and returns {@code false}, if {@code source} or + * {@code group} are not valid (for example because they were removed from + * the state by a different thread). + * + * @return {@code true}, if set of sources in {@code group} changed as a + * result of the call + * + * @throws NullPointerException + * if {@code source == null} or {@code group == null} + */ + @Override + public synchronized boolean removeSourceFromGroup( final SourceAndConverter< ? > source, final SourceGroup group ) + { + try + { + return state.removeSourceFromGroup( source, group ); + } + catch ( final IllegalArgumentException e ) + { + if ( DEBUG ) + e.printStackTrace(); + return false; + } + } + + /** + * Remove all sources in {@code collection} from {@code group}. + * <p> + * Returns {@code true}, if at least one source was removed from + * {@code group}. Returns {@code false}, if none of the sources were + * contained in {@code group}. + * <p> + * Does nothing and returns {@code false}, if {@code group} or any source in + * {@code collection} are not valid (for example because they were removed + * from the state by a different thread). + * + * @return {@code true}, if set of sources in {@code group} changed as a + * result of the call + * + * @throws NullPointerException + * if {@code group == null} or {@code collection == null} or any element + * of {@code collection} is {@code null}. + */ + @Override + public synchronized boolean removeSourcesFromGroup( final Collection< ? extends SourceAndConverter< ? > > collection, final SourceGroup group ) + { + try + { + return state.removeSourcesFromGroup( collection, group ); + } + catch ( final IllegalArgumentException e ) + { + if ( DEBUG ) + e.printStackTrace(); + return false; + } + } + + /** + * Get the set sources in {@code group}. The returned {@code Set} reflects + * changes to the viewer state. It is unmodifiable and not thread-safe. + * <p> + * If {@code group} is not valid (for example because it has been removed + * from the state by a different thread), an empty set is returned. + * + * @return the set of sources in {@code group} + * + * @throws NullPointerException + * if {@code group == null} + */ + @Override + public synchronized Set< SourceAndConverter< ? > > getSourcesInGroup( final SourceGroup group ) + { + try + { + return state.getSourcesInGroup( group ); + } + catch ( final IllegalArgumentException e ) + { + if ( DEBUG ) + e.printStackTrace(); + return Collections.emptySet(); + } + } + + /** + * Remove all groups from the state. + */ + @Override + public synchronized void clearGroups() + { + state.clearGroups(); + } + + /** + * Returns a {@link Comparator} that compares groups according to the order + * in which they occur in the groups list. (Groups that do not occur in the + * list are ordered before any group in the list). + */ + @Override + public synchronized Comparator< SourceGroup > groupOrder() + { + return state.groupOrder(); + } + + /** + * Returns the wrapped {@code ViewerState}. + * <p> + * <em>When using this, explicit synchronization (on this + * {@code SynchronizedViewerState}) is required. PLEASE BE CAREFUL!</em> + */ + // TODO: REMOVE? + public ViewerState getWrappedState() + { + return state; + } +} diff --git a/src/main/java/bdv/viewer/TimePointListener.java b/src/main/java/bdv/viewer/TimePointListener.java index 8181a4c1203f255c14e3b9b3180a504b9a8161d2..abd0d022b291825c966c1f06e1d721a83ecb5a3a 100644 --- a/src/main/java/bdv/viewer/TimePointListener.java +++ b/src/main/java/bdv/viewer/TimePointListener.java @@ -1,9 +1,8 @@ /*- * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -31,5 +30,5 @@ package bdv.viewer; public interface TimePointListener { - public void timePointChanged( int timePointIndex ); + void timePointChanged( int timePointIndex ); } diff --git a/src/main/java/bdv/viewer/TransformListener.java b/src/main/java/bdv/viewer/TransformListener.java new file mode 100644 index 0000000000000000000000000000000000000000..512615632b67e7d48c7d8761c78fcf8c56aa9440 --- /dev/null +++ b/src/main/java/bdv/viewer/TransformListener.java @@ -0,0 +1,34 @@ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer; + +public interface TransformListener< A > +{ + void transformChanged( A transform ); +} diff --git a/src/main/java/bdv/viewer/UnmodifiableViewerState.java b/src/main/java/bdv/viewer/UnmodifiableViewerState.java new file mode 100644 index 0000000000000000000000000000000000000000..061b214d4954c67e3959c71c2575006e8fc519a1 --- /dev/null +++ b/src/main/java/bdv/viewer/UnmodifiableViewerState.java @@ -0,0 +1,370 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import net.imglib2.realtransform.AffineTransform3D; +import org.scijava.listeners.Listeners; + +/** + * Wraps another {@link ViewerState} and throws + * {@code UnsupportedOperationException} for all modification operations. + * + * @author Tobias Pietzsch + */ +class UnmodifiableViewerState implements ViewerState +{ + private final ViewerState state; + + public UnmodifiableViewerState( final ViewerState state ) + { + this.state = state; + } + + @Override + public ViewerState snapshot() + { + return new UnmodifiableViewerState( state.snapshot() ); + } + + @Override + public Listeners< ViewerStateChangeListener > changeListeners() + { + return state.changeListeners(); + } + + @Override + public Interpolation getInterpolation() + { + return state.getInterpolation(); + } + + @Override + public void setInterpolation( final Interpolation i ) + { + throw new UnsupportedOperationException(); + } + + @Override + public DisplayMode getDisplayMode() + { + return state.getDisplayMode(); + } + + @Override + public void setDisplayMode( final DisplayMode mode ) + { + throw new UnsupportedOperationException(); + } + + @Override + public int getNumTimepoints() + { + return state.getNumTimepoints(); + } + + @Override + public void setNumTimepoints( final int n ) + { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentTimepoint() + { + return state.getCurrentTimepoint(); + } + + @Override + public void setCurrentTimepoint( final int t ) + { + throw new UnsupportedOperationException(); + } + + @Override + public void getViewerTransform( final AffineTransform3D t ) + { + state.getViewerTransform( t ); + } + + @Override + public void setViewerTransform( final AffineTransform3D t ) + { + throw new UnsupportedOperationException(); + } + + @Override + public List< SourceAndConverter< ? > > getSources() + { + return state.getSources(); + } + + @Override + public SourceAndConverter< ? > getCurrentSource() + { + return state.getCurrentSource(); + } + + @Override + public boolean isCurrentSource( final SourceAndConverter< ? > source ) + { + return state.isCurrentSource( source ); + } + + @Override + public boolean setCurrentSource( final SourceAndConverter< ? > source ) + { + throw new UnsupportedOperationException(); + } + + @Override + public Set< SourceAndConverter< ? > > getActiveSources() + { + return state.getActiveSources(); + } + + @Override + public boolean isSourceActive( final SourceAndConverter< ? > source ) + { + return state.isSourceActive( source ); + } + + @Override + public boolean setSourceActive( final SourceAndConverter< ? > source, final boolean active ) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setSourcesActive( final Collection< ? extends SourceAndConverter< ? > > collection, final boolean active ) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isSourceVisible( final SourceAndConverter< ? > source ) + { + return state.isSourceVisible( source ); + } + + @Override + public boolean isSourceVisibleAndPresent( final SourceAndConverter< ? > source ) + { + return state.isSourceVisibleAndPresent( source ); + } + + @Override + public Set< SourceAndConverter< ? > > getVisibleSources() + { + return state.getVisibleSources(); + } + + @Override + public Set< SourceAndConverter< ? > > getVisibleAndPresentSources() + { + return state.getVisibleAndPresentSources(); + } + + @Override + public boolean containsSource( final SourceAndConverter< ? > source ) + { + return state.containsSource( source ); + } + + @Override + public boolean addSource( final SourceAndConverter< ? > source ) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addSources( final Collection< ? extends SourceAndConverter< ? > > collection ) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeSource( final SourceAndConverter< ? > source ) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeSources( final Collection< ? extends SourceAndConverter< ? > > collection ) + { + throw new UnsupportedOperationException(); + } + + @Override + public void clearSources() + { + throw new UnsupportedOperationException(); + } + + @Override + public Comparator< SourceAndConverter< ? > > sourceOrder() + { + return state.sourceOrder(); + } + + @Override + public List< SourceGroup > getGroups() + { + return state.getGroups(); + } + + @Override + public SourceGroup getCurrentGroup() + { + return state.getCurrentGroup(); + } + + @Override + public boolean isCurrentGroup( final SourceGroup group ) + { + return state.isCurrentGroup( group ); + } + + @Override + public boolean setCurrentGroup( final SourceGroup group ) + { + throw new UnsupportedOperationException(); + } + + @Override + public Set< SourceGroup > getActiveGroups() + { + return state.getActiveGroups(); + } + + @Override + public boolean isGroupActive( final SourceGroup group ) + { + return state.isGroupActive( group ); + } + + @Override + public boolean setGroupActive( final SourceGroup group, final boolean active ) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setGroupsActive( final Collection< ? extends SourceGroup > collection, final boolean active ) + { + throw new UnsupportedOperationException(); + } + + @Override + public String getGroupName( final SourceGroup group ) + { + return state.getGroupName( group ); + } + + @Override + public void setGroupName( final SourceGroup group, final String name ) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsGroup( final SourceGroup group ) + { + return state.containsGroup( group ); + } + + @Override + public boolean addGroup( final SourceGroup group ) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addGroups( final Collection< ? extends SourceGroup > collection ) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeGroup( final SourceGroup group ) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeGroups( final Collection< ? extends SourceGroup > collection ) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addSourceToGroup( final SourceAndConverter< ? > source, final SourceGroup group ) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addSourcesToGroup( final Collection< ? extends SourceAndConverter< ? > > collection, final SourceGroup group ) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeSourceFromGroup( final SourceAndConverter< ? > source, final SourceGroup group ) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeSourcesFromGroup( final Collection< ? extends SourceAndConverter< ? > > collection, final SourceGroup group ) + { + throw new UnsupportedOperationException(); + } + + @Override + public Set< SourceAndConverter< ? > > getSourcesInGroup( final SourceGroup group ) + { + return state.getSourcesInGroup( group ); + } + + @Override + public void clearGroups() + { + throw new UnsupportedOperationException(); + } + + @Override + public Comparator< SourceGroup > groupOrder() + { + return state.groupOrder(); + } +} diff --git a/src/main/java/bdv/viewer/ViewerFrame.java b/src/main/java/bdv/viewer/ViewerFrame.java index a0f156715b995375a12a1ec8c6c882a984564471..b6d273da6615fd2e10e41899e3b15fbfa349075c 100644 --- a/src/main/java/bdv/viewer/ViewerFrame.java +++ b/src/main/java/bdv/viewer/ViewerFrame.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,6 +28,10 @@ */ package bdv.viewer; +import bdv.TransformEventHandler; +import bdv.ui.CardPanel; +import bdv.ui.BdvDefaultCards; +import bdv.ui.splitpanel.SplitPanel; import java.awt.BorderLayout; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; @@ -40,30 +43,35 @@ import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import org.scijava.ui.behaviour.MouseAndKeyHandler; +import org.scijava.ui.behaviour.util.Behaviours; import org.scijava.ui.behaviour.util.InputActionBindings; import org.scijava.ui.behaviour.util.TriggerBehaviourBindings; -import bdv.BehaviourTransformEventHandler; import bdv.cache.CacheControl; -import net.imglib2.ui.TransformEventHandler; -import net.imglib2.ui.util.GuiUtil; +import bdv.util.AWTUtils; /** * A {@link JFrame} containing a {@link ViewerPanel} and associated * {@link InputActionBindings}. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class ViewerFrame extends JFrame { private static final long serialVersionUID = 1L; - protected final ViewerPanel viewer; + private final ViewerPanel viewer; + + private final CardPanel cards; + + private final SplitPanel splitPanel; private final InputActionBindings keybindings; private final TriggerBehaviourBindings triggerbindings; + private final ConverterSetups setups; + public ViewerFrame( final List< SourceAndConverter< ? > > sources, final int numTimepoints, @@ -90,13 +98,21 @@ public class ViewerFrame extends JFrame final ViewerOptions optional ) { // super( "BigDataViewer", GuiUtil.getSuitableGraphicsConfiguration( GuiUtil.ARGB_COLOR_MODEL ) ); - super( "BigDataViewer", GuiUtil.getSuitableGraphicsConfiguration( GuiUtil.RGB_COLOR_MODEL ) ); + super( "BigDataViewer", AWTUtils.getSuitableGraphicsConfiguration( AWTUtils.RGB_COLOR_MODEL ) ); viewer = new ViewerPanel( sources, numTimepoints, cacheControl, optional ); + setups = new ConverterSetups( viewer.state() ); + setups.listeners().add( s -> viewer.requestRepaint() ); + keybindings = new InputActionBindings(); triggerbindings = new TriggerBehaviourBindings(); + cards = new CardPanel(); + BdvDefaultCards.setup( cards, viewer, setups ); + splitPanel = new SplitPanel( viewer, cards ); + getRootPane().setDoubleBuffered( true ); - add( viewer, BorderLayout.CENTER ); +// add( viewer, BorderLayout.CENTER ); + add( splitPanel, BorderLayout.CENTER ); pack(); setDefaultCloseOperation( WindowConstants.DISPOSE_ON_CLOSE ); addWindowListener( new WindowAdapter() @@ -108,8 +124,8 @@ public class ViewerFrame extends JFrame } } ); - SwingUtilities.replaceUIActionMap( getRootPane(), keybindings.getConcatenatedActionMap() ); - SwingUtilities.replaceUIInputMap( getRootPane(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, keybindings.getConcatenatedInputMap() ); + SwingUtilities.replaceUIActionMap( viewer, keybindings.getConcatenatedActionMap() ); + SwingUtilities.replaceUIInputMap( viewer, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, keybindings.getConcatenatedInputMap() ); final MouseAndKeyHandler mouseAndKeyHandler = new MouseAndKeyHandler(); mouseAndKeyHandler.setInputMap( triggerbindings.getConcatenatedInputTriggerMap() ); @@ -117,9 +133,12 @@ public class ViewerFrame extends JFrame mouseAndKeyHandler.setKeypressManager( optional.values.getKeyPressedManager(), viewer.getDisplay() ); viewer.getDisplay().addHandler( mouseAndKeyHandler ); - final TransformEventHandler< ? > tfHandler = viewer.getDisplay().getTransformEventHandler(); - if ( tfHandler instanceof BehaviourTransformEventHandler ) - ( ( BehaviourTransformEventHandler< ? > ) tfHandler ).install( triggerbindings ); + // TODO: should be a field? + final Behaviours transformBehaviours = new Behaviours( optional.values.getInputTriggerConfig(), "bdv" ); + transformBehaviours.install( triggerbindings, "transform" ); + + final TransformEventHandler tfHandler = viewer.getTransformEventHandler(); + tfHandler.install( transformBehaviours ); } public ViewerPanel getViewerPanel() @@ -127,6 +146,16 @@ public class ViewerFrame extends JFrame return viewer; } + public CardPanel getCardPanel() + { + return cards; + } + + public SplitPanel getSplitPanel() + { + return splitPanel; + } + public InputActionBindings getKeybindings() { return keybindings; @@ -136,4 +165,9 @@ public class ViewerFrame extends JFrame { return triggerbindings; } + + public ConverterSetups getConverterSetups() + { + return setups; + } } diff --git a/src/main/java/bdv/viewer/ViewerOptions.java b/src/main/java/bdv/viewer/ViewerOptions.java index 79d8f7fa19d0efed9c5a39b572f372e5e0855f8a..b977c4585e8fdfadafe7b32ffc2efb6bb0a283f4 100644 --- a/src/main/java/bdv/viewer/ViewerOptions.java +++ b/src/main/java/bdv/viewer/ViewerOptions.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,12 +28,12 @@ */ package bdv.viewer; +import bdv.TransformEventHandler3D; import java.awt.event.KeyListener; import org.scijava.ui.behaviour.KeyPressedManager; import org.scijava.ui.behaviour.io.InputTriggerConfig; -import bdv.BehaviourTransformEventHandler3D; import bdv.viewer.animate.MessageOverlayAnimator; import bdv.viewer.render.AccumulateProjector; import bdv.viewer.render.AccumulateProjectorARGB; @@ -42,12 +41,12 @@ import bdv.viewer.render.AccumulateProjectorFactory; import bdv.viewer.render.MultiResolutionRenderer; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.type.numeric.ARGBType; -import net.imglib2.ui.TransformEventHandlerFactory; +import bdv.TransformEventHandlerFactory; /** * Optional parameters for {@link ViewerPanel}. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class ViewerOptions { @@ -111,19 +110,6 @@ public class ViewerOptions return this; } - /** - * Set whether to used double buffered rendering. - * - * @param d - * Whether to use double buffered rendering. - * @see MultiResolutionRenderer - */ - public ViewerOptions doubleBuffered( final boolean d ) - { - values.doubleBuffered = d; - return this; - } - /** * Set how many threads to use for rendering. * @@ -169,7 +155,7 @@ public class ViewerOptions return this; } - public ViewerOptions transformEventHandlerFactory( final TransformEventHandlerFactory< AffineTransform3D > f ) + public ViewerOptions transformEventHandlerFactory( final TransformEventHandlerFactory f ) { values.transformEventHandlerFactory = f; return this; @@ -232,8 +218,6 @@ public class ViewerOptions private long targetRenderNanos = 30 * 1000000l; - private boolean doubleBuffered = true; - private int numRenderingThreads = 3; private int numSourceGroups = 10; @@ -242,7 +226,7 @@ public class ViewerOptions private MessageOverlayAnimator msgOverlay = new MessageOverlayAnimator( 800 ); - private TransformEventHandlerFactory< AffineTransform3D > transformEventHandlerFactory = BehaviourTransformEventHandler3D.factory(); + private TransformEventHandlerFactory transformEventHandlerFactory = TransformEventHandler3D::new; private AccumulateProjectorFactory< ARGBType > accumulateProjectorFactory = AccumulateProjectorARGB.factory; @@ -257,7 +241,6 @@ public class ViewerOptions height( height ). screenScales( screenScales ). targetRenderNanos( targetRenderNanos ). - doubleBuffered( doubleBuffered ). numRenderingThreads( numRenderingThreads ). numSourceGroups( numSourceGroups ). useVolatileIfAvailable( useVolatileIfAvailable ). @@ -287,11 +270,6 @@ public class ViewerOptions return targetRenderNanos; } - public boolean isDoubleBuffered() - { - return doubleBuffered; - } - public int getNumRenderingThreads() { return numRenderingThreads; @@ -312,7 +290,7 @@ public class ViewerOptions return msgOverlay; } - public TransformEventHandlerFactory< AffineTransform3D > getTransformEventHandlerFactory() + public TransformEventHandlerFactory getTransformEventHandlerFactory() { return transformEventHandlerFactory; } diff --git a/src/main/java/bdv/viewer/ViewerPanel.java b/src/main/java/bdv/viewer/ViewerPanel.java index ec19a2d83849addaa707e907ca440f68ca941a95..39bc9a54847fe633dd6451fd708e4ffc58bda556 100644 --- a/src/main/java/bdv/viewer/ViewerPanel.java +++ b/src/main/java/bdv/viewer/ViewerPanel.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,15 +28,8 @@ */ package bdv.viewer; -import static bdv.viewer.VisibilityAndGrouping.Event.CURRENT_SOURCE_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.DISPLAY_MODE_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.GROUP_ACTIVITY_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.GROUP_NAME_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.NUM_GROUPS_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.NUM_SOURCES_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.SOURCE_ACTVITY_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.VISIBILITY_CHANGED; - +import bdv.TransformEventHandler; +import bdv.TransformState; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Font; @@ -48,8 +40,8 @@ import java.awt.event.ComponentEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; -import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -63,14 +55,14 @@ import javax.swing.DefaultBoundedRangeModel; import javax.swing.JPanel; import javax.swing.JSlider; import javax.swing.SwingConstants; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; +import javax.swing.SwingUtilities; +import net.imglib2.Interval; +import bdv.viewer.render.awt.BufferedImageOverlayRenderer; import org.jdom2.Element; import bdv.cache.CacheControl; import bdv.util.Affine3DHelpers; -import bdv.util.InvokeOnEDT; import bdv.util.Prefs; import bdv.viewer.animate.AbstractTransformAnimator; import bdv.viewer.animate.MessageOverlayAnimator; @@ -82,9 +74,7 @@ import bdv.viewer.overlay.MultiBoxOverlayRenderer; import bdv.viewer.overlay.ScaleBarOverlayRenderer; import bdv.viewer.overlay.SourceInfoOverlayRenderer; import bdv.viewer.render.MultiResolutionRenderer; -import bdv.viewer.render.TransformAwareBufferedImageOverlayRenderer; import bdv.viewer.state.SourceGroup; -import bdv.viewer.state.SourceState; import bdv.viewer.state.ViewerState; import bdv.viewer.state.XmlIoViewerState; import net.imglib2.Positionable; @@ -92,25 +82,21 @@ import net.imglib2.RealLocalizable; import net.imglib2.RealPoint; import net.imglib2.RealPositionable; import net.imglib2.realtransform.AffineTransform3D; -import net.imglib2.ui.InteractiveDisplayCanvasComponent; -import net.imglib2.ui.OverlayRenderer; -import net.imglib2.ui.PainterThread; -import net.imglib2.ui.TransformEventHandler; -import net.imglib2.ui.TransformListener; +import bdv.viewer.render.PainterThread; import net.imglib2.util.LinAlgHelpers; - +import org.scijava.listeners.Listeners; /** * A JPanel for viewing multiple of {@link Source}s. The panel contains a - * {@link InteractiveDisplayCanvasComponent canvas} and a time slider (if there + * {@link InteractiveDisplayCanvas canvas} and a time slider (if there * are multiple time-points). Maintains a {@link ViewerState render state}, the * renderer, and basic navigation help overlays. It has it's own * {@link PainterThread} for painting, which is started on construction (use * {@link #stop() to stop the PainterThread}. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ -public class ViewerPanel extends JPanel implements OverlayRenderer, TransformListener< AffineTransform3D >, PainterThread.Paintable, VisibilityAndGrouping.UpdateListener, RequestRepaint +public class ViewerPanel extends JPanel implements OverlayRenderer, PainterThread.Paintable, ViewerStateChangeListener, RequestRepaint { private static final long serialVersionUID = 1L; @@ -128,7 +114,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis /** * TODO */ - protected final TransformAwareBufferedImageOverlayRenderer renderTarget; + protected final BufferedImageOverlayRenderer renderTarget; /** * Overlay navigation boxes. @@ -147,19 +133,18 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis */ protected final ScaleBarOverlayRenderer scaleBarOverlayRenderer; - /** - * Transformation set by the interactive viewer. - */ - protected final AffineTransform3D viewerTransform; + private final TransformEventHandler transformEventHandler; /** * Canvas used for displaying the rendered {@link #renderTarget image} and * overlays. */ - protected final InteractiveDisplayCanvasComponent< AffineTransform3D > display; + protected final InteractiveDisplayCanvas display; protected final JSlider sliderTime; + private boolean blockSliderTimeEvents; + /** * A {@link ThreadGroup} for (only) the threads used by this * {@link ViewerPanel}, that is, {@link #painterThread} and @@ -191,20 +176,11 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis /** * These listeners will be notified about changes to the - * {@link #viewerTransform}. This is done <em>before</em> calling + * viewer-transform. This is done <em>before</em> calling * {@link #requestRepaint()} so listeners have the chance to interfere. */ protected final CopyOnWriteArrayList< TransformListener< AffineTransform3D > > transformListeners; - /** - * These listeners will be notified about changes to the - * {@link #viewerTransform} that was used to render the current image. This - * is intended for example for {@link OverlayRenderer}s that need to exactly - * match the transform of their overlaid content to the transform of the - * image. - */ - protected final CopyOnWriteArrayList< TransformListener< AffineTransform3D > > lastRenderTransformListeners; - /** * These listeners will be notified about changes to the current timepoint * {@link ViewerState#getCurrentTimepoint()}. This is done <em>before</em> @@ -274,23 +250,21 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis threadGroup = new ThreadGroup( this.toString() ); painterThread = new PainterThread( threadGroup, this ); painterThread.setDaemon( true ); - viewerTransform = new AffineTransform3D(); - display = new InteractiveDisplayCanvasComponent<>( - options.getWidth(), options.getHeight(), options.getTransformEventHandlerFactory() ); - display.addTransformListener( this ); - renderTarget = new TransformAwareBufferedImageOverlayRenderer(); - renderTarget.setCanvasSize( options.getWidth(), options.getHeight() ); - display.addOverlayRenderer( renderTarget ); - display.addOverlayRenderer( this ); + transformEventHandler = options.getTransformEventHandlerFactory().create( + TransformState.from( state()::getViewerTransform, state()::setViewerTransform ) ); + renderTarget = new BufferedImageOverlayRenderer(); + display = new InteractiveDisplayCanvas( options.getWidth(), options.getHeight() ); + display.setTransformEventHandler( transformEventHandler ); + display.overlays().add( renderTarget ); + display.overlays().add( this ); renderingExecutorService = Executors.newFixedThreadPool( options.getNumRenderingThreads(), - new RenderThreadFactory() ); + new RenderThreadFactory( threadGroup ) ); imageRenderer = new MultiResolutionRenderer( renderTarget, painterThread, options.getScreenScales(), options.getTargetRenderNanos(), - options.isDoubleBuffered(), options.getNumRenderingThreads(), renderingExecutorService, options.isUseVolatileIfAvailable(), @@ -301,25 +275,19 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis display.addHandler( mouseCoordinates ); sliderTime = new JSlider( SwingConstants.HORIZONTAL, 0, numTimepoints - 1, 0 ); - sliderTime.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( final ChangeEvent e ) - { - if ( e.getSource().equals( sliderTime ) ) - setTimepoint( sliderTime.getValue() ); - } + sliderTime.addChangeListener( e -> { + if ( !blockSliderTimeEvents ) + setTimepoint( sliderTime.getValue() ); } ); add( display, BorderLayout.CENTER ); if ( numTimepoints > 1 ) add( sliderTime, BorderLayout.SOUTH ); + setFocusable( false ); visibilityAndGrouping = new VisibilityAndGrouping( state ); - visibilityAndGrouping.addUpdateListener( this ); transformListeners = new CopyOnWriteArrayList<>(); - lastRenderTransformListeners = new CopyOnWriteArrayList<>(); timePointListeners = new CopyOnWriteArrayList<>(); interpolationModeListeners = new CopyOnWriteArrayList<>(); @@ -339,75 +307,95 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis } } ); + state.getState().changeListeners().add( this ); + painterThread.start(); } + /** + * @deprecated Modify {@link #state()} directly + */ + @Deprecated public void addSource( final SourceAndConverter< ? > sourceAndConverter ) { - synchronized ( visibilityAndGrouping ) - { - state.addSource( sourceAndConverter ); - visibilityAndGrouping.update( NUM_SOURCES_CHANGED ); - } - requestRepaint(); + state().addSource( sourceAndConverter ); + state().setSourceActive( sourceAndConverter, true ); } + /** + * @deprecated Modify {@link #state()} directly + */ + @Deprecated public void addSources( final Collection< SourceAndConverter< ? > > sourceAndConverter ) { - synchronized ( visibilityAndGrouping ) - { - sourceAndConverter.forEach( state::addSource ); - visibilityAndGrouping.update( NUM_SOURCES_CHANGED ); - } - requestRepaint(); + state().addSources( sourceAndConverter ); + } + + // helper for deprecated methods taking Source<?> + @Deprecated + private SourceAndConverter< ? > soc( final Source< ? > source ) + { + for ( final SourceAndConverter< ? > soc : state().getSources() ) + if ( soc.getSpimSource() == source ) + return soc; + return null; } + /** + * @deprecated Modify {@link #state()} directly + */ + @Deprecated public void removeSource( final Source< ? > source ) { - synchronized ( visibilityAndGrouping ) + synchronized ( state() ) { - state.removeSource( source ); - visibilityAndGrouping.update( NUM_SOURCES_CHANGED ); + state().removeSource( soc( source ) ); } - requestRepaint(); } + /** + * @deprecated Modify {@link #state()} directly + */ + @Deprecated public void removeSources( final Collection< Source< ? > > sources ) { - synchronized ( visibilityAndGrouping ) + synchronized ( state() ) { - sources.forEach( state::removeSource ); - visibilityAndGrouping.update( NUM_SOURCES_CHANGED ); + state().removeSources( sources.stream().map( this::soc ).collect( Collectors.toList() ) ); } - requestRepaint(); } + /** + * @deprecated Modify {@link #state()} directly + */ + @Deprecated public void removeAllSources() { - synchronized ( visibilityAndGrouping ) - { - removeSources( getState().getSources().stream().map( SourceAndConverter::getSpimSource ).collect( Collectors.toList() ) ); - } + state().clearSources(); } + /** + * @deprecated Modify {@link #state()} directly + */ + @Deprecated public void addGroup( final SourceGroup group ) { - synchronized ( visibilityAndGrouping ) + synchronized ( state() ) { state.addGroup( group ); - visibilityAndGrouping.update( NUM_GROUPS_CHANGED ); } - requestRepaint(); } + /** + * @deprecated Modify {@link #state()} directly + */ + @Deprecated public void removeGroup( final SourceGroup group ) { - synchronized ( visibilityAndGrouping ) + synchronized ( state() ) { state.removeGroup( group ); - visibilityAndGrouping.update( NUM_GROUPS_CHANGED ); } - requestRepaint(); } /** @@ -421,7 +409,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis { assert gPos.length >= 3; - viewerTransform.applyInverse( gPos, gPos ); + state().getViewerTransform().applyInverse( gPos, gPos ); } /** @@ -435,7 +423,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis { assert gPos.numDimensions() >= 3; - viewerTransform.applyInverse( gPos, gPos ); + state().getViewerTransform().applyInverse( gPos, gPos ); } /** @@ -451,7 +439,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis final RealPoint lPos = new RealPoint( 3 ); lPos.setPosition( x, 0 ); lPos.setPosition( y, 1 ); - viewerTransform.applyInverse( gPos, lPos ); + state().getViewerTransform().applyInverse( gPos, lPos ); } /** @@ -466,7 +454,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis assert gPos.numDimensions() == 3; final RealPoint lPos = new RealPoint( 3 ); mouseCoordinates.getMouseCoordinates( lPos ); - viewerTransform.applyInverse( gPos, lPos ); + state().getViewerTransform().applyInverse( gPos, lPos ); } /** @@ -482,7 +470,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis @Override public void paint() { - imageRenderer.paint( state ); + imageRenderer.paint( state.getState() ); display.repaint(); @@ -490,12 +478,12 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis { if ( currentAnimator != null ) { - final TransformEventHandler< AffineTransform3D > handler = display.getTransformEventHandler(); final AffineTransform3D transform = currentAnimator.getCurrent( System.currentTimeMillis() ); - handler.setTransform( transform ); - transformChanged( transform ); + state().setViewerTransform( transform ); if ( currentAnimator.isComplete() ) currentAnimator = null; + else + requestRepaint(); } } } @@ -509,21 +497,26 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis imageRenderer.requestRepaint(); } + public void requestRepaint( final Interval screenInterval ) + { + imageRenderer.requestRepaint( screenInterval ); + } + @Override public void drawOverlays( final Graphics g ) { boolean requiresRepaint = false; if ( Prefs.showMultibox() ) { - multiBoxOverlayRenderer.setViewerState( state ); + multiBoxOverlayRenderer.setViewerState( state() ); multiBoxOverlayRenderer.updateVirtualScreenSize( display.getWidth(), display.getHeight() ); multiBoxOverlayRenderer.paint( ( Graphics2D ) g ); requiresRepaint = multiBoxOverlayRenderer.isHighlightInProgress(); } - if( Prefs.showTextOverlay() ) + if ( Prefs.showTextOverlay() ) { - sourceInfoOverlayRenderer.setViewerState( state ); + sourceInfoOverlayRenderer.setViewerState( state() ); sourceInfoOverlayRenderer.paint( ( Graphics2D ) g ); final RealPoint gPos = new RealPoint( 3 ); @@ -537,7 +530,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis if ( Prefs.showScaleBar() ) { - scaleBarOverlayRenderer.setViewerState( state ); + scaleBarOverlayRenderer.setViewerState( state() ); scaleBarOverlayRenderer.paint( ( Graphics2D ) g ); } @@ -557,32 +550,25 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis } @Override - public synchronized void transformChanged( final AffineTransform3D transform ) + public void viewerStateChanged( final ViewerStateChange change ) { - viewerTransform.set( transform ); - state.setViewerTransform( transform ); - for ( final TransformListener< AffineTransform3D > l : transformListeners ) - l.transformChanged( viewerTransform ); - requestRepaint(); - } - - @Override - public void visibilityChanged( final VisibilityAndGrouping.Event e ) - { - switch ( e.id ) + switch ( change ) { case CURRENT_SOURCE_CHANGED: - multiBoxOverlayRenderer.highlight( visibilityAndGrouping.getCurrentSource() ); + multiBoxOverlayRenderer.highlight( state().getSources().indexOf( state().getCurrentSource() ) ); display.repaint(); break; case DISPLAY_MODE_CHANGED: - showMessage( visibilityAndGrouping.getDisplayMode().getName() ); + showMessage( state().getDisplayMode().getName() ); display.repaint(); break; case GROUP_NAME_CHANGED: display.repaint(); break; - case SOURCE_ACTVITY_CHANGED: + case CURRENT_GROUP_CHANGED: + // TODO multiBoxOverlayRenderer.highlight() all sources in group that became current + break; + case SOURCE_ACTIVITY_CHANGED: // TODO multiBoxOverlayRenderer.highlight() all sources that became visible break; case GROUP_ACTIVITY_CHANGED: @@ -591,6 +577,50 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis case VISIBILITY_CHANGED: requestRepaint(); break; +// case SOURCE_TO_GROUP_ASSIGNMENT_CHANGED: +// case NUM_SOURCES_CHANGED: +// case NUM_GROUPS_CHANGED: + case INTERPOLATION_CHANGED: + final Interpolation interpolation = state().getInterpolation(); + showMessage( interpolation.getName() ); + for ( final InterpolationModeListener l : interpolationModeListeners ) + l.interpolationModeChanged( interpolation ); + requestRepaint(); + break; + case NUM_TIMEPOINTS_CHANGED: + { + final int numTimepoints = state().getNumTimepoints(); + final int timepoint = Math.max( 0, Math.min( state.getCurrentTimepoint(), numTimepoints - 1 ) ); + SwingUtilities.invokeLater( () -> { + final boolean sliderVisible = Arrays.asList( getComponents() ).contains( sliderTime ); + if ( numTimepoints > 1 && !sliderVisible ) + add( sliderTime, BorderLayout.SOUTH ); + else if ( numTimepoints == 1 && sliderVisible ) + remove( sliderTime ); + sliderTime.setModel( new DefaultBoundedRangeModel( timepoint, 0, 0, numTimepoints - 1 ) ); + revalidate(); + } ); + break; + } + case CURRENT_TIMEPOINT_CHANGED: + { + final int timepoint = state().getCurrentTimepoint(); + SwingUtilities.invokeLater( () -> { + blockSliderTimeEvents = true; + if ( sliderTime.getValue() != timepoint ) + sliderTime.setValue( timepoint ); + blockSliderTimeEvents = false; + } ); + for ( final TimePointListener l : timePointListeners ) + l.timePointChanged( timepoint ); + requestRepaint(); + break; + } + case VIEWER_TRANSFORM_CHANGED: + final AffineTransform3D transform = state().getViewerTransform(); + for ( final TransformListener< AffineTransform3D > l : transformListeners ) + l.transformChanged( transform ); + requestRepaint(); } } @@ -600,7 +630,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis * The planes which can be aligned with the viewer coordinate system: XY, * ZY, and XZ plane. */ - public static enum AlignPlane + public enum AlignPlane { XY( "XY", 2, new double[] { 1, 0, 0, 0 } ), ZY( "ZY", 0, new double[] { c, 0, -c, 0 } ), @@ -643,9 +673,9 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis */ protected synchronized void align( final AlignPlane plane ) { - final SourceState< ? > source = state.getSources().get( state.getCurrentSource() ); + final Source< ? > source = state().getCurrentSource().getSpimSource(); final AffineTransform3D sourceTransform = new AffineTransform3D(); - source.getSpimSource().getSourceTransform( state.getCurrentTimepoint(), 0, sourceTransform ); + source.getSourceTransform( state.getCurrentTimepoint(), 0, sourceTransform ); final double[] qSource = new double[ 4 ]; Affine3DHelpers.extractRotationAnisotropic( sourceTransform, qSource ); @@ -657,7 +687,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis final double[] qTarget = new double[ 4 ]; LinAlgHelpers.quaternionInvert( qTmpSource, qTarget ); - final AffineTransform3D transform = display.getTransformEventHandler().getTransform(); + final AffineTransform3D transform = state().getViewerTransform(); double centerX; double centerY; if ( mouseCoordinates.isMouseInsidePanel() ) @@ -672,7 +702,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis } currentAnimator = new RotationAnimator( transform, centerX, centerY, qTarget, 300 ); currentAnimator.setTime( System.currentTimeMillis() ); - transformChanged( transform ); + requestRepaint(); } public synchronized void setTransformAnimator( final AbstractTransformAnimator animator ) @@ -686,45 +716,37 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis * Switch to next interpolation mode. (Currently, there are two * interpolation modes: nearest-neighbor and N-linear.) */ + // TODO: Deprecate or leave as convenience? public synchronized void toggleInterpolation() { - final int i = state.getInterpolation().ordinal(); - final int n = Interpolation.values().length; - final Interpolation mode = Interpolation.values()[ ( i + 1 ) % n ]; - setInterpolation( mode ); + state().setInterpolation( state().getInterpolation().next() ); } /** * Set the {@link Interpolation} mode. */ + // TODO: Deprecate or leave as convenience? public synchronized void setInterpolation( final Interpolation mode ) { - final Interpolation interpolation = state.getInterpolation(); - if ( mode != interpolation ) - { - state.setInterpolation( mode ); - showMessage( mode.getName() ); - for ( final InterpolationModeListener l : interpolationModeListeners ) - l.interpolationModeChanged( state.getInterpolation() ); - requestRepaint(); - } + state().setInterpolation( mode ); } /** * Set the {@link DisplayMode}. */ + // TODO: Deprecate or leave as convenience? public synchronized void setDisplayMode( final DisplayMode displayMode ) { - visibilityAndGrouping.setDisplayMode( displayMode ); + state().setDisplayMode( displayMode ); } /** - * Set the viewer transform. + * @deprecated Modify {@link #state()} directly ({@code state().setViewerTransform(t)}) */ - public synchronized void setCurrentViewerTransform( final AffineTransform3D viewerTransform ) + @Deprecated + public void setCurrentViewerTransform( final AffineTransform3D viewerTransform ) { - display.getTransformEventHandler().setTransform( viewerTransform ); - transformChanged( viewerTransform ); + state().setViewerTransform( viewerTransform ); } /** @@ -733,34 +755,40 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis * @param timepoint * time-point index. */ + // TODO: Deprecate or leave as convenience? public synchronized void setTimepoint( final int timepoint ) { - if ( state.getCurrentTimepoint() != timepoint ) - { - state.setCurrentTimepoint( timepoint ); - sliderTime.setValue( timepoint ); - for ( final TimePointListener l : timePointListeners ) - l.timePointChanged( timepoint ); - requestRepaint(); - } + state().setCurrentTimepoint( timepoint ); } /** * Show the next time-point. */ + // TODO: Deprecate or leave as convenience? public synchronized void nextTimePoint() { - if ( state.getNumTimepoints() > 1 ) - sliderTime.setValue( sliderTime.getValue() + 1 ); + final SynchronizedViewerState state = state(); + synchronized ( state ) + { + final int t = state.getCurrentTimepoint() + 1; + if ( t < state.getNumTimepoints() ) + state.setCurrentTimepoint( t ); + } } /** * Show the previous time-point. */ + // TODO: Deprecate or leave as convenience? public synchronized void previousTimePoint() { - if ( state.getNumTimepoints() > 1 ) - sliderTime.setValue( sliderTime.getValue() - 1 ); + final SynchronizedViewerState state = state(); + synchronized ( state ) + { + final int t = state.getCurrentTimepoint() - 1; + if ( t >= 0 ) + state.setCurrentTimepoint( t ); + } } /** @@ -768,64 +796,55 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis * this will hide the time slider, otherwise show it. If the currently * displayed timepoint would be out of range with the new number of * timepoints, the current timepoint is set to {@code numTimepoints - 1}. + * <p> + * This is equivalent to {@code state().setNumTimepoints(numTimepoints}}. * * @param numTimepoints * number of available timepoints. Must be {@code >= 1}. */ public void setNumTimepoints( final int numTimepoints ) { - try - { - InvokeOnEDT.invokeAndWait( () -> setNumTimepointsSynchronized( numTimepoints ) ); - } - catch ( InvocationTargetException | InterruptedException e ) - { - e.printStackTrace(); - } - } - - private synchronized void setNumTimepointsSynchronized( final int numTimepoints ) - { - if ( numTimepoints < 1 || state.getNumTimepoints() == numTimepoints ) - return; - else if ( numTimepoints == 1 && state.getNumTimepoints() > 1 ) - remove( sliderTime ); - else if ( numTimepoints > 1 && state.getNumTimepoints() == 1 ) - add( sliderTime, BorderLayout.SOUTH ); - state.setNumTimepoints( numTimepoints ); - if ( state.getCurrentTimepoint() >= numTimepoints ) - { - final int timepoint = numTimepoints - 1; - state.setCurrentTimepoint( timepoint ); - for ( final TimePointListener l : timePointListeners ) - l.timePointChanged( timepoint ); - } - sliderTime.setModel( new DefaultBoundedRangeModel( state.getCurrentTimepoint(), 0, 0, numTimepoints - 1 ) ); - revalidate(); - requestRepaint(); } /** + * @deprecated Use {@link #state()} instead. + * * Get a copy of the current {@link ViewerState}. * * @return a copy of the current {@link ViewerState}. */ + @Deprecated public ViewerState getState() { return state.copy(); } + /** + * Get the ViewerState. This can be directly used for modifications, e.g., + * adding/removing sources etc. See {@link SynchronizedViewerState} for + * thread-safety considerations. + */ + public SynchronizedViewerState state() + { + return state.getState(); + } + /** * Get the viewer canvas. * * @return the viewer canvas. */ - public InteractiveDisplayCanvasComponent< AffineTransform3D > getDisplay() + public InteractiveDisplayCanvas getDisplay() { return display; } + public TransformEventHandler getTransformEventHandler() + { + return transformEventHandler; + } + /** * Display the specified message in a text overlay for a short time. * @@ -841,7 +860,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis /** * Add a new {@link OverlayAnimator} to the list of animators. The animation * is immediately started. The new {@link OverlayAnimator} will remain in - * the list of animators until it {@link OverlayAnimator#isComplete()}.å + * the list of animators until it {@link OverlayAnimator#isComplete()}. * * @param animator * animator to add. @@ -877,37 +896,34 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis } /** - * Add a {@link TransformListener} to notify about viewer transformation + * Add/remove {@code TransformListener}s to notify about viewer transformation * changes. Listeners will be notified when a new image has been painted * with the viewer transform used to render that image. - * + * <p> * This happens immediately after that image is painted onto the screen, * before any overlays are painted. - * - * @param listener - * the transform listener to add. */ + public Listeners< TransformListener< AffineTransform3D > > renderTransformListeners() + { + return renderTarget.transformListeners(); + } + + /** + * @deprecated Use {@code renderTransformListeners().add( listener )}. + */ + @Deprecated public void addRenderTransformListener( final TransformListener< AffineTransform3D > listener ) { - renderTarget.addTransformListener( listener ); + renderTransformListeners().add( listener ); } /** - * Add a {@link TransformListener} to notify about viewer transformation - * changes. Listeners will be notified when a new image has been painted - * with the viewer transform used to render that image. - * - * This happens immediately after that image is painted onto the screen, - * before any overlays are painted. - * - * @param listener - * the transform listener to add. - * @param index - * position in the list of listeners at which to insert this one. + * @deprecated Use {@code renderTransformListeners().add( index, listener )}. */ + @Deprecated public void addRenderTransformListener( final TransformListener< AffineTransform3D > listener, final int index ) { - renderTarget.addTransformListener( listener, index ); + renderTransformListeners().add( index, listener ); } /** @@ -939,7 +955,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis { final int s = transformListeners.size(); transformListeners.add( index < 0 ? 0 : index > s ? s : index, listener ); - listener.transformChanged( viewerTransform ); + listener.transformChanged( state().getViewerTransform() ); } } @@ -955,7 +971,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis { transformListeners.remove( listener ); } - renderTarget.removeTransformListener( listener ); + renderTarget.transformListeners().remove( listener ); } /** @@ -1093,10 +1109,13 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis {} /** + * @deprecated Modify {@link #state()} directly + * * Returns the {@link VisibilityAndGrouping} that can be used to modify * visibility and currentness of sources and groups, as well as grouping of * sources, and display mode. */ + @Deprecated public VisibilityAndGrouping getVisibilityAndGrouping() { return visibilityAndGrouping; @@ -1129,18 +1148,26 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis renderingExecutorService.shutdown(); state.kill(); imageRenderer.kill(); + renderTarget.kill(); } protected static final AtomicInteger panelNumber = new AtomicInteger( 1 ); - protected class RenderThreadFactory implements ThreadFactory + protected static class RenderThreadFactory implements ThreadFactory { + private final ThreadGroup threadGroup; + private final String threadNameFormat = String.format( "bdv-panel-%d-thread-%%d", panelNumber.getAndIncrement() ); private final AtomicInteger threadNumber = new AtomicInteger( 1 ); + protected RenderThreadFactory( final ThreadGroup threadGroup ) + { + this.threadGroup = threadGroup; + } + @Override public Thread newThread( final Runnable r ) { @@ -1155,9 +1182,9 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis } } - public TransformAwareBufferedImageOverlayRenderer renderTarget() + @Override + public boolean requestFocusInWindow() { - return this.renderTarget; + return display.requestFocusInWindow(); } - } diff --git a/src/main/java/bdv/viewer/ViewerState.java b/src/main/java/bdv/viewer/ViewerState.java new file mode 100644 index 0000000000000000000000000000000000000000..c8766a3410ed75be10d1fef48b4b3ae30c62cef9 --- /dev/null +++ b/src/main/java/bdv/viewer/ViewerState.java @@ -0,0 +1,790 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import net.imglib2.realtransform.AffineTransform3D; +import org.scijava.listeners.Listeners; + +/** + * Reading and writing the BigDataViewer state: + * <ul> + * <li>interpolation and display mode</li> + * <li>contained sources and source groups</li> + * <li>activeness / currentness of sources and groups</li> + * <li>current timepoint and transformation</li> + * </ul> + * + * @author Tobias Pietzsch + */ +public interface ViewerState +{ + /** + * Get a snapshot of this ViewerState. + * + * @return unmodifiable copy of the current state + */ + ViewerState snapshot(); + + /** + * {@code ViewerStateChangeListener}s can be added/removed here, + * and will be notified about changes to this ViewerState. + */ + Listeners< ViewerStateChangeListener > changeListeners(); + + /** + * Get the interpolation method. + * + * @return interpolation method + */ + Interpolation getInterpolation(); + + /** + * Set the interpolation method (optional operation). + * + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + void setInterpolation( Interpolation interpolation ); + + /** + * Get the current {@code DisplayMode}. + * <ul> + * <li>In {@code DisplayMode.SINGLE} only the current source is visible.</li> + * <li>In {@code DisplayMode.GROUP} the sources in the current group are visible.</li> + * <li>In {@code DisplayMode.FUSED} all active sources are visible.</li> + * <li>In {@code DisplayMode.FUSEDROUP} the sources in all active groups are visible.</li> + * </ul> + * + * @return the current display mode + */ + DisplayMode getDisplayMode(); + + /** + * Set the {@link DisplayMode} (optional operation). + * + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + void setDisplayMode( DisplayMode mode ); + + /** + * Get the number of timepoints. + * + * @return the number of timepoints + */ + int getNumTimepoints(); + + /** + * Set the number of timepoints (optional operation). + * <p> + * If {@link #getCurrentTimepoint()} current timepoint} is + * {@code >= n}, it will be adjusted to {@code n-1}. + * + * @throws IllegalArgumentException + * if {@code n < 1}. + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + void setNumTimepoints( int n ); + + /** + * Get the current timepoint. + * + * @return current timepoint (index) + */ + int getCurrentTimepoint(); + + /** + * Set the current timepoint (optional operation). + * + * @throws IllegalArgumentException + * if {@code t >= getNumTimepoints()} or {@code t < 0}. + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + void setCurrentTimepoint( int t ); + + /** + * Get the viewer transform. + * + * @param transform + * is set to the viewer transform + */ + void getViewerTransform( AffineTransform3D transform ); + + /** + * Get the viewer transform. + * + * @return a copy of the current viewer transform + */ + default AffineTransform3D getViewerTransform() + { + final AffineTransform3D transform = new AffineTransform3D(); + getViewerTransform( transform ); + return transform; + } + + /** + * Set the viewer transform (optional operation). + * + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + void setViewerTransform( AffineTransform3D transform ); + + // -------------------- + // -- sources -- + // -------------------- + + /** + * Get the list of sources. The returned {@code List} reflects changes to + * the viewer state. It is unmodifiable and not thread-safe. + * + * @return the list of sources + */ + List< SourceAndConverter< ? > > getSources(); + + /** + * Get the current source. (May return {@code null} if there is no current + * source) + * + * @return the current source + */ + SourceAndConverter< ? > getCurrentSource(); + + /** + * Returns {@code true} if {@code source} is the current source. Equivalent + * to {@code (getCurrentSource() == source)}. + * + * @param source + * the source. Passing {@code null} checks whether no source is current. + * @return {@code true} if {@code source} is the current source + */ + boolean isCurrentSource( SourceAndConverter< ? > source ); + + /** + * Make {@code source} the current source (optional operation). Returns + * {@code true}, if current source changes as a result of the call. Returns + * {@code false}, if {@code source} is already the current source. + * + * @param source + * the source to make current. Passing {@code null} clears the current + * source. + * + * @return {@code true}, if current source changed as a result of the call + * + * @throws IllegalArgumentException + * if {@code source} is not contained in the state (and not + * {@code null}). + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + boolean setCurrentSource( SourceAndConverter< ? > source ); + + /** + * Get the set of active sources. The returned {@code Set} reflects changes + * to the viewer state. It is unmodifiable and not thread-safe. + * + * @return the set of active sources + */ + Set< SourceAndConverter< ? > > getActiveSources(); + + /** + * Check whether the given {@code source} is active. + * + * @return {@code true}, if {@code source} is active + * + * @throws NullPointerException + * if {@code source == null} + * @throws IllegalArgumentException + * if {@code source} is not contained in the state (and not + * {@code null}). + */ + boolean isSourceActive( SourceAndConverter< ? > source ); + + /** + * Set {@code source} active or inactive (optional operation). + * <p> + * Returns {@code true}, if source activity changes as a result of the call. + * Returns {@code false}, if {@code source} is already in the desired + * {@code active} state. + * + * @return {@code true}, if source activity changed as a result of the call + * + * @throws NullPointerException + * if {@code source == null} + * @throws IllegalArgumentException + * if {@code source} is not contained in the state (and not + * {@code null}). + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + boolean setSourceActive( SourceAndConverter< ? > source, boolean active ); + + /** + * Set all sources in {@code collection} active or inactive (optional + * operation). + * <p> + * Returns {@code true}, if source activity changes as a result of the call. + * Returns {@code false}, if all sources were already in the desired + * {@code active} state. + * + * @return {@code true}, if source activity changed as a result of the call + * + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + * @throws IllegalArgumentException + * if any element of {@code collection} is not contained in the state. + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + boolean setSourcesActive( Collection< ? extends SourceAndConverter< ? > > collection, boolean active ); + + /** + * Check whether the given {@code source} is visible. + * <p> + * Whether a source is visible depends on the {@link #getDisplayMode() + * display mode}: + * <ul> + * <li>In {@code DisplayMode.SINGLE} only the current source is visible.</li> + * <li>In {@code DisplayMode.GROUP} the sources in the current group are visible.</li> + * <li>In {@code DisplayMode.FUSED} all active sources are visible.</li> + * <li>In {@code DisplayMode.FUSEDROUP} the sources in all active groups are visible.</li> + * </ul> + * + * @return {@code true}, if {@code source} is visible + * + * @throws NullPointerException + * if {@code source == null} + * @throws IllegalArgumentException + * if {@code source} is not contained in the state (and not + * {@code null}). + */ + boolean isSourceVisible( SourceAndConverter< ? > source ); + + /** + * Check whether the given {@code source} is both visible and provides image + * data for the current timepoint. + * <p> + * Whether a source is visible depends on the {@link #getDisplayMode() + * display mode}: + * <ul> + * <li>In {@code DisplayMode.SINGLE} only the current source is visible.</li> + * <li>In {@code DisplayMode.GROUP} the sources in the current group are visible.</li> + * <li>In {@code DisplayMode.FUSED} all active sources are visible.</li> + * <li>In {@code DisplayMode.FUSEDROUP} the sources in all active groups are visible.</li> + * </ul> + * Additionally, the source must be {@link bdv.viewer.Source#isPresent(int) + * present}, i.e., provide image data for the {@link #getCurrentTimepoint() + * current timepoint}. + * + * @return {@code true}, if {@code source} is both visible and present + * + * @throws NullPointerException + * if {@code source == null} + * @throws IllegalArgumentException + * if {@code source} is not contained in the state (and not + * {@code null}). + */ + boolean isSourceVisibleAndPresent( SourceAndConverter< ? > source ); + + /** + * Get the set of visible sources. + * <p> + * The returned {@code Set} is a copy. Changes to the set will not be + * reflected in the viewer state, and vice versa. + * <p> + * Whether a source is visible depends on the {@link #getDisplayMode() + * display mode}: + * <ul> + * <li>In {@code DisplayMode.SINGLE} only the current source is visible.</li> + * <li>In {@code DisplayMode.GROUP} the sources in the current group are visible.</li> + * <li>In {@code DisplayMode.FUSED} all active sources are visible.</li> + * <li>In {@code DisplayMode.FUSEDROUP} the sources in all active groups are visible.</li> + * </ul> + * + * @return the set of visible sources + */ + Set< SourceAndConverter< ? > > getVisibleSources(); + + /** + * Get the set of visible sources that also provide image data for the + * current timepoint. + * <p> + * The returned {@code Set} is a copy. Changes to the set will not be + * reflected in the viewer state, and vice versa. + * <p> + * Whether a source is visible depends on the {@link #getDisplayMode() + * display mode}: + * <ul> + * <li>In {@code DisplayMode.SINGLE} only the current source is visible.</li> + * <li>In {@code DisplayMode.GROUP} the sources in the current group are visible.</li> + * <li>In {@code DisplayMode.FUSED} all active sources are visible.</li> + * <li>In {@code DisplayMode.FUSEDROUP} the sources in all active groups are visible.</li> + * </ul> + * Additionally, the source must be {@link bdv.viewer.Source#isPresent(int) + * present}, i.e., provide image data for the {@link #getCurrentTimepoint() + * current timepoint}. + * + * @return the set of sources that are both visible and present + */ + Set< SourceAndConverter< ? > > getVisibleAndPresentSources(); + + /** + * Check whether the state contains the {@code source}. + * + * @return {@code true}, if {@code source} is in the list of sources. + * + * @throws NullPointerException + * if {@code source == null} + */ + boolean containsSource( SourceAndConverter< ? > source ); + + /** + * Add {@code source} to the state (optional operation). Returns + * {@code true}, if the source is added. Returns {@code false}, if the + * source is already present. + * <p> + * If {@code source} is added and no other source was current, then + * {@code source} is made current + * + * @return {@code true}, if list of sources changed as a result of the call. + * + * @throws NullPointerException + * if {@code source == null} + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + boolean addSource( SourceAndConverter< ? > source ); + + /** + * Add all sources in {@code collection} to the state (optional operation). + * Returns {@code true}, if at least one source was added. Returns + * {@code false}, if all sources were already present. + * <p> + * If any sources are added and no other source was current, then the first + * added sources will be made current. + * + * @return {@code true}, if list of sources changed as a result of the call. + * + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + boolean addSources( Collection< ? extends SourceAndConverter< ? > > collection ); + + /** + * Remove {@code source} from the state (optional operation). + * <p> + * Returns {@code true}, if {@code source} was removed from the state. + * Returns {@code false}, if {@code source} was not contained in state. + * <p> + * The {@code source} is also removed from any groups that contained it. If + * {@code source} was current, then the first source in the list of sources + * is made current (if it exists). + * + * @return {@code true}, if list of sources changed as a result of the call + * + * @throws NullPointerException + * if {@code source == null} + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + boolean removeSource( SourceAndConverter< ? > source ); + + /** + * Remove all sources in {@code collection} from the state (optional + * operation). Returns {@code true}, if at least one source was removed. + * Returns {@code false}, if none of the sources was present. + * <p> + * Removed sources are also removed from any groups containing them. If the + * current source was removed, then the first source in the remaining list + * of sources is made current (if it exists). + * + * @return {@code true}, if list of sources changed as a result of the call. + * + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + boolean removeSources( Collection< ? extends SourceAndConverter< ? > > collection ); + + // TODO addSource with index + // TODO addSources with index + + /** + * Remove all sources from the state (optional operation). + * + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + void clearSources(); + + /** + * Returns a {@link Comparator} that compares sources according to the order + * in which they occur in the sources list. (Sources that do not occur in + * the list are ordered before any source in the list). + */ + Comparator< SourceAndConverter< ? > > sourceOrder(); + + // -------------------- + // -- groups -- + // -------------------- + + /** + * Get the list of groups. The returned {@code List} reflects changes to the + * viewer state. It is unmodifiable and not thread-safe. + * + * @return the list of groups + */ + List< SourceGroup > getGroups(); + + /** + * Get the current group. (May return {@code null} if there is no current + * group) + * + * @return the current group + */ + SourceGroup getCurrentGroup(); + + /** + * Returns {@code true} if {@code group} is the current group. Equivalent to + * {@code (getCurrentGroup() == group)}. + * + * @return {@code true} if {@code group} is the current group + */ + boolean isCurrentGroup( SourceGroup group ); + + /** + * Make {@code group} the current group (optional operation). Returns + * {@code true}, if current group changes as a result of the call. Returns + * {@code false}, if {@code group} is already the current group. + * + * @param group + * the group to make current + * @return {@code true}, if current group changed as a result of the call. + * + * @throws IllegalArgumentException + * if {@code group} is not contained in the state (and not + * {@code null}). + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + boolean setCurrentGroup( SourceGroup group ); + + /** + * Get the set of active groups. The returned {@code Set} reflects changes + * to the viewer state. It is unmodifiable and not thread-safe. + * + * @return the set of active groups + */ + Set< SourceGroup > getActiveGroups(); + + /** + * Check whether the given {@code group} is active. + * + * @return {@code true}, if {@code group} is active + * + * @throws NullPointerException + * if {@code group == null} + * @throws IllegalArgumentException + * if {@code group} is not contained in the state (and not + * {@code null}). + */ + boolean isGroupActive( SourceGroup group ); + + /** + * Set {@code group} active or inactive (optional operation). + * <p> + * Returns {@code true}, if group activity changes as a result of the call. + * Returns {@code false}, if {@code group} is already in the desired + * {@code active} state. + * + * @return {@code true}, if group activity changed as a result of the call + * + * @throws NullPointerException + * if {@code group == null} + * @throws IllegalArgumentException + * if {@code group} is not contained in the state (and not + * {@code null}). + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + boolean setGroupActive( SourceGroup group, boolean active ); + + /** + * Set all groups in {@code collection} active or inactive (optional + * operation). + * <p> + * Returns {@code true}, if group activity changes as a result of the call. + * Returns {@code false}, if all groups were already in the desired + * {@code active} state. + * + * @return {@code true}, if group activity changed as a result of the call + * + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + * @throws IllegalArgumentException + * if any element of {@code collection} is not contained in the state. + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + boolean setGroupsActive( Collection< ? extends SourceGroup > collection, boolean active ); + + /** + * Get the name of a {@code group}. + * + * @return name of the group, may be {@code null} + * + * @throws NullPointerException + * if {@code group == null} + * @throws IllegalArgumentException + * if {@code group} is not contained in the state (and not + * {@code null}). + */ + String getGroupName( final SourceGroup group ); + + /** + * Set the {@code name} of a {@code group} (optional operation). + * + * @throws NullPointerException + * if {@code group == null} + * @throws IllegalArgumentException + * if {@code group} is not contained in the state (and not + * {@code null}). + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + void setGroupName( SourceGroup group, String name ); + + /** + * Check whether the state contains the {@code group}. + * + * @return {@code true}, if {@code group} is in the list of groups. + * + * @throws NullPointerException + * if {@code group == null} + */ + boolean containsGroup( SourceGroup group ); + + /** + * Add {@code group} to the state (optional operation). Returns + * {@code true}, if the group is added. Returns {@code false}, if the group + * is already present. + * <p> + * If {@code group} is added and no other group was current, then + * {@code group} is made current + * + * @return {@code true}, if list of groups changed as a result of the call. + * + * @throws NullPointerException + * if {@code group == null} + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + boolean addGroup( SourceGroup group ); + + /** + * Add all groups in {@code collection} to the state (optional operation). + * Returns {@code true}, if at least one group was added. Returns + * {@code false}, if all groups were already present. + * <p> + * If any groups are added and no other group was current, then the first + * added groups will be made current. + * + * @return {@code true}, if list of groups changed as a result of the call. + * + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + boolean addGroups( Collection< ? extends SourceGroup > collection ); + + /** + * Remove {@code group} from the state (optional operation). + * <p> + * Returns {@code true}, if {@code group} was removed from the state. + * Returns {@code false}, if {@code group} was not contained in state. + * <p> + * If {@code group} was current, then the first group in the list of groups + * is made current (if it exists). + * + * @return {@code true}, if list of groups changed as a result of the call + * + * @throws NullPointerException + * if {@code group == null} + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + boolean removeGroup( SourceGroup group ); + + /** + * Remove all groups in {@code collection} from the state (optional + * operation). Returns {@code true}, if at least one group was removed. + * Returns {@code false}, if none of the groups was present. + * <p> + * If the current group was removed, then the first group in the remaining + * list of groups is made current (if it exists). + * + * @return {@code true}, if list of groups changed as a result of the call. + * + * @throws NullPointerException + * if {@code collection == null} or any element of {@code collection} is + * {@code null}. + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + boolean removeGroups( Collection< ? extends SourceGroup > collection ); + + /** + * Add {@code source} to {@code group} (optional operation). + * <p> + * Returns {@code true}, if {@code source} was added to {@code group}. + * Returns {@code false}, if {@code source} was already contained in + * {@code group}. or either of {@code source} and {@code group} is not valid + * (not in the BDV sources/groups list). + * + * @return {@code true}, if set of sources in {@code group} changed as a + * result of the call + * + * @throws NullPointerException + * if {@code source == null} or {@code group == null} + * @throws IllegalArgumentException + * if either of {@code source} and {@code group} is not contained in the + * state (and not {@code null}). + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + boolean addSourceToGroup( SourceAndConverter< ? > source, SourceGroup group ); + + /** + * Add all sources in {@code collection} to {@code group} (optional + * operation). + * <p> + * Returns {@code true}, if at least one source was added to {@code group}. + * Returns {@code false}, if all sources were already contained in + * {@code group}. + * + * @return {@code true}, if set of sources in {@code group} changed as a + * result of the call + * + * @throws NullPointerException + * if {@code group == null} or {@code collection == null} or any element + * of {@code collection} is {@code null}. + * @throws IllegalArgumentException + * if {@code group} or any element of {@code collection} is is not + * contained in the state (and not {@code null}). + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + boolean addSourcesToGroup( Collection< ? extends SourceAndConverter< ? > > collection, SourceGroup group ); + + /** + * Remove {@code source} from {@code group} (optional operation). + * <p> + * Returns {@code true}, if {@code source} was removed from {@code group}. + * Returns {@code false}, if {@code source} was not contained in + * {@code group}, + * + * @return {@code true}, if set of sources in {@code group} changed as a + * result of the call + * + * @throws NullPointerException + * if {@code source == null} or {@code group == null} + * @throws IllegalArgumentException + * if either of {@code source} and {@code group} is not contained in the + * state (and not {@code null}). + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + boolean removeSourceFromGroup( SourceAndConverter< ? > source, SourceGroup group ); + + /** + * Remove all sources in {@code collection} from {@code group} (optional + * operation). + * <p> + * Returns {@code true}, if at least one source was removed from + * {@code group}. Returns {@code false}, if none of the sources were + * contained in {@code group}. + * + * @return {@code true}, if set of sources in {@code group} changed as a + * result of the call + * + * @throws NullPointerException + * if {@code group == null} or {@code collection == null} or any element + * of {@code collection} is {@code null}. + * @throws IllegalArgumentException + * if {@code group} or any element of {@code collection} is is not + * contained in the state (and not {@code null}). + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + boolean removeSourcesFromGroup( Collection< ? extends SourceAndConverter< ? > > collection, SourceGroup group ); + + /** + * Get the set sources in {@code group}. The returned {@code Set} reflects + * changes to the viewer state. It is unmodifiable and not thread-safe. + * + * @return the set of sources in {@code group} + * + * @throws NullPointerException + * if {@code group == null} + * @throws IllegalArgumentException + * if {@code group} is not contained in the state (and not + * {@code null}). + */ + Set< SourceAndConverter< ? > > getSourcesInGroup( SourceGroup group ); + + /** + * Remove all groups from the state (optional operation). + * + * @throws UnsupportedOperationException + * if the operation is not supported by this ViewerState + */ + void clearGroups(); + + /** + * Returns a {@link Comparator} that compares groups according to the order + * in which they occur in the groups list. (Groups that do not occur in the + * list are ordered before any group in the list). + */ + Comparator< SourceGroup > groupOrder(); +} diff --git a/src/main/java/bdv/viewer/ViewerStateChange.java b/src/main/java/bdv/viewer/ViewerStateChange.java new file mode 100644 index 0000000000000000000000000000000000000000..dd1f7def1d573768ab9cf99f03513098bb4c7c2a --- /dev/null +++ b/src/main/java/bdv/viewer/ViewerStateChange.java @@ -0,0 +1,53 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer; + +/** + * Types of BigDataViewer state changes that {@link ViewerStateChangeListener}s + * can be notified about. + * + * @author Tobias Pietzsch + */ +public enum ViewerStateChange +{ + CURRENT_SOURCE_CHANGED, + CURRENT_GROUP_CHANGED, + SOURCE_ACTIVITY_CHANGED, + GROUP_ACTIVITY_CHANGED, + SOURCE_TO_GROUP_ASSIGNMENT_CHANGED, + GROUP_NAME_CHANGED, + NUM_SOURCES_CHANGED, + NUM_GROUPS_CHANGED, + VISIBILITY_CHANGED, + DISPLAY_MODE_CHANGED, + INTERPOLATION_CHANGED, + NUM_TIMEPOINTS_CHANGED, + CURRENT_TIMEPOINT_CHANGED, + VIEWER_TRANSFORM_CHANGED; +} diff --git a/src/main/java/bdv/BehaviourTransformEventHandlerFactory.java b/src/main/java/bdv/viewer/ViewerStateChangeListener.java similarity index 72% rename from src/main/java/bdv/BehaviourTransformEventHandlerFactory.java rename to src/main/java/bdv/viewer/ViewerStateChangeListener.java index 762d835bca7dc5a783a17d35efe9ba15743bb110..c4db593af6d975c796b096f78ee6a9849727a1d6 100644 --- a/src/main/java/bdv/BehaviourTransformEventHandlerFactory.java +++ b/src/main/java/bdv/viewer/ViewerStateChangeListener.java @@ -1,9 +1,8 @@ /*- * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -27,13 +26,15 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package bdv; +package bdv.viewer; -import org.scijava.ui.behaviour.io.InputTriggerConfig; - -import net.imglib2.ui.TransformEventHandlerFactory; - -public interface BehaviourTransformEventHandlerFactory< A > extends TransformEventHandlerFactory< A > +/** + * {@link ViewerStateChangeListener}s are notified about BigDataViewer state + * changes. + * + * @author Tobias Pietzsch + */ +public interface ViewerStateChangeListener { - public void setConfig( final InputTriggerConfig config ); + void viewerStateChanged( ViewerStateChange change ); } diff --git a/src/main/java/bdv/viewer/VisibilityAndGrouping.java b/src/main/java/bdv/viewer/VisibilityAndGrouping.java index 18a01a5b290a690bd7dd85579f4cf55beafbc9ee..894f2b9beeda4011a9fef2470b3c78e3fbfbb5e4 100644 --- a/src/main/java/bdv/viewer/VisibilityAndGrouping.java +++ b/src/main/java/bdv/viewer/VisibilityAndGrouping.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,36 +28,30 @@ */ package bdv.viewer; +import bdv.viewer.state.SourceGroup; +import bdv.viewer.state.SourceState; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + import static bdv.viewer.DisplayMode.FUSED; import static bdv.viewer.DisplayMode.FUSEDGROUP; import static bdv.viewer.DisplayMode.GROUP; import static bdv.viewer.DisplayMode.SINGLE; -import static bdv.viewer.VisibilityAndGrouping.Event.CURRENT_GROUP_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.CURRENT_SOURCE_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.DISPLAY_MODE_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.GROUP_ACTIVITY_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.GROUP_NAME_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.SOURCE_ACTVITY_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.SOURCE_TO_GROUP_ASSIGNMENT_CHANGED; -import static bdv.viewer.VisibilityAndGrouping.Event.VISIBILITY_CHANGED; - -import java.util.Arrays; -import java.util.List; -import java.util.SortedSet; -import java.util.concurrent.CopyOnWriteArrayList; - -import bdv.viewer.state.SourceGroup; -import bdv.viewer.state.SourceState; -import bdv.viewer.state.ViewerState; /** + * @deprecated This is not necessary anymore, because {@link ViewerState} can be modified directly. + * (See {@link ViewerPanel#state()}.) + * * Manage visibility and currentness of sources and groups, as well as grouping * of sources, and display mode. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ +@Deprecated public class VisibilityAndGrouping { + @Deprecated public static final class Event { public static final int CURRENT_SOURCE_CHANGED = 0; @@ -92,56 +85,108 @@ public class VisibilityAndGrouping } } + @Deprecated public interface UpdateListener { - public void visibilityChanged( Event e ); + void visibilityChanged( Event e ); } protected final CopyOnWriteArrayList< UpdateListener > updateListeners; - protected final ViewerState state; + private final bdv.viewer.state.ViewerState deprecatedViewerState; + + private final ViewerState state; - public VisibilityAndGrouping( final ViewerState viewerState ) + @Deprecated + public ViewerState getState() + { + return state; + } + + @Deprecated + public VisibilityAndGrouping( final bdv.viewer.state.ViewerState viewerState ) { updateListeners = new CopyOnWriteArrayList<>(); - state = viewerState; + deprecatedViewerState = viewerState; + state = viewerState.getState(); + viewerState.getState().changeListeners().add( e -> + { + switch ( e ) + { + case CURRENT_SOURCE_CHANGED: + update( Event.CURRENT_SOURCE_CHANGED ); + break; + case CURRENT_GROUP_CHANGED: + update( Event.CURRENT_GROUP_CHANGED ); + break; + case SOURCE_ACTIVITY_CHANGED: + update( Event.SOURCE_ACTVITY_CHANGED ); + break; + case GROUP_ACTIVITY_CHANGED: + update( Event.GROUP_ACTIVITY_CHANGED ); + break; + case SOURCE_TO_GROUP_ASSIGNMENT_CHANGED: + update( Event.SOURCE_TO_GROUP_ASSIGNMENT_CHANGED ); + break; + case GROUP_NAME_CHANGED: + update( Event.GROUP_NAME_CHANGED ); + break; + case NUM_SOURCES_CHANGED: + update( Event.NUM_SOURCES_CHANGED ); + break; + case NUM_GROUPS_CHANGED: + update( Event.NUM_GROUPS_CHANGED ); + break; + case VISIBILITY_CHANGED: + update( Event.VISIBILITY_CHANGED ); + break; + case DISPLAY_MODE_CHANGED: + update( Event.DISPLAY_MODE_CHANGED ); + break; + } + } ); } + @Deprecated public int numSources() { - return state.numSources(); + return state.getSources().size(); } + @Deprecated public List< SourceState< ? > > getSources() { - return state.getSources(); + return deprecatedViewerState.getSources(); } + @Deprecated public int numGroups() { - return state.numSourceGroups(); + return state.getGroups().size(); } + @Deprecated public List< SourceGroup > getSourceGroups() { - return state.getSourceGroups(); + return deprecatedViewerState.getSourceGroups(); } + @Deprecated public synchronized DisplayMode getDisplayMode() { return state.getDisplayMode(); } + @Deprecated public synchronized void setDisplayMode( final DisplayMode displayMode ) { state.setDisplayMode( displayMode ); - checkVisibilityChange(); - update( DISPLAY_MODE_CHANGED ); } + @Deprecated public synchronized int getCurrentSource() { - return state.getCurrentSource(); + return state.getSources().indexOf( state.getCurrentSource() ); } /** @@ -149,29 +194,28 @@ public class VisibilityAndGrouping * * @param sourceIndex */ + @Deprecated public synchronized void setCurrentSource( final int sourceIndex ) { if ( sourceIndex < 0 || sourceIndex >= numSources() ) return; - state.setCurrentSource( sourceIndex ); - checkVisibilityChange(); - update( CURRENT_SOURCE_CHANGED ); + state.setCurrentSource( state.getSources().get( sourceIndex ) ); }; + @Deprecated public synchronized void setCurrentSource( final Source< ? > source ) { - state.setCurrentSource( source ); - checkVisibilityChange(); - update( CURRENT_SOURCE_CHANGED ); + state.setCurrentSource( soc( source ) ); }; + @Deprecated public synchronized boolean isSourceActive( final int sourceIndex ) { if ( sourceIndex < 0 || sourceIndex >= numSources() ) return false; - return state.getSources().get( sourceIndex ).isActive(); + return state.isSourceActive( state.getSources().get( sourceIndex ) ); } /** @@ -180,14 +224,13 @@ public class VisibilityAndGrouping * @param sourceIndex * @param isActive */ + @Deprecated public synchronized void setSourceActive( final int sourceIndex, final boolean isActive ) { if ( sourceIndex < 0 || sourceIndex >= numSources() ) return; - state.getSources().get( sourceIndex ).setActive( isActive ); - update( SOURCE_ACTVITY_CHANGED ); - checkVisibilityChange(); + state.setSourceActive( state.getSources().get( sourceIndex ), isActive ); } /** @@ -196,20 +239,16 @@ public class VisibilityAndGrouping * @param source * @param isActive */ + @Deprecated public synchronized void setSourceActive( final Source< ? > source, final boolean isActive ) { - for ( final SourceState< ? > s : state.getSources() ) - { - if ( s.getSpimSource().equals( source ) ) - s.setActive( isActive ); - } - update( SOURCE_ACTVITY_CHANGED ); - checkVisibilityChange(); + state.setSourceActive( soc( source ), isActive ); } + @Deprecated public synchronized int getCurrentGroup() { - return state.getCurrentGroup(); + return state.getGroups().indexOf( state.getCurrentGroup() ); } /** @@ -217,28 +256,29 @@ public class VisibilityAndGrouping * * @param groupIndex */ + @Deprecated public synchronized void setCurrentGroup( final int groupIndex ) { if ( groupIndex < 0 || groupIndex >= numGroups() ) return; - state.setCurrentGroup( groupIndex ); - checkVisibilityChange(); - update( CURRENT_GROUP_CHANGED ); - final SortedSet< Integer > ids = state.getSourceGroups().get( groupIndex ).getSourceIds(); - if ( !ids.isEmpty() ) + final bdv.viewer.SourceGroup group = state.getGroups().get( groupIndex ); + state.setCurrentGroup( group ); + final List< SourceAndConverter< ? > > sources = new ArrayList<>( state.getSourcesInGroup( group ) ); + if ( ! sources.isEmpty() ) { - state.setCurrentSource( ids.first() ); - update( CURRENT_SOURCE_CHANGED ); + sources.sort( state.sourceOrder() ); + state.setCurrentSource( sources.get( 0 ) ); } } + @Deprecated public synchronized boolean isGroupActive( final int groupIndex ) { if ( groupIndex < 0 || groupIndex >= numGroups() ) return false; - return state.getSourceGroups().get( groupIndex ).isActive(); + return state.isGroupActive( state.getGroups().get( groupIndex ) ); } /** @@ -247,49 +287,47 @@ public class VisibilityAndGrouping * @param groupIndex * @param isActive */ + @Deprecated public synchronized void setGroupActive( final int groupIndex, final boolean isActive ) { if ( groupIndex < 0 || groupIndex >= numGroups() ) return; - state.getSourceGroups().get( groupIndex ).setActive( isActive ); - update( GROUP_ACTIVITY_CHANGED ); - checkVisibilityChange(); + state.setGroupActive( state.getGroups().get( groupIndex ), isActive ); } + @Deprecated public synchronized void setGroupName( final int groupIndex, final String name ) { if ( groupIndex < 0 || groupIndex >= numGroups() ) return; - state.getSourceGroups().get( groupIndex ).setName( name ); - update( GROUP_NAME_CHANGED ); + state.setGroupName( state.getGroups().get( groupIndex ), name ); } + @Deprecated public synchronized void addSourceToGroup( final int sourceIndex, final int groupIndex ) { if ( groupIndex < 0 || groupIndex >= numGroups() ) return; - state.getSourceGroups().get( groupIndex ).addSource( sourceIndex ); - update( SOURCE_TO_GROUP_ASSIGNMENT_CHANGED ); - checkVisibilityChange(); + state.addSourceToGroup( state.getSources().get( sourceIndex ), state.getGroups().get( groupIndex ) ); } + @Deprecated public synchronized void removeSourceFromGroup( final int sourceIndex, final int groupIndex ) { if ( groupIndex < 0 || groupIndex >= numGroups() ) return; - state.getSourceGroups().get( groupIndex ).removeSource( sourceIndex ); - update( SOURCE_TO_GROUP_ASSIGNMENT_CHANGED ); - checkVisibilityChange(); + state.removeSourceFromGroup( state.getSources().get( sourceIndex ), state.getGroups().get( groupIndex ) ); } /** * TODO * @param index */ + @Deprecated public synchronized void setCurrentGroupOrSource( final int index ) { if ( isGroupingEnabled() ) @@ -302,6 +340,7 @@ public class VisibilityAndGrouping * TODO * @param index */ + @Deprecated public synchronized void toggleActiveGroupOrSource( final int index ) { if ( isGroupingEnabled() ) @@ -310,64 +349,39 @@ public class VisibilityAndGrouping setSourceActive( index, !isSourceActive( index ) ); } + @Deprecated public synchronized boolean isGroupingEnabled() { final DisplayMode mode = state.getDisplayMode(); return ( mode == GROUP ) || ( mode == FUSEDGROUP ); } + @Deprecated public synchronized boolean isFusedEnabled() { final DisplayMode mode = state.getDisplayMode(); return ( mode == FUSED ) || ( mode == FUSEDGROUP ); } + @Deprecated public synchronized void setGroupingEnabled( final boolean enable ) { setDisplayMode( isFusedEnabled() ? ( enable ? FUSEDGROUP : FUSED ) : ( enable ? GROUP : SINGLE ) ); } + @Deprecated public synchronized void setFusedEnabled( final boolean enable ) { setDisplayMode( isGroupingEnabled() ? ( enable ? FUSEDGROUP : GROUP ) : ( enable ? FUSED : SINGLE ) ); } + @Deprecated public synchronized boolean isSourceVisible( final int sourceIndex ) { - return state.isSourceVisible( sourceIndex ); - } - - protected boolean[] previousVisibleSources = null; - - protected boolean[] currentVisibleSources = null; - - protected void checkVisibilityChange() - { - final boolean[] tmp = previousVisibleSources; - previousVisibleSources = currentVisibleSources; - currentVisibleSources = tmp; - - final int n = numSources(); - if ( currentVisibleSources == null || currentVisibleSources.length != n ) - currentVisibleSources = new boolean[ n ]; - Arrays.fill( currentVisibleSources, false ); - for ( final int i : state.getVisibleSourceIndices() ) - currentVisibleSources[ i ] = true; - - if ( previousVisibleSources == null || previousVisibleSources.length != n ) - { - update( VISIBILITY_CHANGED ); - return; - } - - for ( int i = 0; i < currentVisibleSources.length; ++i ) - if ( currentVisibleSources[ i ] != previousVisibleSources[ i ] ) - { - update( VISIBILITY_CHANGED ); - return; - } + return state.isSourceVisibleAndPresent( state.getSources().get( sourceIndex ) ); } + @Deprecated protected void update( final int id ) { final Event event = new Event( id, this ); @@ -375,13 +389,24 @@ public class VisibilityAndGrouping l.visibilityChanged( event ); } + @Deprecated public void addUpdateListener( final UpdateListener l ) { updateListeners.add( l ); } + @Deprecated public void removeUpdateListener( final UpdateListener l ) { updateListeners.remove( l ); } + + @Deprecated + private SourceAndConverter< ? > soc( Source< ? > source ) + { + for ( SourceAndConverter< ? > soc : state.getSources() ) + if ( soc.getSpimSource() == source ) + return soc; + return null; + } } diff --git a/src/main/java/bdv/viewer/animate/AbstractAnimator.java b/src/main/java/bdv/viewer/animate/AbstractAnimator.java index 119cd257d9078b31fdf24bf941b5c25d50d30f28..1b24cb2e34e160c6e6432b51f9d55e4301166bab 100644 --- a/src/main/java/bdv/viewer/animate/AbstractAnimator.java +++ b/src/main/java/bdv/viewer/animate/AbstractAnimator.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -38,8 +37,8 @@ import bdv.viewer.ViewerFrame; * from {@link System#currentTimeMillis()} or a frame number when rendering * movies. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> - * @author Jean-Yves Tinevez <jeanyves.tinevez@gmail.com> + * @author Tobias Pietzsch + * @author Jean-Yves Tinevez */ public class AbstractAnimator { diff --git a/src/main/java/bdv/viewer/animate/AbstractTransformAnimator.java b/src/main/java/bdv/viewer/animate/AbstractTransformAnimator.java index 73ee9b6bffdafa202fd36b7a3c8d0e5c837fc8b0..d72adc7594e8c045efa6baf08f701b9c40ca8cb6 100644 --- a/src/main/java/bdv/viewer/animate/AbstractTransformAnimator.java +++ b/src/main/java/bdv/viewer/animate/AbstractTransformAnimator.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -39,8 +38,8 @@ import bdv.viewer.ViewerFrame; * example you can use <b>ms</b> obtained from * {@link System#currentTimeMillis()} or a frame number when rendering movies. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> - * @author Jean-Yves Tinevez <jeanyves.tinevez@gmail.com> + * @author Tobias Pietzsch + * @author Jean-Yves Tinevez */ public abstract class AbstractTransformAnimator extends AbstractAnimator { diff --git a/src/main/java/bdv/viewer/animate/MessageOverlayAnimator.java b/src/main/java/bdv/viewer/animate/MessageOverlayAnimator.java index c88ada72a830d0d08ab269c8f1de7e50643eeb37..12338cd6cd746cb0ed449505eddb265a435c8555 100644 --- a/src/main/java/bdv/viewer/animate/MessageOverlayAnimator.java +++ b/src/main/java/bdv/viewer/animate/MessageOverlayAnimator.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -44,7 +43,7 @@ import java.util.List; * fading in a specified time. If several messages are drawn at the same time, * old messages scroll up. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class MessageOverlayAnimator implements OverlayAnimator { diff --git a/src/main/java/bdv/viewer/animate/OverlayAnimator.java b/src/main/java/bdv/viewer/animate/OverlayAnimator.java index d2debb3a621f72a344455734bca0acdbd173b74d..d247dde22df1426b2785b729edf2e831038564a8 100644 --- a/src/main/java/bdv/viewer/animate/OverlayAnimator.java +++ b/src/main/java/bdv/viewer/animate/OverlayAnimator.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -33,7 +32,7 @@ import java.awt.Graphics2D; public interface OverlayAnimator { - public abstract void paint( final Graphics2D g, final long time ); + void paint( final Graphics2D g, final long time ); /** * Returns true if the animation is complete and the animator can be @@ -42,7 +41,7 @@ public interface OverlayAnimator * @return whether the animation is complete and the animator can be * removed. */ - public boolean isComplete(); + boolean isComplete(); /** * Returns true if the animator requires an immediate repaint to continue @@ -50,5 +49,5 @@ public interface OverlayAnimator * * @return whetherhe animator requires an immediate repaint. */ - public boolean requiresRepaint(); + boolean requiresRepaint(); } diff --git a/src/main/java/bdv/viewer/animate/RotationAnimator.java b/src/main/java/bdv/viewer/animate/RotationAnimator.java index d834284ef81cd7f63f5c8c980499d3cc531d055e..6ba0ea2bb7d974e7811c49d8c12f30faa8ff7dcc 100644 --- a/src/main/java/bdv/viewer/animate/RotationAnimator.java +++ b/src/main/java/bdv/viewer/animate/RotationAnimator.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/viewer/animate/SimilarityTransformAnimator.java b/src/main/java/bdv/viewer/animate/SimilarityTransformAnimator.java index 6173c38b4e47fe300043539afc38f047a50303d0..c80025d6759469b1713f36c68b3fcb470dc44c44 100644 --- a/src/main/java/bdv/viewer/animate/SimilarityTransformAnimator.java +++ b/src/main/java/bdv/viewer/animate/SimilarityTransformAnimator.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/viewer/animate/TextOverlayAnimator.java b/src/main/java/bdv/viewer/animate/TextOverlayAnimator.java index da9f4e3a4b9100ff22ba8ffd9dede27c1849f670..71b20ba3f290f5aa8dc1bf0d1d0c71495aea3c42 100644 --- a/src/main/java/bdv/viewer/animate/TextOverlayAnimator.java +++ b/src/main/java/bdv/viewer/animate/TextOverlayAnimator.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -40,7 +39,7 @@ import java.awt.geom.Rectangle2D; * Draw one line of text in the center or bottom right of the display. Text is * fading in and out. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class TextOverlayAnimator extends AbstractAnimator implements OverlayAnimator { diff --git a/src/main/java/bdv/viewer/animate/TranslationAnimator.java b/src/main/java/bdv/viewer/animate/TranslationAnimator.java index 03d4ecbe74434bd7c2eb72762c17cf78d5936aa8..d232946eebef22e2a888dd77f246d4bddf662430 100644 --- a/src/main/java/bdv/viewer/animate/TranslationAnimator.java +++ b/src/main/java/bdv/viewer/animate/TranslationAnimator.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -35,7 +34,7 @@ import net.imglib2.realtransform.AffineTransform3D; * An animator that just executes a constant speed translation of the current * viewpoint to a target location, keeping all other view parameters constant. * - * @author Jean-Yves Tinevez <jeanyves.tinevez@gmail.com> + * @author Jean-Yves Tinevez */ public class TranslationAnimator extends AbstractTransformAnimator { diff --git a/src/main/java/bdv/viewer/overlay/IntervalAndTransform.java b/src/main/java/bdv/viewer/overlay/IntervalAndTransform.java index 49f5cb640fba50e7377617aa63001a470764657e..acac9d7f20ab5d11e434e2fe1e580dff53b52143 100644 --- a/src/main/java/bdv/viewer/overlay/IntervalAndTransform.java +++ b/src/main/java/bdv/viewer/overlay/IntervalAndTransform.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/viewer/overlay/MultiBoxOverlay.java b/src/main/java/bdv/viewer/overlay/MultiBoxOverlay.java index ab6d0a5cc52670fbaea93d0739a4856f72b4c79a..6715b1f7f3c245a46de10648d64f453d7bde951d 100644 --- a/src/main/java/bdv/viewer/overlay/MultiBoxOverlay.java +++ b/src/main/java/bdv/viewer/overlay/MultiBoxOverlay.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -53,7 +52,7 @@ import net.imglib2.util.LinAlgHelpers; * colors depending whether the sources are visible. * * @author Stephan Saalfeld - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class MultiBoxOverlay { @@ -71,21 +70,21 @@ public class MultiBoxOverlay public interface IntervalAndTransform { - public boolean isVisible(); + boolean isVisible(); /** * Get interval of the source (stack) in source-local coordinates. * * @return extents of the source. */ - public Interval getSourceInterval(); + Interval getSourceInterval(); /** * Current transformation from {@link #getSourceInterval() source} to * viewer. This is a concatenation of source-local-to-global transform * and the interactive viewer transform. */ - public AffineTransform3D getSourceToViewer(); + AffineTransform3D getSourceToViewer(); } /** diff --git a/src/main/java/bdv/viewer/overlay/MultiBoxOverlayRenderer.java b/src/main/java/bdv/viewer/overlay/MultiBoxOverlayRenderer.java index fb55d9b7626ffc9d5702750e3a1a9c5515cc115d..3b67ba7c661d4c3ddc28975d726ef36e6ef237de 100644 --- a/src/main/java/bdv/viewer/overlay/MultiBoxOverlayRenderer.java +++ b/src/main/java/bdv/viewer/overlay/MultiBoxOverlayRenderer.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,21 +28,20 @@ */ package bdv.viewer.overlay; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.ViewerState; import java.awt.Graphics2D; import java.util.ArrayList; import java.util.List; - import net.imglib2.Interval; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.util.Intervals; -import bdv.viewer.state.SourceState; -import bdv.viewer.state.ViewerState; /** * Render multibox overlay corresponding to a {@link ViewerState} into a * {@link Graphics2D}. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class MultiBoxOverlayRenderer { @@ -119,19 +117,32 @@ public class MultiBoxOverlayRenderer /** * Update data to show in the box overlay. */ - public synchronized void setViewerState( final ViewerState viewerState ) + @Deprecated + public synchronized void setViewerState( final bdv.viewer.state.ViewerState viewerState ) { synchronized ( viewerState ) { - final List< SourceState< ? > > sources = viewerState.getSources(); - final List< Integer > visible = viewerState.getVisibleSourceIndices(); - final int timepoint = viewerState.getCurrentTimepoint(); - - final int numSources = sources.size(); - int numPresentSources = 0; - for ( final SourceState< ? > source : sources ) - if ( source.getSpimSource().isPresent( timepoint ) ) - numPresentSources++; + setViewerState( viewerState.getState() ); + } + } + + /** + * Update data to show in the box overlay. + */ + public synchronized void setViewerState( final ViewerState state ) + { + synchronized ( state ) + { + final List< SourceAndConverter< ? > > sources = state.getSources(); + final int timepoint = state.getCurrentTimepoint(); + + final List< SourceAndConverter< ? > > presentSources = new ArrayList<>(); + sources.forEach( s -> { + if ( s.getSpimSource().isPresent( timepoint ) ) + presentSources.add( s ); + } ); + + final int numPresentSources = presentSources.size(); if ( boxSources.size() != numPresentSources ) { while ( boxSources.size() < numPresentSources ) @@ -140,21 +151,17 @@ public class MultiBoxOverlayRenderer boxSources.remove( boxSources.size() - 1 ); } - final AffineTransform3D sourceToViewer = new AffineTransform3D(); + final AffineTransform3D sourceToViewer = state.getViewerTransform(); final AffineTransform3D sourceTransform = new AffineTransform3D(); - for ( int i = 0, j = 0; i < numSources; ++i ) + int i = 0; + for ( final SourceAndConverter< ? > source : presentSources ) { - final SourceState< ? > source = sources.get( i ); - if ( source.getSpimSource().isPresent( timepoint ) ) - { - final IntervalAndTransform boxsource = boxSources.get( j++ ); - viewerState.getViewerTransform( sourceToViewer ); - source.getSpimSource().getSourceTransform( timepoint, 0, sourceTransform ); - sourceToViewer.concatenate( sourceTransform ); - boxsource.setSourceToViewer( sourceToViewer ); - boxsource.setSourceInterval( source.getSpimSource().getSource( timepoint, 0 ) ); - boxsource.setVisible( visible.contains( i ) ); - } + final IntervalAndTransform boxsource = boxSources.get( i++ ); + source.getSpimSource().getSourceTransform( timepoint, 0, sourceTransform ); + sourceTransform.preConcatenate( sourceToViewer ); + boxsource.setSourceToViewer( sourceTransform ); + boxsource.setSourceInterval( source.getSpimSource().getSource( timepoint, 0 ) ); + boxsource.setVisible( state.isSourceVisible( source ) ); } } } diff --git a/src/main/java/bdv/viewer/overlay/RenderBoxHelper.java b/src/main/java/bdv/viewer/overlay/RenderBoxHelper.java index a649cfcbb4d314fd95c781586b7ee887c6bb35f6..4c24b7d540ca57ee66865b171558e341db2e1bb3 100644 --- a/src/main/java/bdv/viewer/overlay/RenderBoxHelper.java +++ b/src/main/java/bdv/viewer/overlay/RenderBoxHelper.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -38,7 +37,7 @@ import net.imglib2.realtransform.AffineTransform3D; * Helper for rendering overlay boxes. * * @author Stephan Saalfeld - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class RenderBoxHelper { diff --git a/src/main/java/bdv/viewer/overlay/ScaleBarOverlayRenderer.java b/src/main/java/bdv/viewer/overlay/ScaleBarOverlayRenderer.java index e238cd09de6aa60681f52cca185cd916fbd015f1..05c49f2a21007775772f1ca1b1fd2e6767e4a8d1 100644 --- a/src/main/java/bdv/viewer/overlay/ScaleBarOverlayRenderer.java +++ b/src/main/java/bdv/viewer/overlay/ScaleBarOverlayRenderer.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -36,15 +35,15 @@ import java.awt.font.FontRenderContext; import java.awt.font.TextLayout; import java.awt.geom.Rectangle2D; import java.text.DecimalFormat; -import java.util.List; -import mpicbg.spim.data.sequence.VoxelDimensions; import net.imglib2.realtransform.AffineTransform3D; + import bdv.util.Affine3DHelpers; import bdv.util.Prefs; import bdv.viewer.Source; -import bdv.viewer.state.SourceState; -import bdv.viewer.state.ViewerState; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.ViewerState; +import mpicbg.spim.data.sequence.VoxelDimensions; public class ScaleBarOverlayRenderer { @@ -113,6 +112,18 @@ public class ScaleBarOverlayRenderer private static final String[] lengthUnits = { "nm", "µm", "mm", "m", "km" }; + /** + * Update data to show in the overlay. + */ + @Deprecated + public synchronized void setViewerState( final bdv.viewer.state.ViewerState state ) + { + synchronized ( state ) + { + setViewerState( state.getState() ); + } + } + /** * Update data to show in the overlay. */ @@ -120,82 +131,85 @@ public class ScaleBarOverlayRenderer { synchronized ( state ) { - final List< SourceState< ? > > sources = state.getSources(); - if ( ! sources.isEmpty() ) + final SourceAndConverter< ? > current = state.getCurrentSource(); + if ( current == null ) { - final Source< ? > spimSource = sources.get( state.getCurrentSource() ).getSpimSource(); - final VoxelDimensions voxelDimensions = spimSource.getVoxelDimensions(); - if ( voxelDimensions == null ) - { - drawScaleBar = false; - return; - } - drawScaleBar = true; + drawScaleBar = false; + return; + } - state.getViewerTransform( transform ); + final Source< ? > spimSource = current.getSpimSource(); + final VoxelDimensions voxelDimensions = spimSource.getVoxelDimensions(); + if ( voxelDimensions == null ) + { + drawScaleBar = false; + return; + } + drawScaleBar = true; - final int t = state.getCurrentTimepoint(); - spimSource.getSourceTransform( t, 0, sourceTransform ); - transform.concatenate( sourceTransform ); - final double sizeOfOnePixel = voxelDimensions.dimension( 0 ) / Affine3DHelpers.extractScale( transform, 0 ); + state.getViewerTransform( transform ); - // find good scaleBarLength and corresponding scale value - final double sT = targetScaleBarLength * sizeOfOnePixel; - final double pot = Math.floor( Math.log10( sT ) ); - final double l2 = sT / Math.pow( 10, pot ); - final int fracs = ( int ) ( 0.1 * l2 * subdivPerPowerOfTen ); - final double scale1 = ( fracs > 0 ) ? Math.pow( 10, pot + 1 ) * fracs / subdivPerPowerOfTen : Math.pow( 10, pot ); - final double scale2 = ( fracs == 3 ) ? Math.pow( 10, pot + 1 ) : Math.pow( 10, pot + 1 ) * ( fracs + 1 ) / subdivPerPowerOfTen; + final int t = state.getCurrentTimepoint(); + spimSource.getSourceTransform( t, 0, sourceTransform ); + transform.concatenate( sourceTransform ); + final double sizeOfOnePixel = voxelDimensions.dimension( 0 ) / Affine3DHelpers.extractScale( transform, 0 ); - final double lB1 = scale1 / sizeOfOnePixel; - final double lB2 = scale2 / sizeOfOnePixel; + // find good scaleBarLength and corresponding scale value + final double sT = targetScaleBarLength * sizeOfOnePixel; + final double pot = Math.floor( Math.log10( sT ) ); + final double l2 = sT / Math.pow( 10, pot ); + final int fracs = ( int ) ( 0.1 * l2 * subdivPerPowerOfTen ); + final double scale1 = ( fracs > 0 ) ? Math.pow( 10, pot + 1 ) * fracs / subdivPerPowerOfTen : Math.pow( 10, pot ); + final double scale2 = ( fracs == 3 ) ? Math.pow( 10, pot + 1 ) : Math.pow( 10, pot + 1 ) * ( fracs + 1 ) / subdivPerPowerOfTen; - if ( Math.abs( lB1 - targetScaleBarLength ) < Math.abs( lB2 - targetScaleBarLength ) ) - { - scale = scale1; - scaleBarLength = lB1; - } - else + final double lB1 = scale1 / sizeOfOnePixel; + final double lB2 = scale2 / sizeOfOnePixel; + + if ( Math.abs( lB1 - targetScaleBarLength ) < Math.abs( lB2 - targetScaleBarLength ) ) + { + scale = scale1; + scaleBarLength = lB1; + } + else + { + scale = scale2; + scaleBarLength = lB2; + } + + // If unit is a known unit (such as nm) then try to modify scale + // and unit such that the displayed string is short. + // For example, replace "0.021 µm" by "21 nm". + String scaleUnit = voxelDimensions.unit(); + if ( "um".equals( scaleUnit ) ) + scaleUnit = "µm"; + int scaleUnitIndex = -1; + for ( int i = 0; i < lengthUnits.length; ++i ) + if ( lengthUnits[ i ].equals( scaleUnit ) ) { - scale = scale2; - scaleBarLength = lB2; + scaleUnitIndex = i; + break; } - - // If unit is a known unit (such as nm) then try to modify scale - // and unit such that the displayed string is short. - // For example, replace "0.021 µm" by "21 nm". - String scaleUnit = voxelDimensions.unit(); - if ( "um".equals( scaleUnit ) ) - scaleUnit = "µm"; - int scaleUnitIndex = -1; - for ( int i = 0; i < lengthUnits.length; ++i ) - if ( lengthUnits[ i ].equals( scaleUnit ) ) - { - scaleUnitIndex = i; - break; - } - if ( scaleUnitIndex >= 0 ) + if ( scaleUnitIndex >= 0 ) + { + int shifts = ( int ) Math.floor( ( Math.log10( scale ) + 1 ) / 3 ); + int shiftedIndex = scaleUnitIndex + shifts; + if ( shiftedIndex < 0 ) { - int shifts = ( int ) Math.floor( ( Math.log10( scale ) + 1 ) / 3 ); - int shiftedIndex = scaleUnitIndex + shifts; - if ( shiftedIndex < 0 ) - { - shifts = -scaleUnitIndex; - shiftedIndex = 0; - } - else if ( shiftedIndex >= lengthUnits.length ) - { - shifts = lengthUnits.length - 1 - scaleUnitIndex; - shiftedIndex = lengthUnits.length - 1; - } - - scale = scale / Math.pow( 1000, shifts ); - unit = lengthUnits[ shiftedIndex ]; + shifts = -scaleUnitIndex; + shiftedIndex = 0; } - else + else if ( shiftedIndex >= lengthUnits.length ) { - unit = scaleUnit; + shifts = lengthUnits.length - 1 - scaleUnitIndex; + shiftedIndex = lengthUnits.length - 1; } + + scale = scale / Math.pow( 1000, shifts ); + unit = lengthUnits[ shiftedIndex ]; + } + else + { + unit = scaleUnit; } } } diff --git a/src/main/java/bdv/viewer/overlay/SourceInfoOverlayRenderer.java b/src/main/java/bdv/viewer/overlay/SourceInfoOverlayRenderer.java index ec6f4d6d5d2b8cb9128ab2bc9152655ab7ed1dd7..cf6b7d598dccb71e6289805058f1c4c274102a34 100644 --- a/src/main/java/bdv/viewer/overlay/SourceInfoOverlayRenderer.java +++ b/src/main/java/bdv/viewer/overlay/SourceInfoOverlayRenderer.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,24 +28,19 @@ */ package bdv.viewer.overlay; -import static bdv.viewer.DisplayMode.FUSEDGROUP; -import static bdv.viewer.DisplayMode.GROUP; - import java.awt.Font; import java.awt.Graphics2D; import java.util.List; -import bdv.viewer.DisplayMode; -import bdv.viewer.state.SourceGroup; -import bdv.viewer.state.SourceState; -import bdv.viewer.state.ViewerState; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.ViewerState; import mpicbg.spim.data.sequence.TimePoint; /** * Render current source name and current timepoint of a {@link ViewerState} * into a {@link Graphics2D}. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class SourceInfoOverlayRenderer { @@ -71,6 +65,18 @@ public class SourceInfoOverlayRenderer this.timePointsOrdered = timePointsOrdered; } + /** + * Update data to show in the overlay. + */ + @Deprecated + public synchronized void setViewerState( final bdv.viewer.state.ViewerState state ) + { + synchronized ( state ) + { + setViewerState( state.getState() ); + } + } + /** * Update data to show in the overlay. */ @@ -78,18 +84,13 @@ public class SourceInfoOverlayRenderer { synchronized ( state ) { - final List< SourceState< ? > > sources = state.getSources(); - if ( ! sources.isEmpty() ) - sourceName = sources.get( state.getCurrentSource() ).getSpimSource().getName(); - else - sourceName = ""; + final SourceAndConverter< ? > currentSource = state.getCurrentSource(); + sourceName = currentSource != null + ? currentSource.getSpimSource().getName() : ""; - final List< SourceGroup > groups = state.getSourceGroups(); - final DisplayMode mode = state.getDisplayMode(); - if ( ( mode == GROUP || mode == FUSEDGROUP ) && ! groups.isEmpty() ) - groupName = groups.get( state.getCurrentGroup() ).getName(); - else - groupName = ""; + final bdv.viewer.SourceGroup currentGroup = state.getCurrentGroup(); + groupName = currentGroup != null && state.getDisplayMode().hasGrouping() + ? state.getGroupName( currentGroup ) : ""; final int t = state.getCurrentTimepoint(); if ( timePointsOrdered != null && t >= 0 && t < timePointsOrdered.size() ) diff --git a/src/main/java/bdv/viewer/render/AccumulateProjector.java b/src/main/java/bdv/viewer/render/AccumulateProjector.java index eadc68de9760f35485e561cb8ff908494c36f500..91be77098862c3dec46629db2bb75981534b836b 100644 --- a/src/main/java/bdv/viewer/render/AccumulateProjector.java +++ b/src/main/java/bdv/viewer/render/AccumulateProjector.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -30,56 +29,63 @@ package bdv.viewer.render; import java.util.ArrayList; +import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; - import net.imglib2.Cursor; import net.imglib2.IterableInterval; import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; -import net.imglib2.ui.InterruptibleProjector; -import net.imglib2.ui.util.StopWatch; +import net.imglib2.util.StopWatch; import net.imglib2.view.Views; +// TODO javadoc public abstract class AccumulateProjector< A, B > implements VolatileProjector { - protected final ArrayList< VolatileProjector > sourceProjectors; + /** + * Projectors that render the source images to accumulate. + * For every rendering pass, ({@link VolatileProjector#map(boolean)}) is run on each source projector that is not yet {@link VolatileProjector#isValid() valid}. + */ + private final List< VolatileProjector > sourceProjectors; - protected final ArrayList< IterableInterval< ? extends A > > sources; + /** + * The source images to accumulate + */ + private final List< IterableInterval< ? extends A > > sources; /** * The target interval. Pixels of the target interval should be set by - * {@link InterruptibleProjector#map()} + * {@link #map} */ - protected final RandomAccessibleInterval< B > target; + private final RandomAccessibleInterval< B > target; /** * A reference to the target image as an iterable. Used for source-less * operations such as clearing its content. */ - protected final IterableInterval< B > iterableTarget; + private final IterableInterval< B > iterableTarget; /** * Number of threads to use for rendering */ - protected final int numThreads; + private final int numThreads; - protected final ExecutorService executorService; + private final ExecutorService executorService; /** * Time needed for rendering the last frame, in nano-seconds. */ - protected long lastFrameRenderNanoTime; + private long lastFrameRenderNanoTime; - protected final AtomicBoolean interrupted = new AtomicBoolean(); + private final AtomicBoolean canceled = new AtomicBoolean(); - protected volatile boolean valid = false; + private volatile boolean valid = false; public AccumulateProjector( - final ArrayList< VolatileProjector > sourceProjectors, - final ArrayList< ? extends RandomAccessible< ? extends A > > sources, + final List< VolatileProjector > sourceProjectors, + final List< ? extends RandomAccessible< ? extends A > > sources, final RandomAccessibleInterval< B > target, final int numThreads, final ExecutorService executorService ) @@ -95,19 +101,12 @@ public abstract class AccumulateProjector< A, B > implements VolatileProjector lastFrameRenderNanoTime = -1; } - @Override - public boolean map() - { - return map( true ); - } - @Override public boolean map( final boolean clearUntouchedTargetPixels ) { - interrupted.set( false ); + canceled.set( false ); - final StopWatch stopWatch = new StopWatch(); - stopWatch.start(); + final StopWatch stopWatch = StopWatch.createAndStart(); valid = true; for ( final VolatileProjector p : sourceProjectors ) @@ -119,49 +118,21 @@ public abstract class AccumulateProjector< A, B > implements VolatileProjector final int width = ( int ) target.dimension( 0 ); final int height = ( int ) target.dimension( 1 ); - final int length = width * height; + final int size = width * height; + + final int numTasks = numThreads <= 1 ? 1 : Math.min( numThreads * 10, height ); + final double taskLength = ( double ) size / numTasks; + final int[] taskOffsets = new int[ numTasks + 1 ]; + for ( int i = 0; i < numTasks; ++i ) + taskOffsets[ i ] = ( int ) ( i * taskLength ); + taskOffsets[ numTasks ] = size; final boolean createExecutor = ( executorService == null ); final ExecutorService ex = createExecutor ? Executors.newFixedThreadPool( numThreads ) : executorService; - final int numTasks = Math.min( numThreads * 10, height ); - final double taskLength = ( double ) length / numTasks; - final int numSources = sources.size(); - final ArrayList< Callable< Void > > tasks = new ArrayList<>( numTasks ); - for ( int taskNum = 0; taskNum < numTasks; ++taskNum ) - { - final int myOffset = ( int ) ( taskNum * taskLength ); - final int myLength = ( (taskNum == numTasks - 1 ) ? length : ( int ) ( ( taskNum + 1 ) * taskLength ) ) - myOffset; - - final Callable< Void > r = new Callable< Void >() - { - @SuppressWarnings( "unchecked" ) - @Override - public Void call() - { - if ( interrupted.get() ) - return null; - - final Cursor< ? extends A >[] sourceCursors = new Cursor[ numSources ]; - for ( int s = 0; s < numSources; ++s ) - { - final Cursor< ? extends A > c = sources.get( s ).cursor(); - c.jumpFwd( myOffset ); - sourceCursors[ s ] = c; - } - final Cursor< B > targetCursor = iterableTarget.cursor(); - targetCursor.jumpFwd( myOffset ); - - for ( int i = 0; i < myLength; ++i ) - { - for ( int s = 0; s < numSources; ++s ) - sourceCursors[ s ].fwd(); - accumulate( sourceCursors, targetCursor.next() ); - } - return null; - } - }; - tasks.add( r ); - } + + final List< Callable< Void > > tasks = new ArrayList<>( numTasks ); + for( int i = 0; i < numTasks; ++i ) + tasks.add( createMapTask( taskOffsets[ i ], taskOffsets[ i + 1 ] ) ); try { ex.invokeAll( tasks ); @@ -175,7 +146,50 @@ public abstract class AccumulateProjector< A, B > implements VolatileProjector lastFrameRenderNanoTime = stopWatch.nanoTime(); - return !interrupted.get(); + return !canceled.get(); + } + + /** + * @return a {@code Callable} that runs {@code map(startOffset, endOffset)} + */ + private Callable< Void > createMapTask( final int startOffset, final int endOffset ) + { + return Executors.callable( () -> map( startOffset, endOffset ), null ); + } + + /** + * Accumulate pixels from {@code startOffset} up to {@code endOffset} + * (exclusive) of all sources to target. Before starting, check + * whether rendering was {@link #cancel() canceled}. + * + * @param startOffset + * pixel range start (flattened index) + * @param endOffset + * pixel range end (exclusive, flattened index) + */ + private void map( final int startOffset, final int endOffset ) + { + if ( canceled.get() ) + return; + + final int numSources = sources.size(); + final Cursor< ? extends A >[] sourceCursors = new Cursor[ numSources ]; + for ( int s = 0; s < numSources; ++s ) + { + final Cursor< ? extends A > c = sources.get( s ).cursor(); + c.jumpFwd( startOffset ); + sourceCursors[ s ] = c; + } + final Cursor< B > targetCursor = iterableTarget.cursor(); + targetCursor.jumpFwd( startOffset ); + + final int size = endOffset - startOffset; + for ( int i = 0; i < size; ++i ) + { + for ( int s = 0; s < numSources; ++s ) + sourceCursors[ s ].fwd(); + accumulate( sourceCursors, targetCursor.next() ); + } } protected abstract void accumulate( final Cursor< ? extends A >[] accesses, final B target ); @@ -183,7 +197,7 @@ public abstract class AccumulateProjector< A, B > implements VolatileProjector @Override public void cancel() { - interrupted.set( true ); + canceled.set( true ); for ( final VolatileProjector p : sourceProjectors ) p.cancel(); } diff --git a/src/main/java/bdv/viewer/render/AccumulateProjectorARGB.java b/src/main/java/bdv/viewer/render/AccumulateProjectorARGB.java index dd84de6d7bbb8feb8c5efa081983b4cca9bd726b..c92260f53354d218e78dc68740ac5e268ce22dfc 100644 --- a/src/main/java/bdv/viewer/render/AccumulateProjectorARGB.java +++ b/src/main/java/bdv/viewer/render/AccumulateProjectorARGB.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -29,49 +28,259 @@ */ package bdv.viewer.render; +import bdv.viewer.SourceAndConverter; import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; - -import bdv.viewer.Source; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; import net.imglib2.Cursor; +import net.imglib2.Interval; import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; import net.imglib2.type.numeric.ARGBType; +import net.imglib2.util.Intervals; +import net.imglib2.util.StopWatch; -public class AccumulateProjectorARGB extends AccumulateProjector< ARGBType, ARGBType > +public class AccumulateProjectorARGB implements VolatileProjector { public static AccumulateProjectorFactory< ARGBType > factory = new AccumulateProjectorFactory< ARGBType >() { @Override - public AccumulateProjectorARGB createAccumulateProjector( - final ArrayList< VolatileProjector > sourceProjectors, - final ArrayList< Source< ? > > sources, - final ArrayList< ? extends RandomAccessible< ? extends ARGBType > > sourceScreenImages, - final RandomAccessibleInterval< ARGBType > targetScreenImages, + public VolatileProjector createProjector( + final List< VolatileProjector > sourceProjectors, + final List< SourceAndConverter< ? > > sources, + final List< ? extends RandomAccessible< ? extends ARGBType > > sourceScreenImages, + final RandomAccessibleInterval< ARGBType > targetScreenImage, final int numThreads, final ExecutorService executorService ) { - return new AccumulateProjectorARGB( sourceProjectors, sourceScreenImages, targetScreenImages, numThreads, executorService ); + try + { + return new AccumulateProjectorARGB( sourceProjectors, sourceScreenImages, targetScreenImage, numThreads, executorService ); + } + catch ( IllegalArgumentException ignored ) + {} + return new AccumulateProjectorARGBGeneric( sourceProjectors, sourceScreenImages, targetScreenImage, numThreads, executorService ); } }; + public static class AccumulateProjectorARGBGeneric extends AccumulateProjector< ARGBType, ARGBType > + { + public AccumulateProjectorARGBGeneric( + final List< VolatileProjector > sourceProjectors, + final List< ? extends RandomAccessible< ? extends ARGBType > > sources, + final RandomAccessibleInterval< ARGBType > target, + final int numThreads, + final ExecutorService executorService ) + { + super( sourceProjectors, sources, target, numThreads, executorService ); + } + + @Override + protected void accumulate( final Cursor< ? extends ARGBType >[] accesses, final ARGBType target ) + { + int aSum = 0, rSum = 0, gSum = 0, bSum = 0; + for ( final Cursor< ? extends ARGBType > access : accesses ) + { + final int value = access.get().get(); + final int a = ARGBType.alpha( value ); + final int r = ARGBType.red( value ); + final int g = ARGBType.green( value ); + final int b = ARGBType.blue( value ); + aSum += a; + rSum += r; + gSum += g; + bSum += b; + } + if ( aSum > 255 ) + aSum = 255; + if ( rSum > 255 ) + rSum = 255; + if ( gSum > 255 ) + gSum = 255; + if ( bSum > 255 ) + bSum = 255; + target.set( ARGBType.rgba( rSum, gSum, bSum, aSum ) ); + } + } + + /** + * Projectors that render the source images to accumulate. + * For every rendering pass, ({@link VolatileProjector#map(boolean)}) is run on each source projector that is not yet {@link VolatileProjector#isValid() valid}. + */ + private final List< VolatileProjector > sourceProjectors; + + /** + * The source images to accumulate + */ + private final List< ? extends RandomAccessible< ? extends ARGBType > > sources; + + private final int[][] sourceData; + + /** + * The target interval. Pixels of the target interval should be set by + * {@link #map} + */ + private final RandomAccessibleInterval< ARGBType > target; + + private final int[] targetData; + + /** + * Number of threads to use for rendering + */ + private final int numThreads; + + private final ExecutorService executorService; + + /** + * Time needed for rendering the last frame, in nano-seconds. + */ + private long lastFrameRenderNanoTime; + + private final AtomicBoolean canceled = new AtomicBoolean(); + + private volatile boolean valid = false; + public AccumulateProjectorARGB( - final ArrayList< VolatileProjector > sourceProjectors, - final ArrayList< ? extends RandomAccessible< ? extends ARGBType > > sources, + final List< VolatileProjector > sourceProjectors, + final List< ? extends RandomAccessible< ? extends ARGBType > > sources, final RandomAccessibleInterval< ARGBType > target, final int numThreads, final ExecutorService executorService ) { - super( sourceProjectors, sources, target, numThreads, executorService ); + this.sourceProjectors = sourceProjectors; + this.sources = sources; + this.target = target; + this.numThreads = numThreads; + this.executorService = executorService; + lastFrameRenderNanoTime = -1; + + targetData = ProjectorUtils.getARGBArrayImgData( target ); + if ( targetData == null ) + throw new IllegalArgumentException(); + + final int numSources = sources.size(); + sourceData = new int[ numSources ][]; + for ( int i = 0; i < numSources; ++i ) + { + final RandomAccessible< ? extends ARGBType > source = sources.get( i ); + if ( ! ( source instanceof RandomAccessibleInterval ) ) + throw new IllegalArgumentException(); + if ( ! Intervals.equals( target, ( Interval ) source ) ) + throw new IllegalArgumentException(); + sourceData[ i ] = ProjectorUtils.getARGBArrayImgData( source ); + if ( sourceData[ i ] == null ) + throw new IllegalArgumentException(); + } + } + + @Override + public boolean map( final boolean clearUntouchedTargetPixels ) + { + if ( canceled.get() ) + return false; + + final StopWatch stopWatch = StopWatch.createAndStart(); + + valid = true; + for ( final VolatileProjector p : sourceProjectors ) + if ( !p.isValid() ) + if ( !p.map( clearUntouchedTargetPixels ) ) + return false; + else + valid &= p.isValid(); + + final int size = ( int ) Intervals.numElements( target ); + + final int numTasks = numThreads <= 1 ? 1 : Math.min( numThreads * 10, size ); + final double taskLength = ( double ) size / numTasks; + final int[] taskOffsets = new int[ numTasks + 1 ]; + for ( int i = 0; i < numTasks; ++i ) + taskOffsets[ i ] = ( int ) ( i * taskLength ); + taskOffsets[ numTasks ] = size; + + final boolean createExecutor = ( executorService == null ); + final ExecutorService ex = createExecutor ? Executors.newFixedThreadPool( numThreads ) : executorService; + + final List< Callable< Void > > tasks = new ArrayList<>( numTasks ); + for( int i = 0; i < numTasks; ++i ) + tasks.add( createMapTask( taskOffsets[ i ], taskOffsets[ i + 1 ] ) ); + try + { + ex.invokeAll( tasks ); + } + catch ( final InterruptedException e ) + { + Thread.currentThread().interrupt(); + } + if ( createExecutor ) + ex.shutdown(); + + lastFrameRenderNanoTime = stopWatch.nanoTime(); + + return !canceled.get(); + } + + @Override + public void cancel() + { + canceled.set( true ); + for ( final VolatileProjector p : sourceProjectors ) + p.cancel(); } @Override - protected void accumulate( final Cursor< ? extends ARGBType >[] accesses, final ARGBType target ) + public long getLastFrameRenderNanoTime() + { + return lastFrameRenderNanoTime; + } + + @Override + public boolean isValid() + { + return valid; + } + + /** + * @return a {@code Callable} that runs {@code map(startOffset, endOffset)} + */ + private Callable< Void > createMapTask( final int startOffset, final int endOffset ) + { + return Executors.callable( () -> map( startOffset, endOffset ), null ); + } + + /** + * Accumulate pixels from {@code startOffset} up to {@code endOffset} + * (exclusive) of all sources to target. Before starting, check + * whether rendering was {@link #cancel() canceled}. + * + * @param startOffset + * pixel range start (flattened index) + * @param endOffset + * pixel range end (exclusive, flattened index) + */ + private void map( final int startOffset, final int endOffset ) + { + if ( canceled.get() ) + return; + + final int numSources = sources.size(); + final int[] values = new int[ numSources ]; + for ( int i = startOffset; i < endOffset; ++i ) + { + for ( int s = 0; s < numSources; ++s ) + values[ s ] = sourceData[ s ][ i ]; + targetData[ i ] = accumulate( values ); + } + } + + protected int accumulate( final int[] values ) { int aSum = 0, rSum = 0, gSum = 0, bSum = 0; - for ( final Cursor< ? extends ARGBType > access : accesses ) + for ( final int value : values ) { - final int value = access.get().get(); final int a = ARGBType.alpha( value ); final int r = ARGBType.red( value ); final int g = ARGBType.green( value ); @@ -89,6 +298,6 @@ public class AccumulateProjectorARGB extends AccumulateProjector< ARGBType, ARGB gSum = 255; if ( bSum > 255 ) bSum = 255; - target.set( ARGBType.rgba( rSum, gSum, bSum, aSum ) ); + return ARGBType.rgba( rSum, gSum, bSum, aSum ); } } diff --git a/src/main/java/bdv/viewer/render/AccumulateProjectorFactory.java b/src/main/java/bdv/viewer/render/AccumulateProjectorFactory.java index 2bba999b662d21011f66cc53bbebb2593ae7a925..8a141730781be7eca6c1decbed8fa39c48fbb94d 100644 --- a/src/main/java/bdv/viewer/render/AccumulateProjectorFactory.java +++ b/src/main/java/bdv/viewer/render/AccumulateProjectorFactory.java @@ -1,19 +1,18 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -29,7 +28,9 @@ */ package bdv.viewer.render; +import bdv.viewer.SourceAndConverter; import java.util.ArrayList; +import java.util.List; import java.util.concurrent.ExecutorService; import bdv.viewer.Source; @@ -53,11 +54,52 @@ public interface AccumulateProjectorFactory< A > * @param executorService * {@link ExecutorService} to use for rendering. may be null. */ - public VolatileProjector createAccumulateProjector( + default VolatileProjector createProjector( + final List< VolatileProjector > sourceProjectors, + final List< SourceAndConverter< ? > > sources, + final List< ? extends RandomAccessible< ? extends A > > sourceScreenImages, + final RandomAccessibleInterval< A > targetScreenImage, + final int numThreads, + final ExecutorService executorService ) + { + final ArrayList< Source< ? > > spimSources = new ArrayList<>(); + for ( SourceAndConverter< ? > source : sources ) + spimSources.add( source.getSpimSource() ); + final ArrayList< VolatileProjector > sp = sourceProjectors instanceof ArrayList + ? ( ArrayList ) sourceProjectors + : new ArrayList<>( sourceProjectors ); + final ArrayList< ? extends RandomAccessible< ? extends A > > si = sourceScreenImages instanceof ArrayList + ? ( ArrayList ) sourceScreenImages + : new ArrayList<>( sourceScreenImages ); + return createAccumulateProjector( sp, spimSources, si, targetScreenImage, numThreads, executorService ); + } + + /** + * @deprecated Use {@link #createProjector(List, List, List, RandomAccessibleInterval, int, ExecutorService)} instead. + * + * @param sourceProjectors + * projectors that will be used to render {@code sources}. + * @param sources + * sources to identify which channels are being rendered + * @param sourceScreenImages + * rendered images that will be accumulated into + * {@code target}. + * @param targetScreenImage + * final image to render. + * @param numThreads + * how many threads to use for rendering. + * @param executorService + * {@link ExecutorService} to use for rendering. may be null. + */ + @Deprecated + default VolatileProjector createAccumulateProjector( final ArrayList< VolatileProjector > sourceProjectors, final ArrayList< Source< ? > > sources, final ArrayList< ? extends RandomAccessible< ? extends A > > sourceScreenImages, final RandomAccessibleInterval< A > targetScreenImage, final int numThreads, - final ExecutorService executorService ); + final ExecutorService executorService ) + { + throw new UnsupportedOperationException( "AccumulateProjectorFactory::createAccumulateProjector is deprecated and by default not implemented" ); + } } diff --git a/src/main/java/bdv/viewer/render/DefaultMipmapOrdering.java b/src/main/java/bdv/viewer/render/DefaultMipmapOrdering.java index 3ad8f96cae9abe3fc892392f57fd371c00b1d6a2..332bc1cdf392509ec71b6bbf18b96b18a227bf40 100644 --- a/src/main/java/bdv/viewer/render/DefaultMipmapOrdering.java +++ b/src/main/java/bdv/viewer/render/DefaultMipmapOrdering.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -54,7 +53,7 @@ import net.imglib2.realtransform.AffineTransform3D; * between images that have all data present already or that we move to a new * image with no data present at all. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class DefaultMipmapOrdering implements MipmapOrdering { diff --git a/src/main/java/bdv/viewer/render/EmptyProjector.java b/src/main/java/bdv/viewer/render/EmptyProjector.java index 3562d3086089a0f59064c8067acf0e00c79c141f..63a40b4f3edb34ea034e43f1a9aa286c6262a764 100644 --- a/src/main/java/bdv/viewer/render/EmptyProjector.java +++ b/src/main/java/bdv/viewer/render/EmptyProjector.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,9 +28,15 @@ */ package bdv.viewer.render; +import java.util.Arrays; import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.array.ArrayImg; +import net.imglib2.img.basictypeaccess.array.IntArray; +import net.imglib2.type.numeric.ARGBType; import net.imglib2.type.numeric.NumericType; -import net.imglib2.ui.util.StopWatch; +import net.imglib2.util.Intervals; +import net.imglib2.util.StopWatch; +import net.imglib2.util.Util; import net.imglib2.view.Views; public class EmptyProjector< T extends NumericType< T> > implements VolatileProjector @@ -46,20 +51,24 @@ public class EmptyProjector< T extends NumericType< T> > implements VolatileProj lastFrameRenderNanoTime = -1; } - @Override - public boolean map() - { - return map( false ); - } - @Override public boolean map( final boolean clearUntouchedTargetPixels ) { - final StopWatch stopWatch = new StopWatch(); - stopWatch.start(); + final StopWatch stopWatch = StopWatch.createAndStart(); if ( clearUntouchedTargetPixels ) - for ( final T t : Views.iterable( target ) ) - t.setZero(); + { + final int[] data = ProjectorUtils.getARGBArrayImgData( target ); + if ( data != null ) + { + final int size = ( int ) Intervals.numElements( target ); + Arrays.fill( data, 0, size, 0 ); + } + else + { + for ( final T t : Views.iterable( target ) ) + t.setZero(); + } + } lastFrameRenderNanoTime = stopWatch.nanoTime(); return true; } diff --git a/src/main/java/bdv/viewer/render/MipmapOrdering.java b/src/main/java/bdv/viewer/render/MipmapOrdering.java index d6779e615ce0f1e8f828dd02092b194d2ce2fdcf..9de2ba7bc0ff14a0e1fb9052760fb2c5571ccdac 100644 --- a/src/main/java/bdv/viewer/render/MipmapOrdering.java +++ b/src/main/java/bdv/viewer/render/MipmapOrdering.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -48,9 +47,9 @@ public interface MipmapOrdering * @param previousTimepoint * previous timepoint index */ - public MipmapHints getMipmapHints( AffineTransform3D screenTransform, int timepoint, int previousTimepoint ); + MipmapHints getMipmapHints( AffineTransform3D screenTransform, int timepoint, int previousTimepoint ); - public static class Level + class Level { // level index in Source private final int mipmapLevel; @@ -112,7 +111,7 @@ public interface MipmapOrdering } } - public static class RenderOrderComparator implements Comparator< Level > + class RenderOrderComparator implements Comparator< Level > { @Override public int compare( final Level o1, final Level o2 ) @@ -121,7 +120,7 @@ public interface MipmapOrdering } } - public static class PrefetchOrderComparator implements Comparator< Level > + class PrefetchOrderComparator implements Comparator< Level > { @Override public int compare( final Level o1, final Level o2 ) @@ -130,11 +129,11 @@ public interface MipmapOrdering } } - public static RenderOrderComparator renderOrderComparator = new RenderOrderComparator(); + RenderOrderComparator renderOrderComparator = new RenderOrderComparator(); - public static PrefetchOrderComparator prefetchOrderComparator = new PrefetchOrderComparator(); + PrefetchOrderComparator prefetchOrderComparator = new PrefetchOrderComparator(); - public static class MipmapHints + class MipmapHints { private final List< Level > levels; diff --git a/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java b/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java index 924dd0349d37e2721d2739f9d9a31ed8719c07ff..792980a5f4e4239178906374284a8833fef67b49 100644 --- a/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java +++ b/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,259 +28,218 @@ */ package bdv.viewer.render; -import java.awt.image.BufferedImage; -import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.concurrent.ExecutorService; -import bdv.cache.CacheControl; -import bdv.img.cache.VolatileCachedCellImg; -import bdv.viewer.Interpolation; -import bdv.viewer.Source; -import bdv.viewer.render.MipmapOrdering.Level; -import bdv.viewer.render.MipmapOrdering.MipmapHints; -import bdv.viewer.state.SourceState; -import bdv.viewer.state.ViewerState; -import net.imglib2.Dimensions; -import net.imglib2.RandomAccess; -import net.imglib2.RandomAccessible; +import net.imglib2.Interval; import net.imglib2.RandomAccessibleInterval; -import net.imglib2.RealRandomAccessible; import net.imglib2.Volatile; import net.imglib2.cache.iotiming.CacheIoTiming; -import net.imglib2.cache.volatiles.CacheHints; -import net.imglib2.cache.volatiles.LoadingStrategy; -import net.imglib2.converter.Converter; -import net.imglib2.display.screenimage.awt.ARGBScreenImage; import net.imglib2.realtransform.AffineTransform3D; -import net.imglib2.realtransform.RealViews; import net.imglib2.type.numeric.ARGBType; -import net.imglib2.ui.PainterThread; -import net.imglib2.ui.RenderTarget; -import net.imglib2.ui.Renderer; -import net.imglib2.ui.SimpleInterruptibleProjector; -import net.imglib2.ui.TransformListener; -import net.imglib2.ui.util.GuiUtil; +import net.imglib2.util.Intervals; + +import bdv.cache.CacheControl; +import bdv.util.MovingAverage; +import bdv.viewer.RequestRepaint; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.ViewerState; +import bdv.viewer.render.ScreenScales.IntervalRenderData; +import bdv.viewer.render.ScreenScales.ScreenScale; /** - * A {@link Renderer} that uses a coarse-to-fine rendering scheme. First, a - * small {@link BufferedImage} at a fraction of the canvas resolution is - * rendered. Then, increasingly larger images are rendered, until the full - * canvas resolution is reached. - * <p> - * When drawing the low-resolution {@link BufferedImage} to the screen, they - * will be scaled up by Java2D to the full canvas size, which is relatively - * fast. Rendering the small, low-resolution images is usually very fast, such - * that the display is very interactive while the user changes the viewing - * transformation for example. When the transformation remains fixed for a - * longer period, higher-resolution details are filled in successively. + * A renderer that uses a coarse-to-fine rendering scheme. First, a small target + * image at a fraction of the canvas resolution is rendered. Then, increasingly + * larger images are rendered, until the full canvas resolution is reached. * <p> - * The renderer allocates a {@link BufferedImage} for each of a predefined set - * of <em>screen scales</em> (a screen scale of 1 means that 1 pixel in the - * screen image is displayed as 1 pixel on the canvas, a screen scale of 0.5 - * means 1 pixel in the screen image is displayed as 2 pixel on the canvas, - * etc.) + * When drawing the low-resolution target images to the screen, they will be + * scaled up by Java2D (or JavaFX, etc) to the full canvas size, which is + * relatively fast. Rendering the small, low-resolution images is usually very + * fast, such that the display is very interactive while the user changes the + * viewing transformation for example. When the transformation remains fixed for + * a longer period, higher-resolution details are filled in successively. * <p> - * At any time, one of these screen scales is selected as the - * <em>highest screen scale</em>. Rendering starts with this highest screen - * scale and then proceeds to lower screen scales (higher resolution images). - * Unless the highest screen scale is currently rendering, - * {@link #requestRepaint() repaint request} will cancel rendering, such that - * display remains interactive. + * The renderer allocates a {@code RenderResult} for each of a predefined set of + * <em>screen scales</em> (a screen scale of 1 means that 1 pixel in the screen + * image is displayed as 1 pixel on the canvas, a screen scale of 0.5 means 1 + * pixel in the screen image is displayed as 2 pixel on the canvas, etc.) * <p> - * The renderer tries to maintain a per-frame rendering time close to a desired - * number of <code>targetRenderNanos</code> nanoseconds. If the rendering time - * (in nanoseconds) for the (currently) highest scaled screen image is above - * this threshold, a coarser screen scale is chosen as the highest screen scale - * to use. Similarly, if the rendering time for the (currently) second-highest - * scaled screen image is below this threshold, this finer screen scale chosen - * as the highest screen scale to use. + * At any time, one of these screen scales is selected as the <em>highest screen + * scale</em>. Rendering starts with this highest screen scale and then proceeds + * to lower screen scales (higher resolution images). Unless the highest screen + * scale is currently rendering, {@link #requestRepaint() repaint request} will + * cancel rendering, such that display remains interactive. * <p> - * The renderer uses multiple threads (if desired) and double-buffering (if - * desired). + * The renderer tries to maintain a per-frame rendering time close to + * {@code targetRenderNanos} nanoseconds. The current highest screen scale is + * chosen to match this time based on per-output-pixel time measured in previous + * frames. * <p> - * Double buffering means that three {@link BufferedImage BufferedImages} are - * created for every screen scale. After rendering the first one of them and - * setting it to the {@link RenderTarget}, next time, rendering goes to the - * second one, then to the third. The {@link RenderTarget} will always have a - * complete image, which is not rendered to while it is potentially drawn to the - * screen. When setting an image to the {@link RenderTarget}, the - * {@link RenderTarget} will release one of the previously set images to be - * rendered again. Thus, rendering will not interfere with painting the - * {@link BufferedImage} to the canvas. + * The renderer uses multiple threads (if desired). * <p> * The renderer supports rendering of {@link Volatile} sources. In each * rendering pass, all currently valid data for the best fitting mipmap level - * and all coarser levels is rendered to a {@link #renderImages temporary image} - * for each visible source. Then the temporary images are combined to the final - * image for display. The number of passes required until all data is valid - * might differ between visible sources. + * and all coarser levels is rendered to a temporary image for each visible + * source. Then the temporary images are combined to the final image for + * display. The number of passes required until all data is valid might differ + * between visible sources. * <p> - * Rendering timing is tied to a {@link CacheControl} control for IO budgeting, etc. + * Rendering timing is tied to a {@link CacheControl} control for IO budgeting, + * etc. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class MultiResolutionRenderer { /** - * Receiver for the {@link BufferedImage BufferedImages} that we render. + * Receiver for the {@code BufferedImage BufferedImages} that we render. */ - protected final TransformAwareRenderTarget display; + private final RenderTarget< ? > display; /** * Thread that triggers repainting of the display. * Requests for repainting are send there. */ - protected final PainterThread painterThread; + private final RequestRepaint painterThread; /** - * Currently active projector, used to re-paint the display. It maps the - * source data to {@link #screenImages}. + * Creates projectors for rendering current {@code ViewerState} to a + * {@code screenImage}. */ - protected VolatileProjector projector; + private final ProjectorFactory projectorFactory; /** - * The index of the screen scale of the {@link #projector current projector}. + * Controls IO budgeting and fetcher queue. */ - protected int currentScreenScaleIndex; + private final CacheControl cacheControl; - /** - * Whether double buffering is used. - */ - protected final boolean doubleBuffered; + // TODO: should be settable + private final long[] iobudget = new long[] { 100l * 1000000l, 10l * 1000000l }; /** - * Double-buffer index of next {@link #screenImages image} to render. + * Maintains current sizes and transforms at every screen scale level. + * Records interval rendering requests. */ - protected final ArrayDeque< Integer > renderIdQueue; + private final ScreenScales screenScales; /** - * Maps from {@link BufferedImage} to double-buffer index. - * Needed for double-buffering. + * Maintains arrays for intermediate per-source render images and masks. */ - protected final HashMap< BufferedImage, Integer > bufferedImageToRenderId; + private final RenderStorage renderStorage; /** - * Used to render an individual source. One image per screen resolution and - * visible source. First index is screen scale, second index is index in - * list of visible sources. + * Estimate of the time it takes to render one screen pixel from one source, + * in nanoseconds. */ - protected ARGBScreenImage[][] renderImages; + private final MovingAverage renderNanosPerPixelAndSource; /** - * Storage for mask images of {@link VolatileHierarchyProjector}. - * One array per visible source. (First) index is index in list of visible sources. + * Currently active projector, used to re-paint the display. It maps the + * source data to {@code ©screenImages}. {@code projector.cancel()} can be + * used to cancel the ongoing rendering operation. */ - protected byte[][] renderMaskArrays; + private VolatileProjector projector; /** - * Used to render the image for display. Three images per screen resolution - * if double buffering is enabled. First index is screen scale, second index - * is double-buffer. + * Whether the current rendering operation may be cancelled (to start a new + * one). Rendering may be cancelled unless we are rendering at the + * (estimated) coarsest screen scale meeting the rendering time threshold. */ - protected ARGBScreenImage[][] screenImages; + private boolean renderingMayBeCancelled; /** - * {@link BufferedImage}s wrapping the data in the {@link #screenImages}. - * First index is screen scale, second index is double-buffer. + * Snapshot of the ViewerState that is currently being rendered. + * A new snapshot is taken in the first {@code paint()} pass after a (full frame) {@link #requestRepaint()} */ - protected BufferedImage[][] bufferedImages; + private ViewerState currentViewerState; /** - * Scale factors from the {@link #display viewer canvas} to the - * {@link #screenImages}. - * - * A scale factor of 1 means 1 pixel in the screen image is displayed as 1 - * pixel on the canvas, a scale factor of 0.5 means 1 pixel in the screen - * image is displayed as 2 pixel on the canvas, etc. + * The sources that are actually visible on screen currently. This means + * that the sources both are visible in the {@link #currentViewerState} (via + * {@link ViewerState#getVisibleAndPresentSources() + * getVisibleAndPresentSources}) and, when transformed to viewer + * coordinates, overlap the screen area ({@link #display}). */ - protected final double[] screenScales; + private final List< SourceAndConverter< ? > > currentVisibleSourcesOnScreen; + /** - * The scale transformation from viewer to {@link #screenImages screen - * image}. Each transformations corresponds to a {@link #screenScales screen - * scale}. + * The last successfully rendered (not cancelled) full frame result. + * This result is by full frame refinement passes and/or interval rendering passes. */ - protected AffineTransform3D[] screenScaleTransforms; + private RenderResult currentRenderResult; /** - * If the rendering time (in nanoseconds) for the (currently) highest scaled - * screen image is above this threshold, increase the - * {@link #maxScreenScaleIndex index} of the highest screen scale to use. - * Similarly, if the rendering time for the (currently) second-highest - * scaled screen image is below this threshold, decrease the - * {@link #maxScreenScaleIndex index} of the highest screen scale to use. + * If {@code true}, then we are painting intervals currently. + * If {@code false}, then we are painting full frames. */ - protected final long targetRenderNanos; + private boolean intervalMode; - /** - * The index of the (coarsest) screen scale with which to start rendering. - * Once this level is painted, rendering proceeds to lower screen scales - * until index 0 (full resolution) has been reached. While rendering, the - * maxScreenScaleIndex is adapted such that it is the highest index for - * which rendering in {@link #targetRenderNanos} nanoseconds is still - * possible. + /* + * + * === FULL FRAME RENDERING === + * */ - protected int maxScreenScaleIndex; /** - * The index of the screen scale which should be rendered next. + * Screen scale of the last successful (not cancelled) rendering pass in + * full-frame mode. */ - protected int requestedScreenScaleIndex; + private int currentScreenScaleIndex; /** - * Whether the current rendering operation may be cancelled (to start a - * new one). Rendering may be cancelled unless we are rendering at - * coarsest screen scale and coarsest mipmap level. + * The index of the screen scale which should be rendered next in full-frame + * mode. */ - protected volatile boolean renderingMayBeCancelled; + private int requestedScreenScaleIndex; /** - * How many threads to use for rendering. + * Whether a full frame repaint was {@link #requestRepaint() requested}. + * Supersedes {@link #newIntervalRequest}. */ - protected final int numRenderingThreads; + private boolean newFrameRequest; - /** - * {@link ExecutorService} used for rendering. + /* + * + * === INTERVAL RENDERING === + * */ - protected final ExecutorService renderingExecutorService; /** - * TODO + * Screen scale of the last successful (not cancelled) rendering pass in + * interval mode. */ - protected final AccumulateProjectorFactory< ARGBType > accumulateProjectorFactory; + private int currentIntervalScaleIndex; /** - * Controls IO budgeting and fetcher queue. + * The index of the screen scale which should be rendered next in interval + * mode. */ - protected final CacheControl cacheControl; + private int requestedIntervalScaleIndex; /** - * Whether volatile versions of sources should be used if available. + * Whether repainting of an interval was {@link #requestRepaint(Interval) + * requested}. The union of all pending intervals are recorded in + * {@link #screenScales}. Pending interval requests are obsoleted by full + * frame repaints requests ({@link #newFrameRequest}). */ - protected final boolean useVolatileIfAvailable; + private boolean newIntervalRequest; /** - * Whether a repaint was {@link #requestRepaint() requested}. This will - * cause {@link CacheControl#prepareNextFrame()}. + * Re-used for all interval rendering. */ - protected boolean newFrameRequest; + private final RenderResult intervalResult; /** - * The timepoint for which last a projector was - * {@link #createProjector(ViewerState, ARGBScreenImage) created}. + * Currently rendering interval. This is pulled from {@link #screenScales} + * at the start of {@code paint()}, which clears requested intervals at + * {@link #currentIntervalScaleIndex} or coarser. (This ensures that new + * interval requests arriving during rendering are not missed. If the + * requested intervals would be cleared after rendering, this might happen. + * Instead we re-request the pulled intervals, if rendering fails.) */ - protected int previousTimepoint; - - // TODO: should be settable - protected long[] iobudget = new long[] { 100l * 1000000l, 10l * 1000000l }; - - // TODO: should be settable - protected boolean prefetchCells = true; + private IntervalRenderData intervalRenderData; /** * @param display @@ -289,7 +247,7 @@ public class MultiResolutionRenderer * @param painterThread * Thread that triggers repainting of the display. Requests for * repainting are send there. - * @param screenScales + * @param screenScaleFactors * Scale factors from the viewer canvas to screen images of * different resolutions. A scale factor of 1 means 1 pixel in * the screen image is displayed as 1 pixel on the canvas, a @@ -298,8 +256,6 @@ public class MultiResolutionRenderer * @param targetRenderNanos * Target rendering time in nanoseconds. The rendering time for * the coarsest rendered scale should be below this threshold. - * @param doubleBuffered - * Whether to use double buffered rendering. * @param numRenderingThreads * How many threads to use for rendering. * @param renderingExecutorService @@ -316,194 +272,249 @@ public class MultiResolutionRenderer * the cache controls IO budgeting and fetcher queue. */ public MultiResolutionRenderer( - final RenderTarget display, - final PainterThread painterThread, - final double[] screenScales, + final RenderTarget< ? > display, + final RequestRepaint painterThread, + final double[] screenScaleFactors, final long targetRenderNanos, - final boolean doubleBuffered, final int numRenderingThreads, final ExecutorService renderingExecutorService, final boolean useVolatileIfAvailable, final AccumulateProjectorFactory< ARGBType > accumulateProjectorFactory, final CacheControl cacheControl ) { - this.display = wrapTransformAwareRenderTarget( display ); + this.display = display; this.painterThread = painterThread; projector = null; currentScreenScaleIndex = -1; - this.screenScales = screenScales.clone(); - this.doubleBuffered = doubleBuffered; - renderIdQueue = new ArrayDeque<>(); - bufferedImageToRenderId = new HashMap<>(); - renderImages = new ARGBScreenImage[ screenScales.length ][ 0 ]; - renderMaskArrays = new byte[ 0 ][]; - screenImages = new ARGBScreenImage[ screenScales.length ][ 3 ]; - bufferedImages = new BufferedImage[ screenScales.length ][ 3 ]; - screenScaleTransforms = new AffineTransform3D[ screenScales.length ]; - - this.targetRenderNanos = targetRenderNanos; - - maxScreenScaleIndex = screenScales.length - 1; - requestedScreenScaleIndex = maxScreenScaleIndex; - renderingMayBeCancelled = true; - this.numRenderingThreads = numRenderingThreads; - this.renderingExecutorService = renderingExecutorService; - this.useVolatileIfAvailable = useVolatileIfAvailable; - this.accumulateProjectorFactory = accumulateProjectorFactory; + currentVisibleSourcesOnScreen = new ArrayList<>(); + screenScales = new ScreenScales( screenScaleFactors, targetRenderNanos ); + renderStorage = new RenderStorage(); + + renderNanosPerPixelAndSource = new MovingAverage( 3 ); + renderNanosPerPixelAndSource.init( 500 ); + + requestedScreenScaleIndex = screenScales.size() - 1; + renderingMayBeCancelled = false; this.cacheControl = cacheControl; newFrameRequest = false; - previousTimepoint = -1; + + intervalResult = display.createRenderResult(); + + projectorFactory = new ProjectorFactory( + numRenderingThreads, + renderingExecutorService, + useVolatileIfAvailable, + accumulateProjectorFactory ); } /** - * Check whether the size of the display component was changed and - * recreate {@link #screenImages} and {@link #screenScaleTransforms} accordingly. - * - * @return whether the size was changed. + * Request a repaint of the display from the painter thread. The painter + * thread will trigger a {@link #paint} as soon as possible (that is, + * immediately or after the currently running {@link #paint} has completed). */ - protected synchronized boolean checkResize() + public synchronized void requestRepaint() { - final int componentW = display.getWidth(); - final int componentH = display.getHeight(); - if ( screenImages[ 0 ][ 0 ] == null || screenImages[ 0 ][ 0 ].dimension( 0 ) != ( int ) ( componentW * screenScales[ 0 ] ) || screenImages[ 0 ][ 0 ].dimension( 1 ) != ( int ) ( componentH * screenScales[ 0 ] ) ) - { - renderIdQueue.clear(); - renderIdQueue.addAll( Arrays.asList( 0, 1, 2 ) ); - bufferedImageToRenderId.clear(); - for ( int i = 0; i < screenScales.length; ++i ) - { - final double screenToViewerScale = screenScales[ i ]; - final int w = ( int ) ( screenToViewerScale * componentW ); - final int h = ( int ) ( screenToViewerScale * componentH ); - if ( doubleBuffered ) - { - for ( int b = 0; b < 3; ++b ) - { - // reuse storage arrays of level 0 (highest resolution) - screenImages[ i ][ b ] = ( i == 0 ) ? - new ARGBScreenImage( w, h ) : - new ARGBScreenImage( w, h, screenImages[ 0 ][ b ].getData() ); - final BufferedImage bi = GuiUtil.getBufferedImage( screenImages[ i ][ b ] ); - bufferedImages[ i ][ b ] = bi; - bufferedImageToRenderId.put( bi, b ); - } - } - else - { - screenImages[ i ][ 0 ] = new ARGBScreenImage( w, h ); - bufferedImages[ i ][ 0 ] = GuiUtil.getBufferedImage( screenImages[ i ][ 0 ] ); - } - final AffineTransform3D scale = new AffineTransform3D(); - final double xScale = ( double ) w / componentW; - final double yScale = ( double ) h / componentH; - scale.set( xScale, 0, 0 ); - scale.set( yScale, 1, 1 ); - scale.set( 0.5 * xScale - 0.5, 0, 3 ); - scale.set( 0.5 * yScale - 0.5, 1, 3 ); - screenScaleTransforms[ i ] = scale; - } - - return true; - } - return false; + if ( renderingMayBeCancelled && projector != null ) + projector.cancel(); + newFrameRequest = true; + painterThread.requestRepaint(); } - protected boolean checkRenewRenderImages( final int numVisibleSources ) + /** + * Request a repaint of the given {@code interval} of the display from the + * painter thread. The painter thread will trigger a {@link #paint} as soon + * as possible (that is, immediately or after the currently running + * {@link #paint} has completed). + */ + public synchronized void requestRepaint( final Interval interval ) { - final int n = numVisibleSources > 1 ? numVisibleSources : 0; - if ( n != renderImages[ 0 ].length || - ( n != 0 && - ( renderImages[ 0 ][ 0 ].dimension( 0 ) != screenImages[ 0 ][ 0 ].dimension( 0 ) || - renderImages[ 0 ][ 0 ].dimension( 1 ) != screenImages[ 0 ][ 0 ].dimension( 1 ) ) ) ) + // if interval doesn't overlap the screen, do nothing + if ( Intervals.isEmpty( screenScales.clipToScreen( interval ) ) ) + return; + + if ( !intervalMode && !renderingMayBeCancelled ) { - renderImages = new ARGBScreenImage[ screenScales.length ][ n ]; - for ( int i = 0; i < screenScales.length; ++i ) - { - final int w = ( int ) screenImages[ i ][ 0 ].dimension( 0 ); - final int h = ( int ) screenImages[ i ][ 0 ].dimension( 1 ); - for ( int j = 0; j < n; ++j ) - { - renderImages[ i ][ j ] = ( i == 0 ) ? - new ARGBScreenImage( w, h ) : - new ARGBScreenImage( w, h, renderImages[ 0 ][ j ].getData() ); - } - } - return true; + /* + * We are currently rendering a full frame at the coarsest + * resolution. There is no point in painting an interval now. Just + * request a new full frame. + */ + newFrameRequest = true; } - return false; - } - - protected boolean checkRenewMaskArrays( final int numVisibleSources ) - { - if ( numVisibleSources != renderMaskArrays.length || - ( numVisibleSources != 0 && ( renderMaskArrays[ 0 ].length < screenImages[ 0 ][ 0 ].size() ) ) ) + else { - final int size = ( int ) screenImages[ 0 ][ 0 ].size(); - renderMaskArrays = new byte[ numVisibleSources ][]; - for ( int j = 0; j < numVisibleSources; ++j ) - renderMaskArrays[ j ] = new byte[ size ]; - return true; + if ( renderingMayBeCancelled && projector != null ) + projector.cancel(); + screenScales.requestInterval( interval ); + newIntervalRequest = true; } - return false; + painterThread.requestRepaint(); } - protected final AffineTransform3D currentProjectorTransform = new AffineTransform3D(); + /** + * DON'T USE THIS. + * <p> + * This is a work around for JDK bug + * https://bugs.openjdk.java.net/browse/JDK-8029147 which leads to + * ViewerPanel not being garbage-collected when ViewerFrame is closed. So + * instead we need to manually let go of resources... + */ + public void kill() + { + projector = null; + currentViewerState = null; + currentRenderResult = null; + currentVisibleSourcesOnScreen.clear(); + renderStorage.clear(); + } /** * Render image at the {@link #requestedScreenScaleIndex requested screen * scale}. */ - public boolean paint( final ViewerState state ) + public boolean paint( final ViewerState viewerState ) { - if ( display.getWidth() <= 0 || display.getHeight() <= 0 ) + final int screenW = display.getWidth(); + final int screenH = display.getHeight(); + if ( screenW <= 0 || screenH <= 0 ) return false; - final boolean resized = checkResize(); + final boolean newFrame; + final boolean newInterval; + final boolean prepareNextFrame; + final boolean createProjector; + synchronized ( this ) + { + final boolean resized = screenScales.checkResize( screenW, screenH ); + + newFrame = newFrameRequest || resized; + if ( newFrame ) + { + intervalMode = false; + screenScales.clearRequestedIntervals(); + } + + newInterval = newIntervalRequest && !newFrame; + if ( newInterval ) + { + intervalMode = true; + final int numSources = currentVisibleSourcesOnScreen.size(); + final double renderNanosPerPixel = renderNanosPerPixelAndSource.getAverage() * numSources; + requestedIntervalScaleIndex = screenScales.suggestIntervalScreenScale( renderNanosPerPixel, currentScreenScaleIndex ); + } + + prepareNextFrame = newFrame || newInterval; + renderingMayBeCancelled = !prepareNextFrame; - // the BufferedImage that is rendered to (to paint to the canvas) - final BufferedImage bufferedImage; + if ( intervalMode ) + { + createProjector = newInterval || ( requestedIntervalScaleIndex != currentIntervalScaleIndex ); + if ( createProjector ) + intervalRenderData = screenScales.pullIntervalRenderData( requestedIntervalScaleIndex, currentScreenScaleIndex ); + } + else + createProjector = newFrame || ( requestedScreenScaleIndex != currentScreenScaleIndex ); + + newFrameRequest = false; + newIntervalRequest = false; + } + if ( prepareNextFrame ) + cacheControl.prepareNextFrame(); + + if ( newFrame ) + { + currentViewerState = viewerState.snapshot(); + VisibilityUtils.computeVisibleSourcesOnScreen( currentViewerState, screenScales.get( 0 ), currentVisibleSourcesOnScreen ); + final int numSources = currentVisibleSourcesOnScreen.size(); + final double renderNanosPerPixel = renderNanosPerPixelAndSource.getAverage() * numSources; + requestedScreenScaleIndex = screenScales.suggestScreenScale( renderNanosPerPixel ); + } + + if ( !intervalMode && requestedScreenScaleIndex < 0 ) + return true; + + return intervalMode + ? paintInterval( createProjector ) + : paintFullFrame( createProjector ); + } + + private boolean paintFullFrame( final boolean createProjector ) + { // the projector that paints to the screenImage. final VolatileProjector p; - final boolean clearQueue; + // holds new RenderResult, in case that a new projector is created in full frame mode + RenderResult renderResult = null; - final boolean createProjector; + // whether to request a newFrame, in case that a new projector is created in full frame mode + boolean requestNewFrameIfIncomplete = false; synchronized ( this ) { - // Rendering may be cancelled unless we are rendering at coarsest - // screen scale and coarsest mipmap level. - renderingMayBeCancelled = ( requestedScreenScaleIndex < maxScreenScaleIndex ); - - clearQueue = newFrameRequest; - if ( clearQueue ) - cacheControl.prepareNextFrame(); - createProjector = newFrameRequest || resized || ( requestedScreenScaleIndex != currentScreenScaleIndex ); - newFrameRequest = false; - if ( createProjector ) { - final int renderId = renderIdQueue.peek(); + final ScreenScale screenScale = screenScales.get( requestedScreenScaleIndex ); + + renderResult = display.getReusableRenderResult(); + renderResult.init( screenScale.width(), screenScale.height() ); + renderResult.setScaleFactor( screenScale.scale() ); + currentViewerState.getViewerTransform( renderResult.getViewerTransform() ); + + renderStorage.checkRenewData( screenScales.get( 0 ).width(), screenScales.get( 0 ).height(), currentVisibleSourcesOnScreen.size() ); + projector = createProjector( currentViewerState, currentVisibleSourcesOnScreen, requestedScreenScaleIndex, renderResult.getTargetImage(), 0, 0 ); + requestNewFrameIfIncomplete = projectorFactory.requestNewFrameIfIncomplete(); + } + p = projector; + } + + // try rendering + final boolean success = p.map( createProjector ); + final long rendertime = p.getLastFrameRenderNanoTime(); + + synchronized ( this ) + { + // if rendering was not cancelled... + if ( success ) + { currentScreenScaleIndex = requestedScreenScaleIndex; - bufferedImage = bufferedImages[ currentScreenScaleIndex ][ renderId ]; - final ARGBScreenImage screenImage = screenImages[ currentScreenScaleIndex ][ renderId ]; - synchronized ( state ) + if ( createProjector ) { - final int numVisibleSources = state.getVisibleSourceIndices().size(); - checkRenewRenderImages( numVisibleSources ); - checkRenewMaskArrays( numVisibleSources ); - p = createProjector( state, screenImage ); + renderResult.setUpdated(); + ( ( RenderTarget ) display ).setRenderResult( renderResult ); + currentRenderResult = renderResult; + recordRenderTime( renderResult, rendertime ); } - projector = p; + else + currentRenderResult.setUpdated(); + + if ( !p.isValid() && requestNewFrameIfIncomplete ) + requestRepaint(); + else if ( p.isValid() && currentScreenScaleIndex == 0 ) + // indicate that rendering is complete + requestedScreenScaleIndex = -1; + else + iterateRepaint( Math.max( 0, currentScreenScaleIndex - 1 ) ); } - else + } + + return success; + } + + private boolean paintInterval( final boolean createProjector ) + { + // the projector that paints to the screenImage. + final VolatileProjector p; + + synchronized ( this ) + { + if ( createProjector ) { - bufferedImage = null; - p = projector; + intervalResult.init( intervalRenderData.width(), intervalRenderData.height() ); + intervalResult.setScaleFactor( intervalRenderData.scale() ); + projector = createProjector( currentViewerState, currentVisibleSourcesOnScreen, requestedIntervalScaleIndex, intervalResult.getTargetImage(), intervalRenderData.offsetX(), intervalRenderData.offsetY() ); } - - requestedScreenScaleIndex = 0; + p = projector; } // try rendering @@ -515,357 +526,112 @@ public class MultiResolutionRenderer // if rendering was not cancelled... if ( success ) { - if ( createProjector ) - { - final BufferedImage bi = display.setBufferedImageAndTransform( bufferedImage, currentProjectorTransform ); - if ( doubleBuffered ) - { - renderIdQueue.pop(); - final Integer id = bufferedImageToRenderId.get( bi ); - if ( id != null ) - renderIdQueue.add( id ); - } + currentIntervalScaleIndex = requestedIntervalScaleIndex; + currentRenderResult.patch( intervalResult, intervalRenderData.targetInterval(), intervalRenderData.tx(), intervalRenderData.ty() ); - if ( currentScreenScaleIndex == maxScreenScaleIndex ) - { - if ( rendertime > targetRenderNanos && maxScreenScaleIndex < screenScales.length - 1 ) - maxScreenScaleIndex++; - else if ( rendertime < targetRenderNanos / 3 && maxScreenScaleIndex > 0 ) - maxScreenScaleIndex--; - } - else if ( currentScreenScaleIndex == maxScreenScaleIndex - 1 ) - { - if ( rendertime < targetRenderNanos && maxScreenScaleIndex > 0 ) - maxScreenScaleIndex--; - } -// System.out.println( String.format( "rendering:%4d ms", rendertime / 1000000 ) ); -// System.out.println( "scale = " + currentScreenScaleIndex ); -// System.out.println( "maxScreenScaleIndex = " + maxScreenScaleIndex + " (" + screenImages[ maxScreenScaleIndex ][ 0 ].dimension( 0 ) + " x " + screenImages[ maxScreenScaleIndex ][ 0 ].dimension( 1 ) + ")" ); - } + if ( createProjector ) + recordRenderTime( intervalResult, rendertime ); - if ( currentScreenScaleIndex > 0 ) - requestRepaint( currentScreenScaleIndex - 1 ); - else if ( !p.isValid() ) + if ( currentIntervalScaleIndex > currentScreenScaleIndex ) + iterateRepaintInterval( currentIntervalScaleIndex - 1 ); + else if ( p.isValid() ) { - try + // if full frame rendering was not yet complete + if ( requestedScreenScaleIndex >= 0 ) { - Thread.sleep( 1 ); + // go back to full frame rendering + intervalMode = false; + if ( requestedScreenScaleIndex == currentScreenScaleIndex ) + ++currentScreenScaleIndex; + painterThread.requestRepaint(); } - catch ( final InterruptedException e ) - { - // restore interrupted state - Thread.currentThread().interrupt(); - } - requestRepaint( currentScreenScaleIndex ); } + else + iterateRepaintInterval( currentIntervalScaleIndex ); } + // if rendering was cancelled... + else + intervalRenderData.reRequest(); } return success; } - /** - * Request a repaint of the display from the painter thread, with maximum - * screen scale index and mipmap level. - */ - public synchronized void requestRepaint() + private void recordRenderTime( final RenderResult result, final long renderNanos ) { - newFrameRequest = true; - requestRepaint( maxScreenScaleIndex ); + final int numSources = currentVisibleSourcesOnScreen.size(); + final int numRenderPixels = ( int ) Intervals.numElements( result.getTargetImage() ) * numSources; + if ( numRenderPixels >= 4096 ) + renderNanosPerPixelAndSource.add( renderNanos / ( double ) numRenderPixels ); } /** - * Request a repaint of the display from the painter thread. The painter - * thread will trigger a {@link #paint(ViewerState)} as soon as possible (that is, - * immediately or after the currently running {@link #paint(ViewerState)} has - * completed). + * Request iterated repaint at the specified {@code screenScaleIndex}. This + * is used to repaint the {@code currentViewerState} in a loop, until + * everything is painted at highest resolution from valid data (or until + * painting is interrupted by a new request}. */ - public synchronized void requestRepaint( final int screenScaleIndex ) + private void iterateRepaint( final int screenScaleIndex ) { - if ( renderingMayBeCancelled && projector != null ) - projector.cancel(); - if ( screenScaleIndex > requestedScreenScaleIndex ) - requestedScreenScaleIndex = screenScaleIndex; + if ( screenScaleIndex == currentScreenScaleIndex ) + usleep(); + requestedScreenScaleIndex = screenScaleIndex; painterThread.requestRepaint(); } /** - * DON'T USE THIS. - * <p> - * This is a work around for JDK bug - * https://bugs.openjdk.java.net/browse/JDK-8029147 which leads to - * ViewerPanel not being garbage-collected when ViewerFrame is closed. So - * instead we need to manually let go of resources... + * Request iterated repaint at the specified {@code intervalScaleIndex}. + * This is used to repaint the current interval in a loop, until everything + * is painted at highest resolution from valid data (or until painting is + * interrupted by a new request}. */ - public void kill() - { - if ( display instanceof TransformAwareBufferedImageOverlayRenderer ) - ( ( TransformAwareBufferedImageOverlayRenderer ) display ).kill(); - projector = null; - renderIdQueue.clear(); - bufferedImageToRenderId.clear(); - for ( int i = 0; i < renderImages.length; ++i ) - renderImages[ i ] = null; - for ( int i = 0; i < renderMaskArrays.length; ++i ) - renderMaskArrays[ i ] = null; - for ( int i = 0; i < screenImages.length; ++i ) - screenImages[ i ] = null; - for ( int i = 0; i < bufferedImages.length; ++i ) - bufferedImages[ i ] = null; - } - - private VolatileProjector createProjector( - final ViewerState viewerState, - final ARGBScreenImage screenImage ) + private void iterateRepaintInterval( final int intervalScaleIndex ) { - /* - * This shouldn't be necessary, with - * CacheHints.LoadingStrategy==VOLATILE - */ -// CacheIoTiming.getIoTimeBudget().clear(); // clear time budget such that prefetching doesn't wait for loading blocks. - final List< SourceState< ? > > sourceStates = viewerState.getSources(); - final List< Integer > visibleSourceIndices = viewerState.getVisibleSourceIndices(); - VolatileProjector projector; - if ( visibleSourceIndices.isEmpty() ) - projector = new EmptyProjector<>( screenImage ); - else if ( visibleSourceIndices.size() == 1 ) - { - final int i = visibleSourceIndices.get( 0 ); - projector = createSingleSourceProjector( viewerState, sourceStates.get( i ), i, currentScreenScaleIndex, screenImage, renderMaskArrays[ 0 ] ); - } - else + if ( intervalScaleIndex == currentIntervalScaleIndex ) { - final ArrayList< VolatileProjector > sourceProjectors = new ArrayList<>(); - final ArrayList< ARGBScreenImage > sourceImages = new ArrayList<>(); - final ArrayList< Source< ? > > sources = new ArrayList<>(); - int j = 0; - for ( final int i : visibleSourceIndices ) - { - final ARGBScreenImage renderImage = renderImages[ currentScreenScaleIndex ][ j ]; - final byte[] maskArray = renderMaskArrays[ j ]; - ++j; - final VolatileProjector p = createSingleSourceProjector( - viewerState, sourceStates.get( i ), i, currentScreenScaleIndex, - renderImage, maskArray ); - sourceProjectors.add( p ); - sources.add( sourceStates.get( i ).getSpimSource() ); - sourceImages.add( renderImage ); - } - projector = accumulateProjectorFactory.createAccumulateProjector( sourceProjectors, sources, sourceImages, screenImage, numRenderingThreads, renderingExecutorService ); + intervalRenderData.reRequest(); + usleep(); } - previousTimepoint = viewerState.getCurrentTimepoint(); - viewerState.getViewerTransform( currentProjectorTransform ); - CacheIoTiming.getIoTimeBudget().reset( iobudget ); - return projector; + requestedIntervalScaleIndex = intervalScaleIndex; + painterThread.requestRepaint(); } - private static class SimpleVolatileProjector< A, B > extends SimpleInterruptibleProjector< A, B > implements VolatileProjector + /** + * Wait for 1ms so that fetcher threads get a chance to do work. + */ + private void usleep() { - private boolean valid = false; - - public SimpleVolatileProjector( - final RandomAccessible< A > source, - final Converter< ? super A, B > converter, - final RandomAccessibleInterval< B > target, - final int numThreads, - final ExecutorService executorService ) - { - super( source, converter, target, numThreads, executorService ); - } - - @Override - public boolean map( final boolean clearUntouchedTargetPixels ) + try { - final boolean success = super.map(); - valid |= success; - return success; + Thread.sleep( 1 ); } - - @Override - public boolean isValid() - { - return valid; - } - } - - private < T > VolatileProjector createSingleSourceProjector( - final ViewerState viewerState, - final SourceState< T > source, - final int sourceIndex, - final int screenScaleIndex, - final ARGBScreenImage screenImage, - final byte[] maskArray ) - { - if ( useVolatileIfAvailable ) + catch ( final InterruptedException e ) { - if ( source.asVolatile() != null ) - return createSingleSourceVolatileProjector( viewerState, source.asVolatile(), sourceIndex, screenScaleIndex, screenImage, maskArray ); - else if ( source.getSpimSource().getType() instanceof Volatile ) - { - @SuppressWarnings( "unchecked" ) - final SourceState< ? extends Volatile< ? > > vsource = ( SourceState< ? extends Volatile< ? > > ) source; - return createSingleSourceVolatileProjector( viewerState, vsource, sourceIndex, screenScaleIndex, screenImage, maskArray ); - } + // restore interrupted state + Thread.currentThread().interrupt(); } - - final AffineTransform3D screenScaleTransform = screenScaleTransforms[ currentScreenScaleIndex ]; - final int bestLevel = viewerState.getBestMipMapLevel( screenScaleTransform, sourceIndex ); - return new SimpleVolatileProjector<>( - getTransformedSource( viewerState, source.getSpimSource(), screenScaleTransform, bestLevel, null ), - source.getConverter(), screenImage, numRenderingThreads, renderingExecutorService ); } - private < T extends Volatile< ? > > VolatileProjector createSingleSourceVolatileProjector( + private VolatileProjector createProjector( final ViewerState viewerState, - final SourceState< T > source, - final int sourceIndex, + final List< SourceAndConverter< ? > > visibleSourcesOnScreen, final int screenScaleIndex, - final ARGBScreenImage screenImage, - final byte[] maskArray ) + final RandomAccessibleInterval< ARGBType > screenImage, + final int offsetX, + final int offsetY ) { - final AffineTransform3D screenScaleTransform = screenScaleTransforms[ currentScreenScaleIndex ]; - final ArrayList< RandomAccessible< T > > renderList = new ArrayList<>(); - final Source< T > spimSource = source.getSpimSource(); - final int t = viewerState.getCurrentTimepoint(); - - final MipmapOrdering ordering = MipmapOrdering.class.isInstance( spimSource ) ? - ( MipmapOrdering ) spimSource : new DefaultMipmapOrdering( spimSource ); - - final AffineTransform3D screenTransform = new AffineTransform3D(); - viewerState.getViewerTransform( screenTransform ); + final AffineTransform3D screenScaleTransform = screenScales.get( screenScaleIndex ).scaleTransform(); + final AffineTransform3D screenTransform = viewerState.getViewerTransform(); screenTransform.preConcatenate( screenScaleTransform ); - final MipmapHints hints = ordering.getMipmapHints( screenTransform, t, previousTimepoint ); - final List< Level > levels = hints.getLevels(); - - if ( prefetchCells ) - { - Collections.sort( levels, MipmapOrdering.prefetchOrderComparator ); - for ( final Level l : levels ) - { - final CacheHints cacheHints = l.getPrefetchCacheHints(); - if ( cacheHints == null || cacheHints.getLoadingStrategy() != LoadingStrategy.DONTLOAD ) - prefetch( viewerState, spimSource, screenScaleTransform, l.getMipmapLevel(), cacheHints, screenImage ); - } - } - - Collections.sort( levels, MipmapOrdering.renderOrderComparator ); - for ( final Level l : levels ) - renderList.add( getTransformedSource( viewerState, spimSource, screenScaleTransform, l.getMipmapLevel(), l.getRenderCacheHints() ) ); - - if ( hints.renewHintsAfterPaintingOnce() ) - newFrameRequest = true; - - return new VolatileHierarchyProjector<>( renderList, source.getConverter(), screenImage, maskArray, numRenderingThreads, renderingExecutorService ); - } - - private static < T > RandomAccessible< T > getTransformedSource( - final ViewerState viewerState, - final Source< T > source, - final AffineTransform3D screenScaleTransform, - final int mipmapIndex, - final CacheHints cacheHints ) - { - final int timepoint = viewerState.getCurrentTimepoint(); - - final RandomAccessibleInterval< T > img = source.getSource( timepoint, mipmapIndex ); - if ( VolatileCachedCellImg.class.isInstance( img ) ) - ( ( VolatileCachedCellImg< ?, ? > ) img ).setCacheHints( cacheHints ); - - final Interpolation interpolation = viewerState.getInterpolation(); - final RealRandomAccessible< T > ipimg = source.getInterpolatedSource( timepoint, mipmapIndex, interpolation ); - - final AffineTransform3D sourceToScreen = new AffineTransform3D(); - viewerState.getViewerTransform( sourceToScreen ); - final AffineTransform3D sourceTransform = new AffineTransform3D(); - source.getSourceTransform( timepoint, mipmapIndex, sourceTransform ); - sourceToScreen.concatenate( sourceTransform ); - sourceToScreen.preConcatenate( screenScaleTransform ); - - return RealViews.affine( ipimg, sourceToScreen ); - } - - private static < T > void prefetch( - final ViewerState viewerState, - final Source< T > source, - final AffineTransform3D screenScaleTransform, - final int mipmapIndex, - final CacheHints prefetchCacheHints, - final Dimensions screenInterval ) - { - final int timepoint = viewerState.getCurrentTimepoint(); - final RandomAccessibleInterval< T > img = source.getSource( timepoint, mipmapIndex ); - if ( VolatileCachedCellImg.class.isInstance( img ) ) - { - final VolatileCachedCellImg< ?, ? > cellImg = ( VolatileCachedCellImg< ?, ? > ) img; - - CacheHints hints = prefetchCacheHints; - if ( hints == null ) - { - final CacheHints d = cellImg.getDefaultCacheHints(); - hints = new CacheHints( LoadingStrategy.VOLATILE, d.getQueuePriority(), false ); - } - cellImg.setCacheHints( hints ); - final int[] cellDimensions = new int[ 3 ]; - cellImg.getCellGrid().cellDimensions( cellDimensions ); - final long[] dimensions = new long[ 3 ]; - cellImg.dimensions( dimensions ); - final RandomAccess< ? > cellsRandomAccess = cellImg.getCells().randomAccess(); - - final Interpolation interpolation = viewerState.getInterpolation(); - - final AffineTransform3D sourceToScreen = new AffineTransform3D(); - viewerState.getViewerTransform( sourceToScreen ); - final AffineTransform3D sourceTransform = new AffineTransform3D(); - source.getSourceTransform( timepoint, mipmapIndex, sourceTransform ); - sourceToScreen.concatenate( sourceTransform ); - sourceToScreen.preConcatenate( screenScaleTransform ); - - Prefetcher.fetchCells( sourceToScreen, cellDimensions, dimensions, screenInterval, interpolation, cellsRandomAccess ); - } - } - - private static TransformAwareRenderTarget wrapTransformAwareRenderTarget( final RenderTarget t ) - { - if ( t instanceof TransformAwareRenderTarget ) - return ( TransformAwareRenderTarget ) t; - else - return new TransformAwareRenderTarget() - { - @Override - public BufferedImage setBufferedImage( final BufferedImage img ) - { - return t.setBufferedImage( img ); - } - - @Override - public int getWidth() - { - return t.getWidth(); - } - - @Override - public int getHeight() - { - return t.getHeight(); - } - - @Override - public BufferedImage setBufferedImageAndTransform( final BufferedImage img, final AffineTransform3D transform ) - { - return t.setBufferedImage( img ); - } - - @Override - public void removeTransformListener( final TransformListener< AffineTransform3D > listener ) - {} - - @Override - public void addTransformListener( final TransformListener< AffineTransform3D > listener, final int index ) - {} - - @Override - public void addTransformListener( final TransformListener< AffineTransform3D > listener ) - {} - }; + screenTransform.translate( -offsetX, -offsetY, 0 ); + + final VolatileProjector projector = projectorFactory.createProjector( + viewerState, + visibleSourcesOnScreen, + screenImage, + screenTransform, + renderStorage ); + CacheIoTiming.getIoTimeBudget().reset( iobudget ); + return projector; } } diff --git a/src/main/java/bdv/viewer/render/PainterThread.java b/src/main/java/bdv/viewer/render/PainterThread.java new file mode 100644 index 0000000000000000000000000000000000000000..4c4083e3f3ce74fa4abe31564b2c357f2ca68349 --- /dev/null +++ b/src/main/java/bdv/viewer/render/PainterThread.java @@ -0,0 +1,118 @@ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer.render; + +import bdv.viewer.RequestRepaint; +import java.util.concurrent.RejectedExecutionException; + +/** + * Thread to repaint display. + */ +public class PainterThread extends Thread implements RequestRepaint +{ + public interface Paintable + { + /** + * This is called by the painter thread to repaint the display. + */ + void paint(); + } + + private final Paintable paintable; + + private boolean pleaseRepaint; + + public PainterThread( final Paintable paintable ) + { + this( null, "PainterThread", paintable ); + } + + public PainterThread( final ThreadGroup group, final Paintable paintable ) + { + this( group, "PainterThread", paintable ); + } + + public PainterThread( final ThreadGroup group, final String name, final Paintable paintable ) + { + super( group, name ); + this.paintable = paintable; + this.pleaseRepaint = false; + } + + @Override + public void run() + { + while ( !isInterrupted() ) + { + final boolean b; + synchronized ( this ) + { + b = pleaseRepaint; + pleaseRepaint = false; + } + if ( b ) + try + { + paintable.paint(); + } + catch ( final RejectedExecutionException e ) + { + // this happens when the rendering threadpool + // is killed before the painter thread. + } + synchronized ( this ) + { + try + { + if ( !pleaseRepaint ) + wait(); + } + catch ( final InterruptedException e ) + { + break; + } + } + } + } + + /** + * Request repaint. This will trigger a call to {@link Paintable#paint()} + * from the {@link PainterThread}. + */ + @Override + public void requestRepaint() + { + synchronized ( this ) + { +// DebugHelpers.printStackTrace( 15 ); + pleaseRepaint = true; + notify(); + } + } +} diff --git a/src/main/java/bdv/viewer/render/Prefetcher.java b/src/main/java/bdv/viewer/render/Prefetcher.java index 6153ca1a060855923275d5813ffc15bcbf0529c5..16a02f068c77fdcc9486276dfe9f049158df6974 100644 --- a/src/main/java/bdv/viewer/render/Prefetcher.java +++ b/src/main/java/bdv/viewer/render/Prefetcher.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/bdv/viewer/render/ProjectorFactory.java b/src/main/java/bdv/viewer/render/ProjectorFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..c262a7c515a060a36375bfa28b75b70a29ee1e40 --- /dev/null +++ b/src/main/java/bdv/viewer/render/ProjectorFactory.java @@ -0,0 +1,336 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer.render; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; + +import net.imglib2.Dimensions; +import net.imglib2.RandomAccess; +import net.imglib2.RandomAccessible; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.RealRandomAccessible; +import net.imglib2.Volatile; +import net.imglib2.cache.volatiles.CacheHints; +import net.imglib2.cache.volatiles.LoadingStrategy; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.realtransform.RealViews; +import net.imglib2.type.numeric.ARGBType; + +import bdv.img.cache.VolatileCachedCellImg; +import bdv.util.MipmapTransforms; +import bdv.viewer.Interpolation; +import bdv.viewer.Source; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.ViewerState; + +/** + * Creates projectors for rendering a given {@code ViewerState} to a given + * {@code screenImage}, with the current visible sources and timepoint of the + * {@code ViewerState}, and a given {@code screenTransform} from global + * coordinates to coordinates in the {@code screenImage}. + */ +class ProjectorFactory +{ + /** + * How many threads to use for rendering. + */ + private final int numRenderingThreads; + + /** + * {@link ExecutorService} used for rendering. + */ + private final ExecutorService renderingExecutorService; + + /** + * Whether volatile versions of sources should be used if available. + */ + private final boolean useVolatileIfAvailable; + + /** + * Constructs projector that combines rendere ARGB images for individual + * sources to the final screen image. This can be used to customize how + * sources are combined. + */ + private final AccumulateProjectorFactory< ARGBType > accumulateProjectorFactory; + + /** + * Whether repainting should be triggered after the previously + * {@link #createProjector constructed} projector returns an incomplete + * result. + * <p> + * The use case for this is the following: + * <p> + * When scrolling through time, we often get frames for which no data was + * loaded yet. To speed up rendering in these cases, use only two mipmap + * levels: the optimal and the coarsest. By doing this, we require at most + * two passes over the image at the expense of ignoring data present in + * intermediate mipmap levels. The assumption is, that we will either be + * moving back and forth between images that have all data present already + * or that we move to a new image with no data present at all. + * <p> + * However, we only want this two-pass rendering to happen once, then switch + * to normal multi-pass rendering if we remain longer on the same frame. + * <p> + * (This method is implemented by {@link DefaultMipmapOrdering}.) + */ + private boolean newFrameRequest; + + /** + * The timepoint for which last a projector was {@link #createProjector + * created}. + */ + private int previousTimepoint = -1; + + // TODO: should be settable + private final boolean prefetchCells = true; + + /** + * @param numRenderingThreads + * How many threads to use for rendering. + * @param renderingExecutorService + * if non-null, this is used for rendering. Note, that it is still + * important to supply the numRenderingThreads parameter, because that + * is used to determine into how many sub-tasks rendering is split. + * @param useVolatileIfAvailable + * whether volatile versions of sources should be used if available. + * @param accumulateProjectorFactory + * can be used to customize how sources are combined. + */ + public ProjectorFactory( + final int numRenderingThreads, + final ExecutorService renderingExecutorService, + final boolean useVolatileIfAvailable, + final AccumulateProjectorFactory< ARGBType > accumulateProjectorFactory ) + { + this.numRenderingThreads = numRenderingThreads; + this.renderingExecutorService = renderingExecutorService; + this.useVolatileIfAvailable = useVolatileIfAvailable; + this.accumulateProjectorFactory = accumulateProjectorFactory; + } + + /** + * Create a projector for rendering the specified {@code ViewerState} to the + * specified {@code screenImage}, with the current visible sources (visible + * in {@code ViewerState} and actually currently visible on screen) and + * timepoint of the {@code ViewerState}, and the specified + * {@code screenTransform} from global coordinates to coordinates in the + * {@code screenImage}. + */ + public VolatileProjector createProjector( + final ViewerState viewerState, + final List< SourceAndConverter< ? > > visibleSourcesOnScreen, + final RandomAccessibleInterval< ARGBType > screenImage, + final AffineTransform3D screenTransform, + final RenderStorage renderStorage ) + { + /* + * This shouldn't be necessary, with + * CacheHints.LoadingStrategy==VOLATILE + */ +// CacheIoTiming.getIoTimeBudget().clear(); // clear time budget such that prefetching doesn't wait for loading blocks. + newFrameRequest = false; + + final int width = ( int ) screenImage.dimension( 0 ); + final int height = ( int ) screenImage.dimension( 1 ); + + VolatileProjector projector; + if ( visibleSourcesOnScreen.isEmpty() ) + projector = new EmptyProjector<>( screenImage ); + else if ( visibleSourcesOnScreen.size() == 1 ) + { + final byte[] maskArray = renderStorage.getMaskArray( 0 ); + projector = createSingleSourceProjector( viewerState, visibleSourcesOnScreen.get( 0 ), screenImage, screenTransform, maskArray ); + } + else + { + final ArrayList< VolatileProjector > sourceProjectors = new ArrayList<>(); + final ArrayList< RandomAccessibleInterval< ARGBType > > sourceImages = new ArrayList<>(); + int j = 0; + for ( final SourceAndConverter< ? > source : visibleSourcesOnScreen ) + { + final RandomAccessibleInterval< ARGBType > renderImage = renderStorage.getRenderImage( width, height, j ); + final byte[] maskArray = renderStorage.getMaskArray( j ); + ++j; + final VolatileProjector p = createSingleSourceProjector( viewerState, source, renderImage, screenTransform, maskArray ); + sourceProjectors.add( p ); + sourceImages.add( renderImage ); + } + projector = accumulateProjectorFactory.createProjector( sourceProjectors, visibleSourcesOnScreen, sourceImages, screenImage, numRenderingThreads, renderingExecutorService ); + } + previousTimepoint = viewerState.getCurrentTimepoint(); + return projector; + } + + private < T > VolatileProjector createSingleSourceProjector( + final ViewerState viewerState, + final SourceAndConverter< T > source, + final RandomAccessibleInterval< ARGBType > screenImage, + final AffineTransform3D screenTransform, + final byte[] maskArray ) + { + if ( useVolatileIfAvailable ) + { + if ( source.asVolatile() != null ) + return createSingleSourceVolatileProjector( viewerState, source.asVolatile(), screenImage, screenTransform, maskArray ); + else if ( source.getSpimSource().getType() instanceof Volatile ) + { + @SuppressWarnings( "unchecked" ) + final SourceAndConverter< ? extends Volatile< ? > > vsource = ( SourceAndConverter< ? extends Volatile< ? > > ) source; + return createSingleSourceVolatileProjector( viewerState, vsource, screenImage, screenTransform, maskArray ); + } + } + + final int bestLevel = getBestMipMapLevel( viewerState, source, screenTransform ); + return new SimpleVolatileProjector<>( + getTransformedSource( viewerState, source.getSpimSource(), screenTransform, bestLevel, null ), + source.getConverter(), screenImage, numRenderingThreads, renderingExecutorService ); + } + + private < T extends Volatile< ? > > VolatileProjector createSingleSourceVolatileProjector( + final ViewerState viewerState, + final SourceAndConverter< T > source, + final RandomAccessibleInterval< ARGBType > screenImage, + final AffineTransform3D screenTransform, + final byte[] maskArray ) + { + final ArrayList< RandomAccessible< T > > renderList = new ArrayList<>(); + final Source< T > spimSource = source.getSpimSource(); + final int t = viewerState.getCurrentTimepoint(); + + final MipmapOrdering ordering = spimSource instanceof MipmapOrdering ? + ( MipmapOrdering ) spimSource : new DefaultMipmapOrdering( spimSource ); + + final MipmapOrdering.MipmapHints hints = ordering.getMipmapHints( screenTransform, t, previousTimepoint ); + final List< MipmapOrdering.Level > levels = hints.getLevels(); + + if ( prefetchCells ) + { + levels.sort( MipmapOrdering.prefetchOrderComparator ); + for ( final MipmapOrdering.Level l : levels ) + { + final CacheHints cacheHints = l.getPrefetchCacheHints(); + if ( cacheHints == null || cacheHints.getLoadingStrategy() != LoadingStrategy.DONTLOAD ) + prefetch( viewerState, spimSource, screenTransform, l.getMipmapLevel(), cacheHints, screenImage ); + } + } + + levels.sort( MipmapOrdering.renderOrderComparator ); + for ( final MipmapOrdering.Level l : levels ) + renderList.add( getTransformedSource( viewerState, spimSource, screenTransform, l.getMipmapLevel(), l.getRenderCacheHints() ) ); + + if ( hints.renewHintsAfterPaintingOnce() ) + newFrameRequest = true; + + return new VolatileHierarchyProjector<>( renderList, source.getConverter(), screenImage, maskArray, numRenderingThreads, renderingExecutorService ); + } + + /** + * Get the mipmap level that best matches the given screen scale for the + * given source. + * + * @param screenTransform + * transforms screen image coordinates to global coordinates. + * + * @return mipmap level + */ + private static int getBestMipMapLevel( + final ViewerState viewerState, + final SourceAndConverter< ? > source, + final AffineTransform3D screenTransform ) + { + return MipmapTransforms.getBestMipMapLevel( screenTransform, source.getSpimSource(), viewerState.getCurrentTimepoint() ); + } + + private static < T > RandomAccessible< T > getTransformedSource( + final ViewerState viewerState, + final Source< T > source, + final AffineTransform3D screenTransform, + final int mipmapIndex, + final CacheHints cacheHints ) + { + final int timepoint = viewerState.getCurrentTimepoint(); + + final RandomAccessibleInterval< T > img = source.getSource( timepoint, mipmapIndex ); + if ( img instanceof VolatileCachedCellImg ) + ( ( VolatileCachedCellImg< ?, ? > ) img ).setCacheHints( cacheHints ); + + final Interpolation interpolation = viewerState.getInterpolation(); + final RealRandomAccessible< T > ipimg = source.getInterpolatedSource( timepoint, mipmapIndex, interpolation ); + + final AffineTransform3D sourceToScreen = new AffineTransform3D(); + source.getSourceTransform( timepoint, mipmapIndex, sourceToScreen ); + sourceToScreen.preConcatenate( screenTransform ); + + return RealViews.affine( ipimg, sourceToScreen ); + } + + private static < T > void prefetch( + final ViewerState viewerState, + final Source< T > source, + final AffineTransform3D screenTransform, + final int mipmapIndex, + final CacheHints prefetchCacheHints, + final Dimensions screenInterval ) + { + final int timepoint = viewerState.getCurrentTimepoint(); + final RandomAccessibleInterval< T > img = source.getSource( timepoint, mipmapIndex ); + if ( img instanceof VolatileCachedCellImg ) + { + final VolatileCachedCellImg< ?, ? > cellImg = ( VolatileCachedCellImg< ?, ? > ) img; + + CacheHints hints = prefetchCacheHints; + if ( hints == null ) + { + final CacheHints d = cellImg.getDefaultCacheHints(); + hints = new CacheHints( LoadingStrategy.VOLATILE, d.getQueuePriority(), false ); + } + cellImg.setCacheHints( hints ); + final int[] cellDimensions = new int[ 3 ]; + cellImg.getCellGrid().cellDimensions( cellDimensions ); + final long[] dimensions = new long[ 3 ]; + cellImg.dimensions( dimensions ); + final RandomAccess< ? > cellsRandomAccess = cellImg.getCells().randomAccess(); + + final Interpolation interpolation = viewerState.getInterpolation(); + + final AffineTransform3D sourceToScreen = new AffineTransform3D(); + source.getSourceTransform( timepoint, mipmapIndex, sourceToScreen ); + sourceToScreen.preConcatenate( screenTransform ); + + Prefetcher.fetchCells( sourceToScreen, cellDimensions, dimensions, screenInterval, interpolation, cellsRandomAccess ); + } + } + + public boolean requestNewFrameIfIncomplete() + { + return newFrameRequest; + } +} diff --git a/src/main/java/bdv/viewer/render/ProjectorUtils.java b/src/main/java/bdv/viewer/render/ProjectorUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..12a96dc9728ebe620f1a7feedfad214ed5f0734d --- /dev/null +++ b/src/main/java/bdv/viewer/render/ProjectorUtils.java @@ -0,0 +1,58 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer.render; + +import net.imglib2.RandomAccessible; +import net.imglib2.img.array.ArrayImg; +import net.imglib2.img.basictypeaccess.array.IntArray; +import net.imglib2.type.numeric.ARGBType; + +public class ProjectorUtils +{ + /** + * Extracts the underlying {@code int[]} array in case {@code img} is a + * standard {@code ArrayImg<ARGBType>}. This supports certain (optional) + * optimizations in projector implementations. + * + * @return the underlying {@code int[]} array of {@code img}, if it is a + * standard {@code ArrayImg<ARGBType>}. Otherwise {@code null}. + */ + public static int[] getARGBArrayImgData( final RandomAccessible< ? > img ) + { + if ( ! ( img instanceof ArrayImg ) ) + return null; + final ArrayImg< ?, ? > aimg = ( ArrayImg< ?, ? > ) img; + if( ! ( aimg.firstElement() instanceof ARGBType ) ) + return null; + final Object access = aimg.update( null ); + if ( ! ( access instanceof IntArray ) ) + return null; + return ( ( IntArray ) access ).getCurrentStorageArray(); + } +} diff --git a/src/main/java/bdv/viewer/render/RenderResult.java b/src/main/java/bdv/viewer/render/RenderResult.java new file mode 100644 index 0000000000000000000000000000000000000000..6957817fb65bd4bdb45c98e8e192fd2db9959f5c --- /dev/null +++ b/src/main/java/bdv/viewer/render/RenderResult.java @@ -0,0 +1,98 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer.render; + +import net.imglib2.Interval; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.ARGBType; + +/** + * Provides the {@link MultiResolutionRenderer renderer} with a target image + * ({@code RandomAccessibleInterval<ARGBType>}) to render to. Provides the + * {@link RenderTarget} with the rendered image and transform etc necessary to + * display it. + */ +public interface RenderResult +{ + /** + * Allocate storage such that {@link #getTargetImage()} holds an image of + * {@code width * height}. + * <p> + * (Called by the {@link MultiResolutionRenderer renderer}.) + */ + void init( int width, int height ); + + /** + * Get the image to render to. + * <p> + * (Called by the {@link MultiResolutionRenderer renderer}.) + * + * @return the image to render to + */ + // TODO: rename getTargetImage() ??? + RandomAccessibleInterval< ARGBType > getTargetImage(); + + /** + * Get the viewer transform used to render image. This is with respect to + * the screen resolution (doesn't include scaling). + * <p> + * (Called by the {@link MultiResolutionRenderer renderer} to set the + * transform.) + */ + AffineTransform3D getViewerTransform(); + + /** + * Get the scale factor from target coordinates to screen resolution. + */ + double getScaleFactor(); + + /** + * Set the scale factor from target coordinates to screen resolution. + */ + void setScaleFactor( double scaleFactor ); + + /** + * Fill in {@code interval} with data from {@code patch}, scaled by the + * relative scale between this {@code RenderResult} and {@code patch}, and + * shifted such that {@code (0,0)} of the {@code patch} is placed at + * {@code (ox,oy)} of this {@code RenderResult} + * <p> + * Note that only data in {@code interval} will be modified, although the + * scaled and shifted {@code patch} might fall partially outside. + */ + void patch( final RenderResult patch, final Interval interval, final double ox, final double oy ); + + /** + * Notify that the {@link #getTargetImage() target image} data was changed. + * <p> + * (Called by the {@link MultiResolutionRenderer renderer}.) + */ + void setUpdated(); +} diff --git a/src/main/java/bdv/viewer/render/RenderStorage.java b/src/main/java/bdv/viewer/render/RenderStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..edccd32fe4785d1030ce883b5ece314e876b1222 --- /dev/null +++ b/src/main/java/bdv/viewer/render/RenderStorage.java @@ -0,0 +1,92 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer.render; + +import java.util.ArrayList; +import java.util.List; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.array.ArrayImgs; +import net.imglib2.type.numeric.ARGBType; + +/** + * Maintains {@code byte[]} and {@code int[]} arrays for mask and intermediate images needed for rendering. + * <p> + * Call {@link #checkRenewData} to update number and size of arrays when number of visible sources or screen size changes. + */ +class RenderStorage +{ + /** + * Storage for mask images of {@link VolatileHierarchyProjector}. One array + * per visible source. + */ + private final List< byte[] > renderMaskArrays = new ArrayList<>(); + + /** + * Storage for render images of {@link VolatileHierarchyProjector}. + * Used to render an individual source before combining to final target image. + * One array per visible source, if more than one source is visible. + * (If exactly one source is visible, it is rendered directly to the target image.) + */ + private final List< int[] > renderImageArrays = new ArrayList<>(); + + public void checkRenewData( final int screenW, final int screenH, final int numVisibleSources ) + { + final int size = screenW * screenH; + final int currentSize = renderMaskArrays.isEmpty() ? 0 : renderMaskArrays.get( 0 ).length; + if ( size != currentSize ) + clear(); + + while ( renderMaskArrays.size() > numVisibleSources ) + renderMaskArrays.remove( renderMaskArrays.size() - 1 ); + while ( renderMaskArrays.size() < numVisibleSources ) + renderMaskArrays.add( new byte[ size ] ); + + final int numRenderImages = numVisibleSources > 1 ? numVisibleSources : 0; + while ( renderImageArrays.size() > numRenderImages ) + renderImageArrays.remove( renderImageArrays.size() - 1 ); + while ( renderImageArrays.size() < numRenderImages ) + renderImageArrays.add( new int[ size ] ); + } + + public byte[] getMaskArray( final int index ) + { + return renderMaskArrays.get( index ); + } + + public RandomAccessibleInterval< ARGBType > getRenderImage( final int width, final int height, final int index ) + { + return ArrayImgs.argbs( renderImageArrays.get( index ), width, height ); + } + + public void clear() + { + renderMaskArrays.clear(); + renderImageArrays.clear(); + } +} diff --git a/src/main/java/bdv/viewer/render/RenderTarget.java b/src/main/java/bdv/viewer/render/RenderTarget.java new file mode 100644 index 0000000000000000000000000000000000000000..4151fbc6168c2a73382718e4992fd8c974cae917 --- /dev/null +++ b/src/main/java/bdv/viewer/render/RenderTarget.java @@ -0,0 +1,78 @@ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer.render; + +import bdv.viewer.render.RenderResult; + +/** + * Receiver for a rendered image (to be drawn onto a canvas later). + * <p> + * A renderer will render source data into a {@link RenderResult} and + * provide this to the {@code RenderTarget}. + * <p> + * See {@code BufferedImageOverlayRenderer}, which is both a {@code RenderTarget} and + * an {@code OverlayRenderer} that draws the {@code RenderResult}. + * + * @author Tobias Pietzsch + */ +public interface RenderTarget< R extends RenderResult > +{ + /** + * Returns a {@code RenderResult} for rendering to. + * This may be a new {@code RenderResult} or a previously {@link #setRenderResult set RenderResult} + * that is no longer needed for display. + * Note that consecutive {@code getReusableRenderResult()} calls without intermediate + * {@code setRenderResult()} may return the same {@code RenderResult}. + */ + R getReusableRenderResult(); + + /** + * Returns a new {@code RenderResult}. + */ + R createRenderResult(); + + /** + * Set the {@link RenderResult} that is to be drawn on the canvas. + */ + void setRenderResult( R renderResult ); + + /** + * Get the current canvas width. + * + * @return canvas width. + */ + int getWidth(); + + /** + * Get the current canvas height. + * + * @return canvas height. + */ + int getHeight(); +} diff --git a/src/main/java/bdv/viewer/render/ScreenScales.java b/src/main/java/bdv/viewer/render/ScreenScales.java new file mode 100644 index 0000000000000000000000000000000000000000..eb19226070a885aea25b33fb79215da1e64ef27b --- /dev/null +++ b/src/main/java/bdv/viewer/render/ScreenScales.java @@ -0,0 +1,365 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer.render; + +import java.util.ArrayList; +import java.util.List; + +import net.imglib2.Interval; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.util.Intervals; + +/** + * Maintains current sizes and transforms at every screen scale level. Records + * interval rendering requests. Suggests full frame or interval scale to render + * in order to meet a specified target rendering time in nanoseconds. + */ +class ScreenScales +{ + /** + * Target rendering time in nanoseconds. The rendering time for the coarsest + * rendered scale should be below this threshold. After the coarsest scale, + * increasingly finer scales are rendered, but these render passes may be + * canceled (while the coarsest may not). + */ + private final double targetRenderNanos; + + private final List< ScreenScale > screenScales; + + private int screenW = 0; + + private int screenH = 0; + + /** + * @param screenScaleFactors + * Scale factors from the viewer canvas to screen images of different + * resolutions. A scale factor of 1 means 1 pixel in the screen image is + * displayed as 1 pixel on the canvas, a scale factor of 0.5 means 1 + * pixel in the screen image is displayed as 2 pixel on the canvas, etc. + * @param targetRenderNanos + * Target rendering time in nanoseconds. The rendering time for the + * coarsest rendered scale should be below this threshold. + */ + public ScreenScales( final double[] screenScaleFactors, final double targetRenderNanos ) + { + this.targetRenderNanos = targetRenderNanos; + screenScales = new ArrayList<>(); + for ( final double scale : screenScaleFactors ) + screenScales.add( new ScreenScale( scale ) ); + } + + /** + * Check whether the screen size was changed and resize {@link #screenScales} accordingly. + * + * @return whether the size was changed. + */ + public boolean checkResize( final int newScreenW, final int newScreenH ) + { + if ( newScreenW != screenW || newScreenH != screenH ) + { + screenW = newScreenW; + screenH = newScreenH; + screenScales.forEach( s -> s.resize( screenW, screenH ) ); + return true; + } + return false; + } + + /** + * @return the screen scale at {@code index} + */ + public ScreenScale get( final int index ) + { + return screenScales.get( index ); + } + + /** + * @return number of screen scales. + */ + public int size() + { + return screenScales.size(); + } + + public int suggestScreenScale( final double renderNanosPerPixel ) + { + for ( int i = 0; i < screenScales.size() - 1; i++ ) + { + final double renderTime = screenScales.get( i ).estimateRenderNanos( renderNanosPerPixel ); + if ( renderTime <= targetRenderNanos ) + return i; + } + return screenScales.size() - 1; + } + + public int suggestIntervalScreenScale( final double renderNanosPerPixel, final int minScreenScaleIndex ) + { + for ( int i = minScreenScaleIndex; i < screenScales.size() - 1; i++ ) + { + final double renderTime = screenScales.get( i ).estimateIntervalRenderNanos( renderNanosPerPixel ); + if ( renderTime <= targetRenderNanos ) + return i; + } + return screenScales.size() - 1; + } + + /** + * Compute the intersection of {@code interval} and the screen area. + * + * @param interval + * a 2D interval in screen coordinates + * + * @return intersection of {@code interval} and the screen area + */ + public Interval clipToScreen( final Interval interval ) + { + // This is equivalent to + // Intervals.intersect( interval, new FinalInterval( screenW, screenH ) ); + return Intervals.createMinMax( + Math.max( 0, interval.min( 0 ) ), + Math.max( 0, interval.min( 1 ) ), + Math.min( screenW - 1, interval.max( 0 ) ), + Math.min( screenH - 1, interval.max( 1 ) ) ); + } + + public void requestInterval( final Interval screenInterval ) + { + screenScales.forEach( s -> s.requestInterval( screenInterval ) ); + } + + public void clearRequestedIntervals() + { + screenScales.forEach( ScreenScale::pullScreenInterval ); + } + + public IntervalRenderData pullIntervalRenderData( final int intervalScaleIndex, final int targetScaleIndex ) + { + return new IntervalRenderData( intervalScaleIndex, targetScaleIndex ); + } + + static class ScreenScale + { + /** + * Scale factor from the viewer coordinates to target image of this screen scale. + */ + private final double scale; + + /** + * The width of the target image at this ScreenScale. + */ + private int w = 0; + + /** + * The height of the target image at this ScreenScale. + */ + private int h = 0; + + /** + * The transformation from viewer to target image coordinates at this ScreenScale. + */ + private final AffineTransform3D scaleTransform = new AffineTransform3D(); + + /** + * Pending interval request. + * This is in viewer coordinates. + * To transform to target coordinates of this scale, use {@link #scaleScreenInterval}. + */ + private Interval requestedScreenInterval = null; + + /** + * @param scale + * Scale factor from the viewer coordinates to target image of this screen scale/ + */ + ScreenScale( final double scale ) + { + this.scale = scale; + } + + /** + * Add {@code screenInterval} to requested interval (union). + * Note that the requested interval is maintained in screen coordinates! + */ + public void requestInterval( final Interval screenInterval ) + { + requestedScreenInterval = requestedScreenInterval == null + ? screenInterval + : Intervals.union( requestedScreenInterval, screenInterval ); + } + + /** + * Return and clear requested interval. + * Note that the requested interval is maintained in screen coordinates! + */ + public Interval pullScreenInterval() + { + final Interval interval = requestedScreenInterval; + requestedScreenInterval = null; + return interval; + } + + void resize( final int screenW, final int screenH ) + { + w = ( int ) Math.ceil( scale * screenW ); + h = ( int ) Math.ceil( scale * screenH ); + + scaleTransform.set( scale, 0, 0 ); + scaleTransform.set( scale, 1, 1 ); + scaleTransform.set( 0.5 * scale - 0.5, 0, 3 ); + scaleTransform.set( 0.5 * scale - 0.5, 1, 3 ); + + requestedScreenInterval = null; + } + + double estimateRenderNanos( final double renderNanosPerPixel ) + { + return renderNanosPerPixel * w * h; + } + + double estimateIntervalRenderNanos( final double renderNanosPerPixel ) + { + return renderNanosPerPixel * Intervals.numElements( scaleScreenInterval( requestedScreenInterval ) ); + } + + Interval scaleScreenInterval( final Interval requestedScreenInterval ) + { + // This is equivalent to + // Intervals.intersect( new FinalInterval( w, h ), Intervals.smallestContainingInterval( Intervals.scale( requestedScreenInterval, screenToViewerScale ) ) ); + return Intervals.createMinMax( + Math.max( 0, ( int ) Math.floor( requestedScreenInterval.min( 0 ) * scale ) ), + Math.max( 0, ( int ) Math.floor( requestedScreenInterval.min( 1 ) * scale ) ), + Math.min( w - 1, ( int ) Math.ceil( requestedScreenInterval.max( 0 ) * scale ) ), + Math.min( h - 1, ( int ) Math.ceil( requestedScreenInterval.max( 1 ) * scale ) ) + ); + } + + public int width() + { + return w; + } + + public int height() + { + return h; + } + + public double scale() + { + return scale; + } + + public AffineTransform3D scaleTransform() + { + return scaleTransform; + } + } + + class IntervalRenderData + { + private final int renderScaleIndex; + + private final Interval renderInterval; + + private final Interval targetInterval; + + private final double tx; + + private final double ty; + + private final Interval[] screenIntervals; + + public IntervalRenderData( final int renderScaleIndex, final int targetScaleIndex ) + { + this.renderScaleIndex = renderScaleIndex; + + screenIntervals = new Interval[ size() ]; + for ( int i = renderScaleIndex; i < screenIntervals.length; ++i ) + screenIntervals[ i ] = get( i ).pullScreenInterval(); + final Interval screenInterval = screenIntervals[ renderScaleIndex ]; + + final ScreenScale renderScale = get( renderScaleIndex ); + renderInterval = renderScale.scaleScreenInterval( screenInterval ); + + final ScreenScale targetScale = get( targetScaleIndex ); + targetInterval = targetScale.scaleScreenInterval( screenInterval ); + + final double relativeScale = targetScale.scale() / renderScale.scale(); + tx = renderInterval.min( 0 ) * relativeScale; + ty = renderInterval.min( 1 ) * relativeScale; + } + + public void reRequest() + { + for ( int i = renderScaleIndex; i < screenIntervals.length; ++i ) + { + final Interval interval = screenIntervals[ i ]; + if ( interval != null ) + get( i ).requestInterval( interval ); + } + } + + public int width() + { + return ( int ) renderInterval.dimension( 0 ); + } + + public int height() + { + return ( int ) renderInterval.dimension( 1 ); + } + + public int offsetX() + { + return ( int ) renderInterval.min( 0 ); + } + public int offsetY() + { + return ( int ) renderInterval.min( 1 ); + } + + public double scale() + { + return get( renderScaleIndex ).scale(); + } + + public Interval targetInterval() + { + return targetInterval; + } + + public double tx() + { + return tx; + } + + public double ty() + { + return ty; + } + } +} diff --git a/src/main/java/bdv/viewer/render/SimpleVolatileProjector.java b/src/main/java/bdv/viewer/render/SimpleVolatileProjector.java new file mode 100644 index 0000000000000000000000000000000000000000..70eaef32c0b51bf9e9f66db840af685c39726a26 --- /dev/null +++ b/src/main/java/bdv/viewer/render/SimpleVolatileProjector.java @@ -0,0 +1,252 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer.render; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import net.imglib2.FinalInterval; +import net.imglib2.RandomAccess; +import net.imglib2.RandomAccessible; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.converter.Converter; +import net.imglib2.util.Intervals; +import net.imglib2.util.StopWatch; + +// TODO Fix naming. This is a VolatileProjector for a non-volatile source... +/** + * An {@link VolatileProjector}, that renders a target 2D + * {@code RandomAccessibleInterval} by copying values from a source + * {@code RandomAccessible}. The source can have more dimensions than the + * target. Target coordinate <em>(x,y)</em> is copied from source coordinate + * <em>(x,y,0,...,0)</em>. + * <p> + * A specified number of threads is used for rendering. + * + * @param <A> + * pixel type of the source {@code RandomAccessible}. + * @param <B> + * pixel type of the target {@code RandomAccessibleInterval}. + * + * @author Tobias Pietzsch + * @author Stephan Saalfeld + */ +public class SimpleVolatileProjector< A, B > implements VolatileProjector +{ + /** + * A converter from the source pixel type to the target pixel type. + */ + private final Converter< ? super A, B > converter; + + /** + * The target interval. Pixels of the target interval should be set by + * {@link #map} + */ + private final RandomAccessibleInterval< B > target; + + private final RandomAccessible< A > source; + + /** + * Source interval which will be used for rendering. This is the 2D target + * interval expanded to source dimensionality (usually 3D) with + * {@code min=max=0} in the additional dimensions. + */ + private final FinalInterval sourceInterval; + + /** + * Number of threads to use for rendering + */ + private final int numThreads; + + private final ExecutorService executorService; + + /** + * Time needed for rendering the last frame, in nano-seconds. + */ + private long lastFrameRenderNanoTime; + + private AtomicBoolean canceled = new AtomicBoolean(); + + private boolean valid = false; + + /** + * Create new projector with the given source and a converter from source to + * target pixel type. + * + * @param source + * source pixels. + * @param converter + * converts from the source pixel type to the target pixel type. + * @param target + * the target interval that this projector maps to + * @param numThreads + * how many threads to use for rendering. + * @param executorService + */ + public SimpleVolatileProjector( + final RandomAccessible< A > source, + final Converter< ? super A, B > converter, + final RandomAccessibleInterval< B > target, + final int numThreads, + final ExecutorService executorService ) + { + this.converter = converter; + this.target = target; + this.source = source; + + final int n = Math.max( 2, source.numDimensions() ); + final long[] min = new long[ n ]; + final long[] max = new long[ n ]; + min[ 0 ] = target.min( 0 ); + max[ 0 ] = target.max( 0 ); + min[ 1 ] = target.min( 1 ); + max[ 1 ] = target.max( 1 ); + sourceInterval = new FinalInterval( min, max ); + + this.numThreads = numThreads; + this.executorService = executorService; + lastFrameRenderNanoTime = -1; + } + + @Override + public void cancel() + { + canceled.set( true ); + } + + @Override + public long getLastFrameRenderNanoTime() + { + return lastFrameRenderNanoTime; + } + + @Override + public boolean isValid() + { + return valid; + } + + /** + * Render the 2D target image by copying values from the source. Source can + * have more dimensions than the target. Target coordinate <em>(x,y)</em> is + * copied from source coordinate <em>(x,y,0,...,0)</em>. + * + * @return true if rendering was completed (all target pixels written). + * false if rendering was interrupted. + */ + @Override + public boolean map( final boolean clearUntouchedTargetPixels ) + { + if ( canceled.get() ) + return false; + + final StopWatch stopWatch = StopWatch.createAndStart(); + + final int targetHeight = ( int ) target.dimension( 1 ); + final int numTasks = numThreads <= 1 ? 1 : Math.min( numThreads * 10, targetHeight ); + final double taskHeight = ( double ) targetHeight / numTasks; + final int[] taskStartHeights = new int[ numTasks + 1 ]; + for ( int i = 0; i < numTasks; ++i ) + taskStartHeights[ i ] = ( int ) ( i * taskHeight ); + taskStartHeights[ numTasks ] = targetHeight; + + final boolean createExecutor = ( executorService == null ); + final ExecutorService ex = createExecutor ? Executors.newFixedThreadPool( numThreads ) : executorService; + + final List< Callable< Void > > tasks = new ArrayList<>( numTasks ); + for( int i = 0; i < numTasks; ++i ) + tasks.add( createMapTask( taskStartHeights[ i ], taskStartHeights[ i + 1 ] ) ); + try + { + ex.invokeAll( tasks ); + } + catch ( final InterruptedException e ) + { + Thread.currentThread().interrupt(); + } + + if ( createExecutor ) + ex.shutdown(); + + lastFrameRenderNanoTime = stopWatch.nanoTime(); + + final boolean success = !canceled.get(); + valid |= success; + return success; + } + + /** + * @return a {@code Callable} that runs {@code map(startHeight, endHeight)} + */ + private Callable< Void > createMapTask( final int startHeight, final int endHeight ) + { + return Executors.callable( () -> map( startHeight, endHeight ), null ); + } + + /** + * Copy lines from {@code y = startHeight} up to {@code endHeight} + * (exclusive) from source to target. Check after + * each line whether rendering was {@link #cancel() canceled}. + * + * @param startHeight + * start of line range to copy (relative to target min coordinate) + * @param endHeight + * end (exclusive) of line range to copy (relative to target min + * coordinate) + */ + private void map( final int startHeight, final int endHeight ) + { + if ( canceled.get() ) + return; + + final RandomAccess< B > targetRandomAccess = target.randomAccess( target ); + final RandomAccess< A > sourceRandomAccess = source.randomAccess( sourceInterval ); + final int width = ( int ) target.dimension( 0 ); + final long[] smin = Intervals.minAsLongArray( sourceInterval ); + + final int targetMin = ( int ) target.min( 1 ); + for ( int y = startHeight; y < endHeight; ++y ) + { + if ( canceled.get() ) + return; + smin[ 1 ] = y + targetMin; + sourceRandomAccess.setPosition( smin ); + targetRandomAccess.setPosition( smin ); + for ( int x = 0; x < width; ++x ) + { + converter.convert( sourceRandomAccess.get(), targetRandomAccess.get() ); + sourceRandomAccess.fwd( 0 ); + targetRandomAccess.fwd( 0 ); + } + } + } +} diff --git a/src/main/java/bdv/viewer/render/TransformAwareBufferedImageOverlayRenderer.java b/src/main/java/bdv/viewer/render/TransformAwareBufferedImageOverlayRenderer.java deleted file mode 100644 index 4a30b5bb9cb1aec3e68a311ee82cd745f9014696..0000000000000000000000000000000000000000 --- a/src/main/java/bdv/viewer/render/TransformAwareBufferedImageOverlayRenderer.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * #%L - * BigDataViewer core classes with minimal dependencies - * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic - * %% - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * #L% - */ -package bdv.viewer.render; - -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.RenderingHints; -import java.awt.image.BufferedImage; -import java.util.concurrent.CopyOnWriteArrayList; - -import net.imglib2.realtransform.AffineTransform3D; -import net.imglib2.ui.OverlayRenderer; -import net.imglib2.ui.TransformListener; -import net.imglib2.ui.overlay.BufferedImageOverlayRenderer; - -public class TransformAwareBufferedImageOverlayRenderer extends BufferedImageOverlayRenderer implements TransformAwareRenderTarget -{ - protected AffineTransform3D pendingTransform; - - protected AffineTransform3D paintedTransform; - - /** - * These listeners will be notified about the transform that is associated - * to the currently rendered image. This is intended for example for - * {@link OverlayRenderer}s that need to exactly match the transform of - * their overlaid content to the transform of the image. - */ - protected final CopyOnWriteArrayList< TransformListener< AffineTransform3D > > paintedTransformListeners; - - public TransformAwareBufferedImageOverlayRenderer() - { - super(); - pendingTransform = new AffineTransform3D(); - paintedTransform = new AffineTransform3D(); - paintedTransformListeners = new CopyOnWriteArrayList<>(); - } - - @Override - public synchronized BufferedImage setBufferedImageAndTransform( final BufferedImage img, final AffineTransform3D transform ) - { - pendingTransform.set( transform ); - return super.setBufferedImage( img ); - } - - @Override - public void drawOverlays( final Graphics g ) - { - boolean notifyTransformListeners = false; - synchronized ( this ) - { - if ( pending ) - { - final BufferedImage tmp = bufferedImage; - bufferedImage = pendingImage; - paintedTransform.set( pendingTransform ); - pendingImage = tmp; - pending = false; - notifyTransformListeners = true; - } - } - if ( bufferedImage != null ) - { -// final StopWatch watch = new StopWatch(); -// watch.start(); -// ( ( Graphics2D ) g ).setRenderingHint( RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR ); - ( ( Graphics2D ) g ).setRenderingHint( RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR ); - ( ( Graphics2D ) g ).setRenderingHint( RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED ); - ( ( Graphics2D ) g ).setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF ); - ( ( Graphics2D ) g ).setRenderingHint( RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED ); - ( ( Graphics2D ) g ).setRenderingHint( RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED ); - g.drawImage( bufferedImage, 0, 0, getWidth(), getHeight(), null ); - if ( notifyTransformListeners ) - for ( final TransformListener< AffineTransform3D > listener : paintedTransformListeners ) - listener.transformChanged( paintedTransform ); -// System.out.println( String.format( "g.drawImage() :%4d ms", watch.nanoTime() / 1000000 ) ); - } - } - - /** - * Add a {@link TransformListener} to notify about viewer transformation - * changes. Listeners will be notified when a new image has been rendered - * (immediately before that image is displayed) with the viewer transform - * used to render that image. - * - * @param listener - * the transform listener to add. - */ - @Override - public void addTransformListener( final TransformListener< AffineTransform3D > listener ) - { - addTransformListener( listener, Integer.MAX_VALUE ); - } - - /** - * Add a {@link TransformListener} to notify about viewer transformation - * changes. Listeners will be notified when a new image has been rendered - * (immediately before that image is displayed) with the viewer transform - * used to render that image. - * - * @param listener - * the transform listener to add. - * @param index - * position in the list of listeners at which to insert this one. - */ - @Override - public void addTransformListener( final TransformListener< AffineTransform3D > listener, final int index ) - { - synchronized ( paintedTransformListeners ) - { - final int s = paintedTransformListeners.size(); - paintedTransformListeners.add( index < 0 ? 0 : index > s ? s : index, listener ); - listener.transformChanged( paintedTransform ); - } - } - - /** - * Remove a {@link TransformListener}. - * - * @param listener - * the transform listener to remove. - */ - @Override - public void removeTransformListener( final TransformListener< AffineTransform3D > listener ) - { - synchronized ( paintedTransformListeners ) - { - paintedTransformListeners.remove( listener ); - } - } - - /** - * DON'T USE THIS. - * <p> - * This is a work around for JDK bug - * https://bugs.openjdk.java.net/browse/JDK-8029147 which leads to - * ViewerPanel not being garbage-collected when ViewerFrame is closed. So - * instead we need to manually let go of resources... - */ - void kill() - { - bufferedImage = null; - pendingImage = null; - } -} diff --git a/src/main/java/bdv/viewer/render/VisibilityUtils.java b/src/main/java/bdv/viewer/render/VisibilityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3568be69b86bf756eb55950a7c5e73f17eebc503 --- /dev/null +++ b/src/main/java/bdv/viewer/render/VisibilityUtils.java @@ -0,0 +1,124 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer.render; + +import bdv.util.MipmapTransforms; +import bdv.viewer.Interpolation; +import bdv.viewer.Source; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.ViewerState; +import java.util.List; +import java.util.Set; +import net.imglib2.FinalRealInterval; +import net.imglib2.Interval; +import net.imglib2.realtransform.AffineTransform3D; + +class VisibilityUtils +{ + /** + * Compute a list of sources are currently visible on screen. + * <p> + * This means that the sources + * <ul> + * <li>are visible in the given {@code ViewerState}, and,</li> + * <li>when transformed to viewer coordinates, overlap the screen area.</li> + * </ul> + * The returned list of sources is sorted by {@code viewerState.sourceOrder()}. + * + * @param viewerState + * specifies sources, transform, and current timepoint + * @param screenScale + * specifies screen size and scale transform + * @param result + * list of currently visible sources is stored here + */ + static void computeVisibleSourcesOnScreen( + final ViewerState viewerState, + final ScreenScales.ScreenScale screenScale, + final List< SourceAndConverter< ? > > result ) + { + result.clear(); + + final int screenMinX = 0; + final int screenMinY = 0; + final int screenMaxX = screenScale.width() - 1; + final int screenMaxY = screenScale.height() - 1; + + final AffineTransform3D screenTransform = viewerState.getViewerTransform(); + screenTransform.preConcatenate( screenScale.scaleTransform() ); + + final AffineTransform3D sourceToScreen = new AffineTransform3D(); + final double[] sourceMin = new double[ 3 ]; + final double[] sourceMax = new double[ 3 ]; + + final Set< SourceAndConverter< ? > > sources = viewerState.getVisibleAndPresentSources(); + final int t = viewerState.getCurrentTimepoint(); + final double expand = viewerState.getInterpolation() == Interpolation.NEARESTNEIGHBOR ? 0.5 : 1.0; + + for ( final SourceAndConverter< ? > source : sources ) + { + if( !source.getSpimSource().doBoundingBoxCulling() ) + { + result.add( source ); + continue; + } + + final Source< ? > spimSource = source.getSpimSource(); + final int level = MipmapTransforms.getBestMipMapLevel( screenTransform, spimSource, t ); + spimSource.getSourceTransform( t, level, sourceToScreen ); + sourceToScreen.preConcatenate( screenTransform ); + + final Interval interval = spimSource.getSource( t, level ); + for ( int d = 0; d < 3; d++ ) + { + sourceMin[ d ] = interval.realMin( d ) - expand; + sourceMax[ d ] = interval.realMax( d ) + expand; + } + final FinalRealInterval bb = sourceToScreen.estimateBounds( new FinalRealInterval( sourceMin, sourceMax ) ); + + if ( bb.realMax( 0 ) >= screenMinX + && bb.realMin( 0 ) <= screenMaxX + && bb.realMax( 1 ) >= screenMinY + && bb.realMin( 1 ) <= screenMaxY + && bb.realMax( 2 ) >= 0 + && bb.realMin( 2 ) <= 0 ) + { + result.add( source ); + } + } + + result.sort( viewerState.sourceOrder() ); + } + // TODO: Eventually, for thousands of sources, this could be moved to ViewerState, + // in order to avoid creating a new intermediate HashSet for every + // viewerState.getVisibleAndPresentSources(). + // However, other issues will become bottlenecks before that, e.g., + // copying source list when taking snapshots of ViewerState every frame, + // painting MultiBoxOverlay, etc. +} diff --git a/src/main/java/bdv/viewer/render/VolatileHierarchyProjector.java b/src/main/java/bdv/viewer/render/VolatileHierarchyProjector.java index bbe86420e90b14832f80d90805edc1fe6d3647f1..66387e69b7a8b9e95c16fc21e137df2fc7ca06a4 100644 --- a/src/main/java/bdv/viewer/render/VolatileHierarchyProjector.java +++ b/src/main/java/bdv/viewer/render/VolatileHierarchyProjector.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -37,10 +36,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; - -import net.imglib2.Cursor; import net.imglib2.FinalInterval; -import net.imglib2.IterableInterval; import net.imglib2.RandomAccess; import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; @@ -48,12 +44,9 @@ import net.imglib2.Volatile; import net.imglib2.cache.iotiming.CacheIoTiming; import net.imglib2.cache.iotiming.IoStatistics; import net.imglib2.converter.Converter; -import net.imglib2.img.Img; -import net.imglib2.img.array.ArrayImgs; -import net.imglib2.type.numeric.NumericType; -import net.imglib2.type.numeric.integer.ByteType; -import net.imglib2.ui.AbstractInterruptibleProjector; -import net.imglib2.ui.util.StopWatch; +import net.imglib2.type.operators.SetZero; +import net.imglib2.util.Intervals; +import net.imglib2.util.StopWatch; import net.imglib2.view.Views; /** @@ -61,75 +54,87 @@ import net.imglib2.view.Views; * {@link #map()} call, the projector has a {@link #isValid() state} that * signalizes whether all projected pixels were perfect. * - * @author Stephan Saalfeld <saalfeld@mpi-cbg.de> - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Stephan Saalfeld + * @author Tobias Pietzsch */ -public class VolatileHierarchyProjector< A extends Volatile< ? >, B extends NumericType< B > > extends AbstractInterruptibleProjector< A, B > implements VolatileProjector +public class VolatileHierarchyProjector< A extends Volatile< ? >, B extends SetZero > implements VolatileProjector { - protected final ArrayList< RandomAccessible< A > > sources = new ArrayList<>(); - - private final byte[] maskArray; - - protected final Img< ByteType > mask; - - protected volatile boolean valid = false; + /** + * A converter from the source pixel type to the target pixel type. + */ + private final Converter< ? super A, B > converter; - protected int numInvalidLevels; + /** + * The target interval. Pixels of the target interval should be set by + * {@link #map} + */ + private final RandomAccessibleInterval< B > target; /** - * Extends of the source to be used for mapping. + * List of source resolutions starting with the optimal resolution at index + * 0. During each {@link #map(boolean)}, for every pixel, resolution levels + * are successively queried until a valid pixel is found. */ - protected final FinalInterval sourceInterval; + private final List< RandomAccessible< A > > sources; /** - * Target width + * Records, for every target pixel, the best (smallest index) source + * resolution level that has provided a valid value. Only better (lower + * index) resolutions are re-tried in successive {@link #map(boolean)} + * calls. */ - protected final int width; + private final byte[] mask; /** - * Target height + * {@code true} iff all target pixels were rendered with valid data from the + * optimal resolution level (level {@code 0}). */ - protected final int height; + private volatile boolean valid = false; /** - * Steps for carriage return. Typically -{@link #width} + * How many levels (starting from level {@code 0}) have to be re-rendered in + * the next rendering pass, i.e., {@code map()} call. */ - protected final int cr; + private int numInvalidLevels; /** - * A reference to the target image as an iterable. Used for source-less - * operations such as clearing its content. + * Source interval which will be used for rendering. This is the 2D target + * interval expanded to source dimensionality (usually 3D) with + * {@code min=max=0} in the additional dimensions. */ - protected final IterableInterval< B > iterableTarget; + private final FinalInterval sourceInterval; /** * Number of threads to use for rendering */ - protected final int numThreads; + private final int numThreads; - protected final ExecutorService executorService; + /** + * Executor service to be used for rendering + */ + private final ExecutorService executorService; /** * Time needed for rendering the last frame, in nano-seconds. * This does not include time spent in blocking IO. */ - protected long lastFrameRenderNanoTime; + private long lastFrameRenderNanoTime; /** * Time spent in blocking IO rendering the last frame, in nano-seconds. */ - protected long lastFrameIoNanoTime; // TODO move to derived implementation for local sources only + private long lastFrameIoNanoTime; // TODO move to derived implementation for local sources only /** * temporary variable to store the number of invalid pixels in the current * rendering pass. */ - protected final AtomicInteger numInvalidPixels = new AtomicInteger(); + private final AtomicInteger numInvalidPixels = new AtomicInteger(); /** - * Flag to indicate that someone is trying to interrupt rendering. + * Flag to indicate that someone is trying to {@link #cancel()} rendering. */ - protected final AtomicBoolean interrupted = new AtomicBoolean(); + private final AtomicBoolean canceled = new AtomicBoolean(); public VolatileHierarchyProjector( final List< ? extends RandomAccessible< A > > sources, @@ -149,27 +154,21 @@ public class VolatileHierarchyProjector< A extends Volatile< ? >, B extends Nume final int numThreads, final ExecutorService executorService ) { - super( Math.max( 2, sources.get( 0 ).numDimensions() ), converter, target ); - - this.sources.addAll( sources ); + this.converter = converter; + this.target = target; + this.sources = new ArrayList<>( sources ); numInvalidLevels = sources.size(); + mask = maskArray; - this.maskArray = maskArray; - mask = ArrayImgs.bytes( maskArray, target.dimension( 0 ), target.dimension( 1 ) ); - - iterableTarget = Views.iterable( target ); - - for ( int d = 2; d < min.length; ++d ) - min[ d ] = max[ d ] = 0; - + final int n = Math.max( 2, sources.get( 0 ).numDimensions() ); + final long[] min = new long[ n ]; + final long[] max = new long[ n ]; + min[ 0 ] = target.min( 0 ); max[ 0 ] = target.max( 0 ); + min[ 1 ] = target.min( 1 ); max[ 1 ] = target.max( 1 ); sourceInterval = new FinalInterval( min, max ); - width = ( int )target.dimension( 0 ); - height = ( int )target.dimension( 1 ); - cr = -width; - this.numThreads = numThreads; this.executorService = executorService; @@ -180,7 +179,7 @@ public class VolatileHierarchyProjector< A extends Volatile< ? >, B extends Nume @Override public void cancel() { - interrupted.set( true ); + canceled.set( true ); } @Override @@ -206,164 +205,175 @@ public class VolatileHierarchyProjector< A extends Volatile< ? >, B extends Nume */ public void clearMask() { - Arrays.fill( maskArray, 0, ( int ) mask.size(), Byte.MAX_VALUE ); + final int size = ( int ) Intervals.numElements( target ); + Arrays.fill( mask, 0, size, Byte.MAX_VALUE ); numInvalidLevels = sources.size(); } /** * Clear target pixels that were never written. */ - protected void clearUntouchedTargetPixels() + private void clearUntouchedTargetPixels() { - final Cursor< ByteType > maskCursor = mask.cursor(); - for ( final B t : iterableTarget ) - if ( maskCursor.next().get() == Byte.MAX_VALUE ) - t.setZero(); - } - - @Override - public boolean map() - { - return map( true ); + final int[] data = ProjectorUtils.getARGBArrayImgData( target ); + if ( data != null ) + { + final int size = ( int ) Intervals.numElements( target ); + for ( int i = 0; i < size; ++i ) + if ( mask[ i ] == Byte.MAX_VALUE ) + data[ i ] = 0; + } + else + { + int i = 0; + for ( final B t : Views.flatIterable( target ) ) + if ( mask[ i++ ] == Byte.MAX_VALUE ) + t.setZero(); + } } @Override public boolean map( final boolean clearUntouchedTargetPixels ) { - interrupted.set( false ); + if ( canceled.get() ) + return false; + + valid = false; - final StopWatch stopWatch = new StopWatch(); - stopWatch.start(); + final StopWatch stopWatch = StopWatch.createAndStart(); final IoStatistics iostat = CacheIoTiming.getIoStatistics(); final long startTimeIo = iostat.getIoNanoTime(); final long startTimeIoCumulative = iostat.getCumulativeIoNanoTime(); -// final long startIoBytes = iostat.getIoBytes(); - - final int numTasks; - if ( numThreads > 1 ) - { - numTasks = Math.min( numThreads * 10, height ); - } - else - numTasks = 1; - final double taskHeight = ( double )height / numTasks; - - int i; - valid = false; + final int targetHeight = ( int ) target.dimension( 1 ); + final int numTasks = numThreads <= 1 ? 1 : Math.min( numThreads * 10, targetHeight ); + final double taskHeight = ( double ) targetHeight / numTasks; + final int[] taskStartHeights = new int[ numTasks + 1 ]; + for ( int i = 0; i < numTasks; ++i ) + taskStartHeights[ i ] = ( int ) ( i * taskHeight ); + taskStartHeights[ numTasks ] = targetHeight; final boolean createExecutor = ( executorService == null ); final ExecutorService ex = createExecutor ? Executors.newFixedThreadPool( numThreads ) : executorService; - for ( i = 0; i < numInvalidLevels && !valid; ++i ) + try { - final byte iFinal = ( byte ) i; - - valid = true; - numInvalidPixels.set( 0 ); - - final ArrayList< Callable< Void > > tasks = new ArrayList<>( numTasks ); - for ( int taskNum = 0; taskNum < numTasks; ++taskNum ) + /* + * After the for loop, resolutionLevel is the highest (coarsest) + * resolution for which all pixels could be filled from valid data. This + * means that in the next pass, i.e., map() call, levels up to + * resolutionLevel have to be re-rendered. + */ + int resolutionLevel; + for ( resolutionLevel = 0; resolutionLevel < numInvalidLevels; ++resolutionLevel ) { - final int myOffset = width * ( int ) ( taskNum * taskHeight ); - final long myMinY = min[ 1 ] + ( int ) ( taskNum * taskHeight ); - final int myHeight = ( int ) ( ( (taskNum == numTasks - 1 ) ? height : ( int ) ( ( taskNum + 1 ) * taskHeight ) ) - myMinY - min[ 1 ] ); - - final Callable< Void > r = new Callable< Void >() + final List< Callable< Void > > tasks = new ArrayList<>( numTasks ); + for ( int i = 0; i < numTasks; ++i ) + tasks.add( createMapTask( ( byte ) resolutionLevel, taskStartHeights[ i ], taskStartHeights[ i + 1 ] ) ); + numInvalidPixels.set( 0 ); + try { - @Override - public Void call() - { - if ( interrupted.get() ) - return null; - - final RandomAccess< B > targetRandomAccess = target.randomAccess( target ); - final Cursor< ByteType > maskCursor = mask.cursor(); - final RandomAccess< A > sourceRandomAccess = sources.get( iFinal ).randomAccess( sourceInterval ); - int myNumInvalidPixels = 0; - - final long[] smin = new long[ n ]; - System.arraycopy( min, 0, smin, 0, n ); - smin[ 1 ] = myMinY; - sourceRandomAccess.setPosition( smin ); - - targetRandomAccess.setPosition( min[ 0 ], 0 ); - targetRandomAccess.setPosition( myMinY, 1 ); - - maskCursor.jumpFwd( myOffset ); - - for ( int y = 0; y < myHeight; ++y ) - { - if ( interrupted.get() ) - return null; - - for ( int x = 0; x < width; ++x ) - { - final ByteType m = maskCursor.next(); - if ( m.get() > iFinal ) - { - final A a = sourceRandomAccess.get(); - final boolean v = a.isValid(); - if ( v ) - { - converter.convert( a, targetRandomAccess.get() ); - m.set( iFinal ); - } - else - ++myNumInvalidPixels; - } - sourceRandomAccess.fwd( 0 ); - targetRandomAccess.fwd( 0 ); - } - ++smin[ 1 ]; - sourceRandomAccess.setPosition( smin ); - targetRandomAccess.move( cr, 0 ); - targetRandomAccess.fwd( 1 ); - } - numInvalidPixels.addAndGet( myNumInvalidPixels ); - if ( myNumInvalidPixels != 0 ) - valid = false; - return null; - } - }; - tasks.add( r ); - } - try - { - ex.invokeAll( tasks ); - } - catch ( final InterruptedException e ) - { - Thread.currentThread().interrupt(); - } - if ( interrupted.get() ) - { -// System.out.println( "interrupted" ); - if ( createExecutor ) - ex.shutdown(); - return false; + ex.invokeAll( tasks ); + } + catch ( final InterruptedException e ) + { + Thread.currentThread().interrupt(); + } + if ( canceled.get() ) + return false; + if ( numInvalidPixels.get() == 0 ) + // if this pass was all valid + numInvalidLevels = resolutionLevel; } -// System.out.println( "numInvalidPixels(" + i + ") = " + numInvalidPixels ); } - if ( createExecutor ) - ex.shutdown(); + finally + { + if ( createExecutor ) + ex.shutdown(); + } - if ( clearUntouchedTargetPixels && !interrupted.get() ) + if ( clearUntouchedTargetPixels && !canceled.get() ) clearUntouchedTargetPixels(); final long lastFrameTime = stopWatch.nanoTime(); -// final long numIoBytes = iostat.getIoBytes() - startIoBytes; lastFrameIoNanoTime = iostat.getIoNanoTime() - startTimeIo; lastFrameRenderNanoTime = lastFrameTime - ( iostat.getCumulativeIoNanoTime() - startTimeIoCumulative ) / numThreads; - // System.out.println( "lastFrameTime = " + lastFrameTime / 1000000 ); // System.out.println( "lastFrameRenderNanoTime = " + lastFrameRenderNanoTime / 1000000 ); - if ( valid ) - numInvalidLevels = i - 1; valid = numInvalidLevels == 0; -// System.out.println( "Mapping complete after " + ( s + 1 ) + " levels." ); + return !canceled.get(); + } + + /** + * @return a {@code Callable} that runs + * {@code map(resolutionIndex, startHeight, endHeight)} + */ + private Callable< Void > createMapTask( final byte resolutionIndex, final int startHeight, final int endHeight ) + { + return Executors.callable( () -> map( resolutionIndex, startHeight, endHeight ), null ); + } + + /** + * Copy lines from {@code y = startHeight} up to {@code endHeight} + * (exclusive) from source {@code resolutionIndex} to target. Check after + * each line whether rendering was {@link #cancel() canceled}. + * <p> + * Only valid source pixels with a current mask value + * {@code mask>resolutionIndex} are copied to target, and their mask value + * is set to {@code mask=resolutionIndex}. Invalid source pixels are + * ignored. Pixels with {@code mask<=resolutionIndex} are ignored, because + * they have already been written to target during a previous pass. + * <p> + * + * @param resolutionIndex + * index of source resolution level + * @param startHeight + * start of line range to copy (relative to target min coordinate) + * @param endHeight + * end (exclusive) of line range to copy (relative to target min + * coordinate) + */ + private void map( final byte resolutionIndex, final int startHeight, final int endHeight ) + { + if ( canceled.get() ) + return; + + final RandomAccess< B > targetRandomAccess = target.randomAccess( target ); + final RandomAccess< A > sourceRandomAccess = sources.get( resolutionIndex ).randomAccess( sourceInterval ); + final int width = ( int ) target.dimension( 0 ); + final long[] smin = Intervals.minAsLongArray( sourceInterval ); + int myNumInvalidPixels = 0; + + final int targetMin = ( int ) target.min( 1 ); + for ( int y = startHeight; y < endHeight; ++y ) + { + if ( canceled.get() ) + return; + + smin[ 1 ] = y + targetMin; + sourceRandomAccess.setPosition( smin ); + targetRandomAccess.setPosition( smin ); + final int mi = y * width; + for ( int x = 0; x < width; ++x ) + { + if ( mask[ mi + x ] > resolutionIndex ) + { + final A a = sourceRandomAccess.get(); + final boolean v = a.isValid(); + if ( v ) + { + converter.convert( a, targetRandomAccess.get() ); + mask[ mi + x ] = resolutionIndex; + } + else + ++myNumInvalidPixels; + } + sourceRandomAccess.fwd( 0 ); + targetRandomAccess.fwd( 0 ); + } + } - return !interrupted.get(); + numInvalidPixels.addAndGet( myNumInvalidPixels ); } } diff --git a/src/main/java/bdv/viewer/render/VolatileProjector.java b/src/main/java/bdv/viewer/render/VolatileProjector.java index 18f61203e6e4848d1ac133093065bae25197fb9a..4e94698a5148b3fc9067b770cf39686ad8ffdd15 100644 --- a/src/main/java/bdv/viewer/render/VolatileProjector.java +++ b/src/main/java/bdv/viewer/render/VolatileProjector.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,10 +28,16 @@ */ package bdv.viewer.render; -import net.imglib2.Volatile; -import net.imglib2.ui.InterruptibleProjector; - -public interface VolatileProjector extends InterruptibleProjector +/** + * Render a 2D target copying pixels from a nD source. + * <p> + * Rendering can be interrupted, in which case {@link #map} will return false. + * Also, the rendering time for the last {@link #map} can be queried. + * + * @author Tobias Pietzsch + * @author Stephan Saalfeld + */ +public interface VolatileProjector { /** * Render the target image. @@ -41,10 +46,27 @@ public interface VolatileProjector extends InterruptibleProjector * @return true if rendering was completed (all target pixels written). * false if rendering was interrupted. */ - public boolean map( boolean clearUntouchedTargetPixels ); + boolean map( boolean clearUntouchedTargetPixels ); + + default boolean map() + { + return map( true ); + } + + /** + * Abort {@link #map()} if it is currently running. + */ + void cancel(); + + /** + * How many nano-seconds did the last {@link #map()} take. + * + * @return time needed for rendering the last frame, in nano-seconds. + */ + long getLastFrameRenderNanoTime(); /** - * @return true if all mapped pixels were {@link Volatile#isValid() valid}. + * @return true if all mapped pixels were valid. */ - public boolean isValid(); + boolean isValid(); } diff --git a/src/main/java/bdv/viewer/render/awt/BufferedImageOverlayRenderer.java b/src/main/java/bdv/viewer/render/awt/BufferedImageOverlayRenderer.java new file mode 100644 index 0000000000000000000000000000000000000000..160bc2fc5ab5d9f065f76d94572f0516dd72889b --- /dev/null +++ b/src/main/java/bdv/viewer/render/awt/BufferedImageOverlayRenderer.java @@ -0,0 +1,177 @@ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer.render.awt; + +import bdv.util.TripleBuffer; +import bdv.util.TripleBuffer.ReadableBuffer; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import net.imglib2.realtransform.AffineTransform3D; +import bdv.viewer.OverlayRenderer; +import bdv.viewer.render.RenderTarget; +import bdv.viewer.TransformListener; +import org.scijava.listeners.Listeners; + +/** + * {@link OverlayRenderer} drawing a {@link BufferedImage}, scaled to fill the + * canvas. It can be used as a {@link RenderTarget}, such that the + * {@link BufferedImageRenderResult} to draw is set by a renderer. + * + * @author Tobias Pietzsch + */ +public class BufferedImageOverlayRenderer implements OverlayRenderer, RenderTarget< BufferedImageRenderResult > +{ + private final TripleBuffer< BufferedImageRenderResult > tripleBuffer; + + /** + * These listeners will be notified about the transform that is associated + * to the currently rendered image. This is intended for example for + * {@link OverlayRenderer}s that need to exactly match the transform of + * their overlaid content to the transform of the image. + */ + private final Listeners.List< TransformListener< AffineTransform3D > > paintedTransformListeners; + + private final AffineTransform3D paintedTransform; + + /** + * The current canvas width. + */ + private volatile int width; + + /** + * The current canvas height. + */ + private volatile int height; + + public BufferedImageOverlayRenderer() + { + tripleBuffer = new TripleBuffer<>( BufferedImageRenderResult::new ); + width = 0; + height = 0; + paintedTransform = new AffineTransform3D(); + paintedTransformListeners = new Listeners.SynchronizedList<>( l -> l.transformChanged( paintedTransform ) ); + } + + @Override + public BufferedImageRenderResult getReusableRenderResult() + { + return tripleBuffer.getWritableBuffer(); + } + + @Override + public BufferedImageRenderResult createRenderResult() + { + return new BufferedImageRenderResult(); + } + + /** + * Set the {@code RenderResult} that is to be drawn on the canvas. + * + * @param result + * image to draw (may be null). + */ + @Override + public synchronized void setRenderResult( final BufferedImageRenderResult result ) + { + tripleBuffer.doneWriting( result ); + } + + @Override + public int getWidth() + { + return width; + } + + @Override + public int getHeight() + { + return height; + } + + @Override + public void drawOverlays( final Graphics g ) + { + final ReadableBuffer< BufferedImageRenderResult > rb = tripleBuffer.getReadableBuffer(); + final BufferedImageRenderResult result = rb.getBuffer(); + final BufferedImage image = result != null ? result.getBufferedImage() : null; + if ( image != null ) + { +// ( ( Graphics2D ) g ).setRenderingHint( RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR ); + ( ( Graphics2D ) g ).setRenderingHint( RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR ); + ( ( Graphics2D ) g ).setRenderingHint( RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED ); + ( ( Graphics2D ) g ).setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF ); + ( ( Graphics2D ) g ).setRenderingHint( RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED ); + ( ( Graphics2D ) g ).setRenderingHint( RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED ); + + final double scaleFactor = result.getScaleFactor(); + final int w = Math.max( width, ( int ) ( image.getWidth() / scaleFactor + 0.5 ) ); + final int h = Math.max( height, ( int ) ( image.getHeight() / scaleFactor + 0.5 ) ); + g.drawImage( image, 0, 0, w, h, null ); + + if ( rb.isUpdated() ) + { + paintedTransform.set( result.getViewerTransform() ); + paintedTransformListeners.list.forEach( listener -> listener.transformChanged( paintedTransform ) ); + } + } + } + + @Override + public void setCanvasSize( final int width, final int height ) + { + this.width = width; + this.height = height; + } + + /** + * Add/remove {@code TransformListener}s to notify about viewer transformation + * changes. Listeners will be notified when a new image has been rendered + * (immediately before that image is displayed) with the viewer transform + * used to render that image. + */ + public Listeners< TransformListener< AffineTransform3D > > transformListeners() + { + return paintedTransformListeners; + } + + /** + * DON'T USE THIS. + * <p> + * This is a work around for JDK bug + * https://bugs.openjdk.java.net/browse/JDK-8029147 which leads to + * ViewerPanel not being garbage-collected when ViewerFrame is closed. So + * instead we need to manually let go of resources... + */ + public void kill() + { + tripleBuffer.clear(); + } +} diff --git a/src/main/java/bdv/viewer/render/awt/BufferedImageRenderResult.java b/src/main/java/bdv/viewer/render/awt/BufferedImageRenderResult.java new file mode 100644 index 0000000000000000000000000000000000000000..4206a0d95136ce00d1f2ea984be54251ddaab250 --- /dev/null +++ b/src/main/java/bdv/viewer/render/awt/BufferedImageRenderResult.java @@ -0,0 +1,129 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer.render.awt; + +import bdv.util.AWTUtils; +import bdv.viewer.render.RenderResult; +import java.awt.geom.AffineTransform; +import java.awt.image.AffineTransformOp; +import java.awt.image.BufferedImage; +import net.imglib2.Interval; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.display.screenimage.awt.ARGBScreenImage; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.ARGBType; + +public class BufferedImageRenderResult implements RenderResult +{ + private final AffineTransform3D viewerTransform = new AffineTransform3D(); + + private int width; + private int height; + + private int[] data = new int[ 0 ]; + + private ARGBScreenImage screenImage; + + private BufferedImage bufferedImage; + + private double scaleFactor; + + @Override + public void init( final int width, final int height ) + { + if ( this.width == width && this.height == height ) + return; + + this.width = width; + this.height = height; + + if ( data.length < width * height ) + data = new int[ width * height ]; + + screenImage = new ARGBScreenImage( width, height, data ); + bufferedImage = AWTUtils.getBufferedImage( screenImage );; + } + + @Override + public RandomAccessibleInterval< ARGBType > getTargetImage() + { + return screenImage; + } + + public BufferedImage getBufferedImage() + { + return bufferedImage; + } + + @Override + public AffineTransform3D getViewerTransform() + { + return viewerTransform; + } + + @Override + public double getScaleFactor() + { + return scaleFactor; + } + + @Override + public void setScaleFactor( final double scaleFactor ) + { + this.scaleFactor = scaleFactor; + } + + @Override + public void setUpdated() + { + // ignored. BufferedImage is always up-to-date + } + + @Override + public void patch( final RenderResult patch, final Interval interval, final double ox, final double oy ) + { + final BufferedImageRenderResult biresult = ( BufferedImageRenderResult ) patch; + + final double s = scaleFactor / patch.getScaleFactor(); + final double tx = ox - interval.min( 0 ); + final double ty = oy - interval.min( 1 ); + final AffineTransform transform = new AffineTransform( s, 0, 0, s, tx, ty ); + final AffineTransformOp op = new AffineTransformOp( transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR ); + op.filter( biresult.getBufferedImage(), subImage( interval ) ); + } + + private BufferedImage subImage( final Interval interval ) + { + final int x = ( int ) interval.min( 0 ); + final int y = ( int ) interval.min( 1 ); + final int w = ( int ) interval.dimension( 0 ); + final int h = ( int ) interval.dimension( 1 ); + return bufferedImage.getSubimage( x, y, w, h ); + } +} diff --git a/src/main/java/bdv/viewer/state/SourceGroup.java b/src/main/java/bdv/viewer/state/SourceGroup.java index 5925b4efba4c03f6755eb7a2424d90b6f770209f..3d1d19baeec0f6c66a1ac8770b7566d3594dfcda 100644 --- a/src/main/java/bdv/viewer/state/SourceGroup.java +++ b/src/main/java/bdv/viewer/state/SourceGroup.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,6 +28,8 @@ */ package bdv.viewer.state; +import bdv.viewer.ViewerState; +import java.util.Collections; import java.util.SortedSet; import java.util.TreeSet; @@ -37,11 +38,16 @@ import bdv.viewer.DisplayMode; /** * TODO * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ +@Deprecated public class SourceGroup { - protected final TreeSet< Integer > sourceIds; + protected final SortedSet< Integer > sourceIds; + + private final ViewerState state; + + private final bdv.viewer.SourceGroup handle; protected String name; @@ -56,9 +62,18 @@ public class SourceGroup */ protected boolean isCurrent; + public SourceGroup( final ViewerState state, final bdv.viewer.SourceGroup handle ) + { + this.state = state; + this.handle = handle; + sourceIds = Collections.synchronizedSortedSet( new TreeSet<>() ); + } + public SourceGroup( final String name ) { - sourceIds = new TreeSet<>(); + this.state = null; + this.handle = null; + sourceIds = Collections.synchronizedSortedSet( new TreeSet<>() ); this.name = name; isActive = true; isCurrent = false; @@ -66,10 +81,15 @@ public class SourceGroup protected SourceGroup( final SourceGroup g ) { - sourceIds = new TreeSet<>( g.sourceIds ); - name = g.name; - isActive = g.isActive; - isCurrent = g.isCurrent; + this.state = null; + this.handle = null; + synchronized ( g.getSourceIds() ) + { + sourceIds = Collections.synchronizedSortedSet( new TreeSet<>( g.getSourceIds() ) ); + } + name = g.getName(); + isActive = g.isActive(); + isCurrent = g.isCurrent(); } public SourceGroup copy() @@ -79,26 +99,49 @@ public class SourceGroup public void addSource( final int sourceId ) { - sourceIds.add( sourceId ); + if ( handle == null ) + sourceIds.add( sourceId ); + else + { + state.addSourceToGroup( state.getSources().get( sourceId ), handle ); + sourceIds.clear(); + state.getSourcesInGroup( handle ).forEach( source -> sourceIds.add( state.getSources().indexOf( source ) ) ); + } } public void removeSource( final int sourceId ) { - sourceIds.remove( sourceId ); + if ( handle == null ) + sourceIds.remove( sourceId ); + else + { + state.removeSourceFromGroup( state.getSources().get( sourceId ), handle ); + sourceIds.clear(); + state.getSourcesInGroup( handle ).forEach( source -> sourceIds.add( state.getSources().indexOf( source ) ) ); + } } public SortedSet< Integer > getSourceIds() { + if ( handle != null ) + { + sourceIds.clear(); + state.getSourcesInGroup( handle ).forEach( source -> sourceIds.add( state.getSources().indexOf( source ) ) ); + } return sourceIds; } public String getName() { + if ( handle != null ) + name = state.getGroupName( handle ); return name; } public void setName( final String name ) { + if ( handle != null ) + state.setGroupName( handle, name ); this.name = name; } @@ -109,6 +152,8 @@ public class SourceGroup */ public boolean isActive() { + if ( handle != null ) + isActive = state.isGroupActive( handle ); return isActive; } @@ -117,6 +162,8 @@ public class SourceGroup */ public void setActive( final boolean isActive ) { + if ( handle != null ) + state.setGroupActive( handle, isActive ); this.isActive = isActive; } @@ -127,6 +174,8 @@ public class SourceGroup */ public boolean isCurrent() { + if ( handle != null ) + isCurrent = state.isCurrentGroup( handle ); return isCurrent; } @@ -135,6 +184,8 @@ public class SourceGroup */ public void setCurrent( final boolean isCurrent ) { + if ( handle != null && isCurrent ) + state.setCurrentGroup( handle ); this.isCurrent = isCurrent; } diff --git a/src/main/java/bdv/viewer/state/SourceState.java b/src/main/java/bdv/viewer/state/SourceState.java index 2e5b189f14aaec5aa1f288f5c3b29c133b694525..79034875ea7c4b6fae09a666149fb259a185c825 100644 --- a/src/main/java/bdv/viewer/state/SourceState.java +++ b/src/main/java/bdv/viewer/state/SourceState.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -36,74 +35,45 @@ import bdv.viewer.SourceAndConverter; /** * Source with some attached state needed for rendering. */ +@Deprecated public class SourceState< T > extends SourceAndConverter< T > { - protected static class Data - { - /** - * Whether the source is active (visible in {@link DisplayMode#FUSED} mode). - */ - protected boolean isActive; - - /** - * Whether the source is current. - */ - protected boolean isCurrent; - - public Data() - { - isActive = true; - isCurrent = false; - } - - protected Data( final Data d ) - { - isActive = d.isActive; - isCurrent = d.isCurrent; - } - - public Data copy() - { - return new Data( this ); - } - } - static class VolatileSourceState< T, V extends Volatile< T > > extends SourceState< V > { - public VolatileSourceState( final SourceAndConverter< V > soc, final ViewerState owner, final Data data ) + public VolatileSourceState( final SourceAndConverter< V > soc, final ViewerState owner, final SourceAndConverter< ? > handle ) { - super( soc, owner, data ); + super( soc, owner, handle ); } - public static < T, V extends Volatile< T > > VolatileSourceState< T, V > create( final SourceAndConverter< V > soc, final ViewerState owner, final Data data ) + public static < T, V extends Volatile< T > > VolatileSourceState< T, V > create( final SourceAndConverter< V > soc, final ViewerState owner, final SourceAndConverter< ? > handle ) { if ( soc == null ) return null; else - return new VolatileSourceState<>( soc, owner, data ); + return new VolatileSourceState<>( soc, owner, handle ); } } final ViewerState owner; - final Data data; + final SourceAndConverter< ? > handle; final VolatileSourceState< T, ? extends Volatile< T > > volatileSourceState; public SourceState( final SourceAndConverter< T > soc, final ViewerState owner ) { super( soc ); - data = new Data(); this.owner = owner; - volatileSourceState = VolatileSourceState.create( soc.asVolatile(), owner, data ); + handle = soc; + volatileSourceState = VolatileSourceState.create( soc.asVolatile(), owner, handle ); } - protected SourceState( final SourceAndConverter< T > soc, final ViewerState owner, final Data data ) + protected SourceState( final SourceAndConverter< T > soc, final ViewerState owner, final SourceAndConverter< ? > handle ) { super( soc ); - this.data = data; this.owner = owner; - volatileSourceState = VolatileSourceState.create( soc.asVolatile(), owner, data ); + this.handle = handle; + volatileSourceState = VolatileSourceState.create( soc.asVolatile(), owner, handle ); } /** @@ -113,9 +83,9 @@ public class SourceState< T > extends SourceAndConverter< T > protected SourceState( final SourceState< T > s, final ViewerState owner ) { super( s ); - data = s.data.copy(); this.owner = owner; - volatileSourceState = VolatileSourceState.create( s.volatileSourceAndConverter, owner, data ); + handle = s.handle; + volatileSourceState = VolatileSourceState.create( s.volatileSourceAndConverter, owner, handle ); } public SourceState< T > copy( final ViewerState owner ) @@ -130,7 +100,7 @@ public class SourceState< T > extends SourceAndConverter< T > */ public boolean isActive() { - return data.isActive; + return owner.state.isSourceActive( handle ); } /** @@ -140,7 +110,7 @@ public class SourceState< T > extends SourceAndConverter< T > { synchronized ( owner ) { - data.isActive = isActive; + owner.state.setSourceActive( handle, isActive ); } } @@ -151,7 +121,7 @@ public class SourceState< T > extends SourceAndConverter< T > */ public boolean isCurrent() { - return data.isCurrent; + return owner.state.isCurrentSource( handle ); } /** @@ -161,7 +131,7 @@ public class SourceState< T > extends SourceAndConverter< T > { synchronized ( owner ) { - data.isCurrent = isCurrent; + owner.state.setCurrentSource( handle ); } } @@ -178,5 +148,14 @@ public class SourceState< T > extends SourceAndConverter< T > { return volatileSourceState; } + + /** + * Get the SourceAndConverter that this SourceState represents. + * This handle is used to make modifications to the {@link bdv.viewer.ViewerState} + */ + public SourceAndConverter< ? > getHandle() + { + return handle; + } } diff --git a/src/main/java/bdv/viewer/state/ViewerState.java b/src/main/java/bdv/viewer/state/ViewerState.java index 1f1dc38fe31c7c79c95565e0ee12e6fdfcbcbcd3..75ac5fdc325883327f91a1a396f796e3068f3a30 100644 --- a/src/main/java/bdv/viewer/state/ViewerState.java +++ b/src/main/java/bdv/viewer/state/ViewerState.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -29,141 +28,89 @@ */ package bdv.viewer.state; -import static bdv.viewer.DisplayMode.FUSED; -import static bdv.viewer.DisplayMode.SINGLE; -import static bdv.viewer.Interpolation.NEARESTNEIGHBOR; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.SortedSet; -import java.util.TreeSet; - import bdv.util.MipmapTransforms; import bdv.viewer.DisplayMode; import bdv.viewer.Interpolation; import bdv.viewer.Source; import bdv.viewer.SourceAndConverter; +import bdv.viewer.BasicViewerState; +import bdv.viewer.SynchronizedViewerState; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import net.imglib2.realtransform.AffineTransform3D; +import static bdv.viewer.DisplayMode.FUSED; +import static bdv.viewer.DisplayMode.SINGLE; +import static bdv.viewer.Interpolation.NEARESTNEIGHBOR; + /** * Description of everything required to render the current image, such as the * current timepoint, the visible and current sources and groups respectively, * the viewer transformation, etc. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ +@Deprecated public class ViewerState { - private final ArrayList< SourceState< ? > > sources; - - /** - * read-only view of {@link #sources}. - */ - private final List< SourceState< ? > > unmodifiableSources; - - private final ArrayList< SourceGroup > groups; - - /** - * read-only view of {@link #groups}. - */ - private final List< SourceGroup > unmodifiableGroups; - - /** - * number of available timepoints. - */ - private int numTimepoints; - - /** - * Transformation set by the interactive viewer. Transforms from global - * coordinate system to viewer coordinate system. - */ - private final AffineTransform3D viewerTransform; - - /** - * Which interpolation method is currently used to render the display. - */ - private Interpolation interpolation; - - /** - * Is the display mode <em>single-source</em>? In <em>single-source</em> - * mode, only the current source (SPIM angle). Otherwise, in <em>fused</em> - * mode, all active sources are blended. - */ -// protected boolean singleSourceMode; - /** - * TODO - */ - private DisplayMode displayMode; - - /** - * The index of the current source. - * (In single-source mode only the current source is shown.) - */ - private int currentSource; - - /** - * The index of the current group. - * (In single-group mode only the sources in the current group are shown.) - */ - private int currentGroup; + public SynchronizedViewerState getState() + { + return state; + } - /** - * which timepoint is currently shown. - */ - private int currentTimepoint; + final SynchronizedViewerState state; public ViewerState( final List< SourceAndConverter< ? > > sources, final int numTimePoints ) { - this( sources, null, numTimePoints ); + this( sources, new ArrayList<>(), numTimePoints ); } /** - * * @param sources - * the {@link SourceAndConverter sources} to display. + * the {@link SourceAndConverter sources} to display. * @param numTimePoints - * number of available timepoints. + * number of available timepoints. */ public ViewerState( final List< SourceAndConverter< ? > > sources, final List< SourceGroup > sourceGroups, final int numTimePoints ) { - this.sources = new ArrayList<>( sources.size() ); - for ( final SourceAndConverter< ? > source : sources ) - this.sources.add( SourceState.create( source, this ) ); - unmodifiableSources = Collections.unmodifiableList( this.sources ); - groups = ( sourceGroups == null ) ? new ArrayList<>() : new ArrayList<>( sourceGroups ); - unmodifiableGroups = Collections.unmodifiableList( this.groups ); - this.numTimepoints = numTimePoints; + state = new SynchronizedViewerState( new BasicViewerState() ); + state.addSources( sources ); + state.setSourcesActive( sources, true ); + sourceGroups.forEach( sourceGroup -> { + final bdv.viewer.SourceGroup handle = new bdv.viewer.SourceGroup(); + state.addGroup( handle ); + state.setGroupName( handle, sourceGroup.getName() ); + state.setGroupActive( handle, sourceGroup.isActive() ); + sourceGroup.getSourceIds().forEach( i -> state.addSourceToGroup( sources.get( i ), handle ) ); + } ); + state.setNumTimepoints( numTimePoints ); + state.setViewerTransform( new AffineTransform3D() ); + state.setInterpolation( NEARESTNEIGHBOR ); + state.setDisplayMode( SINGLE ); + state.setCurrentSource( sources.isEmpty() ? null : sources.get( 0 ) ); + state.setCurrentGroup( state.getGroups().isEmpty() ? null : state.getGroups().get( 0 ) ); + state.setCurrentTimepoint( 0 ); + } - viewerTransform = new AffineTransform3D(); - interpolation = NEARESTNEIGHBOR; - displayMode = SINGLE; - currentSource = sources.isEmpty() ? -1 : 0; - currentGroup = groups.isEmpty() ? -1 : 0; - currentTimepoint = 0; + public ViewerState( final SynchronizedViewerState state ) + { + this.state = state; } /** * copy constructor + * * @param s */ protected ViewerState( final ViewerState s ) { - sources = new ArrayList<>( s.sources.size() ); - for ( final SourceState< ? > source : s.sources ) - this.sources.add( source.copy( this ) ); - unmodifiableSources = Collections.unmodifiableList( sources ); - groups = new ArrayList<>( s.groups.size() ); - for ( final SourceGroup group : s.groups ) - groups.add( group.copy() ); - unmodifiableGroups = Collections.unmodifiableList( groups ); - numTimepoints = s.numTimepoints; - viewerTransform = s.viewerTransform.copy(); - interpolation = s.interpolation; - displayMode = s.displayMode; - currentSource = s.currentSource; - currentGroup = s.currentGroup; - currentTimepoint = s.currentTimepoint; + synchronized ( s.state ) + { + state = new SynchronizedViewerState( new BasicViewerState( s.state ) ); + } } public synchronized ViewerState copy() @@ -180,84 +127,106 @@ public class ViewerState /** * Get the viewer transform. * - * @param t is set to the viewer transform. + * @param t + * is set to the viewer transform. */ - public synchronized void getViewerTransform( final AffineTransform3D t ) + public void getViewerTransform( final AffineTransform3D t ) { - t.set( viewerTransform ); + state.getViewerTransform( t ); } /** * Set the viewer transform. * - * @param t transform parameters. + * @param t + * transform parameters. */ - public synchronized void setViewerTransform( final AffineTransform3D t ) + public void setViewerTransform( final AffineTransform3D t ) { - viewerTransform.set( t ); + state.setViewerTransform( t ); } /** * Get the index of the current source. */ - public synchronized int getCurrentSource() + public int getCurrentSource() { - return currentSource; + synchronized ( state ) + { + return state.getSources().indexOf( state.getCurrentSource() ); + } } /** * Make the source with the given index current. */ - public synchronized void setCurrentSource( final int index ) + public void setCurrentSource( final int index ) { - final int minIndex = sources.isEmpty() ? -1 : 0; - if ( index >= minIndex && index < sources.size() ) + synchronized ( state ) { - sources.get( currentSource ).setCurrent( false ); - currentSource = index; - sources.get( currentSource ).setCurrent( true ); + state.setCurrentSource( state.getSources().get( index ) ); } } /** * Make the given source current. */ - public synchronized void setCurrentSource( final Source< ? > source ) + public void setCurrentSource( final Source< ? > source ) { - final int i = getSourceIndex( source ); - if ( i >= 0 ) - setCurrentSource( i ); + state.setCurrentSource( soc( source ) ); + } + + private SourceAndConverter< ? > soc( Source< ? > source ) + { + for ( SourceAndConverter< ? > soc : state.getSources() ) + if ( soc.getSpimSource() == source ) + return soc; + return null; } /** * Get the index of the current group. */ - public synchronized int getCurrentGroup() + public int getCurrentGroup() { - return currentGroup; + synchronized ( state ) + { + return state.getGroups().indexOf( state.getCurrentGroup() ); + } } /** * Make the group with the given index current. */ - public synchronized void setCurrentGroup( final int index ) + public void setCurrentGroup( final int index ) { - if ( index >= 0 && index < groups.size() ) + synchronized ( state ) { - groups.get( currentGroup ).setCurrent( false ); - currentGroup = index; - groups.get( currentGroup ).setCurrent( true ); + state.setCurrentGroup( state.getGroups().get( index ) ); } } /** * Make the given group current. */ - public synchronized void setCurrentGroup( final SourceGroup group ) + public void setCurrentGroup( final SourceGroup group ) { - final int i = getGroupIndex( group ); - if ( i >= 0 ) - setCurrentGroup( i ); + synchronized ( state ) + { + state.setCurrentGroup( getHandle( group ) ); + } + } + + private bdv.viewer.SourceGroup getHandle( final SourceGroup group ) + { + for ( bdv.viewer.SourceGroup handle : state.getGroups() ) + { + SourceGroup g = new SourceGroup( state.getGroupName( handle ) ); + state.getSourcesInGroup( handle ).forEach( s -> g.addSource( state.getSources().indexOf( s ) ) ); + if ( g.equals( group ) ) + return handle; + } + return null; } /** @@ -265,19 +234,20 @@ public class ViewerState * * @return interpolation method. */ - public synchronized Interpolation getInterpolation() + public Interpolation getInterpolation() { - return interpolation; + return state.getInterpolation(); } /** * Set the interpolation method. * - * @param method interpolation method. + * @param method + * interpolation method. */ - public synchronized void setInterpolation( final Interpolation method ) + public void setInterpolation( final Interpolation method ) { - interpolation = method; + state.setInterpolation( method ); } /** @@ -290,9 +260,9 @@ public class ViewerState * @deprecated replaced by {@link #getDisplayMode()} */ @Deprecated - public synchronized boolean isSingleSourceMode() + public boolean isSingleSourceMode() { - return displayMode == SINGLE; + return state.getDisplayMode() == SINGLE; } /** @@ -301,13 +271,13 @@ public class ViewerState * angle) is shown. In <em>fused</em> mode, all active sources are blended. * * @param singleSourceMode - * If true, set <em>single-source</em> mode. If false, set - * <em>fused</em> mode. + * If true, set <em>single-source</em> mode. If false, set + * <em>fused</em> mode. * * @deprecated replaced by {@link #setDisplayMode(DisplayMode)} */ @Deprecated - public synchronized void setSingleSourceMode( final boolean singleSourceMode ) + public void setSingleSourceMode( final boolean singleSourceMode ) { if ( singleSourceMode ) setDisplayMode( SINGLE ); @@ -326,11 +296,11 @@ public class ViewerState * </ul> * * @param mode - * the display mode + * the display mode */ - public synchronized void setDisplayMode( final DisplayMode mode ) + public void setDisplayMode( final DisplayMode mode ) { - displayMode = mode; + state.setDisplayMode( mode ); } /** @@ -345,9 +315,9 @@ public class ViewerState * * @return the current display mode */ - public synchronized DisplayMode getDisplayMode() + public DisplayMode getDisplayMode() { - return displayMode; + return state.getDisplayMode(); } /** @@ -355,20 +325,20 @@ public class ViewerState * * @return current timepoint index */ - public synchronized int getCurrentTimepoint() + public int getCurrentTimepoint() { - return currentTimepoint; + return state.getCurrentTimepoint(); } /** * Set the current timepoint index. * * @param timepoint - * timepoint index. + * timepoint index. */ - public synchronized void setCurrentTimepoint( final int timepoint ) + public void setCurrentTimepoint( final int timepoint ) { - currentTimepoint = timepoint; + state.setCurrentTimepoint( timepoint ); } /** @@ -378,7 +348,16 @@ public class ViewerState */ public List< SourceState< ? > > getSources() { - return unmodifiableSources; + synchronized ( state ) + { + List< SourceState< ? > > sourceStates = new ArrayList<>(); + for ( SourceAndConverter< ? > source : state.getSources() ) + { + final SourceState< ? > ss = new SourceState<>( source, this ); + sourceStates.add( ss ); + } + return sourceStates; + } } /** @@ -388,9 +367,11 @@ public class ViewerState */ public int numSources() { - return sources.size(); + return state.getSources().size(); } + final Map< bdv.viewer.SourceGroup, SourceGroup > handleToSourceGroup = new HashMap<>(); + /** * Returns a list of all source groups. * @@ -398,7 +379,18 @@ public class ViewerState */ public List< SourceGroup > getSourceGroups() { - return unmodifiableGroups; + synchronized ( state ) + { + List< SourceGroup > sourceGroups = new ArrayList<>(); + for ( bdv.viewer.SourceGroup handle : state.getGroups() ) + { + sourceGroups.add( handleToSourceGroup.computeIfAbsent( handle, h -> { + SourceGroup g = new SourceGroup( state, handle ); + return g; + } ) ); + } + return sourceGroups; + } } /** @@ -408,139 +400,83 @@ public class ViewerState */ public int numSourceGroups() { - return groups.size(); + synchronized ( state ) + { + return state.getGroups().size(); + } } - public synchronized void addSource( final SourceAndConverter< ? > source ) + public void addSource( final SourceAndConverter< ? > source ) { - sources.add( SourceState.create( source, this ) ); - if ( currentSource < 0 ) - currentSource = 0; + synchronized ( state ) + { + state.addSource( source ); + state.setSourceActive( source, true ); + } } - public synchronized void removeSource( final Source< ? > source ) + public void removeSource( final Source< ? > source ) { - for ( int i = 0; i < sources.size(); ) + synchronized ( state ) { - final SourceState< ? > s = sources.get( i ); - if ( s.getSpimSource() == source ) - removeSource( i ); - else - i++; + state.removeSource( soc ( source ) ); } } protected void removeSource( final int index ) { - sources.remove( index ); - if ( sources.isEmpty() ) - currentSource = -1; - else if ( currentSource == index ) - currentSource = 0; - else if ( currentSource > index ) - --currentSource; - for( final SourceGroup group : groups ) + synchronized ( state ) { - final SortedSet< Integer > ids = group.getSourceIds(); - final ArrayList< Integer > oldids = new ArrayList<>( ids ); - ids.clear(); - for ( final int id : oldids ) - { - if ( id < index ) - ids.add( id ); - else if ( id > index ) - ids.add( id - 1 ); - } + state.removeSource( state.getSources().get( index ) ); } } - public synchronized void addGroup( final SourceGroup group ) + public void addGroup( final SourceGroup group ) { - if ( !groups.contains( group ) ) + synchronized ( state ) { - groups.add( group ); - if ( currentGroup < 0 ) - currentGroup = 0; + final bdv.viewer.SourceGroup handle = new bdv.viewer.SourceGroup(); + state.addGroup( handle ); + state.setGroupName( handle, group.getName() ); + state.setGroupActive( handle, group.isActive() ); + group.getSourceIds().forEach( i -> state.addSourceToGroup( state.getSources().get( i ), handle ) ); } } - public synchronized void removeGroup( final SourceGroup group ) + public void removeGroup( final SourceGroup group ) { - final int i = groups.indexOf( group ); - if ( i >= 0 ) - removeGroup( i ); + synchronized ( state ) + { + state.removeGroup( getHandle( group ) ); + } } protected void removeGroup( final int index ) { - groups.remove( index ); - if ( groups.isEmpty() ) - currentGroup = -1; - else if ( currentGroup == index ) - currentGroup = 0; - else if ( currentGroup > index ) - --currentGroup; + state.removeGroup( state.getGroups().get( index ) ); } - public synchronized boolean isSourceVisible( final int index ) + public boolean isSourceVisible( final int index ) { - switch ( displayMode ) + synchronized ( state ) { - case SINGLE: - return ( index == currentSource ) && isPresent( index ); - case GROUP: - return groups.get( currentGroup ).getSourceIds().contains( index ) && isPresent( index ); - case FUSED: - return sources.get( index ).isActive() && isPresent( index ); - case FUSEDGROUP: - default: - for ( final SourceGroup group : groups ) - if ( group.isActive() && group.getSourceIds().contains( index ) && isPresent( index ) ) - return true; - return false; + return state.isSourceVisibleAndPresent( state.getSources().get( index ) ); } } - private boolean isPresent( final int sourceId ) - { - return sources.get( sourceId ).getSpimSource().isPresent( currentTimepoint ); - } - /** * Returns a list of the indices of all currently visible sources. * * @return indices of all currently visible sources. */ - public synchronized List< Integer > getVisibleSourceIndices() + public List< Integer > getVisibleSourceIndices() { - final ArrayList< Integer > visible = new ArrayList<>(); - switch ( displayMode ) + synchronized ( state ) { - case SINGLE: - if ( currentSource >= 0 && isPresent( currentSource ) ) - visible.add( currentSource ); - break; - case GROUP: - for ( final int sourceId : groups.get( currentGroup ).getSourceIds() ) - if ( isPresent( sourceId ) ) - visible.add( sourceId ); - break; - case FUSED: - for ( int i = 0; i < sources.size(); ++i ) - if ( sources.get( i ).isActive() && isPresent( i ) ) - visible.add( i ); - break; - case FUSEDGROUP: - final TreeSet< Integer > gactive = new TreeSet<>(); - for ( final SourceGroup group : groups ) - if ( group.isActive() ) - gactive.addAll( group.getSourceIds() ); - for ( final int sourceId : new ArrayList<>( gactive ) ) - if ( isPresent( sourceId ) ) - visible.add( sourceId ); - break; + final List< SourceAndConverter< ? > > sources = new ArrayList<>( state.getVisibleAndPresentSources() ); + sources.sort( state.sourceOrder() ); + return sources.stream().map( state.getSources()::indexOf ).collect( Collectors.toList() ); } - return visible; } /* @@ -551,28 +487,36 @@ public class ViewerState * Get the mipmap level that best matches the given screen scale for the given source. * * @param screenScaleTransform - * screen scale, transforms screen coordinates to viewer coordinates. + * screen scale, transforms screen coordinates to viewer coordinates. + * * @return mipmap level */ - public synchronized int getBestMipMapLevel( final AffineTransform3D screenScaleTransform, final int sourceIndex ) + public int getBestMipMapLevel( final AffineTransform3D screenScaleTransform, final int sourceIndex ) { - return getBestMipMapLevel( screenScaleTransform, sources.get( sourceIndex ).getSpimSource() ); + synchronized ( state ) + { + return getBestMipMapLevel( screenScaleTransform, state.getSources().get( sourceIndex ).getSpimSource() ); + } } /** * Get the mipmap level that best matches the given screen scale for the given source. * * @param screenScaleTransform - * screen scale, transforms screen coordinates to viewer coordinates. + * screen scale, transforms screen coordinates to viewer coordinates. + * * @return mipmap level */ - public synchronized int getBestMipMapLevel( final AffineTransform3D screenScaleTransform, final Source< ? > source ) + public int getBestMipMapLevel( final AffineTransform3D screenScaleTransform, final Source< ? > source ) { - final AffineTransform3D screenTransform = new AffineTransform3D(); - getViewerTransform( screenTransform ); - screenTransform.preConcatenate( screenScaleTransform ); + synchronized ( state ) + { + final AffineTransform3D screenTransform = new AffineTransform3D(); + getViewerTransform( screenTransform ); + screenTransform.preConcatenate( screenScaleTransform ); - return MipmapTransforms.getBestMipMapLevel( screenTransform, source, currentTimepoint ); + return MipmapTransforms.getBestMipMapLevel( screenTransform, source, state.getCurrentTimepoint() ); + } } /** @@ -580,20 +524,20 @@ public class ViewerState * * @return the number of timepoints. */ - public synchronized int getNumTimepoints() + public int getNumTimepoints() { - return numTimepoints; + return state.getNumTimepoints(); } /** * Set the number of timepoints. * * @param numTimepoints - * the number of timepoints. + * the number of timepoints. */ - public synchronized void setNumTimepoints( final int numTimepoints ) + public void setNumTimepoints( final int numTimepoints ) { - this.numTimepoints = numTimepoints; + state.setNumTimepoints( numTimepoints ); } /** @@ -606,31 +550,7 @@ public class ViewerState */ public void kill() { - sources.clear(); - groups.clear(); - } - - /** - * Get index of (first) {@link SourceState} that matches the given - * {@link Source} or {@code -1} if not found. - */ - private int getSourceIndex( final Source< ? > source ) - { - for ( int i = 0; i < sources.size(); ++i ) - { - final SourceState< ? > s = sources.get( i ); - if ( s.getSpimSource() == source ) - return i; - } - return -1; - } - - /** - * Get index of (first) {@link SourceGroup} that matches the given - * {@code group} or {@code -1} if not found. - */ - private int getGroupIndex( final SourceGroup group ) - { - return groups.indexOf( group ); + state.clearGroups(); + state.clearSources(); } } diff --git a/src/main/java/bdv/viewer/state/XmlIoViewerState.java b/src/main/java/bdv/viewer/state/XmlIoViewerState.java index 5ec74f5e47bc1531a5f9860bb90c2e511a46ec4c..97331ae6ae6872504e9572998b39621704270a5b 100644 --- a/src/main/java/bdv/viewer/state/XmlIoViewerState.java +++ b/src/main/java/bdv/viewer/state/XmlIoViewerState.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/net/imglib2/display/ARGBARGBColorConverter.java b/src/main/java/net/imglib2/display/ARGBARGBColorConverter.java index 0b0998b090f46202c41478575cafcad6582b6b60..cc5a3f23e7d2ac1fc63eddff7eb5e669f241823b 100644 --- a/src/main/java/net/imglib2/display/ARGBARGBColorConverter.java +++ b/src/main/java/net/imglib2/display/ARGBARGBColorConverter.java @@ -1,9 +1,8 @@ /*- * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/java/net/imglib2/display/ColorConverter.java b/src/main/java/net/imglib2/display/ColorConverter.java index a257883d495435901f7be093c7f399a4304573bd..ffde94ae86b72a7ecd0ed7617d3a9f40e079ddf2 100644 --- a/src/main/java/net/imglib2/display/ColorConverter.java +++ b/src/main/java/net/imglib2/display/ColorConverter.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -33,9 +32,9 @@ import net.imglib2.type.numeric.ARGBType; public interface ColorConverter extends LinearRange { - public ARGBType getColor(); + ARGBType getColor(); - public void setColor( final ARGBType c ); + void setColor( final ARGBType c ); - public boolean supportsColor(); + boolean supportsColor(); } diff --git a/src/main/java/net/imglib2/display/RealARGBColorConverter.java b/src/main/java/net/imglib2/display/RealARGBColorConverter.java index bf4019db065b7f1e47a2082e694a22b4e30248d2..5a66799fc23f3e284c9e66f81d9a33ca69c71a0b 100644 --- a/src/main/java/net/imglib2/display/RealARGBColorConverter.java +++ b/src/main/java/net/imglib2/display/RealARGBColorConverter.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -36,7 +35,7 @@ import net.imglib2.type.numeric.RealType; public interface RealARGBColorConverter< R extends RealType< ? > > extends ColorConverter, Converter< R, ARGBType > { - public static < R extends RealType< ? > > RealARGBColorConverter< R > create( final R type, final double min, final double max ) + static < R extends RealType< ? > > RealARGBColorConverter< R > create( final R type, final double min, final double max ) { return Instances.create( type, min, max ); } @@ -58,7 +57,7 @@ class Instances provider = new ClassCopyProvider<>( Imp.class, RealARGBColorConverter.class, double.class, double.class ); } } - return provider.newInstanceForKey( type, min, max ); + return provider.newInstanceForKey( type.getClass(), min, max ); } public static class Imp< R extends RealType< ? > > implements RealARGBColorConverter< R > diff --git a/src/main/java/net/imglib2/display/ScaledARGBConverter.java b/src/main/java/net/imglib2/display/ScaledARGBConverter.java index 8b8e616574ea018717d521c7297eebe2feb2231d..dd9f50cdeea6b554925b2a99f72aa51d3265ac7d 100644 --- a/src/main/java/net/imglib2/display/ScaledARGBConverter.java +++ b/src/main/java/net/imglib2/display/ScaledARGBConverter.java @@ -1,9 +1,8 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies + * BigDataViewer core classes with minimal dependencies. * %% - * Copyright (C) 2012 - 2016 Tobias Pietzsch, Stephan Saalfeld, Stephan Preibisch, - * Jean-Yves Tinevez, HongKee Moon, Johannes Schindelin, Curtis Rueden, John Bogovic + * Copyright (C) 2012 - 2020 BigDataViewer developers. * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: diff --git a/src/main/resources/bdv/ui/splitpanel/leftdoublearrow_tiny.png b/src/main/resources/bdv/ui/splitpanel/leftdoublearrow_tiny.png new file mode 100644 index 0000000000000000000000000000000000000000..9059a79198b68dd5ac9553667fe3ac5b6516ca46 Binary files /dev/null and b/src/main/resources/bdv/ui/splitpanel/leftdoublearrow_tiny.png differ diff --git a/src/main/resources/bdv/ui/splitpanel/rightdoublearrow_tiny.png b/src/main/resources/bdv/ui/splitpanel/rightdoublearrow_tiny.png new file mode 100644 index 0000000000000000000000000000000000000000..4cf801e7ee181796b6366a782c603594b6c764d1 Binary files /dev/null and b/src/main/resources/bdv/ui/splitpanel/rightdoublearrow_tiny.png differ diff --git a/src/main/resources/bdv/ui/viewermodepanel/fusion_mode.png b/src/main/resources/bdv/ui/viewermodepanel/fusion_mode.png new file mode 100644 index 0000000000000000000000000000000000000000..cd2fd7e61e2e776c6b34e540a7720effca5664c9 Binary files /dev/null and b/src/main/resources/bdv/ui/viewermodepanel/fusion_mode.png differ diff --git a/src/main/resources/bdv/ui/viewermodepanel/fusion_mode.svg b/src/main/resources/bdv/ui/viewermodepanel/fusion_mode.svg new file mode 100644 index 0000000000000000000000000000000000000000..a246bd94e7ec4d5442321af9ed8f8f1d005a28bd --- /dev/null +++ b/src/main/resources/bdv/ui/viewermodepanel/fusion_mode.svg @@ -0,0 +1,115 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- + #%L + BigDataViewer core classes with minimal dependencies. + %% + Copyright (C) 2012 - 2020 BigDataViewer developers. + %% + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + #L% + --> + +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="40" + height="39.995888" + viewBox="0 0 10.583333 10.582246" + version="1.1" + id="svg8" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="fusion_mode.svg" + inkscape:export-filename="/home/random/Development/imagej/bigdataviewer/bigdataviewer-core/src/main/resources/bdv/ui/viewermodepanel/fusion_mode.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96"> + <defs + id="defs2" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#4a4a4a" + borderopacity="1" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="11.313709" + inkscape:cx="28.264648" + inkscape:cy="15.375783" + inkscape:document-units="mm" + inkscape:current-layer="g824" + showgrid="false" + units="px" + inkscape:pagecheckerboard="true" + inkscape:window-width="1920" + inkscape:window-height="1023" + inkscape:window-x="0" + inkscape:window-y="33" + inkscape:window-maximized="1" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" /> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-2.4093051e-8,-282.97707)"> + <g + id="g824" + transform="translate(0,-3.4406551)"> + <path + style="opacity:1;fill:#2e2e2e;fill-opacity:0.4545455;fill-rule:nonzero;stroke:#000000;stroke-width:0.5333333;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + d="M 13.224609 5.5175781 C 12.198936 5.5175781 11.373047 6.3435658 11.373047 7.3691406 L 11.373047 11.087891 L 19.203125 11.087891 C 28.329593 11.087891 28.028457 11.065854 28.552734 11.8125 L 28.830078 12.208984 L 28.861328 20.791016 L 28.890625 28.623047 L 32.628906 28.623047 C 33.65458 28.623047 34.480469 27.797097 34.480469 26.771484 L 34.480469 7.3691406 C 34.480469 6.3435658 33.65458 5.5175781 32.628906 5.5175781 L 13.224609 5.5175781 z " + transform="matrix(0.26458333,0,0,0.26458333,2.4093051e-8,286.41773)" + id="rect815-3" /> + <path + style="opacity:1;fill:#2e2e2e;fill-opacity:0.4545455;fill-rule:nonzero;stroke:#000000;stroke-width:0.5333333;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + d="M 7.3710938 11.373047 C 6.3454271 11.373047 5.5195312 12.199035 5.5195312 13.224609 L 5.5195312 32.626953 C 5.5195312 33.652528 6.3454271 34.476562 7.3710938 34.476562 L 26.775391 34.476562 C 27.801057 34.476562 28.625 33.652528 28.625 32.626953 L 28.625 28.894531 L 20.814453 28.890625 C 13.22531 28.888168 12.225401 28.868164 11.884766 28.699219 C 11.673808 28.595282 11.3961 28.338191 11.265625 28.126953 C 11.033853 27.752024 11.027344 27.545226 11.027344 19.154297 L 11.027344 11.373047 L 7.3710938 11.373047 z " + id="rect815" + transform="matrix(0.26458333,0,0,0.26458333,2.4093051e-8,286.41773)" /> + <path + style="opacity:1;fill:#878787;fill-opacity:0.49416342;stroke:none;stroke-width:4.15748024;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0" + d="m 13.048835,28.5158 c -0.859221,-0.0424 -1.06224,-0.08004 -1.227539,-0.227579 -0.462376,-0.412699 -0.449152,-0.180172 -0.50409,-8.863998 l -0.05115,-8.084579 7.946868,0.04983 c 4.605136,0.02888 8.114124,0.08595 8.344615,0.135737 0.289611,0.06255 0.475847,0.176181 0.68501,0.417944 l 0.287262,0.332036 2.65e-4,6.371332 c 8.9e-5,3.504232 0.02633,7.17677 0.05822,8.161196 l 0.05799,1.789864 -7.283971,-0.01549 c -4.006184,-0.0085 -7.747239,-0.03835 -8.313455,-0.06629 z" + id="path4776" + inkscape:connector-curvature="0" + transform="matrix(0.26458333,0,0,0.26458333,2.4093051e-8,286.41773)" /> + </g> + </g> +</svg> diff --git a/src/main/resources/bdv/ui/viewermodepanel/grouping_mode.png b/src/main/resources/bdv/ui/viewermodepanel/grouping_mode.png new file mode 100644 index 0000000000000000000000000000000000000000..69aebc0bdf841d0404c6b976aeb436978373c4ec Binary files /dev/null and b/src/main/resources/bdv/ui/viewermodepanel/grouping_mode.png differ diff --git a/src/main/resources/bdv/ui/viewermodepanel/grouping_mode.svg b/src/main/resources/bdv/ui/viewermodepanel/grouping_mode.svg new file mode 100644 index 0000000000000000000000000000000000000000..70fa601515b9f610c6e3a5161e60080c4a4b6da8 --- /dev/null +++ b/src/main/resources/bdv/ui/viewermodepanel/grouping_mode.svg @@ -0,0 +1,146 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- + #%L + BigDataViewer core classes with minimal dependencies. + %% + Copyright (C) 2012 - 2020 BigDataViewer developers. + %% + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + #L% + --> + +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="40" + height="40" + viewBox="0 0 10.583333 10.583334" + version="1.1" + id="svg8" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="grouping_mode.svg" + inkscape:export-filename="/home/random/Development/imagej/bigdataviewer/bigdataviewer-core/src/main/resources/bdv/ui/viewermodepanel/grouping_mode.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96"> + <defs + id="defs2" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#4a4a4a" + borderopacity="1" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="7.1200577" + inkscape:cx="-10.128712" + inkscape:cy="23.002358" + inkscape:document-units="mm" + inkscape:current-layer="g835" + showgrid="false" + units="px" + inkscape:pagecheckerboard="true" + inkscape:window-width="1920" + inkscape:window-height="1023" + inkscape:window-x="0" + inkscape:window-y="33" + inkscape:window-maximized="1" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" /> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-5.555898e-5,-282.97696)"> + <g + id="g835" + transform="translate(0,-3.4396431)"> + <path + style="opacity:1;fill:#2e2e2e;fill-opacity:0.22568093;fill-rule:nonzero;stroke:#000000;stroke-width:0.5333333;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + d="M 23.878906 3.2832031 C 23.512957 3.2373067 23.139925 3.4095307 22.943359 3.75 L 20.339844 8.2578125 L 5.0351562 8.2578125 C 4.4648104 8.2578125 4.0058594 8.7168165 4.0058594 9.2871094 L 4.0058594 24.736328 C 4.0058594 25.306621 4.4648104 25.767578 5.0351562 25.767578 L 14.421875 25.767578 L 17.205078 36.154297 C 17.351909 36.702253 17.910947 37.025552 18.458984 36.878906 L 33.304688 32.900391 C 33.852726 32.753556 34.176128 32.194478 34.029297 31.646484 L 31.300781 21.462891 L 36.880859 11.796875 C 37.142946 11.342916 36.987196 10.765828 36.533203 10.503906 L 24.236328 3.4023438 C 24.12283 3.3367973 24.000889 3.2985019 23.878906 3.2832031 z " + transform="matrix(0.26458333,0,0,0.26458333,5.555898e-5,286.4166)" + id="rect4760" /> + <g + style="stroke-width:1.93506229;opacity:0.217" + transform="matrix(0.51677959,0,0,0.51677889,1.0233023,139.78716)" + id="g1509"> + <rect + style="opacity:1;fill:#2e2e2e;fill-opacity:0.09338521;fill-rule:nonzero;stroke:#000000;stroke-width:0.27305877;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect815" + width="8.9647007" + height="8.963871" + x="0.070555583" + y="287.96555" + rx="0.52709115" + ry="0.52704239" /> + </g> + <g + style="stroke-width:1.94521153;opacity:0.217" + transform="matrix(0.49656638,-0.13305461,0.13305429,0.49656554,-35.4577,149.73876)" + id="g1505"> + <rect + style="opacity:1;fill:#2e2e2e;fill-opacity:0.09338521;fill-rule:nonzero;stroke:#000000;stroke-width:0.27449092;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect815-3" + width="8.9647007" + height="8.963871" + x="1.5480773" + y="286.48828" + rx="0.52709115" + ry="0.52704239" /> + </g> + <g + style="stroke-width:2.10530305;opacity:0.217" + transform="matrix(0.41135457,0.23749567,-0.23749535,0.41135401,74.556992,168.71925)" + id="g1509-5"> + <rect + style="opacity:1;fill:#2e2e2e;fill-opacity:0.09338523;fill-rule:nonzero;stroke:#000000;stroke-width:0.29708165;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect815-35" + width="8.9647007" + height="8.963871" + x="0.070555463" + y="287.96555" + rx="0.52709115" + ry="0.52704239" /> + </g> + </g> + </g> +</svg> diff --git a/src/main/resources/bdv/ui/viewermodepanel/linear.png b/src/main/resources/bdv/ui/viewermodepanel/linear.png new file mode 100644 index 0000000000000000000000000000000000000000..05c16670bb2394bfb6c8690ad8b1f3f2569e3683 Binary files /dev/null and b/src/main/resources/bdv/ui/viewermodepanel/linear.png differ diff --git a/src/main/resources/bdv/ui/viewermodepanel/linear.svg b/src/main/resources/bdv/ui/viewermodepanel/linear.svg new file mode 100644 index 0000000000000000000000000000000000000000..a4a64dcc28a820e41dea7e68ec8d5a018029f70e --- /dev/null +++ b/src/main/resources/bdv/ui/viewermodepanel/linear.svg @@ -0,0 +1,128 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- + #%L + BigDataViewer core classes with minimal dependencies. + %% + Copyright (C) 2012 - 2020 BigDataViewer developers. + %% + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + #L% + --> + +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="40" + height="40" + viewBox="0 0 10.583333 10.583334" + version="1.1" + id="svg8" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="linear.svg" + inkscape:export-filename="/home/random/Development/imagej/bigdataviewer/bigdataviewer-core/src/main/resources/bdv/ui/viewermodepanel/linear.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96"> + <defs + id="defs2"> + <linearGradient + inkscape:collect="always" + id="linearGradient891"> + <stop + style="stop-color:#5a5a5a;stop-opacity:1" + offset="0" + id="stop887" /> + <stop + style="stop-color:#5a5a5a;stop-opacity:0.0233463" + offset="1" + id="stop889" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient891" + id="linearGradient893" + x1="4.1781349" + y1="286.73645" + x2="2.4347966" + y2="284.99265" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.82135529,0,0,0.82135529,2.0319136,52.584295)" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="6.2181202" + inkscape:cx="3.4996214" + inkscape:cy="-12.93145" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + units="px" + inkscape:pagecheckerboard="true" + inkscape:window-width="1920" + inkscape:window-height="1023" + inkscape:window-x="0" + inkscape:window-y="33" + inkscape:window-maximized="1" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" /> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-6.9485441e-7,-282.97708)"> + <rect + style="opacity:1;fill:url(#linearGradient893);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.34395838;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect885" + width="6.0729942" + height="6.0729942" + x="2.2551703" + y="285.23227" + rx="0.48999995" + ry="0.48999995" /> + </g> +</svg> diff --git a/src/main/resources/bdv/ui/viewermodepanel/nearest.png b/src/main/resources/bdv/ui/viewermodepanel/nearest.png new file mode 100644 index 0000000000000000000000000000000000000000..40b4806a4adf1f8204adc9f8c16e0be8d700a691 Binary files /dev/null and b/src/main/resources/bdv/ui/viewermodepanel/nearest.png differ diff --git a/src/main/resources/bdv/ui/viewermodepanel/nearest.svg b/src/main/resources/bdv/ui/viewermodepanel/nearest.svg new file mode 100644 index 0000000000000000000000000000000000000000..456b4d112d667ff63424bb7a090b2b3cc945f62a --- /dev/null +++ b/src/main/resources/bdv/ui/viewermodepanel/nearest.svg @@ -0,0 +1,100 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- + #%L + BigDataViewer core classes with minimal dependencies. + %% + Copyright (C) 2012 - 2020 BigDataViewer developers. + %% + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + #L% + --> + +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="40" + height="40" + viewBox="0 0 10.583333 10.583334" + version="1.1" + id="svg8" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="nearest.svg" + inkscape:export-filename="/home/random/Development/imagej/bigdataviewer/bigdataviewer-core/src/main/resources/bdv/ui/viewermodepanel/nearest.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96"> + <defs + id="defs2" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="8.40625" + inkscape:cx="-20.858952" + inkscape:cy="14.91223" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + units="px" + inkscape:pagecheckerboard="true" + inkscape:window-width="1920" + inkscape:window-height="1023" + inkscape:window-x="0" + inkscape:window-y="33" + inkscape:window-maximized="1" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" /> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-1.0335785e-4,-282.97696)"> + <path + style="opacity:1;fill:#5a5a5a;fill-opacity:1;stroke:none;stroke-width:1.10000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0" + d="m 6.7112716,285.2712 -0.00273,0.66224 -0.00312,0.75149 -0.7514942,0.003 -0.7514942,0.003 v 0.74798 0.74838 H 4.4478234 3.6932109 v 0.75461 0.75462 H 2.9385991 2.2931247 v 1.1974 c 0,0.17916 0.125953,0.32775 0.2946731,0.36211 h 2.6395846 2.7635342 c 0.1363515,-0.0278 0.2432168,-0.13067 0.2798615,-0.26349 v -2.77367 -2.67701 c -0.043122,-0.1563 -0.1843167,-0.27089 -0.3546991,-0.27089 z" + id="path4774" + inkscape:connector-curvature="0" /> + </g> +</svg> diff --git a/src/main/resources/bdv/ui/viewermodepanel/single_mode.png b/src/main/resources/bdv/ui/viewermodepanel/single_mode.png new file mode 100644 index 0000000000000000000000000000000000000000..5004a50d23df51705b4a325befae3702b8752de2 Binary files /dev/null and b/src/main/resources/bdv/ui/viewermodepanel/single_mode.png differ diff --git a/src/main/resources/bdv/ui/viewermodepanel/single_mode.svg b/src/main/resources/bdv/ui/viewermodepanel/single_mode.svg new file mode 100644 index 0000000000000000000000000000000000000000..ee61526d19f7910d8404df549c14d44c37a3b466 --- /dev/null +++ b/src/main/resources/bdv/ui/viewermodepanel/single_mode.svg @@ -0,0 +1,122 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- + #%L + BigDataViewer core classes with minimal dependencies. + %% + Copyright (C) 2012 - 2020 BigDataViewer developers. + %% + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + #L% + --> + +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="40" + height="40" + viewBox="0 0 10.583333 10.583336" + version="1.1" + id="svg8" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="single_mode.svg" + inkscape:export-filename="/home/random/Development/imagej/bigdataviewer/bigdataviewer-core/src/main/resources/bdv/ui/viewermodepanel/single_mode.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96"> + <defs + id="defs2" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#4a4a4a" + borderopacity="1" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="11.607393" + inkscape:cx="13.046863" + inkscape:cy="10.536255" + inkscape:document-units="mm" + inkscape:current-layer="g1509" + showgrid="false" + units="px" + inkscape:pagecheckerboard="true" + inkscape:window-width="1716" + inkscape:window-height="1080" + inkscape:window-x="0" + inkscape:window-y="33" + inkscape:window-maximized="0" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" /> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-2.7536975e-5,-282.97592)"> + <g + id="g823" + transform="translate(2.7512882e-5,-3.4407212)"> + <rect + ry="0.48980322" + rx="0.48984864" + y="287.81021" + x="2.9690354" + height="6.2210817" + width="6.221662" + id="rect815-3" + style="opacity:1;fill:#2e2e2e;fill-opacity:0;fill-rule:nonzero;stroke:#000000;stroke-width:0.141;stroke-miterlimit:4;stroke-dasharray:0.564, 0.564;stroke-dashoffset:0;stroke-opacity:1" /> + <g + style="stroke-width:1.07603621" + transform="matrix(0.92933683,0,0,0.92933672,0.00498568,20.982005)" + id="g1509"> + <rect + style="opacity:1;fill:#2e2e2e;fill-opacity:0.4545455;fill-rule:nonzero;stroke:#000000;stroke-width:0.15184066;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect815" + width="6.6946864" + height="6.6940637" + x="1.4931475" + y="288.8129" + rx="0.52725768" + ry="0.52704239" /> + </g> + </g> + </g> +</svg> diff --git a/src/main/resources/bdv/ui/viewermodepanel/source_mode.png b/src/main/resources/bdv/ui/viewermodepanel/source_mode.png new file mode 100644 index 0000000000000000000000000000000000000000..0c4bc54bd33ae20c984aa0e7a3b3bf2d2463143d Binary files /dev/null and b/src/main/resources/bdv/ui/viewermodepanel/source_mode.png differ diff --git a/src/main/resources/bdv/ui/viewermodepanel/source_mode.svg b/src/main/resources/bdv/ui/viewermodepanel/source_mode.svg new file mode 100644 index 0000000000000000000000000000000000000000..8aebb3008e2a96ffaa036f0fed3a09fae5ae14cd --- /dev/null +++ b/src/main/resources/bdv/ui/viewermodepanel/source_mode.svg @@ -0,0 +1,133 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- + #%L + BigDataViewer core classes with minimal dependencies. + %% + Copyright (C) 2012 - 2020 BigDataViewer developers. + %% + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + #L% + --> + +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="40" + height="40" + viewBox="0 0 10.583333 10.583334" + version="1.1" + id="svg8" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="source_mode.svg" + inkscape:export-filename="/home/random/Development/imagej/bigdataviewer/bigdataviewer-core/src/main/resources/bdv/ui/viewermodepanel/source_mode.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96"> + <defs + id="defs2" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#4a4a4a" + borderopacity="1" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="9.0909466" + inkscape:cx="11.163088" + inkscape:cy="13.068008" + inkscape:document-units="mm" + inkscape:current-layer="g824" + showgrid="false" + units="px" + inkscape:pagecheckerboard="true" + inkscape:window-width="1920" + inkscape:window-height="1023" + inkscape:window-x="0" + inkscape:window-y="33" + inkscape:window-maximized="1" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" /> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(1.9123157e-7,-282.97707)"> + <g + id="g824" + transform="translate(-1.9123157e-7,-3.4395876)"> + <rect + ry="0.27236438" + rx="0.27238995" + y="288.60168" + x="1.059764" + height="4.6323395" + width="4.6327744" + id="rect815" + style="opacity:1;fill:#2e2e2e;fill-opacity:0.19844358;fill-rule:nonzero;stroke:#000000;stroke-width:0.141;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <rect + transform="rotate(30)" + ry="0.25034022" + rx="0.25036374" + y="245.61761" + x="148.9614" + height="4.2577553" + width="4.2581553" + id="rect815-35" + style="opacity:1;fill:#484848;fill-opacity:0.45136186;fill-rule:nonzero;stroke:#000000;stroke-width:0.141;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <rect + ry="0.27094325" + rx="0.27096879" + y="282.73801" + x="-72.2089" + height="4.6081691" + width="4.6086035" + id="rect815-3" + style="opacity:1;fill:none;fill-opacity:0.10894943;fill-rule:nonzero;stroke:#000000;stroke-width:0.14111109;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + transform="rotate(-15.000005)" /> + <path + inkscape:connector-curvature="0" + id="rect4773" + d="m 6.3179602,287.28534 c -0.096824,-0.0121 -0.1955218,0.0334 -0.2475301,0.12351 l -0.6888467,1.19269 H 1.3322181 c -0.150904,0 -0.2723347,0.12144 -0.2723347,0.27233 v 4.08761 c 0,0.15089 0.1214307,0.27233 0.2723347,0.27233 h 2.4835694 l 0.7363891,2.74867 c 0.038849,0.14499 0.1867611,0.23058 0.3317626,0.19172 l 3.9279258,-1.05264 c 0.1450015,-0.0389 0.2305688,-0.18678 0.1917195,-0.33177 l -0.7219199,-2.69492 1.4763956,-2.55695 c 0.069344,-0.12011 0.028652,-0.27275 -0.091467,-0.3421 l -3.2540649,-1.87896 c -0.03003,-0.0173 -0.062293,-0.0275 -0.094568,-0.0315 z" + style="opacity:1;fill:#2e2e2e;fill-opacity:0.1984436;fill-rule:nonzero;stroke:none;stroke-width:0.14111109;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </g> + </g> +</svg> diff --git a/src/main/resources/openconnectome-bock11-neariso.xml b/src/main/resources/openconnectome-bock11-neariso.xml index a57d05c3124212397e336e0143c5c6d7ba80bb86..170f1f9fcd6df13afcbcc5059822ab53bf680fb4 100644 --- a/src/main/resources/openconnectome-bock11-neariso.xml +++ b/src/main/resources/openconnectome-bock11-neariso.xml @@ -1,35 +1,64 @@ -<?xml version="1.0" encoding="UTF-8"?><SequenceDescription> - <BasePath type="relative">.</BasePath> - <ImageLoader class="bdv.img.openconnectome.OpenConnectomeImageLoader"> - <baseUrl>http://openconnecto.me/ocp/ca</baseUrl> - <token>bock11</token> - <mode>neariso</mode> - </ImageLoader> - <ViewSetup> - <id>0</id> - <angle>0</angle> - <illumination>0</illumination> - <channel>0</channel> - <width>0</width> - <height>0</height> - <depth>0</depth> - <pixelWidth>1.0</pixelWidth> - <pixelHeight>1.0</pixelHeight> - <pixelDepth>1.0</pixelDepth> - </ViewSetup> - <Timepoints type="range"> - <first>0</first> - <last>0</last> - </Timepoints> - <ViewRegistrations> - <ReferenceTimepoint>0</ReferenceTimepoint> - <ViewRegistration> - <timepoint>0</timepoint> - <setup>0</setup> - <affine>1.0 0.0 0.0 0.0 - 0.0 1.0 0.0 0.0 - 0.0 0.0 1.0 0.0</affine> - </ViewRegistration> - </ViewRegistrations> -</SequenceDescription> - +<?xml version="1.0" encoding="UTF-8"?> +<!-- + #%L + BigDataViewer core classes with minimal dependencies. + %% + Copyright (C) 2012 - 2020 BigDataViewer developers. + %% + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + #L% + --> +<SequenceDescription> + <BasePath type="relative">.</BasePath> + <ImageLoader class="bdv.img.openconnectome.OpenConnectomeImageLoader"> + <baseUrl>http://openconnecto.me/ocp/ca</baseUrl> + <token>bock11</token> + <mode>neariso</mode> + </ImageLoader> + <ViewSetup> + <id>0</id> + <angle>0</angle> + <illumination>0</illumination> + <channel>0</channel> + <width>0</width> + <height>0</height> + <depth>0</depth> + <pixelWidth>1.0</pixelWidth> + <pixelHeight>1.0</pixelHeight> + <pixelDepth>1.0</pixelDepth> + </ViewSetup> + <Timepoints type="range"> + <first>0</first> + <last>0</last> + </Timepoints> + <ViewRegistrations> + <ReferenceTimepoint>0</ReferenceTimepoint> + <ViewRegistration> + <timepoint>0</timepoint> + <setup>0</setup> + <affine>1.0 0.0 0.0 0.0 + 0.0 1.0 0.0 0.0 + 0.0 0.0 1.0 0.0</affine> + </ViewRegistration> + </ViewRegistrations> +</SequenceDescription> + diff --git a/src/main/resources/viewer/Help.html b/src/main/resources/viewer/Help.html index d75aa2229742547f5ced915250e40a80408501f1..dcd9325fce0bdd09b3092e4ee3e72b3b01d66b9c 100644 --- a/src/main/resources/viewer/Help.html +++ b/src/main/resources/viewer/Help.html @@ -1,3 +1,31 @@ +<!-- + #%L + BigDataViewer core classes with minimal dependencies. + %% + Copyright (C) 2012 - 2020 BigDataViewer developers. + %% + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + #L% + --> <!-- vim::set expandtab tabstop=4 softtabstop=2 shiftwidth=2: --> <html> <style TYPE="text/css"> diff --git a/src/test/java/bdv/IntervalPaintingExample.java b/src/test/java/bdv/IntervalPaintingExample.java new file mode 100644 index 0000000000000000000000000000000000000000..78d234bbe476e3bf50bba288a8cd8045ccd4306e --- /dev/null +++ b/src/test/java/bdv/IntervalPaintingExample.java @@ -0,0 +1,146 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv; + +import bdv.export.ProgressWriterConsole; +import bdv.viewer.ViewerFrame; +import bdv.viewer.ViewerOptions; +import bdv.viewer.ViewerPanel; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.io.File; +import mpicbg.spim.data.SpimDataException; +import net.imglib2.Interval; +import bdv.viewer.OverlayRenderer; +import net.imglib2.util.Intervals; +import org.scijava.ui.behaviour.DragBehaviour; +import org.scijava.ui.behaviour.io.InputTriggerConfig; +import org.scijava.ui.behaviour.util.Behaviours; + +public class IntervalPaintingExample +{ + public static void main( final String[] args ) throws SpimDataException + { + final String fn = "/Users/pietzsch/workspace/data/111010_weber_full.xml"; + System.setProperty( "apple.laf.useScreenMenuBar", "true" ); + final BigDataViewer bdv = BigDataViewer.open( + fn, + new File( fn ).getName(), + new ProgressWriterConsole(), + ViewerOptions.options() ); + new IntervalPaintingExample( bdv.getViewerFrame() ); + } + + public IntervalPaintingExample( final ViewerFrame viewerFrame ) + { + this.viewer = viewerFrame.getViewerPanel(); + viewer.getDisplay().overlays().add( new Overlay() ); + + final Behaviours behaviours = new Behaviours( new InputTriggerConfig(), "bdv" ); + behaviours.behaviour( new RepaintIntervalBehaviour(), "repaint interval", "SPACE" ); + behaviours.install( viewerFrame.getTriggerbindings(), "repaint" ); + } + + private final ViewerPanel viewer; + + private Interval repaintInterval; + + private final int halfw = 30; + + private final int halfh = 30; + + private synchronized void repaint( final int x, final int y ) + { + if ( x < 0 || y < 0 ) + repaintInterval = null; + else + repaintInterval = Intervals.createMinMax( x - halfw, y - halfh, x + halfw, y + halfh ); + + if ( repaintInterval != null ) + viewer.requestRepaint( repaintInterval ); + + viewer.getDisplay().repaint(); + } + + private synchronized void drawOverlay( final Graphics2D g ) + { + if ( repaintInterval != null ) + { + g.setColor( Color.green ); + draw( g, Intervals.expand( repaintInterval, 10 ) ); + g.setColor( Color.red ); + draw( g, Intervals.expand( repaintInterval, -10 ) ); + } + } + + private static void draw( final Graphics2D g, final Interval interval ) + { + final int x = ( int ) interval.min( 0 ); + final int y = ( int ) interval.min( 1 ); + final int w = ( int ) interval.dimension( 0 ); + final int h = ( int ) interval.dimension( 1 ); + g.drawRect( x, y, w, h ); + } + + private class RepaintIntervalBehaviour implements DragBehaviour + { + @Override + public void init( final int x, final int y ) + { + repaint( x, y ); + } + + @Override + public void drag( final int x, final int y ) + { + repaint( x, y ); + } + + @Override + public void end( final int x, final int y ) + { + repaint( -1, -1 ); + } + } + + private class Overlay implements OverlayRenderer + { + @Override + public void drawOverlays( final Graphics g ) + { + drawOverlay( ( Graphics2D ) g ); + } + + @Override + public void setCanvasSize( final int width, final int height ) + { + } + } +} diff --git a/src/test/java/bdv/ui/CardPanelExample.java b/src/test/java/bdv/ui/CardPanelExample.java new file mode 100644 index 0000000000000000000000000000000000000000..1520ecd3fda44519f6af10e49d80e7a95e85cca5 --- /dev/null +++ b/src/test/java/bdv/ui/CardPanelExample.java @@ -0,0 +1,99 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui; + +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import javax.swing.AbstractAction; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.WindowConstants; +import net.miginfocom.swing.MigLayout; + +/** + * Card panel example. + * + * @author Tim-Oliver Buchholz, MPI-CBG CSBD, Dresden + */ +public class CardPanelExample +{ + + public static void main( String[] args ) + { + System.setProperty( "apple.laf.useScreenMenuBar", "true" ); + + final JFrame frame = new JFrame( "CardPanel Example" ); + frame.setLayout( new MigLayout( "fillx", "[]", "" ) ); + final JButton add = new JButton( "Add Card" ); + final JButton remove = new JButton( "Remove Card" ); + final JButton toggle = new JButton( "Toggle Card" ); + frame.add( add, "growx, wrap" ); + frame.add( remove, "growx, wrap" ); + frame.add( toggle, "growx, wrap" ); + + final CardPanel cardPanel = new CardPanel(); + + frame.setPreferredSize( new Dimension( 200, 300 ) ); + frame.add( cardPanel.getComponent(), "growx, growy" ); + frame.pack(); + frame.setDefaultCloseOperation( WindowConstants.DISPOSE_ON_CLOSE ); + frame.setVisible( true ); + + final Random rand = new Random(); + final List< String > names = new ArrayList<>(); + + add.addActionListener( e -> + { + final String name = "Card " + rand.nextInt(); + cardPanel.addCard( name, new JLabel( "Conent " + rand.nextFloat() ), rand.nextBoolean() ); + names.add( name ); + } ); + remove.addActionListener( e-> + { + if ( names.size() > 0 ) + { + final int idx = rand.nextInt( names.size() ); + cardPanel.removeCard( names.get( idx ) ); + names.remove( idx ); + } + } ); + toggle.addActionListener( e -> + { + if ( names.size() > 0 ) + { + final int idx = rand.nextInt( names.size() ); + cardPanel.setCardExpanded( names.get( idx ), !cardPanel.isCardExpanded( names.get( idx ) ) ); + } + } ); + } +} diff --git a/src/test/java/bdv/ui/CreateViewerState.java b/src/test/java/bdv/ui/CreateViewerState.java new file mode 100644 index 0000000000000000000000000000000000000000..678c91b75a0d4fe86ff723da647b9a3af4eb314e --- /dev/null +++ b/src/test/java/bdv/ui/CreateViewerState.java @@ -0,0 +1,167 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui; + +import bdv.tools.brightness.ConverterSetup; +import bdv.viewer.Interpolation; +import bdv.viewer.Source; +import bdv.viewer.SourceAndConverter; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntPredicate; +import mpicbg.spim.data.sequence.VoxelDimensions; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.RealRandomAccessible; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.ARGBType; +import net.imglib2.type.numeric.integer.UnsignedShortType; + +/** + * Creates dummy sources and converters for testing SourceTable and SourceGroupTree + * + * @author Tobias Pietzsch + */ +public class CreateViewerState +{ + static class TestSource implements Source< UnsignedShortType > + { + private static final AtomicInteger id = new AtomicInteger(); + + private static final Random random = new Random(); + + private static String nextName() + { + return "source(" + id.getAndIncrement() + ")"; + } + + private final String name; + + private final IntPredicate isPresent; + + private final DefaultConverterSetup converterSetup; + + public TestSource() + { + this( nextName(), i -> false ); + } + + public TestSource( final String name ) + { + this( name, i -> false ); + } + + public TestSource( final IntPredicate isPresent ) + { + this( nextName(), isPresent ); + } + + public TestSource( final String name, final IntPredicate isPresent ) + { + this.name = name; + this.isPresent = isPresent; + + final DefaultConverterSetup s = new DefaultConverterSetup( id.get(), random.nextBoolean() ); + final double a = random.nextDouble() * 65535; + final double b = random.nextDouble() * 65535; + s.setDisplayRange( Math.min( a, b ), Math.max( a, b ) ); + s.setColor( new ARGBType( random.nextInt() ) ); + converterSetup = s; + } + + @Override + public UnsignedShortType getType() + { + return new UnsignedShortType(); + } + + @Override + public String getName() + { + return name; + } + + @Override + public VoxelDimensions getVoxelDimensions() + { + return null; + } + + @Override + public int getNumMipmapLevels() + { + return 1; + } + + @Override + public boolean isPresent( final int t ) + { + return isPresent.test( t ); + } + + @Override + public RandomAccessibleInterval< UnsignedShortType > getSource( final int t, final int level ) + { + return null; + } + + @Override + public RealRandomAccessible< UnsignedShortType > getInterpolatedSource( final int t, final int level, final Interpolation method ) + { + return null; + } + + @Override + public void getSourceTransform( final int t, final int level, final AffineTransform3D transform ) + { + transform.identity(); + } + + /** + * ConverterSetups need to be associated to Sources somehow. + */ + public DefaultConverterSetup getConverterSetup() + { + return converterSetup; + } + } + + public static SourceAndConverter< ? > createSource() + { + final TestSource source = new TestSource(); + return new SourceAndConverter<>( source, source.getConverterSetup() ); + } + + public static ConverterSetup getConverterSetup( SourceAndConverter< ? > source ) + { + if ( source.getSpimSource() instanceof TestSource ) + return ( ( TestSource ) source.getSpimSource() ).getConverterSetup(); + else + return null; + }; +} diff --git a/src/test/java/bdv/ui/DefaultConverterSetup.java b/src/test/java/bdv/ui/DefaultConverterSetup.java new file mode 100644 index 0000000000000000000000000000000000000000..2cac6e3798c4e8f75a2a4d9289fb0561c961222a --- /dev/null +++ b/src/test/java/bdv/ui/DefaultConverterSetup.java @@ -0,0 +1,126 @@ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui; + +import bdv.tools.brightness.ConverterSetup; +import net.imglib2.converter.Converter; +import net.imglib2.type.numeric.ARGBType; +import net.imglib2.type.numeric.integer.UnsignedShortType; +import org.scijava.listeners.Listeners; + +/** + * Dummy Converter and ConverterSetup for testing SourceTable and SourceGroupTree + * + * @author Tobias Pietzsch + */ +public class DefaultConverterSetup implements ConverterSetup, Converter< UnsignedShortType, ARGBType > +{ + private final int setupId; + + private final boolean supportsColor; + + private double displayRangeMin; + + private double displayRangeMax; + + private final ARGBType color = new ARGBType(); + + private final Listeners.List< SetupChangeListener > listeners = new Listeners.SynchronizedList<>(); + + public DefaultConverterSetup( final int setupId, final boolean supportsColor ) + { + this.setupId = setupId; + this.supportsColor = supportsColor; + this.displayRangeMin = 0; + this.displayRangeMax = 1; + } + + @Override + public Listeners< SetupChangeListener > setupChangeListeners() + { + return listeners; + } + + @Override + public synchronized void setDisplayRange( final double min, final double max ) + { + if ( displayRangeMin != min || displayRangeMax != max ) + { + displayRangeMin = min; + displayRangeMax = max; + listeners.list.forEach( l -> l.setupParametersChanged( this ) ); + } + } + + @Override + public synchronized void setColor( final ARGBType argb ) + { + if ( supportsColor && color.get() != argb.get() ) + { + color.set( argb ); + listeners.list.forEach( l -> l.setupParametersChanged( this ) ); + } + } + + @Override + public boolean supportsColor() + { + return supportsColor; + } + + @Override + public int getSetupId() + { + return setupId; + } + + @Override + public double getDisplayRangeMin() + { + return displayRangeMin; + } + + @Override + public double getDisplayRangeMax() + { + return displayRangeMax; + } + + @Override + public ARGBType getColor() + { + return color; + } + + @Override + public void convert( final UnsignedShortType input, final ARGBType output ) + { + throw new UnsupportedOperationException(); + } +} diff --git a/src/test/java/bdv/ui/PlaygroundCards.java b/src/test/java/bdv/ui/PlaygroundCards.java new file mode 100644 index 0000000000000000000000000000000000000000..a858ea03c9577886bd1675f4f47dd1aac0a236b1 --- /dev/null +++ b/src/test/java/bdv/ui/PlaygroundCards.java @@ -0,0 +1,209 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui; + +import bdv.ui.sourcetable.SourceTable; +import bdv.ui.sourcegrouptree.SourceGroupTree; +import bdv.ui.convertersetupeditor.ConverterSetupEditPanel; +import bdv.viewer.BasicViewerState; +import bdv.viewer.ConverterSetups; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.SourceGroup; +import bdv.viewer.SynchronizedViewerState; +import bdv.viewer.ViewerState; +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.KeyboardFocusManager; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.List; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.border.EmptyBorder; +import javax.swing.tree.TreeSelectionModel; + +public class PlaygroundCards +{ + public static void main( final String[] args ) + { + // create a ViewerState to show + final ViewerState state = new SynchronizedViewerState( new BasicViewerState() ); + final List< SourceAndConverter< ? > > sources = new ArrayList<>(); + for ( int i = 0; i < 10; ++i ) + sources.add( CreateViewerState.createSource() ); + state.addSources( sources ); + state.setSourceActive( sources.get( 1 ), true ); + state.setSourceActive( sources.get( 2 ), true ); + state.setSourceActive( sources.get( 7 ), true ); + + final List< SourceGroup > groups = new ArrayList<>(); + for ( int i = 0; i < 10; ++i ) + { + final SourceGroup g = new SourceGroup(); + groups.add( g ); + state.addGroup( g ); + state.setGroupName( g, "group(" + i + ")" ); + } + state.setGroupActive( groups.get( 1 ), true ); + state.setGroupActive( groups.get( 2 ), true ); + state.setGroupActive( groups.get( 7 ), true ); + + state.addSourceToGroup( sources.get( 0 ), groups.get( 0 ) ); + state.addSourceToGroup( sources.get( 1 ), groups.get( 0 ) ); + state.addSourceToGroup( sources.get( 2 ), groups.get( 0 ) ); + state.addSourceToGroup( sources.get( 3 ), groups.get( 3 ) ); + state.addSourceToGroup( sources.get( 8 ), groups.get( 3 ) ); + state.addSourceToGroup( sources.get( 4 ), groups.get( 3 ) ); + state.addSourceToGroup( sources.get( 2 ), groups.get( 3 ) ); + + final ConverterSetups converterSetups = new ConverterSetups( state ); + for ( SourceAndConverter< ? > source : sources ) + converterSetups.put( source, CreateViewerState.getConverterSetup( source ) ); + + // -- Sources table -- + + final SourceTable table = new SourceTable( state, converterSetups ); + table.setPreferredScrollableViewportSize( new Dimension( 400, 400 ) ); + table.setFillsViewportHeight( true ); + table.setDragEnabled( true ); + + final ConverterSetupEditPanel editPanelTable = new ConverterSetupEditPanel( table, converterSetups ); + + final JPanel tablePanel = new JPanel( new BorderLayout() ); + final JScrollPane scrollPaneTable = new JScrollPane( table ); + scrollPaneTable.setBorder( new EmptyBorder( 0, 0, 0, 0 ) ); + tablePanel.add( scrollPaneTable, BorderLayout.CENTER ); + tablePanel.add( editPanelTable, BorderLayout.SOUTH ); + + + // -- Groups tree -- + + SourceGroupTree tree = new SourceGroupTree( state ); + tree.setEditable( true ); + tree.setSelectionRow( 0 ); + tree.setRootVisible( false ); + tree.setShowsRootHandles( true ); + tree.setExpandsSelectedPaths( true ); + tree.getSelectionModel().setSelectionMode( TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION ); + + final ConverterSetupEditPanel editPanelTree = new ConverterSetupEditPanel( tree, converterSetups ); + + final JPanel treePanel = new JPanel( new BorderLayout() ); + final JScrollPane scrollPaneTree = new JScrollPane( tree ); + scrollPaneTree.setBorder( new EmptyBorder( 0, 0, 0, 0 ) ); + treePanel.add( scrollPaneTree, BorderLayout.CENTER ); + treePanel.add( editPanelTree, BorderLayout.SOUTH ); + + + // -- handle focus -- + + KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener( "focusOwner", new PropertyChangeListener() + { + static final int MAX_DEPTH = 7; + boolean tableFocused; + boolean treeFocused; + + void focusTable( boolean focus ) + { + if ( focus != tableFocused ) + { + tableFocused = focus; + table.setSelectionBackground( focus ); + } + } + + void focusTree( boolean focus ) + { + if ( focus != treeFocused ) + { + treeFocused = focus; + tree.setSelectionBackground( focus ); + } + } + + @Override + public void propertyChange( final PropertyChangeEvent evt ) + { + if ( evt.getNewValue() instanceof JComponent ) + { + JComponent component = ( JComponent ) evt.getNewValue(); + for ( int i = 0; i < MAX_DEPTH; ++i ) + { + Container parent = component.getParent(); + if ( ! ( parent instanceof JComponent ) ) + break; + + component = ( JComponent ) parent; +// System.out.println( " -> " + component ); + if ( component == treePanel && !treeFocused ) + { + focusTable( false ); + focusTree( true ); + return; + } + else if ( component == tablePanel ) + { + focusTable( true ); + focusTree( false ); + return; + } + } + focusTable( false ); + focusTree( false ); + } + } + } ); + + + // -- cards -- + + CardPanel cardPanel = new CardPanel(); + cardPanel.addCard( "Sources", tablePanel, true, new Insets( 0, 4, 0, 0 ) ); + cardPanel.addCard( "Groups", treePanel, true, new Insets( 0, 4, 0, 0 ) ); + + + //Create and set up the window. + final JFrame frame = new JFrame( "Sources Table" ); + frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); + frame.getRootPane().setDoubleBuffered( true ); + final JScrollPane scrollPane = new JScrollPane( cardPanel.getComponent() ); + scrollPane.setHorizontalScrollBarPolicy( JScrollPane.HORIZONTAL_SCROLLBAR_NEVER ); + scrollPane.getVerticalScrollBar().setUnitIncrement(16); + frame.add( scrollPane, BorderLayout.CENTER ); +// frame.add( cardPanel.getComponent(), BorderLayout.CENTER ); + frame.pack(); + frame.setVisible( true ); + } + +} diff --git a/src/test/java/bdv/ui/PlaygroundSourcesDragAndDrop.java b/src/test/java/bdv/ui/PlaygroundSourcesDragAndDrop.java new file mode 100644 index 0000000000000000000000000000000000000000..652858a57c85d53555dfb59fc855dcc4946884e7 --- /dev/null +++ b/src/test/java/bdv/ui/PlaygroundSourcesDragAndDrop.java @@ -0,0 +1,145 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui; + +import bdv.ui.sourcetable.SourceTable; +import bdv.ui.sourcegrouptree.SourceGroupTree; +import bdv.viewer.BasicViewerState; +import bdv.viewer.ConverterSetups; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.SourceGroup; +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.util.ArrayList; +import java.util.List; +import javax.swing.JFrame; +import javax.swing.JScrollPane; +import javax.swing.tree.TreeSelectionModel; + +public class PlaygroundSourcesDragAndDrop +{ + public static void main( String[] args ) + { + // create a ViewerState to show + final BasicViewerState state = new BasicViewerState(); + final List< SourceAndConverter< ? > > sources = new ArrayList<>(); + for ( int i = 0; i < 10; ++i ) + sources.add( CreateViewerState.createSource() ); + state.addSources( sources ); + state.setSourceActive( sources.get( 1 ), true ); + state.setSourceActive( sources.get( 2 ), true ); + state.setSourceActive( sources.get( 7 ), true ); + + final List< SourceGroup > groups = new ArrayList<>(); + for ( int i = 0; i < 10; ++i ) + { + final SourceGroup g = new SourceGroup(); + groups.add( g ); + state.addGroup( g ); + state.setGroupName( g, "group(" + i + ")" ); + } + state.setGroupActive( groups.get( 1 ), true ); + state.setGroupActive( groups.get( 2 ), true ); + state.setGroupActive( groups.get( 7 ), true ); + + state.addSourceToGroup( sources.get( 0 ), groups.get( 0 ) ); + state.addSourceToGroup( sources.get( 1 ), groups.get( 0 ) ); + state.addSourceToGroup( sources.get( 2 ), groups.get( 0 ) ); + state.addSourceToGroup( sources.get( 3 ), groups.get( 3 ) ); + state.addSourceToGroup( sources.get( 8 ), groups.get( 3 ) ); + state.addSourceToGroup( sources.get( 4 ), groups.get( 3 ) ); + state.addSourceToGroup( sources.get( 2 ), groups.get( 3 ) ); + + final ConverterSetups converterSetups = new ConverterSetups( state ); + for ( SourceAndConverter< ? > source : sources ) + converterSetups.put( source, CreateViewerState.getConverterSetup( source ) ); + + // create the table + SourceTable table = new SourceTable( state, converterSetups ); + table.setPreferredScrollableViewportSize( new Dimension( 400, 200 ) ); + table.setFillsViewportHeight( true ); + table.setDragEnabled( true ); + table.setDropTarget( null ); + + // create the tree + SourceGroupTree tree = new SourceGroupTree( state ); + tree.setEditable( true ); + tree.setSelectionRow( 0 ); + tree.setRootVisible( false ); + tree.setShowsRootHandles( true ); + tree.setExpandsSelectedPaths( true ); + tree.getSelectionModel().setSelectionMode( TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION ); +// tree.setDropMode( DropMode.ON ); + + + // handle focus + table.addFocusListener( new FocusListener() + { + @Override + public void focusGained( final FocusEvent e ) + { + table.setSelectionBackground( true ); + } + + @Override + public void focusLost( final FocusEvent e ) + { + table.setSelectionBackground( false ); + } + } ); + tree.addFocusListener( new FocusListener() + { + @Override + public void focusGained( final FocusEvent e ) + { + tree.setSelectionBackground( true ); + } + + @Override + public void focusLost( final FocusEvent e ) + { + tree.setSelectionBackground( false ); + } + } ); + + + //Create and set up the window. + JFrame frame = new JFrame( "Sources Table" ); + frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); + frame.getRootPane().setDoubleBuffered( true ); + JScrollPane scrollPane = new JScrollPane( table ); + frame.add( scrollPane, BorderLayout.NORTH ); + frame.add( tree, BorderLayout.CENTER ); + frame.pack(); + frame.setVisible( true ); + } + +} diff --git a/src/test/java/bdv/ui/sourcegrouptree/PlaygroundTree.java b/src/test/java/bdv/ui/sourcegrouptree/PlaygroundTree.java new file mode 100644 index 0000000000000000000000000000000000000000..5b2bb21fd5c8f05a1fc0655780d6c4c9292628ca --- /dev/null +++ b/src/test/java/bdv/ui/sourcegrouptree/PlaygroundTree.java @@ -0,0 +1,136 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.sourcegrouptree; + +import bdv.ui.CreateViewerState; +import bdv.ui.sourcegrouptree.SourceGroupTree; +import bdv.ui.sourcegrouptree.SourceGroupTreeModel.GroupModel; +import bdv.viewer.BasicViewerState; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.SourceGroup; +import java.awt.BorderLayout; +import java.util.ArrayList; +import java.util.List; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.tree.TreeSelectionModel; + +public class PlaygroundTree +{ + public static void main( String[] args ) + { +// try +// { +// UIManager.setLookAndFeel( UIManager.getCrossPlatformLookAndFeelClassName()); +// } +// catch ( ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e ) +// { +// e.printStackTrace(); +// } + + // create a ViewerState to show + final BasicViewerState state = new BasicViewerState(); + final List< SourceAndConverter< ? > > sources = new ArrayList<>(); + for ( int i = 0; i < 10; ++i ) + sources.add( CreateViewerState.createSource() ); + state.addSources( sources ); + + final List< SourceGroup > groups = new ArrayList<>(); + for ( int i = 0; i < 10; ++i ) + { + final SourceGroup g = new SourceGroup(); + groups.add( g ); + state.addGroup( g ); + state.setGroupName( g, "group(" + i + ")" ); + } + state.setGroupActive( groups.get( 1 ), true ); + state.setGroupActive( groups.get( 2 ), true ); + state.setGroupActive( groups.get( 7 ), true ); + + state.addSourceToGroup( sources.get( 0 ), groups.get( 0 ) ); + state.addSourceToGroup( sources.get( 1 ), groups.get( 0 ) ); + state.addSourceToGroup( sources.get( 2 ), groups.get( 0 ) ); + state.addSourceToGroup( sources.get( 3 ), groups.get( 3 ) ); + state.addSourceToGroup( sources.get( 8 ), groups.get( 3 ) ); + state.addSourceToGroup( sources.get( 4 ), groups.get( 3 ) ); + state.addSourceToGroup( sources.get( 2 ), groups.get( 3 ) ); + + // create the tree + SourceGroupTree tree = new SourceGroupTree( state ); + tree.setEditable( true ); + tree.setSelectionRow( 0 ); + tree.setRootVisible( false ); + tree.setShowsRootHandles( true ); + tree.setExpandsSelectedPaths( true ); + tree.getSelectionModel().setSelectionMode( TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION ); +// tree.setToggleClickCount( 0 ); +// tree.setSelectionModel( ); + + JPanel buttons = new JPanel(); + buttons.setLayout( new BoxLayout( buttons, BoxLayout.Y_AXIS ) ); + final JButton toggle5Active = new JButton("a5"); + toggle5Active.addActionListener( e -> state.setGroupActive( groups.get( 5 ), !state.isGroupActive( groups.get( 5 ) ) ) ); + final JButton add5to3 = new JButton("5->3"); + add5to3.addActionListener( e -> state.addSourceToGroup( sources.get( 5 ), groups.get( 3 ) ) ); + final JButton remove02from0 = new JButton("0,2x>0"); + remove02from0.addActionListener( e -> + { + state.removeSourceFromGroup( sources.get( 0 ), groups.get( 0 ) ); + state.removeSourceFromGroup( sources.get( 2 ), groups.get( 0 ) ); + } ); + final JButton name4 = new JButton("rename4"); + name4.addActionListener( e -> state.setGroupName( groups.get( 4 ), "renamed group 4" ) ); + final JButton remove0 = new JButton("x0"); + remove0.addActionListener( e -> state.removeGroup( groups.get( 0 ) ) ); + final JButton remove4 = new JButton("x4"); + remove4.addActionListener( e -> state.removeGroup( groups.get( 4 ) ) ); + final JButton edit4 = new JButton("e4"); + edit4.addActionListener( e -> tree.startEditingAtPath( tree.getPathTo( groups.get( 4 ) ) ) ); + buttons.add( toggle5Active ); + buttons.add( add5to3 ); + buttons.add( remove02from0 ); + buttons.add( name4 ); + buttons.add( remove0 ); + buttons.add( remove4 ); + buttons.add( edit4 ); + + //Create and set up the window. + JFrame frame = new JFrame( "Groups Tree" ); + frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); + frame.getRootPane().setDoubleBuffered( true ); + JScrollPane scrollPane = new JScrollPane( tree ); + frame.add( scrollPane, BorderLayout.CENTER ); + frame.add( buttons, BorderLayout.EAST ); + frame.pack(); + frame.setVisible( true ); + } +} diff --git a/src/test/java/bdv/ui/sourcetable/PlaygroundTable.java b/src/test/java/bdv/ui/sourcetable/PlaygroundTable.java new file mode 100644 index 0000000000000000000000000000000000000000..60063b2e014e445e5f6df9e6c6204daca6e3fa1c --- /dev/null +++ b/src/test/java/bdv/ui/sourcetable/PlaygroundTable.java @@ -0,0 +1,91 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.sourcetable; + +import bdv.ui.CreateViewerState; +import bdv.ui.sourcetable.SourceTable; +import bdv.ui.convertersetupeditor.ConverterSetupEditPanel; +import bdv.viewer.BasicViewerState; +import bdv.viewer.ConverterSetups; +import bdv.viewer.SourceAndConverter; +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.util.ArrayList; +import java.util.List; +import javax.swing.JFrame; +import javax.swing.JScrollPane; + +public class PlaygroundTable +{ + public static void main( final String[] args ) + { +// try +// { +// UIManager.setLookAndFeel( UIManager.getCrossPlatformLookAndFeelClassName()); +// } +// catch ( ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e ) +// { +// e.printStackTrace(); +// } + + // create a ViewerState to show + final BasicViewerState state = new BasicViewerState(); + final List< SourceAndConverter< ? > > sources = new ArrayList<>(); + for ( int i = 0; i < 10; ++i ) + sources.add( CreateViewerState.createSource() ); + state.addSources( sources ); + state.setSourceActive( sources.get( 1 ), true ); + state.setSourceActive( sources.get( 2 ), true ); + state.setSourceActive( sources.get( 7 ), true ); + + final ConverterSetups converterSetups = new ConverterSetups( state ); + for ( SourceAndConverter< ? > source : sources ) + converterSetups.put( source, CreateViewerState.getConverterSetup( source ) ); + + + // TODO: create the table + final SourceTable table = new SourceTable( state, converterSetups ); + table.setPreferredScrollableViewportSize( new Dimension( 400, 800 ) ); + table.setFillsViewportHeight( true ); + table.setDragEnabled( true ); + + final ConverterSetupEditPanel editPanel = new ConverterSetupEditPanel( table, converterSetups ); + + //Create and set up the window. + final JFrame frame = new JFrame( "Sources Table" ); + frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); + frame.getRootPane().setDoubleBuffered( true ); + final JScrollPane scrollPane = new JScrollPane( table ); + frame.add( scrollPane, BorderLayout.CENTER ); + frame.add( editPanel, BorderLayout.SOUTH ); + frame.pack(); + frame.setVisible( true ); + } +} + diff --git a/src/test/java/bdv/ui/splitpanel/SplitPanelExample.java b/src/test/java/bdv/ui/splitpanel/SplitPanelExample.java new file mode 100644 index 0000000000000000000000000000000000000000..ccbddc46e7c3ea8a875c298c9531004e35673b8a --- /dev/null +++ b/src/test/java/bdv/ui/splitpanel/SplitPanelExample.java @@ -0,0 +1,62 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.ui.splitpanel; + +import bdv.cache.CacheControl; +import bdv.ui.CardPanel; +import bdv.viewer.ViewerOptions; +import bdv.viewer.ViewerPanel; +import java.awt.Dimension; +import java.util.ArrayList; +import javax.swing.JFrame; +import javax.swing.WindowConstants; +import net.miginfocom.swing.MigLayout; + +public class SplitPanelExample +{ + public static void main( String[] args ) + { + System.setProperty( "apple.laf.useScreenMenuBar", "true" ); + + final JFrame frame = new JFrame( "SplitPanel Example" ); + frame.setLayout( new MigLayout( "ins 0, fillx, filly", "[grow]", "" ) ); + + final CardPanel cardPanel = new CardPanel(); + final ViewerPanel viewerPanel = new ViewerPanel( new ArrayList<>(), 1, new CacheControl.Dummy(), ViewerOptions.options().width( 600 ) ); + + final SplitPanel splitPanel = new SplitPanel( viewerPanel, cardPanel ); + splitPanel.setBorder( null ); + + frame.setPreferredSize( new Dimension( 800, 600 ) ); + frame.add( splitPanel, "growx, growy" ); + frame.pack(); + frame.setDefaultCloseOperation( WindowConstants.DISPOSE_ON_CLOSE ); + frame.setVisible( true ); + } +} diff --git a/src/test/java/bdv/util/BoundedRangeTest.java b/src/test/java/bdv/util/BoundedRangeTest.java new file mode 100644 index 0000000000000000000000000000000000000000..34685aa9db2306653e5bd46cc0830f6a70afc6be --- /dev/null +++ b/src/test/java/bdv/util/BoundedRangeTest.java @@ -0,0 +1,144 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.util; + +import org.junit.Assert; +import org.junit.Test; + +public class BoundedRangeTest +{ + @Test + public void testConstructor() + { + new BoundedRange( 0, 1, 0, 1 ); + } + + @Test( expected = IllegalArgumentException.class ) + public void testConstructorFails1() + { + new BoundedRange( 0, 1, -1, 1 ); + } + + @Test( expected = IllegalArgumentException.class ) + public void testConstructorFails2() + { + new BoundedRange( 0, -1, 0, 0 ); + } + + @Test( expected = IllegalArgumentException.class ) + public void testConstructorFails3() + { + new BoundedRange( 0, 11, 1, 0 ); + } + + @Test + public void testWithMin() + { + Assert.assertEquals( + new BoundedRange( 0, 1, 0, 1 ).withMin( -1 ), + new BoundedRange( -1, 1, -1, 1 ) + ); + Assert.assertEquals( + new BoundedRange( 0, 1, 0, 1 ).withMin( 0.5 ), + new BoundedRange( 0, 1, 0.5, 1 ) + ); + Assert.assertEquals( + new BoundedRange( 0, 1, 0, 1 ).withMin( 2 ), + new BoundedRange( 0, 2, 2, 2 ) + ); + } + + @Test + public void testWithMax() + { + Assert.assertEquals( + new BoundedRange( 0, 1, 0, 1 ).withMax( 2 ), + new BoundedRange( 0, 2, 0, 2 ) + ); + Assert.assertEquals( + new BoundedRange( 0, 1, 0, 1 ).withMax( 0.5 ), + new BoundedRange( 0, 1, 0, 0.5 ) + ); + Assert.assertEquals( + new BoundedRange( 0, 1, 0, 1 ).withMax( -1 ), + new BoundedRange( -1, 1, -1, -1 ) + ); + } + + @Test + public void testWithMinBound() + { + Assert.assertEquals( + new BoundedRange( 0, 3, 1, 2 ).withMinBound( -1 ), + new BoundedRange( -1, 3, 1, 2 ) + ); + Assert.assertEquals( + new BoundedRange( 0, 3, 1, 2 ).withMinBound( 0.5 ), + new BoundedRange( 0.5, 3, 1, 2 ) + ); + Assert.assertEquals( + new BoundedRange( 0, 3, 1, 2 ).withMinBound( 1.5 ), + new BoundedRange( 1.5, 3, 1.5, 2 ) + ); + Assert.assertEquals( + new BoundedRange( 0, 3, 1, 2 ).withMinBound( 4 ), + new BoundedRange( 4, 4, 4, 4 ) + ); + } + + @Test + public void testWithMaxBound() + { + Assert.assertEquals( + new BoundedRange( 0, 3, 1, 2 ).withMaxBound( 4 ), + new BoundedRange( 0, 4, 1, 2 ) + ); + Assert.assertEquals( + new BoundedRange( 0, 3, 1, 2 ).withMaxBound( 2.5 ), + new BoundedRange( 0, 2.5, 1, 2 ) + ); + Assert.assertEquals( + new BoundedRange( 0, 3, 1, 2 ).withMaxBound( 1.5 ), + new BoundedRange( 0, 1.5, 1, 1.5 ) + ); + Assert.assertEquals( + new BoundedRange( 0, 3, 1, 2 ).withMaxBound( -1 ), + new BoundedRange( -1, -1, -1, -1 ) + ); + } + + @Test + public void testJoin() + { + Assert.assertEquals( + new BoundedRange( 0, 3, 1, 2 ).join( new BoundedRange( 5, 8, 6, 7 ) ), + new BoundedRange( 0, 8, 1, 7 ) + ); + } +} diff --git a/src/test/java/bdv/util/MovingAverageTest.java b/src/test/java/bdv/util/MovingAverageTest.java new file mode 100644 index 0000000000000000000000000000000000000000..633c8f186d9725e453d771b8135a23bd6573f3c2 --- /dev/null +++ b/src/test/java/bdv/util/MovingAverageTest.java @@ -0,0 +1,64 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.util; + +import java.util.Arrays; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class MovingAverageTest +{ + @Test + public void testAverage() + { + final double[] values = new double[ 1000 ]; + Arrays.setAll( values, i -> Math.random() ); + + final int width = 3; + final MovingAverage avg = new MovingAverage( width ); + avg.init( 0 ); + + for ( int i = 0; i < values.length; ++i ) + { + avg.add( values[ i ] ); + double expected = average( values, i - width + 1, i + 1 ); + assertEquals( expected, avg.getAverage(), 1e-6 ); + } + } + + private static double average( final double[] values, final int fromIndex, final int toIndex ) + { + final int numValues = toIndex - fromIndex; + double sum = 0; + for ( int i = fromIndex; i < toIndex; i++ ) + sum += i < 0 ? 0 : values[ i ]; + return sum / numValues; + } +} diff --git a/src/test/java/bdv/util/TripleBufferDemo.java b/src/test/java/bdv/util/TripleBufferDemo.java new file mode 100644 index 0000000000000000000000000000000000000000..621d8d64343a67a01739c82e7fe400de98f57b93 --- /dev/null +++ b/src/test/java/bdv/util/TripleBufferDemo.java @@ -0,0 +1,111 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.util; + +import java.awt.Graphics; + +import javax.swing.JComponent; +import javax.swing.JFrame; + +import net.imglib2.display.screenimage.awt.ARGBScreenImage; +import net.imglib2.img.array.ArrayCursor; +import net.imglib2.type.numeric.ARGBType; + +/** + * Test {@link TripleBuffer}. + * + * @author Matthias Arzt + */ +public class TripleBufferDemo +{ + private static TripleBuffer< ARGBScreenImage > tripleBuffer = new TripleBuffer<>( () -> new ARGBScreenImage( 100, 100 ) ); + + private static ImageComponent imageComponent = new ImageComponent(); + + public static void main( final String... args ) throws InterruptedException + { + final JFrame frame = new JFrame(); + frame.setSize( 100, 100 ); + frame.add( imageComponent ); + frame.setVisible( true ); + + new PainterThread().start(); + } + + private static class PainterThread extends Thread + { + @Override + public void run() + { + try + { + while ( true ) + { + for ( double r = 0; r < 2 * Math.PI; r += 0.01 ) + { + renderCircle( r ); + Thread.sleep( 1 ); + imageComponent.repaint(); + } + } + } + catch ( final InterruptedException e ) + { + e.printStackTrace(); + } + } + + private void renderCircle( final double r ) + { + final ARGBScreenImage image = tripleBuffer.getWritableBuffer(); + image.forEach( pixel -> pixel.set( 0xff00ff00 ) ); + final ArrayCursor< ARGBType > cursor = image.cursor(); + while ( cursor.hasNext() ) + { + final ARGBType pixel = cursor.next(); + final double x = cursor.getDoublePosition( 0 ); + final double y = cursor.getDoublePosition( 1 ); + pixel.set( ARGBType.rgba( Math.sin( x / 10 + r ) * 127 + 127, Math.cos( y / 10 + r ) * 127 + 127, 0.0, 255.0 ) ); + } + tripleBuffer.doneWriting( image ); + } + } + + private static class ImageComponent extends JComponent + { + @Override + protected void paintComponent( final Graphics g ) + { + final ARGBScreenImage image = tripleBuffer.getReadableBuffer().getBuffer(); + if ( image != null ) + g.drawImage( image.image(), 0, 0, getWidth(), getHeight(), null ); + } + } +} + diff --git a/src/test/java/bdv/util/TripleBufferTest.java b/src/test/java/bdv/util/TripleBufferTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0ab01992b66e3c377cbebe29819bfc58cbfc8d66 --- /dev/null +++ b/src/test/java/bdv/util/TripleBufferTest.java @@ -0,0 +1,102 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.util; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * Test {@link TripleBuffer}. + * + * @author Matthias Arzt + */ +public class TripleBufferTest +{ + @Test + public void testSimple() + { + final TripleBuffer< ModifiableString > db = new TripleBuffer<>( ModifiableString::new ); + db.getWritableBuffer().set( "Hello" ); + db.doneWriting(); + assertEquals( "Hello", db.getReadableBuffer().getBuffer().get() ); + } + + @Test + public void testWriteTwice() + { + final TripleBuffer< ModifiableString > db = new TripleBuffer<>( ModifiableString::new ); + db.getWritableBuffer().set( "Hello" ); + db.doneWriting(); + db.getWritableBuffer().set( "World" ); + db.doneWriting(); + assertEquals( "World", db.getReadableBuffer().getBuffer().get() ); + } + + @Test + public void testReadTwice() + { + final TripleBuffer< ModifiableString > db = new TripleBuffer<>( ModifiableString::new ); + db.getWritableBuffer().set( "Hello" ); + db.doneWriting(); + assertEquals( "Hello", db.getReadableBuffer().getBuffer().get() ); + assertEquals( "Hello", db.getReadableBuffer().getBuffer().get() ); + } + + @Test + public void testThreeBuffers() + { + final TripleBuffer< ModifiableString > db = new TripleBuffer<>( ModifiableString::new ); + db.getWritableBuffer().set( "1" ); + db.doneWriting(); + db.getReadableBuffer(); + db.getWritableBuffer().set( "2" ); + db.doneWriting(); + db.getReadableBuffer(); + db.getWritableBuffer().set( "3" ); + db.doneWriting(); + db.getReadableBuffer(); + assertEquals( "1", db.getWritableBuffer().get() ); + } + + private static class ModifiableString + { + private String value = ""; + + public String get() + { + return value; + } + + public void set( final String value ) + { + this.value = value; + } + } +} diff --git a/src/test/java/bdv/viewer/BasicViewerStateTest.java b/src/test/java/bdv/viewer/BasicViewerStateTest.java new file mode 100644 index 0000000000000000000000000000000000000000..8ef2cf96fe54b2571f1b7093d3f47a1080f119a0 --- /dev/null +++ b/src/test/java/bdv/viewer/BasicViewerStateTest.java @@ -0,0 +1,411 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package bdv.viewer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntPredicate; +import mpicbg.spim.data.sequence.VoxelDimensions; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.RealRandomAccessible; +import net.imglib2.realtransform.AffineTransform3D; +import org.junit.Assert; +import org.junit.Test; + +import static bdv.viewer.DisplayMode.FUSED; +import static bdv.viewer.DisplayMode.SINGLE; +import static bdv.viewer.Interpolation.NEARESTNEIGHBOR; +import static bdv.viewer.Interpolation.NLINEAR; +import static bdv.viewer.ViewerStateChange.CURRENT_GROUP_CHANGED; +import static bdv.viewer.ViewerStateChange.CURRENT_SOURCE_CHANGED; +import static bdv.viewer.ViewerStateChange.DISPLAY_MODE_CHANGED; +import static bdv.viewer.ViewerStateChange.INTERPOLATION_CHANGED; +import static bdv.viewer.ViewerStateChange.NUM_GROUPS_CHANGED; +import static bdv.viewer.ViewerStateChange.NUM_SOURCES_CHANGED; +import static bdv.viewer.ViewerStateChange.VISIBILITY_CHANGED; + +public class BasicViewerStateTest +{ + @Test + public void addRemoveSource() + { + final BasicViewerState state = new BasicViewerState(); + final SourceAndConverter< ? > s = createSource(); + + state.addSource( s ); + Assert.assertEquals( state.getSources(), Collections.singletonList( s ) ); + + state.removeSource( s ); + Assert.assertEquals( state.getSources(), Collections.emptyList() ); + } + + @Test + public void addRemoveGroup() + { + final BasicViewerState state = new BasicViewerState(); + final SourceGroup g = new SourceGroup(); + + state.addGroup( g ); + Assert.assertEquals( state.getGroups(), Collections.singletonList( g ) ); + + state.removeGroup( g ); + Assert.assertEquals( state.getGroups(), Collections.emptyList() ); + } + + @Test + public void setGroupName() + { + final BasicViewerState state = new BasicViewerState(); + final SourceGroup g = new SourceGroup(); + state.addGroup( g ); + + final String a_group_name = "a group name"; + state.setGroupName( g, a_group_name ); + Assert.assertEquals( state.getGroupName( g ), a_group_name ); + } + + @Test + public void addSourceToGroup() + { + final BasicViewerState state = new BasicViewerState(); + final SourceGroup g = new SourceGroup(); + state.addGroup( g ); + final SourceAndConverter< ? > s = createSource(); + state.addSource( s ); + + state.addSourceToGroup( s, g ); + Assert.assertEquals( state.getSourcesInGroup( g ), Collections.singleton( s ) ); + + state.removeSourceFromGroup( s, g ); + Assert.assertEquals( state.getSourcesInGroup( g ), Collections.emptySet() ); + + state.addSourceToGroup( s, g ); + Assert.assertEquals( state.getSourcesInGroup( g ), Collections.singleton( s ) ); + + state.removeSourceFromGroup( s, g ); + Assert.assertEquals( state.getSourcesInGroup( g ), Collections.emptySet() ); + } + + @Test + public void sourceOrderComparator() + { + final BasicViewerState state = new BasicViewerState(); + final List< SourceAndConverter< ? > > expected = new ArrayList<>(); + for ( int i = 0; i < 10; ++i ) + { + final SourceAndConverter< ? > source = createSource(); + state.addSource( source ); + expected.add( source ); + } + final List< SourceAndConverter< ? > > actual = new ArrayList<>( expected ); + Collections.shuffle( actual ); + + actual.sort( state.sourceOrder() ); + + Assert.assertEquals( expected, actual ); + } + + @Test + public void sourceIndices() + { + final BasicViewerState state = new BasicViewerState(); + final SourceAndConverter< ? > s0 = createSource(); + final SourceAndConverter< ? > s1 = createSource(); + final SourceAndConverter< ? > s2 = createSource(); + + state.addSource( s0 ); + state.addSource( s1 ); + state.addSource( s2 ); + state.removeSource( s1 ); + + Assert.assertEquals( state.getSources().indexOf( s0 ), 0 ); + Assert.assertEquals( state.getSources().indexOf( s2 ), 1 ); + Assert.assertFalse( state.containsSource( s1 ) ); + } + + @Test + public void groupIndices() + { + final BasicViewerState state = new BasicViewerState(); + final SourceGroup g0 = new SourceGroup(); + final SourceGroup g1 = new SourceGroup(); + final SourceGroup g2 = new SourceGroup(); + + state.addGroup( g0 ); + state.addGroup( g1 ); + state.addGroup( g2 ); + state.removeGroup( g1 ); + + Assert.assertEquals( state.getGroups().indexOf( g0 ), 0 ); + Assert.assertEquals( state.getGroups().indexOf( g2 ), 1 ); + Assert.assertFalse( state.containsGroup( g1 ) ); + } + + @Test + public void addSourceEvents() + { + final BasicViewerState state = new BasicViewerState(); + + final ReceiveEvents r = new ReceiveEvents( NUM_SOURCES_CHANGED, CURRENT_SOURCE_CHANGED, VISIBILITY_CHANGED ); + state.changeListeners().add( r ); + + state.addSource( createSource() ); + + Assert.assertTrue( r.allReceivedExclusively() ); + } + + @Test + public void removeSourceEvents() + { + final BasicViewerState state = new BasicViewerState(); + final SourceAndConverter< ? > s0 = createSource(); + state.addSource( s0 ); + + final ReceiveEvents r = new ReceiveEvents( NUM_SOURCES_CHANGED, CURRENT_SOURCE_CHANGED, VISIBILITY_CHANGED ); + state.changeListeners().add( r ); + + state.removeSource( s0 ); + + Assert.assertTrue( r.allReceivedExclusively() ); + } + + @Test + public void addGroupEvents() + { + final BasicViewerState state = new BasicViewerState(); + state.addSource( createSource() ); + + final ReceiveEvents r = new ReceiveEvents( NUM_GROUPS_CHANGED, CURRENT_GROUP_CHANGED ); + state.changeListeners().add( r ); + + state.addGroup( new SourceGroup() ); + + Assert.assertTrue( r.allReceivedExclusively() ); + } + + @Test + public void interpolationEvents1() + { + final BasicViewerState state = new BasicViewerState(); + state.setInterpolation( NEARESTNEIGHBOR ); + + final ReceiveEvents r = new ReceiveEvents( INTERPOLATION_CHANGED ); + state.changeListeners().add( r ); + + state.setInterpolation( NLINEAR ); + + Assert.assertTrue( r.allReceivedExclusively() ); + } + + @Test + public void displayModeEvents1() + { + final BasicViewerState state = new BasicViewerState(); + final SourceAndConverter< ? > s0 = createSource(); + final SourceAndConverter< ? > s1 = createSource(); + state.addSource( s0 ); + state.addSource( s1 ); + state.setSourceActive( s0, true ); + state.setSourceActive( s1, false ); + state.setDisplayMode( SINGLE ); + + final ReceiveEvents r = new ReceiveEvents( DISPLAY_MODE_CHANGED ); + state.changeListeners().add( r ); + + state.setDisplayMode( FUSED ); + + Assert.assertTrue( r.allReceivedExclusively() ); + } + + @Test + public void displayModeEvents2() + { + final BasicViewerState state = new BasicViewerState(); + final SourceAndConverter< ? > s0 = createSource(); + final SourceAndConverter< ? > s1 = createSource(); + state.addSource( s0 ); + state.addSource( s1 ); + state.setSourceActive( s0, true ); + state.setSourceActive( s1, true ); + state.setDisplayMode( SINGLE ); + + final ReceiveEvents r = new ReceiveEvents( DISPLAY_MODE_CHANGED, VISIBILITY_CHANGED ); + state.changeListeners().add( r ); + + state.setDisplayMode( FUSED ); + + Assert.assertTrue( r.allReceivedExclusively() ); + } + + // -- helpers -- + + static class ReceiveEvents implements ViewerStateChangeListener + { + private final ViewerStateChange[] changes; + + private final List< ViewerStateChange > received = new ArrayList<>(); + + public ReceiveEvents( final ViewerStateChange... changes ) + { + this.changes = changes; + } + + @Override + public void viewerStateChanged( final ViewerStateChange change ) + { + received.add( change ); + } + + /** + * Check whether all of expected events were received at least once + */ + public boolean allReceived() + { + final Set< ViewerStateChange > expected = new HashSet<>(); + expected.addAll( Arrays.asList( changes ) ); + expected.removeAll( received ); + return expected.isEmpty(); + } + + /** + * Check whether all of expected events were received at least once, and no other event was received + */ + public boolean allReceivedExclusively() + { + final Set< ViewerStateChange > expected = new HashSet<>(); + expected.addAll( Arrays.asList( changes ) ); + expected.removeAll( received ); + final Set< ViewerStateChange > additional = new HashSet<>(); + additional.addAll( received ); + additional.removeAll( Arrays.asList( changes ) ); + final boolean success = expected.isEmpty() && additional.isEmpty(); + if ( !success ) + { + if ( !expected.isEmpty() ) + System.out.println( "expected, but not received: " + expected ); + if ( !additional.isEmpty() ) + System.out.println( "received, but not expected: " + additional ); + } + return success; + } + } + + static class TestSource implements Source< Void > + { + private static final AtomicInteger id = new AtomicInteger(); + + private static String nextName() + { + return "source(" + id.getAndIncrement() + ")"; + } + + private final String name; + + private final IntPredicate isPresent; + + public TestSource() + { + this( nextName(), i -> false ); + } + + public TestSource( final String name ) + { + this( name, i -> false ); + } + + public TestSource( final IntPredicate isPresent ) + { + this( nextName(), isPresent ); + } + + public TestSource( final String name, final IntPredicate isPresent ) + { + this.name = name; + this.isPresent = isPresent; + } + + @Override + public Void getType() + { + return null; + } + + @Override + public String getName() + { + return name; + } + + @Override + public VoxelDimensions getVoxelDimensions() + { + return null; + } + + @Override + public int getNumMipmapLevels() + { + return 1; + } + + @Override + public boolean isPresent( final int t ) + { + return isPresent.test( t ); + } + + @Override + public RandomAccessibleInterval< Void > getSource( final int t, final int level ) + { + return null; + } + + @Override + public RealRandomAccessible< Void > getInterpolatedSource( final int t, final int level, final Interpolation method ) + { + return null; + } + + @Override + public void getSourceTransform( final int t, final int level, final AffineTransform3D transform ) + { + transform.identity(); + } + } + + static SourceAndConverter< ? > createSource() + { + return new SourceAndConverter<>( new TestSource(), null ); + } +} diff --git a/src/test/java/bdv/viewer/InterpolationTest.java b/src/test/java/bdv/viewer/InterpolationTest.java index f5f7a8e50c5e800dca94a02e599c0d94fdee434d..8e66b504ecd1d346958ca2bff58fcb0c331ef966 100644 --- a/src/test/java/bdv/viewer/InterpolationTest.java +++ b/src/test/java/bdv/viewer/InterpolationTest.java @@ -1,3 +1,31 @@ +/*- + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2020 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package bdv.viewer; import org.junit.Assert;