diff --git a/pom.xml b/pom.xml index d17d3aa2e..ed9a6fece 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.scijava pom-scijava - 37.0.0 + 38.0.1 org.janelia.saalfeldlab @@ -29,7 +29,6 @@ - 21 org.janelia.saalfeldlab.paintera.Paintera GNU General Public License v2.0 @@ -41,6 +40,8 @@ org.janelia.saalfeldlab.paintera + + 21 1.9.24 true @@ -51,14 +52,13 @@ true ${javadoc.skip} - 3.0.7 1.4.12 2.0.0 22.0.1 - 1.4.2 + 2.0.0-SNAPSHOT 4.0.16-alpha 1.4.1 @@ -74,14 +74,12 @@ - - 3.2.0 - 2.2.0 - 4.1.0 - 4.2.0 - 1.3.4 - 7.0.0 - 1.6.0 + + 3.3.0 + 4.2.1 + 4.1.1 + 1.3.5 + 0.14.0 3.13.0 @@ -319,17 +317,6 @@ commons-io - - io.reactivex.rxjava2 - rxjavafx - 2.2.2 - - - io.reactivex.rxjava2 - rxjava - 2.1.6 - - org.scijava @@ -351,7 +338,7 @@ - org.codehaus.groovy + org.apache.groovy groovy ${groovy.version} @@ -568,7 +555,7 @@ hanslovskyp@janelia.hhmi.org HHMI Janelia - http://janelia.org/ + https://janelia.org/ founder lead diff --git a/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/scalebar/ScaleBarOverlayRenderer.java b/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/scalebar/ScaleBarOverlayRenderer.java index b6b5e4a95..379a25525 100644 --- a/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/scalebar/ScaleBarOverlayRenderer.java +++ b/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/scalebar/ScaleBarOverlayRenderer.java @@ -1,12 +1,12 @@ package org.janelia.saalfeldlab.bdv.fx.viewer.scalebar; -import org.janelia.saalfeldlab.bdv.fx.viewer.render.OverlayRendererGeneric; import bdv.viewer.TransformListener; import javafx.geometry.Bounds; import javafx.scene.canvas.GraphicsContext; import javafx.scene.text.Text; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.util.LinAlgHelpers; +import org.janelia.saalfeldlab.bdv.fx.viewer.render.OverlayRendererGeneric; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,8 +25,6 @@ public class ScaleBarOverlayRenderer implements OverlayRendererGeneric l.accept(this)); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/ValueDisplayListener.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/ValueDisplayListener.java index 69cd44c0d..cdb3c5ca2 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/ValueDisplayListener.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/ValueDisplayListener.java @@ -1,18 +1,19 @@ package org.janelia.saalfeldlab.paintera.control.navigation; -import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX; -import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerState; import bdv.viewer.Interpolation; import bdv.viewer.Source; import bdv.viewer.TransformListener; +import javafx.application.Platform; import javafx.beans.value.ObservableValue; -import javafx.concurrent.Task; import javafx.event.EventHandler; import javafx.scene.input.MouseEvent; +import kotlinx.coroutines.Deferred; import net.imglib2.RealRandomAccess; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.realtransform.RealViews; import net.imglib2.view.composite.Composite; +import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX; +import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerState; import org.janelia.saalfeldlab.fx.Tasks; import org.janelia.saalfeldlab.paintera.data.ChannelDataSource; import org.janelia.saalfeldlab.paintera.data.DataSource; @@ -95,7 +96,7 @@ private static D getVal(final RealRandomAccess access, final ViewerPanelF return access.get(); } - private final Map, Task> taskMap = new HashMap<>(); + private final Map, Deferred> taskMap = new HashMap<>(); private final ExecutorService executor = Executors.newSingleThreadExecutor(new NamedThreadFactory("value-display-listener", true, 2)); @@ -103,9 +104,9 @@ private void getInfo() { final Optional> optionalSource = Optional.ofNullable(currentSource.getValue()); if (optionalSource.isPresent() && optionalSource.get() instanceof DataSource) { - final DataSource source = (DataSource) optionalSource.get(); + final DataSource source = (DataSource)optionalSource.get(); - final var taskObj = Tasks.createTask(t -> { + final var job = Tasks.createTask(() -> { final ViewerState state = viewer.getState(); final Interpolation interpolation = this.interpolation.apply(source); final AffineTransform3D screenScaleTransform = new AffineTransform3D(); @@ -123,16 +124,17 @@ private void getInfo() { ).realRandomAccess(); final var val = getVal(x, y, access, viewer); return stringConverterFromSource(source).apply(val); - }).onSuccess((event, t) -> { - /* submit the value if the task is completed; remove from the map*/ - submitValue.accept(t.getValue()); + }).onSuccess(result -> { + Platform.runLater(() -> submitValue.accept(result)); taskMap.remove(source); }); + + /* If we are creating a task for a source which has a running task, cancel the old task after removing. */ - Optional.ofNullable(taskMap.put(source, taskObj)).ifPresent(Task::cancel); + Optional.ofNullable(taskMap.put(source, job)).ifPresent(it -> it.cancel(null)); - taskObj.submit(executor); + job.start(); } } @@ -140,11 +142,11 @@ private void getInfo() { private static Function stringConverterFromSource(final DataSource source) { if (source instanceof ChannelDataSource) { - final long numChannels = ((ChannelDataSource) source).numChannels(); + final long numChannels = ((ChannelDataSource)source).numChannels(); // Cast not actually redundant //noinspection unchecked,RedundantCast - return (Function) (Function, String>) comp -> { + return (Function)(Function, String>)comp -> { StringBuilder sb = new StringBuilder("("); if (numChannels > 0) sb.append(comp.get(0).toString()); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java deleted file mode 100644 index 8d402ce86..000000000 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.java +++ /dev/null @@ -1,462 +0,0 @@ -package org.janelia.saalfeldlab.paintera.control.paint; - -import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX; -import gnu.trove.list.TLongList; -import gnu.trove.list.array.TLongArrayList; -import gnu.trove.set.hash.TLongHashSet; -import javafx.animation.AnimationTimer; -import javafx.beans.value.ObservableValue; -import net.imglib2.Cursor; -import net.imglib2.FinalInterval; -import net.imglib2.Interval; -import net.imglib2.Localizable; -import net.imglib2.Point; -import net.imglib2.RandomAccess; -import net.imglib2.RandomAccessible; -import net.imglib2.RandomAccessibleInterval; -import net.imglib2.RealInterval; -import net.imglib2.RealLocalizable; -import net.imglib2.RealPoint; -import net.imglib2.RealPositionable; -import net.imglib2.algorithm.neighborhood.DiamondShape; -import net.imglib2.algorithm.neighborhood.Neighborhood; -import net.imglib2.algorithm.neighborhood.Shape; -import net.imglib2.realtransform.AffineTransform3D; -import net.imglib2.type.label.Label; -import net.imglib2.type.label.LabelMultisetType; -import net.imglib2.type.numeric.IntegerType; -import net.imglib2.type.numeric.integer.UnsignedLongType; -import org.janelia.saalfeldlab.net.imglib2.util.AccessBoxRandomAccessible; -import net.imglib2.util.Intervals; -import net.imglib2.util.Pair; -import net.imglib2.util.Util; -import net.imglib2.view.Views; -import org.janelia.saalfeldlab.fx.Tasks; -import org.janelia.saalfeldlab.fx.UtilityTask; -import org.janelia.saalfeldlab.paintera.Paintera; -import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment; -import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo; -import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; -import org.janelia.saalfeldlab.paintera.data.mask.SourceMask; -import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse; -import org.janelia.saalfeldlab.util.NamedThreadFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.invoke.MethodHandles; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.BiPredicate; -import java.util.function.BooleanSupplier; -import java.util.function.Consumer; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -public class FloodFill> { - - private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - - private static ExecutorService floodFillExector = newFloodFillExecutor(); - - private static ExecutorService newFloodFillExecutor() { - return Executors.newFixedThreadPool(Math.min(Runtime.getRuntime().availableProcessors() - 1, 1), new NamedThreadFactory("flood-fill-3d", true, 8)); - } - - private final ObservableValue activeViewerProperty; - - private final MaskedSource source; - - private final FragmentSegmentAssignment assignment; - - private final Consumer requestRepaint; - - private final BooleanSupplier isVisible; - - public FloodFill( - final ObservableValue activeViewerProperty, - final MaskedSource source, - final FragmentSegmentAssignment assignment, - final Consumer requestRepaint, - final BooleanSupplier isVisible) { - - super(); - Objects.requireNonNull(activeViewerProperty); - Objects.requireNonNull(source); - Objects.requireNonNull(assignment); - Objects.requireNonNull(requestRepaint); - Objects.requireNonNull(isVisible); - - this.activeViewerProperty = activeViewerProperty; - this.source = source; - this.assignment = assignment; - this.requestRepaint = requestRepaint; - this.isVisible = isVisible; - } - - public UtilityTask fillAt(final double x, final double y, final Supplier fillSupplier) { - - final Long fill = fillSupplier.get(); - if (fill == null) { - LOG.info("Received invalid label {} -- will not fill.", fill); - return null; - } - return fillAt(x, y, fill); - } - - private UtilityTask fillAt(final double x, final double y, final long fill) { - - // TODO should this check happen outside? - if (!isVisible.getAsBoolean()) { - LOG.info("Selected source is not visible -- will not fill"); - return null; - } - - final int level = 0; - final AffineTransform3D labelTransform = new AffineTransform3D(); - // TODO What to do for time series? - final int time = 0; - source.getSourceTransform(time, level, labelTransform); - - final RealPoint realSourceSeed = viewerToSourceCoordinates(x, y, activeViewerProperty.getValue(), labelTransform); - final Point sourceSeed = new Point(realSourceSeed.numDimensions()); - for (int d = 0; d < sourceSeed.numDimensions(); ++d) { - sourceSeed.setPosition(Math.round(realSourceSeed.getDoublePosition(d)), d); - } - - LOG.debug("Filling source {} with label {} at {}", source, fill, sourceSeed); - try { - return fill(time, level, fill, sourceSeed, assignment); - } catch (final MaskInUse e) { - LOG.info(e.getMessage()); - return null; - } - - } - - private static RealPoint viewerToSourceCoordinates( - final double x, - final double y, - final ViewerPanelFX viewer, - final AffineTransform3D labelTransform) { - - return viewerToSourceCoordinates(x, y, new RealPoint(labelTransform.numDimensions()), viewer, labelTransform); - } - - private static

