diff --git a/pom.xml b/pom.xml index 9b2b2c07d..13bcc78fa 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ org.janelia.saalfeldlab paintera - 1.4.4-SNAPSHOT + 1.5.0-SNAPSHOT Paintera New Era Painting and annotation tool @@ -68,7 +68,7 @@ org.janelia.saalfeldlab.paintera.Paintera Paintera paintera - 1.4.1 + 1.5.0 javafx.base,javafx.controls,javafx.fxml,javafx.media,javafx.swing,javafx.web,javafx.graphics,java.naming,java.management,java.sql UTF-8 @@ -81,7 +81,9 @@ 4.2.1 4.1.1 1.3.5 - 0.15.0 + 0.15.1 + 6.1.0 + 4.0.1 3.13.0 true @@ -185,7 +187,6 @@ net.imglib2 imglib2 - 6.1.0 net.imglib2 @@ -194,7 +195,6 @@ net.imglib2 imglib2-realtransform - 4.0.1 net.imglib2 @@ -687,7 +687,7 @@ maven-surefire-plugin 1 - -Dtestfx.robot=glass -Dglass.platform=Monocle -Dmonocle.platform=Headless -Dprism.order=sw --enable-preview + -Dtestfx.robot=glass -Dglass.platform=Monocle -Dmonocle.platform=Headless -Dprism.order=sw @@ -709,7 +709,6 @@ 21 21 - --enable-preview @@ -919,7 +918,6 @@ ${kotlin.version} - --enable-preview diff --git a/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/render/MultiResolutionRendererGeneric.java b/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/render/MultiResolutionRendererGeneric.java index be93a9e51..19d794fe1 100644 --- a/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/render/MultiResolutionRendererGeneric.java +++ b/src/main/java/org/janelia/saalfeldlab/bdv/fx/viewer/render/MultiResolutionRendererGeneric.java @@ -256,7 +256,7 @@ public interface ImageGenerator { /** * @param display The canvas that will display the images we render. * @param painterThread Thread that triggers repainting of the display. Requests for repainting are send there. - * @param screenScales Scale factors from the viewer canvas to screen images of different resolutions. A scale factor of 1 means 1 + * @param initialScreenScales Scale factors from the viewer canvas to screen images of different resolutions. A scale factor of 1 means 1 * pixel in the screen image is displayed as 1 pixel on the canvas, a scale factor of 0.5 means 1 pixel in the * screen image is displayed as 2 pixel on the canvas, etc. * @param targetRenderNanos Target rendering time in nanoseconds. The rendering time for the coarsest rendered scale should be below @@ -271,7 +271,7 @@ public interface ImageGenerator { MultiResolutionRendererGeneric( final TransformAwareRenderTargetGeneric display, final PainterThreadFx painterThread, - final double[] screenScales, + final double[] initialScreenScales, final long targetRenderNanos, final boolean doubleBuffered, final TaskExecutor renderingTaskExecutor, @@ -287,7 +287,7 @@ public interface ImageGenerator { this.painterThread = painterThread; projector = null; currentScreenScaleIndex = -1; - this.screenScales = screenScales.clone(); + this.screenScales = initialScreenScales.clone(); this.doubleBuffered = doubleBuffered; createVariables(); diff --git a/src/main/java/org/janelia/saalfeldlab/fx/ortho/OrthogonalViews.java b/src/main/java/org/janelia/saalfeldlab/fx/ortho/OrthogonalViews.java index 68b1924a3..3718b0c79 100644 --- a/src/main/java/org/janelia/saalfeldlab/fx/ortho/OrthogonalViews.java +++ b/src/main/java/org/janelia/saalfeldlab/fx/ortho/OrthogonalViews.java @@ -211,6 +211,10 @@ public void requestRepaint(final RealInterval intervalInGlobalSpace) { this.applyToAll(v -> v.requestRepaint(intervalInGlobalSpace)); } + public void drawOverlays() { + applyToAll(it -> it.getDisplay().drawOverlays()); + } + /** * {@link ViewerPanelFX#setAllSources(Collection)}} for all {@link ViewerPanelFX viewer children} (top left, top right, bottom left) * diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraConfigYaml.java b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraConfigYaml.java index ff09e7f71..f2b9bedbc 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/PainteraConfigYaml.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/PainteraConfigYaml.java @@ -1,6 +1,7 @@ package org.janelia.saalfeldlab.paintera; import com.pivovarit.function.ThrowingSupplier; +import org.janelia.saalfeldlab.paintera.config.PainteraDirectoriesConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yaml.snakeyaml.Yaml; @@ -14,50 +15,63 @@ import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; +import java.util.function.Function; import java.util.function.Supplier; public class PainteraConfigYaml { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private static final String USER_HOME = System.getProperty("user.home"); + private static final String PAINTERA_YAML = "paintera.yml"; - private static final Path PAINTERA_YAML = Paths.get(USER_HOME, ".config", "paintera.yml"); + public static T getConfig(final Supplier fallBack, final String... segments) { - public static Object getConfig(final Supplier fallBack, final String... segments) { - Object currentConfig = getConfig(); - for (final String segment : segments) { - if (!(currentConfig instanceof Map)) - return fallBack.get(); - final Map map = (Map)currentConfig; - if (!map.containsKey(segment)) + Map currentConfig = getConfig(); + + final Function getOrFallback = (config) -> { + try { + return (T) config; + } catch (ClassCastException e) { return fallBack.get(); - currentConfig = map.get(segment); + } + }; + + for (int i = 0; i < segments.length; i++) { + final String segment = segments[i]; + if (currentConfig.containsKey(segment)) { + final Object config = currentConfig.get(segment); + if (i == segments.length - 1) + return getOrFallback.apply(config); + if (config instanceof Map) + currentConfig = (Map) config; + else + return fallBack.get(); + } } - return currentConfig; + return fallBack.get(); } - // TODO should this return copy? public static Map getConfig() { - return CONFIG; + String appConfigDir; + try { + appConfigDir = Paintera.getPaintera().getProperties().getPainteraDirectoriesConfig().getAppConfigDir(); + } catch (Exception e) { + appConfigDir = PainteraDirectoriesConfig.APPLICATION_DIRECTORIES.configDir; + } + return readConfigUnchecked(Paths.get(appConfigDir, PAINTERA_YAML)); } - private static final Map CONFIG = readConfigUnchecked(); - - private static Map readConfigUnchecked() { - - return ThrowingSupplier.unchecked(PainteraConfigYaml::readConfig).get(); - } + private static Map readConfigUnchecked(Path painteraYaml) { + return ThrowingSupplier.unchecked(() -> readConfig(painteraYaml)).get(); + }; - private static Map readConfig() throws IOException { + private static Map readConfig(Path painteraYaml) throws IOException { final Yaml yaml = new Yaml(); - try (final InputStream fis = new FileInputStream(PAINTERA_YAML.toFile())) { - // TODO is this cast always safe? - // TODO make this type safe, maybe create config class - final Map data = (Map)yaml.load(fis); + try (final InputStream fis = new FileInputStream(painteraYaml.toFile())) { + final Map data = yaml.load(fis); LOG.debug("Loaded paintera info: {}", data); return data; } catch (final FileNotFoundException e) { diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuActionType.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuActionType.java index fc8c90f5d..59e36b8ae 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuActionType.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/actions/MenuActionType.java @@ -16,7 +16,8 @@ public enum MenuActionType implements ActionType { LoadProject, DetachViewer, - OpenProject; + OpenProject, + ExportSource; public static EnumSet of(final MenuActionType first, final MenuActionType... rest) { diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/CoordinateDisplayListener.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/CoordinateDisplayListener.java index 0059ca167..1384d61ef 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/CoordinateDisplayListener.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/CoordinateDisplayListener.java @@ -10,8 +10,6 @@ import java.util.function.Consumer; -import static java.util.FormatProcessor.FMT; - public class CoordinateDisplayListener { private final ViewerPanelFX viewer; @@ -93,7 +91,7 @@ public static String realPointToString(final RealPoint p) { final double d0 = p.getDoublePosition(0); final double d1 = p.getDoublePosition(1); final double d2 = p.getDoublePosition(2); - return FMT."(%8.3f\{d0}, %8.3f\{d1}, %8.3f\{d2})"; + return String.format("(%8.3f, %8.3f, %8.3f)", d0, d1, d2); } } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/TranslationController.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/TranslationController.java index 869633833..b5b24f352 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/TranslationController.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/TranslationController.java @@ -40,23 +40,32 @@ public void translate(final double dX, final double dY, final double dZ, final D manager.getTransform(globalTransform); globalToViewerTransformListener.getTransformCopy(globalToViewerTransform); - /* undo global transform, left with only scale, rotation, translation in viewer space */ - globalToViewerTransform.concatenate(globalTransform.inverse()); - globalToViewerTransform.setTranslation(0.0, 0.0, 0.0); - delta[0] = dX; delta[1] = dY; delta[2] = dZ; - globalToViewerTransform.applyInverse(delta, delta); - globalTransform.translate(delta); + translateFromViewer(globalTransform, globalToViewerTransform, delta); manager.setTransform(globalTransform, duration != null ? duration : Duration.ZERO); } } + public static void translateFromViewer( + final AffineTransform3D globalTransform, + final AffineTransform3D globalToViewerTransform, + final double[] delta + ) { + + /* undo global transform, left with only scale, rotation, translation in viewer space */ + globalToViewerTransform.concatenate(globalTransform.inverse()); + globalToViewerTransform.setTranslation(0.0, 0.0, 0.0); + + globalToViewerTransform.applyInverse(delta, delta); + globalTransform.translate(delta); + } + /*TODO: Move this to somewhere else*/ public static final class TransformTracker implements TransformListener { diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/ValueDisplayListener.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/ValueDisplayListener.java index 246f4679a..0b6d8a1d0 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/ValueDisplayListener.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/navigation/ValueDisplayListener.java @@ -11,8 +11,10 @@ import javafx.scene.input.MouseEvent; import kotlinx.coroutines.Deferred; import net.imglib2.RealRandomAccess; +import net.imglib2.Volatile; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.realtransform.RealViews; +import net.imglib2.type.label.VolatileLabelMultisetType; import net.imglib2.view.composite.Composite; import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX; import org.janelia.saalfeldlab.fx.Tasks; @@ -25,15 +27,15 @@ import java.util.function.Consumer; import java.util.function.Function; -public class ValueDisplayListener implements EventHandler, TransformListener { +public class ValueDisplayListener implements EventHandler, TransformListener { private final ViewerPanelFX viewer; private final AffineTransform3D viewerTransform = new AffineTransform3D(); private final SimpleBooleanProperty viewerTransformChanged = new SimpleBooleanProperty(); - private final ObservableValue> dataSource; - private final ObservableValue> accessBinding; + private final ObservableValue> source; + private final ObservableValue> accessBinding; private double x = -1; private double y = -1; @@ -47,16 +49,18 @@ public ValueDisplayListener( final Consumer submitValue) { this.viewer = viewer; - this.dataSource = currentSource.map(it -> (DataSource)it).when(currentSource.map(it -> it instanceof DataSource)); + this.source = currentSource.map(it -> (Source)it); this.submitValue = submitValue; this.accessBinding = Bindings.createObjectBinding(() -> { - final var source = this.dataSource.getValue(); + final var source = this.source.getValue(); + if (source == null) + return null; final int level = viewer.getState().getBestMipMapLevel(source); final var interp = interpolation.apply(source); final var affine = new AffineTransform3D(); source.getSourceTransform(0, level, affine); return RealViews.transformReal( - source.getInterpolatedDataSource( + source.getInterpolatedSource( 0, level, interp @@ -65,11 +69,22 @@ public ValueDisplayListener( ).realRandomAccess(); }, currentSource, viewer.getRenderUnit().getScreenScalesProperty(), viewerTransformChanged ); + + this.accessBinding.addListener((obs, old, newAccess) -> { + if (newAccess == null) + return; + synchronized (viewer) { + getInfo(); + } + }); } @Override public void handle(final MouseEvent e) { + if (x == e.getX() && y == e.getY()) + return; + x = e.getX(); y = e.getY(); @@ -86,13 +101,10 @@ public void transformChanged(final AffineTransform3D transform) { if (isChanged) { viewerTransform.set(transform); viewerTransformChanged.setValue(!viewerTransformChanged.getValue()); - synchronized (viewer) { - getInfo(); - } } } - private D getVal() { + private T getVal() { final var access = accessBinding.getValue(); access.setPosition(x, 0); @@ -102,11 +114,11 @@ private D getVal() { return access.get(); } - private final Map, Deferred> taskMap = new HashMap<>(); + private final Map, Deferred> taskMap = new HashMap<>(); private void getInfo() { - final DataSource source = dataSource.getValue(); + final Source source = this.source.getValue(); final var job = Tasks.createTask(() -> stringConverterFromSource(source).apply(getVal())) .onSuccess(result -> Platform.runLater(() -> submitValue.accept(result))) @@ -122,14 +134,14 @@ private void getInfo() { job.start(); } - private static Function stringConverterFromSource(final DataSource source) { + private static Function stringConverterFromSource(final Source source) { if (source instanceof ChannelDataSource) { final long numChannels = ((ChannelDataSource)source).numChannels(); // Cast not actually redundant //noinspection unchecked,RedundantCast - return (Function)(Function, String>)comp -> { + return (Function)(Function, String>)comp -> { StringBuilder sb = new StringBuilder("("); if (numChannels > 0) sb.append(comp.get(0).toString()); @@ -142,12 +154,13 @@ private static Function stringConverterFromSource(final DataSourc return sb.toString(); }; } - return stringConverter(source.getDataType()); + return stringConverter(source.getType()); } - private static Function stringConverter(final D d) { - // TODO are we ever going to need anything other than toString? - return D::toString; + private static Function stringConverter(final T t) { + if (t instanceof Volatile) + return it -> ((Volatile)it).get().toString(); + return T::toString; } } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/IntersectPainting.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/IntersectPainting.java index 591a35565..0e41d13fd 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/IntersectPainting.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/IntersectPainting.java @@ -1,7 +1,5 @@ package org.janelia.saalfeldlab.paintera.control.paint; -import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX; -import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerState; import bdv.viewer.Source; import gnu.trove.list.TLongList; import gnu.trove.list.array.TLongArrayList; @@ -23,13 +21,16 @@ import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.type.Type; import net.imglib2.type.label.Label; +import net.imglib2.type.label.LabelMultisetEntry; import net.imglib2.type.label.LabelMultisetType; 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.AccessBoxRandomAccessible; import net.imglib2.util.Pair; import net.imglib2.view.Views; +import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX; +import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerState; +import org.janelia.saalfeldlab.net.imglib2.util.AccessBoxRandomAccessible; import org.janelia.saalfeldlab.paintera.data.mask.MaskInfo; import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource; import org.janelia.saalfeldlab.paintera.data.mask.SourceMask; @@ -92,24 +93,24 @@ public void intersectAt(final double x, final double y) { final Source currentSource = sourceInfo.currentSourceProperty().get(); final ViewerState viewerState = viewer.getState(); if (currentSource == null) { - LOG.info("No current source selected -- will not fill"); + LOG.info("No current source selected -- will not intersect"); return; } final SourceState currentSourceState = sourceInfo.getState(currentSource); if (!currentSourceState.isVisibleProperty().get()) { - LOG.info("Selected source is not visible -- will not fill"); + LOG.info("Selected source is not visible -- will not intersect"); return; } if (!(currentSource instanceof MaskedSource)) { - LOG.info("Selected source is not painting-enabled -- will not fill"); + LOG.info("Selected source is not painting-enabled -- will not intersect"); return; } if (maskForLabel == null) { - LOG.info("Cannot generate boolean mask for this source -- will not fill"); + LOG.info("Cannot generate boolean mask for this source -- will not intersect"); return; } @@ -118,7 +119,7 @@ public void intersectAt(final double x, final double y) { final Type t = source.getDataType(); if (!(t instanceof IntegerType)) { - LOG.info("Data type is not integer type or LabelMultisetType -- will not fill"); + LOG.info("Data type is not integer type or LabelMultisetType -- will not intersect"); return; } @@ -271,12 +272,13 @@ private static void intersectWithLabelMultisetTypeAt( Views.extendValue(canvas, new UnsignedLongType(Label.INVALID)) ); + var ref = new LabelMultisetEntry(); intersectAt( paired, accessTracker, seed, new DiamondShape(1), - bg -> bg.contains(backgroundSeedLabel), + bg -> bg.argMax() == backgroundSeedLabel || bg.contains(backgroundSeedLabel, ref), cv -> cv.valueEquals(paintedLabel) ); @@ -350,7 +352,11 @@ private static , U extends IntegerType> void interse if (maskVal.valueEquals(unvisited) && canvasFilter.test(canvasVal)) { // If background is same as at seed, mark mask with canvas label, else with the background label. final T backgroundVal = backgroundAndCanvas.getA(); - final long label = backgroundFilter.test(backgroundVal) ? canvasVal.getIntegerLong() : backgroundVal.getIntegerLong(); + final long label = backgroundFilter.test(backgroundVal) + ? canvasVal.getIntegerLong() + : backgroundVal.getIntegerLong() == Label.INVALID + ? 0L + : backgroundVal.getIntegerLong(); maskVal.setInteger(label); for (int d = 0; d < n; ++d) { coordinates[d].add(neighborhoodCursor.getLongPosition(d)); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/SelectNextId.java b/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/SelectNextId.java deleted file mode 100644 index 29851522a..000000000 --- a/src/main/java/org/janelia/saalfeldlab/paintera/control/paint/SelectNextId.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.janelia.saalfeldlab.paintera.control.paint; - -import org.janelia.saalfeldlab.paintera.control.selection.SelectedIds; -import org.janelia.saalfeldlab.paintera.id.IdService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.invoke.MethodHandles; -import java.util.function.BiConsumer; - -public class SelectNextId { - - private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - private final IdService idService; - - private final SelectedIds selectedIds; - - public SelectNextId(final IdService idService, final SelectedIds selectedIds) { - - super(); - this.idService = idService; - this.selectedIds = selectedIds; - } - - public long getNextId() { - - return getNextId(SelectedIds::activate); - } - - public long getNextId(final BiConsumer action) { - - long next = idService.next(); - action.accept(selectedIds, next); - return next; - } - -} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/data/n5/CommitCanvasN5.java b/src/main/java/org/janelia/saalfeldlab/paintera/data/n5/CommitCanvasN5.java index 2e0e7ef76..5c7441053 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/data/n5/CommitCanvasN5.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/data/n5/CommitCanvasN5.java @@ -122,10 +122,10 @@ public boolean supportsLabelBlockLookupUpdate() { @Override public void updateLabelBlockLookup(final List> blockDiffsByLevel) throws UnableToUpdateLabelBlockLookup { - LOG.debug(() -> STR."Updating label block lookup with \{blockDiffsByLevel}"); + LOG.debug(() -> "Updating label block lookup with " + blockDiffsByLevel); try { - final String uniqueLabelsPath = STR."\{dataset()}/unique-labels"; - LOG.debug(() -> STR."uniqueLabelsPath \{uniqueLabelsPath}"); + final String uniqueLabelsPath = "%s/unique-labels".formatted(dataset()); + LOG.debug(() -> "uniqueLabelsPath %s".formatted(uniqueLabelsPath)); final LabelBlockLookup labelBlockLoader; try { @@ -136,9 +136,9 @@ public void updateLabelBlockLookup(final List> blockDi final String[] scaleUniqueLabels = N5Helpers.listAndSortScaleDatasets(getN5(), uniqueLabelsPath); - LOG.debug(() -> STR."Found scale datasets \{scaleUniqueLabels}"); + LOG.debug(() -> "Found scale datasets %s".formatted(scaleUniqueLabels)); for (int level = 0; level < scaleUniqueLabels.length; ++level) { - final String uniqueLabelScalePath = N5URI.normalizeGroupPath(STR."\{uniqueLabelsPath}/\{scaleUniqueLabels[level]}"); + final String uniqueLabelScalePath = N5URI.normalizeGroupPath("%s/%s".formatted(uniqueLabelsPath, scaleUniqueLabels[level])); final DatasetSpec datasetUniqueLabels = DatasetSpec.of(getN5(), uniqueLabelScalePath); final TLongObjectMap removedById = new TLongObjectHashMap<>(); final TLongObjectMap addedById = new TLongObjectHashMap<>(); @@ -152,7 +152,7 @@ public void updateLabelBlockLookup(final List> blockDi blockSpec.fromLinearIndex(blockId); - LOG.trace(() -> STR."Unique labels for block (\{blockId}: \{blockSpec.min} \{blockSpec.max}): \{blockDiff}"); + LOG.trace(() -> "Unique labels for block (%d: %s %s): %s".formatted(blockId, blockSpec.min, blockSpec.max, blockDiff)); getN5().writeBlock( datasetUniqueLabels.dataset, @@ -178,8 +178,8 @@ public void updateLabelBlockLookup(final List> blockDi final TLongSet modifiedIds = new TLongHashSet(); modifiedIds.addAll(removedById.keySet()); modifiedIds.addAll(addedById.keySet()); - LOG.debug(() -> STR."Removed by id: \{removedById}"); - LOG.debug(() -> STR."Added by id: \{addedById}"); + LOG.debug(() -> "Removed by id: " + removedById); + LOG.debug(() -> "Added by id: " + addedById); for (final long modifiedId : modifiedIds.toArray()) { final Interval[] blockList = labelBlockLoader.read(new LabelBlockLookupKey(level, modifiedId)); final TLongSet blockListLinearIndices = new TLongHashSet(); @@ -191,8 +191,8 @@ public void updateLabelBlockLookup(final List> blockDi final TLongSet removed = removedById.get(modifiedId); final TLongSet added = addedById.get(modifiedId); - LOG.debug(() -> STR."Removed for id \{modifiedId}: \{removed}"); - LOG.debug(() -> STR."Added for id \{modifiedId}: \{added}"); + LOG.debug(() -> "Removed for id %d: %s".formatted(modifiedId, removed)); + LOG.debug(() -> "Added for id %d: %s".formatted(modifiedId, added)); if (removed != null) blockListLinearIndices.removeAll(removed); @@ -207,7 +207,7 @@ public void updateLabelBlockLookup(final List> blockDi blockSpec.fromLinearIndex(blockId); final Interval interval = blockSpec.asInterval(); updatedIntervals[index] = interval; - LOG.trace(() -> STR."Added interval \{interval} for linear index \{blockId} and block spec \{blockSpec}"); + LOG.trace(() -> "Added interval %s for linear index %d and block spec %s".formatted(interval, blockId, blockSpec)); } labelBlockLoader.write(new LabelBlockLookupKey(level, modifiedId), updatedIntervals); } @@ -216,7 +216,7 @@ public void updateLabelBlockLookup(final List> blockDi } catch (final IOException e) { LOG.error(e, () -> null); - throw new UnableToUpdateLabelBlockLookup(STR."Unable to update label block lookup for \{dataset()}", e); + throw new UnableToUpdateLabelBlockLookup("Unable to update label block lookup for %s".formatted(dataset()), e); } LOG.info(() -> "Finished updating label-block-lookup"); } @@ -224,8 +224,8 @@ public void updateLabelBlockLookup(final List> blockDi @Override public List> persistCanvas(final CachedCellImg canvas, final long[] blocks) throws UnableToPersistCanvas { - LOG.info(() -> STR."Committing canvas: \{blocks.length} blocks"); - LOG.debug(() -> STR."Affected blocks in grid \{canvas.getCellGrid()}: \{blocks}"); + LOG.info(() -> "Committing canvas: %d blocks".formatted(blocks.length)); + LOG.debug(() -> "Affected blocks in grid %s: %s".formatted(canvas.getCellGrid(), blocks)); InvokeOnJavaFXApplicationThread.invoke(() -> progress.set(0.1)); try { final String datasetPath = getDatasetPath(); @@ -240,7 +240,7 @@ public List> persistCanvas(final CachedCellImg STR."Persisting canvas with grid=\{canvasGrid} into background with grid=\{highestResolutionDataset.grid}"); + LOG.debug(() -> "Persisting canvas with grid=%s into background with grid=%s".formatted(canvasGrid, highestResolutionDataset.grid)); final List> blockDiffs = new ArrayList<>(); final TLongObjectHashMap blockDiffsAtHighestLevel = new TLongObjectHashMap<>(); @@ -265,9 +265,9 @@ public List> persistCanvas(final CachedCellImg> persistCanvas(final CachedCellImg STR."Affected blocks at higher targetLevel: \{affectedLowResBlocks}"); + LOG.debug(() -> "Affected blocks at higher targetLevel: %s".formatted(affectedLowResBlocks)); final Scale3D targetToPrevious = new Scale3D(relativeDownsamplingFactors); @@ -287,7 +287,7 @@ public List> persistCanvas(final CachedCellImg STR."targetLevel=\{finalTargetLevel}: Got \{affectedLowResBlocks.length} blocks"); + LOG.debug(() -> "targetLevel=%d: Got %d blocks".formatted(finalTargetLevel, affectedLowResBlocks.length)); final BlockSpec targetBlockSpec = new BlockSpec(targetDataset.grid); @@ -331,7 +331,7 @@ public List> persistCanvas(final CachedCellImg STR."Checking if dataset \{dataset} is label multiset type."); + LOG.debug(() -> "Checking if dataset %s is label multiset type.".formatted(dataset)); if (!N5Helpers.getBooleanAttribute(n5, dataset, N5Helpers.LABEL_MULTISETTYPE_KEY, false)) { throw new RuntimeException("Only label multiset type accepted currently!"); } @@ -406,7 +406,7 @@ private static void checkGridsCompatibleOrFail( ) { if (!highestResolutionGrid.equals(canvasGrid)) { - final RuntimeException error = new RuntimeException(STR."Canvas grid \{canvasGrid} and highest resolution dataset grid \{highestResolutionGrid} incompatible!"); + final RuntimeException error = new RuntimeException("Canvas grid %s and highest resolution dataset grid %s incompatible!".formatted(canvasGrid, highestResolutionGrid)); LOG.error(error, () -> null); throw error; } @@ -460,7 +460,7 @@ private static & NativeType> BlockDiff downsampleIn i.setInteger(Label.OUTSIDE); final RandomAccessibleInterval input = Views.isZeroMin(data) ? data : Views.zeroMin(data); final RandomAccessibleInterval output = new ArrayImgFactory<>(i).create(size); - WinnerTakesAll.downsample(input, output, relativeFactors); + WinnerTakesAll.downsample(Views.extendMirrorDouble(input), output, relativeFactors); final RandomAccessibleInterval previousContents = Views.offsetInterval(N5Utils.open(n5, dataset), blockInterval); final BlockDiff blockDiff = createBlockDiffInteger(previousContents, output); @@ -707,7 +707,6 @@ private static & NativeType> void writeBlocksLabelI } } - // TODO: switch to N5LabelMultisets for writing label multiset data private static void downsampleAndWriteBlocksLabelMultisetType( final long[] affectedTargetBlocks, final N5Writer n5, @@ -749,7 +748,7 @@ private static void downsampleAndWriteBlocksLabelMultisetType( targetToPrevious.apply(realSourceMin, realSourceMin); targetToPrevious.apply(realSourceMax, realSourceMax); - LOG.debug(() -> STR."level=\{level}: realSourceMin=\{realSourceMin} realSourceMax=\{realSourceMax}"); + LOG.debug(() -> "level=%d: realSourceMin=%s realSourceMax=%s".formatted(level, realSourceMin, realSourceMax)); final long[] sourceMin = ArrayMath.minOf3(ArrayMath.asLong3(ArrayMath.floor3(realSourceMin, realSourceMin)), sourceDataset.dimensions); final long[] sourceMax = ArrayMath.minOf3(ArrayMath.asLong3(ArrayMath.ceil3(realSourceMax, realSourceMax)), sourceDataset.dimensions); @@ -763,7 +762,7 @@ private static void downsampleAndWriteBlocksLabelMultisetType( ArrayMath.add3(sourceMax, -1, sourceMax); ArrayMath.minOf3(sourceMax, sourceMin, sourceMax); - LOG.trace(() -> STR."Reading existing access at position \{blockSpecCopy.pos} and size \{size}. (\{blockSpecCopy.min} \{blockSpecCopy.max})"); + LOG.trace(() -> "Reading existing access at position %s and size %s. (%s %s)".formatted(blockSpecCopy.pos, size, blockSpecCopy.min, blockSpecCopy.max)); final DataBlock block = n5.readBlock(targetDataset.dataset, targetDataset.attributes, blockSpecCopy.pos); final VolatileLabelMultisetArray oldAccess = block != null && block.getData() instanceof byte[] ? LabelUtils.fromBytes((byte[])block.getData(), (int)Intervals.numElements(size)) @@ -827,7 +826,7 @@ private static & NativeType> void downsampleAndWrit targetToPrevious.apply(blockMinDouble, blockMinDouble); targetToPrevious.apply(blockMaxDouble, blockMaxDouble); - LOG.debug(() -> STR."level=\{level}: blockMinDouble=\{blockMinDouble} blockMaxDouble=\{blockMaxDouble}"); + LOG.debug(() -> "level=%d: blockMinDouble=%s blockMaxDouble=%s".formatted(level, blockMinDouble, blockMaxDouble)); final long[] blockMin = ArrayMath.minOf3(ArrayMath.asLong3(ArrayMath.floor3(blockMinDouble, blockMinDouble)), previousDataset.dimensions); final long[] blockMax = ArrayMath.minOf3(ArrayMath.asLong3(ArrayMath.ceil3(blockMaxDouble, blockMaxDouble)), previousDataset.dimensions); @@ -842,7 +841,7 @@ private static & NativeType> void downsampleAndWrit ArrayMath.add3(blockMax, -1, blockMax); ArrayMath.minOf3(blockMax, blockMin, blockMax); - LOG.trace(() -> STR."Reading old access at position \{blockSpec.pos} and size \{size}. (\{blockSpec.min} \{blockSpec.max})"); + LOG.trace(() -> "Reading old access at position %s and size %s. (%s %s)".formatted(blockSpec.pos, size, blockSpec.min, blockSpec.max)); final BlockDiff blockDiff = downsampleIntegerTypeAndSerialize( n5, diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/data/n5/N5DataSource.java b/src/main/java/org/janelia/saalfeldlab/paintera/data/n5/N5DataSource.java index c88e5727b..0e3a554c0 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/data/n5/N5DataSource.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/data/n5/N5DataSource.java @@ -2,17 +2,24 @@ import bdv.cache.SharedQueue; import bdv.viewer.Interpolation; +import net.imglib2.FinalRealInterval; +import net.imglib2.Interval; import net.imglib2.RandomAccessible; +import net.imglib2.RandomAccessibleInterval; import net.imglib2.Volatile; import net.imglib2.interpolation.InterpolatorFactory; import net.imglib2.interpolation.randomaccess.NLinearInterpolatorFactory; import net.imglib2.interpolation.randomaccess.NearestNeighborInterpolatorFactory; +import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.type.NativeType; import net.imglib2.type.numeric.RealType; +import net.imglib2.util.Intervals; +import net.imglib2.view.Views; import org.janelia.saalfeldlab.n5.N5Reader; import org.janelia.saalfeldlab.n5.N5Writer; import org.janelia.saalfeldlab.paintera.data.RandomAccessibleIntervalDataSource; import org.janelia.saalfeldlab.paintera.state.metadata.MetadataState; +import org.janelia.saalfeldlab.paintera.state.metadata.MultiScaleMetadataState; import java.io.IOException; import java.util.function.Function; @@ -54,6 +61,37 @@ public N5DataSource( this.metadataState = metadataState; } + @Override public RandomAccessibleInterval getSource(int t, int level) { + + final Interval virtualCrop = getVirtualCrop(t, level); + if (virtualCrop == null) { + return super.getSource(t, level); + } + return Views.interval(super.getSource(t, level), virtualCrop); + } + + @Override public RandomAccessibleInterval getDataSource(int t, int level) { + + final Interval virtualCrop = getVirtualCrop(t, level); + if (virtualCrop == null) { + return super.getDataSource(t, level); + } + return Views.interval(super.getDataSource(t, level), virtualCrop); + } + + private Interval getVirtualCrop(int t, int level) { + + var virtualCrop = metadataState.getVirtualCrop(); + if (virtualCrop == null) return null; + else if (level == 0) return virtualCrop; + + MultiScaleMetadataState state = (MultiScaleMetadataState)metadataState; + final AffineTransform3D[] transforms = state.getScaleTransforms(); + + final FinalRealInterval realVirtualCropForLevel = transforms[0].copy().concatenate(transforms[level].inverse()).estimateBounds(virtualCrop); + return Intervals.smallestContainingInterval(realVirtualCropForLevel); + } + public MetadataState getMetadataState() { return metadataState; diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/id/IdService.java b/src/main/java/org/janelia/saalfeldlab/paintera/id/IdService.java index 2d8c2d997..253284638 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/id/IdService.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/id/IdService.java @@ -3,6 +3,7 @@ import net.imglib2.type.label.Label; import org.checkerframework.checker.units.qual.A; +import java.util.Iterator; import java.util.Random; import java.util.stream.LongStream; @@ -11,7 +12,7 @@ public interface IdService { // Labels in this should not be persisted, other than the reserved values above long FIRST_TEMPORARY_ID = 0xfff0ffffffffffffL; long LAST_TEMPORARY_ID = 0xfff1ffffffffffffL; - LongStream randomTemps = new Random().longs(FIRST_TEMPORARY_ID, LAST_TEMPORARY_ID); + Iterator randomTemps = new Random().longs(FIRST_TEMPORARY_ID, LAST_TEMPORARY_ID).iterator(); /** @@ -102,7 +103,7 @@ static public long max(final long[] ids) { } static long randomTemporaryId() { - return randomTemps.findFirst().getAsLong(); + return randomTemps.next(); } static boolean isTemporary(final long id) { diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/id/LocalIdService.java b/src/main/java/org/janelia/saalfeldlab/paintera/id/LocalIdService.java index 5310a2d98..062b432c1 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/id/LocalIdService.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/id/LocalIdService.java @@ -9,7 +9,12 @@ public class LocalIdService implements IdService { public LocalIdService() { - this(0); + this(1); + } + + public LocalIdService(final long next) { + + this.next = next; } @Override public long nextTemporary() { @@ -26,11 +31,6 @@ public LocalIdService() { return tempIds; } - public LocalIdService(final long maxId) { - - this.next = maxId; - } - @Override public synchronized void invalidate(final long id) { diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/SegmentMaskGenerators.java b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/SegmentMaskGenerators.java index fa38c1a15..a19a0beb8 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/SegmentMaskGenerators.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/meshes/cache/SegmentMaskGenerators.java @@ -159,7 +159,6 @@ public void convert(final LabelMultisetType input, final B output) { } final var validLabelsContainedCount = new AtomicLong(0); if (validLabelsSize < inputSize) { - final var ref = new LabelMultisetEntry(); final var breakEarly = !validLabels.forEach(label -> { final var count = validLabelsContainedCount.addAndGet(input.countWithRef(label, reusableReference)); if (count >= minNumRequiredPixels) { diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/serialization/RealIntervalSerializer.java b/src/main/java/org/janelia/saalfeldlab/paintera/serialization/RealIntervalSerializer.java new file mode 100644 index 000000000..667201bbf --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/paintera/serialization/RealIntervalSerializer.java @@ -0,0 +1,87 @@ +package org.janelia.saalfeldlab.paintera.serialization; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import jnr.ffi.annotations.In; +import net.imglib2.FinalInterval; +import net.imglib2.FinalRealInterval; +import net.imglib2.Interval; +import net.imglib2.RealInterval; +import org.scijava.plugin.Plugin; + +import java.lang.reflect.Type; + +@Plugin(type = PainteraSerialization.PainteraAdapter.class) +public class RealIntervalSerializer implements PainteraSerialization.PainteraAdapter { + + @Override + public RealInterval deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) + throws JsonParseException { + + final JsonObject intervalObj = json.getAsJsonObject(); + final JsonArray min = intervalObj.get("min").getAsJsonArray(); + final JsonArray max = intervalObj.get("max").getAsJsonArray(); + try { + return deserializeToInterval(min, max); + } catch (NumberFormatException e) { + return deserializeToRealInterval(min, max); + } + } + + private Interval deserializeToInterval(JsonArray min, JsonArray max) { + + final int size = min.size(); + final long[] intervalMin = new long[size]; + final long[] intervalMax = new long[size]; + + for (int i = 0; i < size; i++) { + intervalMin[i] = min.get(i).getAsLong(); + intervalMax[i] = max.get(i).getAsLong(); + } + return new FinalInterval(intervalMin, intervalMax); + } + + private RealInterval deserializeToRealInterval(JsonArray min, JsonArray max) { + + final int size = min.size(); + final double[] intervalMin = new double[size]; + final double[] intervalMax = new double[size]; + + for (int i = 0; i < size; i++) { + intervalMin[i] = min.get(i).getAsDouble(); + intervalMax[i] = max.get(i).getAsDouble(); + } + return new FinalRealInterval(intervalMin, intervalMax); + } + + @Override + public JsonElement serialize(final RealInterval src, final Type typeOfSrc, final JsonSerializationContext context) { + + final JsonObject interval = new JsonObject(); + if (src instanceof Interval) { + interval.add("min", context.serialize(((Interval)src).minAsLongArray())); + interval.add("max", context.serialize(((Interval)src).maxAsLongArray())); + } else { + interval.add("min", context.serialize(src.minAsDoubleArray())); + interval.add("max", context.serialize(src.maxAsDoubleArray())); + } + return interval; + } + + @Override + public Class getTargetClass() { + + return RealInterval.class; + } + + @Override + public boolean isHierarchyAdapter() { + + return true; + } +} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/state/ThresholdingSourceState.java b/src/main/java/org/janelia/saalfeldlab/paintera/state/ThresholdingSourceState.java index 2aae20965..838f5d72f 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/state/ThresholdingSourceState.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/state/ThresholdingSourceState.java @@ -124,6 +124,9 @@ public ThresholdingSourceState(final String name, final SourceState toBeTh viewer.getMeshManagerExecutorService(), viewer.getMeshWorkerExecutorService(), new MeshViewUpdateQueue<>()); + + /*Threshold Meshes turned off by default */ + this.meshes.getManagedSettings().getMeshesEnabledProperty().set(false); } private void updateThreshold() { diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterLabelMultisetType.java b/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterLabelMultisetType.java index 1bcdb7602..dcc669585 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterLabelMultisetType.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/stream/HighlightingStreamConverterLabelMultisetType.java @@ -24,7 +24,8 @@ public void convert(final VolatileLabelMultisetType input, final ARGBType output final var entries = input.get().entrySet(); final int numEntries = entries.size(); if (numEntries == 0) { - output.set(stream.argb(Label.INVALID)); + final long emptyValue = 0; + output.set(stream.argb(emptyValue)); } else { double a = 0; double r = 0; diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/VolatileHelpers.java b/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/VolatileHelpers.java index 9fbb2ae7e..1e2f621ec 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/VolatileHelpers.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/VolatileHelpers.java @@ -4,15 +4,12 @@ import net.imglib2.img.cell.Cell; import net.imglib2.img.cell.CellGrid; import net.imglib2.type.label.Label; -import net.imglib2.type.label.LabelMultisetEntry; -import net.imglib2.type.label.LabelMultisetEntryList; -import net.imglib2.type.label.LongMappedAccessData; import net.imglib2.type.label.VolatileLabelMultisetArray; -import net.imglib2.util.Intervals; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.invoke.MethodHandles; +import java.util.Random; public class VolatileHelpers { @@ -38,23 +35,10 @@ public Cell createInvalid(final Long key) throws Exc final int[] cellDims = new int[cellPosition.length]; grid.getCellDimensions(cellPosition, cellMin, cellDims); - final LabelMultisetEntry e = new LabelMultisetEntry(Label.INVALID, 1); - final int numEntities = (int)Intervals.numElements(cellDims); - - final LongMappedAccessData listData = LongMappedAccessData.factory.createStorage(32); - final LabelMultisetEntryList list = new LabelMultisetEntryList(listData, 0); - list.createListAt(listData, 0); - list.add(e); - final int[] data = new int[numEntities]; - final VolatileLabelMultisetArray array = new VolatileLabelMultisetArray( - data, - listData, - false, - new long[]{Label.INVALID} - ); - return new Cell<>(cellDims, cellMin, array); + return new Cell<>(cellDims, cellMin, EMPTY_ACCESS); } - } + private static final VolatileLabelMultisetArray EMPTY_ACCESS = new VolatileLabelMultisetArray(0, true, new long[]{Label.INVALID}); + } diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/menu/n5/GenericBackendDialogN5.java b/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/menu/n5/GenericBackendDialogN5.java index ecfc61a8f..062f9e1fa 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/menu/n5/GenericBackendDialogN5.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/menu/n5/GenericBackendDialogN5.java @@ -7,10 +7,8 @@ import javafx.beans.binding.StringBinding; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; -import javafx.beans.property.MapProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleMapProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableObjectValue; import javafx.beans.value.ObservableStringValue; @@ -36,7 +34,6 @@ import org.janelia.saalfeldlab.fx.ui.MatchSelectionMenuButton; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.N5Reader; -import org.janelia.saalfeldlab.n5.N5Writer; import org.janelia.saalfeldlab.n5.universe.N5TreeNode; import org.janelia.saalfeldlab.n5.universe.metadata.N5Metadata; import org.janelia.saalfeldlab.n5.universe.metadata.SpatialMetadata; @@ -97,10 +94,6 @@ public class GenericBackendDialogN5 implements Closeable { private final SimpleObjectProperty containerState = new SimpleObjectProperty<>(); - private final ObjectBinding sourceWriter = Bindings.createObjectBinding( - () -> containerState.isNotNull().get() ? containerState.get().getWriter() : null, - containerState); - private final BooleanBinding isContainerValid = containerState.isNotNull(); private final ObjectProperty activeN5Node = new SimpleObjectProperty<>(); @@ -169,38 +162,32 @@ public class GenericBackendDialogN5 implements Closeable { return entries[entries.length - 1]; }, activeN5Node); - private final MapProperty datasetChoices = new SimpleMapProperty<>(); + private final ObservableMap datasetChoices = FXCollections.synchronizedObservableMap(FXCollections.observableMap(new HashMap<>())); static final HashMap> previousContainerChoices = new HashMap<>(); - private final String identifier; - private final Node node; public GenericBackendDialogN5( final Node n5RootNode, final Node browseNode, - final String identifier, final ObservableValue containerState, final BooleanProperty isOpeningContainer) { - this("_Dataset", n5RootNode, browseNode, identifier, containerState, isOpeningContainer); + this("_Dataset", n5RootNode, browseNode, containerState, isOpeningContainer); } public GenericBackendDialogN5( final String datasetPrompt, final Node n5RootNode, final Node browseNode, - final String identifier, final ObservableValue containerState, final BooleanProperty isOpeningContainer) { - this.identifier = identifier; this.isBusy = Bindings.createBooleanBinding(() -> isOpeningContainer.get() || discoveryIsActive().get(), isOpeningContainer, discoveryIsActive); this.containerState.bind(containerState); - this.isContainerValid.addListener((obs, oldv, newv) -> datasetUpdateFailed.set(false)); - this.isContainerValid.addListener((obs, oldv, newv) -> datasetUpdateFailed.set(false)); + this.isContainerValid.subscribe(() -> datasetUpdateFailed.set(false)); this.node = initializeNode(n5RootNode, datasetPrompt, browseNode); @@ -211,7 +198,7 @@ public GenericBackendDialogN5( /* otherwise, clear the existing choices, reset the active node*/ invoke(() -> { - resetDatasetChoices(); + setDatasetChoices(null); this.activeN5Node.set(null); }); @@ -248,6 +235,10 @@ public GenericBackendDialogN5( } + public ObservableValue getVisibleProperty() { + return node.visibleProperty(); + } + public static double[] asPrimitiveArray(final DoubleProperty[] data) { return Arrays.stream(data).mapToDouble(DoubleProperty::get).toArray(); @@ -271,9 +262,8 @@ private void updateDatasetChoices(Map choices) { cancelDiscovery(); // If discovery is ongoing, cancel it. LOG.debug("Updating dataset choices!"); discoveryIsActive.set(true); - resetDatasetChoices(); // clean up whatever is currently shown mapRootToContainerName(choices); - datasetChoices.set(FXCollections.synchronizedObservableMap(FXCollections.observableMap(choices))); + setDatasetChoices(choices); discoveryIsActive.set(false); }); } @@ -295,7 +285,7 @@ private void updateDatasetChoices(N5Reader newReader) { cancelDiscovery(); // If discovery is ongoing, cancel it. LOG.debug("Updating dataset choices!"); discoveryIsActive.set(true); - invoke(this::resetDatasetChoices); // clean up whatever is currently shown + setDatasetChoices(null); }); Tasks.createTask(() -> { /* Parse the container's metadata*/ @@ -321,15 +311,20 @@ private void updateDatasetChoices(N5Reader newReader) { } return validDatasetChoices; }).onSuccess(result -> { - datasetChoices.set(mapRootToContainerName(result)); - previousContainerChoices.put(getContainer(), Map.copyOf(datasetChoices.getValue())); + invoke(() -> { + mapRootToContainerName(result); + setDatasetChoices(result); + previousContainerChoices.put(getContainer(), new HashMap<>(datasetChoices)); + }); }).onEnd((result, error) -> invoke(() -> discoveryIsActive.set(false))); } } - private void resetDatasetChoices() { + private void setDatasetChoices(Map result) { - datasetChoices.set(FXCollections.observableHashMap()); + datasetChoices.clear(); + if (result != null) + datasetChoices.putAll(result); } public void cancelDiscovery() { @@ -463,7 +458,8 @@ private MenuButton createDatasetDropdownMenu(String datasetPromptText) { datasetDropDown.disableProperty().bind(datasetDropDownDisable); datasetDropDown.textProperty().bind(datasetDropDownText); /* If the datasetchoices are changed, create new menuItems, and update*/ - datasetChoices.addListener((obs, oldv, newv) -> { + + datasetChoices.subscribe(() -> { choices.setAll(datasetChoices.keySet()); choices.sort(new AlphanumericComparator()); }); @@ -475,11 +471,6 @@ public ObservableStringValue nameProperty() { return name; } - public String identifier() { - - return identifier; - } - public & NativeType, V extends AbstractVolatileRealType & NativeType> List, VolatileWithSet>>> getChannels( final String name, 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 deleted file mode 100644 index e83c7660c..000000000 --- a/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/menu/n5/N5FactoryOpener.java +++ /dev/null @@ -1,226 +0,0 @@ -package org.janelia.saalfeldlab.paintera.ui.dialogs.opendialog.menu.n5; - -import com.google.common.collect.Lists; -import com.pivovarit.function.ThrowingSupplier; -import javafx.beans.binding.Bindings; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; -import javafx.beans.value.ObservableValue; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; -import javafx.scene.control.MenuButton; -import javafx.scene.control.TextField; -import javafx.scene.control.Tooltip; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.stage.DirectoryChooser; -import javafx.stage.FileChooser; -import javafx.stage.Window; -import org.janelia.saalfeldlab.fx.Tasks; -import org.janelia.saalfeldlab.fx.ui.ObjectField; -import org.janelia.saalfeldlab.n5.N5Reader; -import org.janelia.saalfeldlab.n5.hdf5.N5HDF5Reader; -import org.janelia.saalfeldlab.paintera.Paintera; -import org.janelia.saalfeldlab.paintera.PainteraConfigYaml; -import org.janelia.saalfeldlab.paintera.state.metadata.N5ContainerState; -import org.janelia.saalfeldlab.util.PainteraCache; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.lang.invoke.MethodHandles; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Optional; - -import static org.janelia.saalfeldlab.fx.ui.ObjectField.SubmitOn.*; -import static org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread.invoke; - -public class N5FactoryOpener { - - private static final String DEFAULT_DIRECTORY = (String)PainteraConfigYaml.getConfig( - () -> PainteraConfigYaml.getConfig(() -> null, "data", "defaultDirectory"), - "data", "n5", "defaultDirectory" - ); - - private static final List FAVORITES = Collections.unmodifiableList( - (List)PainteraConfigYaml.getConfig(ArrayList::new, "data", "n5", "favorites")); - - private static final String[] H5_EXTENSIONS = {"*.h5", "*.hdf", "*.hdf5"}; - - private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private static final HashMap n5ContainerStateCache = new HashMap<>(); - - private final StringProperty selectionProperty = new SimpleStringProperty(); - private final ObjectProperty containerState = new SimpleObjectProperty<>(); - private BooleanProperty isOpeningContainer = new SimpleBooleanProperty(false); - - { - selectionProperty.addListener(this::selectionChanged); - Optional.ofNullable(DEFAULT_DIRECTORY).ifPresent(defaultDir -> { - selectionProperty.set(ThrowingSupplier.unchecked(Paths.get(defaultDir)::toRealPath).get().toString()); - }); - } - - public GenericBackendDialogN5 backendDialog() { - - final ObjectField containerField = ObjectField.stringField(selectionProperty.get(), ENTER_PRESSED, FOCUS_LOST); - containerField.getTextField().addEventHandler(KeyEvent.KEY_PRESSED, createCachedContainerResetHandler()); - final TextField containerTextField = containerField.getTextField(); - final var tooltipBinding = Bindings.createObjectBinding(() -> new Tooltip(containerTextField.getText()), containerTextField.textProperty()); - containerTextField.tooltipProperty().bind(tooltipBinding); - containerField.valueProperty().bindBidirectional(selectionProperty); - containerTextField.setMinWidth(0); - containerTextField.setMaxWidth(Double.POSITIVE_INFINITY); - containerTextField.setPromptText("N5 container"); - - final EventHandler onBrowseFoldersClicked = _ -> { - - final File initialDirectory = Optional - .ofNullable(selectionProperty.get()) - .map(File::new) - .filter(File::exists) - .orElse(Path.of(".").toAbsolutePath().toFile()); - updateFromDirectoryChooser(initialDirectory, containerTextField.getScene().getWindow()); - }; - - final EventHandler onBrowseFilesClicked = _ -> { - final File initialDirectory = Optional - .ofNullable(selectionProperty.get()) - .map(File::new) - .map(f -> f.isFile() ? f.getParentFile() : f) - .filter(File::exists) - .orElse(Path.of(".").toAbsolutePath().toFile()); - updateFromFileChooser(initialDirectory, containerTextField.getScene().getWindow()); - }; - - List recentSelections = Lists.reverse(PainteraCache.readLines(this.getClass(), "recent")); - final MenuButton menuButton = BrowseRecentFavorites.menuButton( - "_Find", - recentSelections, - FAVORITES, - onBrowseFoldersClicked, - onBrowseFilesClicked, - selectionProperty::set); - - return new GenericBackendDialogN5(containerTextField, menuButton, "N5", containerState, isOpeningContainer); - } - - @NotNull private EventHandler createCachedContainerResetHandler() { - - return event -> { - if (event.getCode() == KeyCode.ENTER) { - final String url = selectionProperty.get(); - final N5ContainerState oldContainer = n5ContainerStateCache.remove(url); - containerState.set(null); - GenericBackendDialogN5.previousContainerChoices.remove(oldContainer); - selectionChanged(null, null, url); - } - }; - } - - public void selectionAccepted() { - - cacheCurrentSelectionAsRecent(); - } - - private void cacheCurrentSelectionAsRecent() { - - final String path = selectionProperty.get(); - if (path != null) - PainteraCache.appendLine(getClass(), "recent", path, 50); - } - - /** - * Open {@code url} as an N5Reader if possible, else empty. - * - * @param url location of the container we wish to open as an N5Writer. - * @return N5Reader of {@code url} if valid N5 container; else empty - */ - private Optional openN5Reader(final String url) { - - try { - final var reader = Paintera.getN5Factory().openReader(url); - if (!reader.exists("")) { - LOG.debug("{} cannot be opened as an N5Reader.", url); - return Optional.empty(); - } - LOG.debug("{} was opened as an N5Reader.", url); - return Optional.of(reader); - } catch (Exception e) { - LOG.debug("{} cannot be opened as an N5Reader.", url); - } - return Optional.empty(); - } - - private void updateFromFileChooser(final File initialDirectory, final Window owner) { - - final FileChooser fileChooser = new FileChooser(); - fileChooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter("h5", H5_EXTENSIONS)); - fileChooser.setInitialDirectory(initialDirectory); - final File updatedRoot = fileChooser.showOpenDialog(owner); - - LOG.debug("Updating root to {} (was {})", updatedRoot, selectionProperty.get()); - - if (updatedRoot != null && updatedRoot.exists() && updatedRoot.isFile()) - selectionProperty.set(updatedRoot.getAbsolutePath()); - } - - private void updateFromDirectoryChooser(final File initialDirectory, final Window ownerWindow) { - - final DirectoryChooser directoryChooser = new DirectoryChooser(); - Optional.of(initialDirectory) - .map(x -> x.isDirectory() ? x : x.getParentFile()) - .ifPresent(directoryChooser::setInitialDirectory); - Optional.ofNullable(directoryChooser.showDialog(ownerWindow)).ifPresent(updatedRoot -> { - LOG.debug("Updating root to {} (was {})", updatedRoot, selectionProperty.get()); - if (Paintera.getN5Factory().openReaderOrNull(updatedRoot.getAbsolutePath()) != null) { - // set null first to make sure that selectionProperty will be invalidated even if directory is the same - String updatedAbsPath = updatedRoot.getAbsolutePath(); - if (updatedAbsPath.equals(selectionProperty.get())) { - selectionProperty.set(null); - } - selectionProperty.set(updatedAbsPath); - } - }); - } - - private void selectionChanged(ObservableValue obs, String oldSelection, String newSelection) { - - if (newSelection == null || newSelection.isBlank()) { - containerState.set(null); - return; - } - - 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/dialogs/opendialog/menu/n5/N5FactoryOpener.kt b/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/menu/n5/N5FactoryOpener.kt new file mode 100644 index 000000000..a9af6be66 --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/menu/n5/N5FactoryOpener.kt @@ -0,0 +1,175 @@ +package org.janelia.saalfeldlab.paintera.ui.dialogs.opendialog.menu.n5 + +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import javafx.beans.binding.Bindings +import javafx.beans.property.* +import javafx.event.ActionEvent +import javafx.event.EventHandler +import javafx.scene.control.Tooltip +import javafx.scene.input.KeyCode +import javafx.scene.input.KeyEvent +import javafx.scene.input.KeyEvent.KEY_PRESSED +import javafx.stage.DirectoryChooser +import javafx.stage.FileChooser +import javafx.stage.Window +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.janelia.saalfeldlab.fx.extensions.nullable +import org.janelia.saalfeldlab.fx.ui.ObjectField.Companion.stringField +import org.janelia.saalfeldlab.fx.ui.ObjectField.SubmitOn +import org.janelia.saalfeldlab.paintera.Paintera.Companion.n5Factory +import org.janelia.saalfeldlab.paintera.PainteraConfigYaml +import org.janelia.saalfeldlab.paintera.state.metadata.N5ContainerState +import org.janelia.saalfeldlab.util.PainteraCache.appendLine +import org.janelia.saalfeldlab.util.PainteraCache.readLines +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths + +class N5FactoryOpener { + private val selectionProperty: StringProperty = SimpleStringProperty() + private var selection by selectionProperty.nullable() + + private val containerStateProperty: ObjectProperty = SimpleObjectProperty() + private val isOpeningContainer: BooleanProperty = SimpleBooleanProperty(false) + + init { + selectionProperty.subscribe { _, new -> selectionChanged(new) } + DEFAULT_DIRECTORY?.let { + selection = Paths.get(it).toRealPath().toString() + } + } + + fun backendDialog(): GenericBackendDialogN5 { + var ownerWindow: Window? = null + val containerField = stringField(selectionProperty.get(), SubmitOn.ENTER_PRESSED, SubmitOn.FOCUS_LOST).apply { + valueProperty().bindBidirectional(selectionProperty) + textField.apply { + addEventFilter(KEY_PRESSED, createCachedContainerResetHandler()) + minWidth = 0.0 + maxWidth = Double.POSITIVE_INFINITY + promptText = "N5 container" + val tooltipBinding = Bindings.createObjectBinding({ Tooltip(text) }, textProperty()) + tooltipProperty().bind(tooltipBinding) + + textProperty().subscribe { _ -> + containerStateProperty.set(null) + } + + ownerWindow = this.scene?.window + } + } + val onBrowseFoldersClicked = EventHandler { + val startDir = selection?.let { path -> + File(path).let { if (it.exists()) it else null } + } ?: Path.of(".").toAbsolutePath().toFile() + updateFromDirectoryChooser(startDir, ownerWindow) + } + + val onBrowseFilesClicked = EventHandler { + val startDir = selection?.let { path -> + File(path).let { + when { + it.isDirectory -> it + it.parentFile?.isDirectory == true -> it.parentFile + else -> null + } + } + } ?: Path.of(".").toAbsolutePath().toFile() + updateFromFileChooser(startDir, ownerWindow) + } + + val recentSelections = readLines(this.javaClass, "recent").reversed() + val menuButton = BrowseRecentFavorites.menuButton( + "_Find", + recentSelections, + FAVORITES, + onBrowseFoldersClicked, + onBrowseFilesClicked + ) { value: String -> selectionProperty.set(value) } + + val dialog = GenericBackendDialogN5(containerField.textField, menuButton, containerStateProperty, isOpeningContainer) + dialog.visibleProperty.subscribe { visible -> if (visible) selectionChanged(selection) } + return dialog + } + + private fun createCachedContainerResetHandler(): EventHandler { + return EventHandler { event: KeyEvent -> + if (event.code == KeyCode.ENTER) { + val url = selectionProperty.get() + val oldContainer = n5ContainerStateCache.remove(url) + containerStateProperty.set(null) + GenericBackendDialogN5.previousContainerChoices.remove(oldContainer) + selectionChanged(url) + } + } + } + + fun selectionAccepted() { + cacheCurrentSelectionAsRecent() + } + + private fun cacheCurrentSelectionAsRecent() { + val path = selectionProperty.get() + if (path != null) appendLine(javaClass, "recent", path, 50) + } + + private fun updateFromFileChooser(initialDirectory: File, owner: Window?) { + FileChooser().also { + it.extensionFilters.setAll(FileChooser.ExtensionFilter("h5", *H5_EXTENSIONS)) + it.initialDirectory = initialDirectory + }.showOpenDialog(owner)?.let { updatedRoot -> + LOG.debug { "Updating root to $updatedRoot (was $selection)" } + if (updatedRoot.isFile) + selection = updatedRoot.absolutePath + } + + } + + private fun updateFromDirectoryChooser(initialDirectory: File, ownerWindow: Window?) { + DirectoryChooser().also { + it.initialDirectory = if (initialDirectory.isDirectory) initialDirectory else initialDirectory.parentFile + }.showDialog(ownerWindow)?.let { updatedRoot -> + LOG.debug { "Updating root to $updatedRoot (was $selection)" } + n5Factory.openReaderOrNull(updatedRoot.absolutePath)?.let { + selection = if (updatedRoot.absolutePath == selection) null else updatedRoot.absolutePath + } + } + } + + private fun selectionChanged(newSelection: String?) { + if (newSelection.isNullOrBlank()) { + containerStateProperty.set(null) + return + } + + CoroutineScope(Dispatchers.IO).launch { + isOpeningContainer.set(true) + + n5ContainerStateCache.getOrPut(newSelection) { + n5Factory.openWriterOrReaderOrNull(newSelection)?.let { N5ContainerState(it) } + }?.let { containerStateProperty.set(it) } + + }.invokeOnCompletion { cause -> + cause?.let { LOG.error(it) { "Error opening container: $newSelection" } } + isOpeningContainer.set(false) + } + } + + companion object { + private val DEFAULT_DIRECTORY = getPainteraConfig("data", "n5", "defaultDirectory") { + getPainteraConfig("data", "defaultDirectory") { null } + } + + private val FAVORITES: List = getPainteraConfig("data", "n5", "favorites") { listOf() } + + private val H5_EXTENSIONS = arrayOf("*.h5", "*.hdf", "*.hdf5") + + private val LOG: KLogger = KotlinLogging.logger {} + private val n5ContainerStateCache = HashMap() + + private fun getPainteraConfig(vararg segments: String, fallback: () -> T) = PainteraConfigYaml.getConfig(fallback, *segments) as T + } +} diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/menu/n5/N5OpenSourceDialog.java b/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/menu/n5/N5OpenSourceDialog.java index 4e1c2716d..1dde30f71 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/menu/n5/N5OpenSourceDialog.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/menu/n5/N5OpenSourceDialog.java @@ -47,7 +47,6 @@ import org.janelia.saalfeldlab.paintera.ui.dialogs.opendialog.menu.OpenDialogMenuEntry; import org.janelia.saalfeldlab.paintera.ui.dialogs.opendialog.meta.MetaPanel; import org.janelia.saalfeldlab.paintera.ui.menus.PainteraMenuItems; -import org.scijava.plugin.Plugin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,7 +55,6 @@ import java.util.Collection; import java.util.List; import java.util.Optional; -import java.util.concurrent.ExecutorService; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; @@ -69,7 +67,6 @@ public class N5OpenSourceDialog extends Dialog implement private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - @Plugin(type = OpenDialogMenuEntry.class, menuPath = "Raw/Label _Source", priority = Double.MAX_VALUE) public static class N5Opener implements OpenDialogMenuEntry { private static N5FactoryOpener factoryOpener = new N5FactoryOpener(); @@ -145,21 +142,15 @@ public static ActionSet openSourceDialogAction(PainteraBaseView baseView, Suppli private final BooleanBinding isError; - private final ExecutorService propagationExecutor; - private final GenericBackendDialogN5 backendDialog; private final MetaPanel metaPanel = new MetaPanel(); public N5OpenSourceDialog(final PainteraBaseView viewer, final GenericBackendDialogN5 backendDialog) { - super(); - this.backendDialog = backendDialog; this.metaPanel.listenOnDimensions(backendDialog.dimensionsProperty()); - this.propagationExecutor = viewer.getPropagationQueue(); - this.setTitle("Open data set"); this.getDialogPane().getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK); ((Button)this.getDialogPane().lookupButton(ButtonType.CANCEL)).setText("_Cancel"); diff --git a/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/meta/MetaPanel.java b/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/meta/MetaPanel.java index b492e295f..19b2bfc45 100644 --- a/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/meta/MetaPanel.java +++ b/src/main/java/org/janelia/saalfeldlab/paintera/ui/dialogs/opendialog/meta/MetaPanel.java @@ -127,7 +127,7 @@ public MetaPanel() { resolution.textY(), resolution.textZ() ); - addToGrid(spatialInfo, 0, 2, new Label("Offset"), offset.textX(), offset.textY(), offset.textZ()); + addToGrid(spatialInfo, 0, 2, new Label("Offset (physical)"), offset.textX(), offset.textY(), offset.textZ()); spatialInfo.add(reverseButton, 3, 3); reverseButton.setPrefWidth(TEXTFIELD_WIDTH); final ColumnConstraints cc = new ColumnConstraints(); 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 12b3502a6..3dfb946e2 100644 --- a/src/main/java/org/janelia/saalfeldlab/util/n5/N5Data.java +++ b/src/main/java/org/janelia/saalfeldlab/util/n5/N5Data.java @@ -21,6 +21,7 @@ import net.imglib2.interpolation.randomaccess.NearestNeighborInterpolatorFactory; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.type.NativeType; +import net.imglib2.type.label.Label; import net.imglib2.type.label.LabelMultiset; import net.imglib2.type.label.LabelMultisetType; import net.imglib2.type.label.VolatileLabelMultisetArray; @@ -37,6 +38,7 @@ import org.janelia.saalfeldlab.n5.universe.metadata.SpatialMultiscaleMetadata; import org.janelia.saalfeldlab.paintera.cache.WeakRefVolatileCache; import org.janelia.saalfeldlab.paintera.data.DataSource; +import org.janelia.saalfeldlab.paintera.data.n5.LabelMultisetUtilsKt; import org.janelia.saalfeldlab.paintera.data.n5.N5DataSource; import org.janelia.saalfeldlab.paintera.data.n5.ReflectionException; import org.janelia.saalfeldlab.paintera.state.metadata.MetadataState; @@ -273,15 +275,16 @@ ImagesWithTransform openRaw( final SharedQueue queue, final int priority) throws IOException { + try { final CachedCellImg raw = N5Utils.openVolatile(reader, dataset); final TmpVolatileHelpers.RaiWithInvalidate vraw = TmpVolatileHelpers.createVolatileCachedCellImgWithInvalidate( - (CachedCellImg) raw, + (CachedCellImg)raw, queue, new CacheHints(LoadingStrategy.VOLATILE, priority, true)); - return new ImagesWithTransform<>(raw, vraw.getRai(), transform, raw.getCache(), vraw.getInvalidate()); + return new ImagesWithTransform<>(raw, vraw.getRai(), transform, raw.getCache(), vraw.getInvalidate()); } catch (final Exception e) { - throw e instanceof IOException ? (IOException) e : new IOException(e); + throw e instanceof IOException ? (IOException)e : new IOException(e); } } @@ -364,9 +367,10 @@ ImagesWithTransform[] openRawMultiscale( /* get the metadata state for the respective child */ LOG.debug("Populating scale level {}", scaleIdx); imagesWithInvalidate[scaleIdx] = openRaw(reader, ssPaths[scaleIdx], ssTransforms[scaleIdx], queue, priority); - LOG.debug("Populated scale level {}", scaleIdx); + LOG.debug("Populated scale level {}", scaleIdx); return true; })::get))); + futures.forEach(ThrowingConsumer.unchecked(Future::get)); es.shutdown(); return imagesWithInvalidate; @@ -401,13 +405,16 @@ ImagesWithTransform[] openRawMultiscale( futures.add(es.submit(ThrowingSupplier.unchecked(() -> { LOG.debug("Populating scale level {}", fScale); final String scaleDataset = N5URI.normalizeGroupPath(dataset + reader.getGroupSeparator() + scaleDatasets[fScale]); - imagesWithInvalidate[fScale] = openRaw(reader, scaleDataset, transform.copy(), queue, priority); + final double[] downsamplingFactors = N5Helpers.getDownsamplingFactors(reader, scaleDataset); LOG.debug("Read downsampling factors: {}", Arrays.toString(downsamplingFactors)); - imagesWithInvalidate[fScale].transform.set(N5Helpers.considerDownsampling( - imagesWithInvalidate[fScale].transform.copy(), + + final AffineTransform3D scaleTransform = N5Helpers.considerDownsampling( + transform.copy(), downsamplingFactors, - initialDonwsamplingFactors)); + initialDonwsamplingFactors); + imagesWithInvalidate[fScale] = openRaw(reader, scaleDataset, scaleTransform, queue, priority); + LOG.debug("Populated scale level {}", fScale); return true; })::get)); @@ -435,7 +442,7 @@ ImagesWithTransform[] openRawMultiscale( final int priority, final String name) throws IOException, ReflectionException { - return openLabelMultisetAsSource(MetadataUtils.createMetadataState((N5Writer) reader, dataset), queue, priority, name, null); + return openLabelMultisetAsSource(MetadataUtils.createMetadataState((N5Writer)reader, dataset), queue, priority, name, null); } /** @@ -535,8 +542,8 @@ public static ImagesWithTransform final SharedQueue queue, final int priority) { - final CachedCellImg cachedLabelMultisetImage = N5LabelMultisets.openLabelMultiset(n5, dataset); - + final CachedCellImg cachedLabelMultisetImage + = N5LabelMultisets.openLabelMultiset(n5, dataset, LabelMultisetUtilsKt.constantNullReplacementEmptyArgMax(Label.BACKGROUND)); final boolean isDirty = AccessFlags.ofAccess(cachedLabelMultisetImage.getAccessType()).contains(AccessFlags.DIRTY); final WeakRefVolatileCache> vcache = WeakRefVolatileCache.fromCache( @@ -550,7 +557,7 @@ public static ImagesWithTransform final VolatileCachedCellImg vimg = new VolatileCachedCellImg<>( cachedLabelMultisetImage.getCellGrid(), new VolatileLabelMultisetType().getEntitiesPerPixel(), - img -> new VolatileLabelMultisetType((NativeImg) img), + img -> new VolatileLabelMultisetType((NativeImg)img), cacheHints, unchecked::get); vimg.setLinkedType(new VolatileLabelMultisetType(vimg)); @@ -724,8 +731,8 @@ public static , T extends NativeType> DataSource) openLabelMultisetAsSource(reader, dataset, transform, queue, priority, name) - : (DataSource) openScalarAsSource(reader, dataset, transform, queue, priority, name); + ? (DataSource)openLabelMultisetAsSource(reader, dataset, transform, queue, priority, name) + : (DataSource)openScalarAsSource(reader, dataset, transform, queue, priority, name); } /** @@ -780,6 +787,38 @@ public static void createEmptyLabelDataset( final boolean labelMultisetType, final boolean ignoreExisiting) throws IOException { + final String defaultUnit = "pixel"; + createEmptyLabelDataset(writer, group, dimensions, blockSize, resolution, offset, relativeScaleFactors, defaultUnit, maxNumEntries, labelMultisetType, ignoreExisiting); + } + + /** + * @param writer N5Writer + * @param group target group in {@code writer} + * @param dimensions size + * @param blockSize chunk size + * @param resolution voxel size + * @param offset in world coordinates + * @param relativeScaleFactors relative scale factors for multi-scale data, e.g. + * {@code [2,2,1], [2,2,2]} will result in absolute factors {@code [1,1,1], [2,2,1], [4,4,2]}. + * @param unit + * @param maxNumEntries limit number of entries in each {@link LabelMultiset} (set to less than or equal to zero for unbounded) + * @param ignoreExisiting overwrite any existing data set + * @throws IOException if any n5 operation throws {@link IOException} or {@code group} + * already exists and {@code ignorExisting} is {@code false} + */ + public static void createEmptyLabelDataset( + final N5Writer writer, + final String group, + final long[] dimensions, + final int[] blockSize, + final double[] resolution, + final double[] offset, + final double[][] relativeScaleFactors, + final String unit, + @Nullable final int[] maxNumEntries, + final boolean labelMultisetType, + final boolean ignoreExisiting) throws IOException { + final Map pd = new HashMap<>(); pd.put("type", "label"); final String uniqueLabelsGroup = String.format("%s/unique-labels", group); @@ -803,7 +842,6 @@ public static void createEmptyLabelDataset( final String dataGroup = String.format("%s/data", group); writer.createGroup(dataGroup); - writer.setAttribute(dataGroup, N5Helpers.MULTI_SCALE_KEY, true); writer.setAttribute(dataGroup, N5Helpers.OFFSET_KEY, offset); writer.setAttribute(dataGroup, N5Helpers.RESOLUTION_KEY, resolution); @@ -820,7 +858,7 @@ public static void createEmptyLabelDataset( final double[] scaleFactors = downscaledLevel < 0 ? null : relativeScaleFactors[downscaledLevel]; if (scaleFactors != null) { - Arrays.setAll(scaledDimensions, dim -> (long) Math.ceil(scaledDimensions[dim] / scaleFactors[dim])); + Arrays.setAll(scaledDimensions, dim -> (long)Math.ceil(scaledDimensions[dim] / scaleFactors[dim])); Arrays.setAll(accumulatedFactors, dim -> accumulatedFactors[dim] * scaleFactors[dim]); } @@ -835,6 +873,7 @@ public static void createEmptyLabelDataset( } else writer.createDataset(dataset, scaledDimensions, blockSize, DataType.UINT64, new GzipCompression()); + writer.setAttribute(dataset, N5Helpers.UNIT_KEY, unit); writer.createDataset(uniqeLabelsDataset, scaledDimensions, blockSize, DataType.UINT64, new GzipCompression()); if (scaleLevel != 0) { writer.setAttribute(dataset, N5Helpers.DOWNSAMPLING_FACTORS_KEY, accumulatedFactors); diff --git a/src/main/java/org/janelia/saalfeldlab/util/n5/N5FragmentSegmentAssignmentInitialLut.java b/src/main/java/org/janelia/saalfeldlab/util/n5/N5FragmentSegmentAssignmentInitialLut.java index 2176a51f9..b8579cbc6 100644 --- a/src/main/java/org/janelia/saalfeldlab/util/n5/N5FragmentSegmentAssignmentInitialLut.java +++ b/src/main/java/org/janelia/saalfeldlab/util/n5/N5FragmentSegmentAssignmentInitialLut.java @@ -40,6 +40,10 @@ public TLongLongMap get() { final long[] keys = new long[(int)data.dimension(0)]; final long[] values = new long[keys.length]; LOG.debug("Found {} assignments", keys.length); + /* May happen in the case of detaching all existing mappings. + * I would prefer flatIterable to work correctly over an empty interval, but it doesn't (yet) */ + if (data.dimension(0) <= 0) + return new TLongLongHashMap(); final Cursor keyCursor = Views.flatIterable(Views.hyperSlice(data, 1, 0L)).cursor(); final Cursor valueCursor = Views.flatIterable(Views.hyperSlice(data, 1, 1L)).cursor(); for (int i = 0; i < keys.length; ++i) { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/fx/ui/ScaleViews.kt b/src/main/kotlin/org/janelia/saalfeldlab/fx/ui/ScaleViews.kt index b026589f9..7dd795cca 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/fx/ui/ScaleViews.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/fx/ui/ScaleViews.kt @@ -75,7 +75,7 @@ class CircleScaleView(var child : Circle) : ScaleView(), Styleable { override fun isSettable(styleable: CircleScaleView) = !styleable.radiusPercentProperty.isBound override fun getStyleableProperty(styleable: CircleScaleView): StyleableProperty { - return styleable.radiusPercentProperty as StyleableProperty + return styleable.radiusPercentProperty } override fun getInitialValue(styleable: CircleScaleView): Double { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/net/imglib2/MultiRealRAIRandomAccessible.kt b/src/main/kotlin/org/janelia/saalfeldlab/net/imglib2/MultiRealRAIRandomAccessible.kt new file mode 100644 index 000000000..746fe7016 --- /dev/null +++ b/src/main/kotlin/org/janelia/saalfeldlab/net/imglib2/MultiRealRAIRandomAccessible.kt @@ -0,0 +1,49 @@ +package org.janelia.saalfeldlab.net.imglib2 + +import net.imglib2.* +import net.imglib2.util.Intervals +import org.janelia.saalfeldlab.util.realInterval + + +class MultiRealIntervalAccessibleRealRandomAccessible( + val rais : List>, + val outOfBounds: T, + val filter : (T) -> Boolean, +) : RealRandomAccessible { + + override fun numDimensions() = rais[0].numDimensions() + + override fun realRandomAccess() = MultiRealRaiRealRandomAccess( + numDimensions(), + rais.map { it to it.realRandomAccess() }, + outOfBounds, + filter + ) + + override fun realRandomAccess(interval: RealInterval) = realInterval(interval).realRandomAccess() + + + class MultiRealRaiRealRandomAccess( + numDimensions: Int, + val rrais: List>>, + val outOfBounds: T, + val filter: (T) -> Boolean = { true } + ) : RealPoint(numDimensions), RealRandomAccess { + + override fun get(): T { + for ((interval, access) in rrais) { + if (Intervals.contains(interval, this)) { + val at = access.setPositionAndGet(this) + if (filter(at)) + return at + } + } + return outOfBounds + } + + override fun copy() = MultiRealRaiRealRandomAccess(numDimensions(), rrais, outOfBounds, filter) + + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt index 3e17f071d..c564409d1 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/BindingKeys.kt @@ -42,6 +42,7 @@ object PainteraBaseKeys { const val SHOW_REPL_TABS = "open repl" const val TOGGLE_FULL_SCREEN = "toggle full screen" const val OPEN_SOURCE = "open source" + const val EXPORT_SOURCE = "export source" const val SAVE = "save" const val SAVE_AS = "save as" const val TOGGLE_MENUBAR_VISIBILITY = "toggle menubar visibility" @@ -61,6 +62,7 @@ object PainteraBaseKeys { val NAMED_COMBINATIONS = NamedKeyCombination.CombinationMap( OPEN_SOURCE byKeyCombo CONTROL_DOWN + O, + EXPORT_SOURCE byKeyCombo CONTROL_DOWN + E, SAVE byKeyCombo CONTROL_DOWN + S, SAVE_AS byKeyCombo CONTROL_DOWN + SHIFT_DOWN + S, TOGGLE_MENUBAR_VISIBILITY byKeyCombo F2, diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.kt index d5e24da9b..a83022886 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/PainteraDefaultHandlers.kt @@ -53,6 +53,7 @@ import org.janelia.saalfeldlab.paintera.control.modes.ToolMode import org.janelia.saalfeldlab.paintera.control.navigation.DisplayTransformUpdateOnResize import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource import org.janelia.saalfeldlab.paintera.ui.StatusBar.Companion.createPainteraStatusBar +import org.janelia.saalfeldlab.paintera.ui.dialogs.ExportSourceDialog import org.janelia.saalfeldlab.paintera.ui.dialogs.opendialog.menu.n5.N5OpenSourceDialog.N5Opener import org.slf4j.LoggerFactory import java.lang.invoke.MethodHandles @@ -136,7 +137,8 @@ class PainteraDefaultHandlers(private val paintera: PainteraMainWindow, paneWith baseView.orthogonalViews().views().forEach { grabFocusOnMouseOver(it) } - globalActionHandlers + addOpenDatasetAction(paneWithStatus.pane, KeyCode.CONTROL, KeyCode.O) + globalActionHandlers + addOpenDatasetAction(paneWithStatus.pane) + globalActionHandlers + addExportDatasetAction(paneWithStatus.pane) viewerToTransforms[orthogonalViews.topLeft.viewer()] = orthogonalViews.topLeft viewerToTransforms[orthogonalViews.topRight.viewer()] = orthogonalViews.topRight @@ -407,15 +409,21 @@ class PainteraDefaultHandlers(private val paintera: PainteraMainWindow, paneWith } } - fun addOpenDatasetAction(target: Node, vararg keyTrigger: KeyCode): ActionSet { + fun addOpenDatasetAction(target: Node): ActionSet { - assert(keyTrigger.isNotEmpty()) val actionSet = N5Opener.openSourceDialogAction(baseView, projectDirectory) target.installActionSet(actionSet) return actionSet } + fun addExportDatasetAction(target: Node): ActionSet { + + val actionSet = ExportSourceDialog.exportSourceDialogAction(baseView) + target.installActionSet(actionSet) + return actionSet + } + companion object { private val LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/PainteraDirectoriesConfig.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/PainteraDirectoriesConfig.kt index 38498c351..bd1bfeea8 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/PainteraDirectoriesConfig.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/config/PainteraDirectoriesConfig.kt @@ -25,24 +25,31 @@ import java.lang.reflect.Type class PainteraDirectoriesConfig { - private val appCacheDirProperty = SimpleStringProperty(APPLICATION_DIRECTORIES.cacheDir).apply { + internal val appCacheDirProperty = SimpleStringProperty(APPLICATION_DIRECTORIES.cacheDir).apply { addListener { _, _, new -> if (new.isBlank()) appCacheDir = APPLICATION_DIRECTORIES.cacheDir } } var appCacheDir: String by appCacheDirProperty.nonnull() - private val tmpDirProperty = SimpleStringProperty(TEMP_DIRECTORY).apply { + internal val tmpDirProperty = SimpleStringProperty(TEMP_DIRECTORY).apply { addListener { _, _, new -> if (new.isBlank()) tmpDir = TEMP_DIRECTORY } } var tmpDir: String by tmpDirProperty.nonnull() + internal val appConfigDirProperty = SimpleStringProperty(APPLICATION_DIRECTORIES.configDir).apply { + subscribe { _, new -> if (new.isBlank()) appConfigDir = APPLICATION_DIRECTORIES.configDir } + } + var appConfigDir: String by appConfigDirProperty.nonnull() + internal val allDefault get() = appCacheDir == APPLICATION_DIRECTORIES.cacheDir && tmpDir == TEMP_DIRECTORY companion object { @JvmField internal val APPLICATION_DIRECTORIES = ProjectDirectories.from("org", "janelia", "Paintera") + @JvmField internal val DEFAULT_CACHE_DIR = APPLICATION_DIRECTORIES.cacheDir + @JvmField internal val TEMP_DIRECTORY = System.getProperty("java.io.tmpdir") } @@ -57,8 +64,30 @@ class PainteraDirectoriesConfigNode(val config: PainteraDirectoriesConfig) : Tit } private fun createNode() = GridPane().apply { - addCacheDirectoryConfigRow(0) - addTempDirectoryConfigRow(1) + addDirectoryConfigRow( + 0, "Cache Directory", config.appCacheDirProperty, APPLICATION_DIRECTORIES.cacheDir, + """ + Directory used for storing potentially large, temporary data files used during the application runtime for caching non-persisted data. + + By default, the cache directory is shared for all instances of the application, across all projects. + """.trimIndent() + ) + addDirectoryConfigRow( + 1, "Temp Directory", config.tmpDirProperty, TEMP_DIRECTORY, + """ + Directory used for storing temporary non-data files used during the application runtime. + + By default, the temp directory is project-specific, and will change if a new project is loaded.""" + .trimIndent() + ) + addDirectoryConfigRow( + 2, "Config Directory", config.appConfigDirProperty, APPLICATION_DIRECTORIES.configDir, + """ + Directory used to store application wide configuration. + + By default, the config directory is shared for all instances of the application, across all projects, per user. + """.trimIndent() + ) columnConstraints.add(ColumnConstraints().apply { hgrow = Priority.NEVER }) columnConstraints.add(ColumnConstraints().apply { hgrow = Priority.ALWAYS }) @@ -66,93 +95,47 @@ class PainteraDirectoriesConfigNode(val config: PainteraDirectoriesConfig) : Tit columnConstraints.add(ColumnConstraints().apply { hgrow = Priority.NEVER }) } - private fun GridPane.addCacheDirectoryConfigRow(row: Int) { - Label("Cache Directory").also { + private fun GridPane.addDirectoryConfigRow( + row: Int, + label: String, + directoryProperty: SimpleStringProperty, + defaultValue: String, + helpText: String + ) { + Label(label).also { add(it, 0, row) it.alignment = Pos.BASELINE_LEFT it.minWidth = Label.USE_PREF_SIZE } - val cacheTextField = TextField(config.appCacheDir).also { + val textField = TextField(directoryProperty.get()).also { VBox.setVgrow(it, Priority.NEVER) it.maxWidth = Double.MAX_VALUE it.prefWidth - Double.MAX_VALUE it.textProperty().addListener { _, _, new -> if (new.isBlank()) { - it.text = APPLICATION_DIRECTORIES.cacheDir + it.text = defaultValue Platform.runLater { it.positionCaret(0) } } else { - config.appCacheDir = new + directoryProperty.value = new } } add(it, 1, row) } Button().also { it.graphic = FontAwesome[FontAwesomeIcon.UNDO] - it.onAction = EventHandler { cacheTextField.text = APPLICATION_DIRECTORIES.cacheDir } + it.onAction = EventHandler { textField.text = defaultValue } add(it, 2, row) } Button().also { it.graphic = FontAwesome[FontAwesomeIcon.QUESTION] it.onAction = EventHandler { PainteraAlerts.information("Ok", true).also { alert -> - alert.title = "Cache Directory" + alert.title = label alert.headerText = alert.title alert.dialogPane.content = TextArea().also { area -> area.isWrapText = true area.isEditable = false - area.text = """ - Directory used for storing potentially large, temporary data files used during the application runtime for caching non-persisted data. - - - By default, the cache directory is shared for all instances of the application, across all projects. - """.trimIndent() - } - }.showAndWait() - } - add(it, 3, row) - } - } - - private fun GridPane.addTempDirectoryConfigRow(row: Int) { - Label("Temp Directory").also { - add(it, 0, row) - it.alignment = Pos.BASELINE_LEFT - it.minWidth = Label.USE_PREF_SIZE - } - val tempDirTextField = TextField(config.tmpDir).also { - VBox.setVgrow(it, Priority.NEVER) - it.maxWidth = Double.MAX_VALUE - it.prefWidth - Double.MAX_VALUE - it.textProperty().addListener { _, _, new -> - if (new.isBlank()) { - it.text = TEMP_DIRECTORY - Platform.runLater { it.positionCaret(0) } - } else { - config.tmpDir = new - } - } - add(it, 1, row) - } - Button().also { - it.graphic = FontAwesome[FontAwesomeIcon.UNDO] - it.onAction = EventHandler { tempDirTextField.text = TEMP_DIRECTORY } - add(it, 2, row) - } - Button().also { - it.graphic = FontAwesome[FontAwesomeIcon.QUESTION] - it.onAction = EventHandler { - PainteraAlerts.information("Ok", true).also { alert -> - alert.title = "Temp Directory" - alert.headerText = alert.title - alert.dialogPane.content = TextArea().also { area -> - area.isEditable = false - area.isWrapText = true - area.text = """ - Directory used for storing temporary non-data files used during the application runtime. - - - By default, the temp directory is project-specific, and will change if a new project is loaded. - """.trimIndent() + area.text = helpText } }.showAndWait() } 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 ded7982f7..14eede3c7 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/ShapeInterpolationController.kt @@ -2,13 +2,13 @@ package org.janelia.saalfeldlab.paintera.control import bdv.viewer.TransformListener import io.github.oshai.kotlinlogging.KotlinLogging -import javafx.beans.InvalidationListener -import javafx.beans.Observable import javafx.beans.property.ObjectProperty import javafx.beans.property.SimpleBooleanProperty import javafx.beans.property.SimpleDoubleProperty import javafx.beans.property.SimpleObjectProperty import javafx.beans.value.ChangeListener +import javafx.collections.FXCollections +import javafx.collections.ObservableList import javafx.scene.paint.Color import javafx.util.Duration import kotlinx.coroutines.* @@ -16,7 +16,6 @@ import kotlinx.coroutines.javafx.awaitPulse import net.imglib2.* import net.imglib2.algorithm.morphology.distance.DistanceTransform import net.imglib2.converter.BiConverter -import net.imglib2.converter.Converters import net.imglib2.converter.logical.Logical import net.imglib2.converter.read.BiConvertedRealRandomAccessible import net.imglib2.img.array.ArrayImgFactory @@ -41,6 +40,7 @@ import net.imglib2.view.Views import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX import org.janelia.saalfeldlab.fx.extensions.* import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread +import org.janelia.saalfeldlab.net.imglib2.MultiRealIntervalAccessibleRealRandomAccessible import org.janelia.saalfeldlab.net.imglib2.outofbounds.RealOutOfBoundsConstantValueFactory import org.janelia.saalfeldlab.net.imglib2.view.BundleView import org.janelia.saalfeldlab.paintera.Paintera @@ -61,7 +61,6 @@ import org.janelia.saalfeldlab.paintera.util.IntervalHelpers.Companion.smallestC import org.janelia.saalfeldlab.util.* import java.math.BigDecimal import java.math.RoundingMode -import java.util.Collections import java.util.concurrent.atomic.AtomicReference import java.util.function.Supplier import java.util.stream.Collectors @@ -205,7 +204,7 @@ class ShapeInterpolationController>( previousSlice(depth) to nextSlice(depth) } - fun enterShapeInterpolation(viewer: ViewerPanelFX?) { + fun enterShapeInterpolation(viewer: ViewerPanelFX) { if (isControllerActive) { LOG.trace { "Already in shape interpolation" } return @@ -417,20 +416,18 @@ class ShapeInterpolationController>( if (Label.regular(finalLastSelectedId)) { val maskInfo = source.currentMask.info source.resetMasks(false) - val interpolatedMaskImgsA = Converters.convert( - globalCompositeFillAndInterpolationImgs!!.first.affineReal(globalToSource), - { input: UnsignedLongType, output: UnsignedLongType -> + val interpolatedMaskImgsA = globalCompositeFillAndInterpolationImgs!!.first + .affineReal(globalToSource) + .convert(UnsignedLongType(Label.INVALID)) { input, output -> val originalLabel = input.long val label = if (originalLabel == finalInterpolationId) { finalLastSelectedId } else input.get() output.set(label) - }, - UnsignedLongType(Label.INVALID) - ) - val interpolatedMaskImgsB = Converters.convert( - globalCompositeFillAndInterpolationImgs!!.second.affineReal(globalToSource), - { input: VolatileUnsignedLongType, out: VolatileUnsignedLongType -> + } + val interpolatedMaskImgsB = globalCompositeFillAndInterpolationImgs!!.second + .affineReal(globalToSource) + .convert(VolatileUnsignedLongType(Label.INVALID)) { input, out -> val isValid = input.isValid out.isValid = isValid if (isValid) { @@ -438,9 +435,7 @@ class ShapeInterpolationController>( val label = if (originalLabel == finalInterpolationId) finalLastSelectedId else input.get().get() out.get().set(label) } - }, - VolatileUnsignedLongType(Label.INVALID) - ) + } source.setMask( maskInfo, interpolatedMaskImgsA, @@ -483,7 +478,7 @@ class ShapeInterpolationController>( synchronized(source) { source.resetMasks(false) /* If preview is on, hide all except the first and last fill mask */ - val fillMasks: MutableList> = mutableListOf() + val fillMasks: MutableList> = mutableListOf() val slices = slicesAndInterpolants.slices slices.forEachIndexed { idx, slice -> if (idx == 0 || idx == slices.size - 1 || !includeInterpolant) { @@ -493,52 +488,42 @@ class ShapeInterpolationController>( .extendValue(Label.INVALID) .interpolateNearestNeighbor() .affineReal(initialGlobalToMaskTransform.inverse()) + .realInterval(slice.globalBoundingBox!!) } } } - val constantInvalid = ConstantUtils.constantRandomAccessible(UnsignedLongType(Label.INVALID), 3).interpolateNearestNeighbor() - if (fillMasks.isEmpty()) { - fillMasks += constantInvalid - } + val invalidLabel = UnsignedLongType(Label.INVALID) - var compositeFill: RealRandomAccessible = fillMasks[0] - for ((index, dataMask) in fillMasks.withIndex()) { - if (index == 0) continue + val composedSlices = + if (slices.isEmpty()) + ConstantUtils.constantRandomAccessible(invalidLabel, 3).interpolateNearestNeighbor() + else + MultiRealIntervalAccessibleRealRandomAccessible(fillMasks, invalidLabel) { it.get() != Label.INVALID } - compositeFill = compositeFill.convertWith(dataMask, UnsignedLongType(Label.INVALID)) { composite, mask, result -> - val maskVal = mask.get() - result.setInteger(if (maskVal != Label.INVALID) maskVal else composite.get()) - } - } val interpolants = slicesAndInterpolants.interpolants - val dataMasks: MutableList> = mutableListOf() - - if (preview) { - interpolants.forEach { info -> - dataMasks += info.dataInterpolant - } - } - - var compositeInterpolation: RealRandomAccessible = dataMasks.getOrNull(0) ?: ConstantUtils.constantRealRandomAccessible(UnsignedLongType(Label.INVALID), compositeFill.numDimensions()) - for ((index, dataMask) in dataMasks.withIndex()) { - if (index == 0) continue - - compositeInterpolation = compositeInterpolation.convertWith(dataMask, UnsignedLongType(Label.INVALID)) { composite, mask, result -> - val maskVal = mask.get() - result.setInteger(if (maskVal != Label.INVALID) maskVal else composite.get()) + val composedInterpolations = + if (!preview || interpolants.isEmpty()) + ConstantUtils.constantRealRandomAccessible(invalidLabel, composedSlices.numDimensions()) + else { + val interpolantRais = interpolants.map { info -> info.dataInterpolant.realInterval(info.interval!!) }.toList() + val interpolantBounds: RealInterval = interpolantRais.map { it as RealInterval }.reduce { l, r -> l.union(r) } + val outOfBounds = invalidLabel + ExtendedRealRandomAccessibleRealInterval( + MultiRealIntervalAccessibleRealRandomAccessible(interpolantRais, outOfBounds) { it.get() != Label.INVALID }.realInterval(interpolantBounds), + RealOutOfBoundsConstantValueFactory(outOfBounds) + ) } - } - val compositeMaskInGlobal = BiConvertedRealRandomAccessible(compositeFill, compositeInterpolation, Supplier { + val compositeMaskInGlobal = BiConvertedRealRandomAccessible(composedSlices, composedInterpolations, Supplier { BiConverter { fillValue: UnsignedLongType, interpolationValue: UnsignedLongType, compositeValue: UnsignedLongType -> val aVal = fillValue.get() val aOrB = if (aVal.isInterpolationLabel) fillValue else interpolationValue compositeValue.set(aOrB) } - }) { UnsignedLongType(Label.INVALID) } + }) { invalidLabel.copy() } val compositeVolatileMaskInGlobal = compositeMaskInGlobal.convert(VolatileUnsignedLongType(Label.INVALID)) { source, target -> target.get().set(source.get()) target.isValid = true @@ -631,7 +616,7 @@ class ShapeInterpolationController>( fun getMask(targetMipMapLevel: Int = currentBestMipMapLevel, ignoreExisting: Boolean = false): ViewerMask { /* If we have a mask, get it; else create a new one */ - currentViewerMask = (if (ignoreExisting) null else sliceAtCurrentDepth)?.let { oldSlice -> + currentViewerMask = (if (ignoreExisting) null else sliceAtCurrentDepth)?.let { oldSlice -> val oldSliceBoundingBox = oldSlice.maskBoundingBox ?: let { deleteSliceAt() return@let null @@ -858,7 +843,10 @@ class ShapeInterpolationController>( for (i in 0..1) { if (Thread.currentThread().isInterrupted) return null val distanceTransform = ArrayImgFactory(FloatType()).create(slices[i]).also { - val binarySlice = Converters.convert(slices[i], { source, target -> target.set(source.get().isInterpolationLabel) }, BoolType()) + val binarySlice = slices[i]!!.convertRAI(BoolType()){ source, target -> + val label = source.get().isInterpolationLabel + target.set(label) + } computeSignedDistanceTransform(binarySlice, it, DistanceTransform.DISTANCE_TYPE.EUCLIDIAN) } distanceTransformPair.add(distanceTransform) @@ -926,11 +914,10 @@ class ShapeInterpolationController>( .interpolate(NLinearInterpolatorFactory()) .affineReal(distanceScale) - val interpolatedShapeRaiInSource = Converters.convert( - scaledInterpolatedDistanceTransform, - { input: R, output: T -> output.set(if (input.realDouble <= 0) targetValue else invalidValue) }, - targetValue.createVariable() - ) + val interpolatedShapeRaiInSource = scaledInterpolatedDistanceTransform.convert(targetValue.createVariable()) { input, output : T -> + val value = if (input.realDouble <= 0) targetValue else invalidValue + output.set(value) + } .affineReal(transformToSource) .realInterval(transformToSource.copy().concatenate(distanceScale).estimateBounds(distanceTransformStack)) @@ -993,44 +980,31 @@ class ShapeInterpolationController>( } } - private class SlicesAndInterpolants : MutableList by Collections.synchronizedList(mutableListOf()), Observable { + private class SlicesAndInterpolants : ObservableList by FXCollections.synchronizedObservableList(FXCollections.observableArrayList()) { fun removeSlice(slice: SliceInfo): Boolean { - synchronized(this) { - for (idx in indices) { - if (idx >= 0 && idx <= size - 1 && get(idx).equals(slice)) { - removeIfInterpolant(idx + 1) - LOG.trace { "Removing Slice: $idx" } - removeAt(idx).getSlice() - removeIfInterpolant(idx - 1) - - notifyListeners() - - return true - } + for (idx in indices) { + if (idx >= 0 && idx <= size - 1 && get(idx).equals(slice)) { + removeIfInterpolant(idx + 1) + LOG.trace { "Removing Slice: $idx" } + removeAt(idx).getSlice() + removeIfInterpolant(idx - 1) + return true } - return false } + return false } fun removeSliceAtDepth(depth: Double): SliceInfo? { - synchronized(this) { - return getSliceAtDepth(depth)?.also { - removeSlice(it) - } + return getSliceAtDepth(depth)?.also { + removeSlice(it) } } fun removeIfInterpolant(idx: Int): InterpolantInfo? { - synchronized(this) { - return if (idx >= 0 && idx <= size - 1 && get(idx).isInterpolant) { - LOG.trace { "Removing Interpolant: $idx" } - val interp = removeAt(idx).getInterpolant() - - notifyListeners() - - interp - } else null - } + return if (idx >= 0 && idx <= size - 1 && get(idx).isInterpolant) { + LOG.trace { "Removing Interpolant: $idx" } + removeAt(idx).getInterpolant() + } else null } @@ -1043,22 +1017,16 @@ class ShapeInterpolationController>( } fun add(depth: Double, sliceOrInterpolant: SliceOrInterpolant) { - synchronized(this) { - for (idx in this.indices) { - if (get(idx).isSlice && get(idx).sliceDepth > depth) { - LOG.trace { "Adding Slice: $idx" } - add(idx, sliceOrInterpolant) - removeIfInterpolant(idx - 1) - return - } - } - LOG.trace { "Adding Slice: ${this.size}" } - add(sliceOrInterpolant) - - InvokeOnJavaFXApplicationThread { - listeners.forEach { it.invalidated(this@SlicesAndInterpolants) } + for (idx in this.indices) { + if (get(idx).isSlice && get(idx).sliceDepth > depth) { + LOG.trace { "Adding Slice: $idx" } + add(idx, sliceOrInterpolant) + removeIfInterpolant(idx - 1) + return } } + LOG.trace { "Adding Slice: ${this.size}" } + add(sliceOrInterpolant) } fun removeAllInterpolants() { @@ -1160,20 +1128,6 @@ class ShapeInterpolationController>( return false } } - - private val listeners = mutableListOf() - - override fun addListener(p0: InvalidationListener) { - listeners += p0 - } - - override fun removeListener(p0: InvalidationListener) { - listeners -= p0 - } - - private fun notifyListeners() = InvokeOnJavaFXApplicationThread { - listeners.forEach { it.invalidated(this@SlicesAndInterpolants) } - } } var initialGlobalToViewerTransform: AffineTransform3D? = null diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/ExportSourceState.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/ExportSourceState.kt new file mode 100644 index 000000000..207ec535e --- /dev/null +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/actions/ExportSourceState.kt @@ -0,0 +1,159 @@ +package org.janelia.saalfeldlab.paintera.control.actions + +import javafx.beans.property.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable.invokeOnCompletion +import kotlinx.coroutines.launch +import net.imglib2.RandomAccessibleInterval +import net.imglib2.img.cell.CellGrid +import net.imglib2.type.NativeType +import net.imglib2.type.numeric.IntegerType +import net.imglib2.type.numeric.integer.AbstractIntegerType +import org.janelia.saalfeldlab.n5.DataType +import org.janelia.saalfeldlab.n5.DatasetAttributes +import org.janelia.saalfeldlab.n5.GsonKeyValueN5Reader +import org.janelia.saalfeldlab.n5.N5Writer +import org.janelia.saalfeldlab.n5.imglib2.N5Utils +import org.janelia.saalfeldlab.n5.universe.metadata.N5SpatialDatasetMetadata +import org.janelia.saalfeldlab.n5.universe.metadata.axes.Axis +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.OmeNgffMetadata +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.OmeNgffMetadataParser +import org.janelia.saalfeldlab.paintera.Paintera +import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource +import org.janelia.saalfeldlab.paintera.state.label.ConnectomicsLabelState +import org.janelia.saalfeldlab.paintera.state.label.n5.N5Backend +import org.janelia.saalfeldlab.paintera.state.metadata.MetadataUtils.Companion.offset +import org.janelia.saalfeldlab.paintera.state.metadata.MetadataUtils.Companion.resolution +import org.janelia.saalfeldlab.paintera.state.metadata.MultiScaleMetadataState +import org.janelia.saalfeldlab.paintera.state.metadata.get +import org.janelia.saalfeldlab.paintera.ui.dialogs.AnimatedProgressBarAlert +import org.janelia.saalfeldlab.util.convertRAI +import org.janelia.saalfeldlab.util.interval +import org.janelia.saalfeldlab.util.n5.N5Helpers.MAX_ID_KEY +import org.janelia.saalfeldlab.util.n5.N5Helpers.forEachBlockExists +import java.util.concurrent.atomic.AtomicInteger + +class ExportSourceState { + + val backendProperty = SimpleObjectProperty?>() + val maxIdProperty = SimpleLongProperty(-1) + val sourceStateProperty = SimpleObjectProperty?>() + val sourceProperty = SimpleObjectProperty?>() + + val datasetProperty = SimpleStringProperty() + val exportLocationProperty = SimpleStringProperty() + val segmentFragmentMappingProperty = SimpleBooleanProperty(true) + val scaleLevelProperty = SimpleIntegerProperty(0) + val dataTypeProperty = SimpleObjectProperty(DataType.UINT64) + + private val exportableSourceRAI: RandomAccessibleInterval>? + get() { + val source = sourceProperty.value ?: return null + val backend = backendProperty.value ?: return null + + val fragmentMapper = backend.fragmentSegmentAssignment + + val scaleLevel = scaleLevelProperty.value + val mapFragmentToSegment = segmentFragmentMappingProperty.value + val dataType = dataTypeProperty.value + + + val dataSource = (source.getDataSource(0, scaleLevel) as? RandomAccessibleInterval>)!! + val typeVal = N5Utils.type(dataType)!! as AbstractIntegerType> + + val mappedIntSource = if (mapFragmentToSegment) + dataSource.convertRAI(typeVal) { src, target -> target.setInteger(fragmentMapper.getSegment(src.integerLong)) } + else + dataSource + + return mappedIntSource as RandomAccessibleInterval> + } + + //TODO Caleb: some future ideas: + // - Export specific label? Maybe only if LabelBlockLookup is present? + // - Export multiscale pyramid + // - Export interval of label source + // - custom fragment to segment mapping + fun exportSource(showProgressAlert : Boolean = false) { + + val backend = backendProperty.value ?: return + val source = sourceProperty.value ?: return + val exportLocation = exportLocationProperty.value ?: return + val dataset = datasetProperty.value ?: return + + + val scaleLevel = scaleLevelProperty.value + val dataType = dataTypeProperty.value + + val sourceMetadata: N5SpatialDatasetMetadata = backend.getMetadataState().let { it as? MultiScaleMetadataState }?.metadata?.get(scaleLevel) ?: backend.getMetadataState() as N5SpatialDatasetMetadata + val n5 = backend.container as GsonKeyValueN5Reader + + val exportRAI = exportableSourceRAI!! + val cellGrid: CellGrid = source.getCellGrid(0, scaleLevel) + val sourceAttributes: DatasetAttributes = sourceMetadata.attributes + + val exportAttributes = DatasetAttributes(sourceAttributes.dimensions, sourceAttributes.blockSize, dataType, sourceAttributes.compression) + + val totalBlocks = cellGrid.gridDimensions.reduce { acc, dim -> acc * dim } + val (processedBlocks, progressUpdater) = if (showProgressAlert) { + val count = AtomicInteger() + count to AnimatedProgressBarAlert( + "Export Label Source", + "Exporting data...", + "Blocks Written", + count::get, + totalBlocks.toInt() + ) + } else null to null + + val exportJob = CoroutineScope(Dispatchers.IO).launch { + val writer = Paintera.n5Factory.openWriter(exportLocation) + exportOmeNGFFMetadata(writer, dataset, scaleLevel, exportAttributes, sourceMetadata) + if (maxIdProperty.value > -1) + writer.setAttribute(dataset, MAX_ID_KEY, maxIdProperty.value) + val scaleLevelDataset = "$dataset/s$scaleLevel" + + forEachBlockExists(n5, sourceMetadata.path, processedBlocks) { cellInterval -> + val cellRai = exportRAI.interval(cellInterval) + N5Utils.saveBlock(cellRai, writer, scaleLevelDataset, exportAttributes) + } + Paintera.n5Factory.clearKey(exportLocation) + } + progressUpdater?.apply { + exportJob.invokeOnCompletion { finish() } + showAndStart() + } + } +} + +internal fun exportOmeNGFFMetadata( + writer: N5Writer, + dataset: String, + scaleLevel: Int, + datasetAttributes: DatasetAttributes, + sourceMetadata: N5SpatialDatasetMetadata +) { + val scaleLevelDataset = "$dataset/s$scaleLevel" + writer.createGroup(dataset) + writer.createDataset(scaleLevelDataset, datasetAttributes) + + val exportMetadata = OmeNgffMetadata.buildForWriting( + datasetAttributes.numDimensions, + dataset, + arrayOf( + Axis(Axis.SPACE, "x", sourceMetadata.unit(), false), + Axis(Axis.SPACE, "y", sourceMetadata.unit(), false), + Axis(Axis.SPACE, "z", sourceMetadata.unit(), false) + ), + arrayOf("s$scaleLevel"), + arrayOf(sourceMetadata.resolution), + arrayOf(sourceMetadata.offset) + ) + + OmeNgffMetadataParser().writeMetadata( + exportMetadata, + writer, + dataset + ) +} \ No newline at end of file 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 84a804cdb..97f26a554 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 @@ -17,6 +17,7 @@ import javafx.scene.input.KeyEvent.KEY_PRESSED import javafx.scene.input.MouseEvent import javafx.scene.input.MouseEvent.MOUSE_CLICKED import javafx.scene.layout.GridPane +import javafx.util.Subscription import kotlinx.coroutines.* import org.janelia.saalfeldlab.fx.actions.ActionSet import org.janelia.saalfeldlab.fx.actions.ActionSet.Companion.installActionSet @@ -89,29 +90,22 @@ interface ToolMode : SourceMode { LOG.debug { "Switch from $activeTool to $tool" } - /* Deactivate off the main thread */ - val deactivateJob = CoroutineScope(Dispatchers.Default).launch { + /* deactivate/activate off the main thread */ + val switchToolJob = CoroutineScope(Dispatchers.Default).launch { LOG.trace { "Deactivated $activeTool" } activeTool?.deactivate() - } + (activeTool as? ViewerTool)?.removeFromAll() - /* Activate ON the main thread (maybe should refactor this in the future) */ - val activateJob = CoroutineScope(Dispatchers.Main.immediate).launch(start = CoroutineStart.LAZY) { - (activeTool as? ViewerTool)?.removeFromAll() - showToolBars() - tool?.activate() - activeTool = tool + /* If the mode was changed before we can activate, switch to null */ + val activeMode = paintera.baseView.activeModeProperty.value + activeTool = if (activeMode != this@ToolMode) null else tool?.apply { activate() } LOG.trace { "Activated $activeTool" } } - deactivateJob.invokeOnCompletion { - /* If the mode was changed before we can activate, don't activate anymore */ - if (paintera.baseView.activeModeProperty.value == this@ToolMode) - activateJob.start() - } - return activateJob + switchToolJob.invokeOnCompletion { InvokeOnJavaFXApplicationThread { showToolBars() } } + return switchToolJob } private fun showToolBars(show: Boolean = true) { @@ -160,7 +154,8 @@ interface ToolMode : SourceMode { toggles .firstOrNull { it.userData == newTool } ?.also { toggleForTool -> selectToggle(toggleForTool) } - toolActionsBar.set(*newTool.actionSets.toTypedArray()) + val toolActionSets = newTool.actionSets.toTypedArray() + InvokeOnJavaFXApplicationThread { toolActionsBar.set(*toolActionSets) } } } } @@ -373,28 +368,7 @@ abstract class AbstractToolMode : AbstractSourceMode(), ToolMode { override val modeToolsBar: ActionBar = ActionBar() override val toolActionsBar: ActionBar = ActionBar() - override val statusProperty: StringProperty = SimpleStringProperty().apply { - activeToolProperty.addListener { _, _, new -> - new?.let { - bind(it.statusProperty) - } ?: unbind() - } - } - - private val activeViewerToolHandler = ChangeListener { _, old, new -> - (activeTool as? ViewerTool)?.let { tool -> - old?.viewer()?.let { tool.removeFrom(it) } - new?.viewer()?.let { tool.installInto(it) } - } - } - - private val activeToolHandler = ChangeListener { _, old, new -> - activeViewerProperty.get()?.let { viewer -> - (old as? ViewerTool)?.removeFrom(viewer.viewer()) - (new as? ViewerTool)?.installInto(viewer.viewer()) - } - } - + override val statusProperty: StringProperty = SimpleStringProperty() protected fun escapeToDefault() = painteraActionSet("escape to default") { KEY_PRESSED(KeyCode.ESCAPE) { /* Don't change to default if we are default */ @@ -407,15 +381,20 @@ abstract class AbstractToolMode : AbstractSourceMode(), ToolMode { override fun enter() { super.enter() - activeViewerProperty.addListener(activeViewerToolHandler) - activeToolProperty.addListener(activeToolHandler) + var statusSubscription : Subscription? = null + activeToolProperty.subscribe { tool -> + statusSubscription?.unsubscribe() + statusSubscription = tool?.statusProperty?.subscribe { status -> + InvokeOnJavaFXApplicationThread { + this@AbstractToolMode.statusProperty.set(status) + } + } + } switchTool(activeTool ?: defaultTool) } override fun exit() { runBlocking { switchTool(null)?.join() } - activeToolProperty.removeListener(activeToolHandler) - activeViewerProperty.removeListener(activeViewerToolHandler) super.exit() } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/NavigationControlMode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/NavigationControlMode.kt index cce6bc7cf..80a5998f2 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/NavigationControlMode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/NavigationControlMode.kt @@ -67,8 +67,6 @@ object NavigationControlMode : AbstractToolMode() { override val allowedActions = AllowedActions.NAVIGATION - override val defaultTool: Tool = NavigationTool - override val tools: ObservableList = FXCollections.observableArrayList(NavigationTool) } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt index b9fb31ade..a0c86f63b 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/PaintLabelMode.kt @@ -10,6 +10,7 @@ import javafx.scene.input.KeyCode import javafx.scene.input.KeyEvent.KEY_PRESSED import javafx.scene.input.KeyEvent.KEY_RELEASED import kotlinx.coroutines.Job +import kotlinx.coroutines.runBlocking import net.imglib2.type.numeric.IntegerType import org.janelia.saalfeldlab.control.mcu.MCUButtonControl import org.janelia.saalfeldlab.fx.actions.ActionSet @@ -52,8 +53,6 @@ object PaintLabelMode : AbstractToolMode() { private val fill3DTool = Fill3DTool(activeSourceStateProperty, this) private val intersectTool = IntersectPaintWithUnderlyingLabelTool(activeSourceStateProperty, this) - override val defaultTool = NavigationTool - override val tools: ObservableList by lazy { FXCollections.observableArrayList( NavigationTool, @@ -184,6 +183,7 @@ object PaintLabelMode : AbstractToolMode() { override fun switchTool(tool: Tool?) : Job? { val switchToolJob = super.switchTool(tool) /*SAM Tool restrict the active ViewerPanel, so we don't want it changing on mouseover of the other views, for example */ + (tool as? SamTool)?.let { runBlocking { switchToolJob?.join() } } if (activeTool is SamTool) activeViewerProperty.unbind() else if (!activeViewerProperty.isBound) @@ -245,7 +245,6 @@ object PaintLabelMode : AbstractToolMode() { private fun newShapeInterpolationModeForSource(sourceState: SourceState<*, *>?): ShapeInterpolationMode<*>? { return sourceState?.let { state -> - @Suppress("UNCHECKED_CAST") (state as? ConnectomicsLabelState<*, *>)?.run { (dataSource as? MaskedSource, *>)?.let { maskedSource -> ShapeInterpolationController( diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/RawSourceMode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/RawSourceMode.kt index 52a11ab2a..d74dd700b 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/RawSourceMode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/RawSourceMode.kt @@ -39,8 +39,6 @@ import org.janelia.saalfeldlab.util.* object RawSourceMode : AbstractToolMode() { - override val defaultTool: Tool = NavigationTool - override val tools: ObservableList = FXCollections.observableArrayList() override val allowedActions = AllowedActions.NAVIGATION @@ -125,7 +123,7 @@ object RawSourceMode : AbstractToolMode() { private fun > estimateWithHistogram(type: T, screenSource: IntervalView>, rawSource: ConnectomicsRawState<*, *>, converter: ARGBColorConverter>) { val binMapper = Real1dBinMapper(converter.min, converter.max, 4, false) val histogram = Histogram1d(binMapper) - val img = screenSource.convert(type) { src, target -> target.setReal(src.realDouble) }.asIterable() + val img = screenSource.convertRAI(type) { src, target -> target.setReal(src.realDouble) }.asIterable() histogram.countData(img) val numPixels = screenSource.dimensionsAsLongArray().sum() 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 0214cbe71..e1d57718d 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 @@ -79,7 +79,7 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat override val modeActions by lazy { modeActions() } override val allowedActions = AllowedActions.AllowedActionsBuilder() - .add(PaintActionType.ShapeInterpolation, PaintActionType.Paint, PaintActionType.Erase, PaintActionType.SetBrushSize, PaintActionType.Fill) + .add(PaintActionType.ShapeInterpolation, PaintActionType.Paint, PaintActionType.Erase, PaintActionType.SetBrushSize, PaintActionType.Fill, PaintActionType.SegmentAnything) .add(MenuActionType.ToggleMaximizeViewer, MenuActionType.DetachViewer) .add(NavigationActionType.Pan, NavigationActionType.Slice, NavigationActionType.Zoom) .create() @@ -190,8 +190,8 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat filter = true verify("Fill2DTool is active") { activeTool is Fill2DTool } onAction { - val switchBack = { InvokeOnJavaFXApplicationThread { switchTool(shapeInterpolationTool) } } - fill2DTool.fillJob?.invokeOnCompletion { switchBack() } ?: switchBack + val switchBack = { switchTool(shapeInterpolationTool) } + fill2DTool.fillJob?.invokeOnCompletion { switchBack() } ?: switchBack() } } }, @@ -371,23 +371,25 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat } } - internal fun cacheLoadSamSliceInfo(depth: Double, translate: Boolean = depth != controller.currentDepth): SamSliceInfo { + internal fun cacheLoadSamSliceInfo(depth: Double, translate: Boolean = depth != controller.currentDepth, provideGlobalToViewerTransform : AffineTransform3D? = null): SamSliceInfo { return samSliceCache[depth] ?: with(controller) { val viewerAndTransforms = this@ShapeInterpolationMode.activeViewerProperty.value!! val viewer = viewerAndTransforms.viewer()!! val width = viewer.width val height = viewer.height - val globalToViewerTransform = if (translate) { - calculateGlobalToViewerTransformAtDepth(depth) - } else { - AffineTransform3D().also { viewerAndTransforms.viewer().state.getViewerTransform(it) } + val globalToViewerTransform = when { + provideGlobalToViewerTransform != null -> provideGlobalToViewerTransform + translate -> calculateGlobalToViewerTransformAtDepth(depth) + else -> AffineTransform3D().also { viewerAndTransforms.viewer().state.getViewerTransform(it) } } - val maxDistancePositions = controller.getInterpolationImg(globalToViewerTransform, closest = true)?.let { - val interpolantInViewer = if (translate) alignTransformAndViewCenter(it, globalToViewerTransform, width, height) else it - interpolantInViewer.getComponentMaxDistancePosition() - } ?: listOf(doubleArrayOf(width / 2.0, height / 2.0, 0.0)) + val predictionPositions = provideGlobalToViewerTransform?.let { listOf(doubleArrayOf(width / 2.0, height / 2.0, 0.0)) } ?: let { + controller.getInterpolationImg(globalToViewerTransform, closest = true)?.let { + val interpolantInViewer = if (translate) alignTransformAndViewCenter(it, globalToViewerTransform, width, height) else it + interpolantInViewer.getComponentMaxDistancePosition() + } ?: listOf(doubleArrayOf(width / 2.0, height / 2.0, 0.0)) + } val maskInfo = MaskInfo(0, currentBestMipMapLevel) @@ -399,7 +401,7 @@ class ShapeInterpolationMode>(val controller: ShapeInterpolat .toList() val renderState = RenderUnitState(mask.initialGlobalToViewerTransform.copy(), mask.info.time, sources, width.toLong(), height.toLong()) - val predictionRequest = SamPredictor.SparsePrediction(maxDistancePositions.map { (x, y) -> renderState.getSamPoint(x, y, SamPredictor.SparseLabel.IN) }) + val predictionRequest = SamPredictor.SparsePrediction(predictionPositions.map { (x, y) -> renderState.getSamPoint(x, y, SamPredictor.SparseLabel.IN) }) SamSliceInfo(renderState, mask, predictionRequest, null, false).also { SamEmbeddingLoaderCache.load(renderState) @@ -516,8 +518,8 @@ internal fun IntervalView.getComponentMaxDistancePosition(): L /* find the max point to initialize with */ val invalidBorderRai = extendValue(Label.INVALID).interval(Intervals.expand(this, 1, 1, 0)) val distances = ArrayImgs.doubles(*invalidBorderRai.dimensionsAsLongArray()) - val binaryImg = invalidBorderRai.convert(BoolType()) { source, target -> target.set((source.get() != Label.INVALID && source.get() != Label.TRANSPARENT)) }.zeroMin() - val invertedBinaryImg = binaryImg.convert(BoolType()) { source, target -> target.set(!source.get()) } + val binaryImg = invalidBorderRai.convertRAI(BoolType()) { source, target -> target.set((source.get() != Label.INVALID && source.get() != Label.TRANSPARENT)) }.zeroMin() + val invertedBinaryImg = binaryImg.convertRAI(BoolType()) { source, target -> target.set(!source.get()) } val connectedComponents = ArrayImgs.unsignedInts(*binaryImg.dimensionsAsLongArray()) ConnectedComponents.labelAllConnectedComponents( diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/ViewLabelMode.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/ViewLabelMode.kt index 122b46d20..71b289192 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/ViewLabelMode.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/modes/ViewLabelMode.kt @@ -13,8 +13,6 @@ import org.janelia.saalfeldlab.paintera.control.tools.Tool object ViewLabelMode : AbstractToolMode() { - override val defaultTool: Tool = NavigationTool - override val tools: ObservableList = FXCollections.observableArrayList() override val modeActions: List = listOf() 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 index ee4ee4f15..5becc69c8 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/FloodFill.kt @@ -29,7 +29,6 @@ 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, @@ -39,20 +38,26 @@ class FloodFill>( private val isVisible: BooleanSupplier ) { - fun fillAt(x: Double, y: Double, fillSupplier: (() -> Long?)?): Job? { + 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 Job().apply { + val reason = CancellationException("Received invalid label -- will not fill.") + LOG.debug(reason) { } + completeExceptionally(reason) + } } return fillAt(x, y, fill) } - private fun fillAt(x: Double, y: Double, fill: Long): Job? { + 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 + return Job().apply { + val reason = CancellationException("Selected source is not visible -- will not fill") + LOG.debug(reason) { } + completeExceptionally(reason) + } } val level = 0 @@ -67,12 +72,12 @@ class FloodFill>( sourceSeed.setPosition(Math.round(realSourceSeed.getDoublePosition(d)), d) } - LOG.debug("Filling source {} with label {} at {}", source, fill, sourceSeed) + LOG.debug { "Filling source $source with label $fill at $sourceSeed" } try { return fill(time, level, fill, sourceSeed, assignment) } catch (e: MaskInUse) { - LOG.info(e) {} - return null + LOG.error(e) {} + return Job().apply { completeExceptionally(e) } } } @@ -83,15 +88,16 @@ class FloodFill>( fill: Long, seed: Localizable, assignment: FragmentSegmentAssignment? - ): Job? { + ): 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 reason = CancellationException("Cannot fill at irregular label: $seedLabel (${Point(seed)})") + LOG.debug(reason) { } + return Job().apply { completeExceptionally(reason) } } val maskInfo = MaskInfo( 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 index b23bf3c88..ac0377f1b 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/FloodFill2D.kt @@ -32,6 +32,7 @@ import org.janelia.saalfeldlab.paintera.control.paint.ViewerMask.Companion.creat 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.convertRAI import org.janelia.saalfeldlab.util.extendValue import java.util.concurrent.atomic.AtomicBoolean import java.util.function.BooleanSupplier @@ -157,14 +158,10 @@ class FloodFill2D>( throw CancellationException(reason) } - return Converters.convert( - backgroundViewerRai, - { src: RealType?>?, target: BoolType -> - val segmentId = assignment.getSegment(src!!.realDouble.toLong()) - target.set(segmentId == seedLabel) - }, - BoolType() - ) + return backgroundViewerRai.convertRAI(BoolType()) { src, target -> + val segmentId = assignment.getSegment(src!!.realDouble.toLong()) + target.set(segmentId == seedLabel) + } } fun fillAt( diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/ViewerMask.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/ViewerMask.kt index fa3afc3f7..6e738271a 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/ViewerMask.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/paint/ViewerMask.kt @@ -4,7 +4,6 @@ import bdv.util.Affine3DHelpers import javafx.beans.property.SimpleBooleanProperty import net.imglib2.* import net.imglib2.cache.Invalidate -import net.imglib2.converter.Converters import net.imglib2.loops.LoopBuilder import net.imglib2.realtransform.AffineTransform3D import net.imglib2.realtransform.RealViews @@ -217,10 +216,13 @@ class ViewerMask private constructor( @JvmOverloads fun displayPointToMask(displayX: Int, displayY: Int, pointInCurrentDisplay: Boolean = false) = displayPointToMask(Point(displayX, displayY, 0), pointInCurrentDisplay) + @JvmOverloads fun displayPointToMask(displayX: Double, displayY: Double, pointInCurrentDisplay: Boolean = false) = displayPointToMask(RealPoint(displayX, displayY, 0.0), pointInCurrentDisplay) + @JvmOverloads fun displayPointToMask(displayPoint: Point, pointInCurrentDisplay: Boolean = false) = displayPointToMask(displayPoint.positionAsRealPoint(), pointInCurrentDisplay) + @JvmOverloads fun displayPointToMask(displayPoint: RealPoint, pointInCurrentDisplay: Boolean = false): Point { val globalToMask = if (pointInCurrentDisplay) currentGlobalToViewerTransform else initialGlobalToViewerTransform @@ -280,41 +282,36 @@ class ViewerMask private constructor( internal fun pushNewImageLayer(writableSourceImages: Pair, RandomAccessibleInterval>? = newBackingImages()) { writableSourceImages?.let { (newImg, newVolatileImg) -> - val compositeMask = Converters.convert( - viewerImg.wrappedSource.extendValue(Label.INVALID), - newImg.extendValue(Label.INVALID), - { oldVal, newVal, result -> - val new = newVal.get() - if (new != Label.INVALID) { - result.set(new) - } else result.set(oldVal) - }, - UnsignedLongType(Label.INVALID) - ).interval(newImg) - - val compositeVolatileMask = Converters.convert( - volatileViewerImg.wrappedSource.extendValue(VolatileUnsignedLongType(Label.INVALID)), - newVolatileImg.extendValue(VolatileUnsignedLongType(Label.INVALID)), - { original, overlay, composite -> - - var checkOriginal = false - if (overlay.isValid) { - val overlayVal = overlay.get().get() - if (overlayVal != Label.INVALID) { - composite.get().set(overlayVal) - composite.isValid = true - } else checkOriginal = true + + + val currentRA = viewerImg.wrappedSource.extendValue(Label.INVALID) + val newRA = newImg.extendValue(Label.INVALID) + val compositeMask = currentRA.convertWith(newRA, UnsignedLongType(Label.INVALID)) { oldVal, newVal, result -> + val new = newVal.get() + if (new != Label.INVALID) { + result.set(new) + } else result.set(oldVal) + }.interval(newImg) + + val currentVolatileRA = volatileViewerImg.wrappedSource.extendValue(VolatileUnsignedLongType(Label.INVALID)) + val newVolatileRA = newVolatileImg.extendValue(VolatileUnsignedLongType(Label.INVALID)) + val compositeVolatileMask = currentVolatileRA.convertWith(newVolatileRA, VolatileUnsignedLongType(Label.INVALID)) { original, overlay, composite -> + var checkOriginal = false + if (overlay.isValid) { + val overlayVal = overlay.get().get() + if (overlayVal != Label.INVALID) { + composite.get().set(overlayVal) + composite.isValid = true } else checkOriginal = true - if (checkOriginal) { - if (original.isValid) { - composite.set(original) - composite.isValid = true - } else composite.isValid = false - } - composite.isValid = true - }, - VolatileUnsignedLongType(Label.INVALID) - ).interval(newVolatileImg) + } else checkOriginal = true + if (checkOriginal) { + if (original.isValid) { + composite.set(original) + composite.isValid = true + } else composite.isValid = false + } + composite.isValid = true + }.interval(newVolatileImg) val wrappedCompositeMask = WrappedRandomAccessibleInterval(compositeMask) wrappedCompositeMask.writableSource = WrappedRandomAccessibleInterval(viewerImg.wrappedSource).apply { writableSource = viewerImg.writableSource } @@ -520,7 +517,7 @@ class ViewerMask private constructor( * @return The screen interval in ViewerMask space. */ @JvmOverloads - fun getScreenInterval(width: Long = viewer.width.toLong(), height: Long = viewer.height.toLong(), currentScreenInterval : Boolean = false): Interval { + fun getScreenInterval(width: Long = viewer.width.toLong(), height: Long = viewer.height.toLong(), currentScreenInterval: Boolean = false): Interval { val (x: Long, y: Long) = displayPointToMask(0, 0, currentScreenInterval) return Intervals.createMinSize(x, y, 0, width, height, 1) } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/Tool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/Tool.kt index 11dbabf1c..ebda557ef 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/Tool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/Tool.kt @@ -7,6 +7,7 @@ import javafx.beans.property.StringProperty import javafx.event.EventHandler import javafx.scene.Node import javafx.scene.control.* +import javafx.util.Subscription import org.janelia.saalfeldlab.bdv.fx.viewer.ViewerPanelFX import org.janelia.saalfeldlab.fx.actions.Action import org.janelia.saalfeldlab.fx.actions.ActionSet @@ -81,13 +82,26 @@ const val REQUIRES_ACTIVE_VIEWER = "REQUIRES_ACTIVE_VIEWER" abstract class ViewerTool(protected val mode: ToolMode? = null) : Tool, ToolBarItem { private val installedInto: MutableMap> = mutableMapOf() + private var subscriptions : Subscription? = null override fun activate() { activeViewerProperty.bind(mode?.activeViewerProperty ?: paintera.baseView.lastFocusHolder) + /* This handles viewer changes while activated */ + val viewerPropSubscription = activeViewerProperty.subscribe { old, new -> + old?.viewer()?.let { removeFrom(it) } + new?.viewer()?.let { installInto(it) } + } + /* this handles installing into the currently active viewer */ + activeViewerProperty.get()?.viewer()?.let { installInto(it) } + subscriptions = subscriptions?.and(viewerPropSubscription) ?: viewerPropSubscription } override fun deactivate() { - activeViewerAndTransforms?.viewer()?.let { removeFrom(it) } + subscriptions?.let { + it.unsubscribe() + subscriptions = null + } + removeFromAll() activeViewerProperty.unbind() activeViewerProperty.set(null) } @@ -111,8 +125,8 @@ abstract class ViewerTool(protected val mode: ToolMode? = null) : Tool, ToolBarI fun removeFromAll() { synchronized(this) { + LOG.debug { "removing $this from all nodes" } installedInto.forEach { (node, actions) -> - LOG.debug { "removing $this from all nodes" } actions.removeIf { actionSet -> node.removeActionSet(actionSet) true 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 3defa9994..75525f49a 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 @@ -106,7 +106,7 @@ class Fill3DTool(activeSourceStateProperty: SimpleObjectProperty + task.invokeOnCompletion { cause -> fillIsRunningProperty.set(false) paintera.baseView.disabledPropertyBindings -= this statePaintContext?.refreshMeshes?.invoke() diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/IntersectPaintWithUnderlyingLabelTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/IntersectPaintWithUnderlyingLabelTool.kt index 22f014952..40fef65bc 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/IntersectPaintWithUnderlyingLabelTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/IntersectPaintWithUnderlyingLabelTool.kt @@ -42,7 +42,7 @@ class IntersectPaintWithUnderlyingLabelTool(activeSourceStateProperty: SimpleObj override val actionSets: MutableList by LazyForeignValue({ activeViewerAndTransforms }) { mutableListOf( - *super.actionSets.toTypedArray(), + *super.actionSets.toTypedArray(), painteraActionSet("intersect", PaintActionType.Intersect) { MouseEvent.MOUSE_PRESSED(MouseButton.PRIMARY) { keysExclusive = false diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/PaintBrushTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/PaintBrushTool.kt index d424c3a36..95267288c 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/PaintBrushTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/paint/PaintBrushTool.kt @@ -59,6 +59,7 @@ open class PaintBrushTool(activeSourceStateProperty: SimpleObjectProperty Unit = { - statePaintContext?.selectedIds?.lastSelection?.let { currentLabelToPaint = it } + statePaintContext?.selectedIds?.lastSelection?.let { setCurrentLabel(it) } } override fun activate() { super.activate() - setCurrentLabelToSelection() + setCurrentLabel() paint2D.setOverlayValidState() statePaintContext?.selectedIds?.apply { addListener(selectedIdListener) } activeViewerProperty.get()?.viewer()?.scene?.addEventFilter(KEY_PRESSED, filterSpaceHeldDown) @@ -141,7 +142,7 @@ open class PaintBrushTool(activeSourceStateProperty: SimpleObjectProperty - Platform.runLater { statusProperty.set("Predicting...") } + statusProperty.set("Predicting...") val x = viewer.mouseXProperty.get().toLong() val y = viewer.mouseYProperty.get().toLong() @@ -328,7 +327,7 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty lateinit var applyPredictionAction: Action<*> return arrayOf( - painteraActionSet("sam selections", PaintActionType.Paint, ignoreDisable = true) { + painteraActionSet("sam-selections", PaintActionType.Paint, ignoreDisable = true) { /* Handle Painting */ MOUSE_CLICKED(MouseButton.PRIMARY, withKeysDown = arrayOf(KeyCode.CONTROL)) { name = "apply last segmentation result to canvas" @@ -375,7 +374,7 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty output.set(input.get() - min) } + val zeroMinValue = maskRai.convertRAI(FloatType()) { input, output -> output.set(input.get() - min) } val predictionSource = paintera.baseView.addConnectomicsRawSource( zeroMinValue.let { val prediction3D = Views.addDimension(it) @@ -879,7 +878,7 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty + val thresholdFilter = BundleView(predictedImage.extendValue(Float.NEGATIVE_INFINITY)) + .convert(BoolType()) { predictionMaskRA, output -> val predictionType = predictionMaskRA.get() val predictionValue = predictionType.get() val accept = predictionValue >= threshold @@ -920,9 +918,8 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty = ArrayImgs.unsignedLongs(*predictedImage.dimensionsAsLongArray()) /* FIXME: This is annoying, but I don't see a better way around it at the moment. @@ -933,7 +930,7 @@ open class SamTool(activeSourceStateProperty: SimpleObjectProperty - output.set(if (source.get() in acceptedComponents) 1.0f else 0.0f) - }, - FloatType() - ) + val selectedComponents = connectedComponents.convertRAI(FloatType()) { source, output -> + val value = if (source.get() in acceptedComponents) 1.0f else 0.0f + output.set(value) + } val (width, height) = predictedImage.dimensionsAsLongArray() val predictionToViewerScale = Scale2D(setViewer!!.width / width, setViewer!!.height / height) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationPaintBrushTool.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationPaintBrushTool.kt index c57ae2338..b4e7fe40b 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationPaintBrushTool.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/control/tools/shapeinterpolation/ShapeInterpolationPaintBrushTool.kt @@ -105,7 +105,7 @@ internal class ShapeInterpolationPaintBrushTool(activeSourceStateProperty: Simpl verify { activeTool == this@ShapeInterpolationPaintBrushTool } onAction { paintClickOrDrag?.apply { - currentLabelToPaint = controller.interpolationId + setCurrentLabel(controller.interpolationId) } } } @@ -117,7 +117,7 @@ internal class ShapeInterpolationPaintBrushTool(activeSourceStateProperty: Simpl verify { activeTool == this@ShapeInterpolationPaintBrushTool } onAction { paintClickOrDrag!!.apply { - currentLabelToPaint = Label.TRANSPARENT + setCurrentLabel(Label.TRANSPARENT) } } } 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 e68d2a9c8..4a0fc8048 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 @@ -59,14 +59,15 @@ internal class ShapeInterpolationSAMTool(private val controller: ShapeInterpolat lastPrediction?.apply { /* cache the prediction. lock the cached slice, since this was applied manually */ super.applyPrediction() - shapeInterpolationMode.addSelection(maskInterval, replaceExistingSlice = replaceExistingSlice)?.also { - it.prediction = predictionRequest - it.locked = true - } - InvokeOnJavaFXApplicationThread { - shapeInterpolationMode.run { - switchTool(defaultTool) - modeToolsBar.toggleGroup?.selectToggle(null) + shapeInterpolationMode.run { + addSelection(maskInterval, replaceExistingSlice = replaceExistingSlice)?.also { + it.prediction = predictionRequest + it.locked = true + } + switchTool(defaultTool)?.invokeOnCompletion { + InvokeOnJavaFXApplicationThread { + 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 7dbb9fd14..cca8ac031 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 @@ -19,7 +19,7 @@ import org.janelia.saalfeldlab.fx.extensions.createNullableValueBinding import org.janelia.saalfeldlab.fx.extensions.nonnullVal 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.ortho.OrthogonalViews.ViewerAndTransforms import org.janelia.saalfeldlab.fx.ui.GlyphScaleView import org.janelia.saalfeldlab.fx.ui.ScaleView import org.janelia.saalfeldlab.labels.Label @@ -48,8 +48,7 @@ internal class ShapeInterpolationTool( override val actionSets: MutableList by lazy { mutableListOf( *shapeInterpolationActions().filterNotNull().toTypedArray(), - cancelShapeInterpolationTask(), - *NavigationTool.actionSets.toTypedArray() //Kinda ugly to filter like this, but this is a weird case. Still, should do better + cancelShapeInterpolationTask() ) } @@ -66,21 +65,18 @@ internal class ShapeInterpolationTool( paintera.baseView.orthogonalViews().viewerAndTransforms() .filter { !it.viewer().isFocusable } .forEach { disabledViewerAndTransform -> - val disabledTranslationActions = disabledViewerActions.computeIfAbsent(disabledViewerAndTransform, disabledViewerTranslateOnly) + val disabledTranslationActions = disabledViewerActionsMap.computeIfAbsent(disabledViewerAndTransform, disabledViewerActions) val disabledViewer = disabledViewerAndTransform.viewer() disabledTranslationActions.forEach { disabledViewer.installActionSet(it) } } - /* Activate, but we want to bind it to our activeViewer bindings instead of the default. */ - NavigationTool.activeViewerProperty.bind(activeViewerProperty) + /* We want to bind it to our activeViewer bindings instead of the default. */ + NavigationTool.activate() } override fun deactivate() { - /* We intentionally unbound the activeViewer for this, to support the button toggle. - * We now need to explicitly remove the NavigationTool from the activeViewer we care about. - * Still deactive it first, to handle the rest of the cleanup */ + disabledViewerActionsMap.forEach { (vat, actionSets) -> actionSets.forEach { vat.viewer().removeActionSet(it) } } + disabledViewerActionsMap.clear() NavigationTool.deactivate() - disabledViewerActions.forEach { (vat, actionSets) -> actionSets.forEach { vat.viewer().removeActionSet(it) } } - disabledViewerActions.clear() super.deactivate() } @@ -102,21 +98,19 @@ internal class ShapeInterpolationTool( } } - private val disabledViewerActions = mutableMapOf>() + private val disabledViewerActionsMap = mutableMapOf>() - private val disabledViewerTranslateOnly = { vat: OrthogonalViews.ViewerAndTransforms -> - val translator = vat.run { - val globalTransformManager = paintera.baseView.manager() - TranslationController(globalTransformManager, globalToViewerTransform) - } + private val disabledViewerActions = { vat: ViewerAndTransforms -> + val globalTransformManager = paintera.baseView.manager() + val translator = TranslationController(globalTransformManager, vat.globalToViewerTransform) arrayOf( - painteraDragActionSet("disabled_translate_xy", NavigationActionType.Pan) { + painteraDragActionSet("disabled_view_translate_xy", NavigationActionType.Pan) { relative = true verify { it.isSecondaryButtonDown } verify { controller.controllerState != ShapeInterpolationController.ControllerState.Interpolate } onDrag { translator.translate(it.x - startX, it.y - startY) } }, - painteraActionSet("disabled_move_to_cursor", NavigationActionType.Pan, ignoreDisable = true) { + painteraActionSet("disabled_view_move_to_cursor", NavigationActionType.Pan, ignoreDisable = true) { MOUSE_CLICKED(MouseButton.PRIMARY) { verify("only double click") { it?.clickCount!! > 1 } onAction { @@ -126,10 +120,57 @@ internal class ShapeInterpolationTool( translator.translate(x, y, 0.0, Duration.millis(500.0)) } } + }, + painteraActionSet("disabled_view_auto_sam_click", PaintActionType.SegmentAnything, ignoreDisable = true) { + MOUSE_CLICKED(MouseButton.PRIMARY, withKeysDown = arrayOf(KeyCode.SHIFT)) { + onAction { requestSamPredictionAtViewerPoint(vat) } + } } ) } + private fun requestSamPredictionAtViewerPoint(vat : ViewerAndTransforms, requestMidPoint : Boolean = true, runAfter : () -> Unit = {}) { + with(controller) { + val viewer = vat.viewer() + val dX = viewer.width / 2 - viewer.mouseXProperty.value + val dY = viewer.height / 2 - viewer.mouseYProperty.value + val delta = doubleArrayOf(dX, dY, 0.0) + + val globalTransform = paintera.baseView.manager().transform + TranslationController.translateFromViewer( + globalTransform, + vat.globalToViewerTransform.transformCopy, + delta + ) + + val resultActiveGlobalToViewer = activeViewerAndTransforms!!.displayTransform.transformCopy + .concatenate(activeViewerAndTransforms!!.viewerSpaceToViewerTransform.transformCopy) + .concatenate(globalTransform) + + val depth = depthAt(resultActiveGlobalToViewer) + if (!requestMidPoint) { + requestSamPrediction(depth, refresh = true, provideGlobalToViewerTransform = resultActiveGlobalToViewer) {runAfter() } + } else { + requestSamPrediction(depth, refresh = true, provideGlobalToViewerTransform = resultActiveGlobalToViewer) { + val depths = sortedSliceDepths + val sliceIdx = depths.indexOf(depth) + if (sliceIdx - 1 >= 0) { + val prevHalfDepth = (depths[sliceIdx] + depths[sliceIdx - 1]) / 2.0 + requestEmbedding(prevHalfDepth) + } + if (sliceIdx + 1 < depths.size) { + val nextHalfDepth = (depths[sliceIdx + 1] + depths[sliceIdx]) / 2.0 + requestEmbedding(nextHalfDepth) + } + runAfter() + } + } + + } + } + + + internal fun requestEmbedding(depth: Double) { shapeInterpolationMode.cacheLoadSamSliceInfo(depth) } @@ -144,6 +185,7 @@ internal class ShapeInterpolationTool( depth: Double, moveToSlice: Boolean = false, refresh: Boolean = false, + provideGlobalToViewerTransform: AffineTransform3D? = null, afterPrediction: (AffineTransform3D) -> Unit = {} ): AffineTransform3D { @@ -151,7 +193,7 @@ internal class ShapeInterpolationTool( if (newPrediction) SamEmbeddingLoaderCache.cancelPendingRequests() - val samSliceInfo = shapeInterpolationMode.cacheLoadSamSliceInfo(depth) + val samSliceInfo = shapeInterpolationMode.cacheLoadSamSliceInfo(depth, provideGlobalToViewerTransform = provideGlobalToViewerTransform) if (!newPrediction && refresh) { controller.getInterpolationImg(samSliceInfo.globalToViewerTransform, closest = true)?.getComponentMaxDistancePosition()?.let { positions -> diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/data/n5/LabelMultisetUtils.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/data/n5/LabelMultisetUtils.kt index 8c78a2a99..d78182470 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/data/n5/LabelMultisetUtils.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/data/n5/LabelMultisetUtils.kt @@ -13,6 +13,10 @@ import net.imglib2.util.Intervals import org.janelia.saalfeldlab.n5.N5Exception import org.janelia.saalfeldlab.n5.N5Reader import org.janelia.saalfeldlab.n5.imglib2.N5LabelMultisets +import java.nio.ByteBuffer +import java.util.function.BiFunction + +private val LOG = KotlinLogging.logger { } /** * Open an N5 dataset of [LabelMultisetType] as a memory cached @@ -55,6 +59,45 @@ fun openLabelMultiset( return cachedImg } +internal fun constantNullReplacementEmptyArgMax(id : Long): BiFunction = BiFunction { cellGrid, cellPos -> + + val cellMin = LongArray(cellPos.size) { d -> cellPos[d] * cellGrid.cellDimension(d) } + val cellDims = IntArray(cellMin.size) { d -> cellGrid.cellDimension(d) } + val numElements = cellDims.reduce { d1, d2 -> d1 * d2 } + + val listData = LongMappedAccessData.factory.createStorage(0) + val list = LabelMultisetEntryList(listData, 0) + val entry = LabelMultisetEntry(id, 1) + list.createListAt(listData, 0) + list.add(entry) + val listSize = list.sizeInBytes.toInt() + + val bytes = ByteArray( + Integer.BYTES // for argmax size (always zero) + + numElements * Integer.BYTES // for mappings + + list.sizeInBytes.toInt() // for actual entries (one single entry) + ) + + val bb = ByteBuffer.wrap(bytes) + + // argmax ; + // No longer necessary to serialize, since we can calculate fairly cheaply during deserialization + // Indicated by size 0 + bb.putInt(0) + + // offsets + repeat(numElements) { bb.putInt(0) } + + if (id != 0L) { + LOG.debug { "Putting id $id" } + repeat(listSize) { i -> + bb.put(ByteUtils.getByte(listData.data, i.toLong())) + } + } + LOG.debug { "Returning ${bytes.size} bytes for $numElements elements" } + bytes +} + class LabelMultisetCacheLoader(private val n5: N5Reader, private val dataset: String) : AbstractLabelMultisetLoader(generateCellGrid(n5, dataset)) { override fun getData(vararg gridPosition: Long): ByteArray? { @@ -90,8 +133,6 @@ class LabelMultisetCacheLoader(private val n5: N5Reader, private val dataset: St } companion object { - private val LOG = KotlinLogging.logger { } - private val EMPTY_ACCESS = VolatileLabelMultisetArray(0, true, longArrayOf(Label.INVALID)) private fun generateCellGrid(n5: N5Reader, dataset: String): CellGrid { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/data/n5/N5Adapter.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/data/n5/N5Adapter.kt deleted file mode 100644 index 39e40918f..000000000 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/data/n5/N5Adapter.kt +++ /dev/null @@ -1,212 +0,0 @@ -package org.janelia.saalfeldlab.paintera.data.n5 - -import com.google.gson.* -import org.janelia.saalfeldlab.n5.N5FSReader -import org.janelia.saalfeldlab.n5.N5FSWriter -import org.janelia.saalfeldlab.n5.N5Reader -import org.janelia.saalfeldlab.n5.googlecloud.N5GoogleCloudStorageReader -import org.janelia.saalfeldlab.n5.googlecloud.N5GoogleCloudStorageWriter -import org.janelia.saalfeldlab.n5.s3.N5AmazonS3Reader -import org.janelia.saalfeldlab.n5.s3.N5AmazonS3Writer -import org.janelia.saalfeldlab.n5.zarr.N5ZarrReader -import org.janelia.saalfeldlab.n5.zarr.N5ZarrWriter -import org.janelia.saalfeldlab.paintera.Paintera.Companion.n5Factory -import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions -import org.janelia.saalfeldlab.paintera.serialization.StatefulSerializer -import org.janelia.saalfeldlab.paintera.state.SourceState -import org.scijava.plugin.Plugin -import java.lang.reflect.Type -import java.util.function.IntFunction -import java.util.function.Supplier -import java.util.function.ToIntFunction - -private const val BASE_PATH = "basePath" - -private class N5ReaderSerializer(private val projectDirectory: Supplier) : JsonSerializer { - override fun serialize( - container: N5, - typeOfSrc: Type, - context: JsonSerializationContext, - ): JsonElement { - val projectDirectory = this.projectDirectory.get() - return JsonObject().also { jsonMap -> - container.uri!! - .takeUnless { it.path == projectDirectory } - ?.let { jsonMap.addProperty(BASE_PATH, it.toString()) } - } - } -} - -private class N5ReaderDeserializer( - private val projectDirectory: Supplier, - private val n5Constructor: (String) -> N5, -) : JsonDeserializer { - override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): N5 { - return with(GsonExtensions) { - n5Constructor(json.getStringProperty(BASE_PATH) ?: projectDirectory.get()) - } - } -} - - -//TODO Caleb: HDF5 is handled elsewhere; decide what to do about that (or nothing?) -@Plugin(type = StatefulSerializer.SerializerAndDeserializer::class) -class N5FSReaderAdapter : StatefulSerializer.SerializerAndDeserializer, JsonSerializer> { - - override fun createSerializer( - projectDirectory: Supplier, - stateToIndex: ToIntFunction>, - ): JsonSerializer = N5ReaderSerializer(projectDirectory) - - override fun createDeserializer( - arguments: StatefulSerializer.Arguments, - projectDirectory: Supplier, - dependencyFromIndex: IntFunction>?, - ): JsonDeserializer = N5ReaderDeserializer(projectDirectory) { - n5Factory.openReader(it) as N5FSReader - } - - override fun getTargetClass() = N5FSReader::class.java -} - -@Plugin(type = StatefulSerializer.SerializerAndDeserializer::class) -class N5FSWriterAdapter : StatefulSerializer.SerializerAndDeserializer, JsonSerializer> { - - override fun createSerializer( - projectDirectory: Supplier, - stateToIndex: ToIntFunction>, - ): JsonSerializer = N5ReaderSerializer(projectDirectory) - - override fun createDeserializer( - arguments: StatefulSerializer.Arguments, - projectDirectory: Supplier, - dependencyFromIndex: IntFunction>?, - ): JsonDeserializer = N5ReaderDeserializer(projectDirectory) { - n5Factory.openWriterElseOpenReader(it) as N5FSWriter - } - - override fun getTargetClass() = N5FSWriter::class.java -} - - -@Plugin(type = StatefulSerializer.SerializerAndDeserializer::class) -class N5GoogleCloudReaderAdapter : - StatefulSerializer.SerializerAndDeserializer, JsonSerializer> { - - override fun createSerializer( - projectDirectory: Supplier, - stateToIndex: ToIntFunction>, - ): JsonSerializer = N5ReaderSerializer(projectDirectory) - - override fun createDeserializer( - arguments: StatefulSerializer.Arguments, - projectDirectory: Supplier, - dependencyFromIndex: IntFunction>?, - ): JsonDeserializer = N5ReaderDeserializer(projectDirectory) { - n5Factory.openReader(it) as N5GoogleCloudStorageReader - } - - override fun getTargetClass() = N5GoogleCloudStorageReader::class.java -} - -@Plugin(type = StatefulSerializer.SerializerAndDeserializer::class) -class N5GoogleCloudWriterAdapter : - StatefulSerializer.SerializerAndDeserializer, JsonSerializer> { - - override fun createSerializer( - projectDirectory: Supplier, - stateToIndex: ToIntFunction>, - ): JsonSerializer = N5ReaderSerializer(projectDirectory) - - override fun createDeserializer( - arguments: StatefulSerializer.Arguments, - projectDirectory: Supplier, - dependencyFromIndex: IntFunction>?, - ): JsonDeserializer = N5ReaderDeserializer(projectDirectory) { - n5Factory.openWriterElseOpenReader(it) as N5GoogleCloudStorageWriter - } - - override fun getTargetClass() = N5GoogleCloudStorageWriter::class.java -} - -@Plugin(type = StatefulSerializer.SerializerAndDeserializer::class) -class N5AmazonS3ReaderAdapter : - StatefulSerializer.SerializerAndDeserializer, JsonSerializer> { - - override fun createSerializer( - projectDirectory: Supplier, - stateToIndex: ToIntFunction>, - ): JsonSerializer = N5ReaderSerializer(projectDirectory) - - override fun createDeserializer( - arguments: StatefulSerializer.Arguments, - projectDirectory: Supplier, - dependencyFromIndex: IntFunction>?, - ): JsonDeserializer = N5ReaderDeserializer(projectDirectory) { - n5Factory.openReader(it) as N5AmazonS3Reader - } - - override fun getTargetClass() = N5AmazonS3Reader::class.java -} - -@Plugin(type = StatefulSerializer.SerializerAndDeserializer::class) -class N5AmazonS3WriterAdapter : - StatefulSerializer.SerializerAndDeserializer, JsonSerializer> { - - override fun createSerializer( - projectDirectory: Supplier, - stateToIndex: ToIntFunction>, - ): JsonSerializer = N5ReaderSerializer(projectDirectory) - - override fun createDeserializer( - arguments: StatefulSerializer.Arguments, - projectDirectory: Supplier, - dependencyFromIndex: IntFunction>?, - ): JsonDeserializer = N5ReaderDeserializer(projectDirectory) { - n5Factory.openWriterElseOpenReader(it) as N5AmazonS3Writer - } - - override fun getTargetClass() = N5AmazonS3Writer::class.java -} - - -@Plugin(type = StatefulSerializer.SerializerAndDeserializer::class) -class N5ZarrReaderAdapter : StatefulSerializer.SerializerAndDeserializer, JsonSerializer> { - - override fun createSerializer( - projectDirectory: Supplier, - stateToIndex: ToIntFunction>, - ): JsonSerializer = N5ReaderSerializer(projectDirectory) - - override fun createDeserializer( - arguments: StatefulSerializer.Arguments, - projectDirectory: Supplier, - dependencyFromIndex: IntFunction>?, - ): JsonDeserializer = N5ReaderDeserializer(projectDirectory) { - n5Factory.openReader(it) as N5ZarrReader - } - - override fun getTargetClass() = N5ZarrReader::class.java -} - - -@Plugin(type = StatefulSerializer.SerializerAndDeserializer::class) -class N5ZarrWriterAdapter : StatefulSerializer.SerializerAndDeserializer, JsonSerializer> { - - override fun createSerializer( - projectDirectory: Supplier, - stateToIndex: ToIntFunction>, - ): JsonSerializer = N5ReaderSerializer(projectDirectory) - - override fun createDeserializer( - arguments: StatefulSerializer.Arguments, - projectDirectory: Supplier, - dependencyFromIndex: IntFunction>?, - ): JsonDeserializer = N5ReaderDeserializer(projectDirectory) { - n5Factory.openWriterElseOpenReader(it) as N5ZarrWriter - } - - override fun getTargetClass() = N5ZarrWriter::class.java -} - - diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/managed/MeshManagerWithAssignmentForSegments.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/managed/MeshManagerWithAssignmentForSegments.kt index f9d8a0d62..327173247 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/managed/MeshManagerWithAssignmentForSegments.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/meshes/managed/MeshManagerWithAssignmentForSegments.kt @@ -169,7 +169,7 @@ class MeshManagerWithAssignmentForSegments( if (!segmentsToAdd.isEmpty) createMeshes(segmentsToAdd) - if (!(segmentsToAdd.isEmpty && segmentsToAdd.isEmpty)) + if (!segmentsToAdd.isEmpty || !segmentsToRemove.isEmpty) manager.requestCancelAndUpdate() this._meshUpdateObservable.meshUpdateCompleted() diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/RandomAccessibleIntervalBackend.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/RandomAccessibleIntervalBackend.kt index 18ac0be66..bb51a36f1 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/RandomAccessibleIntervalBackend.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/RandomAccessibleIntervalBackend.kt @@ -14,6 +14,7 @@ import javafx.scene.layout.ColumnConstraints import javafx.scene.layout.GridPane import javafx.scene.layout.Priority import javafx.scene.layout.Region +import net.imglib2.Interval import net.imglib2.RandomAccessibleInterval import net.imglib2.Volatile import net.imglib2.cache.Invalidate @@ -33,6 +34,7 @@ import org.janelia.saalfeldlab.fx.ui.SpatialField import org.janelia.saalfeldlab.paintera.data.DataSource import org.janelia.saalfeldlab.paintera.data.RandomAccessibleIntervalDataSource import org.janelia.saalfeldlab.paintera.state.metadata.MetadataUtils +import org.janelia.saalfeldlab.util.convertRAI import java.util.function.Predicate private val NO_OP_INVALIDATE: Invalidate = object : Invalidate { @@ -54,6 +56,8 @@ abstract class RandomAccessibleIntervalBackend( override val translation: DoubleArray get() = translations[0] + override var virtualCrop: Interval? = null + constructor( name: String, source: RandomAccessibleInterval, @@ -108,13 +112,7 @@ abstract class RandomAccessibleIntervalBackend( val volatileType = VolatileTypeMatcher.getVolatileTypeForType(Util.getTypeFromInterval(source)).createVariable() as T volatileType.isValid = true - val volatileSource = Converters.convert( - zeroMinSource, - { s, t -> - (t.get() as NativeType).set(s) - }, - volatileType - ) + val volatileSource = zeroMinSource.convertRAI(volatileType) { s, t -> (t.get() as NativeType).set(s) } dataSources += zeroMinSource volatileSources += volatileSource diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/SourceStateBackend.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/SourceStateBackend.kt index 22bbd76cd..07b52ca9a 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/SourceStateBackend.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/SourceStateBackend.kt @@ -2,6 +2,7 @@ package org.janelia.saalfeldlab.paintera.state import bdv.cache.SharedQueue import javafx.scene.Node +import net.imglib2.Interval import net.imglib2.realtransform.AffineTransform3D import org.janelia.saalfeldlab.paintera.data.DataSource import org.janelia.saalfeldlab.paintera.state.metadata.MetadataUtils @@ -26,6 +27,8 @@ interface SourceStateBackend { val translation: DoubleArray + var virtualCrop: Interval? + fun updateTransform(resolution: DoubleArray, translation: DoubleArray) { val newTransform = MetadataUtils.transformFromResolutionOffset(resolution, translation) updateTransform(newTransform) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/SourceStateBackendN5.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/SourceStateBackendN5.kt index e25281cb9..1d6e96690 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/SourceStateBackendN5.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/SourceStateBackendN5.kt @@ -1,24 +1,29 @@ package org.janelia.saalfeldlab.paintera.state -import bdv.util.Affine3DHelpers +import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon +import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView import javafx.beans.property.DoubleProperty -import javafx.geometry.HPos -import javafx.geometry.Orientation -import javafx.geometry.Pos -import javafx.geometry.VPos +import javafx.geometry.* import javafx.scene.Node +import javafx.scene.control.Button import javafx.scene.control.Label import javafx.scene.control.Separator import javafx.scene.control.TextField import javafx.scene.layout.* +import net.imglib2.Interval import net.imglib2.realtransform.AffineTransform3D +import net.imglib2.util.Intervals import org.janelia.saalfeldlab.fx.Labels import org.janelia.saalfeldlab.fx.TitledPanes +import org.janelia.saalfeldlab.fx.ui.GlyphScaleView +import org.janelia.saalfeldlab.fx.ui.NumberField import org.janelia.saalfeldlab.fx.ui.ObjectField.SubmitOn import org.janelia.saalfeldlab.fx.ui.SpatialField import org.janelia.saalfeldlab.n5.N5Reader +import org.janelia.saalfeldlab.paintera.paintera import org.janelia.saalfeldlab.paintera.state.metadata.MetadataState import org.janelia.saalfeldlab.paintera.state.metadata.MultiScaleMetadataState +import org.janelia.saalfeldlab.paintera.state.metadata.N5ContainerState import org.janelia.saalfeldlab.paintera.state.metadata.SingleScaleMetadataState interface SourceStateBackendN5 : SourceStateBackend { @@ -35,6 +40,12 @@ interface SourceStateBackendN5 : SourceStateBackend { override val translation: DoubleArray get() = getMetadataState().translation + override var virtualCrop: Interval? + get() = getMetadataState().virtualCrop + set(value) { + getMetadataState().virtualCrop = value + } + override fun updateTransform(resolution: DoubleArray, translation: DoubleArray) = getMetadataState().updateTransform(resolution, translation) override fun updateTransform(transform: AffineTransform3D) = getMetadataState().updateTransform(transform) @@ -45,41 +56,156 @@ interface SourceStateBackendN5 : SourceStateBackend { return (metadataState as? MultiScaleMetadataState)?.let { multiScaleMetadataNode(it) } ?: singleScaleMetadataNode(metadataState) } - override fun shutdown() { - container.close() - } - - fun multiScaleMetadataNode(metadataState: MultiScaleMetadataState): VBox { - - return VBox().apply { + override fun shutdown() { + container.close() + } - val n5ContainerState = metadataState.n5ContainerState - val containerLabel = Labels.withTooltip("Container", "N5 container of source dataset `$dataset'") - val datasetLabel = Labels.withTooltip("Dataset", "Dataset path inside container `${n5ContainerState.uri}'") + /** + * Determines whether virtual cropping can be applied to the given metadata state. + * Currently virtual cropping is supported as long as the data is RAW or read-only. + * + * i.e. painting/modifying labels is not allowed on virtual crops + * + * @param metadataState The metadata state to be evaluated. + * @return True if virtual cropping can be applied, false otherwise. + */ + private fun canCropVirtually(metadataState: MetadataState) : Boolean { + return if (metadataState.isLabel && metadataState.n5ContainerState.writer != null) + false + else true - val container = TextField(n5ContainerState.uri.toString()).apply { isEditable = false } - val dataset = TextField(metadataState.dataset).apply { isEditable = false } + } - children += HBox(containerLabel, container) - HBox.setHgrow(containerLabel, Priority.NEVER) - HBox.setHgrow(container, Priority.ALWAYS) + fun multiScaleMetadataNode(metadataState: MultiScaleMetadataState): Node { - children += HBox(datasetLabel, dataset) - HBox.setHgrow(datasetLabel, Priority.NEVER) - HBox.setHgrow(dataset, Priority.ALWAYS) + return VBox().apply { + spacing = 10.0 + val n5ContainerState = metadataState.n5ContainerState + addContainerAndDatasetChildren(n5ContainerState, metadataState) + children += Separator(Orientation.HORIZONTAL) + if (canCropVirtually(metadataState)) + children += newVirtualCropInputGrid(metadataState) metadataState.metadata.childrenMetadata.zip(metadataState.scaleTransforms).forEachIndexed { idx, (scale, transform) -> val title = "Scale $idx: ${scale.name}" val scaleMetadataGrid = singleScaleMetadataNode(SingleScaleMetadataState(n5ContainerState, scale), true, transform) children += TitledPanes.createCollapsed(title, scaleMetadataGrid) } + } + } + + private fun VBox.addContainerAndDatasetChildren(n5ContainerState: N5ContainerState, metadataState: MetadataState) { + val containerLabel = Labels.withTooltip("Container", "N5 container of source dataset `$dataset'") + val datasetLabel = Labels.withTooltip("Dataset", "Dataset path inside container `${n5ContainerState.uri}'") + + + val container = TextField(n5ContainerState.uri.toString()).apply { isEditable = false } + val dataset = TextField(metadataState.dataset).apply { isEditable = false } + + children += HBox(containerLabel, container).apply { spacing = 10.0 } + HBox.setHgrow(containerLabel, Priority.NEVER) + HBox.setHgrow(container, Priority.ALWAYS) + + children += HBox(datasetLabel, dataset).apply { spacing = 10.0 } + HBox.setHgrow(datasetLabel, Priority.NEVER) + HBox.setHgrow(dataset, Priority.ALWAYS) + } + + private fun newVirtualCropInputGrid(metadataState: MetadataState): GridPane { + val virtualCropGrid = GridPane().also { grid -> + grid.columnConstraints.addAll( + ColumnConstraints(), + *Array(3) { + ColumnConstraints().apply { + hgrow = Priority.ALWAYS + halignment = HPos.CENTER + isFillWidth = true + } + }, + ) + grid.maxWidth = Double.MAX_VALUE + grid.maxHeight = Region.USE_COMPUTED_SIZE + + VBox.setVgrow(grid, Priority.ALWAYS) + } + + val virtualCropLabel = Labels.withTooltip("Virtual Crop\n(pixels)", "Virtual crop extents of the source in pixel space. May be modified.") + virtualCropLabel.minWidth = Region.USE_PREF_SIZE + GridPane.setHgrow(virtualCropLabel, Priority.ALWAYS) + virtualCropGrid.add(virtualCropLabel, 0, 0) + virtualCropGrid.add(Label("\tX"), 1, 0) + virtualCropGrid.add(Label("\tY"), 2, 0) + virtualCropGrid.add(Label("\tZ"), 3, 0) + + virtualCropGrid.add(Label("\tMin"), 0, 1) + virtualCropGrid.add(Label("\tMax"), 0, 2) + + val cropMins = LongArray(3) { + metadataState.virtualCrop?.min(it) ?: 0L + } + val imgDimensions = metadataState.datasetAttributes.dimensions + val cropMaxes = LongArray(3) { + metadataState.virtualCrop?.max(it)?.plus(1) ?: (imgDimensions[it]) + } + + val cropExtents = arrayOf(cropMins, cropMaxes) + + lateinit var intervalFromProperties: () -> Interval? + + val valueProps = Array(2) { rowIdx -> + Array(3) { colIdx -> + val initialValue = cropExtents[rowIdx][colIdx] + val boundsCheck: (value: Long) -> Boolean = { it in 0..imgDimensions[colIdx] } + NumberField.longField(initialValue, boundsCheck, SubmitOn.ENTER_PRESSED, SubmitOn.FOCUS_LOST).also { + virtualCropGrid.add(it.textField, colIdx + 1, rowIdx + 1) + it.valueProperty().subscribe { _, new -> + metadataState.virtualCrop = intervalFromProperties() + paintera.baseView.orthogonalViews().requestRepaint() + paintera.baseView.orthogonalViews().drawOverlays() + } + } + } + } + intervalFromProperties = { + val maxExclusiveCrop = Intervals.createMinMax( + *valueProps[0].map { it.value.toLong() }.toLongArray(), + *valueProps[1].map { it.value.toLong() - 1 }.toLongArray() + ) + val cropEqualsFullImage = { + valueProps.zip(arrayOf(longArrayOf(0, 0, 0), imgDimensions)) + .flatMap { (props, extents) -> props.zip(extents.asIterable()) } + .asSequence() + .map { (prop, extent) -> prop.value.toLong() == extent } + .reduce(Boolean::and) + } + if (Intervals.isEmpty(maxExclusiveCrop) || cropEqualsFullImage()) + null + else + maxExclusiveCrop } + + val resetMin = Button(" ", GlyphScaleView(FontAwesomeIconView(FontAwesomeIcon.REFRESH).apply { styleClass += "reset" })) + resetMin.setOnAction { + valueProps[0].forEachIndexed { idx, prop -> + prop.valueProperty().value = 0L + } + } + val resetMax = Button(" ", GlyphScaleView(FontAwesomeIconView(FontAwesomeIcon.REFRESH).apply { styleClass += "reset" })) + imgDimensions + resetMax.setOnAction { + valueProps[1].forEachIndexed { idx, prop -> + prop.valueProperty().value = imgDimensions[idx] + } + } + virtualCropGrid.add(resetMin, 4, 1) + virtualCropGrid.add(resetMax, 4, 2) + return virtualCropGrid } - fun singleScaleMetadataNode(metadataState: MetadataState, asScaleLevel: Boolean = false, transformOverride: AffineTransform3D? = null): GridPane { + fun singleScaleMetadataNode(metadataState: MetadataState, asScaleLevel: Boolean = false, transformOverride: AffineTransform3D? = null): Node { val n5ContainerState = metadataState.n5ContainerState val resolutionLabel = Labels.withTooltip("Resolution", "Resolution of the source dataset") @@ -127,59 +253,58 @@ interface SourceStateBackendN5 : SourceStateBackend { editable = false } - return GridPane().apply { - columnConstraints.add( - 0, ColumnConstraints( - Label.USE_COMPUTED_SIZE, Label.USE_COMPUTED_SIZE, Label.USE_COMPUTED_SIZE, - Priority.ALWAYS, - HPos.LEFT, - true - ) - ) - - hgap = 10.0 - var row = 0 - + return VBox().apply { if (!asScaleLevel) { - val containerLabel = Labels.withTooltip("Container", "N5 container of source dataset `$dataset'") - val container = TextField(n5ContainerState.uri.toString()).apply { isEditable = false } - - val datasetLabel = Labels.withTooltip("Dataset", "Dataset path inside container `${n5ContainerState.uri}'") - val dataset = TextField(metadataState.dataset).apply { isEditable = false } - - add(containerLabel, 0, row) - add(container, 2, row++, 3, 1) - - add(datasetLabel, 0, row) - add(dataset, 2, row++, 3, 1) + spacing = 10.0 + addContainerAndDatasetChildren(n5ContainerState, metadataState) + children += Separator(Orientation.HORIZONTAL) + + if (canCropVirtually(metadataState)) { + children += newVirtualCropInputGrid(metadataState) + children += Separator(Orientation.HORIZONTAL) + } } - - add(Separator(Orientation.VERTICAL), 1, 0, 1, 20) - - add(labelMultisetLabel, 0, row) - add(Label("${metadataState.isLabelMultiset}").also { it.alignment = Pos.CENTER_RIGHT }, 2, row++) - - add(unitLabel, 0, row) - add(Label(metadataState.unit).also { GridPane.setHalignment(it, HPos.RIGHT) }, 2, row++) - - mapOf( - dimensionsLabel to dimensionsField, - resolutionLabel to resolutionField, - offsetLabel to offsetField, - blockSizeLabel to blockSizeField, - ).forEach { (label, field) -> - add(label, 0, row, 1, 2) - GridPane.setValignment(label, VPos.BOTTOM) - add(field.node, 2, row++, 3, 2) - row++ - } - - add(dataTypeLabel, 0, row) - add(Label("${metadataState.datasetAttributes.dataType}").also { GridPane.setHalignment(it, HPos.RIGHT) }, 2, row++) - - add(compressionLabel, 0, row) - add(Label(metadataState.datasetAttributes.compression.type).also { GridPane.setHalignment(it, HPos.RIGHT) }, 2, row++) + children += GridPane().apply { + columnConstraints.add( + 0, ColumnConstraints( + Label.USE_COMPUTED_SIZE, Label.USE_COMPUTED_SIZE, Label.USE_COMPUTED_SIZE, + Priority.ALWAYS, + HPos.LEFT, + true + ) + ) + padding = Insets(10.0, 0.0, 0.0, 0.0) + hgap = 10.0 + var row = 0 + + add(Separator(Orientation.VERTICAL), 1, 0, 1, GridPane.REMAINING) + + add(labelMultisetLabel, 0, row) + add(Label("${metadataState.isLabelMultiset}").also { it.alignment = Pos.CENTER_RIGHT }, 2, row++) + + add(unitLabel, 0, row) + add(Label(metadataState.unit).also { GridPane.setHalignment(it, HPos.RIGHT) }, 2, row++) + + mapOf( + dimensionsLabel to dimensionsField, + resolutionLabel to resolutionField, + offsetLabel to offsetField, + blockSizeLabel to blockSizeField, + ).forEach { (label, field) -> + add(label, 0, row, 1, 2) + GridPane.setValignment(label, VPos.BOTTOM) + add(field.node, 2, row++, 3, 2) + row++ + } + + add(dataTypeLabel, 0, row) + add(Label("${metadataState.datasetAttributes.dataType}").also { GridPane.setHalignment(it, HPos.RIGHT) }, 2, row++) + + add(compressionLabel, 0, row) + add(Label(metadataState.datasetAttributes.compression.type).also { GridPane.setHalignment(it, HPos.RIGHT) }, 2, row++) + } } + } } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/SourceStateWithBackend.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/SourceStateWithBackend.kt index 1e5c7c0e5..2a20d0268 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/SourceStateWithBackend.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/SourceStateWithBackend.kt @@ -1,11 +1,13 @@ package org.janelia.saalfeldlab.paintera.state +import net.imglib2.Interval import org.janelia.saalfeldlab.paintera.PainteraBaseView interface SourceStateWithBackend : SourceState { val backend: SourceStateBackend val resolution: DoubleArray get() = backend.resolution val offset: DoubleArray get() = backend.translation + val virtualCrop: Interval? get() = backend.virtualCrop override fun onShutdown(paintera: PainteraBaseView) { backend.shutdown() diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/channel/ConnectomicsChannelState.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/channel/ConnectomicsChannelState.kt index eb96135e6..974a50a25 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/channel/ConnectomicsChannelState.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/channel/ConnectomicsChannelState.kt @@ -12,6 +12,7 @@ import javafx.beans.property.SimpleObjectProperty import javafx.beans.property.SimpleStringProperty import javafx.scene.Node import javafx.scene.layout.VBox +import net.imglib2.Interval import net.imglib2.Volatile import org.janelia.saalfeldlab.net.imglib2.converter.ARGBCompositeColorConverter import net.imglib2.type.numeric.ARGBType @@ -115,6 +116,7 @@ class ConnectomicsChannelState const val NAME = "name" const val COMPOSITE = "composite" const val CONVERTER = "converter" + const val VIRTUAL_CROP = "virtualCrop" const val INTERPOLATION = "interpolation" const val IS_VISIBLE = "isVisible" const val RESOLUTION = "resolution" @@ -135,6 +137,8 @@ class ConnectomicsChannelState map.addProperty(IS_VISIBLE, state.isVisible) state.resolution.let { map.add(RESOLUTION, context[it]) } state.offset.let { map.add(OFFSET, context[it]) } + state.virtualCrop?.let { map.add(VIRTUAL_CROP, context[it]) } + } return map } @@ -168,7 +172,9 @@ class ConnectomicsChannelState return with(SerializationKeys) { with(GsonExtensions) { val backend = context.fromClassInfo>(json, BACKEND)!! - ConnectomicsChannelState( + backend.virtualCrop = context.get(json, VIRTUAL_CROP) + + ConnectomicsChannelState( backend, queue, priority, diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/ConnectomicsLabelState.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/ConnectomicsLabelState.kt index 4c08f34d7..294be54c7 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/ConnectomicsLabelState.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/ConnectomicsLabelState.kt @@ -565,6 +565,7 @@ class ConnectomicsLabelState, T>( const val INTERPOLATION = "interpolation" const val IS_VISIBLE = "isVisible" const val RESOLUTION = "resolution" + const val VIRTUAL_CROP = "virtualCrop" const val OFFSET = "offset" const val LABEL_BLOCK_LOOKUP = "labelBlockLookup" const val LOCKED_SEGMENTS = "lockedSegments" @@ -597,6 +598,7 @@ class ConnectomicsLabelState, T>( map.addProperty(IS_VISIBLE, state.isVisible) map.add(RESOLUTION, context[state.resolution]) map.add(OFFSET, context[state.offset]) + state.virtualCrop?.let { map.add(VIRTUAL_CROP, context[it]) } state.labelBlockLookup.takeUnless { state.backend.providesLookup }?.let { map.add(LABEL_BLOCK_LOOKUP, context[it]) } state.lockedSegments.lockedSegmentsCopy().takeIf { it.isNotEmpty() }?.let { map.add(LOCKED_SEGMENTS, context[it]) } } @@ -642,7 +644,9 @@ class ConnectomicsLabelState, T>( val name = json[NAME] ?: backend.name val resolution = context[json, RESOLUTION] ?: backend.resolution val offset = context[json, OFFSET] ?: backend.translation + val virtualCrop = context.get(json, VIRTUAL_CROP) backend.updateTransform(resolution, offset) + backend.virtualCrop = virtualCrop val labelBlockLookup: LabelBlockLookup? = if (backend.providesLookup) null else context[json, LABEL_BLOCK_LOOKUP] val state = ConnectomicsLabelState( diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/ReadOnlyConnectomicsLabelBackend.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/ReadOnlyConnectomicsLabelBackend.kt deleted file mode 100644 index 2c0e9b406..000000000 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/ReadOnlyConnectomicsLabelBackend.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.janelia.saalfeldlab.paintera.state.label - -import org.janelia.saalfeldlab.paintera.data.DataSource -import org.janelia.saalfeldlab.paintera.id.IdService -import org.janelia.saalfeldlab.util.grids.LabelBlockLookupNoBlocks - -interface ReadOnlyConnectomicsLabelBackend : ConnectomicsLabelBackend { - - override fun createIdService(source: DataSource) = IdService.IdServiceNotProvided() - - override fun createLabelBlockLookup(source: DataSource) = LabelBlockLookupNoBlocks(); -} 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 5554f79f1..7d9efb114 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 @@ -91,7 +91,7 @@ class N5BackendMultiScaleGroup constructor( it, dataset, Supplier { PainteraAlerts.getN5IdServiceFromData(it, dataset, source) }) - } ?: LocalIdService(metadataState.reader.getAttribute(dataset, "maxId", Long::class.java) ?: 0L) + } ?: LocalIdService(metadataState.reader.getAttribute(dataset, "maxId", Long::class.java) ?: 1L) } override fun createLabelBlockLookup(source: DataSource) = PainteraAlerts.getLabelBlockLookupFromN5DataSource(container, dataset, source)!! diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendPainteraDataset.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendPainteraDataset.kt index 69d06f4b9..2180ce162 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendPainteraDataset.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/N5BackendPainteraDataset.kt @@ -81,9 +81,9 @@ class N5BackendPainteraDataset( //FIXME Caleb: same as above with idService override fun createIdService(source: DataSource): IdService { - return (metadataState.writer ?: metadataState.reader)?.let { + return (metadataState.writer ?: metadataState.reader).let { N5Helpers.idService(it, dataset) - } ?: IdService.IdServiceNotProvided() + } } companion object { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/ReadOnlyN5Backend.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/ReadOnlyN5Backend.kt deleted file mode 100644 index 62840459a..000000000 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/label/n5/ReadOnlyN5Backend.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.janelia.saalfeldlab.paintera.state.label.n5 - -import org.janelia.saalfeldlab.paintera.state.label.ReadOnlyConnectomicsLabelBackend - -interface ReadOnlyN5Backend : N5Backend, ReadOnlyConnectomicsLabelBackend diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/metadata/MetadataState.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/metadata/MetadataState.kt index b89584dd7..2b2899d77 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/metadata/MetadataState.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/metadata/MetadataState.kt @@ -1,6 +1,8 @@ package org.janelia.saalfeldlab.paintera.state.metadata import bdv.cache.SharedQueue +import net.imglib2.FinalInterval +import net.imglib2.Interval import net.imglib2.Volatile import net.imglib2.realtransform.AffineTransform3D import net.imglib2.type.NativeType @@ -40,6 +42,7 @@ interface MetadataState { var maxIntensity: Double var resolution: DoubleArray var translation: DoubleArray + var virtualCrop : Interval? var unit: String var reader: N5Reader @@ -75,6 +78,7 @@ interface MetadataState { target.maxIntensity = source.maxIntensity target.resolution = source.resolution.copyOf() target.translation = source.translation.copyOf() + target.virtualCrop = source.virtualCrop?.let { FinalInterval(it.minAsLongArray(), it.maxAsLongArray()) } target.unit = source.unit target.group = source.group } @@ -94,6 +98,7 @@ open class SingleScaleMetadataState( override var maxIntensity = metadata.maxIntensity() override var resolution = metadata.resolution override var translation = metadata.offset + override var virtualCrop: Interval? = null override var unit: String = metadata.unit() override var reader = n5ContainerState.reader override val writer: N5Writer? @@ -194,6 +199,13 @@ class PainteraDataMultiscaleMetadataState ( override var maxIntensity: Double = (painteraDataMultiscaleMetadata as? N5PainteraLabelMultiScaleGroup)?.maxId?.toDouble() ?: super.maxIntensity val dataMetadataState = MultiScaleMetadataState(n5ContainerState, painteraDataMultiscaleMetadata.dataGroupMetadata) + override var virtualCrop: Interval? = null + get() = field + set(value) { + dataMetadataState.virtualCrop = value + field = value + } + override fun , T : Volatile> getData(queue: SharedQueue, priority: Int): Array> { return if (isLabelMultiset) { N5Data.openLabelMultisetMultiscale(dataMetadataState, queue, priority) @@ -275,9 +287,7 @@ class MetadataUtils { @JvmStatic fun createMetadataState(n5container: String, dataset: String?): Optional { - val reader = with(Paintera.n5Factory) { - openWriterOrNull(n5container) ?: openReaderOrNull(n5container) ?: return Optional.empty() - } + val reader = Paintera.n5Factory.openReader(n5container) ?: return Optional.empty() val n5ContainerState = N5ContainerState(reader) val metadataRoot = N5Helpers.parseMetadata(reader) diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/raw/ConnectomicsRawState.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/raw/ConnectomicsRawState.kt index 72b9f231f..5834aa175 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/raw/ConnectomicsRawState.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/state/raw/ConnectomicsRawState.kt @@ -12,7 +12,8 @@ import javafx.scene.Node import javafx.scene.control.Label import javafx.scene.layout.HBox import javafx.scene.layout.VBox -import org.janelia.saalfeldlab.net.imglib2.converter.ARGBColorConverter +import net.imglib2.Interval +import net.imglib2.RealInterval import net.imglib2.realtransform.AffineTransform3D import net.imglib2.type.NativeType import net.imglib2.type.numeric.ARGBType @@ -21,6 +22,7 @@ import net.imglib2.type.volatiles.AbstractVolatileRealType import org.janelia.saalfeldlab.fx.TitledPanes import org.janelia.saalfeldlab.fx.extensions.TitledPaneExtensions.Companion.graphicsOnly import org.janelia.saalfeldlab.fx.ui.NamedNode +import org.janelia.saalfeldlab.net.imglib2.converter.ARGBColorConverter import org.janelia.saalfeldlab.paintera.PainteraBaseView import org.janelia.saalfeldlab.paintera.RawSourceStateKeys import org.janelia.saalfeldlab.paintera.composition.Composite @@ -49,6 +51,7 @@ import org.janelia.saalfeldlab.paintera.state.raw.ConnectomicsRawState.Serializa import org.janelia.saalfeldlab.paintera.state.raw.ConnectomicsRawState.SerializationKeys.NAME import org.janelia.saalfeldlab.paintera.state.raw.ConnectomicsRawState.SerializationKeys.OFFSET import org.janelia.saalfeldlab.paintera.state.raw.ConnectomicsRawState.SerializationKeys.RESOLUTION +import org.janelia.saalfeldlab.paintera.state.raw.ConnectomicsRawState.SerializationKeys.VIRTUAL_CROP import org.janelia.saalfeldlab.paintera.state.raw.n5.N5BackendRaw import org.janelia.saalfeldlab.util.Colors import org.janelia.saalfeldlab.util.n5.N5Helpers.serializeTo @@ -172,6 +175,7 @@ open class ConnectomicsRawState( const val INTERPOLATION = "interpolation" const val IS_VISIBLE = "isVisible" const val RESOLUTION = "resolution" + const val VIRTUAL_CROP = "virtualCrop" const val OFFSET = "offset" } @@ -194,6 +198,7 @@ open class ConnectomicsRawState( map.addProperty(IS_VISIBLE, state.isVisible) map.add(RESOLUTION, context[state.resolution]) map.add(OFFSET, context[state.offset]) + state.virtualCrop?.let { map.add(VIRTUAL_CROP, context[it]) } } return map } @@ -230,7 +235,10 @@ open class ConnectomicsRawState( val backend: ConnectomicsRawBackend = context.fromClassInfo>(json, BACKEND)!! val resolution = context[json, RESOLUTION] ?: backend.resolution val offset = context[json, OFFSET] ?: backend.translation + val virtualCrop = context.get(json, VIRTUAL_CROP) as? Interval backend.updateTransform(resolution, offset) + backend.virtualCrop = virtualCrop + return ConnectomicsRawState( backend, queue, diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/dialogs/AnimatedProgressBarAlert.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/dialogs/AnimatedProgressBarAlert.kt new file mode 100644 index 000000000..b37750131 --- /dev/null +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/dialogs/AnimatedProgressBarAlert.kt @@ -0,0 +1,98 @@ +package org.janelia.saalfeldlab.paintera.ui.dialogs + +import javafx.animation.AnimationTimer +import javafx.animation.KeyFrame +import javafx.animation.KeyValue +import javafx.animation.Timeline +import javafx.beans.property.SimpleDoubleProperty +import javafx.beans.property.SimpleIntegerProperty +import javafx.scene.control.Label +import javafx.scene.control.ProgressBar +import javafx.scene.layout.HBox +import javafx.scene.layout.VBox +import javafx.util.Duration +import org.janelia.saalfeldlab.fx.extensions.createNonNullValueBinding +import org.janelia.saalfeldlab.fx.extensions.nonnull +import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread +import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts + +class AnimatedProgressBarAlert( + title: String, + header: String, + private var progressLabelPrefix: String, + private val currentCountSupplier: () -> Int, + initialMax: Int +) { + + private val progressProperty = SimpleDoubleProperty(0.0) + private var progress by progressProperty.nonnull() + + val totalCountProperty = SimpleIntegerProperty(initialMax) + private var totalCount by totalCountProperty.nonnull() + + private val currentCountProperty = SimpleIntegerProperty(0) + private var currentCount by currentCountProperty.nonnull() + + private val progressAlert = PainteraAlerts.information("Ok", false).apply { + this.title = title + this.headerText = header + val progressBar = ProgressBar(0.0).apply { + progressProperty().bind(progressProperty) + prefWidth = 300.0 + } + + val doneLabel = Label("Done!") + doneLabel.visibleProperty().bind(progressProperty.isEqualTo(1.0, 0.0001)) + dialogPane.content = VBox(10.0, createProgressLabel(), progressBar, HBox(doneLabel)) + isResizable = false + } + + private fun createProgressLabel() = Label().apply { + val textBinding = currentCountProperty.createNonNullValueBinding(totalCountProperty) { + "$progressLabelPrefix : $currentCount / $totalCount" + } + textProperty().bind(textBinding) + } + + private val updater = object : AnimationTimer() { + + val timeline = Timeline().apply { + cycleCount = Timeline.INDEFINITE + } + + override fun handle(now: Long) { + currentCount = currentCountSupplier() + + val newProgress = currentCount.toDouble() / totalCount + val delta = (newProgress - progress).coerceIn(0.0, 1.0) + if (delta <= 0) + return + + val duration = Duration.millis((200.0 / delta).coerceIn(50.0, 1000.0)) + + timeline.keyFrames.setAll(KeyFrame(duration, KeyValue(progressProperty, newProgress))) + timeline.playFromStart() + } + + override fun stop() { + timeline.stop() + progress = 1.0 + } + + } + + fun showAndStart() = InvokeOnJavaFXApplicationThread { + updater.start() + progressAlert.showAndWait() + } + + fun finish() = InvokeOnJavaFXApplicationThread { + updater.stop() + } + + fun stopAndClose() = InvokeOnJavaFXApplicationThread { + updater.stop() + progressAlert.close() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/dialogs/ExportSourceDialog.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/dialogs/ExportSourceDialog.kt new file mode 100644 index 000000000..1df1402a1 --- /dev/null +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/dialogs/ExportSourceDialog.kt @@ -0,0 +1,254 @@ +package org.janelia.saalfeldlab.paintera.ui.dialogs + +import javafx.collections.FXCollections +import javafx.collections.ObservableList +import javafx.geometry.HPos +import javafx.geometry.Pos +import javafx.scene.control.* +import javafx.scene.input.KeyEvent +import javafx.scene.layout.ColumnConstraints +import javafx.scene.layout.GridPane +import javafx.scene.layout.Priority +import javafx.stage.DirectoryChooser +import javafx.util.StringConverter +import net.imglib2.type.numeric.RealType +import org.janelia.saalfeldlab.fx.actions.painteraActionSet +import org.janelia.saalfeldlab.fx.extensions.createNonNullValueBinding +import org.janelia.saalfeldlab.fx.extensions.nullable +import org.janelia.saalfeldlab.fx.ui.Exceptions.Companion.exceptionAlert +import org.janelia.saalfeldlab.n5.DataType +import org.janelia.saalfeldlab.n5.imglib2.N5Utils +import org.janelia.saalfeldlab.paintera.Constants +import org.janelia.saalfeldlab.paintera.PainteraBaseKeys +import org.janelia.saalfeldlab.paintera.PainteraBaseKeys.namedCombinationsCopy +import org.janelia.saalfeldlab.paintera.PainteraBaseView +import org.janelia.saalfeldlab.paintera.control.actions.ExportSourceState +import org.janelia.saalfeldlab.paintera.control.actions.MenuActionType +import org.janelia.saalfeldlab.paintera.data.mask.MaskedSource +import org.janelia.saalfeldlab.paintera.paintera +import org.janelia.saalfeldlab.paintera.serialization.GsonExtensions.get +import org.janelia.saalfeldlab.paintera.state.label.ConnectomicsLabelState +import org.janelia.saalfeldlab.paintera.state.label.n5.N5Backend +import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts +import org.janelia.saalfeldlab.paintera.ui.menus.PainteraMenuItems +import org.janelia.saalfeldlab.util.n5.N5Helpers.MAX_ID_KEY + +object ExportSourceDialog { + + private const val DIALOG_HEADER = "Export Source" + private const val CANCEL_LABEL = "_Cancel" + private const val EXPORT = "Export" + private val acceptableDataTypes = DataType.entries.filter { it < DataType.FLOAT32 }.toTypedArray() + + + fun askAndExport() { + if (getValidExportSources().isEmpty()) + return + + val state = ExportSourceState() + if (newDialog(state).showAndWait().nullable == ButtonType.OK) { + state.exportSource(true) + } + } + + /** + * Picks the smallest data type that can hold all values less than or equal to maxId value. + * + * @param maxId the maximum ID value + * @return the smallest data type that can contain up to maxId + */ + private fun pickSmallestDataType(maxId: Long): DataType { + + if (maxId < 0) return DataType.UINT64 + + val smallestType = acceptableDataTypes + .asSequence() + .map { it to N5Utils.type(it) } + .filterIsInstance>>() + .filter { maxId < it.second.maxValue } + .map { it.first } + .firstOrNull() ?: DataType.UINT64 + + return smallestType + } + + private fun newDialog(state: ExportSourceState): Alert = PainteraAlerts.confirmation(EXPORT, CANCEL_LABEL, true, paintera.pane.scene?.window).apply { + val choiceBox = createSourceChoiceBox().apply { + maxWidth = Double.MAX_VALUE + } + + choiceBox.selectionModel.selectedItemProperty().subscribe { (_, _, backend) -> + backend.getMetadataState().apply { + state.maxIdProperty.value = reader[dataset, MAX_ID_KEY] ?: -1 + } + } + + state.sourceProperty.bind(choiceBox.selectionModel.selectedItemProperty().map { it.first }) + state.sourceStateProperty.bind(choiceBox.selectionModel.selectedItemProperty().map { it.second }) + state.backendProperty.bind(choiceBox.selectionModel.selectedItemProperty().map { it.third }) + + headerText = DIALOG_HEADER + dialogPane.content = GridPane().apply { + columnConstraints.addAll( + ColumnConstraints().apply { halignment = HPos.RIGHT }, + ColumnConstraints().apply { + hgrow = Priority.NEVER + minWidth = 10.0 + }, + ColumnConstraints().apply { + halignment = HPos.RIGHT + isFillWidth = true + hgrow = Priority.ALWAYS + }, + ColumnConstraints().apply { hgrow = Priority.NEVER } + ) + + add(Label("Source").apply { alignment = Pos.CENTER_LEFT }, 0, 0) + add(choiceBox, 2, 0, GridPane.REMAINING, 1) + + add(Label("Export Container").apply { alignment = Pos.CENTER_LEFT }, 0, 1) + val containerPathField = TextField().apply { + promptText = "Enter the container path" + maxWidth = Double.MAX_VALUE + state.exportLocationProperty.bind(textProperty()) + } + add(containerPathField, 2, 1, 2, 1) + add(Button("Browse").apply { + setOnAction { + val directoryChooser = DirectoryChooser() + val selectedDirectory = directoryChooser.showDialog(null) + selectedDirectory?.let { + containerPathField.text = it.absolutePath + } + } + }, 3, 1) + + add(Label("Dataset").apply { alignment = Pos.CENTER_LEFT }, 0, 2) + add(TextField().apply { + promptText = "Enter the dataset name" + maxWidth = Double.MAX_VALUE + state.datasetProperty.bind(textProperty()) + }, 2, 2, GridPane.REMAINING, 1) + + + val smallOptions = GridPane().apply { + columnConstraints.addAll( + ColumnConstraints().apply { halignment = HPos.RIGHT }, + ColumnConstraints().apply { + hgrow = Priority.NEVER + minWidth = 10.0 + }, + ColumnConstraints().apply { halignment = HPos.LEFT }, + + ColumnConstraints().apply { + halignment = HPos.CENTER + isFillWidth = true + hgrow = Priority.ALWAYS + }, + + ColumnConstraints().apply { halignment = HPos.RIGHT }, + ColumnConstraints().apply { + hgrow = Priority.NEVER + minWidth = 10.0 + }, + ColumnConstraints().apply { halignment = HPos.LEFT }, + ) + } + + + var prevScaleLevels: ObservableList? = null + val scaleLevelsBinding = choiceBox.selectionModel.selectedItemProperty().createNonNullValueBinding { (source, _, _) -> + if (prevScaleLevels != null && prevScaleLevels!!.size == source.numMipmapLevels) + return@createNonNullValueBinding prevScaleLevels + + FXCollections.observableArrayList().also { + for (i in 0 until source.numMipmapLevels) { + it.add(i) + } + prevScaleLevels = it + } + } + val dataTypeChoices = ChoiceBox(FXCollections.observableArrayList(*acceptableDataTypes)).apply { + converter = object : StringConverter() { + override fun toString(`object`: DataType?): String = `object`?.name?.uppercase() ?: "Select a Data Type..." + override fun fromString(string: String?) = DataType.fromString(string?.lowercase()) + } + } + + state.maxIdProperty.subscribe { maxId -> dataTypeChoices.selectionModel.select(pickSmallestDataType(maxId.toLong())) } + state.dataTypeProperty.bind(dataTypeChoices.selectionModel.selectedItemProperty()) + + smallOptions.add(Label("Map Fragment to Segment ID").apply { alignment = Pos.BOTTOM_RIGHT }, 0, 0) + smallOptions.add(CheckBox().apply { + state.segmentFragmentMappingProperty.bind(selectedProperty()) + selectedProperty().set(true) + alignment = Pos.CENTER_LEFT + }, 2, 0) + smallOptions.add(Label("Scale Level").apply { alignment = Pos.BOTTOM_RIGHT }, 4, 0) + smallOptions.add(ChoiceBox().apply { + alignment = Pos.CENTER_LEFT + itemsProperty().bind(scaleLevelsBinding) + itemsProperty().subscribe { _ -> selectionModel.selectFirst() } + state.scaleLevelProperty.bind(valueProperty()) + }, 6, 0) + smallOptions.add(Label("Data Type").apply { alignment = Pos.BOTTOM_RIGHT }, 4, 1) + smallOptions.add(dataTypeChoices, 6, 1) + + add(smallOptions, 0, 3, GridPane.REMAINING, 1) + } + } + + private fun createSourceChoiceBox(): ChoiceBox, ConnectomicsLabelState<*, *>, N5Backend<*, *>>> { + val choices = getValidExportSources() + + return ChoiceBox(choices).apply { + val curChoiceIdx = choices.indexOfFirst { it.second == paintera.currentSource } + if (curChoiceIdx != -1) + selectionModel.select(curChoiceIdx) + else + selectionModel.selectFirst() + maxWidth = Double.MAX_VALUE + converter = object : StringConverter, ConnectomicsLabelState<*, *>, N5Backend<*, *>>>() { + override fun toString(`object`: Triple, ConnectomicsLabelState<*, *>, N5Backend<*, *>>?): String = `object`?.second?.nameProperty()?.get() ?: "Select a Source..." + override fun fromString(string: String?) = choices.first { it.second.nameProperty().get() == string } + } + } + } + + internal fun getValidExportSources(): ObservableList, ConnectomicsLabelState<*, *>, N5Backend<*, *>>> { + return paintera.baseView.sourceInfo().trackSources() + .asSequence() + .filterIsInstance>() + .mapNotNull { source -> + (paintera.baseView.sourceInfo().getState(source) as? ConnectomicsLabelState<*, *>)?.let { state -> + source to state + } + } + .mapNotNull { (source, state) -> + (state.backend as? N5Backend<*, *>)?.let { backend -> + Triple(source, state, backend) + } + }.toCollection(FXCollections.observableArrayList()) + } + + fun exportSourceDialogAction( + baseView: PainteraBaseView, + ) = painteraActionSet("export-source", MenuActionType.ExportSource) { + KeyEvent.KEY_PRESSED(namedCombinationsCopy(), PainteraBaseKeys.EXPORT_SOURCE) { + verify { getValidExportSources().isNotEmpty() } + onAction { PainteraMenuItems.EXPORT_SOURCE.menu.fire() } + handleException { + val scene = baseView.viewer3D().scene.scene + exceptionAlert( + Constants.NAME, + "Error showing export source menu", + it, + null, + scene?.window + ) + } + } + } + +} + 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 1de3afa03..8514b4ff9 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 @@ -133,6 +133,7 @@ class CreateDataset(private val currentSource: Source<*>?, vararg allSources: So private val blockSize = SpatialField.intField(1, { it > 0 }, FIELD_WIDTH, *SubmitOn.entries.toTypedArray()) private val resolution = SpatialField.doubleField(1.0, { it > 0 }, FIELD_WIDTH, *SubmitOn.entries.toTypedArray()) private val offset = SpatialField.doubleField(0.0, { true }, FIELD_WIDTH, *SubmitOn.entries.toTypedArray()) + private val unitField = TextField("pixel") private val labelMultiset = CheckBox().also { it.isSelected = true } private val scaleLevels = TitledPane("Scale Levels", mipmapLevelsNode) //TODO Caleb: Use a proper grid layout instead of this... @@ -143,7 +144,8 @@ class CreateDataset(private val currentSource: Source<*>?, vararg allSources: So nameIt("Dimensions", NAME_WIDTH, false, bufferNode(), dimensions.node), nameIt("Block Size", NAME_WIDTH, false, bufferNode(), blockSize.node), nameIt("Resolution", NAME_WIDTH, false, bufferNode(), resolution.node), - nameIt("Offset", NAME_WIDTH, false, bufferNode(), offset.node), + nameIt("Offset (physical)", NAME_WIDTH, false, bufferNode(), offset.node), + nameIt("Unit", NAME_WIDTH, false, bufferNode(), unitField), nameIt("", NAME_WIDTH, false, bufferNode(), HBox(Label("Label Multiset Type "), labelMultiset)), setFromCurrentBox, scaleLevels @@ -289,8 +291,10 @@ class CreateDataset(private val currentSource: Source<*>?, vararg allSources: So resolution.asDoubleArray(), offset.asDoubleArray(), scaleLevels.map { it.downsamplingFactors() }.toTypedArray(), + unitField.text, if (labelMultiset.isSelected) scaleLevels.stream().mapToInt { it.maxNumEntries() }.toArray() else null, - labelMultiset.isSelected + labelMultiset.isSelected, + false ) N5Helpers.parseMetadata(writer, true).ifPresent { _ -> @@ -356,10 +360,17 @@ class CreateDataset(private val currentSource: Source<*>?, vararg allSources: So resolution.x.value = transform[0, 0] resolution.y.value = transform[1, 1] resolution.z.value = transform[2, 2] - offset.x.value = transform[0, 3] - offset.y.value = transform[1, 3] - offset.z.value = transform[2, 3] + metadataSource?.metadataState?.virtualCrop?.let { + offset.x.value = it.min(0) * transform[0, 0] + offset.y.value = it.min(1) * transform[1, 1] + offset.z.value = it.min(2) * transform[2, 2] + } ?: let { + offset.x.value = transform[0, 3] + offset.y.value = transform[1, 3] + offset.z.value = transform[2, 3] + } + unitField.text = metadataSource?.metadataState?.unit setMipMapLevels(source) } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenuItems.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenuItems.kt index 24eca9b4a..5f23f26ea 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenuItems.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenuItems.kt @@ -13,6 +13,7 @@ import org.janelia.saalfeldlab.paintera.control.actions.MenuActionType import org.janelia.saalfeldlab.paintera.control.modes.ControlMode import org.janelia.saalfeldlab.paintera.paintera import org.janelia.saalfeldlab.paintera.ui.FontAwesome +import org.janelia.saalfeldlab.paintera.ui.dialogs.ExportSourceDialog import org.janelia.saalfeldlab.paintera.ui.dialogs.KeyBindingsDialog import org.janelia.saalfeldlab.paintera.ui.dialogs.ReadMeDialog import org.janelia.saalfeldlab.paintera.ui.dialogs.ReplDialog @@ -31,6 +32,7 @@ enum class PainteraMenuItems( NEW_PROJECT("_New Project", allowedAction = MenuActionType.OpenProject), OPEN_PROJECT("Open _Project", icon = FontAwesomeIcon.FOLDER_OPEN, allowedAction = MenuActionType.OpenProject), OPEN_SOURCE("_Open Source", PBK.OPEN_SOURCE, FontAwesomeIcon.FOLDER_OPEN, MenuActionType.AddSource), + EXPORT_SOURCE("_Export Source", PBK.EXPORT_SOURCE, FontAwesomeIcon.SAVE, MenuActionType.ExportSource), SAVE("_Save", PBK.SAVE, FontAwesomeIcon.SAVE, MenuActionType.SaveProject), SAVE_AS("Save _As", PBK.SAVE_AS, FontAwesomeIcon.FLOPPY_ALT, MenuActionType.SaveProject), QUIT("_Quit", PBK.QUIT, FontAwesomeIcon.SIGN_OUT), @@ -72,6 +74,7 @@ enum class PainteraMenuItems( } }, OPEN_SOURCE { N5Opener().onAction().accept(baseView, getProjectDirectory) }, + EXPORT_SOURCE { ExportSourceDialog.askAndExport() }, SAVE { saveOrSaveAs() }, SAVE_AS { saveAs() }, TOGGLE_MENU_BAR_VISIBILITY { properties.menuBarConfig.toggleIsVisible() }, diff --git a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenus.kt b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenus.kt index 9fa93f59f..c4b0a83ac 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenus.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/paintera/ui/menus/PainteraMenus.kt @@ -1,6 +1,7 @@ package org.janelia.saalfeldlab.paintera.ui.menus import com.google.common.collect.Lists +import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon import javafx.beans.binding.Bindings import javafx.collections.FXCollections import javafx.collections.ObservableList @@ -15,6 +16,7 @@ import org.janelia.saalfeldlab.fx.ui.MatchSelectionMenu import org.janelia.saalfeldlab.paintera.Paintera import org.janelia.saalfeldlab.paintera.control.actions.paint.SmoothAction import org.janelia.saalfeldlab.paintera.paintera +import org.janelia.saalfeldlab.paintera.ui.FontAwesome import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts import org.janelia.saalfeldlab.paintera.ui.menus.PainteraMenuItems.* import org.janelia.saalfeldlab.util.PainteraCache @@ -57,9 +59,9 @@ private val fileMenu by LazyForeignValue(::paintera) { } } } -private val newSourceMenu by LazyForeignValue(::paintera) { Menu("_New", null, NEW_LABEL_SOURCE.menu) } +private val newSourceMenu by LazyForeignValue(::paintera) { Menu("_New", FontAwesome[FontAwesomeIcon.PLUS, 1.5], NEW_LABEL_SOURCE.menu, newVirtualSourceMenu) } private val newVirtualSourceMenu by LazyForeignValue(::paintera) { Menu("_Virtual", null, NEW_CONNECTED_COMPONENT_SOURCE.menu, NEW_THRESHOLDED_SOURCE.menu) } -private val sourcesMenu by LazyForeignValue(::paintera) { Menu("_Sources", null, currentSourceMenu, OPEN_SOURCE.menu, newSourceMenu, newVirtualSourceMenu) } +private val sourcesMenu by LazyForeignValue(::paintera) { Menu("_Sources", null, currentSourceMenu, OPEN_SOURCE.menu, EXPORT_SOURCE.menu, newSourceMenu) } private val menuBarMenu by LazyForeignValue(::paintera) { Menu("_Menu Bar", null, TOGGLE_MENU_BAR_VISIBILITY.menu, TOGGLE_MENU_BAR_MODE.menu) } private val statusBarMenu by LazyForeignValue(::paintera) { Menu("S_tatus Bar", null, TOGGLE_STATUS_BAR_VISIBILITY.menu, TOGGLE_STATUS_BAR_MODE.menu) } private val sideBarMenu by LazyForeignValue(::paintera) { Menu("_Side Bar", null, TOGGLE_SIDE_BAR_MENU_ITEM.menu) } diff --git a/src/main/kotlin/org/janelia/saalfeldlab/util/Imglib2Extensions.kt b/src/main/kotlin/org/janelia/saalfeldlab/util/Imglib2Extensions.kt index f07c99ff6..ed595ee35 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/util/Imglib2Extensions.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/util/Imglib2Extensions.kt @@ -1,6 +1,7 @@ package org.janelia.saalfeldlab.util import net.imglib2.* +import net.imglib2.converter.Converter import net.imglib2.converter.Converters import net.imglib2.converter.read.ConvertedRealRandomAccessible import net.imglib2.interpolation.InterpolatorFactory @@ -15,8 +16,8 @@ import net.imglib2.util.Intervals import net.imglib2.view.IntervalView import net.imglib2.view.RandomAccessibleOnRealRandomAccessible import net.imglib2.view.Views -import org.janelia.saalfeldlab.paintera.util.IntervalHelpers.Companion.smallestContainingInterval import org.janelia.saalfeldlab.net.imglib2.FinalRealRandomAccessibleRealInterval +import org.janelia.saalfeldlab.paintera.util.IntervalHelpers.Companion.smallestContainingInterval import kotlin.math.floor import kotlin.math.roundToLong @@ -61,12 +62,16 @@ fun RandomAccessibleInterval.translate(vararg translation: Long) = Views. fun RealRandomAccessible.affineReal(affine: AffineGet) = RealViews.affineReal(this, affine)!! fun RealRandomAccessible.affine(affine: AffineGet) = RealViews.affine(this, affine)!! -fun > RandomAccessible.convert(type: R, converter: (T, R) -> Unit): RandomAccessible { - return Converters.convert(this, converter, type) +fun , I : Type> RandomAccessible.convert(type: R, converter: Converter) : RandomAccessible { + type as I + converter as Converter + return Converters.convert(this, converter, type) as RandomAccessible } -fun > RandomAccessibleInterval.convert(type: R, converter: (T, R) -> Unit): RandomAccessibleInterval { - return Converters.convert(this, converter, type) +fun , I : Type> RandomAccessibleInterval.convertRAI(type: R, converter: Converter) : RandomAccessibleInterval { + type as I + converter as Converter + return Converters.convert(this, converter, type) as RandomAccessibleInterval } fun > RandomAccessible.convertWith(other: RandomAccessible, type: C, converter: (A, B, C) -> Unit): RandomAccessible { diff --git a/src/main/kotlin/org/janelia/saalfeldlab/util/n5/N5Helpers.kt b/src/main/kotlin/org/janelia/saalfeldlab/util/n5/N5Helpers.kt index 780f4f66b..b74915435 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/util/n5/N5Helpers.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/util/n5/N5Helpers.kt @@ -10,18 +10,19 @@ import javafx.scene.layout.HBox import javafx.scene.layout.Priority import javafx.scene.layout.VBox import javafx.stage.DirectoryChooser +import kotlinx.coroutines.* +import net.imglib2.Interval import net.imglib2.img.cell.CellGrid +import net.imglib2.iterator.IntervalIterator import net.imglib2.realtransform.AffineTransform3D import net.imglib2.realtransform.ScaleAndTranslation import net.imglib2.realtransform.Translation3D +import net.imglib2.util.Intervals import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread import org.janelia.saalfeldlab.labels.blocks.LabelBlockLookup import org.janelia.saalfeldlab.labels.blocks.n5.IsRelativeToContainer import org.janelia.saalfeldlab.labels.blocks.n5.LabelBlockLookupFromN5Relative -import org.janelia.saalfeldlab.n5.DatasetAttributes -import org.janelia.saalfeldlab.n5.N5Reader -import org.janelia.saalfeldlab.n5.N5URI -import org.janelia.saalfeldlab.n5.N5Writer +import org.janelia.saalfeldlab.n5.* import org.janelia.saalfeldlab.n5.hdf5.N5HDF5Reader import org.janelia.saalfeldlab.n5.universe.N5DatasetDiscoverer import org.janelia.saalfeldlab.n5.universe.N5TreeNode @@ -53,6 +54,7 @@ import java.util.* import java.util.List import java.util.concurrent.ExecutorService import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference import java.util.function.* import kotlin.collections.ArrayList @@ -76,6 +78,7 @@ object N5Helpers { const val MAX_ID_KEY = "maxId" const val RESOLUTION_KEY = "resolution" const val OFFSET_KEY = "offset" + const val UNIT_KEY = "unit" const val DOWNSAMPLING_FACTORS_KEY = "downsamplingFactors" const val LABEL_MULTISETTYPE_KEY = "isLabelMultiset" const val MAX_NUM_ENTRIES_KEY = "maxNumEntries" @@ -872,6 +875,53 @@ object N5Helpers { return n5Container.get() ?: throw exception() } + /** + * Iterates through each block in the specified dataset that exists at the given scale level and performs an action using the provided lambda function. + * `withBlock` is called asynchronously for each block that exists, and the overall call will return only + * when all blocks are processed. + * + * @param source The source from which to generate the grid and intervals corresponding to blocks. + * @param scaleLevel The scale level to consider when generating the grid of blocks. + * @param n5 The N5 reader used to check for the existence of blocks. + * @param dataset The dataset path to check within the N5 container. + * @param processedCount An optional AtomicInteger to track the number of processed blocks. + * @param withBlock A lambda function to execute for each existing block's interval. + */ + suspend fun forEachBlockExists( + n5: GsonKeyValueN5Reader, + dataset: String, + processedCount: AtomicInteger? = null, + withBlock: (Interval) -> Unit + ) { + + val blockGrid = n5.getDatasetAttributes(dataset).run { + CellGrid(dimensions, blockSize) + } + + //TODO Caleb: Use Streams.localizable over `cellIntervals()` after bumping to the newest imglib2 version + val gridIterable = IntervalIterator(blockGrid.gridDimensions) + val curBlock = LongArray(gridIterable.numDimensions()) + val cellMin = LongArray(gridIterable.numDimensions()) + val cellDims = LongArray(gridIterable.numDimensions()) { blockGrid.cellDimensions[it].toLong() } + + coroutineScope { + while (gridIterable.hasNext()) { + gridIterable.fwd() + gridIterable.localize(curBlock) + if (n5.keyValueAccess.exists(n5.absoluteDataBlockPath(dataset, *curBlock))) { + for (i in cellMin.indices) + cellMin[i] = blockGrid.getCellMin(i, curBlock[i]) + val cellInterval = Intervals.createMinSize(*cellMin, *cellDims) + launch { + withBlock(cellInterval) + processedCount?.getAndIncrement() + } + } else + processedCount?.getAndIncrement() + } + } + } + class RemoveSourceException : PainteraException { constructor(location: String) : super("Source expected at:\n$location\nshould be removed") diff --git a/src/main/kotlin/org/janelia/saalfeldlab/util/n5/universe/N5FactoryWithCache.kt b/src/main/kotlin/org/janelia/saalfeldlab/util/n5/universe/N5FactoryWithCache.kt index 586ebba92..e57908529 100644 --- a/src/main/kotlin/org/janelia/saalfeldlab/util/n5/universe/N5FactoryWithCache.kt +++ b/src/main/kotlin/org/janelia/saalfeldlab/util/n5/universe/N5FactoryWithCache.kt @@ -1,5 +1,6 @@ package org.janelia.saalfeldlab.util.n5.universe +import io.github.oshai.kotlinlogging.KotlinLogging import org.janelia.saalfeldlab.n5.N5Exception import org.janelia.saalfeldlab.n5.N5Reader import org.janelia.saalfeldlab.n5.N5URI @@ -10,14 +11,11 @@ import org.slf4j.LoggerFactory import java.io.File import java.lang.invoke.MethodHandles import java.net.URI -import java.nio.file.Path -import java.nio.file.Paths -import kotlin.io.path.toPath class N5FactoryWithCache : N5Factory() { companion object { - private val LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()) + private val LOG = KotlinLogging.logger { } private const val ZGROUP = ".zgroup" private const val ZARRAY = ".zarray" @@ -76,14 +74,21 @@ class N5FactoryWithCache : N5Factory() { fun openWriterOrNull(uri : String) : N5Writer? = try { openWriter(uri) } catch (e : Exception) { - LOG.debug("Unable to open $uri as N5Writer", e) + LOG.debug(e) {"Unable to open $uri as N5Writer"} null } fun openReaderOrNull(uri : String) : N5Reader? = try { openReader(uri) } catch (e : Exception) { - LOG.debug("Unable to open $uri as N5Reader", e) + LOG.debug(e) { "Unable to open $uri as N5Reader"} + null + } + + fun openWriterOrReaderOrNull(uri: String) = try { + openWriterOrNull(uri) ?: openReaderOrNull(uri) + } catch (e : N5Exception) { + LOG.trace(e) {"Cannot get N5Reader at $uri"} null } diff --git a/src/packaging/linux/jpackage.txt b/src/packaging/linux/jpackage.txt index 0d6adacba..3bdf995a8 100644 --- a/src/packaging/linux/jpackage.txt +++ b/src/packaging/linux/jpackage.txt @@ -8,4 +8,4 @@ --runtime-image "${project.build.directory}/jvm-image" --temp "${project.build.directory}/installer-work" --resource-dir "${project.build.directory}/packaging/linux" ---java-options "--enable-preview -XX:MaxRAMPercentage=75 --add-opens=javafx.base/javafx.util=ALL-UNNAMED --add-opens=javafx.base/javafx.event=ALL-UNNAMED --add-opens=javafx.base/javafx.beans.property=ALL-UNNAMED --add-opens=javafx.base/com.sun.javafx.binding=ALL-UNNAMED --add-opens=javafx.base/com.sun.javafx.event=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene=ALL-UNNAMED --add-opens=javafx.graphics/javafx.stage=ALL-UNNAMED --add-opens=javafx.graphics/javafx.geometry=ALL-UNNAMED --add-opens=javafx.graphics/javafx.animation=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene.input=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene.image=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.prism=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.image=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.stage=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.perf=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.cursor=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.geom.transform=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.animation=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.animation.shared=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.effect=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.sg.prism=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.prism.paint=ALL-UNNAMED" +--java-options "-XX:MaxRAMPercentage=75 --add-opens=javafx.base/javafx.util=ALL-UNNAMED --add-opens=javafx.base/javafx.event=ALL-UNNAMED --add-opens=javafx.base/javafx.beans.property=ALL-UNNAMED --add-opens=javafx.base/com.sun.javafx.binding=ALL-UNNAMED --add-opens=javafx.base/com.sun.javafx.event=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene=ALL-UNNAMED --add-opens=javafx.graphics/javafx.stage=ALL-UNNAMED --add-opens=javafx.graphics/javafx.geometry=ALL-UNNAMED --add-opens=javafx.graphics/javafx.animation=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene.input=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene.image=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.prism=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.image=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.stage=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.perf=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.cursor=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.geom.transform=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.animation=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.animation.shared=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.effect=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.sg.prism=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.prism.paint=ALL-UNNAMED" diff --git a/src/packaging/osx/jpackage.txt b/src/packaging/osx/jpackage.txt index 987d2a3a3..8897127c8 100644 --- a/src/packaging/osx/jpackage.txt +++ b/src/packaging/osx/jpackage.txt @@ -6,7 +6,7 @@ --input "${project.build.directory}/dependency" --runtime-image "${project.build.directory}/jvm-image" --temp "${project.build.directory}/installer-work" ---java-options "--enable-preview -XX:MaxRAMPercentage=75 --add-opens=javafx.base/javafx.util=ALL-UNNAMED --add-opens=javafx.base/javafx.event=ALL-UNNAMED --add-opens=javafx.base/javafx.beans.property=ALL-UNNAMED --add-opens=javafx.base/com.sun.javafx.binding=ALL-UNNAMED --add-opens=javafx.base/com.sun.javafx.event=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene=ALL-UNNAMED --add-opens=javafx.graphics/javafx.stage=ALL-UNNAMED --add-opens=javafx.graphics/javafx.geometry=ALL-UNNAMED --add-opens=javafx.graphics/javafx.animation=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene.input=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene.image=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.prism=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.image=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.stage=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.perf=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.cursor=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.geom.transform=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.animation=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.animation.shared=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.effect=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.sg.prism=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.prism.paint=ALL-UNNAMED" +--java-options "-XX:MaxRAMPercentage=75 --add-opens=javafx.base/javafx.util=ALL-UNNAMED --add-opens=javafx.base/javafx.event=ALL-UNNAMED --add-opens=javafx.base/javafx.beans.property=ALL-UNNAMED --add-opens=javafx.base/com.sun.javafx.binding=ALL-UNNAMED --add-opens=javafx.base/com.sun.javafx.event=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene=ALL-UNNAMED --add-opens=javafx.graphics/javafx.stage=ALL-UNNAMED --add-opens=javafx.graphics/javafx.geometry=ALL-UNNAMED --add-opens=javafx.graphics/javafx.animation=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene.input=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene.image=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.prism=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.image=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.stage=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.perf=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.cursor=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.geom.transform=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.animation=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.animation.shared=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.effect=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.sg.prism=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.prism.paint=ALL-UNNAMED" ${macos.sign} ${macos.sign.identity} diff --git a/src/packaging/windows/jpackage.txt b/src/packaging/windows/jpackage.txt index 5a0d23e9b..41c4f219e 100644 --- a/src/packaging/windows/jpackage.txt +++ b/src/packaging/windows/jpackage.txt @@ -11,7 +11,7 @@ --input "${project.build.directory}/dependency" --runtime-image "${project.build.directory}/jvm-image" --temp "${project.build.directory}/installer-work" ---java-options "--enable-preview -XX:MaxRAMPercentage=75 --add-opens=javafx.base/javafx.util=ALL-UNNAMED --add-opens=javafx.base/javafx.event=ALL-UNNAMED --add-opens=javafx.base/javafx.beans.property=ALL-UNNAMED --add-opens=javafx.base/com.sun.javafx.binding=ALL-UNNAMED --add-opens=javafx.base/com.sun.javafx.event=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene=ALL-UNNAMED --add-opens=javafx.graphics/javafx.stage=ALL-UNNAMED --add-opens=javafx.graphics/javafx.geometry=ALL-UNNAMED --add-opens=javafx.graphics/javafx.animation=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene.input=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene.image=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.prism=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.image=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.stage=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.perf=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.cursor=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.geom.transform=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.animation=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.animation.shared=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.effect=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.sg.prism=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.prism.paint=ALL-UNNAMED" +--java-options "-XX:MaxRAMPercentage=75 --add-opens=javafx.base/javafx.util=ALL-UNNAMED --add-opens=javafx.base/javafx.event=ALL-UNNAMED --add-opens=javafx.base/javafx.beans.property=ALL-UNNAMED --add-opens=javafx.base/com.sun.javafx.binding=ALL-UNNAMED --add-opens=javafx.base/com.sun.javafx.event=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene=ALL-UNNAMED --add-opens=javafx.graphics/javafx.stage=ALL-UNNAMED --add-opens=javafx.graphics/javafx.geometry=ALL-UNNAMED --add-opens=javafx.graphics/javafx.animation=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene.input=ALL-UNNAMED --add-opens=javafx.graphics/javafx.scene.image=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.prism=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.application=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.geom=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.image=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.stage=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.perf=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.cursor=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.geom.transform=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.animation=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.animation.shared=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.scenario.effect=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.javafx.sg.prism=ALL-UNNAMED --add-opens=javafx.graphics/com.sun.prism.paint=ALL-UNNAMED" --win-upgrade-uuid ${windows.upgrade.uuid} --description "${project.description}" --copyright "(C) ${windows.vendor}" diff --git a/src/test/kotlin/org/janelia/saalfeldlab/paintera/data/n5/CommitCanvasN5Test.kt b/src/test/kotlin/org/janelia/saalfeldlab/paintera/data/n5/CommitCanvasN5Test.kt index d2f7ec75e..9a44010b8 100644 --- a/src/test/kotlin/org/janelia/saalfeldlab/paintera/data/n5/CommitCanvasN5Test.kt +++ b/src/test/kotlin/org/janelia/saalfeldlab/paintera/data/n5/CommitCanvasN5Test.kt @@ -356,6 +356,7 @@ private class DummyMetadataState(override val dataset: String, override val n5Co override var reader: N5Reader = n5ContainerState.reader override var unit: String = "pixel" override var translation: DoubleArray = DoubleArray(0) + override var virtualCrop: Interval? = null override var resolution: DoubleArray = DoubleArray(0) override var maxIntensity: Double = 0.0 override var minIntensity: Double = 0.0