package org.janelia.saalfeldlab.paintera.ui.opendialog.menu.n5; import bdv.util.volatiles.SharedQueue; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.ObjectBinding; import javafx.beans.binding.StringBinding; import javafx.beans.property.*; import javafx.beans.value.ObservableObjectValue; import javafx.beans.value.ObservableStringValue; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import net.imglib2.Cursor; import net.imglib2.Interval; import net.imglib2.Volatile; import net.imglib2.algorithm.util.Grids; import net.imglib2.cache.img.CachedCellImg; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.type.NativeType; import net.imglib2.type.numeric.IntegerType; import net.imglib2.type.numeric.RealType; import net.imglib2.type.volatiles.AbstractVolatileRealType; import net.imglib2.view.IntervalView; import net.imglib2.view.Views; import net.imglib2.view.composite.RealComposite; import org.controlsfx.control.StatusBar; import org.janelia.saalfeldlab.fx.ui.ExceptionNode; import org.janelia.saalfeldlab.fx.ui.MatchSelection; import org.janelia.saalfeldlab.fx.ui.NumberField; import org.janelia.saalfeldlab.fx.ui.ObjectField; import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread; import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.N5Reader; import org.janelia.saalfeldlab.n5.N5Writer; import org.janelia.saalfeldlab.n5.imglib2.N5LabelMultisets; import org.janelia.saalfeldlab.n5.imglib2.N5Utils; import org.janelia.saalfeldlab.paintera.control.assignment.FragmentSegmentAssignmentState; import org.janelia.saalfeldlab.paintera.data.n5.N5Meta; import org.janelia.saalfeldlab.paintera.data.n5.ReflectionException; import org.janelia.saalfeldlab.paintera.data.n5.VolatileWithSet; import org.janelia.saalfeldlab.paintera.id.IdService; import org.janelia.saalfeldlab.paintera.id.N5IdService; import org.janelia.saalfeldlab.paintera.meshes.MeshWorkerPriority; import org.janelia.saalfeldlab.paintera.state.SourceState; import org.janelia.saalfeldlab.paintera.state.channel.ConnectomicsChannelState; import org.janelia.saalfeldlab.paintera.state.channel.n5.N5BackendChannel; import org.janelia.saalfeldlab.paintera.state.label.ConnectomicsLabelState; import org.janelia.saalfeldlab.paintera.state.label.n5.N5Backend; import org.janelia.saalfeldlab.paintera.state.raw.ConnectomicsRawState; import org.janelia.saalfeldlab.paintera.state.raw.n5.N5BackendRaw; import org.janelia.saalfeldlab.paintera.ui.PainteraAlerts; import org.janelia.saalfeldlab.paintera.ui.opendialog.DatasetInfo; import org.janelia.saalfeldlab.paintera.viewer3d.ViewFrustum; import org.janelia.saalfeldlab.util.NamedThreadFactory; import org.janelia.saalfeldlab.util.concurrent.HashPriorityQueueBasedTaskExecutor; import org.janelia.saalfeldlab.util.n5.N5Helpers; import org.janelia.saalfeldlab.util.n5.N5Types; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Closeable; import java.io.IOException; import java.lang.invoke.MethodHandles; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.LongConsumer; import java.util.function.Supplier; public class GenericBackendDialogN5 implements Closeable { private static final String EMPTY_STRING = ""; private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final String MIN_KEY = "min"; private static final String MAX_KEY = "max"; private static final String ERROR_MESSAGE_PATTERN = "n5? %s -- dataset? %s -- update? %s"; private final DatasetInfo datasetInfo = new DatasetInfo(); private final SimpleObjectProperty<Supplier<N5Writer>> n5Supplier = new SimpleObjectProperty<>(() -> null); private final ObjectBinding<N5Writer> n5 = Bindings.createObjectBinding(() -> Optional .ofNullable(n5Supplier.get()) .map(Supplier::get) .orElse(null), n5Supplier); private final StringProperty dataset = new SimpleStringProperty(); private final ArrayList<Thread> discoveryThreads = new ArrayList<>(); private final ArrayList<BooleanProperty> discoveryIsActive = new ArrayList<>(); private final SimpleBooleanProperty isTraversingDirectories = new SimpleBooleanProperty(); private final BooleanBinding isN5Valid = n5.isNotNull(); private final BooleanBinding isDatasetValid = dataset.isNotNull().and(dataset.isNotEqualTo(EMPTY_STRING)); private final SimpleBooleanProperty datasetUpdateFailed = new SimpleBooleanProperty(false); private final ExecutorService propagationExecutor; private final ObjectProperty<DatasetAttributes> datasetAttributes = new SimpleObjectProperty<>(); private final ObjectBinding<long[]> dimensions = Bindings.createObjectBinding(() -> Optional.ofNullable(datasetAttributes.get()).map(DatasetAttributes::getDimensions).orElse(null), datasetAttributes); private final BooleanBinding isReady = isN5Valid .and(isDatasetValid) .and(datasetUpdateFailed.not()); { isN5Valid.addListener((obs, oldv, newv) -> datasetUpdateFailed.set(false)); } private final StringBinding errorMessage = Bindings.createStringBinding( () -> isReady.get() ? null : String.format(ERROR_MESSAGE_PATTERN, isN5Valid.get(), isDatasetValid.get(), datasetUpdateFailed.not().get() ), isReady ); private final StringBinding name = Bindings.createStringBinding(() -> { final String[] entries = Optional .ofNullable(dataset.get()) .map(d -> d.split("/")) .map(a -> a.length > 0 ? a : new String[] {null}) .orElse(new String[] {null}); return entries[entries.length - 1]; }, dataset); private final ObservableList<String> datasetChoices = FXCollections.observableArrayList(); private final String identifier; private final Node node; public GenericBackendDialogN5( final Node n5RootNode, final Node browseNode, final String identifier, final ObservableValue<Supplier<N5Writer>> writerSupplier, final ExecutorService propagationExecutor) { this("_Dataset", n5RootNode, browseNode, identifier, writerSupplier, propagationExecutor); } public GenericBackendDialogN5( final String datasetPrompt, final Node n5RootNode, final Node browseNode, final String identifier, final ObservableValue<Supplier<N5Writer>> writerSupplier, final ExecutorService propagationExecutor) { this.identifier = identifier; this.node = initializeNode(n5RootNode, datasetPrompt, browseNode); this.propagationExecutor = propagationExecutor; n5Supplier.bind(writerSupplier); n5.addListener((obs, oldv, newv) -> { LOG.debug("Updated n5: obs={} oldv={} newv={}", obs, oldv, newv); if (newv == null) { datasetChoices.clear(); return; } LOG.debug("Updating dataset choices!"); synchronized (discoveryIsActive) { this.isTraversingDirectories.set(false); cancelDiscovery(); final BooleanProperty keepLooking = new SimpleBooleanProperty(true); final Thread discoveryThread = new Thread(() -> { this.isTraversingDirectories.set(true); final AtomicBoolean discardDatasetList = new AtomicBoolean(false); try { final List<String> datasets = N5Helpers.discoverDatasets(newv, keepLooking::get); if (!Thread.currentThread().isInterrupted() && !discardDatasetList.get() && keepLooking.get()) { LOG.debug("Found these datasets: {}", datasets); InvokeOnJavaFXApplicationThread.invoke(() -> datasetChoices.setAll(datasets)); if (!newv.equals(oldv)) { InvokeOnJavaFXApplicationThread.invoke(() -> this.dataset.set(null)); } } } finally { this.isTraversingDirectories.set(false); } }); discoveryIsActive.add(keepLooking); discoveryThread.setDaemon(true); discoveryThread.start(); } }); dataset.addListener((obs, oldv, newv) -> Optional.ofNullable(newv).filter(v -> v.length() > 0).ifPresent(v -> updateDatasetInfo( v, this.datasetInfo))); this.isN5Valid.addListener((obs, oldv, newv) -> cancelDiscovery()); dataset.set(""); } public void cancelDiscovery() { LOG.debug("Canceling discovery."); synchronized (discoveryIsActive) { discoveryIsActive.forEach(a -> a.set(false)); discoveryIsActive.clear(); discoveryThreads.forEach(Thread::interrupt); discoveryThreads.clear(); } } public ObservableObjectValue<DatasetAttributes> datsetAttributesProperty() { return this.datasetAttributes; } public ObservableObjectValue<long[]> dimensionsProperty() { return this.dimensions; } public void updateDatasetInfo(final String group, final DatasetInfo info) { LOG.debug("Updating dataset info for dataset {}", group); try { final N5Reader n5 = this.n5.get(); setResolution(N5Helpers.getResolution(n5, group)); setOffset(N5Helpers.getOffset(n5, group)); this.datasetAttributes.set(N5Helpers.getDatasetAttributes(n5, group)); final DataType dataType = N5Types.getDataType(n5, group); // TODO handle array case! for now just try and set to 0, 1 in case of failure // TODO probably best to always handle min and max as array and populate acoording // to n5 meta data try { this.datasetInfo.minProperty().set(Optional.ofNullable(n5.getAttribute( group, MIN_KEY, Double.class)).orElse(N5Types.minForType(dataType))); this.datasetInfo.maxProperty().set(Optional.ofNullable(n5.getAttribute( group, MAX_KEY, Double.class)).orElse(N5Types.maxForType(dataType))); } catch (final ClassCastException e) { this.datasetInfo.minProperty().set(0.0); this.datasetInfo.maxProperty().set(1.0); } } catch (final IOException e) { ExceptionNode.exceptionDialog(e).show(); } } public Node getDialogNode() { return node; } public StringBinding errorMessage() { return errorMessage; } public DoubleProperty[] resolution() { return this.datasetInfo.spatialResolutionProperties(); } public DoubleProperty[] offset() { return this.datasetInfo.spatialOffsetProperties(); } public DoubleProperty min() { return this.datasetInfo.minProperty(); } public DoubleProperty max() { return this.datasetInfo.maxProperty(); } public FragmentSegmentAssignmentState assignments() throws IOException { return N5Helpers.assignments(n5.get(), this.dataset.get()); } public IdService idService() throws IOException { try { LOG.warn("Getting id service for {} -- {}", this.n5.get(), this.dataset.get()); return N5Helpers.idService(this.n5.get(), this.dataset.get()); } catch (final N5Helpers.MaxIDNotSpecified e) { final Alert alert = PainteraAlerts.alert(Alert.AlertType.CONFIRMATION); alert.setHeaderText("maxId not specified in dataset."); final TextArea ta = new TextArea("Could not read maxId attribute from data set. " + "You can specify the max id manually, or read it from the data set (this can take a long time if your data is big).\n" + "Alternatively, press cancel to load the data set without an id service. " + "Fragment-segment-assignments and selecting new (wrt to the data) labels require an id service " + "and will not be available if you press cancel."); ta.setEditable(false); ta.setWrapText(true); final NumberField<LongProperty> nextIdField = NumberField.longField(0, v -> true, ObjectField.SubmitOn.ENTER_PRESSED, ObjectField.SubmitOn.FOCUS_LOST); final Button scanButton = new Button("Scan Data"); scanButton.setOnAction(event -> { event.consume(); try { findMaxId(this.n5.get(), this.dataset.getValue(), nextIdField.valueProperty()::set); } catch (final IOException e1) { throw new RuntimeException(e1); } }); final HBox maxIdBox = new HBox(new Label("Max Id:"), nextIdField.textField(), scanButton); HBox.setHgrow(nextIdField.textField(), Priority.ALWAYS); alert.getDialogPane().setContent(new VBox(ta, maxIdBox)); final Optional<ButtonType> bt = alert.showAndWait(); if (bt.isPresent() && ButtonType.OK.equals(bt.get())) { final long maxId = nextIdField.valueProperty().get() + 1; this.n5.get().setAttribute(dataset.get(), "maxId", maxId); return new N5IdService(this.n5.get(), this.dataset.get(), maxId); } else return new IdService.IdServiceNotProvided(); } } private static <I extends IntegerType<I> & NativeType<I>> void findMaxId( final N5Reader reader, String dataset, final LongConsumer maxIdTarget ) throws IOException { final int numProcessors = Runtime.getRuntime().availableProcessors(); final ExecutorService es = Executors.newFixedThreadPool(numProcessors, new NamedThreadFactory("max-id-discovery-%d", true)); dataset = N5Helpers.isPainteraDataset(reader, dataset) ? dataset + "/data" : dataset; dataset = N5Helpers.isMultiScale(reader, dataset) ? N5Helpers.getFinestLevelJoinWithGroup(reader, dataset) : dataset; final boolean isLabelMultiset = N5Helpers.getBooleanAttribute(reader, dataset, N5Helpers.IS_LABEL_MULTISET_KEY, false); final CachedCellImg<I, ?> img = isLabelMultiset ? (CachedCellImg<I, ?>) (CachedCellImg) N5LabelMultisets.openLabelMultiset(reader, dataset) : N5Utils.open(reader, dataset); final int[] blockSize = new int[img.numDimensions()]; img.getCellGrid().cellDimensions(blockSize); final List<Interval> blocks = Grids.collectAllContainedIntervals(img.getCellGrid().getImgDimensions(), blockSize); final IntegerProperty completedTasks = new SimpleIntegerProperty(0); final LongProperty maxId = new SimpleLongProperty(0); final BooleanProperty wasCanceled = new SimpleBooleanProperty(false); LOG.debug("Scanning for max id over {} blocks of size {} (total size {}).", blocks.size(), blockSize, img.getCellGrid().getImgDimensions()); final Thread t = new Thread(() -> { final List<Callable<Long>> tasks = new ArrayList<>(); for (final Interval block : blocks) { tasks.add(() -> { long localMaxId = 0; try { final IntervalView<I> interval = Views.interval(img, block); final Cursor<I> cursor = interval.cursor(); while (cursor.hasNext() && !wasCanceled.get()) { final long id = cursor.next().getIntegerLong(); if (id > localMaxId) localMaxId = id; } return localMaxId; } finally { synchronized (completedTasks) { if (!wasCanceled.get()) { LOG.trace("Incrementing completed tasks ({}/{}) and maxId ({}) with {}", completedTasks, blocks.size(), maxId, localMaxId); maxId.setValue(Math.max(maxId.getValue(), localMaxId)); completedTasks.set(completedTasks.get() + 1); } } } }); } try { final List<Future<Long>> futures = es.invokeAll(tasks); for (final Future<Long> future : futures) { final Long id = future.get(); } } catch (final InterruptedException | ExecutionException e) { synchronized (completedTasks) { completedTasks.set(-1); wasCanceled.set(true); } LOG.error("Was interrupted while finding max id.", e); } }); t.setName("max-id-discovery-main-thread"); t.setDaemon(true); t.start(); final Alert alert = PainteraAlerts.alert(Alert.AlertType.CONFIRMATION, true); alert.setHeaderText("Finding max id..."); final BooleanBinding stillRuning = Bindings.createBooleanBinding(() -> completedTasks.get() < blocks.size(), completedTasks); alert.getDialogPane().lookupButton(ButtonType.OK).disableProperty().bind(stillRuning); final StatusBar statusBar = new StatusBar(); completedTasks.addListener((obs, oldv, newv) -> InvokeOnJavaFXApplicationThread.invoke(() -> statusBar.setProgress(newv.doubleValue() / blocks.size()))); completedTasks.addListener((obs, oldv, newv) -> InvokeOnJavaFXApplicationThread.invoke(() -> statusBar.setText(String.format("%s/%d", newv, blocks.size())))); alert.getDialogPane().setContent(statusBar); wasCanceled.addListener((obs, oldv, newv) -> InvokeOnJavaFXApplicationThread.invoke(() -> alert.setHeaderText("Cancelled"))); completedTasks.addListener((obs, oldv, newv) -> InvokeOnJavaFXApplicationThread.invoke(() -> alert.setHeaderText(newv.intValue() < blocks.size() ? "Finding max id: " + maxId.getValue() : "Found max id: " + maxId.getValue()))); final Optional<ButtonType> bt = alert.showAndWait(); if (bt.isPresent() && ButtonType.OK.equals(bt.get())) { LOG.warn("Setting max id to {}", maxId.get()); maxIdTarget.accept(maxId.get()); } else { wasCanceled.set(true); } } private Node initializeNode( final Node rootNode, final String datasetPromptText, final Node browseNode) { final MenuButton datasetDropDown = new MenuButton(); final StringBinding datasetDropDownText = Bindings.createStringBinding(() -> dataset.get() == null || dataset.get().length() == 0 ? datasetPromptText : datasetPromptText + ": " + dataset.get(), dataset); final ObjectBinding<Tooltip> datasetDropDownTooltip = Bindings.createObjectBinding(() -> Optional.ofNullable(dataset.get()).map(Tooltip::new).orElse(null), dataset); datasetDropDown.tooltipProperty().bind(datasetDropDownTooltip); datasetDropDown.disableProperty().bind(this.isN5Valid.not()); datasetDropDown.textProperty().bind(datasetDropDownText); datasetChoices.addListener((ListChangeListener<String>) change -> { final MatchSelection matcher = MatchSelection.fuzzySorted(datasetChoices, s -> { dataset.set(s); datasetDropDown.hide(); }); LOG.debug("Updating dataset dropdown to fuzzy matcher with choices: {}", datasetChoices); final CustomMenuItem menuItem = new CustomMenuItem(matcher, false); // clear style to avoid weird blue highlight menuItem.getStyleClass().clear(); datasetDropDown.getItems().setAll(menuItem); datasetDropDown.setOnAction(e -> {datasetDropDown.show(); matcher.requestFocus();}); }); final GridPane grid = new GridPane(); grid.add(rootNode, 0, 0); grid.add(datasetDropDown, 0, 1); GridPane.setHgrow(rootNode, Priority.ALWAYS); GridPane.setHgrow(datasetDropDown, Priority.ALWAYS); grid.add(browseNode, 1, 0); return grid; } public ObservableStringValue nameProperty() { return name; } public String identifier() { return identifier; } public <T extends RealType<T> & NativeType<T>, V extends AbstractVolatileRealType<T, V> & NativeType<V>> List<? extends SourceState<RealComposite<T>, VolatileWithSet<RealComposite<V>>>> getChannels( final String name, final int[] channelSelection, final SharedQueue queue, final int priority) throws Exception { final N5Reader reader = n5.get(); final String dataset = this.dataset.get(); final N5Meta meta = N5Meta.fromReader(reader, dataset); final double[] resolution = asPrimitiveArray(resolution()); final double[] offset = asPrimitiveArray(offset()); final AffineTransform3D transform = N5Helpers.fromResolutionAndOffset(resolution, offset); final long numChannels = datasetAttributes.get().getDimensions()[3]; LOG.debug("Got channel info: num channels={} channels selection={}", numChannels, channelSelection); final N5BackendChannel<T, V> backend = new N5BackendChannel<>(n5.get(), dataset, channelSelection, 3); final ConnectomicsChannelState<T, V, RealComposite<T>, RealComposite<V>, VolatileWithSet<RealComposite<V>>> state = new ConnectomicsChannelState<>( backend, queue, priority, name + "-" + Arrays.toString(channelSelection), resolution, offset); state.converter().setMins(i -> min().get()); state.converter().setMaxs(i -> max().get()); return Collections.singletonList(state); } public <T extends RealType<T> & NativeType<T>, V extends AbstractVolatileRealType<T, V> & NativeType<V>> SourceState<T, V> getRaw( final String name, final SharedQueue queue, final int priority) throws Exception { LOG.debug("Raw data set requested. Name={}", name); final N5Writer writer = n5.get(); final String dataset = this.dataset.get(); final double[] resolution = asPrimitiveArray(resolution()); final double[] offset = asPrimitiveArray(offset()); final N5BackendRaw<T, V> backend = new N5BackendRaw<>(writer, dataset); final SourceState<T, V> state = new ConnectomicsRawState<>(backend, queue, priority, name, resolution, offset); LOG.debug("Returning raw source state {} {}", name, state); return state; } public <D extends NativeType<D> & IntegerType<D>, T extends Volatile<D> & NativeType<T>> SourceState<D, T> getLabels( final String name, final SharedQueue queue, final int priority, final Group meshesGroup, final ObjectProperty<ViewFrustum> viewFrustumProperty, final ObjectProperty<AffineTransform3D> eyeToWorldTransformProperty, final ExecutorService manager, final HashPriorityQueueBasedTaskExecutor<MeshWorkerPriority> workers, final ExecutorService propagationQueue, final Supplier<String> projectDirectory) throws IOException, ReflectionException { final N5Writer reader = n5.get(); final String dataset = this.dataset.get(); final double[] resolution = asPrimitiveArray(resolution()); final double[] offset = asPrimitiveArray(offset()); final N5Backend<D, T> backend = N5Backend.createFrom( reader, dataset, projectDirectory, propagationQueue); return new ConnectomicsLabelState<>( backend, meshesGroup, viewFrustumProperty, eyeToWorldTransformProperty, manager, workers, queue, priority, name, resolution, offset, null); } public boolean isLabelMultisetType() throws Exception { final N5Writer n5 = this.n5.get(); final String dataset = this.dataset.get(); final Boolean attribute = n5.getAttribute( N5Helpers.isPainteraDataset(n5, dataset) ? dataset + "/" + N5Helpers.PAINTERA_DATA_DATASET : dataset, N5Helpers.LABEL_MULTISETTYPE_KEY, Boolean.class ); LOG.debug("Getting label multiset attribute: {}", attribute); return Optional.ofNullable(attribute).orElse(false); } public DatasetAttributes getAttributes() throws IOException { final N5Reader n5 = this.n5.get(); final String ds = this.dataset.get(); if (n5.datasetExists(ds)) { LOG.debug("Getting attributes for {} and {}", n5, ds); return n5.getDatasetAttributes(ds); } if (n5.listAttributes(ds).containsKey("painteraData")) { LOG.debug("Getting attributes for paintera dataset {}", ds); return n5.getDatasetAttributes(String.format("%s/data/s0", ds)); } final String[] scaleDirs = N5Helpers.listAndSortScaleDatasets(n5, ds); if (scaleDirs.length > 0) { LOG.debug("Getting attributes for {} and {}", n5, scaleDirs[0]); return n5.getDatasetAttributes(String.format("%s/s0", ds)); } throw new RuntimeException(String.format( "Cannot read dataset attributes for group %s and dataset %s.", n5, ds)); } public DataType getDataType() throws IOException { return getAttributes().getDataType(); } public boolean isIntegerType() throws Exception { return N5Types.isIntegerType(getDataType()); } public double[] asPrimitiveArray(final DoubleProperty[] data) { return Arrays.stream(data).mapToDouble(DoubleProperty::get).toArray(); } public void setResolution(final double[] resolution) { final DoubleProperty[] res = resolution(); for (int i = 0; i < res.length; ++i) { res[i].set(resolution[i]); } } public void setOffset(final double[] offset) { final DoubleProperty[] off = offset(); for (int i = 0; i < off.length; ++i) { off[i].set(offset[i]); } } @Override public void close() { LOG.debug("Closing {}", this.getClass().getName()); cancelDiscovery(); } }