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 extends String> 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) {