diff --git a/pom.xml b/pom.xml index 42a4011c8e777f298492f6c59cc28312f31d7ee8..f8e37219ddcc86df1040f9d17b44e5979e52deda 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ <groupId>sc.fiji</groupId> <artifactId>bigdataviewer-core</artifactId> - <version>9.0.7-SNAPSHOT</version> + <version>10.0.0-SNAPSHOT</version> <name>BigDataViewer Core</name> <description>BigDataViewer core classes with minimal dependencies.</description> @@ -159,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> diff --git a/src/main/java/bdv/BehaviourTransformEventHandler3D.java b/src/main/java/bdv/BehaviourTransformEventHandler3D.java deleted file mode 100644 index 09f59a6281f623f71758757b9a5a2984a5342808..0000000000000000000000000000000000000000 --- a/src/main/java/bdv/BehaviourTransformEventHandler3D.java +++ /dev/null @@ -1,497 +0,0 @@ -/*- - * #%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.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 26d5e27257312db4c2cbf6c067ebd95bb9320d2d..042a3929c4b5914cf0be3f78e87bcaf74d593e7b 100644 --- a/src/main/java/bdv/BigDataViewer.java +++ b/src/main/java/bdv/BigDataViewer.java @@ -328,8 +328,6 @@ public class BigDataViewer 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.inputTriggerConfig( inputTriggerConfig ) ); if ( windowTitle != null ) @@ -375,11 +373,11 @@ public class BigDataViewer movieDialog = new RecordMovieDialog( viewerFrame, viewer, progressWriter ); // this is just to get updates of window size: - viewer.getDisplay().addOverlayRenderer( movieDialog ); + viewer.getDisplay().overlays().add( movieDialog ); movieMaxProjectDialog = new RecordMaxProjectionDialog( viewerFrame, viewer, progressWriter ); // this is just to get updates of window size: - viewer.getDisplay().addOverlayRenderer( movieMaxProjectDialog ); + viewer.getDisplay().overlays().add( movieMaxProjectDialog ); activeSourcesDialog = new VisibilityAndGroupingDialog( viewerFrame, viewer.state() ); diff --git a/src/main/java/bdv/TransformEventHandler.java b/src/main/java/bdv/TransformEventHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..530ba94a9f05ceb6c5197b6396ecacb22cc67ca0 --- /dev/null +++ b/src/main/java/bdv/TransformEventHandler.java @@ -0,0 +1,74 @@ +/* + * #%L + * ImgLib2: a general-purpose, multidimensional image processing library. + * %% + * Copyright (C) 2009 - 2016 Tobias Pietzsch, Stephan Preibisch, Stephan Saalfeld, + * John Bogovic, Albert Cardona, Barry DeZonia, Christian Dietz, Jan Funke, + * Aivar Grislis, Jonathan Hale, Grant Harris, Stefan Helfrich, Mark Hiner, + * Martin Horn, Steffen Jaensch, Lee Kamentsky, Larry Lindsey, Melissa Linkert, + * Mark Longair, Brian Northan, Nick Perry, Curtis Rueden, Johannes Schindelin, + * Jean-Yves Tinevez and Michael Zinsmaier. + * %% + * 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..1b809e18004684cb99e93629da42edbb0b00dea4 --- /dev/null +++ b/src/main/java/bdv/TransformEventHandler2D.java @@ -0,0 +1,464 @@ +/*- + * #%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 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 -- + + private static final String DRAG_TRANSLATE = "2d drag translate"; + private static final String DRAG_ROTATE = "2d drag rotate"; + + private static final String ZOOM_NORMAL = "2d scroll zoom"; + private static final String SCROLL_TRANSLATE = "2d scroll translate"; + private static final String SCROLL_ROTATE = "2d scroll rotate"; + private static final String ROTATE_LEFT = "2d rotate left"; + private static final String ROTATE_RIGHT = "2d rotate right"; + private static final String KEY_ZOOM_IN = "2d zoom in"; + private static final String KEY_ZOOM_OUT = "2d zoom out"; + + private static final String ZOOM_FAST = "2d scroll zoom fast"; + private static final String SCROLL_TRANSLATE_FAST = "2d scroll translate fast"; + private static final String SCROLL_ROTATE_FAST = "2d scroll rotate fast"; + private static final String ROTATE_LEFT_FAST = "2d rotate left fast"; + private static final String ROTATE_RIGHT_FAST = "2d rotate right fast"; + private static final String KEY_ZOOM_IN_FAST = "2d zoom in fast"; + private static final String KEY_ZOOM_OUT_FAST = "2d zoom out fast"; + + private static final String ZOOM_SLOW = "2d scroll zoom slow"; + private static final String SCROLL_TRANSLATE_SLOW = "2d scroll translate slow"; + private static final String SCROLL_ROTATE_SLOW = "2d scroll rotate slow"; + private static final String ROTATE_LEFT_SLOW = "2d rotate left slow"; + private static final String ROTATE_RIGHT_SLOW = "2d rotate right slow"; + private static final String KEY_ZOOM_IN_SLOW = "2d zoom in slow"; + private 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[] { "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[] { "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[] ZOOM_FAST_KEYS = new String[] { "shift scroll" }; + private static final String[] SCROLL_TRANSLATE_FAST_KEYS = new String[] { "not mapped" }; + private static final String[] SCROLL_TRANSLATE_SLOW_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_ROTATE_FAST_KEYS = new String[] { "shift scroll" }; + private static final String[] SCROLL_ROTATE_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" }; + + // -- 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..e690d9a9337d3c5697d35e1ad6881e63c602e2c5 --- /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/BehaviourTransformEventHandlerFactory.java b/src/main/java/bdv/TransformEventHandlerFactory.java similarity index 63% rename from src/main/java/bdv/BehaviourTransformEventHandlerFactory.java rename to src/main/java/bdv/TransformEventHandlerFactory.java index 3fa4f3a6d8f2c966050cfb6b63f1c551427e3e56..552571a3047639073978b30c3efbcd2a090d1980 100644 --- a/src/main/java/bdv/BehaviourTransformEventHandlerFactory.java +++ b/src/main/java/bdv/TransformEventHandlerFactory.java @@ -1,18 +1,23 @@ -/*- +/* * #%L - * BigDataViewer core classes with minimal dependencies. + * ImgLib2: a general-purpose, multidimensional image processing library. * %% - * Copyright (C) 2012 - 2020 BigDataViewer developers. + * Copyright (C) 2009 - 2016 Tobias Pietzsch, Stephan Preibisch, Stephan Saalfeld, + * John Bogovic, Albert Cardona, Barry DeZonia, Christian Dietz, Jan Funke, + * Aivar Grislis, Jonathan Hale, Grant Harris, Stefan Helfrich, Mark Hiner, + * Martin Horn, Steffen Jaensch, Lee Kamentsky, Larry Lindsey, Melissa Linkert, + * Mark Longair, Brian Northan, Nick Perry, Curtis Rueden, Johannes Schindelin, + * Jean-Yves Tinevez and Michael Zinsmaier. * %% * 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 @@ -28,11 +33,15 @@ */ package bdv; -import org.scijava.ui.behaviour.io.InputTriggerConfig; - -import net.imglib2.ui.TransformEventHandlerFactory; - -public interface BehaviourTransformEventHandlerFactory< A > extends TransformEventHandlerFactory< A > +/** + * Factory for {@code TransformEventHandler}. + * + * @author Tobias Pietzsch + */ +public interface TransformEventHandlerFactory { - public void setConfig( final InputTriggerConfig config ); + /** + * Create a new {@code TransformEventHandler}. + */ + TransformEventHandler create( TransformState transformState ); } diff --git a/src/main/java/bdv/TransformState.java b/src/main/java/bdv/TransformState.java new file mode 100644 index 0000000000000000000000000000000000000000..937c3119c97f1c6153326c4ce79e86992fcb53a5 --- /dev/null +++ b/src/main/java/bdv/TransformState.java @@ -0,0 +1,50 @@ +package bdv; + +import java.util.function.Consumer; +import net.imglib2.realtransform.AffineTransform3D; + +public interface TransformState +{ + /** + * Get the current transform. + * + * @param transform + * is set to the current transform + */ + void get( AffineTransform3D transform ); + + /** + * 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 ); + + static TransformState from( Consumer< AffineTransform3D > get, Consumer< AffineTransform3D > set ) + { + return new TransformState() + { + @Override + public void get( final AffineTransform3D transform ) + { + get.accept( transform ); + } + + @Override + public void set( final AffineTransform3D transform ) + { + set.accept( transform ); + } + }; + } +} diff --git a/src/main/java/bdv/export/n5/WriteSequenceToN5.java b/src/main/java/bdv/export/n5/WriteSequenceToN5.java index a4347857d4e3f8af365796cb6ee2dcd3e1c3d86d..9de577c7c4275ec4ec957b5a5d2b1ce457b56a1f 100644 --- a/src/main/java/bdv/export/n5/WriteSequenceToN5.java +++ b/src/main/java/bdv/export/n5/WriteSequenceToN5.java @@ -104,7 +104,7 @@ public class WriteSequenceToN5 * {@link ExportMipmapInfo} contains for each mipmap level, the * subsampling factors and subdivision block sizes. * @param compression - * whether to compress the data with the HDF5 DEFLATE filter. + * n5 compression scheme. * @param n5File * n5 root. * @param loopbackHeuristic diff --git a/src/main/java/bdv/tools/InitializeViewerState.java b/src/main/java/bdv/tools/InitializeViewerState.java index 0d8edea0198b5bbfd20d8c2c6f126b95547a112a..02f1cb8562cf248382b35c69a9527ee660bc6be7 100644 --- a/src/main/java/bdv/tools/InitializeViewerState.java +++ b/src/main/java/bdv/tools/InitializeViewerState.java @@ -77,7 +77,7 @@ public class InitializeViewerState { final Dimension dim = viewer.getDisplay().getSize(); final AffineTransform3D viewerTransform = initTransform( dim.width, dim.height, false, viewer.state().snapshot() ); - viewer.setCurrentViewerTransform( viewerTransform ); + viewer.state().setViewerTransform( viewerTransform ); } /** diff --git a/src/main/java/bdv/tools/RecordMaxProjectionDialog.java b/src/main/java/bdv/tools/RecordMaxProjectionDialog.java index 5e4680d2708d346bdcb343e2cdd0de029ef99ade..d2b076638596892417775eb2879c09fcd123ee0e 100644 --- a/src/main/java/bdv/tools/RecordMaxProjectionDialog.java +++ b/src/main/java/bdv/tools/RecordMaxProjectionDialog.java @@ -32,10 +32,10 @@ import bdv.cache.CacheControl; import bdv.export.ProgressWriter; import bdv.util.Prefs; import bdv.viewer.BasicViewerState; -import bdv.viewer.SynchronizedViewerState; 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; @@ -73,9 +73,8 @@ 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; public class RecordMaxProjectionDialog extends JDialog implements OverlayRenderer @@ -313,14 +312,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() { @@ -329,8 +325,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 ) @@ -343,7 +352,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 @@ -360,7 +368,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 ) @@ -377,7 +385,7 @@ public class RecordMaxProjectionDialog extends JDialog implements OverlayRendere affine.concatenate( tGV ); renderState.setViewerTransform( affine ); renderer.requestRepaint(); - renderer.paint( new bdv.viewer.state.ViewerState( new SynchronizedViewerState( renderState ) ) ); + renderer.paint( renderState ); } final BufferedImage bi = target.accumulated.image(); diff --git a/src/main/java/bdv/tools/RecordMovieDialog.java b/src/main/java/bdv/tools/RecordMovieDialog.java index d4660159d1913ec168a3b1346889c367e0185dfe..9ae851aa1bcd269c953521ebcd1ff63f0987ca5c 100644 --- a/src/main/java/bdv/tools/RecordMovieDialog.java +++ b/src/main/java/bdv/tools/RecordMovieDialog.java @@ -32,10 +32,10 @@ import bdv.cache.CacheControl; import bdv.export.ProgressWriter; import bdv.util.Prefs; import bdv.viewer.BasicViewerState; -import bdv.viewer.SynchronizedViewerState; 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; @@ -67,9 +67,8 @@ import javax.swing.WindowConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; 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 { @@ -269,17 +268,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 BufferedImage setBufferedImage( final BufferedImage bufferedImage ) + public BufferedImageRenderResult getReusableRenderResult() { - bi = bufferedImage; - return null; + return renderResult; } + @Override + public BufferedImageRenderResult createRenderResult() + { + return new BufferedImageRenderResult(); + } + + @Override + public void setRenderResult( final BufferedImageRenderResult renderResult ) + {} + @Override public int getWidth() { @@ -294,24 +302,25 @@ 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 ) { renderState.setCurrentTimepoint( timepoint ); renderer.requestRepaint(); - renderer.paint( new bdv.viewer.state.ViewerState( new SynchronizedViewerState( renderState ) ) ); + 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/VisibilityAndGroupingDialog.java b/src/main/java/bdv/tools/VisibilityAndGroupingDialog.java index 09b2d22cc00601bbc8a81116f5026dad568895ab..b4a42dca5980fff34b6b93abacda8e8eb0ad09f1 100644 --- a/src/main/java/bdv/tools/VisibilityAndGroupingDialog.java +++ b/src/main/java/bdv/tools/VisibilityAndGroupingDialog.java @@ -32,7 +32,6 @@ import bdv.viewer.SourceAndConverter; import bdv.viewer.ViewerState; import bdv.viewer.VisibilityAndGrouping; import bdv.viewer.SourceGroup; -import bdv.viewer.SynchronizedViewerState; import bdv.viewer.ViewerStateChange; import bdv.viewer.ViewerStateChangeListener; import java.awt.BorderLayout; @@ -42,12 +41,16 @@ import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Window; import java.awt.event.ActionEvent; +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; @@ -89,7 +92,7 @@ public class VisibilityAndGroupingDialog extends JDialog { super( owner, "visibility and grouping", false ); - visibilityPanel = new VisibilityPanel( state ); + visibilityPanel = new VisibilityPanel( state, this::isVisible ); visibilityPanel.setBorder( BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder( 4, 2, 4, 2 ), BorderFactory.createCompoundBorder( @@ -98,7 +101,7 @@ public class VisibilityAndGroupingDialog extends JDialog "visibility" ), BorderFactory.createEmptyBorder( 2, 2, 2, 2 ) ) ) ); - groupingPanel = new GroupingPanel( state ); + groupingPanel = new GroupingPanel( state, this::isVisible ); groupingPanel.setBorder( BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder( 4, 2, 4, 2 ), BorderFactory.createCompoundBorder( @@ -107,7 +110,7 @@ public class VisibilityAndGroupingDialog extends JDialog "grouping" ), BorderFactory.createEmptyBorder( 2, 2, 2, 2 ) ) ) ); - modePanel = new ModePanel( state ); + modePanel = new ModePanel( state, this::isVisible ); final JPanel content = new JPanel(); content.setLayout( new BoxLayout( content, BoxLayout.PAGE_AXIS ) ); @@ -132,6 +135,17 @@ 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 ); } @@ -155,91 +169,128 @@ public class VisibilityAndGroupingDialog extends JDialog private final ArrayList< Consumer< Set< SourceAndConverter< ? > > > > updateVisibleBoxes = new ArrayList<>(); - public VisibilityPanel( final ViewerState state ) + private final BooleanSupplier isVisible; + + public VisibilityPanel( final ViewerState state, BooleanSupplier isVisible ) { super( new GridBagLayout() ); this.state = state; + this.isVisible = isVisible; state.changeListeners().add( this ); - synchronized ( state ) - { - recreateContent(); - update(); - } } - protected void recreateContent() + private void shown() { - removeAll(); - currentButtonsMap.clear(); - updateActiveBoxes.clear(); - updateVisibleBoxes.clear(); + if ( recreateContentPending.getAndSet( false ) ) + recreateContentNow(); + if ( updatePending.getAndSet( false ) ) + updateNow(); + } - final List< SourceAndConverter < ? > > sources = state.getSources(); + private final AtomicBoolean recreateContentPending = new AtomicBoolean( true ); - 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 ) + private void recreateContent() + { + if ( isVisible.getAsBoolean() ) { - final JRadioButton b = new JRadioButton(); - b.addActionListener( e -> { - if ( b.isSelected() ) - state.setCurrentSource( source ); - } ); - currentButtonsMap.put( source, 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 ( final SourceAndConverter< ? > source : sources ) + private void recreateContentNow() + { + synchronized ( state ) { - final JCheckBox b = new JCheckBox(); - b.addActionListener( e -> state.setSourceActive( source, b.isSelected() ) ); - updateActiveBoxes.add( active -> b.setSelected( active.contains( source ) ) ); - add( b, c ); + 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 ) + { + 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 ( final SourceAndConverter< ? > source : sources ) + private final AtomicBoolean updatePending = new AtomicBoolean( true ); + + private void update() + { + if ( isVisible.getAsBoolean() ) { - final JCheckBox b = new JCheckBox(); - updateVisibleBoxes.add( visible -> b.setSelected( visible.contains( source ) ) ); - 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() { final SourceAndConverter< ? > currentSource = state.getCurrentSource(); if ( currentSource == null ) @@ -257,6 +308,9 @@ public class VisibilityAndGroupingDialog extends JDialog @Override public void viewerStateChanged( final ViewerStateChange change ) { + final AtomicBoolean pendingUpdate = new AtomicBoolean(); + final AtomicBoolean pendingRecreate = new AtomicBoolean(); + switch ( change ) { case CURRENT_SOURCE_CHANGED: @@ -265,13 +319,7 @@ public class VisibilityAndGroupingDialog extends JDialog SwingUtilities.invokeLater( this::update ); break; case NUM_SOURCES_CHANGED: - SwingUtilities.invokeLater( () -> { - synchronized ( state ) - { - recreateContent(); - update(); - } - } ); + SwingUtilities.invokeLater( this::recreateContent ); break; } } @@ -287,10 +335,13 @@ public class VisibilityAndGroupingDialog extends JDialog private JCheckBox fusedModeBox; - public ModePanel( final ViewerState state ) + private final BooleanSupplier isVisible; + + public ModePanel( final ViewerState state, BooleanSupplier isVisible ) { super( new GridBagLayout() ); this.state = state; + this.isVisible = isVisible; state.changeListeners().add( this ); synchronized ( state ) { @@ -299,6 +350,12 @@ public class VisibilityAndGroupingDialog extends JDialog } } + private void shown() + { + if ( updatePending.getAndSet( false ) ) + updateNow(); + } + private void recreateContent() { final GridBagConstraints c = new GridBagConstraints(); @@ -333,7 +390,20 @@ public class VisibilityAndGroupingDialog extends JDialog add( new JLabel("enable fused mode"), c ); } + private final AtomicBoolean updatePending = new AtomicBoolean( true ); + private void update() + { + if ( isVisible.getAsBoolean() ) + { + updateNow(); + updatePending.set( false ); + } + else + updatePending.set( true ); + } + + private void updateNow() { groupingBox.setSelected( state.getDisplayMode().hasGrouping() ); fusedModeBox.setSelected( state.getDisplayMode().hasFused() ); @@ -361,154 +431,184 @@ public class VisibilityAndGroupingDialog extends JDialog private final ViewerState state; - public GroupingPanel( final ViewerState state ) + private final BooleanSupplier isVisible; + + public GroupingPanel( final ViewerState state, BooleanSupplier isVisible ) { super( new GridBagLayout() ); this.state = state; + this.isVisible = isVisible; state.changeListeners().add( this ); - synchronized ( state ) - { - recreateContent(); - update(); - } } - private void recreateContent() + private void shown() { - 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 ) - { - final JTextField tf = new JTextField( state.getGroupName( group ), 10 ); - tf.getDocument().addDocumentListener( new DocumentListener() - { - private void doit() - { - state.setGroupName( group, tf.getText() ); - } - - @Override - public void removeUpdate( final DocumentEvent e ) - { - doit(); - } + 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(); + } - @Override - public void insertUpdate( final DocumentEvent e ) - { - doit(); - } + private final AtomicBoolean recreateContentPending = new AtomicBoolean( true ); - @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 ( final SourceGroup group : groups ) + private void recreateContent() + { + if ( isVisible.getAsBoolean() ) { - final JRadioButton b = new JRadioButton(); - b.addActionListener( e -> { - if ( b.isSelected() ) - state.setCurrentGroup( group ); - } ); - currentButtonsMap.put( group, b ); - currentButtonGroup.add( b ); - add( b, c ); + recreateContentNow(); + recreateContentPending.set( false ); } + else + recreateContentPending.set( true ); + } - // "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 ) + private void recreateContentNow() + { + synchronized ( state ) { - final JCheckBox b = new JCheckBox(); - b.addActionListener( e -> state.setGroupActive( group, b.isSelected() ) ); - updateActiveBoxes.add( active -> b.setSelected( active.contains( group ) ) ); - add( b, c ); - } + 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 ) + { + final JTextField tf = new JTextField( state.getGroupName( group ), 10 ); + tf.getDocument().addDocumentListener( new DocumentListener() + { + private void doit() + { + state.setGroupName( group, tf.getText() ); + } + + @Override + public void removeUpdate( final DocumentEvent e ) + { + doit(); + } + + @Override + public void insertUpdate( final DocumentEvent e ) + { + doit(); + } + + @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 ); + } - // 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 ) - { - c.gridy = 1; + // "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 ) { - final JCheckBox b = new JCheckBox(); + final JRadioButton b = new JRadioButton(); b.addActionListener( e -> { if ( b.isSelected() ) - state.addSourceToGroup( source, group ); - else - state.removeSourceFromGroup( source, group ); - } ); - updateAssignBoxes.add( () -> { - b.setSelected( state.getSourcesInGroup( group ).contains( source ) ); + state.setCurrentGroup( group ); } ); + currentButtonsMap.put( group, b ); + currentButtonGroup.add( b ); add( b, c ); - c.gridy++; } - c.gridx++; - } - invalidate(); - final Window frame = SwingUtilities.getWindowAncestor( this ); - if ( frame != null ) - frame.pack(); + // "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 ) + { + 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 = 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 ) + { + c.gridy = 1; + for ( final SourceGroup group : groups ) + { + final JCheckBox b = new JCheckBox(); + b.addActionListener( e -> { + if ( b.isSelected() ) + state.addSourceToGroup( source, group ); + else + 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() @@ -519,23 +619,75 @@ public class VisibilityAndGroupingDialog extends JDialog updateGroupAssignments(); } + private final AtomicBoolean updateGroupNamesPending = new AtomicBoolean( true ); + private void updateGroupNames() + { + if ( isVisible.getAsBoolean() ) + { + updateGroupNamesNow(); + updateGroupNamesPending.set( false ); + } + else + updateGroupNamesPending.set( true ); + } + + private void updateGroupNamesNow() { updateNames.forEach( Runnable::run ); } + private final AtomicBoolean updateGroupAssignmentsPending = new AtomicBoolean( true ); + private void updateGroupAssignments() + { + if ( isVisible.getAsBoolean() ) + { + updateGroupAssignmentsNow(); + updateGroupAssignmentsPending.set( false ); + } + else + updateGroupAssignmentsPending.set( true ); + } + + private void updateGroupAssignmentsNow() { updateAssignBoxes.forEach( Runnable::run ); } + private final AtomicBoolean updateGroupActivityPending = new AtomicBoolean( true ); + 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 ) ); } + private final AtomicBoolean updateCurrentGroupPending = new AtomicBoolean( true ); + private void updateCurrentGroup() + { + if ( isVisible.getAsBoolean() ) + { + updateCurrentGroupNow(); + updateCurrentGroupPending.set( false ); + } + else + updateCurrentGroupPending.set( true ); + } + + private void updateCurrentGroupNow() { final SourceGroup currentGroup = state.getCurrentGroup(); if ( currentGroup == null ) @@ -563,13 +715,7 @@ public class VisibilityAndGroupingDialog extends JDialog break; case NUM_GROUPS_CHANGED: case NUM_SOURCES_CHANGED: - SwingUtilities.invokeLater( () -> { - synchronized ( state ) - { - recreateContent(); - update(); - } - } ); + SwingUtilities.invokeLater( this::recreateContent ); break; } } diff --git a/src/main/java/bdv/tools/boundingbox/BoundingBoxDialog.java b/src/main/java/bdv/tools/boundingbox/BoundingBoxDialog.java index 43c71f4f3f01ce2a7d90fe438bd6228da00ed83c..52002dbe20e47434401d8ce7812f984b5524ddc9 100644 --- a/src/main/java/bdv/tools/boundingbox/BoundingBoxDialog.java +++ b/src/main/java/bdv/tools/boundingbox/BoundingBoxDialog.java @@ -202,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 ); } } @@ -218,7 +218,7 @@ public class BoundingBoxDialog extends JDialog } if ( showBoxOverlay ) { - viewer.getDisplay().removeOverlayRenderer( boxOverlay ); + viewer.getDisplay().overlays().remove( boxOverlay ); viewer.removeTransformListener( boxOverlay ); } } diff --git a/src/main/java/bdv/tools/boundingbox/TransformedBoxEditor.java b/src/main/java/bdv/tools/boundingbox/TransformedBoxEditor.java index 5c828ddd63de76dc1326c605fb4a5d80f6015f3b..0dcc495436eb7f00bbde294d0ff1e84565ac02dc 100644 --- a/src/main/java/bdv/tools/boundingbox/TransformedBoxEditor.java +++ b/src/main/java/bdv/tools/boundingbox/TransformedBoxEditor.java @@ -155,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(); @@ -168,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/TransformedBoxOverlay.java b/src/main/java/bdv/tools/boundingbox/TransformedBoxOverlay.java index 97a127982b9ca1d8dc18a45a2953540156921014..7439703cad1b91241acfb288388da9e8d0c20e4c 100644 --- a/src/main/java/bdv/tools/boundingbox/TransformedBoxOverlay.java +++ b/src/main/java/bdv/tools/boundingbox/TransformedBoxOverlay.java @@ -45,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/brightness/BrightnessDialog.java b/src/main/java/bdv/tools/brightness/BrightnessDialog.java index 0cd6be4dc339814e606df9e70536c38ccb3a8158..1e71f960cdaec7ba438f14de6c7fe35903ae15c1 100644 --- a/src/main/java/bdv/tools/brightness/BrightnessDialog.java +++ b/src/main/java/bdv/tools/brightness/BrightnessDialog.java @@ -42,6 +42,7 @@ 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; @@ -103,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 @@ -111,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 ) @@ -122,6 +134,19 @@ public class BrightnessDialog extends JDialog } } ); + addComponentListener( new ComponentAdapter() + { + @Override + public void componentShown( final ComponentEvent e ) + { + if ( recreateContentPending.getAndSet( false ) ) + { + colorsPanel.recreateContent(); + minMaxPanels.recreateContent(); + } + } + } ); + pack(); setDefaultCloseOperation( WindowConstants.HIDE_ON_CLOSE ); } diff --git a/src/main/java/bdv/tools/transformation/ManualTransformationEditor.java b/src/main/java/bdv/tools/transformation/ManualTransformationEditor.java index 60d03b1fce1a6a6e7fefe3fc69abb016ab356880..d0f9146ba36c40c5a1f6b7d5be222052e1331973 100644 --- a/src/main/java/bdv/tools/transformation/ManualTransformationEditor.java +++ b/src/main/java/bdv/tools/transformation/ManualTransformationEditor.java @@ -39,7 +39,7 @@ import javax.swing.ActionMap; import javax.swing.InputMap; import javax.swing.KeyStroke; 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; @@ -97,7 +97,7 @@ 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 ) ); @@ -118,7 +118,7 @@ public class ManualTransformationEditor implements TransformListener< AffineTran { source.setIncrementalTransform( identity ); } - viewer.setCurrentViewerTransform( frozenTransform ); + viewer.state().setViewerTransform( frozenTransform ); viewer.showMessage( "reset manual transform" ); } } @@ -181,7 +181,7 @@ 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" ); } manualTransformActiveListeners.list.forEach( l -> l.manualTransformActiveChanged( active ) ); diff --git a/src/main/java/bdv/util/AWTUtils.java b/src/main/java/bdv/util/AWTUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c26c86d626bc38ee30e9dec031678ab679a24c27 --- /dev/null +++ b/src/main/java/bdv/util/AWTUtils.java @@ -0,0 +1,118 @@ +/* + * #%L + * ImgLib2: a general-purpose, multidimensional image processing library. + * %% + * Copyright (C) 2009 - 2016 Tobias Pietzsch, Stephan Preibisch, Stephan Saalfeld, + * John Bogovic, Albert Cardona, Barry DeZonia, Christian Dietz, Jan Funke, + * Aivar Grislis, Jonathan Hale, Grant Harris, Stefan Helfrich, Mark Hiner, + * Martin Horn, Steffen Jaensch, Lee Kamentsky, Larry Lindsey, Melissa Linkert, + * Mark Longair, Brian Northan, Nick Perry, Curtis Rueden, Johannes Schindelin, + * Jean-Yves Tinevez and Michael Zinsmaier. + * %% + * 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/MovingAverage.java b/src/main/java/bdv/util/MovingAverage.java new file mode 100644 index 0000000000000000000000000000000000000000..8ea05bd5d943247f2d42ce100907772b5cbe3be6 --- /dev/null +++ b/src/main/java/bdv/util/MovingAverage.java @@ -0,0 +1,43 @@ +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/TripleBuffer.java b/src/main/java/bdv/util/TripleBuffer.java new file mode 100644 index 0000000000000000000000000000000000000000..36f7fa35c79c3cefa5645cbdedf5bcf2dcd7fe23 --- /dev/null +++ b/src/main/java/bdv/util/TripleBuffer.java @@ -0,0 +1,132 @@ +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/viewer/InteractiveDisplayCanvas.java b/src/main/java/bdv/viewer/InteractiveDisplayCanvas.java new file mode 100644 index 0000000000000000000000000000000000000000..2f2b6a228049909241f9e1e86b97b2aaac4d67dd --- /dev/null +++ b/src/main/java/bdv/viewer/InteractiveDisplayCanvas.java @@ -0,0 +1,223 @@ +/* + * #%L + * ImgLib2: a general-purpose, multidimensional image processing library. + * %% + * Copyright (C) 2009 - 2016 Tobias Pietzsch, Stephan Preibisch, Stephan Saalfeld, + * John Bogovic, Albert Cardona, Barry DeZonia, Christian Dietz, Jan Funke, + * Aivar Grislis, Jonathan Hale, Grant Harris, Stefan Helfrich, Mark Hiner, + * Martin Horn, Steffen Jaensch, Lee Kamentsky, Larry Lindsey, Melissa Linkert, + * Mark Longair, Brian Northan, Nick Perry, Curtis Rueden, Johannes Schindelin, + * Jean-Yves Tinevez and Michael Zinsmaier. + * %% + * 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/render/TransformAwareRenderTarget.java b/src/main/java/bdv/viewer/OverlayRenderer.java similarity index 52% rename from src/main/java/bdv/viewer/render/TransformAwareRenderTarget.java rename to src/main/java/bdv/viewer/OverlayRenderer.java index b9f84b92f43dd7cfce6e9a9298928c3797492566..7b8ba718fabe2be2b91285103018f357b7b34525 100644 --- a/src/main/java/bdv/viewer/render/TransformAwareRenderTarget.java +++ b/src/main/java/bdv/viewer/OverlayRenderer.java @@ -1,18 +1,23 @@ /* * #%L - * BigDataViewer core classes with minimal dependencies. + * ImgLib2: a general-purpose, multidimensional image processing library. * %% - * Copyright (C) 2012 - 2020 BigDataViewer developers. + * Copyright (C) 2009 - 2016 Tobias Pietzsch, Stephan Preibisch, Stephan Saalfeld, + * John Bogovic, Albert Cardona, Barry DeZonia, Christian Dietz, Jan Funke, + * Aivar Grislis, Jonathan Hale, Grant Harris, Stefan Helfrich, Mark Hiner, + * Martin Horn, Steffen Jaensch, Lee Kamentsky, Larry Lindsey, Melissa Linkert, + * Mark Longair, Brian Northan, Nick Perry, Curtis Rueden, Johannes Schindelin, + * Jean-Yves Tinevez and Michael Zinsmaier. * %% * 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 @@ -26,28 +31,33 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package bdv.viewer.render; +package bdv.viewer; -import java.awt.image.BufferedImage; +import java.awt.Graphics; -import net.imglib2.realtransform.AffineTransform3D; -import net.imglib2.ui.RenderTarget; -import net.imglib2.ui.TransformListener; - -public interface TransformAwareRenderTarget extends RenderTarget +/** + * Draw something to a {@link Graphics} canvas and receive notifications about + * changes of the canvas size. + * + * @author Tobias Pietzsch + */ +public interface OverlayRenderer { /** - * Set the {@link BufferedImage} that is to be drawn on the canvas, and the - * transform with which this image was created. - * - * @param img - * image to draw (may be null). + * Render overlays. */ - public BufferedImage setBufferedImageAndTransform( final BufferedImage img, final AffineTransform3D transform ); - - public void addTransformListener( final TransformListener< AffineTransform3D > listener ); + void drawOverlays( Graphics g ); - public void addTransformListener( final TransformListener< AffineTransform3D > listener, final int index ); - - public void removeTransformListener( final TransformListener< AffineTransform3D > listener ); + /** + * 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 f0cc527630e3ddb7a8e38301a4e7b80b711ec1c3..a6eedd483db18dbca2a31166a591f42c189c4edd 100644 --- a/src/main/java/bdv/viewer/RequestRepaint.java +++ b/src/main/java/bdv/viewer/RequestRepaint.java @@ -33,5 +33,5 @@ public interface RequestRepaint /** * Repaint as soon as possible. */ - public void requestRepaint(); + void requestRepaint(); } diff --git a/src/main/java/bdv/viewer/TimePointListener.java b/src/main/java/bdv/viewer/TimePointListener.java index 7c3a5cf7589c3f1bcf2174515c7f3bc0bc77b39a..abd0d022b291825c966c1f06e1d721a83ecb5a3a 100644 --- a/src/main/java/bdv/viewer/TimePointListener.java +++ b/src/main/java/bdv/viewer/TimePointListener.java @@ -30,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/BehaviourTransformEventHandler.java b/src/main/java/bdv/viewer/TransformListener.java similarity index 68% rename from src/main/java/bdv/BehaviourTransformEventHandler.java rename to src/main/java/bdv/viewer/TransformListener.java index 83ce47643b22e36efe7baf8326c60227c8c203b1..0f5aa796e490dbb79a3d67d1090dfe7d8a82d3f0 100644 --- a/src/main/java/bdv/BehaviourTransformEventHandler.java +++ b/src/main/java/bdv/viewer/TransformListener.java @@ -1,18 +1,23 @@ -/*- +/* * #%L - * BigDataViewer core classes with minimal dependencies. + * ImgLib2: a general-purpose, multidimensional image processing library. * %% - * Copyright (C) 2012 - 2020 BigDataViewer developers. + * Copyright (C) 2009 - 2016 Tobias Pietzsch, Stephan Preibisch, Stephan Saalfeld, + * John Bogovic, Albert Cardona, Barry DeZonia, Christian Dietz, Jan Funke, + * Aivar Grislis, Jonathan Hale, Grant Harris, Stefan Helfrich, Mark Hiner, + * Martin Horn, Steffen Jaensch, Lee Kamentsky, Larry Lindsey, Melissa Linkert, + * Mark Longair, Brian Northan, Nick Perry, Curtis Rueden, Johannes Schindelin, + * Jean-Yves Tinevez and Michael Zinsmaier. * %% * 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 @@ -26,13 +31,9 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ -package bdv; +package bdv.viewer; -import org.scijava.ui.behaviour.util.TriggerBehaviourBindings; - -import net.imglib2.ui.TransformEventHandler; - -public interface BehaviourTransformEventHandler< A > extends TransformEventHandler< A > +public interface TransformListener< A > { - public void install( final TriggerBehaviourBindings bindings ); + void transformChanged( A transform ); } diff --git a/src/main/java/bdv/viewer/ViewerFrame.java b/src/main/java/bdv/viewer/ViewerFrame.java index a528899b52422f8ce11ac4bf65304ece5c714bd4..1eebe5bfd4099452e743e19d65a161077007f6c3 100644 --- a/src/main/java/bdv/viewer/ViewerFrame.java +++ b/src/main/java/bdv/viewer/ViewerFrame.java @@ -6,13 +6,13 @@ * %% * 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 @@ -28,6 +28,7 @@ */ package bdv.viewer; +import bdv.TransformEventHandler; import bdv.ui.CardPanel; import bdv.ui.BdvDefaultCards; import bdv.ui.splitpanel.SplitPanel; @@ -42,13 +43,12 @@ 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 @@ -98,7 +98,7 @@ 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() ); @@ -133,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() diff --git a/src/main/java/bdv/viewer/ViewerOptions.java b/src/main/java/bdv/viewer/ViewerOptions.java index bed250828e3be6ca430551a5c3fcbbfb50a86e26..fe55ed2464e87bc700545eabe796a071fbfa9b86 100644 --- a/src/main/java/bdv/viewer/ViewerOptions.java +++ b/src/main/java/bdv/viewer/ViewerOptions.java @@ -6,13 +6,13 @@ * %% * 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 @@ -28,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; @@ -41,7 +41,7 @@ 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}. @@ -110,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. * @@ -168,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; @@ -231,8 +218,6 @@ public class ViewerOptions private long targetRenderNanos = 30 * 1000000l; - private boolean doubleBuffered = true; - private int numRenderingThreads = 3; private int numSourceGroups = 10; @@ -241,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; @@ -256,7 +241,6 @@ public class ViewerOptions height( height ). screenScales( screenScales ). targetRenderNanos( targetRenderNanos ). - doubleBuffered( doubleBuffered ). numRenderingThreads( numRenderingThreads ). numSourceGroups( numSourceGroups ). useVolatileIfAvailable( useVolatileIfAvailable ). @@ -286,11 +270,6 @@ public class ViewerOptions return targetRenderNanos; } - public boolean isDoubleBuffered() - { - return doubleBuffered; - } - public int getNumRenderingThreads() { return numRenderingThreads; @@ -311,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 238502f5d6b91ad0fc6fc9bab312e53e3aa0af05..612592832ad6f0b45712c57da4e6b5b673e28cbb 100644 --- a/src/main/java/bdv/viewer/ViewerPanel.java +++ b/src/main/java/bdv/viewer/ViewerPanel.java @@ -28,6 +28,8 @@ */ package bdv.viewer; +import bdv.TransformEventHandler; +import bdv.TransformState; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Font; @@ -54,14 +56,13 @@ import javax.swing.JPanel; import javax.swing.JSlider; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; +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; @@ -73,7 +74,6 @@ 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.ViewerState; import bdv.viewer.state.XmlIoViewerState; @@ -82,17 +82,13 @@ 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 @@ -100,7 +96,7 @@ import net.imglib2.util.LinAlgHelpers; * * @author Tobias Pietzsch */ -public class ViewerPanel extends JPanel implements OverlayRenderer, TransformListener< AffineTransform3D >, PainterThread.Paintable, ViewerStateChangeListener, RequestRepaint +public class ViewerPanel extends JPanel implements OverlayRenderer, PainterThread.Paintable, ViewerStateChangeListener, RequestRepaint { private static final long serialVersionUID = 1L; @@ -118,7 +114,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis /** * TODO */ - protected final TransformAwareBufferedImageOverlayRenderer renderTarget; + protected final BufferedImageOverlayRenderer renderTarget; /** * Overlay navigation boxes. @@ -137,16 +133,13 @@ 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; @@ -183,7 +176,7 @@ 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; @@ -257,14 +250,13 @@ 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(), @@ -273,7 +265,6 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis renderTarget, painterThread, options.getScreenScales(), options.getTargetRenderNanos(), - options.isDoubleBuffered(), options.getNumRenderingThreads(), renderingExecutorService, options.isUseVolatileIfAvailable(), @@ -418,7 +409,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis { assert gPos.length >= 3; - viewerTransform.applyInverse( gPos, gPos ); + state().getViewerTransform().applyInverse( gPos, gPos ); } /** @@ -432,7 +423,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis { assert gPos.numDimensions() >= 3; - viewerTransform.applyInverse( gPos, gPos ); + state().getViewerTransform().applyInverse( gPos, gPos ); } /** @@ -448,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 ); } /** @@ -463,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 ); } /** @@ -479,7 +470,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis @Override public void paint() { - imageRenderer.paint( state ); + imageRenderer.paint( state.getState() ); display.repaint(); @@ -487,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(); } } } @@ -506,6 +497,11 @@ 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 ) { @@ -553,16 +549,6 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis display.repaint(); } - @Override - public synchronized void transformChanged( final AffineTransform3D transform ) - { - viewerTransform.set( transform ); - state.setViewerTransform( transform ); - for ( final TransformListener< AffineTransform3D > l : transformListeners ) - l.transformChanged( viewerTransform ); - requestRepaint(); - } - @Override public void viewerStateChanged( final ViewerStateChange change ) { @@ -630,7 +616,11 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis requestRepaint(); break; } -// case VIEWER_TRANSFORM_CHANGED: + case VIEWER_TRANSFORM_CHANGED: + final AffineTransform3D transform = state().getViewerTransform(); + for ( final TransformListener< AffineTransform3D > l : transformListeners ) + l.transformChanged( transform ); + requestRepaint(); } } @@ -697,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() ) @@ -712,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 ) @@ -751,12 +741,12 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis } /** - * 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 ); } /** @@ -845,11 +835,16 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis * * @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. * @@ -901,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 ); } /** @@ -963,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() ); } } @@ -979,7 +971,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis { transformListeners.remove( listener ); } - renderTarget.removeTransformListener( listener ); + renderTarget.transformListeners().remove( listener ); } /** @@ -1156,6 +1148,7 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis renderingExecutorService.shutdown(); state.kill(); imageRenderer.kill(); + renderTarget.kill(); } protected static final AtomicInteger panelNumber = new AtomicInteger( 1 ); @@ -1182,11 +1175,6 @@ public class ViewerPanel extends JPanel implements OverlayRenderer, TransformLis } } - public TransformAwareBufferedImageOverlayRenderer renderTarget() - { - return this.renderTarget; - } - @Override public boolean requestFocusInWindow() { diff --git a/src/main/java/bdv/viewer/render/AccumulateProjector.java b/src/main/java/bdv/viewer/render/AccumulateProjector.java index 483f44e4cc5e30d86ad964016c305455b1b26900..91be77098862c3dec46629db2bb75981534b836b 100644 --- a/src/main/java/bdv/viewer/render/AccumulateProjector.java +++ b/src/main/java/bdv/viewer/render/AccumulateProjector.java @@ -29,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 ) @@ -94,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 ) @@ -118,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 ); @@ -174,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 ); @@ -182,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 e7b1861c04f9d7d3975f19099eba8824fc7a718f..c92260f53354d218e78dc68740ac5e268ce22dfc 100644 --- a/src/main/java/bdv/viewer/render/AccumulateProjectorARGB.java +++ b/src/main/java/bdv/viewer/render/AccumulateProjectorARGB.java @@ -30,46 +30,257 @@ 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 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 createProjector( - final ArrayList< VolatileProjector > sourceProjectors, - final ArrayList< SourceAndConverter< ? > > sources, - final ArrayList< ? extends RandomAccessible< ? extends ARGBType > > sourceScreenImages, + 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, targetScreenImage, 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 - protected void accumulate( final Cursor< ? extends ARGBType >[] accesses, final ARGBType target ) + 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 + 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 ); @@ -87,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 4343f596c2b6c0cf240b69cc15c61344a6439d47..8a141730781be7eca6c1decbed8fa39c48fbb94d 100644 --- a/src/main/java/bdv/viewer/render/AccumulateProjectorFactory.java +++ b/src/main/java/bdv/viewer/render/AccumulateProjectorFactory.java @@ -30,6 +30,7 @@ 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; @@ -54,9 +55,9 @@ public interface AccumulateProjectorFactory< A > * {@link ExecutorService} to use for rendering. may be null. */ default VolatileProjector createProjector( - final ArrayList< VolatileProjector > sourceProjectors, - final ArrayList< SourceAndConverter< ? > > sources, - final ArrayList< ? extends RandomAccessible< ? extends A > > sourceScreenImages, + 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 ) @@ -64,11 +65,17 @@ public interface AccumulateProjectorFactory< A > final ArrayList< Source< ? > > spimSources = new ArrayList<>(); for ( SourceAndConverter< ? > source : sources ) spimSources.add( source.getSpimSource() ); - return createAccumulateProjector( sourceProjectors, spimSources, sourceScreenImages, targetScreenImage, numThreads, executorService ); + 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(ArrayList, ArrayList, ArrayList, RandomAccessibleInterval, int, ExecutorService)} instead. + * @deprecated Use {@link #createProjector(List, List, List, RandomAccessibleInterval, int, ExecutorService)} instead. * * @param sourceProjectors * projectors that will be used to render {@code sources}. diff --git a/src/main/java/bdv/viewer/render/EmptyProjector.java b/src/main/java/bdv/viewer/render/EmptyProjector.java index 67d1e6edb14a75a9b92691607783acaf1a024d1a..6aee316765ad53c77f08748cea519dbbce9f987a 100644 --- a/src/main/java/bdv/viewer/render/EmptyProjector.java +++ b/src/main/java/bdv/viewer/render/EmptyProjector.java @@ -6,13 +6,13 @@ * %% * 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 @@ -28,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 @@ -45,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/MultiResolutionRenderer.java b/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java index b21777828de374527f353ee2eb442d4e104cbbd9..7e114fbd540a243ee60a58e18dc3d2432184c091 100644 --- a/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java +++ b/src/main/java/bdv/viewer/render/MultiResolutionRenderer.java @@ -6,13 +6,13 @@ * %% * 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 @@ -28,260 +28,218 @@ */ package bdv.viewer.render; -import bdv.viewer.SourceAndConverter; -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,213 +272,247 @@ 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 ( !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(); - - // the BufferedImage that is rendered to (to paint to the canvas) - final BufferedImage bufferedImage; - - final ARGBScreenImage screenImage; - - final boolean clearQueue; - + final boolean newFrame; + final boolean newInterval; + final boolean prepareNextFrame; final boolean createProjector; - 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; + final boolean resized = screenScales.checkResize( screenW, screenH ); - if ( createProjector ) + newFrame = newFrameRequest || resized; + if ( newFrame ) { - final int renderId = renderIdQueue.peek(); - currentScreenScaleIndex = requestedScreenScaleIndex; - bufferedImage = bufferedImages[ currentScreenScaleIndex ][ renderId ]; - screenImage = screenImages[ currentScreenScaleIndex ][ renderId ]; + intervalMode = false; + screenScales.clearRequestedIntervals(); } - else + + newInterval = newIntervalRequest && !newFrame; + if ( newInterval ) { - bufferedImage = null; - screenImage = null; + intervalMode = true; + final int numSources = currentVisibleSourcesOnScreen.size(); + final double renderNanosPerPixel = renderNanosPerPixelAndSource.getAverage() * numSources; + requestedIntervalScaleIndex = screenScales.suggestIntervalScreenScale( renderNanosPerPixel, currentScreenScaleIndex ); + } + + prepareNextFrame = newFrame || newInterval; + renderingMayBeCancelled = !prepareNextFrame; + + if ( intervalMode ) + { + createProjector = newInterval || ( requestedIntervalScaleIndex != currentIntervalScaleIndex ); + if ( createProjector ) + intervalRenderData = screenScales.pullIntervalRenderData( requestedIntervalScaleIndex, currentScreenScaleIndex ); } + else + createProjector = newFrame || ( requestedScreenScaleIndex != currentScreenScaleIndex ); + + newFrameRequest = false; + newIntervalRequest = false; + } - requestedScreenScaleIndex = 0; + 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; - if ( createProjector ) + // holds new RenderResult, in case that a new projector is created in full frame mode + RenderResult renderResult = null; + + // whether to request a newFrame, in case that a new projector is created in full frame mode + boolean requestNewFrameIfIncomplete = false; + + synchronized ( this ) { - synchronized ( state.getState() ) + if ( createProjector ) { - final int numVisibleSources = state.getVisibleSourceIndices().size(); - checkRenewRenderImages( numVisibleSources ); - checkRenewMaskArrays( numVisibleSources ); - p = createProjector( state, screenImage ); + 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(); } - synchronized ( this ) + p = projector; + } + + // try rendering + final boolean success = p.map( createProjector ); + final long rendertime = p.getLastFrameRenderNanoTime(); + + synchronized ( this ) + { + // if rendering was not cancelled... + if ( success ) { - projector = p; + currentScreenScaleIndex = requestedScreenScaleIndex; + if ( createProjector ) + { + renderResult.setUpdated(); + ( ( RenderTarget ) display ).setRenderResult( renderResult ); + currentRenderResult = renderResult; + recordRenderTime( renderResult, rendertime ); + } + 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 ) { - synchronized ( this ) + if ( createProjector ) { - p = projector; + intervalResult.init( intervalRenderData.width(), intervalRenderData.height() ); + intervalResult.setScaleFactor( intervalRenderData.scale() ); + projector = createProjector( currentViewerState, currentVisibleSourcesOnScreen, requestedIntervalScaleIndex, intervalResult.getTargetImage(), intervalRenderData.offsetX(), intervalRenderData.offsetY() ); } + p = projector; } - // try rendering final boolean success = p.map( createProjector ); final long rendertime = p.getLastFrameRenderNanoTime(); @@ -532,357 +522,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 - { - Thread.sleep( 1 ); - } - catch ( final InterruptedException e ) + // if full frame rendering was not yet complete + if ( requestedScreenScaleIndex >= 0 ) { - // restore interrupted state - Thread.currentThread().interrupt(); + // go back to full frame rendering + intervalMode = false; + if ( requestedScreenScaleIndex == currentScreenScaleIndex ) + ++currentScreenScaleIndex; + painterThread.requestRepaint(); } - 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() + private void iterateRepaintInterval( final int intervalScaleIndex ) { - 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 ) - { - /* - * 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< SourceAndConverter< ? > > 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 ).getHandle() ); - sourceImages.add( renderImage ); - } - projector = accumulateProjectorFactory.createProjector( 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 ) + try { - super( source, converter, target, numThreads, executorService ); + Thread.sleep( 1 ); } - - @Override - public boolean map( final boolean clearUntouchedTargetPixels ) + catch ( final InterruptedException e ) { - final boolean success = super.map(); - valid |= success; - return success; + // restore interrupted state + Thread.currentThread().interrupt(); } - - @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 ) - { - 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 ); - } - } - - 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..16292db127f59512ef3ce7348bdd9868acaaa8af --- /dev/null +++ b/src/main/java/bdv/viewer/render/PainterThread.java @@ -0,0 +1,123 @@ +/* + * #%L + * ImgLib2: a general-purpose, multidimensional image processing library. + * %% + * Copyright (C) 2009 - 2016 Tobias Pietzsch, Stephan Preibisch, Stephan Saalfeld, + * John Bogovic, Albert Cardona, Barry DeZonia, Christian Dietz, Jan Funke, + * Aivar Grislis, Jonathan Hale, Grant Harris, Stefan Helfrich, Mark Hiner, + * Martin Horn, Steffen Jaensch, Lee Kamentsky, Larry Lindsey, Melissa Linkert, + * Mark Longair, Brian Northan, Nick Perry, Curtis Rueden, Johannes Schindelin, + * Jean-Yves Tinevez and Michael Zinsmaier. + * %% + * 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/ProjectorFactory.java b/src/main/java/bdv/viewer/render/ProjectorFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..89e249e451855fb5c6c30bccf39c76f66a01c3e4 --- /dev/null +++ b/src/main/java/bdv/viewer/render/ProjectorFactory.java @@ -0,0 +1,308 @@ +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..8c0b7d476ad4ef4a2a9b532703c43069ac1d0536 --- /dev/null +++ b/src/main/java/bdv/viewer/render/ProjectorUtils.java @@ -0,0 +1,30 @@ +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..176952cdca1dc520eaf35ca3d467d200497d2582 --- /dev/null +++ b/src/main/java/bdv/viewer/render/RenderResult.java @@ -0,0 +1,70 @@ +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..d98e99f08ba7c9cf87da66f4aa77d920e4f6c2b0 --- /dev/null +++ b/src/main/java/bdv/viewer/render/RenderStorage.java @@ -0,0 +1,64 @@ +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..7a23c607e26ad6d0eaaa7e6935f7008926f64d95 --- /dev/null +++ b/src/main/java/bdv/viewer/render/RenderTarget.java @@ -0,0 +1,83 @@ +/* + * #%L + * ImgLib2: a general-purpose, multidimensional image processing library. + * %% + * Copyright (C) 2009 - 2016 Tobias Pietzsch, Stephan Preibisch, Stephan Saalfeld, + * John Bogovic, Albert Cardona, Barry DeZonia, Christian Dietz, Jan Funke, + * Aivar Grislis, Jonathan Hale, Grant Harris, Stefan Helfrich, Mark Hiner, + * Martin Horn, Steffen Jaensch, Lee Kamentsky, Larry Lindsey, Melissa Linkert, + * Mark Longair, Brian Northan, Nick Perry, Curtis Rueden, Johannes Schindelin, + * Jean-Yves Tinevez and Michael Zinsmaier. + * %% + * 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..a2aa016d0578311178b98fac078e9ed4fde9bf1d --- /dev/null +++ b/src/main/java/bdv/viewer/render/ScreenScales.java @@ -0,0 +1,318 @@ +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; + } + + 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..47640270913d8792b79dc63419c1c851bed8a370 --- /dev/null +++ b/src/main/java/bdv/viewer/render/SimpleVolatileProjector.java @@ -0,0 +1,224 @@ +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 0296ce68ef4829e7d46182e5fb36ca34fe5512a5..0000000000000000000000000000000000000000 --- a/src/main/java/bdv/viewer/render/TransformAwareBufferedImageOverlayRenderer.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * #%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.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..cb5b182c10d11187709b853657dda6156f3280bf --- /dev/null +++ b/src/main/java/bdv/viewer/render/VisibilityUtils.java @@ -0,0 +1,90 @@ +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 ) + { + 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 12c81e1ddf45cdc38dfa1b372befd3d9b3ead2c2..9d83f6b8fbd25d4f7e520f0cf20f0f5f475602f3 100644 --- a/src/main/java/bdv/viewer/render/VolatileHierarchyProjector.java +++ b/src/main/java/bdv/viewer/render/VolatileHierarchyProjector.java @@ -6,13 +6,13 @@ * %% * 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 @@ -36,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; @@ -47,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; /** @@ -60,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, @@ -148,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; @@ -179,7 +179,7 @@ public class VolatileHierarchyProjector< A extends Volatile< ? >, B extends Nume @Override public void cancel() { - interrupted.set( true ); + canceled.set( true ); } @Override @@ -205,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; - final StopWatch stopWatch = new StopWatch(); - stopWatch.start(); + valid = false; + + 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 0460c969e1ebd6ed88059bae908a4b8e2eea5069..4e94698a5148b3fc9067b770cf39686ad8ffdd15 100644 --- a/src/main/java/bdv/viewer/render/VolatileProjector.java +++ b/src/main/java/bdv/viewer/render/VolatileProjector.java @@ -28,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. @@ -40,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..1ba2b08059cd768a7ae1e0641154649e13b55478 --- /dev/null +++ b/src/main/java/bdv/viewer/render/awt/BufferedImageOverlayRenderer.java @@ -0,0 +1,182 @@ +/* + * #%L + * ImgLib2: a general-purpose, multidimensional image processing library. + * %% + * Copyright (C) 2009 - 2016 Tobias Pietzsch, Stephan Preibisch, Stephan Saalfeld, + * John Bogovic, Albert Cardona, Barry DeZonia, Christian Dietz, Jan Funke, + * Aivar Grislis, Jonathan Hale, Grant Harris, Stefan Helfrich, Mark Hiner, + * Martin Horn, Steffen Jaensch, Lee Kamentsky, Larry Lindsey, Melissa Linkert, + * Mark Longair, Brian Northan, Nick Perry, Curtis Rueden, Johannes Schindelin, + * Jean-Yves Tinevez and Michael Zinsmaier. + * %% + * 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..2b9d882e9f4fccf6d514b9bb033611604cc51f3a --- /dev/null +++ b/src/main/java/bdv/viewer/render/awt/BufferedImageRenderResult.java @@ -0,0 +1,101 @@ +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/test/java/bdv/IntervalPaintingExample.java b/src/test/java/bdv/IntervalPaintingExample.java new file mode 100644 index 0000000000000000000000000000000000000000..54cb871c475372dcf70271316bcb2ebea537c1a4 --- /dev/null +++ b/src/test/java/bdv/IntervalPaintingExample.java @@ -0,0 +1,147 @@ +/*- + * #%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 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/util/MovingAverageTest.java b/src/test/java/bdv/util/MovingAverageTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5a2f851b05a7119c4ab9b82f68fbfac953f1b7a7 --- /dev/null +++ b/src/test/java/bdv/util/MovingAverageTest.java @@ -0,0 +1,36 @@ +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..ed2f632f70236cb213496c61a18337472f28da63 --- /dev/null +++ b/src/test/java/bdv/util/TripleBufferDemo.java @@ -0,0 +1,83 @@ +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..a2be68350877b4095478bdd490787e7ecc745d52 --- /dev/null +++ b/src/test/java/bdv/util/TripleBufferTest.java @@ -0,0 +1,74 @@ +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; + } + } +}