P viewerToSourceCoordinates( - final double x, - final double y, - final P location, - final ViewerPanelFX viewer, - final AffineTransform3D labelTransform) { - - location.setPosition(x, 0); - location.setPosition(y, 1); - location.setPosition(0, 2); - - viewer.displayToGlobalCoordinates(location); - labelTransform.applyInverse(location, location); - - return location; - } - - private UtilityTask fill( - final int time, - final int level, - final long fill, - final Localizable seed, - final FragmentSegmentAssignment assignment) throws MaskInUse { - - final RandomAccessibleInterval data = source.getDataSource(time, level); - final RandomAccess dataAccess = data.randomAccess(); - dataAccess.setPosition(seed); - final T seedValue = dataAccess.get(); - final long seedLabel = assignment != null ? assignment.getSegment(seedValue.getIntegerLong()) : seedValue.getIntegerLong(); - if (!Label.regular(seedLabel)) { - LOG.info("Trying to fill at irregular label: {} ({})", seedLabel, new Point(seed)); - return null; - } - - final MaskInfo maskInfo = new MaskInfo( - time, - level - ); - final SourceMask mask = source.generateMask(maskInfo, MaskedSource.VALID_LABEL_CHECK); - final AffineTransform3D globalToSource = source.getSourceTransformForMask(maskInfo).inverse(); - - final List visibleSourceIntervals = Paintera.getPaintera().getBaseView().orthogonalViews().views().stream() - .filter(it -> it.isVisible() && it.getWidth() > 0.0 && it.getHeight() > 0.0) - .map(ViewerMask::getGlobalViewerInterval) - .map(globalToSource::estimateBounds) - .map(Intervals::smallestContainingInterval) - .collect(Collectors.toList()); - - final AtomicBoolean triggerRefresh = new AtomicBoolean(false); - final AccessBoxRandomAccessible accessTracker = new AccessBoxRandomAccessible<>(Views.extendValue(mask.getRai(), new UnsignedLongType(1))) { - - final Point position = new Point(sourceAccess.numDimensions()); - - @Override - public UnsignedLongType get() { - if (Thread.currentThread().isInterrupted()) - throw new RuntimeException("Flood Fill Interrupted"); - synchronized (this) { - updateAccessBox(); - } - sourceAccess.localize(position); - if (!triggerRefresh.get()) { - for (RealInterval interval : visibleSourceIntervals) { - if (Intervals.contains(interval, position)) { - triggerRefresh.set(true); - break; - } - } - } - return sourceAccess.get(); - } - }; - - final UtilityTask floodFillTask = Tasks.createTask(task -> { - if (seedValue instanceof LabelMultisetType) { - fillMultisetType((RandomAccessibleInterval) data, accessTracker, seed, seedLabel, fill, assignment); - } else { - fillPrimitiveType(data, accessTracker, seed, seedLabel, fill, assignment); - } - } - ).onCancelled((state, task) -> { - try { - source.resetMasks(); - } catch (final MaskInUse e) { - e.printStackTrace(); - } - } - ).onFailed((event, task) -> { - if (!Thread.currentThread().isInterrupted() && task.getException() != null && !(task.getException() instanceof CancellationException)) { - throw new RuntimeException(task.getException()); - } - } - ).onSuccess((state, task) -> { - LOG.debug(Thread.currentThread().isInterrupted() ? "FloodFill has been interrupted" : "FloodFill has been completed"); - - final Interval interval = accessTracker.createAccessInterval(); - LOG.debug( - "Applying mask for interval {} {}", - Arrays.toString(Intervals.minAsLongArray(interval)), - Arrays.toString(Intervals.maxAsLongArray(interval)) - ); - source.applyMask(mask, interval, MaskedSource.VALID_LABEL_CHECK); - } - ); - - final var refreshAnimation = new AnimationTimer() { - - final static long delay = 2_000_000_000; // 2 second delay before refreshes start - final static long REFRESH_RATE = 1_000_000_000; - final long start = System.nanoTime(); - long before = start; - - @Override - public void handle(long now) { - if (now - start < delay || now - before < REFRESH_RATE) return; - if (!floodFillTask.isCancelled() && triggerRefresh.get()) { - requestRepaint.accept(accessTracker.createAccessInterval()); - before = now; - triggerRefresh.set(false); - } - } - }; - - - floodFillTask.onEnd(task -> { - refreshAnimation.stop(); - requestRepaint.accept(accessTracker.createAccessInterval()); - }); - - if (floodFillExector.isShutdown()) { - floodFillExector = newFloodFillExecutor(); - } - refreshAnimation.start(); - floodFillTask.submit(floodFillExector); - return floodFillTask; - } - - private static void fillMultisetType( - final RandomAccessibleInterval input, - final RandomAccessible output, - final Localizable seed, - final long seedLabel, - final long fillLabel, - final FragmentSegmentAssignment assignment) { - - net.imglib2.algorithm.fill.FloodFill.fill( - Views.extendValue(input, new LabelMultisetType()), - output, - seed, - new UnsignedLongType(fillLabel), - new DiamondShape(1), - makePredicate(seedLabel, assignment) - ); - } - - private static > void fillPrimitiveType( - final RandomAccessibleInterval input, - final RandomAccessible output, - final Localizable seed, - final long seedLabel, - final long fillLabel, - final FragmentSegmentAssignment assignment) { - - final T extension = Util.getTypeFromInterval(input).createVariable(); - extension.setInteger(Label.OUTSIDE); - - net.imglib2.algorithm.fill.FloodFill.fill( - Views.extendValue(input, extension), - output, - seed, - new UnsignedLongType(fillLabel), - new DiamondShape(1), - makePredicate(seedLabel, assignment) - ); - } - - private static > BiPredicate makePredicate(final long seedLabel, final FragmentSegmentAssignment assignment) { - - final Long singleFragment; - final TLongHashSet seedFragments; - if (assignment != null) { - seedFragments = assignment.getFragments(seedLabel); - singleFragment = seedFragments.size() == 1 ? seedFragments.toArray()[0] : null; - } else { - singleFragment = seedLabel; - seedFragments = null; - } - - return (sourceVal, targetVal) -> { - if (Thread.currentThread().isInterrupted()) return false; - /* true if sourceFragment is a seedFragment */ - final long sourceFragment = sourceVal.getIntegerLong(); - final var shouldFill = singleFragment != null ? singleFragment == sourceFragment : seedFragments.contains(sourceFragment); - /* Most target vals are typically invalid, so this is rarely not passed; The sourceMatch filter is likely to - * shortcircuit more often */ - return shouldFill && targetVal.getInteger() == Label.INVALID; - }; - - } - - public static class RunAll implements Runnable { - - private final List runnables; - - public RunAll(final Runnable... runnables) { - - this(Arrays.asList(runnables)); - } - - public RunAll(final Collection runnables) { - - super(); - this.runnables = new ArrayList<>(runnables); - } - - @Override - public void run() { - - this.runnables.forEach(Runnable::run); - } - - } - - /** - * Iterative n-dimensional flood fill for arbitrary neighborhoods: Starting - * at seed location, write fillLabel into target at current location and - * continue for each pixel in neighborhood defined by shape if neighborhood - * pixel is in the same connected component and fillLabel has not been - * written into that location yet. - * - * @param source input - * @param target {@link RandomAccessible} to be written into. May be the same - * as input. - * @param seed Start flood fill at this location. - * @param shape Defines neighborhood that is considered for connected - * components, e.g. - * {@link net.imglib2.algorithm.neighborhood.DiamondShape} - * @param filter Returns true if pixel has not been visited yet and should be - * written into. Returns false if target pixel has been visited - * or source pixel is not part of the same connected component. - * @param writer Defines how fill label is written into target at current - * location. - * @param input pixel type - * @param fill label type - */ - public static Interval trackedFill( - final RandomAccessible source, - final RandomAccessible target, - final Localizable seed, - final Shape shape, - final BiPredicate filter, - final Consumer writer) { - - Interval interval = null; - final int n = source.numDimensions(); - - final RandomAccessible> paired = Views.pair(source, target); - - TLongList coordinates = new TLongArrayList(); - for (int d = 0; d < n; ++d) { - coordinates.add(seed.getLongPosition(d)); - } - - final int cleanupThreshold = n * (int) 1e5; - - final RandomAccessible>> neighborhood = shape.neighborhoodsRandomAccessible(paired); - final RandomAccess>> neighborhoodAccess = neighborhood.randomAccess(); - - final RandomAccess targetAccess = target.randomAccess(); - targetAccess.setPosition(seed); - writer.accept(targetAccess.get()); - - for (int i = 0; i < coordinates.size(); i += n) { - for (int d = 0; d < n; ++d) { - neighborhoodAccess.setPosition(coordinates.get(i + d), d); - } - - final Cursor> neighborhoodCursor = neighborhoodAccess.get().cursor(); - - while (neighborhoodCursor.hasNext()) { - final Pair p = neighborhoodCursor.next(); - if (filter.test(p.getA(), p.getB())) { - writer.accept(p.getB()); - if (interval == null) { - interval = new FinalInterval(neighborhoodCursor.positionAsLongArray(), neighborhoodCursor.positionAsLongArray()); - } else { - if (!Intervals.contains(interval, neighborhoodCursor.positionAsPoint())) { - interval = Intervals.union(interval, - new FinalInterval(neighborhoodCursor.positionAsLongArray(), neighborhoodCursor.positionAsLongArray())); - } - } - for (int d = 0; d < n; ++d) { - coordinates.add(neighborhoodCursor.getLongPosition(d)); - } - } - } - - if (i > cleanupThreshold) { - // TODO should it start from i + n? - coordinates = coordinates.subList(i, coordinates.size()); - i = 0; - } - - } - return interval; - - } - -} - diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java deleted file mode 100644 index 146af2d1a..000000000 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.java +++ /dev/null @@ -1,569 +0,0 @@ -package org.janelia.saalfeldlab.paintera.control.paint; - -import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX; -import javafx.animation.AnimationTimer; -import javafx.beans.property.DoubleProperty; -import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.beans.property.SimpleDoubleProperty; -import javafx.beans.value.ObservableValue; -import net.imglib2.FinalInterval; -import net.imglib2.Interval; -import net.imglib2.Point; -import net.imglib2.RandomAccess; -import net.imglib2.RandomAccessible; -import net.imglib2.RandomAccessibleInterval; -import net.imglib2.RealPoint; -import net.imglib2.algorithm.fill.FloodFill; -import net.imglib2.algorithm.neighborhood.DiamondShape; -import net.imglib2.converter.Converters; -import net.imglib2.realtransform.AffineTransform3D; -import net.imglib2.type.label.Label; -import net.imglib2.type.logic.BoolType; -import net.imglib2.type.numeric.IntegerType; -import net.imglib2.type.numeric.integer.UnsignedLongType; -import org.janelia.saalfeldlab.net.imglib2.util.AccessBoxRandomAccessibleOnGet; -import net.imglib2.util.Intervals; -import net.imglib2.view.MixedTransformView; -import net.imglib2.view.Views; -import org.janelia.saalfeldlab.fx.Tasks; -import org.janelia.saalfeldlab.fx.UtilityTask; -import org.janelia.saalfeldlab.fx.ui.Exceptions; -import org.janelia.saalfeldlab.paintera.Paintera; -import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment; -import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo; -import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; -import org.janelia.saalfeldlab.paintera.data.mask.SourceMask; -import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse; -import org.janelia.saalfeldlab.util.NamedThreadFactory; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.invoke.MethodHandles; -import java.util.Arrays; -import java.util.Objects; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.BooleanSupplier; -import java.util.function.Predicate; - -public class FloodFill2D> { - - private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - private static final Predicate FOREGROUND_CHECK = Label::isForeground; - - private static ExecutorService floodFillExector = newFloodFillExecutor(); - - private static ExecutorService newFloodFillExecutor() { - return Executors.newFixedThreadPool(Math.min(Runtime.getRuntime().availableProcessors() - 1, 1), new NamedThreadFactory("flood-fill-2d", true, 8)); - } - - private final ObservableValue activeViewerProperty; - - private final MaskedSource source; - private final BooleanSupplier isVisible; - - private final SimpleDoubleProperty fillDepth = new SimpleDoubleProperty(2.0); - - - private ViewerMask viewerMask; - - private final ReadOnlyObjectWrapper maskIntervalProperty = new ReadOnlyObjectWrapper<>(null); - - public FloodFill2D( - final ObservableValue activeViewerProperty, - final MaskedSource source, - final BooleanSupplier isVisible) { - - super(); - Objects.requireNonNull(activeViewerProperty); - Objects.requireNonNull(source); - Objects.requireNonNull(isVisible); - - this.activeViewerProperty = activeViewerProperty; - this.source = source; - this.isVisible = isVisible; - } - - public void provideMask(@NotNull ViewerMask mask) { - - this.viewerMask = mask; - } - - public ViewerMask getMask() { - return viewerMask; - } - - public void release() { - - this.viewerMask = null; - this.maskIntervalProperty.set(null); - } - - public ReadOnlyObjectProperty getMaskIntervalProperty() { - - return this.maskIntervalProperty.getReadOnlyProperty(); - } - - public Interval getMaskInterval() { - - return this.maskIntervalProperty.get(); - } - - public @Nullable UtilityTask fillViewerAt(final double viewerSeedX, final double viewerSeedY, Long fill, FragmentSegmentAssignment assignment) { - - if (fill == null) { - LOG.info("Received invalid label {} -- will not fill.", fill); - return null; - } - - if (!isVisible.getAsBoolean()) { - LOG.info("Selected source is not visible -- will not fill"); - return null; - } - - final ViewerPanelFX viewer = activeViewerProperty.getValue(); - - final ViewerMask mask; - if (this.viewerMask == null) { - final int level = viewer.getState().getBestMipMapLevel(); - final int time = viewer.getState().getTimepoint(); - final MaskInfo maskInfo = new MaskInfo(time, level); - mask = ViewerMask.createViewerMask(source, maskInfo, viewer, this.fillDepth.get()); - } else { - mask = viewerMask; - } - final var maskPos = mask.displayPointToMask(viewerSeedX, viewerSeedY, true); - final var filter = getBackgorundLabelMaskForAssignment(maskPos, mask, assignment, fill); - if (filter == null) - return null; - final UtilityTask floodFillTask = fillMaskAt(maskPos, mask, fill, filter); - if (this.viewerMask == null) { - floodFillTask.onCancelled(true, (state, task) -> { - try { - mask.getSource().resetMasks(); - mask.requestRepaint(); - } catch (final MaskInUse e) { - e.printStackTrace(); - } - }); - } - floodFillTask.submit(floodFillExector); - return floodFillTask; - } - - public UtilityTask fillViewerAt(final double viewerSeedX, final double viewerSeedY, Long fill, RandomAccessibleInterval filter) { - - if (fill == null) { - LOG.info("Received invalid label {} -- will not fill.", fill); - return null; - } - - if (!isVisible.getAsBoolean()) { - LOG.info("Selected source is not visible -- will not fill"); - return null; - } - - final ViewerPanelFX viewer = activeViewerProperty.getValue(); - - final ViewerMask mask; - if (this.viewerMask == null) { - final int level = viewer.getState().getBestMipMapLevel(); - final int time = viewer.getState().getTimepoint(); - final MaskInfo maskInfo = new MaskInfo(time, level); - mask = ViewerMask.createViewerMask(source, maskInfo, viewer, this.fillDepth.get()); - } else { - mask = viewerMask; - } - final var maskPos = mask.displayPointToMask(viewerSeedX, viewerSeedY, true); - final UtilityTask floodFillTask = fillMaskAt(maskPos, mask, fill, filter); - if (this.viewerMask == null) { - floodFillTask.onCancelled(true, (state, task) -> { - try { - mask.getSource().resetMasks(); - mask.requestRepaint(); - } catch (final MaskInUse e) { - e.printStackTrace(); - } - }); - } - floodFillTask.submit(floodFillExector); - return floodFillTask; - } - - - @NotNull - private UtilityTask fillMaskAt(Point maskPos, ViewerMask mask, Long fill, RandomAccessibleInterval filter) { - - final Interval screenInterval = mask.getScreenInterval(); - - final AtomicBoolean triggerRefresh = new AtomicBoolean(false); - final RandomAccessibleInterval writableViewerImg = mask.getViewerImg().getWritableSource(); - final var sourceAccessTracker = new AccessBoxRandomAccessibleOnGet<>(Views.extendValue(writableViewerImg, new UnsignedLongType(fill))) { - final Point position = new Point(sourceAccess.numDimensions()); - - @Override - public UnsignedLongType get() { - if (Thread.currentThread().isInterrupted()) - throw new RuntimeException("Flood Fill Interrupted"); - synchronized (this) { - updateAccessBox(); - } - sourceAccess.localize(position); - if (!triggerRefresh.get() && Intervals.contains(screenInterval, position)) { - triggerRefresh.set(true); - } - return sourceAccess.get(); - } - }; - sourceAccessTracker.initAccessBox(); - - - final var floodFillTask = createViewerFloodFillTask( - maskPos, - mask, - filter, - sourceAccessTracker, - fill - ); - - final var refreshAnimation = new AnimationTimer() { - - final static long delay = 2_000_000_000; // 2 second delay before refreshes start - final static long REFRESH_RATE = 1_000_000_000; - final long start = System.nanoTime(); - long before = start; - - @Override - public void handle(long now) { - if (now - start < delay || now - before < REFRESH_RATE) return; - if (!floodFillTask.isCancelled()) { - if (triggerRefresh.get()) { - mask.requestRepaint(sourceAccessTracker.createAccessInterval()); - before = now; - triggerRefresh.set(false); - } - } - } - }; - - - if (floodFillExector.isShutdown()) { - floodFillExector = newFloodFillExecutor(); - } - - floodFillTask.onEnd((task) -> { - refreshAnimation.stop(); - /*manually trigger repaint after stop to ensure full interval has been repainted*/ - mask.requestRepaint(sourceAccessTracker.createAccessInterval()); - }); - floodFillTask.onSuccess((state, task) -> { - maskIntervalProperty.set(sourceAccessTracker.createAccessInterval()); - }); - - refreshAnimation.start(); - return floodFillTask; - } - - public static UtilityTask createViewerFloodFillTask( - Point maskPos, - ViewerMask mask, - RandomAccessibleInterval floodFillFilter, - RandomAccessible target, - long fill) { - - return Tasks.createTask(task -> { - - if (floodFillFilter == null) { - try { - mask.getSource().resetMasks(true); - } catch (MaskInUse e) { - Exceptions.alert(e, Paintera.getPaintera().getBaseView().getNode().getScene().getWindow()); - } - } else { - fillAt(maskPos, target, floodFillFilter, fill); - } - }); - } - - /** - * Flood-fills the given mask starting at the specified 2D location in the viewer. - * Returns the affected interval in source coordinates. - * - * @param x - * @param y - * @param viewer - * @param mask - * @param source - * @param assignment - * @param fillValue - * @param fillDepth - * @return affected interval - */ - public static > Interval fillMaskAt( - final double x, - final double y, - final ViewerPanelFX viewer, - final SourceMask mask, - final MaskedSource source, - final FragmentSegmentAssignment assignment, - final long fillValue, - final double fillDepth) { - - final AffineTransform3D labelTransform = source.getSourceTransformForMask(mask.getInfo()); - final RandomAccessibleInterval background = source.getDataSourceForMask(mask.getInfo()); - - final RandomAccess access = background.randomAccess(); - final RealPoint posInLabelSpace = new RealPoint(access.numDimensions()); - viewer.displayToSourceCoordinates(x, y, labelTransform, posInLabelSpace); - for (int d = 0; d < access.numDimensions(); ++d) { - access.setPosition(Math.round(posInLabelSpace.getDoublePosition(d)), d); - } - final long seedLabel = assignment != null ? assignment.getSegment(access.get().getIntegerLong()) : access.get().getIntegerLong(); - LOG.debug("Got seed label {}", seedLabel); - final RandomAccessibleInterval relevantBackground = Converters.convert( - background, - (src, tgt) -> tgt.set((assignment != null ? assignment.getSegment(src.getIntegerLong()) : src.getIntegerLong()) == seedLabel), - new BoolType() - ); - - return fillMaskAt(x, y, viewer, mask, relevantBackground, labelTransform, fillValue, fillDepth); - } - - /** - * Flood-fills the given mask starting at the specified 2D location in the viewer - * based on the given boolean filter. - * Returns the affected interval in source coordinates. - * - * @param x - * @param y - * @param viewer - * @param mask - * @param filter - * @param labelTransform - * @param fillValue - * @param fillDepth - * @return affected interval - */ - public static > Interval fillMaskAt( - final double x, - final double y, - final ViewerPanelFX viewer, - final SourceMask mask, - final RandomAccessibleInterval filter, - final AffineTransform3D labelTransform, - final long fillValue, - final double fillDepth) { - - final AffineTransform3D viewerTransform = new AffineTransform3D(); - viewer.getState().getViewerTransform(viewerTransform); - final AffineTransform3D labelToViewerTransform = viewerTransform.copy().concatenate(labelTransform); - - final RealPoint pos = new RealPoint(labelTransform.numDimensions()); - viewer.displayToSourceCoordinates(x, y, labelTransform, pos); - - final RandomAccessible extendedFilter = Views.extendValue(filter, new BoolType(false)); - - final int fillNormalAxisInLabelCoordinateSystem = PaintUtils.labelAxisCorrespondingToViewerAxis(labelTransform, viewerTransform, 2); - final AccessBoxRandomAccessibleOnGet accessTracker = new - AccessBoxRandomAccessibleOnGet<>( - Views.extendValue(mask.getRai(), new UnsignedLongType(fillValue))); - accessTracker.initAccessBox(); - - if (fillNormalAxisInLabelCoordinateSystem < 0) { - FloodFillTransformedPlane.fill( - labelToViewerTransform, - (fillDepth - 0.5) * PaintUtils.maximumVoxelDiagonalLengthPerDimension( - labelTransform, - viewerTransform - )[2], - extendedFilter.randomAccess(), - accessTracker.randomAccess(), - new RealPoint(x, y, 0), - fillValue - ); - } else { - LOG.debug( - "Flood filling axis aligned. Corressponding viewer axis={}", - fillNormalAxisInLabelCoordinateSystem - ); - final long slicePos = Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem)); - final long numSlices = Math.max((long) Math.ceil(fillDepth) - 1, 0); - if (numSlices == 0) { - // fill only within the given slice, run 2D flood-fill - final long[] seed2D = { - Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem == 0 ? 1 : 0)), - Math.round(pos.getDoublePosition(fillNormalAxisInLabelCoordinateSystem != 2 ? 2 : 1)) - }; - final MixedTransformView relevantBackgroundSlice = Views.hyperSlice( - extendedFilter, - fillNormalAxisInLabelCoordinateSystem, - slicePos - ); - final MixedTransformView relevantAccessTracker = Views.hyperSlice( - accessTracker, - fillNormalAxisInLabelCoordinateSystem, - slicePos - ); - FloodFill.fill( - relevantBackgroundSlice, - relevantAccessTracker, - new Point(seed2D), - new UnsignedLongType(fillValue), - new DiamondShape(1) - ); - } else { - // fill a range around the given slice, run 3D flood-fill restricted by this range - final long[] seed3D = new long[3]; - Arrays.setAll(seed3D, d -> Math.round(pos.getDoublePosition(d))); - - final long[] rangeMin = Intervals.minAsLongArray(filter); - final long[] rangeMax = Intervals.maxAsLongArray(filter); - rangeMin[fillNormalAxisInLabelCoordinateSystem] = slicePos - numSlices; - rangeMax[fillNormalAxisInLabelCoordinateSystem] = slicePos + numSlices; - final Interval range = new FinalInterval(rangeMin, rangeMax); - - final RandomAccessible extendedBackgroundRange = Views.extendValue( - Views.interval(extendedFilter, range), - new BoolType(false) - ); - FloodFill.fill( - extendedBackgroundRange, - accessTracker, - new Point(seed3D), - new UnsignedLongType(fillValue), - new DiamondShape(1) - ); - } - } - - return new FinalInterval(accessTracker.getMin(), accessTracker.getMax()); - } - - public DoubleProperty fillDepthProperty() { - - return this.fillDepth; - } - - @Nullable - public static RandomAccessibleInterval getBackgorundLabelMaskForAssignment(Point initialSeed, ViewerMask mask, - FragmentSegmentAssignment assignment, long fillValue) { - - final var backgroundViewerRai = ViewerMask.getSourceDataInInitialMaskSpace(mask); - - final var id = (long) backgroundViewerRai.getAt(initialSeed).getRealDouble(); - final long seedLabel = assignment != null ? assignment.getSegment(id) : id; - LOG.debug("Got seed label {}", seedLabel); - - if (seedLabel == fillValue) { - return null; - } - - return Converters.convert( - backgroundViewerRai, - (src, target) -> { - long segmentId = (long) src.getRealDouble(); - if (assignment != null) { - segmentId = assignment.getSegment(segmentId); - } - target.set(segmentId == seedLabel); - }, - new BoolType() - ); - } - - public static void fillViewerMaskAt( - final Point initialSeed, - final RandomAccessible source, - final RandomAccessibleInterval filter, - final long fillValue) { - - final RandomAccessible extendedFilter = Views.extendValue(filter, new BoolType(false)); - - - // fill only within the given slice, run 2D flood-fill - LOG.debug("Flood filling into viewer source "); - - final RandomAccessible backgroundSlice; - if (extendedFilter.numDimensions() == 3) { - backgroundSlice = Views.hyperSlice(extendedFilter, 2, 0); - } else { - backgroundSlice = extendedFilter; - } - final RandomAccessible viewerFillImg = Views.hyperSlice(source, 2, 0); - - FloodFill.fill( - backgroundSlice, - viewerFillImg, - initialSeed, - new UnsignedLongType(fillValue), - new DiamondShape(1) - ); - } - - public static void fillAt( - final Point initialSeed, - final RandomAccessible target, - final RandomAccessibleInterval filter, - final long fillValue) { - - final RandomAccessible extendedFilter = Views.extendValue(filter, new BoolType(false)); - - // fill only within the given slice, run 2D flood-fill - LOG.debug("Flood filling "); - - final RandomAccessible backgroundSlice; - if (extendedFilter.numDimensions() == 3) { - backgroundSlice = Views.hyperSlice(extendedFilter, 2, 0); - } else { - backgroundSlice = extendedFilter; - } - final RandomAccessible viewerFillImg = Views.hyperSlice(target, 2, 0); - - FloodFill.fill( - backgroundSlice, - viewerFillImg, - initialSeed, - new UnsignedLongType(fillValue), - new DiamondShape(1) - ); - } - - public static Interval fillViewerMaskAt( - final Point initialSeed, - RandomAccessibleInterval viewerImg, final RandomAccessibleInterval filter, - final long fillValue) { - - final RandomAccessible extendedFilter = Views.extendValue(filter, new BoolType(false)); - - final AccessBoxRandomAccessibleOnGet sourceAccessTracker = new - AccessBoxRandomAccessibleOnGet<>( - Views.extendValue(viewerImg, new UnsignedLongType(fillValue))); - sourceAccessTracker.initAccessBox(); - - // fill only within the given slice, run 2D flood-fill - LOG.debug("Flood filling into viewer mask "); - - final RandomAccessible backgroundSlice; - if (extendedFilter.numDimensions() == 3) { - backgroundSlice = Views.hyperSlice(extendedFilter, 2, 0); - } else { - backgroundSlice = extendedFilter; - } - final RandomAccessible viewerFillImg = Views.hyperSlice(sourceAccessTracker, 2, 0); - - FloodFill.fill( - backgroundSlice, - viewerFillImg, - initialSeed, - new UnsignedLongType(fillValue), - new DiamondShape(1) - ); - - return new FinalInterval(sourceAccessTracker.getMin(), sourceAccessTracker.getMax()); - } -} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java index 2466a1142..439f97340 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/data/mask/MaskedSource.java @@ -43,7 +43,6 @@ import mpicbg.spim.data.sequence.VoxelDimensions; import net.imglib2.FinalInterval; import net.imglib2.FinalRealInterval; -import org.janelia.saalfeldlab.net.imglib2.FinalRealRandomAccessibleRealInterval; import net.imglib2.Interval; import net.imglib2.RandomAccess; import net.imglib2.RandomAccessible; @@ -68,7 +67,6 @@ import net.imglib2.img.cell.CellGrid; import net.imglib2.interpolation.randomaccess.NearestNeighborInterpolatorFactory; import net.imglib2.loops.LoopBuilder; -import org.janelia.saalfeldlab.net.imglib2.outofbounds.RealOutOfBoundsConstantValueFactory; import net.imglib2.parallel.TaskExecutor; import net.imglib2.parallel.TaskExecutors; import net.imglib2.realtransform.AffineTransform3D; @@ -82,18 +80,20 @@ import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.integer.UnsignedLongType; import net.imglib2.type.volatiles.VolatileUnsignedLongType; -import org.janelia.saalfeldlab.net.imglib2.util.AccessedBlocksRandomAccessible; import net.imglib2.util.ConstantUtils; import net.imglib2.util.IntervalIndexer; import net.imglib2.util.Intervals; -import org.janelia.saalfeldlab.fx.UtilityTask; -import org.janelia.saalfeldlab.net.imglib2.view.BundleView; import net.imglib2.view.ExtendedRealRandomAccessibleRealInterval; import net.imglib2.view.IntervalView; -import org.janelia.saalfeldlab.net.imglib2.view.RealRandomAccessibleTriple; import net.imglib2.view.Views; import org.janelia.saalfeldlab.fx.Tasks; +import org.janelia.saalfeldlab.fx.UtilityTask; import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread; +import org.janelia.saalfeldlab.net.imglib2.FinalRealRandomAccessibleRealInterval; +import org.janelia.saalfeldlab.net.imglib2.outofbounds.RealOutOfBoundsConstantValueFactory; +import org.janelia.saalfeldlab.net.imglib2.util.AccessedBlocksRandomAccessible; +import org.janelia.saalfeldlab.net.imglib2.view.BundleView; +import org.janelia.saalfeldlab.net.imglib2.view.RealRandomAccessibleTriple; import org.janelia.saalfeldlab.paintera.data.DataSource; import org.janelia.saalfeldlab.paintera.data.mask.PickOne.PickAndConvert; import org.janelia.saalfeldlab.paintera.data.mask.exception.CannotClearCanvas; @@ -780,7 +780,8 @@ public void resetMasks(final boolean clearOldMask) throws MaskInUse { throw new MaskInUse("Cannot reset the mask."); var mask = getCurrentMask(); - if (mask != null && mask.shutdown != null) mask.shutdown.run(); + if (mask != null && mask.shutdown != null) + mask.shutdown.run(); setCurrentMask(null); this.isBusy.set(true); } @@ -828,7 +829,7 @@ public void persistCanvas(final boolean clearCanvas) throws CannotPersist { final ObservableList states = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); final Consumer nextState = (text) -> InvokeOnJavaFXApplicationThread.invoke(() -> states.add(text)); final Consumer updateState = (update) -> InvokeOnJavaFXApplicationThread.invoke(() -> states.set(states.size() - 1, update)); - persistCanvasTask(clearCanvas, progressBar.progressProperty(), nextState, updateState).submit(); + persistCanvasTask(clearCanvas, progressBar.progressProperty(), nextState, updateState); final BooleanBinding stillPersisting = Bindings.createBooleanBinding( () -> this.isPersisting() || progressBar.progressProperty().get() < 1.0, @@ -902,7 +903,7 @@ private synchronized UtilityTask persistCanvasTask( InvokeOnJavaFXApplicationThread.invoke(timeline::play); }; ChangeListener animateProgressBarListener = (obs, oldv, newv) -> animateProgressBar.accept(newv.doubleValue()); - return Tasks.createTask(task -> { + return Tasks.createTask(() -> { try { nextState.accept("Persisting painted labels..."); InvokeOnJavaFXApplicationThread.invoke(() -> @@ -934,19 +935,18 @@ private synchronized UtilityTask persistCanvasTask( } catch (UnableToPersistCanvas | UnableToUpdateLabelBlockLookup e) { throw new RuntimeException(e); } - return null; - }).onSuccess((e, t) -> { + }).onSuccess(result -> { synchronized (this) { nextState.accept("Successfully finished committing canvas."); } - }).onEnd(t -> { + }).onEnd((result, cause) -> { animateProgressBar.accept(1.0); this.isPersistingProperty.set(false); this.isBusy.set(false); - }).onFailed((e, t) -> { + }).onFailed(cause -> { synchronized (this) { - LOG.error("Unable to commit canvas", t.getException()); - nextState.accept("Unable to commit canvas: " + t.getException().getMessage()); + LOG.error("Unable to commit canvas", cause); + nextState.accept("Unable to commit canvas: " + cause.getMessage()); } }); } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java index d4fa5bfd0..682675009 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/LabelSourceStateIdSelectorHandler.java @@ -1,15 +1,15 @@ package org.janelia.saalfeldlab.paintera.state; -import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX; import com.google.common.util.concurrent.ThreadFactoryBuilder; -import javafx.concurrent.Task; import javafx.event.Event; import javafx.scene.Cursor; import javafx.scene.input.KeyCode; import javafx.scene.input.MouseButton; import kotlin.jvm.functions.Function1; +import kotlinx.coroutines.Job; import net.imglib2.type.label.Label; import net.imglib2.type.numeric.IntegerType; +import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX; import org.janelia.saalfeldlab.fx.Tasks; import org.janelia.saalfeldlab.fx.actions.ActionSet; import org.janelia.saalfeldlab.fx.event.KeyTracker; @@ -32,7 +32,6 @@ import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; import java.util.function.LongPredicate; import java.util.function.Supplier; @@ -61,9 +60,7 @@ public class LabelSourceStateIdSelectorHandler { private final Runnable refreshMeshes; - private Task selectAllTask; - - private Future selectAllFuture; + private Job selectAllTask; /** * @param source that contains the labels to select @@ -123,16 +120,13 @@ public List makeActionSets(KeyTracker keyTracker, Supplier selectAllTask == null); keyAction.onAction(keyEvent -> { - final var selectTask = Tasks.createTask(task -> { - Paintera.getPaintera().getBaseView().getNode().getScene().setCursor(Cursor.WAIT); - selectAllTask = task; - selector.selectAll(); - }) - .onEnd(task -> { - selectAllTask = null; - Paintera.getPaintera().getBaseView().getNode().getScene().setCursor(Cursor.DEFAULT); - }); - selectAllFuture = selectorService.submit(selectTask); + selectAllTask = Tasks.createTask(() -> { + Paintera.getPaintera().getBaseView().getNode().getScene().setCursor(Cursor.WAIT); + selector.selectAll(); + }).onEnd((result, error) -> { + selectAllTask = null; + Paintera.getPaintera().getBaseView().getNode().getScene().setCursor(Cursor.DEFAULT); + }); }); }); actionSet.addKeyAction(KEY_PRESSED, keyAction -> { @@ -142,16 +136,13 @@ public List makeActionSets(KeyTracker keyTracker, Supplier selectAllTask == null); keyAction.verify(event -> getActiveViewer.get() != null); keyAction.onAction(keyEvent -> { - final var selectTask = Tasks.createTask(task -> { - Paintera.getPaintera().getBaseView().getNode().getScene().setCursor(Cursor.WAIT); - selectAllTask = task; - selector.selectAllInCurrentView(getActiveViewer.get()); - }) - .onEnd(objectUtilityTask -> { - selectAllTask = null; - Paintera.getPaintera().getBaseView().getNode().getScene().setCursor(Cursor.DEFAULT); - }); - selectAllFuture = selectorService.submit(selectTask); + selectAllTask = Tasks.createTask(() -> { + Paintera.getPaintera().getBaseView().getNode().getScene().setCursor(Cursor.WAIT); + selector.selectAllInCurrentView(getActiveViewer.get()); + }).onEnd((result, error) -> { + selectAllTask = null; + Paintera.getPaintera().getBaseView().getNode().getScene().setCursor(Cursor.DEFAULT); + }); }); }); actionSet.addKeyAction(KEY_PRESSED, keyAction -> { @@ -160,8 +151,7 @@ public List makeActionSets(KeyTracker keyTracker, Supplier selectAllTask != null); keyAction.onAction(keyEvent -> { - selectAllTask.cancel(); - selectAllFuture.cancel(true); + selectAllTask.cancel(null); refreshMeshes.run(); selectedIds.deactivateAll(); }); @@ -186,9 +176,11 @@ public List makeActionSets(KeyTracker keyTracker, Supplier choices) { } private > T mapRootToContainerName(T choices) { + if (choices.containsKey("/")) { var node = choices.remove("/"); choices.put(getContainerName(), node); @@ -295,37 +297,33 @@ private void updateDatasetChoices(N5Reader newReader) { discoveryIsActive.set(true); invoke(this::resetDatasetChoices); // clean up whatever is currently shown }); - Tasks.>createTask( - thisTask -> { - /* Parse the container's metadata*/ - final ObservableMap validDatasetChoices = FXCollections.synchronizedObservableMap( - FXCollections.observableHashMap()); - final N5TreeNode metadataTree; - try { - metadataTree = N5Helpers.parseMetadata(newReader, discoveryIsActive).orElse(null); - } catch (Exception e) { - if (!discoveryIsActive.get()) { - /* if discovery was cancelled ,this is expected*/ - LOG.debug("Metadata Parsing was Canceled"); - thisTask.cancel(); - return null; - } - throw e; - } - Map validGroups = N5Helpers.validPainteraGroupMap(metadataTree); - invoke(() -> validDatasetChoices.putAll(validGroups)); - - if (metadataTree == null || metadataTree.getMetadata() == null) { - invoke(() -> this.activeN5Node.set(null)); - } - return validDatasetChoices; - }) - .onSuccess((event, task) -> { - datasetChoices.set(mapRootToContainerName(task.getValue())); + Tasks.createTask(() -> { + /* Parse the container's metadata*/ + final ObservableMap validDatasetChoices = FXCollections.synchronizedObservableMap( + FXCollections.observableHashMap()); + final N5TreeNode metadataTree; + try { + metadataTree = N5Helpers.parseMetadata(newReader, discoveryIsActive).orElse(null); + } catch (Exception e) { + if (!discoveryIsActive.get()) { + /* if discovery was cancelled ,this is expected*/ + final String message = "Metadata Parsing was Canceled"; + LOG.debug(message); + throw new CancellationException(message); + } + throw e; + } + Map validGroups = N5Helpers.validPainteraGroupMap(metadataTree); + invoke(() -> validDatasetChoices.putAll(validGroups)); + + if (metadataTree == null || metadataTree.getMetadata() == null) { + invoke(() -> this.activeN5Node.set(null)); + } + return validDatasetChoices; + }).onSuccess(result -> { + datasetChoices.set(mapRootToContainerName(result)); previousContainerChoices.put(getContainer(), Map.copyOf(datasetChoices.getValue())); - }) /* set and cache the choices on success*/ - .onEnd(task -> invoke(() -> discoveryIsActive.set(false))) /* clear the flag when done, regardless */ - .submit(); + }).onEnd((result, error) -> invoke(() -> discoveryIsActive.set(false))); } } @@ -521,6 +519,7 @@ private String getDatasetPath() { } private String getContainerName() { + var pathParts = URI.create(containerState.get().getUri().getPath()).getPath().split("/"); return pathParts[pathParts.length - 1]; } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/menu/n5/N5FactoryOpener.java b/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/menu/n5/N5FactoryOpener.java index 6767e9794..9b90c0cae 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/menu/n5/N5FactoryOpener.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/menu/n5/N5FactoryOpener.java @@ -197,27 +197,26 @@ private void selectionChanged(ObservableValue obs, String oldS return; } - Tasks.createTask( - task -> { - invoke(() -> this.isOpeningContainer.set(true)); - final var newContainerState = Optional.ofNullable(n5ContainerStateCache.get(newSelection)).orElseGet(() -> { - - var container = Paintera.getN5Factory().openReaderOrNull(newSelection); - if (container == null) return null; - if (container instanceof N5HDF5Reader) { - container.close(); - container = Paintera.getN5Factory().openWriterElseOpenReader(newSelection); - } - return new N5ContainerState(container); - }); - if (newContainerState == null) - return false; - - invoke(() -> containerState.set(newContainerState)); - n5ContainerStateCache.put(newSelection, newContainerState); - return true; - }) - .onEnd(task -> invoke(() -> this.isOpeningContainer.set(false))) - .submit(); + Tasks.createTask(() -> { + invoke(() -> this.isOpeningContainer.set(true)); + final var newContainerState = Optional.ofNullable(n5ContainerStateCache.get(newSelection)).orElseGet(() -> { + + var container = Paintera.getN5Factory().openReaderOrNull(newSelection); + if (container == null) + return null; + if (container instanceof N5HDF5Reader) { + container.close(); + container = Paintera.getN5Factory().openWriterElseOpenReader(newSelection); + } + return new N5ContainerState(container); + }); + if (newContainerState == null) + return false; + + invoke(() -> containerState.set(newContainerState)); + n5ContainerStateCache.put(newSelection, newContainerState); + return true; + }) + .onEnd((result, error) -> invoke(() -> this.isOpeningContainer.set(false))); } } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshProgressBar.java b/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshProgressBar.java index a2c8fdc6e..47dd12eb2 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshProgressBar.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/ui/source/mesh/MeshProgressBar.java @@ -1,13 +1,12 @@ package org.janelia.saalfeldlab.paintera.ui.source.mesh; -import io.reactivex.disposables.Disposable; -import io.reactivex.rxjavafx.observables.JavaFxObservable; -import io.reactivex.rxjavafx.schedulers.JavaFxScheduler; +import javafx.animation.AnimationTimer; +import javafx.css.PseudoClass; import javafx.scene.control.Tooltip; +import javafx.util.Subscription; import org.controlsfx.control.StatusBar; import org.janelia.saalfeldlab.paintera.meshes.ObservableMeshProgress; -import java.util.concurrent.TimeUnit; public class MeshProgressBar extends StatusBar { @@ -19,7 +18,7 @@ public class MeshProgressBar extends StatusBar { private ObservableMeshProgress meshProgress; - private Disposable disposable; + private AnimationTimer progressBarUpdater; public MeshProgressBar() { @@ -29,64 +28,80 @@ public MeshProgressBar() { public MeshProgressBar(final long updateIntervalMsec) { this.updateIntervalMsec = updateIntervalMsec; - setStyle("-fx-accent: green; "); setTooltip(statusToolTip); + + setCssProperties(); + } + + private void setCssProperties() { + + getStyleClass().add("mesh-status-bar"); + final PseudoClass complete = PseudoClass.getPseudoClass("complete"); + progressProperty().subscribe(progress -> pseudoClassStateChanged(complete, !(progress.doubleValue() < 1.0))); } public void bindTo(final ObservableMeshProgress meshProgress) { unbind(); this.meshProgress = meshProgress; - if (this.meshProgress != null) { - this.disposable = JavaFxObservable - .invalidationsOf(this.meshProgress) - .throttleLast(updateIntervalMsec, TimeUnit.MILLISECONDS) - .observeOn(JavaFxScheduler.platform()) - .subscribe(val -> { - final int numTasks = meshProgress.getNumTasks(); - final int numCompletedTasks = meshProgress.getNumCompletedTasks(); - - if (numTasks == 0) - setProgress(0.0); // hide progress bar when there is nothing to do - else if (numCompletedTasks <= 0) - setProgress(1e-7); // displays an empty progress bar - else if (numCompletedTasks >= numTasks) { - setStyle(ProgressStyle.FINISHED); - setProgress(1.0); - } else { - setStyle(ProgressStyle.IN_PROGRESS); - setProgress((double)numCompletedTasks / numTasks); - } - - statusToolTip.setText(numCompletedTasks + "/" + numTasks); - }); - } - } + if (this.meshProgress == null) return; - public void unbind() { + progressBarUpdater = createAnimationTimer(meshProgress); + progressBarUpdater.start(); - if (this.meshProgress != null) { - this.disposable.dispose(); - this.disposable = null; - this.meshProgress = null; - } } - private static class ProgressStyle { + private AnimationTimer createAnimationTimer(ObservableMeshProgress meshProgress) { + + return new AnimationTimer() { + + private Subscription subscription; + long lastUpdate = -1L; + boolean handleUpdate = false; - // color combination inspired by - // https://www.designwizard.com/blog/design-trends/colour-combination - // pacific coast (finished) and living coral - private static final String COLOR_FINISHED = "#5B84B1FF"; - private static final String COLOR_IN_PROGRESS = "#FC766AFF"; + @Override public void start() { - private static String getStyle(final String color) { + super.start(); + this.subscription = meshProgress.subscribe(() -> handleUpdate = true); + } - return String.format("-fx-accent: %s; ", color); - } + @Override public void stop() { - public static final String IN_PROGRESS = getStyle(COLOR_IN_PROGRESS); - public static final String FINISHED = getStyle(COLOR_FINISHED); + super.stop(); + if (subscription!= null) + subscription.unsubscribe(); + } + + @Override public void handle(long now) { + if (handleUpdate && now - lastUpdate > updateIntervalMsec) { + lastUpdate = now; + final int numTasks = meshProgress.getNumTasks(); + final int numCompletedTasks = meshProgress.getNumCompletedTasks(); + + if (numTasks == 0) + setProgress(0.0); // hide progress bar when there is nothing to do + else if (numCompletedTasks <= 0) + setProgress(1e-7); // displays an empty progress bar + else if (numCompletedTasks >= numTasks) { + setProgress(1.0); + } else { + setProgress((double)numCompletedTasks / numTasks); + } + + statusToolTip.setText(numCompletedTasks + "/" + numTasks); + } + } + }; } + public void unbind() { + + if (progressBarUpdater != null) + progressBarUpdater.stop(); + + if (meshProgress != null) + meshProgress = null; + + setProgress(1e-7); + } } diff --git a/src/main/java/org/janelia/saalfeldlab/util/n5/N5Data.java b/src/main/java/org/janelia/saalfeldlab/util/n5/N5Data.java index bf28a5278..5f106925f 100644 --- a/src/main/java/org/janelia/saalfeldlab/util/n5/N5Data.java +++ b/src/main/java/org/janelia/saalfeldlab/util/n5/N5Data.java @@ -794,10 +794,10 @@ public static void createEmptyLabelDataset( n5.createGroup(group); if (!ignoreExisiting && n5.getAttribute(group, N5Helpers.PAINTERA_DATA_KEY, JsonObject.class) != null) - throw new IOException(String.format("Group `%s' exists in container `%s' and is Paintera data set", group, container)); + throw new IOException(String.format("Group '%s' already exists in container '%s' and is a Paintera dataset", group, container)); if (!ignoreExisiting && n5.exists(uniqueLabelsGroup)) - throw new IOException(String.format("Unique labels group `%s' exists in container `%s' -- conflict likely.", uniqueLabelsGroup, container)); + throw new IOException(String.format("Unique labels group '%s' already exists in container '%s' -- conflict likely.", uniqueLabelsGroup, container)); n5.setAttribute(group, N5Helpers.PAINTERA_DATA_KEY, pd); n5.setAttribute(group, N5Helpers.MAX_ID_KEY, 0L); diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/Paintera.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/Paintera.kt index 7689e6b75..0930e37c9 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/Paintera.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/Paintera.kt @@ -319,24 +319,23 @@ class Paintera : Application() { SaalFxStyle.registerStylesheets(styleable) } + private val stylesheets : List = listOf( + "style/glyphs.css", + "style/toolbar.css", + "style/navigation.css", + "style/interpolation.css", + "style/sam.css", + "style/paint.css", + "style/raw-source.css", + "style/mesh-status-bar.css" + ) + private fun registerPainteraStylesheets(styleable: Scene) { - styleable.stylesheets.add("style/glyphs.css") - styleable.stylesheets.add("style/toolbar.css") - styleable.stylesheets.add("style/navigation.css") - styleable.stylesheets.add("style/interpolation.css") - styleable.stylesheets.add("style/sam.css") - styleable.stylesheets.add("style/paint.css") - styleable.stylesheets.add("style/raw-source.css") + styleable.stylesheets.addAll(stylesheets) } private fun registerPainteraStylesheets(styleable: Parent) { - styleable.stylesheets.add("style/glyphs.css") - styleable.stylesheets.add("style/toolbar.css") - styleable.stylesheets.add("style/navigation.css") - styleable.stylesheets.add("style/interpolation.css") - styleable.stylesheets.add("style/sam.css") - styleable.stylesheets.add("style/paint.css") - styleable.stylesheets.add("style/raw-source.css") + styleable.stylesheets.addAll(stylesheets) } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt index bd5b0c390..ed32c3b3d 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/cache/SamEmbeddingLoaderCache.kt @@ -3,7 +3,6 @@ package org.janelia.saalfeldlab.paintera.cache import ai.onnxruntime.OnnxTensor import ai.onnxruntime.OrtEnvironment import bdv.cache.SharedQueue -import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX import bdv.fx.viewer.render.BaseRenderUnit import bdv.fx.viewer.render.RenderUnitState import bdv.viewer.Interpolation @@ -25,7 +24,7 @@ import org.apache.http.entity.ContentType import org.apache.http.entity.mime.MultipartEntityBuilder import org.apache.http.impl.client.HttpClientBuilder import org.apache.http.util.EntityUtils -import org.janelia.saalfeldlab.fx.Tasks +import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX import org.janelia.saalfeldlab.fx.extensions.LazyForeignValue import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews.ViewerAndTransforms import org.janelia.saalfeldlab.paintera.PainteraBaseView @@ -44,6 +43,7 @@ import java.nio.file.Files import java.nio.file.Paths import java.util.concurrent.atomic.AtomicInteger import javax.imageio.ImageIO +import kotlin.coroutines.cancellation.CancellationException import kotlin.math.ceil import kotlin.math.max import kotlin.math.min @@ -130,8 +130,10 @@ object SamEmbeddingLoaderCache : AsyncCacheWithLoader() + @OptIn(ExperimentalCoroutinesApi::class) val createOrtSessionTask by LazyForeignValue({ properties.segmentAnythingConfig.modelLocation }) { modelLocation -> - Tasks.createTask { + + CoroutineScope(Dispatchers.IO).async { if (!SamEmbeddingLoaderCache::ortEnv.isInitialized) ortEnv = OrtEnvironment.getEnvironment() val modelArray = try { @@ -141,13 +143,12 @@ object SamEmbeddingLoaderCache : AsyncCacheWithLoader - if (prevTask.isDone) - prevTask.get().close() - prevTask.cancel() } + }.beforeValueChange { job -> + job?.invokeOnCompletion { + job.getCompleted().close() + } + job?.cancel(CancellationException("Ort Model Location Changed")) } internal fun RenderUnitState.calculateTargetSamScreenScaleFactor(): Double { @@ -328,8 +329,11 @@ object SamEmbeddingLoaderCache : AsyncCacheWithLoader Unit) : this(applyBookmark) { Paintera.whenPaintable { InvokeOnJavaFXApplicationThread { - this.bookmarkConfig.set(bookmarkConfig) + this@BookmarkConfigNode.bookmarkConfig.set(bookmarkConfig) } } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfig.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfig.kt index 103f0501b..36ae5fc25 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfig.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/LoggingConfig.kt @@ -9,7 +9,6 @@ import javafx.beans.property.ObjectProperty import javafx.beans.property.SimpleBooleanProperty import javafx.beans.property.SimpleObjectProperty import javafx.collections.FXCollections -import org.janelia.saalfeldlab.fx.extensions.addTriggeredListener import org.janelia.saalfeldlab.fx.extensions.nonnull import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions.set @@ -25,31 +24,31 @@ class LoggingConfig { val unmodifiableLoggerLevels = FXCollections.unmodifiableObservableMap(loggerLevels)!! val rootLoggerLevelProperty = SimpleObjectProperty(LogUtils.rootLoggerLevel ?: DEFAULT_LOG_LEVEL) - .apply { addTriggeredListener { _, _, level -> LogUtils.rootLoggerLevel = level } } + .apply { subscribe { level -> LogUtils.rootLoggerLevel = level } } var rootLoggerLevel: Level by rootLoggerLevelProperty.nonnull() val isLoggingEnabledProperty = SimpleBooleanProperty(DEFAULT_IS_LOGGING_ENABLED).apply { - addTriggeredListener { _, _, new -> - LogUtils.setLoggingEnabled(new) - loggerLevels.forEach { (logger, level) -> LogUtils.setLogLevelFor(logger, level.get()) } - } + subscribe { new -> + LogUtils.setLoggingEnabled(new) + loggerLevels.forEach { (logger, level) -> LogUtils.setLogLevelFor(logger, level.get()) } } + } var isLoggingEnabled: Boolean by isLoggingEnabledProperty.nonnull() val isLoggingToConsoleEnabledProperty = SimpleBooleanProperty(DEFAULT_IS_LOGGING_TO_CONSOLE_ENABLED).apply { - addTriggeredListener { _, _, new -> - LogUtils.setLoggingToConsoleEnabled(new) - loggerLevels.forEach { (logger, level) -> LogUtils.setLogLevelFor(logger, level.get()) } - } + subscribe { new -> + LogUtils.setLoggingToConsoleEnabled(new) + loggerLevels.forEach { (logger, level) -> LogUtils.setLogLevelFor(logger, level.get()) } } + } var isLoggingToConsoleEnabled: Boolean by isLoggingEnabledProperty.nonnull() val isLoggingToFileEnabledProperty = SimpleBooleanProperty(DEFAULT_IS_LOGGING_TO_FILE_ENABLED).apply { - addTriggeredListener { _, _, new -> - LogUtils.setLoggingToFileEnabled(new) - loggerLevels.forEach { (logger, level) -> LogUtils.setLogLevelFor(logger, level.get()) } - } + subscribe { new -> + LogUtils.setLoggingToFileEnabled(new) + loggerLevels.forEach { (logger, level) -> LogUtils.setLogLevelFor(logger, level.get()) } } + } var isLoggingToFileEnabled: Boolean by isLoggingEnabledProperty.nonnull() fun setLogLevelFor(name: String, level: String) = LogUtils.Logback.Levels[level]?.let { setLogLevelFor(name, it) } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt index c1dac01e3..ded7982f7 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt @@ -2,7 +2,6 @@ package org.janelia.saalfeldlab.paintera.control import bdv.viewer.TransformListener import io.github.oshai.kotlinlogging.KotlinLogging -import javafx.application.Platform import javafx.beans.InvalidationListener import javafx.beans.Observable import javafx.beans.property.ObjectProperty @@ -10,10 +9,10 @@ import javafx.beans.property.SimpleBooleanProperty import javafx.beans.property.SimpleDoubleProperty import javafx.beans.property.SimpleObjectProperty import javafx.beans.value.ChangeListener -import javafx.concurrent.Task -import javafx.concurrent.Worker import javafx.scene.paint.Color import javafx.util.Duration +import kotlinx.coroutines.* +import kotlinx.coroutines.javafx.awaitPulse import net.imglib2.* import net.imglib2.algorithm.morphology.distance.DistanceTransform import net.imglib2.converter.BiConverter @@ -40,7 +39,6 @@ import net.imglib2.view.ExtendedRealRandomAccessibleRealInterval import net.imglib2.view.IntervalView import net.imglib2.view.Views import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX -import org.janelia.saalfeldlab.fx.Tasks import org.janelia.saalfeldlab.fx.extensions.* import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.net.imglib2.outofbounds.RealOutOfBoundsConstantValueFactory @@ -64,8 +62,7 @@ import org.janelia.saalfeldlab.util.* import java.math.BigDecimal import java.math.RoundingMode import java.util.Collections -import java.util.concurrent.ExecutionException -import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference import java.util.function.Supplier import java.util.stream.Collectors import kotlin.Pair @@ -101,7 +98,7 @@ class ShapeInterpolationController>( internal val currentDepthProperty = SimpleDoubleProperty() internal val currentDepth: Double by currentDepthProperty.nonnullVal() - internal val sliceAtCurrentDepthProperty = slicesAndInterpolants.createObservableBinding(currentDepthProperty) { it.getSliceAtDepth(currentDepth) } + internal val sliceAtCurrentDepthProperty = slicesAndInterpolants.createObservableBinding(slicesAndInterpolants, currentDepthProperty) { it.getSliceAtDepth(currentDepth) } private val sliceAtCurrentDepth by sliceAtCurrentDepthProperty.nullableVal() val currentSliceMaskInterval get() = sliceAtCurrentDepth?.maskBoundingBox @@ -117,8 +114,7 @@ class ShapeInterpolationController>( private val doneApplyingMaskListener = ChangeListener { _, _, newv -> if (!newv!!) InvokeOnJavaFXApplicationThread { doneApplyingMask() } } - private var requestRepaintInterval: RealInterval? = null - private val requestRepaintAfterTask = AtomicBoolean(false) + private var requestRepaintInterval: AtomicReference = AtomicReference(null) val sortedSliceDepths: List get() = slicesAndInterpolants @@ -135,18 +131,9 @@ class ShapeInterpolationController>( return viewerState.getBestMipMapLevel(screenScaleTransform, source) } - private var selector: Task? = null - private var interpolator: Task? = null - private val onTaskFinished = { - controllerState = ControllerState.Preview - synchronized(requestRepaintAfterTask) { - InvokeOnJavaFXApplicationThread { - if (requestRepaintAfterTask.getAndSet(false)) { - requestRepaintAfterTasks(force = true) - } - } - } - } + private var interpolator: Job? = null + + private var requestRepaintUpdaterJob: Job = Job().apply { complete() } private var globalCompositeFillAndInterpolationImgs: Pair, RealRandomAccessible>? = null @@ -176,15 +163,15 @@ class ShapeInterpolationController>( return repaintInterval } - fun deleteSliceAt(depth: Double = currentDepth): RealInterval? { + fun deleteSliceAt(depth: Double = currentDepth, reinterpolate: Boolean = true): RealInterval? { return sliceAt(depth) ?.let { deleteSliceOrInterpolant(depth) } ?.also { repaintInterval -> - if (preview) { + if (reinterpolate) { isBusy = true - interpolateBetweenSlices(false) + setMaskOverlay() + requestRepaint(repaintInterval) } - requestRepaintAfterTasks(repaintInterval) } } @@ -199,7 +186,7 @@ class ShapeInterpolationController>( isBusy = true val selectionDepth = depthAt(globalTransform) if (replaceExistingSlice && slicesAndInterpolants.getSliceAtDepth(selectionDepth) != null) - slicesAndInterpolants.removeSliceAtDepth(selectionDepth) + deleteSliceAt(selectionDepth) if (slicesAndInterpolants.getSliceAtDepth(selectionDepth) == null) { val slice = SliceInfo(viewerMask, globalTransform, maskIntervalOverSelection) @@ -245,6 +232,8 @@ class ShapeInterpolationController>( } fun exitShapeInterpolation(completed: Boolean) { + requestRepaintUpdaterJob.cancel() + if (!isControllerActive) { LOG.debug { "Not in shape interpolation" } return @@ -282,7 +271,7 @@ class ShapeInterpolationController>( else updateSliceAndInterpolantsCompositeMask() if (slicesAndInterpolants.size > 0) { val globalUnion = slicesAndInterpolants.slices.mapNotNull { it.globalBoundingBox }.reduceOrNull(Intervals::union) - requestRepaintAfterTasks(unionWith = globalUnion) + requestRepaint(interval = globalUnion) } else isBusy = false } @@ -291,7 +280,8 @@ class ShapeInterpolationController>( else { updateSliceAndInterpolantsCompositeMask() val globalUnion = slicesAndInterpolants.slices.map { it.globalBoundingBox }.filterNotNull().reduceOrNull(Intervals::union) - requestRepaintAfterTasks(unionWith = globalUnion) + isBusy = false + requestRepaint(interval = globalUnion) } if (slicesAndInterpolants.isEmpty()) isBusy = false @@ -318,11 +308,11 @@ class ShapeInterpolationController>( } isBusy = true - interpolator = Tasks.createTask { task -> + interpolator = CoroutineScope(Dispatchers.Default).launch { synchronized(this) { var updateInterval: RealInterval? = null for ((firstSlice, secondSlice) in slicesAndInterpolants.zipWithNext().reversed()) { - if (task.isCancelled) return@createTask + if (!coroutineContext.isActive) return@launch if (!(firstSlice.isSlice && secondSlice.isSlice)) continue val slice1 = firstSlice.getSlice() @@ -334,16 +324,29 @@ class ShapeInterpolationController>( .reduceOrNull(Intervals::union) } updateSliceAndInterpolantsCompositeMask() - requestRepaintAfterTasks(updateInterval) + updateInterval?.let { + requestRepaint(updateInterval) + } ?: let { + /* a bit of a band-aid. It shouldn't be triggered often, but occasionally when an interpolation is triggered + * and there is no interpolation to be done (i.e. interpolation is already done, no new slices) then the refresh + * interval can desync, and show an empty interpolation result. This isn't a great fix, doesn't solve the underlying + * cause, but should stop it from happening as frequently. */ + paintera().orthogonalViews().requestRepaint() + } } - } - .onCancelled { _, _ -> LOG.debug { "Interpolation Cancelled" } } - .onSuccess { _, _ -> onTaskFinished() } - .onEnd { + }.also { job -> + job.invokeOnCompletion { cause -> + cause?.let { + LOG.debug(cause) { "Interpolation job cancelled" } + } ?: InvokeOnJavaFXApplicationThread { + controllerState = ControllerState.Preview + } + interpolator = null isBusy = false + } - .submit() + } } enum class EditSelectionChoice { @@ -394,7 +397,7 @@ class ShapeInterpolationController>( } if (controllerState == ControllerState.Interpolate) { // wait until the interpolation is done - interpolator!!.get() + runBlocking { interpolator!!.join() } } assert(controllerState == ControllerState.Preview) @@ -558,41 +561,44 @@ class ShapeInterpolationController>( } } - private fun requestRepaintAfterTasks(unionWith: RealInterval? = null, force: Boolean = false) { - fun Task<*>?.notCompleted() = this?.state?.let { it in listOf(Worker.State.READY, Worker.State.SCHEDULED, Worker.State.RUNNING) } - ?: false - InvokeOnJavaFXApplicationThread { - synchronized(requestRepaintAfterTask) { - if (!force && interpolator.notCompleted() && selector.notCompleted()) { - requestRepaintInterval = requestRepaintInterval?.let { it union unionWith } ?: unionWith - requestRepaintAfterTask.set(true) - return@InvokeOnJavaFXApplicationThread - } + private fun newRepaintRequestUpdater() = InvokeOnJavaFXApplicationThread { + + while (isActive) { + /* Don't trigger repaints while interpolating*/ + if (interpolator?.isActive == true) { + awaitPulse() + continue } - requestRepaintInterval = requestRepaintInterval?.let { it union unionWith } ?: unionWith - requestRepaintInterval?.let { - val sourceToGlobal = sourceToGlobalTransform - val extendedSourceInterval = IntervalHelpers.extendAndTransformBoundingBox(it.smallestContainingInterval, sourceToGlobal.inverse(), 1.0) - val extendedGlobalInterval = sourceToGlobal.estimateBounds(extendedSourceInterval).smallestContainingInterval - paintera().orthogonalViews().requestRepaint(extendedGlobalInterval) + requestRepaintInterval.getAndSet(null)?.let { + processRepaintRequest(it) } - requestRepaintInterval = null - isBusy = false + awaitPulse() } } + private fun processRepaintRequest(interval: RealInterval) { + val sourceToGlobal = sourceToGlobalTransform + val extendedSourceInterval = IntervalHelpers.extendAndTransformBoundingBox(interval.smallestContainingInterval, sourceToGlobal.inverse(), 1.0) + val extendedGlobalInterval = sourceToGlobal.estimateBounds(extendedSourceInterval).smallestContainingInterval + paintera().orthogonalViews().requestRepaint(extendedGlobalInterval) + } + + fun requestRepaint(interval: RealInterval? = null, force: Boolean = false) { + if (!requestRepaintUpdaterJob.isActive) + requestRepaintUpdaterJob = newRepaintRequestUpdater() + + if (force) { + val union = requestRepaintInterval.getAndSet(null)?.union(interval) ?: interval ?: return + processRepaintRequest(union) + } else + requestRepaintInterval.getAndAccumulate(interval) { l, r -> l?.union(r) ?: r } + } + private fun interruptInterpolation() { - if (interpolator != null) { - if (interpolator!!.isRunning) { - interpolator!!.cancel() - } - try { - interpolator?.get() - } catch (e: InterruptedException) { - e.printStackTrace() - } catch (e: ExecutionException) { - e.printStackTrace() - } + interpolator?.let { + it.cancel() + /* Ensure it's done */ + runBlocking { it.join() } } } @@ -1050,7 +1056,7 @@ class ShapeInterpolationController>( add(sliceOrInterpolant) InvokeOnJavaFXApplicationThread { - listeners.forEach { it.invalidated(this) } + listeners.forEach { it.invalidated(this@SlicesAndInterpolants) } } } } @@ -1165,8 +1171,8 @@ class ShapeInterpolationController>( listeners -= p0 } - private fun notifyListeners() = Platform.runLater { - listeners.forEach { it.invalidated(this) } + private fun notifyListeners() = InvokeOnJavaFXApplicationThread { + listeners.forEach { it.invalidated(this@SlicesAndInterpolants) } } } @@ -1217,7 +1223,6 @@ class ShapeInterpolationController>( minPos.zip(maxPos) .map { (min, max) -> min != Long.MAX_VALUE && max != Long.MIN_VALUE } .reduce { a, b -> a && b } - } val sourceInMaskInterval = mask.initialMaskToSourceTransform.inverse().estimateBounds(mask.source.getSource(0, mask.info.level)) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/SmoothAction.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/SmoothAction.kt index 9876c4e6b..f1a193b1e 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/SmoothAction.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/paint/SmoothAction.kt @@ -2,6 +2,7 @@ package org.janelia.saalfeldlab.paintera.control.actions.paint import com.google.common.util.concurrent.ThreadFactoryBuilder import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon +import io.github.oshai.kotlinlogging.KotlinLogging import javafx.animation.KeyFrame import javafx.animation.KeyValue import javafx.animation.Timeline @@ -11,7 +12,6 @@ import javafx.beans.property.SimpleDoubleProperty import javafx.beans.property.SimpleLongProperty import javafx.beans.property.SimpleObjectProperty import javafx.beans.value.ChangeListener -import javafx.concurrent.Worker import javafx.event.ActionEvent import javafx.event.Event import javafx.event.EventHandler @@ -40,15 +40,13 @@ import net.imglib2.realtransform.AffineTransform3D import net.imglib2.type.numeric.integer.UnsignedLongType import net.imglib2.type.numeric.real.DoubleType import net.imglib2.util.Intervals -import org.janelia.saalfeldlab.net.imglib2.view.BundleView -import org.janelia.saalfeldlab.fx.Tasks -import org.janelia.saalfeldlab.fx.UtilityTask import org.janelia.saalfeldlab.fx.actions.Action import org.janelia.saalfeldlab.fx.extensions.* import org.janelia.saalfeldlab.fx.ui.NumberField import org.janelia.saalfeldlab.fx.ui.ObjectField.SubmitOn import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.labels.blocks.LabelBlockLookupKey +import org.janelia.saalfeldlab.net.imglib2.view.BundleView import org.janelia.saalfeldlab.paintera.Paintera import org.janelia.saalfeldlab.paintera.Style.ADD_GLYPH import org.janelia.saalfeldlab.paintera.control.actions.paint.SmoothActionVerifiedState.Companion.verifyState @@ -129,11 +127,11 @@ object SmoothAction : MenuAction("_Smooth") { return ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, LinkedBlockingQueue(), ThreadFactoryBuilder().setNameFormat("gaussian-smoothing-%d").build()) } - private val LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()) + private val LOG = KotlinLogging.logger { } private var scopeJob: Job? = null private var smoothScope: CoroutineScope = CoroutineScope(Dispatchers.Default) - private var smoothTask: UtilityTask>? = null + private var smoothJob: Deferred?>? = null private var convolutionExecutor = newConvolutionExecutor() private val replacementLabelProperty = SimpleLongProperty(0) @@ -319,7 +317,7 @@ object SmoothAction : MenuAction("_Smooth") { } val cleanupOnDialogClose = { scopeJob?.cancel() - smoothTask?.cancel() + smoothJob?.cancel() kernelSizeProperty.removeListener(sizeChangeListener) kernelSizeProperty.unbind() replacementLabelProperty.unbind() @@ -332,9 +330,10 @@ object SmoothAction : MenuAction("_Smooth") { //So the dialog doesn't close until the smoothing is done event.consume() // but listen for when the smoothTask finishes - smoothTask?.stateProperty()?.addListener { _, _, state -> - if (state >= Worker.State.SUCCEEDED) + smoothJob?.invokeOnCompletion { cause -> + cause?.let { cleanupOnDialogClose() + } } // indicate the smoothTask should try to apply the current smoothing mask to canvas progress = 0.0 @@ -381,6 +380,7 @@ object SmoothAction : MenuAction("_Smooth") { */ private var smoothing by smoothingProperty.nonnull() + @OptIn(ExperimentalCoroutinesApi::class) private fun SmoothActionVerifiedState.startSmoothTask() { val prevScales = paintera.activeViewer.get()!!.screenScales @@ -390,20 +390,19 @@ object SmoothAction : MenuAction("_Smooth") { resmooth = true } - smoothTask = Tasks.createTask { task -> - paintera.baseView.disabledPropertyBindings[task] = smoothingProperty + smoothJob = CoroutineScope(Dispatchers.Default).async { kernelSizeProperty.addListener(kernelSizeChange) paintera.baseView.orthogonalViews().setScreenScales(doubleArrayOf(prevScales[0])) smoothing = true initializeSmoothLabel() smoothing = false - var intervals: List? = null - while (!task.isCancelled) { + var intervals: List? = emptyList() + while (coroutineContext.isActive) { if (resmooth || finalizeSmoothing) { val preview = !finalizeSmoothing try { smoothing = true - intervals = runBlocking { updateSmoothMask(preview).map { it.smallestContainingInterval } } + intervals = updateSmoothMask(preview).map { it.smallestContainingInterval } if (!preview) break else requestRepaintOverIntervals(intervals) } catch (c: CancellationException) { @@ -412,40 +411,39 @@ object SmoothAction : MenuAction("_Smooth") { paintera.baseView.orthogonalViews().requestRepaint() } finally { smoothing = false - /* If any remaine on the queue, shut it down */ + /* If any remain on the queue, shut it down */ convolutionExecutor.queue.peek()?.let { convolutionExecutor.shutdown() } /* reset for the next loop */ resmooth = false finalizeSmoothing = false } } else { - Thread.sleep(100) + delay(100) } } - intervals?.also { smoothedIntervals -> - paintContext.dataSource.apply { - val applyProgressProperty = SimpleDoubleProperty() - applyProgressProperty.addListener { _, _, applyProgress -> progress = applyProgress.toDouble() } - applyMaskOverIntervals(currentMask, smoothedIntervals, applyProgressProperty) { it >= 0 } + return@async intervals + }.also { task -> + paintera.baseView.disabledPropertyBindings[task] = smoothingProperty + task.invokeOnCompletion { cause -> + cause?.let { + paintContext.dataSource.resetMasks() + paintera.baseView.orthogonalViews().requestRepaint() + } ?: task.getCompleted()?.let { intervals -> + paintContext.dataSource.apply { + val applyProgressProperty = SimpleDoubleProperty() + applyProgressProperty.addListener { _, _, applyProgress -> progress = applyProgress.toDouble() } + applyMaskOverIntervals(currentMask, intervals, applyProgressProperty) { it >= 0 } + } + requestRepaintOverIntervals(intervals) + statePaintContext?.refreshMeshes?.invoke() } - } ?: let { - task.cancel() - emptyList() + + paintera.baseView.disabledPropertyBindings -= task + kernelSizeProperty.removeListener(kernelSizeChange) + paintera.baseView.orthogonalViews().setScreenScales(prevScales) + convolutionExecutor.shutdown() } - }.onCancelled { _, _ -> - scopeJob?.cancel() - paintContext.dataSource.resetMasks() - paintera.baseView.orthogonalViews().requestRepaint() - }.onSuccess { _, task -> - val intervals = task.get() - requestRepaintOverIntervals(intervals) - statePaintContext?.refreshMeshes?.invoke() - }.onEnd { - paintera.baseView.disabledPropertyBindings -= smoothTask - kernelSizeProperty.removeListener(kernelSizeChange) - paintera.baseView.orthogonalViews().setScreenScales(prevScales) - convolutionExecutor.shutdown() - }.submit() + } } private fun requestRepaintOverIntervals(intervals: List? = null) { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/Modes.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/Modes.kt index 5d94020cf..c6e539f87 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/Modes.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/Modes.kt @@ -21,7 +21,6 @@ import org.janelia.saalfeldlab.fx.actions.ActionSet import org.janelia.saalfeldlab.fx.actions.ActionSet.Companion.installActionSet import org.janelia.saalfeldlab.fx.actions.ActionSet.Companion.removeActionSet import org.janelia.saalfeldlab.fx.actions.painteraActionSet -import org.janelia.saalfeldlab.fx.extensions.addTriggeredListener import org.janelia.saalfeldlab.fx.extensions.createNullableValueBinding import org.janelia.saalfeldlab.fx.extensions.nullable import org.janelia.saalfeldlab.fx.extensions.nullableVal @@ -140,8 +139,8 @@ interface ToolMode : SourceMode { } /* when the active tool changes, update the toggle to reflect the active tool */ - activeToolProperty.addTriggeredListener { _, old, new -> - new?.let { newTool -> + activeToolProperty.subscribe { tool -> + tool?.let { newTool -> toggles .firstOrNull { it.userData == newTool } ?.also { toggleForTool -> selectToggle(toggleForTool) } @@ -149,9 +148,6 @@ interface ToolMode : SourceMode { } } } - - - } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/ShapeInterpolationMode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/ShapeInterpolationMode.kt index 0d9b0daa9..db5b01e92 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/ShapeInterpolationMode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/ShapeInterpolationMode.kt @@ -28,7 +28,7 @@ import org.janelia.saalfeldlab.fx.actions.ActionSet.Companion.removeActionSet import org.janelia.saalfeldlab.fx.actions.NamedKeyBinding import org.janelia.saalfeldlab.fx.actions.painteraActionSet import org.janelia.saalfeldlab.fx.actions.painteraMidiActionSet -import org.janelia.saalfeldlab.fx.extensions.addTriggeredWithListener +import org.janelia.saalfeldlab.fx.extensions.onceWhen import org.janelia.saalfeldlab.fx.midi.MidiButtonEvent import org.janelia.saalfeldlab.fx.midi.MidiToggleEvent import org.janelia.saalfeldlab.fx.midi.ToggleAction @@ -157,9 +157,7 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat private fun ShapeInterpolationController<*>.resetFragmentAlpha() { /* Add the activeFragmentAlpha back when we are done */ - apply { - converter.activeFragmentAlphaProperty().set((activeSelectionAlpha * 255).toInt()) - } + converter.activeFragmentAlphaProperty().set((activeSelectionAlpha * 255).toInt()) } private fun modeActions(): List { @@ -193,11 +191,9 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat filter = true verify("Fill2DTool is active") { activeTool is Fill2DTool } onAction { - fill2DTool.fillIsRunningProperty.addTriggeredWithListener { obs, _, isRunning -> - if (!isRunning) { - switchTool(shapeInterpolationTool) - obs?.removeListener(this) - } + val fillNotRunning = fill2DTool.fillIsRunningProperty.not() + fillNotRunning.onceWhen(fillNotRunning).subscribe { _ -> + InvokeOnJavaFXApplicationThread { switchTool(shapeInterpolationTool) } } } } @@ -471,8 +467,8 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat internal fun addSelection( selectionIntervalOverMask: Interval, - globalTransform: AffineTransform3D = paintera.baseView.manager().transform, viewerMask: ViewerMask = controller.currentViewerMask!!, + globalTransform: AffineTransform3D = viewerMask.currentGlobalTransform, replaceExistingSlice: Boolean = false ): SamSliceInfo? { val globalToViewerTransform = viewerMask.initialGlobalToMaskTransform diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.kt new file mode 100644 index 000000000..25fa660b0 --- /dev/null +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.kt @@ -0,0 +1,268 @@ +package org.janelia.saalfeldlab.paintera.control.paint + +import io.github.oshai.kotlinlogging.KotlinLogging +import javafx.beans.value.ObservableValue +import kotlinx.coroutines.* +import kotlinx.coroutines.javafx.awaitPulse +import net.imglib2.* +import net.imglib2.algorithm.fill.FloodFill +import net.imglib2.algorithm.neighborhood.DiamondShape +import net.imglib2.realtransform.AffineTransform3D +import net.imglib2.type.label.Label +import net.imglib2.type.label.LabelMultisetType +import net.imglib2.type.numeric.IntegerType +import net.imglib2.type.numeric.integer.UnsignedLongType +import net.imglib2.util.Intervals +import net.imglib2.util.Util +import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX +import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread +import org.janelia.saalfeldlab.net.imglib2.util.AccessBoxRandomAccessible +import org.janelia.saalfeldlab.paintera.Paintera.Companion.getPaintera +import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment +import org.janelia.saalfeldlab.paintera.control.paint.ViewerMask.Companion.getGlobalViewerInterval +import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo +import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource +import org.janelia.saalfeldlab.paintera.data.mask.exception.MaskInUse +import org.janelia.saalfeldlab.paintera.util.IntervalHelpers.Companion.smallestContainingInterval +import org.janelia.saalfeldlab.util.extendValue +import java.util.concurrent.CancellationException +import java.util.concurrent.atomic.AtomicBoolean +import java.util.function.* +import java.util.stream.Collectors +import kotlin.coroutines.coroutineContext + +class FloodFill>( + private val activeViewerProperty: ObservableValue, + private val source: MaskedSource, + private val assignment: FragmentSegmentAssignment, + private val requestRepaint: Consumer, + private val isVisible: BooleanSupplier +) { + + fun fillAt(x: Double, y: Double, fillSupplier: (() -> Long?)?): Job? { + val fill = fillSupplier?.invoke() ?: let { + LOG.info { "Received invalid label -- will not fill." } + return null + } + return fillAt(x, y, fill) + } + + private fun fillAt(x: Double, y: Double, fill: Long): Job? { + // TODO should this check happen outside? + + if (!isVisible.asBoolean) { + LOG.info { "Selected source is not visible -- will not fill" } + return null + } + + val level = 0 + val labelTransform = AffineTransform3D() + // TODO What to do for time series? + val time = 0 + source.getSourceTransform(time, level, labelTransform) + + val realSourceSeed = viewerToSourceCoordinates(x, y, activeViewerProperty.value, labelTransform) + val sourceSeed = Point(realSourceSeed.numDimensions()) + for (d in 0 until sourceSeed.numDimensions()) { + sourceSeed.setPosition(Math.round(realSourceSeed.getDoublePosition(d)), d) + } + + LOG.debug("Filling source {} with label {} at {}", source, fill, sourceSeed) + try { + return fill(time, level, fill, sourceSeed, assignment) + } catch (e: MaskInUse) { + LOG.info(e) {} + return null + } + } + + @Throws(MaskInUse::class) + private fun fill( + time: Int, + level: Int, + fill: Long, + seed: Localizable, + assignment: FragmentSegmentAssignment? + ): Job? { + val data = source.getDataSource(time, level) + val dataAccess = data.randomAccess() + dataAccess.setPosition(seed) + val seedValue = dataAccess.get() + val seedLabel = assignment?.getSegment(seedValue!!.integerLong) ?: seedValue!!.integerLong + if (!Label.regular(seedLabel)) { + LOG.info { "Cannot fill at irregular label: $seedLabel (${Point(seed)})" } + return null + } + + val maskInfo = MaskInfo( + time, + level + ) + val mask = source.generateMask(maskInfo, MaskedSource.VALID_LABEL_CHECK) + val globalToSource = source.getSourceTransformForMask(maskInfo).inverse() + + val visibleSourceIntervals = getPaintera().baseView.orthogonalViews().views().stream() + .filter { it: ViewerPanelFX -> it.isVisible && it.width > 0.0 && it.height > 0.0 } + .map { it.getGlobalViewerInterval() } + .map { globalToSource.estimateBounds(it) } + .map { Intervals.smallestContainingInterval(it) } + .collect(Collectors.toList()) + + val triggerRefresh = AtomicBoolean(false) + val accessTracker: AccessBoxRandomAccessible = object : AccessBoxRandomAccessible(mask.rai.extendValue(UnsignedLongType(1))) { + val position: Point = Point(sourceAccess.numDimensions()) + + override fun get(): UnsignedLongType { + if (Thread.currentThread().isInterrupted) throw RuntimeException("Flood Fill Interrupted") + synchronized(this) { + updateAccessBox() + } + sourceAccess.localize(position) + if (!triggerRefresh.get()) { + for (interval in visibleSourceIntervals) { + if (Intervals.contains(interval, position)) { + triggerRefresh.set(true) + break + } + } + } + return sourceAccess.get()!! + } + } + + val floodFillJob = CoroutineScope(Dispatchers.Default).launch { + val fillContext = coroutineContext + InvokeOnJavaFXApplicationThread { + delay(250) + while (fillContext.isActive) { + awaitPulse() + if (triggerRefresh.get()) { + val repaintInterval = globalToSource.inverse().estimateBounds(accessTracker.createAccessInterval()) + requestRepaint.accept(repaintInterval.smallestContainingInterval) + triggerRefresh.set(false) + } + delay(250) + } + } + + if (seedValue is LabelMultisetType) { + fillMultisetType(data as RandomAccessibleInterval, accessTracker, seed, seedLabel, fill, assignment) + } else { + fillPrimitiveType(data, accessTracker, seed, seedLabel, fill, assignment) + } + }.also { + it.invokeOnCompletion { cause -> + val sourceInterval = accessTracker.createAccessInterval() + val globalInterval = globalToSource.inverse().estimateBounds(sourceInterval).smallestContainingInterval + + when (cause) { + null -> { + LOG.trace { "FloodFill has been completed" } + LOG.trace { + "Applying mask for interval ${Intervals.minAsLongArray(sourceInterval).contentToString()} ${Intervals.maxAsLongArray(sourceInterval).contentToString()}" + } + requestRepaint.accept(globalInterval) + source.applyMask(mask, sourceInterval, MaskedSource.VALID_LABEL_CHECK) + } + + is CancellationException -> try { + LOG.debug { "FloodFill has been interrupted" } + source.resetMasks() + requestRepaint.accept(globalInterval) + } catch (e: MaskInUse) { + LOG.error(e) {} + } + + else -> requestRepaint.accept(globalInterval) + } + } + } + return floodFillJob + } + + companion object { + private val LOG = KotlinLogging.logger { } + + private fun viewerToSourceCoordinates( + x: Double, + y: Double, + viewer: ViewerPanelFX, + labelTransform: AffineTransform3D + ): RealPoint { + return viewerToSourceCoordinates(x, y, RealPoint(labelTransform.numDimensions()), viewer, labelTransform) + } + + private fun

viewerToSourceCoordinates( + x: Double, + y: Double, + location: P, + viewer: ViewerPanelFX, + labelTransform: AffineTransform3D + ): P where P : RealLocalizable, P : RealPositionable { + location.setPosition(longArrayOf(x.toLong(), y.toLong(), 0)) + + viewer.displayToGlobalCoordinates(location) + labelTransform.applyInverse(location, location) + + return location + } + + private fun fillMultisetType( + input: RandomAccessibleInterval, + output: RandomAccessible, + seed: Localizable, + seedLabel: Long, + fillLabel: Long, + assignment: FragmentSegmentAssignment? + ) { + val predicate = makePredicate(seedLabel, assignment) + FloodFill.fill( + input.extendValue(LabelMultisetType()), + output, + seed, + UnsignedLongType(fillLabel), + DiamondShape(1) + ) { source, target: UnsignedLongType -> runBlocking { predicate(source, target) } } + } + + private suspend fun > fillPrimitiveType( + input: RandomAccessibleInterval, + output: RandomAccessible, + seed: Localizable, + seedLabel: Long, + fillLabel: Long, + assignment: FragmentSegmentAssignment? + ) { + val extension = Util.getTypeFromInterval(input)!!.createVariable() + extension!!.setInteger(Label.OUTSIDE) + + val predicate = makePredicate(seedLabel, assignment) + FloodFill.fill( + input.extendValue(extension), + output, + seed, + UnsignedLongType(fillLabel), + DiamondShape(1) + ) { source, target: UnsignedLongType -> runBlocking { predicate(source, target) } } + } + + private fun > makePredicate(seedLabel: Long, assignment: FragmentSegmentAssignment?): suspend (T, UnsignedLongType) -> Boolean { + val (singleFragment, seedFragments) = assignment?.let { + val fragments = assignment.getFragments(seedLabel) + val singleFragment = if (fragments.size() == 1) fragments.toArray()[0] else null + singleFragment to fragments + } ?: (seedLabel to null) + + + return { sourceVal: T, targetVal: UnsignedLongType -> + if (!coroutineContext.isActive) + throw CancellationException("Flood fill canceled") + /* true if sourceFragment is a seedFragment */ + val sourceFragment = sourceVal.integerLong + val shouldFill = if (singleFragment != null) singleFragment == sourceFragment else seedFragments!!.contains(sourceFragment) + shouldFill && targetVal.integer.toLong() == Label.INVALID + } + } + } +} + diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.kt new file mode 100644 index 000000000..9f7fb5995 --- /dev/null +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.kt @@ -0,0 +1,197 @@ +package org.janelia.saalfeldlab.paintera.control.paint + +import io.github.oshai.kotlinlogging.KotlinLogging +import javafx.beans.property.ReadOnlyObjectProperty +import javafx.beans.property.ReadOnlyObjectWrapper +import javafx.beans.property.SimpleDoubleProperty +import javafx.beans.value.ObservableValue +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.javafx.awaitPulse +import net.imglib2.Interval +import net.imglib2.Point +import net.imglib2.RandomAccessible +import net.imglib2.RandomAccessibleInterval +import net.imglib2.algorithm.fill.FloodFill +import net.imglib2.algorithm.neighborhood.DiamondShape +import net.imglib2.converter.Converters +import net.imglib2.type.label.Label +import net.imglib2.type.logic.BoolType +import net.imglib2.type.numeric.IntegerType +import net.imglib2.type.numeric.RealType +import net.imglib2.type.numeric.integer.UnsignedLongType +import net.imglib2.util.Intervals +import net.imglib2.view.Views +import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX +import org.janelia.saalfeldlab.fx.extensions.nullableVal +import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread +import org.janelia.saalfeldlab.net.imglib2.util.AccessBoxRandomAccessibleOnGet +import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignment +import org.janelia.saalfeldlab.paintera.control.paint.ViewerMask.Companion.createViewerMask +import org.janelia.saalfeldlab.paintera.control.paint.ViewerMask.Companion.getSourceDataInInitialMaskSpace +import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo +import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource +import org.janelia.saalfeldlab.util.extendValue +import java.util.concurrent.atomic.AtomicBoolean +import java.util.function.BooleanSupplier +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.coroutineContext + +class FloodFill2D>( + val activeViewerProperty: ObservableValue, + val source: MaskedSource, + val isVisible: BooleanSupplier +) { + val fillDepthProperty = SimpleDoubleProperty(2.0) + + internal var viewerMask: ViewerMask? = null + + private val maskIntervalProperty = ReadOnlyObjectWrapper(null) + val readOnlyMaskInterval: ReadOnlyObjectProperty = maskIntervalProperty.readOnlyProperty + val maskInterval by readOnlyMaskInterval.nullableVal() + + fun release() { + viewerMask = null + maskIntervalProperty.set(null) + } + + private fun getOrCreateViewerMask(): ViewerMask { + return viewerMask ?: activeViewerProperty.value.let { + val level = it.state.bestMipMapLevel + val time = it.state.timepoint + val maskInfo = MaskInfo(time, level) + source.createViewerMask(maskInfo, it, paintDepth = fillDepthProperty.get()) + } + } + + suspend fun fillViewerAt(viewerSeedX: Double, viewerSeedY: Double, fill: Long, assignment: FragmentSegmentAssignment) { + + val mask = getOrCreateViewerMask() + val maskPos = mask.displayPointToMask(viewerSeedX, viewerSeedY, true) + val filter = getBackgroundLabelMaskForAssignment(maskPos, mask, assignment, fill) + + fillMaskAt(maskPos, mask, fill, filter) + if (!coroutineContext.isActive) { + mask.source.resetMasks() + mask.requestRepaint() + } + } + + + private suspend fun fillMaskAt(maskPos: Point, mask: ViewerMask, fill: Long, filter: RandomAccessibleInterval) { + if (fill == Label.INVALID) { + val reason = "Received invalid label -- will not fill" + LOG.warn { reason } + throw CancellationException(reason) + } + + if (!isVisible.asBoolean) { + val reason = "Selected source is not visible -- will not fill" + LOG.warn { reason } + throw CancellationException(reason) + } + + val screenInterval = mask.getScreenInterval() + + val triggerRefresh = AtomicBoolean(false) + val writableViewerImg = mask.viewerImg.writableSource!!.extendValue(UnsignedLongType(fill)) + val fillContext = coroutineContext + val sourceAccessTracker: AccessBoxRandomAccessibleOnGet = + object : AccessBoxRandomAccessibleOnGet(writableViewerImg) { + val position: Point = Point(sourceAccess.numDimensions()) + + override fun get(): UnsignedLongType { + if (!fillContext.isActive) throw CancellationException("Flood Fill Canceled") + synchronized(this) { + updateAccessBox() + } + sourceAccess.localize(position) + if (!triggerRefresh.get() && Intervals.contains(screenInterval, position)) { + triggerRefresh.set(true) + } + return sourceAccess.get()!! + } + } + sourceAccessTracker.initAccessBox() + + launchRepaintRequestUpdater(fillContext, triggerRefresh, mask, sourceAccessTracker::createAccessInterval) + + fillAt(maskPos, sourceAccessTracker, filter, fill) + if (fillContext.isActive) + maskIntervalProperty.set(sourceAccessTracker.createAccessInterval()) + } + + private fun launchRepaintRequestUpdater(fillContext: CoroutineContext, triggerRefresh: AtomicBoolean, mask: ViewerMask, interval: () -> Interval) { + InvokeOnJavaFXApplicationThread { + delay(1000) + while (fillContext.isActive) { + awaitPulse() + if (triggerRefresh.get()) { + mask.requestRepaint(interval()) + triggerRefresh.set(false) + } + delay(250) + } + mask.requestRepaint(interval()) + } + } + + companion object { + private val LOG = KotlinLogging.logger { } + + suspend fun getBackgroundLabelMaskForAssignment( + initialSeed: Point, mask: ViewerMask, + assignment: FragmentSegmentAssignment, + fillValue: Long + ): RandomAccessibleInterval { + val backgroundViewerRai = mask.getSourceDataInInitialMaskSpace() + + val id = backgroundViewerRai.getAt(initialSeed)!!.realDouble.toLong() + val seedLabel = assignment.getSegment(id) + LOG.trace { "Got seed label $seedLabel" } + + if (seedLabel == fillValue) { + val reason = "seed label and fill label are the same, nothing to fill" + LOG.warn { reason } + throw CancellationException(reason) + } + + return Converters.convert( + backgroundViewerRai, + { src: RealType?>?, target: BoolType -> + val segmentId = assignment.getSegment(src!!.realDouble.toLong()) + target.set(segmentId == seedLabel) + }, + BoolType() + ) + } + + fun fillAt( + initialSeed: Point, + target: RandomAccessible, + filter: RandomAccessibleInterval, + fillValue: Long + ) { + val extendedFilter = filter.extendValue(false) + + // fill only within the given slice, run 2D flood-fill + LOG.trace { "Flood filling" } + val backgroundSlice = if (extendedFilter.numDimensions() == 3) { + Views.hyperSlice(extendedFilter, 2, 0) + } else { + extendedFilter + } + val viewerFillImg: RandomAccessible = Views.hyperSlice(target, 2, 0) + + FloodFill.fill( + backgroundSlice, + viewerFillImg, + initialSeed, + UnsignedLongType(fillValue), + DiamondShape(1) + ) + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/PaintClickOrDragController.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/PaintClickOrDragController.kt index a51fb1bd8..96c3b57b4 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/PaintClickOrDragController.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/PaintClickOrDragController.kt @@ -1,17 +1,20 @@ package org.janelia.saalfeldlab.paintera.control.paint -import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX import javafx.beans.property.ReadOnlyBooleanProperty import javafx.beans.value.ChangeListener import javafx.event.EventHandler import javafx.scene.control.Button import javafx.scene.control.ButtonType import javafx.scene.input.MouseEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async import net.imglib2.Interval import net.imglib2.RealInterval import net.imglib2.realtransform.AffineTransform3D import net.imglib2.util.LinAlgHelpers -import org.janelia.saalfeldlab.fx.Tasks +import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX import org.janelia.saalfeldlab.fx.ui.Exceptions.Companion.exceptionAlert import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.labels.Label @@ -316,6 +319,7 @@ class PaintClickOrDragController( } else field @Synchronized + @OptIn(ExperimentalCoroutinesApi::class) private fun paint(viewerX: Double, viewerY: Double) { LOG.trace("At {} {}", viewerX, viewerY) when { @@ -324,21 +328,27 @@ class PaintClickOrDragController( } - viewerMask?.run { - Tasks.createTask { - val viewerPointToMaskPoint = this.displayPointToMask(viewerX.toInt(), viewerY.toInt(), pointInCurrentDisplay = true) + viewerMask?.also { mask -> + CoroutineScope(Dispatchers.Default).async { + val viewerPointToMaskPoint = mask.displayPointToMask(viewerX.toInt(), viewerY.toInt(), pointInCurrentDisplay = true) val paintIntervalInMask = Paint2D.paintIntoViewer( - viewerImg.writableSource!!.extendValue(Label.INVALID), + mask.viewerImg.writableSource!!.extendValue(Label.INVALID), paintId(), viewerPointToMaskPoint, - brushRadius() * xScaleChange + brushRadius() * mask.xScaleChange ) paintIntervalInMask - }.onSuccess { _, task -> - val paintIntervalInMask = task.get() - maskInterval = paintIntervalInMask union maskInterval - requestRepaint(paintIntervalInMask) - }.submit(paintService!!) + }.also { job -> + job.invokeOnCompletion { cause -> + cause ?: let { + job.getCompleted()?.let { paintedInterval -> + maskInterval = paintedInterval union maskInterval + mask.requestRepaint(paintedInterval) + } + + } + } + } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill2DTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill2DTool.kt index d85be0300..643a12d72 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill2DTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill2DTool.kt @@ -1,35 +1,41 @@ package org.janelia.saalfeldlab.paintera.control.tools.paint -import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView +import io.github.oshai.kotlinlogging.KotlinLogging import javafx.beans.property.SimpleBooleanProperty import javafx.beans.property.SimpleDoubleProperty import javafx.beans.property.SimpleObjectProperty import javafx.beans.value.ObservableValue -import javafx.scene.Cursor -import javafx.scene.input.* -import javafx.util.Subscription +import javafx.scene.input.KeyEvent +import javafx.scene.input.MouseButton +import javafx.scene.input.MouseEvent +import javafx.scene.input.ScrollEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import net.imglib2.Interval -import net.imglib2.util.Intervals -import org.janelia.saalfeldlab.fx.UtilityTask +import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX import org.janelia.saalfeldlab.fx.actions.ActionSet import org.janelia.saalfeldlab.fx.actions.painteraActionSet import org.janelia.saalfeldlab.fx.extensions.* +import org.janelia.saalfeldlab.fx.ui.GlyphScaleView import org.janelia.saalfeldlab.fx.ui.ScaleView -import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.labels.Label import org.janelia.saalfeldlab.paintera.LabelSourceStateKeys import org.janelia.saalfeldlab.paintera.control.ControlUtils import org.janelia.saalfeldlab.paintera.control.actions.PaintActionType -import org.janelia.saalfeldlab.fx.ui.GlyphScaleView import org.janelia.saalfeldlab.paintera.control.modes.ToolMode import org.janelia.saalfeldlab.paintera.control.paint.FloodFill2D import org.janelia.saalfeldlab.paintera.control.paint.ViewerMask -import org.janelia.saalfeldlab.paintera.meshes.MeshSettings import org.janelia.saalfeldlab.paintera.paintera import org.janelia.saalfeldlab.paintera.state.SourceState import org.janelia.saalfeldlab.paintera.ui.overlays.CursorOverlayWithText +import org.janelia.saalfeldlab.paintera.util.IntervalHelpers.Companion.smallestContainingInterval import kotlin.collections.set +import kotlin.coroutines.cancellation.CancellationException + +private val LOG = KotlinLogging.logger { } open class Fill2DTool(activeSourceStateProperty: SimpleObjectProperty?>, mode: ToolMode? = null) : PaintTool(activeSourceStateProperty, mode) { @@ -45,14 +51,17 @@ open class Fill2DTool(activeSourceStateProperty: SimpleObjectProperty vat?.viewer() }, dataSource, - ) { MeshSettings.Defaults.Values.isVisible } - floodFill2D.fillDepthProperty().bindBidirectional(brushProperties.brushDepthProperty) + ) { activeSourceStateProperty.value?.isVisibleProperty?.get() ?: false } + floodFill2D.fillDepthProperty.bindBidirectional(brushProperties.brushDepthProperty) floodFill2D } } - val fillTaskProperty: SimpleObjectProperty?> = SimpleObjectProperty(null) - private var fillTask by fillTaskProperty.nullable() + val fillJobProperty: SimpleObjectProperty = SimpleObjectProperty(null) + private var fillJob by fillJobProperty.nullable() + + open protected val afterFill : (Interval) -> Unit = {} + private val overlay by lazy { Fill2DOverlay(activeViewerProperty.createNullableValueBinding { it?.viewer() }).apply { @@ -64,7 +73,6 @@ open class Fill2DTool(activeSourceStateProperty: SimpleObjectProperty - if (!isRunning) { + val fillNotRunning = fillIsRunningProperty.not() + fillNotRunning.onceWhen(fillNotRunning).subscribe { notRunning -> + if (notRunning) { overlay.visible = false fill2D.release() - obs?.removeListener(this) super.deactivate() } } @@ -101,92 +109,78 @@ open class Fill2DTool(activeSourceStateProperty: SimpleObjectProperty Unit = {}): UtilityTask<*>? { + internal fun executeFill2DAction(x: Double, y: Double, afterFill: (Interval) -> Unit = {}): Job? { fillIsRunningProperty.set(true) - val applyIfMaskNotProvided = fill2D.mask == null + val applyIfMaskNotProvided = fill2D.viewerMask == null if (applyIfMaskNotProvided) { - statePaintContext!!.dataSource.resetMasks(true); - } - fillTask = fill2D.fillViewerAt(x, y, fillLabel(), statePaintContext!!.assignment) - if (fillTask == null) { - fillIsRunningProperty.set(false) + statePaintContext!!.dataSource.resetMasks(true) } - return fillTask?.also { task -> - if (task.isDone) { - /* If it's already done, do this now*/ - if (!task.isCancelled) { - val maskFillInterval = fill2D.maskIntervalProperty.value - afterFill(maskFillInterval) + paintera.baseView.disabledPropertyBindings[this] = fillIsRunningProperty + + fillJob = CoroutineScope(Dispatchers.Default).launch { + fill2D.fillViewerAt(x, y, fillLabel(), statePaintContext!!.assignment) + }.also { job -> + job.invokeOnCompletion { cause -> + + cause?.let { + if (it is CancellationException) LOG.trace(it) {} + else LOG.error(it) {"Flood Fill 2D Failed"} if (applyIfMaskNotProvided) { /* Then apply when done */ val source = statePaintContext!!.dataSource val mask = source.currentMask as ViewerMask - val affectedSourceInterval = Intervals.smallestContainingInterval( - mask.currentMaskToSourceWithDepthTransform.estimateBounds(maskFillInterval)) - source.applyMask(mask, affectedSourceInterval, net.imglib2.type.label.Label::isForeground) + source.resetMasks(true) + mask.requestRepaint() } + cleanup() - } - } else { - paintera.baseView.disabledPropertyBindings[this] = fillIsRunningProperty - paintera.baseView.isDisabledProperty.addTriggeredWithListener { obs, _, isBusy -> - if (isBusy) { - overlay.cursor = Cursor.WAIT - } else { - overlay.cursor = Cursor.CROSSHAIR - if (!paintera.keyTracker.areKeysDown(*keyTrigger.keyCodes.toTypedArray()) && !enteredWithoutKeyTrigger) { - InvokeOnJavaFXApplicationThread { mode?.switchTool(mode.defaultTool) } - } - obs?.removeListener(this) - } + return@invokeOnCompletion } - /* Otherwise, do it when it's done */ - task.onEnd(append = true) { - fillIsRunningProperty.set(false) - paintera.baseView.disabledPropertyBindings -= this - fillTask = null + val maskFillInterval = fill2D.maskInterval!! + afterFill(maskFillInterval) + if (applyIfMaskNotProvided) { + /* Then apply when done */ + val source = statePaintContext!!.dataSource + val mask = source.currentMask as ViewerMask + val affectedSourceInterval = mask.currentMaskToSourceWithDepthTransform.estimateBounds(maskFillInterval).smallestContainingInterval + source.applyMask(mask, affectedSourceInterval, net.imglib2.type.label.Label::isForeground) } - - task.onSuccess(append = true) { _, _ -> - val maskFillInterval = fill2D.maskIntervalProperty.value - afterFill(maskFillInterval) - if (applyIfMaskNotProvided) { - /* Then apply when done */ - val source = statePaintContext!!.dataSource - val mask = source.currentMask as ViewerMask - val affectedSourceInterval = Intervals.smallestContainingInterval( - mask.currentMaskToSourceWithDepthTransform.estimateBounds(maskFillInterval)) - source.applyMask(mask, affectedSourceInterval, net.imglib2.type.label.Label::isForeground) - } - } - + cleanup() } } + return fillJob + } + + private fun cleanup() { + fillIsRunningProperty.set(false) + paintera.baseView.disabledPropertyBindings -= this + fillJob = null } private class Fill2DOverlay(viewerProperty: ObservableValue) : CursorOverlayWithText(viewerProperty) { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill3DTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill3DTool.kt index f697b4b6e..3defa9994 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill3DTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/Fill3DTool.kt @@ -1,6 +1,5 @@ package org.janelia.saalfeldlab.paintera.control.tools.paint -import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView import javafx.beans.property.SimpleBooleanProperty import javafx.beans.property.SimpleObjectProperty @@ -11,23 +10,22 @@ import javafx.scene.input.KeyEvent.KEY_PRESSED import javafx.scene.input.MouseButton import javafx.scene.input.MouseEvent import javafx.scene.input.ScrollEvent -import net.imglib2.realtransform.AffineTransform3D -import org.janelia.saalfeldlab.fx.UtilityTask +import kotlinx.coroutines.Job +import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX import org.janelia.saalfeldlab.fx.actions.ActionSet import org.janelia.saalfeldlab.fx.actions.painteraActionSet import org.janelia.saalfeldlab.fx.extensions.LazyForeignValue import org.janelia.saalfeldlab.fx.extensions.createNullableValueBinding import org.janelia.saalfeldlab.fx.extensions.nonnull import org.janelia.saalfeldlab.fx.extensions.nullable +import org.janelia.saalfeldlab.fx.ui.GlyphScaleView import org.janelia.saalfeldlab.fx.ui.ScaleView import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.paintera.LabelSourceStateKeys import org.janelia.saalfeldlab.paintera.control.ControlUtils import org.janelia.saalfeldlab.paintera.control.actions.PaintActionType -import org.janelia.saalfeldlab.fx.ui.GlyphScaleView import org.janelia.saalfeldlab.paintera.control.modes.ToolMode import org.janelia.saalfeldlab.paintera.control.paint.FloodFill -import org.janelia.saalfeldlab.paintera.meshes.MeshSettings import org.janelia.saalfeldlab.paintera.paintera import org.janelia.saalfeldlab.paintera.state.SourceState import org.janelia.saalfeldlab.paintera.ui.overlays.CursorOverlayWithText @@ -40,8 +38,8 @@ class Fill3DTool(activeSourceStateProperty: SimpleObjectProperty?>() - private var floodFillTask: UtilityTask<*>? by floodFillTaskProperty.nullable() + private val floodFillTaskProperty = SimpleObjectProperty() + private var floodFillTask: Job? by floodFillTaskProperty.nullable() val fill by LazyForeignValue({ statePaintContext }) { with(it!!) { @@ -49,13 +47,8 @@ class Fill3DTool(activeSourceStateProperty: SimpleObjectProperty vat?.viewer() }, dataSource, assignment, - { interval -> - val sourceToGlobal = AffineTransform3D().also { transform -> - dataSource.getSourceTransform(dataSource.currentMask.info, transform) - } - paintera.baseView.orthogonalViews().requestRepaint(sourceToGlobal.estimateBounds(interval)) - }, - { MeshSettings.Defaults.Values.isVisible } + { interval -> paintera.baseView.orthogonalViews().requestRepaint(interval) }, + { activeSourceStateProperty.value?.isVisibleProperty?.get() ?: false } ) } } @@ -113,20 +106,11 @@ class Fill3DTool(activeSourceStateProperty: SimpleObjectProperty fillIsRunningProperty.set(false) paintera.baseView.disabledPropertyBindings -= this statePaintContext?.refreshMeshes?.invoke() floodFillTask = null - } else { - /* Otherwise, do it when it's done */ - task.onEnd(append = true) { - floodFillTask = null - fillIsRunningProperty.set(false) - paintera.baseView.disabledPropertyBindings -= this - statePaintContext?.refreshMeshes?.invoke() - } } } } @@ -135,7 +119,7 @@ class Fill3DTool(activeSourceStateProperty: SimpleObjectProperty, activeSourceStateProperty: SimpleObjectProperty?>, val shapeInterpolationMode: ShapeInterpolationMode<*>) : Fill2DTool(activeSourceStateProperty, shapeInterpolationMode) { - private val controllerPaintOnFill = ChangeListener { _, _, new -> - new?.let { interval -> shapeInterpolationMode.addSelection(interval)?.also { it.locked = true } } + override val afterFill = ::addSelectionOverFillInterval + + private fun addSelectionOverFillInterval(interval: Interval?) { + interval?.also { + val slice = shapeInterpolationMode.addSelection(it) + slice?.locked = true + } } override fun activate() { @@ -27,12 +30,6 @@ internal class ShapeInterpolationFillTool(private val controller : ShapeInterpol /* Don't allow filling with depth during shape interpolation */ brushProperties?.brushDepth = 1.0 fillLabel = { controller.interpolationId } - fill2D.maskIntervalProperty.addListener(controllerPaintOnFill) - } - - override fun deactivate() { - fill2D.maskIntervalProperty.removeListener(controllerPaintOnFill) - super.deactivate() } override val actionSets: MutableList by LazyForeignValue({ activeViewerAndTransforms }) { @@ -60,16 +57,15 @@ internal class ShapeInterpolationFillTool(private val controller : ShapeInterpol source.resetMasks(false) val mask = controller.getMask() mask.pushNewImageLayer() - fillTaskProperty.addWithListener { obs, _, task -> - task?.let { - task.onCancelled(true) { _, _ -> + fillJobProperty.onceWhen(fillJobProperty.isNotNull).subscribe { _, job -> + job?.invokeOnCompletion { cause -> + cause?.let { mask.popImageLayer() - mask.requestRepaint() + controller.setMaskOverlay() } - task.onEnd(true) { obs?.removeListener(this) } - } ?: obs?.removeListener(this) + } } - fill2D.provideMask(mask) + fill2D.viewerMask = mask } } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationSAMTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationSAMTool.kt index 6226c694d..e68d2a9c8 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationSAMTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationSAMTool.kt @@ -46,10 +46,8 @@ internal class ShapeInterpolationSAMTool(private val controller: ShapeInterpolat super.activate() - if (!info.locked && !info.preGenerated) { - temporaryPrompt = false - requestPrediction(info.prediction) - } + temporaryPrompt = !info.locked + requestPrediction(info.prediction) } override fun deactivate() { @@ -66,7 +64,6 @@ internal class ShapeInterpolationSAMTool(private val controller: ShapeInterpolat it.locked = true } InvokeOnJavaFXApplicationThread { - shapeInterpolationMode.run { switchTool(defaultTool) modeToolsBar.toggleGroup?.selectToggle(null) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationTool.kt index 8442bf36f..9eed18abf 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationTool.kt @@ -11,20 +11,18 @@ import javafx.scene.input.MouseEvent.MOUSE_CLICKED import javafx.util.Duration import kotlinx.coroutines.* import net.imglib2.realtransform.AffineTransform3D -import net.imglib2.util.Intervals -import org.janelia.saalfeldlab.fx.UtilityTask import org.janelia.saalfeldlab.fx.actions.* import org.janelia.saalfeldlab.fx.actions.ActionSet.Companion.installActionSet import org.janelia.saalfeldlab.fx.actions.ActionSet.Companion.removeActionSet -import org.janelia.saalfeldlab.fx.extensions.addWithListener import org.janelia.saalfeldlab.fx.extensions.createNonNullValueBinding import org.janelia.saalfeldlab.fx.extensions.createNullableValueBinding import org.janelia.saalfeldlab.fx.extensions.nonnullVal +import org.janelia.saalfeldlab.fx.extensions.onceWhen import org.janelia.saalfeldlab.fx.midi.MidiButtonEvent import org.janelia.saalfeldlab.fx.midi.MidiToggleEvent import org.janelia.saalfeldlab.fx.ortho.OrthogonalViews -import org.janelia.saalfeldlab.fx.ui.ScaleView import org.janelia.saalfeldlab.fx.ui.GlyphScaleView +import org.janelia.saalfeldlab.fx.ui.ScaleView import org.janelia.saalfeldlab.labels.Label import org.janelia.saalfeldlab.paintera.DeviceManager import org.janelia.saalfeldlab.paintera.LabelSourceStateKeys.* @@ -59,7 +57,7 @@ internal class ShapeInterpolationTool( override val graphic = { GlyphScaleView(FontAwesomeIconView().also { it.styleClass += listOf("navigation-tool") }) } override val name: String = "Shape Interpolation" override val keyTrigger = SHAPE_INTERPOLATION__TOGGLE_MODE - private var currentTask: UtilityTask<*>? = null + private var currentJob: Job? = null override fun activate() { @@ -116,7 +114,7 @@ internal class ShapeInterpolationTool( } arrayOf( painteraDragActionSet("disabled_translate_xy", NavigationActionType.Pan) { - relative = true; + relative = true verify { it.isSecondaryButtonDown } verify { controller.controllerState != ShapeInterpolationController.ControllerState.Interpolate } onDrag { translator.translate(it.x - startX, it.y - startY) } @@ -185,7 +183,7 @@ internal class ShapeInterpolationTool( samTool.lastPredictionProperty.addListener { _, _, prediction -> prediction ?: return@addListener - shapeInterpolationMode.addSelection(prediction.maskInterval, globalTransform, viewerMask) ?: return@addListener + shapeInterpolationMode.addSelection(prediction.maskInterval, viewerMask, globalTransform) ?: return@addListener afterPrediction(globalTransform) @@ -382,7 +380,7 @@ internal class ShapeInterpolationTool( source.resetMasks(false) val mask = getMask() - fill2D.fill2D.provideMask(mask) + fill2D.fill2D.viewerMask = mask val pointInMask = mask.displayPointToMask(event!!.x, event.y, pointInCurrentDisplay = true) val pointInSource = pointInMask.positionAsRealPoint().also { mask.initialMaskToSourceTransform.apply(it, it) } val info = mask.info @@ -391,12 +389,20 @@ internal class ShapeInterpolationTool( } onAction { event -> + val prevSlice = controller.sliceAt(currentDepth)?.also { + deleteSliceAt(currentDepth, reinterpolate = false) + } /* get value at position */ - deleteSliceOrInterpolant()?.let { prevSliceGlobalInterval -> - source.resetMasks(true) - paintera.baseView.orthogonalViews().requestRepaint(Intervals.smallestContainingInterval(prevSliceGlobalInterval)) + currentJob = fillObjectInSlice(event!!, true)?.apply { + invokeOnCompletion { cause -> + prevSlice?.maskBoundingBox?.let { interval -> + + cause?.let { + addSelection(interval, true, prevSlice.globalTransform, prevSlice.mask) + } ?: requestRepaint(prevSlice.globalBoundingBox) + } + } } - currentTask = fillObjectInSlice(event!!) } } MOUSE_CLICKED { @@ -410,7 +416,7 @@ internal class ShapeInterpolationTool( triggerByRightClick || triggerByCtrlLeftClick } onAction { event -> - currentTask = fillObjectInSlice(event!!) + currentJob = fillObjectInSlice(event!!) } } }, @@ -462,17 +468,17 @@ internal class ShapeInterpolationTool( KEY_PRESSED(CANCEL) { name = "cancel current shape interpolation tool task" filter = true - verify("No task to cancel") { currentTask != null } + verify("No task to cancel") { currentJob != null } onAction { - currentTask?.cancel() - currentTask = null + currentJob?.cancel() + currentJob = null } } } } } - private fun fillObjectInSlice(event: MouseEvent): UtilityTask<*>? { + private fun fillObjectInSlice(event: MouseEvent, replaceExistingSlice: Boolean = false): Job? { with(controller) { source.resetMasks(false) val mask = getMask() @@ -480,18 +486,18 @@ internal class ShapeInterpolationTool( /* If a current slice exists, try to preserve it if cancelled */ currentSliceMaskInterval?.also { mask.pushNewImageLayer() - fill2D.fillTaskProperty.addWithListener { obs, _, task -> - task?.let { - task.onCancelled(true) { _, _ -> + + fill2D.fillJobProperty.onceWhen(fill2D.fillJobProperty.isNotNull).subscribe { job -> + job?.invokeOnCompletion { cause -> + cause?.let { mask.popImageLayer() - mask.requestRepaint() + controller.setMaskOverlay() } - task.onEnd(true) { obs?.removeListener(this) } - } ?: obs?.removeListener(this) + } } } - fill2D.fill2D.provideMask(mask) + fill2D.fill2D.viewerMask = mask val pointInMask = mask.displayPointToMask(event.x, event.y, pointInCurrentDisplay = true) val pointInSource = pointInMask.positionAsRealPoint().also { mask.initialMaskToSourceTransform.apply(it, it) } val info = mask.info @@ -504,8 +510,8 @@ internal class ShapeInterpolationTool( fill2D.brushProperties?.brushDepth = 1.0 fill2D.fillLabel = { if (maskLabel == interpolationId) Label.TRANSPARENT else interpolationId } return fill2D.executeFill2DAction(event.x, event.y) { fillInterval -> - shapeInterpolationMode.addSelection(fillInterval)?.also { it.locked = true } - currentTask = null + shapeInterpolationMode.addSelection(fillInterval, replaceExistingSlice = replaceExistingSlice)?.also { it.locked = true } + currentJob = null fill2D.fill2D.release() } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/CommitHandler.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/CommitHandler.kt index 429eea2ae..c5bd7c624 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/CommitHandler.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/CommitHandler.kt @@ -18,6 +18,7 @@ import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts import org.slf4j.LoggerFactory import java.lang.invoke.MethodHandles import java.util.function.BiFunction +import kotlin.jvm.optionals.getOrNull class CommitHandler>(private val state: S, private val fragmentProvider: () -> FragmentSegmentAssignmentState) { @@ -82,7 +83,7 @@ class CommitHandler>(private val state: S, private val fra } } } - return buttonType?.get() + return buttonType?.getOrNull() } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendMultiScaleGroup.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendMultiScaleGroup.kt index 628358b5f..5554f79f1 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendMultiScaleGroup.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendMultiScaleGroup.kt @@ -12,6 +12,7 @@ import org.janelia.saalfeldlab.paintera.data.mask.Masks import org.janelia.saalfeldlab.paintera.data.n5.CommitCanvasN5 import org.janelia.saalfeldlab.paintera.data.n5.N5DataSource import org.janelia.saalfeldlab.paintera.id.IdService +import org.janelia.saalfeldlab.paintera.id.LocalIdService import org.janelia.saalfeldlab.paintera.paintera import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions.get @@ -90,9 +91,7 @@ class N5BackendMultiScaleGroup constructor( it, dataset, Supplier { PainteraAlerts.getN5IdServiceFromData(it, dataset, source) }) - } ?: let { - IdService.IdServiceNotProvided() - } + } ?: LocalIdService(metadataState.reader.getAttribute(dataset, "maxId", Long::class.java) ?: 0L) } override fun createLabelBlockLookup(source: DataSource) = PainteraAlerts.getLabelBlockLookupFromN5DataSource(container, dataset, source)!! diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/dialogs/create/CreateDataset.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/dialogs/create/CreateDataset.kt index de494515b..2e05a36f8 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/dialogs/create/CreateDataset.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/dialogs/create/CreateDataset.kt @@ -55,7 +55,6 @@ import org.janelia.saalfeldlab.paintera.ui.FontAwesome import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts import org.janelia.saalfeldlab.util.n5.N5Data import org.janelia.saalfeldlab.util.n5.N5Helpers -import java.io.IOException import java.nio.file.Path import java.util.* @@ -257,17 +256,24 @@ class CreateDataset(private val currentSource: Source<*>?, vararg allSources: So val container = n5Container.directoryProperty().value!!.absolutePath val dataset = dataset.value val name = nameField.text + LOG.debug { "Trying to create empty label dataset `$dataset' in container `$container'"} + var invalidCause : String? = null + if (dataset.isNullOrEmpty()) invalidCause = "Dataset not specified" + if (name.isNullOrEmpty()) invalidCause = invalidCause?.let { "$it, Name not specified" } ?: "Name not specified" + invalidCause?.let { + alertIfInvalidInput(it) + e.consume() + return@addEventFilter + } + + /* Remove Scales where downsampling factors are 1 */ + val scaleLevels = mutableListOf() + mipmapLevels.forEach { level -> + if (level.relativeDownsamplingFactors.asDoubleArray().reduce { l, r -> l * r } != 1.0) + scaleLevels.add(level) + } + try { - LOG.debug { "Trying to create empty label dataset `$dataset' in container `$container'"} - if (dataset.isNullOrEmpty()) throw IOException("Dataset not specified!") - if (name.isNullOrEmpty()) throw IOException("Name not specified!") - - /* Remove Scales where downsampling factors are 1 */ - val scaleLevels = mutableListOf() - mipmapLevels.forEach { level -> - if (level.relativeDownsamplingFactors.asDoubleArray().reduce { l, r -> l * r } != 1.0) - scaleLevels.add(level) - } N5Data.createEmptyLabelDataset( container, dataset, @@ -285,10 +291,10 @@ class CreateDataset(private val currentSource: Source<*>?, vararg allSources: So val containerState = N5ContainerState(writer) createMetadataState(containerState, dataset).ifPresent { metadataStateProp.set(it) } } - } catch (ex: IOException) { - LOG.error(ex) { "Unable to create empty dataset" } + } catch (ex : Exception) { + alertIfError(ex) e.consume() - exceptionAlert(Constants.NAME, "Unable to create new dataset: ${ex.message}", ex).show() + return@addEventFilter } } mipmapLevels.addListener { change: ListChangeListener.Change -> @@ -302,6 +308,19 @@ class CreateDataset(private val currentSource: Source<*>?, vararg allSources: So return Optional.ofNullable(metadataStateProp.get()).map { metadataState: MetadataState -> Pair(metadataState, name) } } + private fun alertIfError(ex: Exception) { + LOG.error(ex) { "Unable to create new label dataset" } + exceptionAlert(Constants.NAME, "Unable to create new label dataset: ${ex.message}", ex).show() + } + + private fun alertIfInvalidInput(reason: String) { + LOG.warn { reason } + PainteraAlerts.alert(Alert.AlertType.ERROR).apply { + headerText = "Unable to create new dataset" + contentText = reason + }.showAndWait() + } + private fun populateFrom(source: Source<*>?) { val metadataSource = when (source ) { null -> return diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/source/SourceTabs.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/source/SourceTabs.kt index b8f100941..4adab8b24 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/source/SourceTabs.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/source/SourceTabs.kt @@ -40,7 +40,7 @@ class SourceTabs(private val info: SourceInfo) { private val statePanes = FXCollections.observableArrayList().also { p -> p.addListener(ListChangeListener { - OnJFXAppThread { this.contents.children.setAll(p.map { it.pane }) } + OnJFXAppThread { this@SourceTabs.contents.children.setAll(p.map { it.pane }) } }) } private val activeSourceToggleGroup = ToggleGroup() diff --git a/src/main/resources/style/mesh-status-bar.css b/src/main/resources/style/mesh-status-bar.css new file mode 100644 index 000000000..6143f85f2 --- /dev/null +++ b/src/main/resources/style/mesh-status-bar.css @@ -0,0 +1,7 @@ +.mesh-status-bar { + -fx-accent: #FC766AFF; +} + +.mesh-status-bar:complete { + -fx-accent: #5B84B1FF; +} \ No newline at end of file diff --git a/src/test/kotlin/org/janelia/saalfeldlab/paintera/ui/SplashScreenTest.kt b/src/test/kotlin/org/janelia/saalfeldlab/paintera/ui/SplashScreenTest.kt index 3a9c3f380..33bcc54f8 100644 --- a/src/test/kotlin/org/janelia/saalfeldlab/paintera/ui/SplashScreenTest.kt +++ b/src/test/kotlin/org/janelia/saalfeldlab/paintera/ui/SplashScreenTest.kt @@ -34,17 +34,16 @@ fun main(args: Array) { class SplashScreenApp : Application() { override fun init() { - val task = Tasks.createTask { + Tasks.createTask { notifyPreloader(SplashScreenUpdateNumItemsNotification(10)) for (i in 0..10) { Thread.sleep(250) notifyPreloader(SplashScreenUpdateNotification("$i / 10")) } "Done!" - }.onEnd { + }.onEnd { _, _ -> notifyPreloader(Preloader.StateChangeNotification(Preloader.StateChangeNotification.Type.BEFORE_START)) - }.submit() - task.get() + }.get() } override fun start(primaryStage: Stage) {