package org.janelia.saalfeldlab.paintera.ui.opendialog.menu.n5; import com.pivovarit.function.ThrowingFunction; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.ObjectBinding; import javafx.beans.binding.StringBinding; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableObjectValue; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.geometry.Insets; import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.CustomMenuItem; import javafx.scene.control.Dialog; import javafx.scene.control.Label; import javafx.scene.control.MenuButton; import javafx.scene.control.TitledPane; import javafx.scene.control.Tooltip; import javafx.scene.effect.InnerShadow; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import net.imglib2.Volatile; import net.imglib2.img.cell.CellGrid; 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.composite.RealComposite; import org.janelia.saalfeldlab.fx.ui.Exceptions; import org.janelia.saalfeldlab.fx.ui.MatchSelection; import org.janelia.saalfeldlab.fx.util.InvokeOnJavaFXApplicationThread; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.paintera.Paintera; import org.janelia.saalfeldlab.paintera.PainteraBaseView; import org.janelia.saalfeldlab.paintera.data.n5.VolatileWithSet; import org.janelia.saalfeldlab.paintera.state.SourceState; import org.janelia.saalfeldlab.paintera.ui.opendialog.CombinesErrorMessages; import org.janelia.saalfeldlab.paintera.ui.opendialog.NameField; import org.janelia.saalfeldlab.paintera.ui.opendialog.menu.OpenDialogMenuEntry; import org.janelia.saalfeldlab.paintera.ui.opendialog.meta.MetaPanel; import org.scijava.plugin.Plugin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.invoke.MethodHandles; import java.util.Arrays; 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; import java.util.stream.Collectors; public class N5OpenSourceDialog extends Dialog<GenericBackendDialogN5> implements CombinesErrorMessages { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @Plugin(type = OpenDialogMenuEntry.class, menuPath = "_N5", priority = Double.MAX_VALUE) public static class N5FSOpener implements OpenDialogMenuEntry { private static final FileSystem fs = new FileSystem(); @Override public BiConsumer<PainteraBaseView, Supplier<String>> onAction() { return (pbv, projectDirectory) -> { try (final GenericBackendDialogN5 dialog = fs.backendDialog(pbv.getPropagationQueue())) { N5OpenSourceDialog osDialog = new N5OpenSourceDialog(pbv, dialog); osDialog.setHeaderFromBackendType("N5"); Optional<GenericBackendDialogN5> backend = osDialog.showAndWait(); if (backend == null || !backend.isPresent()) return; N5OpenSourceDialog.addSource(osDialog.getName(), osDialog.getType(), dialog, osDialog.getChannelSelection(), pbv, projectDirectory); fs.containerAccepted(); } catch (Exception e1) { LOG.debug("Unable to open n5 dataset", e1); Exceptions.exceptionAlert(Paintera.Constants.NAME, "Unable to open N5 data set", e1).show(); } }; } } @Plugin(type = OpenDialogMenuEntry.class, menuPath = "_HDF5", priority = Double.MAX_VALUE / 2.0) public static class N5HDFOpener implements OpenDialogMenuEntry { private static final HDF5 hdf5 = new HDF5(); @Override public BiConsumer<PainteraBaseView, Supplier<String>> onAction() { return (pbv, projectDirectory) -> { try (final GenericBackendDialogN5 dialog = hdf5.backendDialog(pbv.getPropagationQueue())) { N5OpenSourceDialog osDialog = new N5OpenSourceDialog(pbv, dialog); osDialog.setHeaderFromBackendType("HDF5"); Optional<GenericBackendDialogN5> backend = osDialog.showAndWait(); if (backend == null || !backend.isPresent()) return; N5OpenSourceDialog.addSource(osDialog.getName(), osDialog.getType(), dialog, osDialog.getChannelSelection(), pbv, projectDirectory); hdf5.containerAccepted(); } catch (Exception e1) { LOG.debug("Unable to open hdf5 dataset", e1); Exceptions.exceptionAlert(Paintera.Constants.NAME, "Unable to open HDF5 data set", e1).show(); } }; } } @Plugin(type = OpenDialogMenuEntry.class, menuPath= "_Google Cloud", priority = Double.MAX_VALUE / 4.0) public static class GoogleCloudOpener implements OpenDialogMenuEntry { @Override public BiConsumer<PainteraBaseView, Supplier<String>> onAction() { return (pbv, projectDirectory) -> { try { final GoogleCloud googleCloud = new GoogleCloud(); try (final GenericBackendDialogN5 dialog = googleCloud.backendDialog(pbv.getPropagationQueue())) { final N5OpenSourceDialog osDialog = new N5OpenSourceDialog(pbv, dialog); osDialog.setHeaderFromBackendType("Google Cloud"); Optional<GenericBackendDialogN5> backend = osDialog.showAndWait(); if (backend == null || !backend.isPresent()) return; N5OpenSourceDialog.addSource(osDialog.getName(), osDialog.getType(), dialog, osDialog.getChannelSelection(), pbv, projectDirectory); } } catch (Exception e1) { LOG.debug("Unable to open google cloud dataset", e1); Exceptions.exceptionAlert(Paintera.Constants.NAME, "Unable to open Google Cloud data set", e1).show(); } }; } } private final VBox dialogContent; private final GridPane grid; private final MenuButton typeChoiceButton; private final ObjectProperty<MetaPanel.TYPE> typeChoice = new SimpleObjectProperty<>(MetaPanel.TYPE.LABEL); private final Label errorMessage; private final TitledPane errorInfo; private final ObservableList<MetaPanel.TYPE> typeChoices = FXCollections.observableArrayList(MetaPanel.TYPE.values()); private final NameField nameField = new NameField( "Source name", "Specify source name (required)", new InnerShadow(10, Color.ORANGE) ); 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"); ((Button)this.getDialogPane().lookupButton(ButtonType.OK)).setText("_OK"); this.errorMessage = new Label(""); this.errorInfo = new TitledPane("", errorMessage); this.isError = Bindings.createBooleanBinding(() -> Optional.ofNullable(this.errorMessage.textProperty().get()) .orElse( "").length() > 0, this.errorMessage.textProperty()); errorInfo.textProperty().bind(Bindings.createStringBinding( () -> this.isError.get() ? "ERROR" : "", this.isError )); this.getDialogPane().lookupButton(ButtonType.OK).disableProperty().bind(this.isError); this.errorInfo.visibleProperty().bind(this.isError); final Tooltip revertAxisTooltip = new Tooltip("If you data is using `zyx` you should revert it."); this.grid = new GridPane(); this.nameField.errorMessageProperty().addListener((obs, oldv, newv) -> combineErrorMessages()); this.dialogContent = new VBox(10, nameField.textField(), grid, metaPanel.getPane(), errorInfo); this.setResizable(true); GridPane.setMargin(this.backendDialog.getDialogNode(), new Insets(0, 0, 0, 30)); this.grid.add(this.backendDialog.getDialogNode(), 1, 0); GridPane.setHgrow(this.backendDialog.getDialogNode(), Priority.ALWAYS); this.getDialogPane().setContent(dialogContent); final VBox choices = new VBox(); this.typeChoiceButton = new MenuButton("_Type"); List<String> typeChoicesString = typeChoices.stream().map(Enum::name).collect(Collectors.toList()); final StringBinding typeChoiceButtonText = Bindings.createStringBinding(() -> typeChoice.get() == null ? "_Type" : "_Type: " + typeChoice.get(), typeChoice); final ObjectBinding<Tooltip> datasetDropDownTooltip = Bindings.createObjectBinding(() -> Optional.ofNullable(typeChoice.get()).map(t -> "Type of the dataset: " + t).map(Tooltip::new).orElse(null), typeChoice); typeChoiceButton.tooltipProperty().bind(datasetDropDownTooltip); typeChoiceButton.textProperty().bind(typeChoiceButtonText); final MatchSelection matcher = MatchSelection.fuzzySorted(typeChoicesString, s -> { typeChoice.set(MetaPanel.TYPE.valueOf(s)); typeChoiceButton.hide(); }); // clear style to avoid weird blue highlight final CustomMenuItem cmi = new CustomMenuItem(matcher, false); cmi.getStyleClass().clear(); typeChoiceButton.getItems().setAll(cmi); typeChoiceButton.setOnAction(e -> {typeChoiceButton.show(); matcher.requestFocus();}); this.metaPanel.bindDataTypeTo(this.typeChoice); backendDialog.datsetAttributesProperty().addListener((obs, oldv, newv) -> Optional .ofNullable(newv) .map(ThrowingFunction.unchecked(this::updateType)) .ifPresent(this.typeChoice::set)); final ObservableObjectValue<DatasetAttributes> attributesProperty = backendDialog.datsetAttributesProperty(); final ObjectBinding<long[]> dimensionsProperty = Bindings.createObjectBinding(() -> attributesProperty.get().getDimensions().clone(), attributesProperty); final DoubleProperty[] res = backendDialog.resolution(); final DoubleProperty[] off = backendDialog.offset(); this.metaPanel.listenOnResolution(res[0], res[1], res[2]); this.metaPanel.listenOnOffset(off[0], off[1], off[2]); this.metaPanel.listenOnMinMax(backendDialog.min(), backendDialog.max()); backendDialog.errorMessage().addListener((obs, oldErr, newErr) -> combineErrorMessages()); backendDialog.nameProperty().addListener((obs, oldName, newName) -> Optional.ofNullable(newName).ifPresent(nameField.textField().textProperty()::set)); combineErrorMessages(); Optional.ofNullable(backendDialog.nameProperty().get()).ifPresent(nameField.textField()::setText); metaPanel.listenOnResolution(backendDialog.resolution()[0], backendDialog.resolution()[1], backendDialog.resolution()[2]); metaPanel.getRevertButton().setOnAction(event -> { backendDialog.setResolution(revert(metaPanel.getResolution())); backendDialog.setOffset(revert(metaPanel.getOffset())); }); this.typeChoice.setValue(typeChoices.get(0)); this.typeChoiceButton.setMinWidth(100); choices.getChildren().addAll(this.typeChoiceButton); this.grid.add(choices, 0, 0); this.setResultConverter(button -> button.equals(ButtonType.OK) ? backendDialog : null); combineErrorMessages(); setTitle(Paintera.Constants.NAME); } public MetaPanel.TYPE getType() { return typeChoice.getValue(); } public int[] getChannelSelection() { return metaPanel.channelInformation().getChannelSelectionCopy(); } public String getName() { return nameField.getText(); } public MetaPanel getMeta() { return this.metaPanel; } @Override public Collection<ObservableValue<String>> errorMessages() { return Arrays.asList(this.nameField.errorMessageProperty(), getBackend().errorMessage()); } @Override public Consumer<Collection<String>> combiner() { return strings -> InvokeOnJavaFXApplicationThread.invoke(() -> this.errorMessage.setText(String.join( "\n", strings ))); } private GenericBackendDialogN5 getBackend() { return this.backendDialog; } private static final double[] revert(final double[] array) { final double[] reverted = new double[array.length]; for (int i = 0; i < array.length; ++i) { reverted[i] = array[array.length - 1 - i]; } return reverted; } private static void addSource( final String name, final MetaPanel.TYPE type, final GenericBackendDialogN5 dataset, final int[] channelSelection, final PainteraBaseView viewer, final Supplier<String> projectDirectory) throws Exception { LOG.debug("Type={}", type); switch (type) { case RAW: LOG.trace("Adding raw data"); addRaw(name, channelSelection, dataset, viewer); break; case LABEL: addLabel(name, dataset, viewer, projectDirectory); break; default: break; } } private static <T extends RealType<T> & NativeType<T>, V extends AbstractVolatileRealType<T, V> & NativeType<V>> void addRaw( final String name, final int[] channelSelection, final GenericBackendDialogN5 dataset, PainteraBaseView viewer) throws Exception { final DatasetAttributes attributes = dataset.getAttributes(); if (attributes.getNumDimensions() == 4) { LOG.debug("4-dimensional data, assuming channel index at {}", 3); final List<? extends SourceState<RealComposite<T>, VolatileWithSet<RealComposite<V>>>> channels = dataset.getChannels( name, channelSelection, viewer.getQueue(), viewer.getQueue().getNumPriorities() - 1); LOG.debug("Got {} channel sources", channels.size()); InvokeOnJavaFXApplicationThread.invoke(() -> channels.forEach(viewer::addState)); LOG.debug("Added {} channel sources", channels.size()); } else { final SourceState<T, V> raw = dataset.getRaw(name, viewer.getQueue(), viewer.getQueue().getNumPriorities() - 1); LOG.debug("Got raw: {}", raw); InvokeOnJavaFXApplicationThread.invoke(() -> viewer.addState(raw)); } } private static <D extends NativeType<D> & IntegerType<D>, T extends Volatile<D> & NativeType<T>> void addLabel( final String name, final GenericBackendDialogN5 dataset, final PainteraBaseView viewer, final Supplier<String> projectDirectory) throws Exception { final DatasetAttributes attributes = dataset.getAttributes(); if (attributes.getNumDimensions() > 3) throw new Exception("Only 3D label data supported but got " + attributes.getNumDimensions() + " dimensions."); final SourceState<D, T> rep = dataset.getLabels( name, viewer.getQueue(), viewer.getQueue().getNumPriorities() - 1, viewer.viewer3D().meshesGroup(), viewer.viewer3D().viewFrustumProperty(), viewer.viewer3D().eyeToWorldTransformProperty(), viewer.getMeshManagerExecutorService(), viewer.getMeshWorkerExecutorService(), viewer.getPropagationQueue(), projectDirectory ); InvokeOnJavaFXApplicationThread.invoke(() -> viewer.addState(rep)); } private static int[] blockSize(final CellGrid grid) { final int[] blockSize = new int[grid.numDimensions()]; Arrays.setAll(blockSize, grid::cellDimension); return blockSize; } public void setHeaderFromBackendType(String backendType) { this.setHeaderText(String.format("Open %s dataset", backendType)); } private MetaPanel.TYPE updateType(final DatasetAttributes attributes) throws Exception { if (attributes == null) return null; if (this.backendDialog.isLabelMultisetType()) { return MetaPanel.TYPE.LABEL; } switch (attributes.getDataType()) { case UINT64: case UINT32: case INT64: return MetaPanel.TYPE.LABEL; default: return MetaPanel.TYPE.RAW; } } }