diff --git a/.editorconfig b/.editorconfig index d3b2842..03a7a74 100644 --- a/.editorconfig +++ b/.editorconfig @@ -41,20 +41,20 @@ ij_java_array_initializer_new_line_after_left_brace = false ij_java_array_initializer_right_brace_on_new_line = false ij_java_array_initializer_wrap = on_every_item ij_java_assert_statement_colon_on_next_line = false -ij_java_assert_statement_wrap = off +ij_java_assert_statement_wrap = normal ij_java_assignment_wrap = off ij_java_binary_operation_sign_on_next_line = false ij_java_binary_operation_wrap = normal ij_java_blank_lines_after_anonymous_class_header = 0 -ij_java_blank_lines_after_class_header = 0 -ij_java_blank_lines_after_imports = 1 -ij_java_blank_lines_after_package = 1 -ij_java_blank_lines_around_class = 1 +ij_java_blank_lines_after_class_header = 1 +ij_java_blank_lines_after_imports = 2 +ij_java_blank_lines_after_package = 2 +ij_java_blank_lines_around_class = 2 ij_java_blank_lines_around_field = 0 ij_java_blank_lines_around_field_in_interface = 0 ij_java_blank_lines_around_initializer = 1 ij_java_blank_lines_around_method = 2 -ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_around_method_in_interface = 2 ij_java_blank_lines_before_class_end = 1 ij_java_blank_lines_before_imports = 1 ij_java_blank_lines_before_method_body = 0 @@ -63,23 +63,23 @@ ij_java_block_brace_style = end_of_line ij_java_block_comment_at_first_column = true ij_java_call_parameters_new_line_after_left_paren = false ij_java_call_parameters_right_paren_on_new_line = false -ij_java_call_parameters_wrap = off +ij_java_call_parameters_wrap = normal ij_java_case_statement_on_separate_line = true ij_java_catch_on_new_line = false ij_java_class_annotation_wrap = split_into_lines ij_java_class_brace_style = end_of_line -ij_java_class_count_to_use_import_on_demand = 11 +ij_java_class_count_to_use_import_on_demand = 20 ij_java_class_names_in_javadoc = 1 ij_java_do_not_indent_top_level_class_members = false ij_java_do_not_wrap_after_single_annotation = false -ij_java_do_while_brace_force = never +ij_java_do_while_brace_force = always ij_java_doc_add_blank_line_after_description = true ij_java_doc_add_blank_line_after_param_comments = true ij_java_doc_add_blank_line_after_return = true ij_java_doc_add_p_tag_on_empty_lines = true ij_java_doc_align_exception_comments = true ij_java_doc_align_param_comments = true -ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_do_not_wrap_if_one_line = true ij_java_doc_enable_formatting = true ij_java_doc_enable_leading_asterisks = true ij_java_doc_indent_on_continuation = false @@ -92,19 +92,19 @@ ij_java_doc_param_description_on_new_line = false ij_java_doc_preserve_line_breaks = false ij_java_doc_use_throws_not_exception_tag = true ij_java_else_on_new_line = false -ij_java_enum_constants_wrap = off -ij_java_extends_keyword_wrap = normal +ij_java_enum_constants_wrap = on_every_item +ij_java_extends_keyword_wrap = off ij_java_extends_list_wrap = on_every_item ij_java_field_annotation_wrap = split_into_lines ij_java_finally_on_new_line = false -ij_java_for_brace_force = never +ij_java_for_brace_force = always ij_java_for_statement_new_line_after_left_paren = false ij_java_for_statement_right_paren_on_new_line = false ij_java_for_statement_wrap = on_every_item ij_java_generate_final_locals = false ij_java_generate_final_parameters = false -ij_java_if_brace_force = never -ij_java_imports_layout = *,|,javax.**,java.**,|,$* +ij_java_if_brace_force = always +ij_java_imports_layout = *, |, javax.**, java.**, |, $* ij_java_indent_case_from_switch = true ij_java_insert_inner_class_imports = false ij_java_insert_override_annotation = true @@ -112,15 +112,16 @@ ij_java_keep_blank_lines_before_right_brace = 2 ij_java_keep_blank_lines_between_package_declaration_and_header = 2 ij_java_keep_blank_lines_in_code = 2 ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_builder_methods_indents = true ij_java_keep_control_statement_in_one_line = true ij_java_keep_first_column_comment = true ij_java_keep_indents_on_empty_lines = false ij_java_keep_line_breaks = true ij_java_keep_multiple_expressions_in_one_line = false ij_java_keep_simple_blocks_in_one_line = false -ij_java_keep_simple_classes_in_one_line = false -ij_java_keep_simple_lambdas_in_one_line = false -ij_java_keep_simple_methods_in_one_line = false +ij_java_keep_simple_classes_in_one_line = true +ij_java_keep_simple_lambdas_in_one_line = true +ij_java_keep_simple_methods_in_one_line = true # <- For empty methods ij_java_label_indent_absolute = false ij_java_label_indent_size = 0 ij_java_lambda_brace_style = end_of_line @@ -129,10 +130,10 @@ ij_java_line_comment_add_space = false ij_java_line_comment_at_first_column = true ij_java_method_annotation_wrap = split_into_lines ij_java_method_brace_style = end_of_line -ij_java_method_call_chain_wrap = normal +ij_java_method_call_chain_wrap = on_every_item ij_java_method_parameters_new_line_after_left_paren = false ij_java_method_parameters_right_paren_on_new_line = false -ij_java_method_parameters_wrap = on_every_item +ij_java_method_parameters_wrap = normal ij_java_modifier_list_wrap = false ij_java_names_count_to_use_import_on_demand = 11 ij_java_new_line_after_lparen_in_record_header = false @@ -149,7 +150,7 @@ ij_java_replace_null_check = true ij_java_replace_sum_lambda_with_method_ref = true ij_java_resource_list_new_line_after_left_paren = false ij_java_resource_list_right_paren_on_new_line = false -ij_java_resource_list_wrap = normal +ij_java_resource_list_wrap = on_every_item ij_java_rparen_on_new_line_in_record_header = false ij_java_space_after_closing_angle_bracket_in_type_argument = false ij_java_space_after_colon = true @@ -232,16 +233,16 @@ ij_java_ternary_operation_signs_on_next_line = false ij_java_ternary_operation_wrap = normal ij_java_test_name_suffix = Test ij_java_throws_keyword_wrap = normal -ij_java_throws_list_wrap = normal +ij_java_throws_list_wrap = on_every_item ij_java_use_external_annotations = false ij_java_use_fq_class_names = false ij_java_use_relative_indents = false ij_java_use_single_class_imports = true ij_java_variable_annotation_wrap = off ij_java_visibility = public -ij_java_while_brace_force = never +ij_java_while_brace_force = always ij_java_while_on_new_line = false -ij_java_wrap_comments = false +ij_java_wrap_comments = true ij_java_wrap_first_method_in_call_chain = false ij_java_wrap_long_lines = false diff --git a/README.md b/README.md index ef0685a..1662102 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ An ImageJ plugin to run a script on a batch of images from/to OMERO. 1. Install the [OMERO.insight plugin](https://omero-guides.readthedocs.io/en/latest/fiji/docs/installation.html) (if you haven't already). -2. Download the JAR file for this [library](https://github.com/GReD-Clermont/simple-omero-client/releases/tag/5.12.1/). -3. Download the JAR file ([for this plugin](https://github.com/GReD-Clermont/omero_batch-plugin/releases/tag/1.0.5/)). +2. Download the JAR file for this [library](https://github.com/GReD-Clermont/simple-omero-client/releases/tag/5.16.0/). +3. Download the JAR file ([for this plugin](https://github.com/GReD-Clermont/omero_batch-plugin/releases/tag/2.0.0/)). 4. Place these JAR files in your "plugins" folder. ## How to use diff --git a/pom.xml b/pom.xml index 63d14ce..04485a7 100644 --- a/pom.xml +++ b/pom.xml @@ -6,12 +6,12 @@ pom-scijava org.scijava - 33.2.0 + 37.0.0 fr.igred omero_batch-plugin - 1.0.5 + 2.0.0-SNAPSHOT omero_batch-plugin ImageJ plugin to batch process images from OMERO. @@ -48,7 +48,7 @@ ppouchin Pierre Pouchin pierre.pouchin@uca.fr - https://www.igred.fr/directory/member/pierre-pouchin/ + https://www.igred.fr/en/member/pierre_pouchin/ GReD (INSERM U1103 / CNRS UMR 6293 / UCA) https://www.igred.fr @@ -114,8 +114,6 @@ UTF-8 - 8 - 8 gpl_v2 MICA & GReD @@ -144,7 +142,7 @@ fr.igred simple-omero-client - 5.12.1 + 5.16.0 org.scijava @@ -174,9 +172,7 @@ - org.apache.maven.plugins maven-assembly-plugin - 2.6 package @@ -204,16 +200,4 @@ - - - - - maven-javadoc-plugin - - false - - - - - diff --git a/src/main/java/fr/igred/ij/gui/OMEROConnectDialog.java b/src/main/java/fr/igred/ij/gui/OMEROConnectDialog.java index 8284683..6b6cceb 100644 --- a/src/main/java/fr/igred/ij/gui/OMEROConnectDialog.java +++ b/src/main/java/fr/igred/ij/gui/OMEROConnectDialog.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 MICA & GReD + * Copyright (C) 2021-2023 MICA & GReD * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software @@ -16,6 +16,7 @@ */ package fr.igred.ij.gui; + import fr.igred.omero.Client; import fr.igred.omero.exception.ServiceException; import ij.Prefs; @@ -39,18 +40,27 @@ import static javax.swing.JOptionPane.showMessageDialog; + /** * Connection dialog for OMERO. */ public class OMEROConnectDialog extends JDialog implements ActionListener { + /** The host field. */ private final JTextField hostField = new JTextField(""); + /** The port field. */ private final JFormattedTextField portField = new JFormattedTextField(NumberFormat.getIntegerInstance()); + /** The user field. */ private final JTextField userField = new JTextField(""); + /** The password field. */ private final JPasswordField passwordField = new JPasswordField(""); + /** The login button. */ private final JButton login = new JButton("Login"); + /** The cancel button. */ private final JButton cancel = new JButton("Cancel"); + /** The client. */ private transient Client client; + /** True if cancel was pressed. */ private boolean cancelled = false; @@ -58,8 +68,6 @@ public class OMEROConnectDialog extends JDialog implements ActionListener { * Creates a new dialog to connect the specified client, but does not display it. */ public OMEROConnectDialog() { - super(); - final int width = 350; final int height = 200; diff --git a/src/main/java/fr/igred/ij/gui/ProgressDialog.java b/src/main/java/fr/igred/ij/gui/ProgressDialog.java index edd31c8..b2e3bf9 100644 --- a/src/main/java/fr/igred/ij/gui/ProgressDialog.java +++ b/src/main/java/fr/igred/ij/gui/ProgressDialog.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 MICA & GReD + * Copyright (C) 2021-2023 MICA & GReD * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software @@ -16,6 +16,7 @@ */ package fr.igred.ij.gui; + import fr.igred.ij.macro.ProgressMonitor; import javax.swing.BoxLayout; @@ -27,12 +28,19 @@ import java.awt.Container; import java.awt.Font; + /** * Progress dialog for batch processing. */ public class ProgressDialog extends JFrame implements ProgressMonitor { + + /** The progress label. */ private final JLabel progressLabel = new JLabel("", SwingConstants.CENTER); + + /** The state label. */ private final JLabel stateLabel = new JLabel("", SwingConstants.CENTER); + + /** The OK button. */ private final JButton ok = new JButton("OK"); @@ -48,7 +56,8 @@ public ProgressDialog() { Font progFont = new Font("Arial", Font.BOLD, 12); JLabel warnLabel = new JLabel("", SwingConstants.CENTER); warnLabel - .setText(" Warning:
Image processing can take time
depending on your network rate "); + .setText( + " Warning:
Image processing can take time
depending on your network rate "); warnLabel.setFont(warnFont); progressLabel.setFont(progFont); stateLabel.setFont(progFont); diff --git a/src/main/java/fr/igred/ij/gui/package-info.java b/src/main/java/fr/igred/ij/gui/package-info.java index c13bb54..c9d16a6 100644 --- a/src/main/java/fr/igred/ij/gui/package-info.java +++ b/src/main/java/fr/igred/ij/gui/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 MICA & GReD + * Copyright (C) 2021-2023 MICA & GReD * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software diff --git a/src/main/java/fr/igred/ij/io/BatchImage.java b/src/main/java/fr/igred/ij/io/BatchImage.java new file mode 100644 index 0000000..1adff81 --- /dev/null +++ b/src/main/java/fr/igred/ij/io/BatchImage.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2021-2023 MICA & GReD + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 Franklin + * Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package fr.igred.ij.io; + + +import fr.igred.omero.repository.ImageWrapper; +import ij.ImagePlus; + + +/** + * Interface to open images and retrieve the corresponding image on OMERO, if applicable. + */ +public interface BatchImage { + + /** + * Returns the related ImageWrapper, or null if there is none. + * + * @return See above. + */ + ImageWrapper getImageWrapper(); + + + /** + * Opens the image and returns the corresponding ImagePlus. + * + * @param mode The mode used to load ROIs. + * + * @return See above. + */ + ImagePlus getImagePlus(ROIMode mode); + + + /** + * Opens the image and returns the corresponding ImagePlus, with no ROI. + * + * @return See above. + */ + default ImagePlus getImagePlus() { + return getImagePlus(ROIMode.DO_NOT_LOAD); + } + +} diff --git a/src/main/java/fr/igred/ij/io/LocalBatchImage.java b/src/main/java/fr/igred/ij/io/LocalBatchImage.java new file mode 100644 index 0000000..870eacb --- /dev/null +++ b/src/main/java/fr/igred/ij/io/LocalBatchImage.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2021-2023 MICA & GReD + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 Franklin + * Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package fr.igred.ij.io; + + +import fr.igred.omero.repository.ImageWrapper; +import ij.ImagePlus; +import ij.gui.Overlay; +import ij.gui.Roi; +import ij.plugin.frame.RoiManager; +import loci.formats.FileStitcher; +import loci.formats.FormatException; +import loci.plugins.BF; +import loci.plugins.in.ImportProcess; +import loci.plugins.in.ImporterOptions; + +import java.io.File; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.logging.Logger; + + +/** + * Image stored in a local file. + */ +public class LocalBatchImage implements BatchImage { + + /** The logger. */ + private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass().getName()); + + /** Empty file array defined once. */ + private static final File[] EMPTY_FILE_ARRAY = new File[0]; + + /** The path to the image. */ + private final String path; + /** The image index. */ + private final Integer index; + + + /** + * Creates a new instance with the specified path and index. + * + * @param path The path. + * @param index The image index. + */ + public LocalBatchImage(String path, Integer index) { + this.path = path; + this.index = index; + } + + + /** + * List the files contained in the directory. + * + * @param directory The directory. + * @param recursive Whether files should be listed recursively. + * + * @return The list of file paths. + */ + private static List listFiles(File directory, boolean recursive) { + File[] files = directory.listFiles(); + if (files == null) { + files = EMPTY_FILE_ARRAY; + } + List paths = new ArrayList<>(files.length); + for (File file : files) { + if (!file.isDirectory()) { + paths.add(file.getAbsolutePath()); + } else if (recursive) { + paths.addAll(listFiles(file, true)); + } + } + return paths; + } + + + /** + * Creates a list of images to be opened, contained in the specified directory. + * + * @param directory The directory. + * @param recursive Whether files should be listed recursively. + * + * @return The list of images. + * + * @throws IOException ImporterOptions could not be instantiated. + */ + public static List listImages(String directory, boolean recursive) throws IOException { + ImporterOptions options = initImporterOptions(); + List batchImages = new LinkedList<>(); + File dir = new File(directory); + List files = listFiles(dir, recursive); + List used = new ArrayList<>(files.size()); + for (String file : files) { + if (!used.contains(file)) { + // Open the image + options.setId(file); + ImportProcess process = new ImportProcess(options); + try { + process.execute(); + int n = process.getSeriesCount(); + FileStitcher fs = process.getFileStitcher(); + if (fs != null) { + used = Arrays.asList(fs.getUsedFiles()); + } else { + used.add(file); + } + for (int i = 0; i < n; i++) { + batchImages.add(new LocalBatchImage(file, i)); + } + } catch (IOException | FormatException e) { + LOGGER.severe(e.getMessage()); + } + } + } + return batchImages; + } + + + /** + * Initializes the Bio-Formats importer options. + * + * @return See above. + * + * @throws IOException If the importer options could not be initialized. + */ + private static ImporterOptions initImporterOptions() throws IOException { + ImporterOptions options = new ImporterOptions(); + options.setStackFormat(ImporterOptions.VIEW_HYPERSTACK); + options.setSwapDimensions(false); + options.setOpenAllSeries(false); + options.setSpecifyRanges(false); + options.setShowMetadata(false); + options.setShowOMEXML(false); + options.setCrop(false); + options.setSplitChannels(false); + options.setSplitFocalPlanes(false); + options.setSplitTimepoints(false); + return options; + } + + + /** + * Returns null. + * + * @return See above. + */ + @Override + public ImageWrapper getImageWrapper() { + return null; + } + + + /** + * Opens the image and returns the corresponding ImagePlus. + * + * @return See above. + */ + @Override + public ImagePlus getImagePlus(ROIMode mode) { + ImagePlus imp = null; + boolean loadROIs = mode != ROIMode.DO_NOT_LOAD; + try { + ImporterOptions options = initImporterOptions(); + options.setShowROIs(loadROIs); + if (loadROIs) { + options.setROIsMode(mode.toString()); + loadROIs(imp, RoiManager.getInstance2(), mode); + } + options.setId(path); + options.setSeriesOn(index, true); + ImagePlus[] imps = BF.openImagePlus(options); + imp = imps[0]; + } catch (FormatException | IOException e) { + LOGGER.severe(e.getMessage()); + } + return imp; + } + + + /** + * Returns the path to the ROI next to the image file. + * + * @return See above. + */ + @SuppressWarnings("MagicCharacter") + private String getRoiPath() { + String beforeExt = path.substring(0, path.lastIndexOf('.')); + beforeExt = beforeExt.isEmpty() ? path : beforeExt; + if (beforeExt.toLowerCase(Locale.ROOT).endsWith(".ome") && beforeExt.lastIndexOf('.') > 0) { + beforeExt = beforeExt.substring(0, beforeExt.lastIndexOf('.')); + } + + String imageIndex = index == null || index.equals(0) ? "" : "-" + index; + + boolean roiExists; + String roiPath = beforeExt + imageIndex + ".roi"; + File roiFile = new File(roiPath); + if (!roiFile.exists() || !roiFile.isFile()) { + roiPath = beforeExt + imageIndex + "_RoiSet.zip"; + File zipFile = new File(roiPath); + roiExists = zipFile.exists() && zipFile.isFile(); + } else { + roiExists = true; + } + return roiExists ? roiPath : ""; + } + + + /** + * Loads ROIs from an image in OMERO into ImageJ. + * + * @param imp The image in ImageJ ROIs should be linked to. + * @param manager The ROI Manager. + * @param roiMode The mode used to load ROIs. + */ + private void loadROIs(ImagePlus imp, RoiManager manager, ROIMode roiMode) { + RoiManager rm = manager; + if (rm == null) { + rm = RoiManager.getRoiManager(); + } + + String roiPath = getRoiPath(); + if (!roiPath.isEmpty() && roiMode != ROIMode.DO_NOT_LOAD) { + rm.open(roiPath); + Roi[] ijRois = rm.getRoisAsArray(); + for (Roi ijRoi : ijRois) { + ijRoi.setImage(imp); + } + if (imp != null && roiMode == ROIMode.OVERLAY) { + Overlay overlay = imp.getOverlay(); + for (Roi ijRoi : ijRois) { + overlay.add(ijRoi, ijRoi.getName()); + } + rm.reset(); + } + } + } + + + @Override + public String toString() { + return "LocalBatchImage{" + + "path='" + path + "'" + + ", index=" + index + + "}"; + } + +} diff --git a/src/main/java/fr/igred/ij/io/OMEROBatchImage.java b/src/main/java/fr/igred/ij/io/OMEROBatchImage.java new file mode 100644 index 0000000..e03ef0e --- /dev/null +++ b/src/main/java/fr/igred/ij/io/OMEROBatchImage.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2021-2023 MICA & GReD + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 Franklin + * Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package fr.igred.ij.io; + + +import fr.igred.omero.Client; +import fr.igred.omero.exception.AccessException; +import fr.igred.omero.exception.ServiceException; +import fr.igred.omero.repository.ImageWrapper; +import fr.igred.omero.roi.ROIWrapper; +import ij.ImagePlus; +import ij.gui.Overlay; +import ij.gui.Roi; +import ij.plugin.frame.RoiManager; + +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; +import java.util.stream.Collectors; + + +/** + * Image from OMERO. + */ +public class OMEROBatchImage implements BatchImage { + + /** The logger. */ + private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass().getName()); + + /** The OMERO client. */ + private final Client client; + /** The OMERO image. */ + private final ImageWrapper imageWrapper; + + + /** + * Creates a new instance with the specified client and image. + * + * @param client The OMERO client. + * @param imageWrapper The OMERO image. + */ + public OMEROBatchImage(Client client, ImageWrapper imageWrapper) { + this.client = client; + this.imageWrapper = imageWrapper; + } + + + /** + * Creates a list of OMERO images to be opened. + * + * @param client The OMERO client. + * @param images The list of ImageWrappers. + * + * @return The list of images. + */ + public static List listImages(Client client, Collection images) { + return images.stream().map(i -> new OMEROBatchImage(client, i)).collect(Collectors.toList()); + } + + + /** + * Returns the related ImageWrapper, or null if there is none. + * + * @return See above. + */ + @Override + public ImageWrapper getImageWrapper() { + return imageWrapper; + } + + + /** + * Opens the image and returns the corresponding ImagePlus. + * + * @return See above. + */ + @Override + public ImagePlus getImagePlus(ROIMode mode) { + ImagePlus imp = null; + try { + imp = imageWrapper.toImagePlus(client); + // Store image "annotate" permissions as a property in the ImagePlus object + imp.setProp("Annotatable", String.valueOf(imageWrapper.canAnnotate())); + if (mode != ROIMode.DO_NOT_LOAD) { + loadROIs(imp, RoiManager.getInstance2(), mode); + } + } catch (ExecutionException | ServiceException | AccessException e) { + LOGGER.severe("Could not load image: " + e.getMessage()); + } + return imp; + } + + + /** + * Loads ROIs from an image in OMERO into ImageJ (removes previous ROIs). + * + * @param imp The image in ImageJ ROIs should be linked to. + * @param manager The ROI Manager. + * @param roiMode The mode used to load ROIs. + */ + private void loadROIs(ImagePlus imp, RoiManager manager, ROIMode roiMode) { + List ijRois = new ArrayList<>(0); + RoiManager rm = manager; + if (rm == null) { + rm = RoiManager.getRoiManager(); + } + try { + ijRois = ROIWrapper.toImageJ(imageWrapper.getROIs(client)); + } catch (ExecutionException | ServiceException | AccessException e) { + LOGGER.severe("Could not load ROIs: " + e.getMessage()); + } + if (roiMode == ROIMode.OVERLAY && imp != null) { + Overlay overlay = imp.getOverlay(); + if (overlay != null) { + overlay.clear(); + } else { + overlay = new Overlay(); + } + for (Roi ijRoi : ijRois) { + ijRoi.setImage(imp); + overlay.add(ijRoi, ijRoi.getName()); + } + } else if (roiMode == ROIMode.MANAGER && rm != null) { + rm.reset(); // Reset ROI manager to clear previous ROIs + for (Roi ijRoi : ijRois) { + ijRoi.setImage(imp); + rm.addRoi(ijRoi); + } + } + } + + + @Override + public String toString() { + return "OMEROBatchImage{" + + "client=" + client + + ", imageWrapper=" + imageWrapper + + "}"; + } + +} diff --git a/src/main/java/fr/igred/ij/io/ROIMode.java b/src/main/java/fr/igred/ij/io/ROIMode.java new file mode 100644 index 0000000..573dc97 --- /dev/null +++ b/src/main/java/fr/igred/ij/io/ROIMode.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2021-2023 MICA & GReD + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 Franklin + * Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package fr.igred.ij.io; + + +import loci.plugins.in.ImporterOptions; + + +/** + * Modes used to load ROIs. + */ +public enum ROIMode { + /** + * Do not load ROIs. + */ + DO_NOT_LOAD("No"), + /** + * Load ROIs in the ROI Manager. + */ + MANAGER(ImporterOptions.ROIS_MODE_MANAGER), + /** + * Load ROIs as overlay. + */ + OVERLAY(ImporterOptions.ROIS_MODE_OVERLAY); + + /** + * ROI mode String value for ImporterOptions and user selection. + */ + private final String value; + + + /** + * Constructor of the ROIMode enum. + * + * @param value The ROI mode String value for ImporterOptions. + */ + ROIMode(String value) { + this.value = value; + } + + + /** + * Returns the ROI mode String value for ImporterOptions. + * + * @return See above. + */ + @Override + public String toString() { + return value; + } +} diff --git a/src/main/java/fr/igred/ij/io/package-info.java b/src/main/java/fr/igred/ij/io/package-info.java new file mode 100644 index 0000000..0b87a2c --- /dev/null +++ b/src/main/java/fr/igred/ij/io/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2021-2023 MICA & GReD + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 Franklin + * Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +/** + * This package contains interfaces and classes meant to handle input/output, most notably images: + *
    + *
  • {@link fr.igred.ij.io.BatchImage} to generally handle images in batch
  • + *
  • {@link fr.igred.ij.io.OMEROBatchImage} to manage images from OMERO
  • + *
  • {@link fr.igred.ij.io.LocalBatchImage} to manage local images
  • + *
+ * It also contains {@link fr.igred.ij.io.ROIMode} to handle ROI loading. + */ +package fr.igred.ij.io; \ No newline at end of file diff --git a/src/main/java/fr/igred/ij/macro/BatchListener.java b/src/main/java/fr/igred/ij/macro/BatchListener.java index 923288b..2a42ec2 100644 --- a/src/main/java/fr/igred/ij/macro/BatchListener.java +++ b/src/main/java/fr/igred/ij/macro/BatchListener.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 MICA & GReD + * Copyright (C) 2021-2023 MICA & GReD * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software @@ -16,8 +16,10 @@ */ package fr.igred.ij.macro; + import java.util.EventListener; + /** * Listens to batch runner thread. */ diff --git a/src/main/java/fr/igred/ij/macro/BatchParameters.java b/src/main/java/fr/igred/ij/macro/BatchParameters.java new file mode 100644 index 0000000..6090671 --- /dev/null +++ b/src/main/java/fr/igred/ij/macro/BatchParameters.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2021-2023 MICA & GReD + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 Franklin + * Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package fr.igred.ij.macro; + + +import fr.igred.ij.io.ROIMode; + + +/** + * Holds the parameters to batch run scripts. + */ +public class BatchParameters { + + private ROIMode roiMode; + private boolean saveImages; + private boolean saveROIs; + private boolean saveResults; + private boolean saveLog; + private boolean clearROIs; + private boolean outputOnOMERO; + private boolean outputOnLocal; + private long outputDatasetId; + private long outputProjectId; + private String directoryOut; + private String suffix; + + + /** + * Default constructor. + */ + public BatchParameters() { + this.roiMode = ROIMode.DO_NOT_LOAD; + this.saveImages = false; + this.saveROIs = false; + this.saveResults = false; + this.saveLog = false; + this.clearROIs = false; + this.outputOnOMERO = false; + this.outputOnLocal = false; + this.outputDatasetId = -1L; + this.outputProjectId = -1L; + this.suffix = ""; + this.directoryOut = null; + } + + + /** + * Copy constructor. + * + * @param parameters The parameters to copy. + */ + public BatchParameters(BatchParameters parameters) { + this.roiMode = parameters.roiMode; + this.saveImages = parameters.saveImages; + this.saveROIs = parameters.saveROIs; + this.saveResults = parameters.saveResults; + this.saveLog = parameters.saveLog; + this.clearROIs = parameters.clearROIs; + this.outputOnOMERO = parameters.outputOnOMERO; + this.outputOnLocal = parameters.outputOnLocal; + this.outputDatasetId = parameters.outputDatasetId; + this.outputProjectId = parameters.outputProjectId; + this.suffix = parameters.suffix; + this.directoryOut = parameters.directoryOut; + } + + + /** + * Returns the output project ID. + * + * @return See above. + */ + public long getOutputProjectId() { + return outputProjectId; + } + + + /** + * Sets the output project ID. + * + * @param outputProjectId See above. + */ + public void setOutputProjectId(Long outputProjectId) { + if (outputProjectId != null) { + this.outputProjectId = outputProjectId; + } + } + + + /** + * Returns the output dataset ID. + * + * @return See above. + */ + public long getOutputDatasetId() { + return outputDatasetId; + } + + + /** + * Sets the output dataset ID. + * + * @param outputDatasetId See above. + */ + public void setOutputDatasetId(Long outputDatasetId) { + if (outputDatasetId != null) { + this.outputDatasetId = outputDatasetId; + } + } + + + /** + * Returns whether the ROIs should be saved or not. + * + * @return See above. + */ + public boolean shouldSaveROIs() { + return saveROIs; + } + + + /** + * Sets whether the ROIs should be saved or not. + * + * @param saveROIs See above. + */ + public void setSaveROIs(boolean saveROIs) { + this.saveROIs = saveROIs; + } + + + /** + * Returns whether the results tables should be saved or not. + * + * @return See above. + */ + public boolean shouldSaveResults() { + return saveResults; + } + + + /** + * Sets whether the results tables should be saved or not. + * + * @param saveResults See above. + */ + public void setSaveResults(boolean saveResults) { + this.saveResults = saveResults; + } + + + /** + * Returns whether the images should be saved or not. + * + * @return See above. + */ + public boolean shouldSaveImages() { + return saveImages; + } + + + /** + * Sets whether the images should be saved or not. + * + * @param saveImages See above. + */ + public void setSaveImages(boolean saveImages) { + this.saveImages = saveImages; + } + + + /** + * Returns whether the ROIs should be loaded or not. + * + * @return See above. + */ + public ROIMode getROIMode() { + return roiMode; + } + + + /** + * Sets whether the ROIs should be loaded or not. + * + * @param roiMode See above. + */ + public void setROIMode(ROIMode roiMode) { + this.roiMode = roiMode; + } + + + /** + * Returns whether the ROIs should be cleared or not. + * + * @return See above. + */ + public boolean shouldClearROIs() { + return clearROIs; + } + + + /** + * Sets whether the ROIs should be cleared or not. + * + * @param clearROIs See above. + */ + public void setClearROIS(boolean clearROIs) { + this.clearROIs = clearROIs; + } + + + /** + * Returns the output directory. + * + * @return See above. + */ + public String getDirectoryOut() { + return directoryOut; + } + + + /** + * Sets the output directory. + * + * @param directoryOut See above. + */ + public void setDirectoryOut(String directoryOut) { + this.directoryOut = directoryOut; + } + + + /** + * Returns the suffix to append to the output files. + * + * @return See above. + */ + public String getSuffix() { + return suffix; + } + + + /** + * Sets the suffix to append to the output files. + * + * @param suffix See above. + */ + public void setSuffix(String suffix) { + this.suffix = suffix; + } + + + /** + * Returns whether the output will be on OMERO or not. + * + * @return See above. + */ + public boolean isOutputOnOMERO() { + return outputOnOMERO; + } + + + /** + * Sets whether the output will be on OMERO or not. + * + * @param outputOnOMERO See above. + */ + public void setOutputOnOMERO(boolean outputOnOMERO) { + this.outputOnOMERO = outputOnOMERO; + } + + + /** + * Returns whether the output will be saved locally or not. + * + * @return See above. + */ + public boolean isOutputOnLocal() { + return outputOnLocal; + } + + + /** + * Sets whether the output will be saved locally or not. + * + * @param outputOnLocal See above. + */ + public void setOutputOnLocal(boolean outputOnLocal) { + this.outputOnLocal = outputOnLocal; + } + + + /** + * Returns whether the log should be saved or not. + * + * @return See above. + */ + public boolean shouldSaveLog() { + return this.saveLog; + } + + + /** + * Sets whether the log should be saved or not. + * + * @param saveLog See above. + */ + public void setSaveLog(boolean saveLog) { + this.saveLog = saveLog; + } + +} diff --git a/src/main/java/fr/igred/ij/macro/OMEROBatchRunner.java b/src/main/java/fr/igred/ij/macro/OMEROBatchRunner.java index ebf2f77..19c1ad6 100644 --- a/src/main/java/fr/igred/ij/macro/OMEROBatchRunner.java +++ b/src/main/java/fr/igred/ij/macro/OMEROBatchRunner.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 MICA & GReD + * Copyright (C) 2021-2023 MICA & GReD * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software @@ -16,7 +16,11 @@ */ package fr.igred.ij.macro; + import fr.igred.ij.gui.ProgressDialog; +import fr.igred.ij.io.BatchImage; +import fr.igred.ij.io.ROIMode; +import fr.igred.omero.AnnotatableWrapper; import fr.igred.omero.Client; import fr.igred.omero.annotations.TableWrapper; import fr.igred.omero.exception.AccessException; @@ -35,31 +39,26 @@ import ij.measure.ResultsTable; import ij.plugin.frame.RoiManager; import ij.text.TextWindow; -import loci.formats.FileStitcher; -import loci.formats.FormatException; -import loci.plugins.BF; -import loci.plugins.in.ImportProcess; -import loci.plugins.in.ImporterOptions; import java.awt.Component; import java.awt.Frame; import java.io.BufferedOutputStream; import java.io.DataOutputStream; import java.io.File; -import java.io.FileOutputStream; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.lang.invoke.MethodHandles; import java.nio.file.Files; +import java.nio.file.Paths; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.logging.Logger; @@ -68,58 +67,76 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; +import static java.nio.file.Files.newOutputStream; + + /** * Runs a script over multiple images retrieved from local files or from OMERO. */ public class OMEROBatchRunner extends Thread { + /** The logger. */ private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass().getName()); - private static final File[] EMPTY_FILE_ARRAY = new File[0]; + /** The empty int array. */ private static final int[] EMPTY_INT_ARRAY = new int[0]; + /** The pattern to remove the file extension from an image title. */ private static final Pattern TITLE_AFTER_EXT = Pattern.compile("\\w+\\s?\\[?([^\\[\\]]*)]?"); + /** The images. */ + private final List images; + /** The script. */ private final ScriptRunner script; + /** The OMERO client. */ private final Client client; + /** The progress monitor. */ private final ProgressMonitor progress; + /** The parameters. */ + private final BatchParameters params; + /** The tables. */ private final Map tables = new HashMap<>(5); - private boolean inputOnOMERO; - private boolean saveImage; - private boolean saveROIs; - private boolean saveResults; - private boolean saveLog; - private boolean loadROIs; - private boolean clearROIs; - private boolean outputOnOMERO; - private boolean outputOnLocal; - private boolean recursive; - private long inputDatasetId; - private long outputDatasetId; - private long outputProjectId; - private String directoryIn; - private String directoryOut; - private String suffix; - + /** The ROI manager. */ private RoiManager rm; + /** The listener. */ private BatchListener listener; - public OMEROBatchRunner(ScriptRunner script, Client client) { - this(script, client, new ProgressLog(LOGGER)); + /** + * Creates a new instance with the specified script, images and parameters. + * + * @param script The script. + * @param images The images. + * @param params The parameters. + * @param client The OMERO client. + */ + public OMEROBatchRunner(ScriptRunner script, List images, BatchParameters params, Client client) { + this(script, images, params, client, new ProgressLog(LOGGER)); } - public OMEROBatchRunner(ScriptRunner script, Client client, ProgressMonitor progress) { + /** + * Creates a new instance with the specified script, images, parameters and progress monitor. + * + * @param script The script. + * @param images The images. + * @param params The parameters. + * @param client The OMERO client. + * @param progress The progress monitor. + */ + public OMEROBatchRunner(ScriptRunner script, + List images, + BatchParameters params, + Client client, + ProgressMonitor progress) { this.script = script; + this.images = new ArrayList<>(images); + this.params = new BatchParameters(params); this.client = client; this.progress = progress; - this.directoryIn = ""; - this.suffix = ""; - this.directoryOut = null; this.rm = null; this.listener = null; } @@ -142,6 +159,7 @@ private static String timestamp() { * * @return The title, without the extension. */ + @SuppressWarnings("MagicCharacter") private static String removeExtension(String title) { if (title != null) { int index = title.lastIndexOf('.'); @@ -150,7 +168,7 @@ private static String removeExtension(String title) { } else { String afterExt = TITLE_AFTER_EXT.matcher(title.substring(index + 1)).replaceAll("$1"); String beforeExt = title.substring(0, index); - if(beforeExt.toLowerCase().endsWith(".ome") && beforeExt.lastIndexOf('.') > 0) { + if (beforeExt.toLowerCase().endsWith(".ome") && beforeExt.lastIndexOf('.') > 0) { beforeExt = beforeExt.substring(0, beforeExt.lastIndexOf('.')); } return afterExt.isEmpty() ? beforeExt : beforeExt + "_" + afterExt; @@ -186,62 +204,6 @@ private static boolean deleteTemp(String tmpDir) { } - /** - * List all files contained in a directory - * - * @param directory The folder to process - * - * @return The list of images paths. - */ - private static List getFilesFromDirectory(String directory, boolean recursive) { - File dir = new File(directory); - File[] files = dir.listFiles(); - if (files == null) files = EMPTY_FILE_ARRAY; - List paths = new ArrayList<>(files.length); - for (File file : files) { - String path = file.getAbsolutePath(); - if (!file.isDirectory()) { - paths.add(path); - } else if (recursive) { - paths.addAll(getFilesFromDirectory(path, true)); - } - } - return paths; - } - - - /** - * Retrieves the images in a list of files using Bio-Formats. - * - * @param files The list of files. - * @param options The Bio-Formats importer options. - * - * @return A map containing the number of images for each file. - */ - private static Map getImagesFromFiles(Collection files, ImporterOptions options) { - List used = new ArrayList<>(files.size()); - Map imageFiles = new LinkedHashMap<>(files.size()); - for (String file : files) { - if (!used.contains(file)) { - // Open the image - options.setId(file); - ImportProcess process = new ImportProcess(options); - try { - process.execute(); - int n = process.getSeriesCount(); - FileStitcher fs = process.getFileStitcher(); - if (fs != null) used = Arrays.asList(fs.getUsedFiles()); - else used.add(file); - imageFiles.put(file, n); - } catch (IOException | FormatException e) { - LOGGER.info(e.getMessage()); - } - } - } - return imageFiles; - } - - /** * Retrieves the list of images open after the script was run. * @@ -283,7 +245,9 @@ private static List getOverlay(ImagePlus imp) { if (overlay != null) { ijRois = new ArrayList<>(Arrays.asList(overlay.toArray())); } - for (Roi roi : ijRois) roi.setImage(imp); + for (Roi roi : ijRois) { + roi.setImage(imp); + } return ijRois; } @@ -314,7 +278,7 @@ private static List getROIsFromOverlay(ImagePlus imp, String propert * @param path The path to the file. */ private static void saveRoiFile(List ijRois, String path) { - try (ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(path))); + try (ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(newOutputStream(Paths.get(path)))); DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(zos))) { RoiEncoder re = new RoiEncoder(dos); for (int i = 0; i < ijRois.size(); i++) { @@ -333,36 +297,48 @@ private static void saveRoiFile(List ijRois, String path) { /** - * Initializes the ROI manager. + * Adds a timestamp to the table name. + * + * @param table The table. + * @param name The table name. */ - private void initRoiManager() { - rm = RoiManager.getInstance2(); - if (rm == null) rm = RoiManager.getRoiManager(); - rm.setVisible(false); + private static String renameTable(TableWrapper table, String name) { + String newName; + if (name == null || name.isEmpty()) { + newName = timestamp() + "_" + table.getName(); + } else { + newName = timestamp() + "_" + name; + } + table.setName(newName); + return newName; } /** - * Initializes the Bio-Formats importer options. - * - * @return See above. + * Saves a table as a text file. * - * @throws IOException If the importer options could not be initialized. + * @param table The table. + * @param path The path to the file. */ - private ImporterOptions initImporterOptions() throws IOException { - ImporterOptions options = new ImporterOptions(); - options.setStackFormat(ImporterOptions.VIEW_HYPERSTACK); - options.setSwapDimensions(false); - options.setOpenAllSeries(false); - options.setSpecifyRanges(false); - options.setShowMetadata(false); - options.setShowOMEXML(false); - options.setShowROIs(loadROIs); - options.setCrop(false); - options.setSplitChannels(false); - options.setSplitFocalPlanes(false); - options.setSplitTimepoints(false); - return options; + private static void saveTable(TableWrapper table, String path) { + try { + //noinspection MagicCharacter + table.saveAs(path, '\t'); + } catch (FileNotFoundException | UnsupportedEncodingException e) { + IJ.error("Could not save table as file: " + e.getMessage()); + } + } + + + /** + * Initializes the ROI manager. + */ + private void initRoiManager() { + rm = RoiManager.getInstance2(); + if (rm == null) { + rm = RoiManager.getRoiManager(); + } + rm.setVisible(false); } @@ -372,7 +348,9 @@ private ImporterOptions initImporterOptions() throws IOException { * @param text The text for the current state. */ private void setState(String text) { - if (progress != null) progress.setState(text); + if (progress != null) { + progress.setState(text); + } } @@ -382,7 +360,9 @@ private void setState(String text) { * @param text The text for the current progress. */ private void setProgress(String text) { - if (progress != null) progress.setProgress(text); + if (progress != null) { + progress.setProgress(text); + } } @@ -390,15 +370,15 @@ private void setProgress(String text) { * Signals the process is done. */ private void setDone() { - if (progress != null) progress.setDone(); + if (progress != null) { + progress.setDone(); + } } /** - * If this thread was constructed using a separate - * {@code Runnable} run object, then that - * {@code Runnable} object's {@code run} method is called; - * otherwise, this method does nothing and returns. + * If this thread was constructed using a separate {@code Runnable} run object, then that {@code Runnable} object's + * {@code run} method is called; otherwise, this method does nothing and returns. *

* Subclasses of {@code Thread} should override this method. * @@ -407,57 +387,51 @@ private void setDone() { */ @Override public void run() { - boolean finished = false; + boolean running = true; if (progress instanceof ProgressDialog) { ((Component) progress).setVisible(true); } try { - if (!outputOnLocal) { + if (!params.isOutputOnLocal()) { setState("Temporary directory creation..."); - directoryOut = Files.createTempDirectory("Fiji_analysis").toString(); + params.setDirectoryOut(Files.createTempDirectory("Fiji_analysis").toString()); } - if (inputOnOMERO) { - setState("Retrieving images from OMERO..."); - DatasetWrapper dataset = client.getDataset(inputDatasetId); - List images = dataset.getImages(client); - setState("Macro running..."); - runMacro(images); - } else { - setState("Retrieving files from input folder..."); - List files = getFilesFromDirectory(directoryIn, recursive); - setState("Macro running..."); - runMacroOnLocalImages(files); - } + setState("Macro running..."); + runMacro(); setProgress(""); uploadTables(); - if (!outputOnLocal) { + if (!params.isOutputOnLocal()) { setState("Temporary directory deletion..."); - if (!deleteTemp(directoryOut)) { + if (!deleteTemp(params.getDirectoryOut())) { LOGGER.warning("Temp directory may not be deleted."); } } - finished = true; + running = false; setState(""); setDone(); - } catch (NoSuchElementException | IOException | ServiceException | AccessException | ExecutionException e) { - finished = true; + } catch (IOException e) { + running = false; setDone(); setProgress("Macro cancelled"); - if (e.getMessage() != null && "Macro cancelled".equals(e.getMessage())) { + if ("Macro cancelled".equals(e.getMessage())) { IJ.run("Close"); } IJ.error(e.getMessage()); } finally { - if (!finished) { + if (running) { setDone(); setProgress("An unexpected error occurred."); } - if (listener != null) listener.onThreadFinished(); - rm.setVisible(true); - rm.close(); + if (listener != null) { + listener.onThreadFinished(); + } + if (rm != null) { + rm.setVisible(true); + rm.close(); + } } } @@ -471,11 +445,8 @@ private void deleteROIs(ImageWrapper image) { setState("ROIs deletion from OMERO"); try { List rois = image.getROIs(client); - for (ROIWrapper roi : rois) { - if (roi.getOwner().getId() == client.getId()) { - client.delete(roi); - } - } + rois.removeIf(roi -> roi.getOwner().getId() != client.getId()); + client.delete(rois); } catch (ExecutionException | OMEROServerError | ServiceException | AccessException exception) { LOGGER.warning(exception.getMessage()); } catch (InterruptedException e) { @@ -494,7 +465,9 @@ private void deleteROIs(ImageWrapper image) { */ private List getManagedRois(ImagePlus imp) { List ijRois = new ArrayList<>(Arrays.asList(rm.getRoisAsArray())); - for (Roi roi : ijRois) roi.setImage(imp); + for (Roi roi : ijRois) { + roi.setImage(imp); + } return ijRois; } @@ -519,31 +492,29 @@ private List getROIsFromManager(ImagePlus imp, String property) { /** - * Runs a macro on images from OMERO and saves the results. - * - * @param images List of images on OMERO. + * Runs a macro on images and saves the results. */ - void runMacro(List images) { + private void runMacro() { String property = ROIWrapper.IJ_PROPERTY; WindowManager.closeAllWindows(); + int index = 0; - for (ImageWrapper image : images) { - setProgress("Image " + (index + 1) + "/" + images.size()); - long inputImageId = image.getId(); + for (BatchImage image : images) { + // Initialize ROI Manager + initRoiManager(); - // Open image from OMERO - ImagePlus imp = openImage(image); + //noinspection HardcodedFileSeparator + setProgress("Image " + (index + 1) + "/" + images.size()); + setState("Opening image..."); + ImagePlus imp = image.getImagePlus(params.getROIMode()); // If image could not be loaded, continue to next image. if (imp != null) { - // Initialize ROI Manager - initRoiManager(); - - // Load ROIs - if (loadROIs) loadROIs(image, imp, false); - + ImageWrapper imageWrapper = image.getImageWrapper(); + Long inputImageId = imageWrapper != null ? imageWrapper.getId() : null; imp.show(); - // Analyse the image + // Process the image + setState("Processing image..."); script.setImage(imp); script.run(); @@ -556,89 +527,21 @@ void runMacro(List images) { } - /** - * Runs a macro on local files and saves the results. - * - * @param files List of image files. - * - * @throws IOException A problem occurred reading a file. - */ - void runMacroOnLocalImages(Collection files) throws IOException { - String property = ROIWrapper.IJ_PROPERTY; - WindowManager.closeAllWindows(); - - ImporterOptions options = initImporterOptions(); - Map imageFiles = getImagesFromFiles(files, options); - int nFile = 1; - for (Map.Entry entry : imageFiles.entrySet()) { - int n = entry.getValue(); - options.setId(entry.getKey()); - for (int i = 0; i < n; i++) { - String msg = String.format("File %d/%d, image %d/%d", nFile, imageFiles.size(), i + 1, n); - setProgress(msg); - options.setSeriesOn(i, true); - try { - ImagePlus[] imps = BF.openImagePlus(options); - ImagePlus imp = imps[0]; - imp.show(); - - // Initialize ROI Manager - initRoiManager(); - - // Analyse the image - script.setImage(imp); - script.run(); - - // Save and Close the various components - imp.changes = false; // Prevent "Save Changes?" dialog - save(imp, null, property); - } catch (FormatException e) { - IJ.error(e.getMessage()); - } - closeWindows(); - options.setSeriesOn(i, false); - } - nFile++; - } - } - - - /** - * Opens an image from OMERO. - * - * @param image An OMERO image. - * - * @return An ImagePlus. - */ - private ImagePlus openImage(ImageWrapper image) { - setState("Opening image from OMERO..."); - ImagePlus imp = null; - try { - imp = image.toImagePlus(client); - // Store image "annotate" permissions as a property in the ImagePlus object - imp.setProp("Annotatable", String.valueOf(image.canAnnotate())); - } catch (ExecutionException | ServiceException | AccessException e) { - IJ.error("Could not load image: " + e.getMessage()); - } - return imp; - } - - /** * Loads ROIs from an image in OMERO into ImageJ. * - * @param image The OMERO image. - * @param imp The image in ImageJ ROIs should be linked to. - * @param toOverlay Whether the ROIs should be loaded to the ROI Manager (false) or the overlay (true). + * @param image The OMERO image. + * @param imp The image in ImageJ ROIs should be linked to. + * @param roiMode The mode used to load ROIs. */ - private void loadROIs(ImageWrapper image, ImagePlus imp, boolean toOverlay) { + private void loadROIs(ImageWrapper image, ImagePlus imp, ROIMode roiMode) { List ijRois = new ArrayList<>(0); try { ijRois = ROIWrapper.toImageJ(image.getROIs(client)); } catch (ExecutionException | ServiceException | AccessException e) { IJ.error("Could not load ROIs: " + e.getMessage()); } - if (toOverlay) { + if (roiMode == ROIMode.OVERLAY) { Overlay overlay = imp.getOverlay(); if (overlay != null) { overlay.clear(); @@ -649,7 +552,7 @@ private void loadROIs(ImageWrapper image, ImagePlus imp, boolean toOverlay) { ijRoi.setImage(imp); overlay.add(ijRoi, ijRoi.getName()); } - } else { + } else if (roiMode == ROIMode.MANAGER) { rm.reset(); // Reset ROI manager to clear previous ROIs for (Roi ijRoi : ijRois) { ijRoi.setImage(imp); @@ -672,31 +575,33 @@ private void save(ImagePlus inputImage, Long omeroInputId, String property) { Long omeroOutputId = omeroInputId; List outputs = getOutputImages(inputImage); - ImagePlus outputImage = inputImage; - if (!outputs.isEmpty()) outputImage = outputs.get(0); + ImagePlus outputImage = outputs.isEmpty() ? inputImage : outputs.get(0); - // If input image is expected as output for ROIs on OMERO but is not annotable, import it. - boolean annotable = Boolean.parseBoolean(inputImage.getProp("Annotable")); + boolean annotatable = Boolean.parseBoolean(inputImage.getProp("Annotatable")); boolean outputIsNotInput = !inputImage.equals(outputImage); - if (!outputOnOMERO || !saveROIs || annotable || outputIsNotInput) { + if (!params.isOutputOnOMERO() || !params.shouldSaveROIs() || annotatable || outputIsNotInput) { outputs.removeIf(inputImage::equals); } - if (saveImage) { - if (outputs.isEmpty()) LOGGER.info("Warning: there is no new image."); - List outputIds = new ArrayList<>(outputs.size()); - outputs.forEach(imp -> outputIds.addAll(saveImage(imp, property))); + if (params.shouldSaveImages()) { + List outputIds = saveImages(outputs, property); if (!outputIds.isEmpty() && outputIsNotInput) { omeroOutputId = outputIds.get(0); } } - if (saveROIs) { - if (!saveImage) saveOverlay(outputImage, omeroOutputId, inputTitle, property); + if (params.shouldSaveROIs()) { + if (!params.shouldSaveImages()) { + saveOverlay(outputImage, omeroOutputId, inputTitle, property); + } saveROIManager(outputImage, omeroOutputId, inputTitle, property); } - if (saveResults) saveResults(outputImage, omeroOutputId, inputTitle, property); - if (saveLog) saveLog(omeroOutputId, inputTitle); + if (params.shouldSaveResults()) { + saveResults(outputImage, omeroOutputId, inputTitle, property); + } + if (params.shouldSaveLog()) { + saveLog(omeroOutputId, inputTitle); + } for (ImagePlus imp : outputs) { imp.changes = false; @@ -705,6 +610,24 @@ private void save(ImagePlus inputImage, Long omeroInputId, String property) { } + /** + * Saves images. + * + * @param outputs The images to save. + * @param property The ROI property used to group shapes in OMERO. + * + * @return The OMERO IDs of the (possibly) uploaded images. + */ + private List saveImages(Collection outputs, String property) { + if (outputs.isEmpty()) { + LOGGER.info("Warning: there is no new image."); + } + List outputIds = new ArrayList<>(outputs.size()); + outputs.forEach(imp -> outputIds.addAll(saveImage(imp, property))); + return outputIds; + } + + /** * Saves an image. * @@ -716,14 +639,15 @@ private void save(ImagePlus inputImage, Long omeroInputId, String property) { private List saveImage(ImagePlus image, String property) { List ids = new ArrayList<>(0); String title = removeExtension(image.getTitle()); - String path = directoryOut + File.separator + title + suffix + ".tif"; + String path = params.getDirectoryOut() + File.separator + + title + params.getSuffix() + ".tif"; IJ.saveAsTiff(image, path); - if (outputOnOMERO) { + if (params.isOutputOnOMERO()) { try { setState("Import on OMERO..."); - DatasetWrapper dataset = client.getDataset(outputDatasetId); + DatasetWrapper dataset = client.getDataset(params.getOutputDatasetId()); ids = dataset.importImage(client, path); - if (saveROIs && !ids.isEmpty()) { + if (params.shouldSaveROIs() && !ids.isEmpty()) { saveOverlay(image, ids.get(0), title, property); } } catch (AccessException | ServiceException | OMEROServerError | ExecutionException e) { @@ -743,22 +667,24 @@ private List saveImage(ImagePlus image, String property) { * @param property The ROI property used to group shapes on OMERO. */ private void saveOverlay(ImagePlus imp, Long imageId, String title, String property) { - if (outputOnLocal) { // local save + if (params.isOutputOnLocal()) { // local save setState("Saving overlay ROIs..."); - String path = directoryOut + File.separator + title + "_" + timestamp() + "_RoiSet.zip"; + String timestamp = params.shouldClearROIs() ? "" : timestamp() + "_"; + String path = params.getDirectoryOut() + File.separator + + title + "_" + timestamp + "RoiSet.zip"; List ijRois = getOverlay(imp); saveRoiFile(ijRois, path); } - if (outputOnOMERO && imageId != null) { // save on Omero + if (params.isOutputOnOMERO() && imageId != null) { // save on Omero List rois = getROIsFromOverlay(imp, property); try { ImageWrapper image = client.getImage(imageId); - if (clearROIs) { + if (params.shouldClearROIs()) { deleteROIs(image); } setState("Saving overlay ROIs on OMERO..."); image.saveROIs(client, rois); - loadROIs(image, imp, true); // reload ROIs + loadROIs(image, imp, ROIMode.OVERLAY); // reload ROIs } catch (ServiceException | AccessException | ExecutionException e) { IJ.error("Could not import overlay ROIs to OMERO: " + e.getMessage()); } @@ -775,22 +701,24 @@ private void saveOverlay(ImagePlus imp, Long imageId, String title, String prope * @param property The ROI property used to group shapes on OMERO. */ private void saveROIManager(ImagePlus imp, Long imageId, String title, String property) { - if (outputOnLocal) { // local save + if (params.isOutputOnLocal()) { // local save setState("Saving ROIs..."); - String path = directoryOut + File.separator + title + "_" + timestamp() + "_RoiSet.zip"; + String timestamp = params.shouldClearROIs() ? "" : timestamp() + "_"; + String path = params.getDirectoryOut() + File.separator + + title + "_" + timestamp + "RoiSet.zip"; List ijRois = getManagedRois(imp); saveRoiFile(ijRois, path); } - if (outputOnOMERO && imageId != null) { // save on Omero + if (params.isOutputOnOMERO() && imageId != null) { // save on Omero List rois = getROIsFromManager(imp, property); try { ImageWrapper image = client.getImage(imageId); - if (clearROIs) { + if (params.shouldClearROIs()) { deleteROIs(image); } setState("Saving ROIs on OMERO..."); image.saveROIs(client, rois); - loadROIs(image, imp, false); // reload ROIs + loadROIs(image, imp, ROIMode.MANAGER); // reload ROIs } catch (ServiceException | AccessException | ExecutionException e) { IJ.error("Could not import ROIs to OMERO: " + e.getMessage()); } @@ -821,12 +749,14 @@ private void saveResults(ImagePlus imp, Long imageId, String title, String prope if (rt != null) { String name = rt.getTitle(); if (!Boolean.TRUE.equals(processed.get(name)) && rt.getHeadings().length > 0) { - String path = directoryOut + File.separator + name + "_" + title + "_" + timestamp() + ".csv"; + String path = params.getDirectoryOut() + + File.separator + + name + "_" + + title + "_" + + timestamp() + ".csv"; rt.save(path); - if (outputOnOMERO) { - appendTable(rt, imageId, ijRois, property); - uploadFile(imageId, path); - } + appendTable(rt, imageId, ijRois, property); + uploadFileToImage(imageId, path); rt.reset(); processed.put(name, true); } @@ -842,10 +772,10 @@ private void saveResults(ImagePlus imp, Long imageId, String title, String prope * @param title The image title used to name the file when saving locally. */ private void saveLog(Long imageId, String title) { - String path = directoryOut + File.separator + title + "_log.txt"; + String path = params.getDirectoryOut() + File.separator + title + "_log.txt"; IJ.selectWindow("Log"); IJ.saveAs("txt", path); - if (outputOnOMERO) uploadFile(imageId, path); + uploadFileToImage(imageId, path); } @@ -855,16 +785,34 @@ private void saveLog(Long imageId, String title) { * @param imageId The image ID on OMERO. * @param path The path to the file. */ - private void uploadFile(Long imageId, String path) { - if (imageId != null) { + private void uploadFileToImage(Long imageId, String path) { + if (imageId != null && params.isOutputOnOMERO()) { + ImageWrapper image = null; try { setState("Uploading results files..."); - ImageWrapper image = client.getImage(imageId); - image.addFile(client, new File(path)); + image = client.getImage(imageId); } catch (ExecutionException | ServiceException | AccessException e) { - IJ.error("Error adding file to image:" + e.getMessage()); + IJ.error("Error retrieving image:" + e.getMessage()); + } + uploadFile(image, path); + } + } + + + /** + * Uploads a file to an annotatable object on OMERO. + * + * @param object The object on OMERO. + * @param path The path to the file. + */ + private void uploadFile(AnnotatableWrapper object, String path) { + if (object != null && params.isOutputOnOMERO()) { + try { + object.addFile(client, new File(path)); + } catch (ExecutionException e) { + IJ.error("Error adding file to object:" + e.getMessage()); } catch (InterruptedException e) { - IJ.error("Error adding file to image:" + e.getMessage()); + IJ.error("Error adding file to object:" + e.getMessage()); Thread.currentThread().interrupt(); } } @@ -879,7 +827,7 @@ private void uploadFile(Long imageId, String path) { * @param ijRois The ROIs in ImageJ. * @param property The ROI property used to group shapes on OMERO. */ - private void appendTable(ResultsTable results, Long imageId, List ijRois, String property) { + private void appendTable(ResultsTable results, Long imageId, List ijRois, String property) { String resultsName = results.getTitle(); TableWrapper table = tables.get(resultsName); try { @@ -894,33 +842,45 @@ private void appendTable(ResultsTable results, Long imageId, List ijRois, S } + /** + * Uploads a table to a project, if required. + * + * @param project The project the table belongs to. + * @param table The table. + */ + private void uploadTable(ProjectWrapper project, TableWrapper table) { + if (project != null && params.isOutputOnOMERO()) { + try { + project.addTable(client, table); + } catch (ExecutionException | ServiceException | AccessException e) { + IJ.error("Could not upload table: " + e.getMessage()); + } + } + } + + /** * Upload the tables to OMERO. */ private void uploadTables() { - if (outputOnOMERO && saveResults) { + ProjectWrapper project = null; + if (params.shouldSaveResults()) { setState("Uploading tables..."); - try { - ProjectWrapper project = client.getProject(outputProjectId); - for (Map.Entry entry : tables.entrySet()) { - String name = entry.getKey(); - TableWrapper table = entry.getValue(); - String newName; - if (name == null || name.isEmpty()) newName = timestamp() + "_" + table.getName(); - else newName = timestamp() + "_" + name; - table.setName(newName); - project.addTable(client, table); - String path = directoryOut + File.separator + newName + ".csv"; - table.saveAs(path, 'c'); - project.addFile(client, new File(path)); + if (params.isOutputOnOMERO()) { + try { + project = client.getProject(params.getOutputProjectId()); + } catch (ExecutionException | ServiceException | AccessException e) { + IJ.error("Could not retrieve project: " + e.getMessage()); } - } catch (ExecutionException | ServiceException | AccessException e) { - IJ.error("Could not save table: " + e.getMessage()); - } catch (IOException e) { - IJ.error("Could not save table as file: " + e.getMessage()); - } catch (InterruptedException e) { - IJ.error("Could not upload CSV to project: " + e.getMessage()); - Thread.currentThread().interrupt(); + } + for (Map.Entry entry : tables.entrySet()) { + String name = entry.getKey(); + TableWrapper table = entry.getValue(); + String newName = renameTable(table, name); + uploadTable(project, table); + String path = params.getDirectoryOut() + File.separator + newName + ".csv"; + saveTable(table, path); + uploadFile(project, path); } } } @@ -940,163 +900,23 @@ private void closeWindows() { } + /** + * Returns the client. + * + * @return See above. + */ public Client getClient() { return client; } - public long getOutputProjectId() { - return outputProjectId; - } - - - public void setOutputProjectId(Long outputProjectId) { - if (outputProjectId != null) this.outputProjectId = outputProjectId; - } - - - public long getOutputDatasetId() { - return outputDatasetId; - } - - - public void setOutputDatasetId(Long outputDatasetId) { - if (outputDatasetId != null) this.outputDatasetId = outputDatasetId; - } - - - public long getInputDatasetId() { - return inputDatasetId; - } - - - public void setInputDatasetId(Long inputDatasetId) { - if (inputDatasetId != null) this.inputDatasetId = inputDatasetId; - } - - - public boolean shouldSaveROIs() { - return saveROIs; - } - - - public void setSaveROIs(boolean saveROIs) { - this.saveROIs = saveROIs; - } - - - public boolean shouldSaveResults() { - return saveResults; - } - - - public void setSaveResults(boolean saveResults) { - this.saveResults = saveResults; - } - - - public boolean isInputOnOMERO() { - return inputOnOMERO; - } - - - public void setInputOnOMERO(boolean inputOnOMERO) { - this.inputOnOMERO = inputOnOMERO; - } - - - public boolean shouldSaveImage() { - return saveImage; - } - - - public void setSaveImage(boolean saveImage) { - this.saveImage = saveImage; - } - - - public boolean shouldLoadROIs() { - return loadROIs; - } - - - public void setLoadROIS(boolean loadROIs) { - this.loadROIs = loadROIs; - } - - - public boolean shouldClearROIs() { - return clearROIs; - } - - - public void setClearROIS(boolean clearROIs) { - this.clearROIs = clearROIs; - } - - - public String getDirectoryIn() { - return directoryIn; - } - - - public void setDirectoryIn(String directoryIn) { - this.directoryIn = directoryIn; - } - - - public String getDirectoryOut() { - return directoryOut; - } - - - public void setDirectoryOut(String directoryOut) { - this.directoryOut = directoryOut; - } - - - public String getSuffix() { - return suffix; - } - - - public void setSuffix(String suffix) { - this.suffix = suffix; - } - - - public boolean isOutputOnOMERO() { - return outputOnOMERO; - } - - - public void setOutputOnOMERO(boolean outputOnOMERO) { - this.outputOnOMERO = outputOnOMERO; - } - - - public boolean isOutputOnLocal() { - return outputOnLocal; - } - - - public void setOutputOnLocal(boolean outputOnLocal) { - this.outputOnLocal = outputOnLocal; - } - - - public void setSaveLog(boolean saveLog) { - this.saveLog = saveLog; - } - - + /** + * Sets the listener. + * + * @param listener The listener. + */ public void setListener(BatchListener listener) { this.listener = listener; } - - public void setRecursive(boolean recursive) { - this.recursive = recursive; - } - } diff --git a/src/main/java/fr/igred/ij/macro/ProgressLog.java b/src/main/java/fr/igred/ij/macro/ProgressLog.java index 5544b86..4424855 100644 --- a/src/main/java/fr/igred/ij/macro/ProgressLog.java +++ b/src/main/java/fr/igred/ij/macro/ProgressLog.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 MICA & GReD + * Copyright (C) 2021-2023 MICA & GReD * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software @@ -16,9 +16,11 @@ */ package fr.igred.ij.macro; + import java.util.logging.Level; import java.util.logging.Logger; + /** * Logs the progress of the batch process. */ diff --git a/src/main/java/fr/igred/ij/macro/ProgressMonitor.java b/src/main/java/fr/igred/ij/macro/ProgressMonitor.java index ce9f5f0..6844c34 100644 --- a/src/main/java/fr/igred/ij/macro/ProgressMonitor.java +++ b/src/main/java/fr/igred/ij/macro/ProgressMonitor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 MICA & GReD + * Copyright (C) 2021-2023 MICA & GReD * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software @@ -16,6 +16,7 @@ */ package fr.igred.ij.macro; + /** * Monitors the batch process progress. */ @@ -28,6 +29,7 @@ public interface ProgressMonitor { */ void setProgress(String text); + /** * Sets the current state. * @@ -35,6 +37,7 @@ public interface ProgressMonitor { */ void setState(String text); + /** * Signals the process is done. */ diff --git a/src/main/java/fr/igred/ij/macro/ScriptRunner.java b/src/main/java/fr/igred/ij/macro/ScriptRunner.java index b993002..18aff0f 100644 --- a/src/main/java/fr/igred/ij/macro/ScriptRunner.java +++ b/src/main/java/fr/igred/ij/macro/ScriptRunner.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 MICA & GReD + * Copyright (C) 2021-2023 MICA & GReD * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software @@ -16,16 +16,20 @@ */ package fr.igred.ij.macro; + import ij.IJ; import ij.ImagePlus; import ij.gui.GenericDialog; + /** * Runs an ImageJ macro. */ public class ScriptRunner { + /** The path to the macro. */ private final String path; + /** The arguments for the macro. */ private String arguments = ""; @@ -69,8 +73,11 @@ private static boolean isSciJavaLoaded() { * @return A new ScriptRunner object. */ public static ScriptRunner createScriptRunner(String path) { - if (isSciJavaLoaded()) return new ScriptRunner2(path); - else return new ScriptRunner(path); + if (isSciJavaLoaded()) { + return new ScriptRunner2(path); + } else { + return new ScriptRunner(path); + } } @@ -109,6 +116,7 @@ public void setArguments(String arguments) { * * @return See above. */ + @SuppressWarnings("MagicCharacter") public String getLanguage() { return path.substring(path.lastIndexOf('.')); } diff --git a/src/main/java/fr/igred/ij/macro/ScriptRunner2.java b/src/main/java/fr/igred/ij/macro/ScriptRunner2.java index 75b428f..d232f37 100644 --- a/src/main/java/fr/igred/ij/macro/ScriptRunner2.java +++ b/src/main/java/fr/igred/ij/macro/ScriptRunner2.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 MICA & GReD + * Copyright (C) 2021-2023 MICA & GReD * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software @@ -16,6 +16,7 @@ */ package fr.igred.ij.macro; + import ij.IJ; import ij.ImagePlus; import ij.gui.Overlay; @@ -40,14 +41,19 @@ import java.util.Map; import java.util.stream.Collectors; + /** * Runs an ImageJ2 script. */ public class ScriptRunner2 extends ScriptRunner { + /** Whether inputs were detected. */ private final boolean detectedInputs; + /** The inputs. */ protected Map inputs; + /** The script. */ private ScriptModule script; + /** The script language. */ private String language = ""; @@ -265,10 +271,12 @@ private void addInputs() { ModuleItem newInput = new DefaultMutableModuleItem<>(scriptInfo, input.getKey(), Long.class); scriptInfo.registerInput(newInput); } else if (value.getClass().equals(Double.class)) { - ModuleItem newInput = new DefaultMutableModuleItem<>(scriptInfo, input.getKey(), Double.class); + ModuleItem newInput = new DefaultMutableModuleItem<>(scriptInfo, input.getKey(), + Double.class); scriptInfo.registerInput(newInput); } else { - ModuleItem newInput = new DefaultMutableModuleItem<>(scriptInfo, input.getKey(), String.class); + ModuleItem newInput = new DefaultMutableModuleItem<>(scriptInfo, input.getKey(), + String.class); scriptInfo.registerInput(newInput); } } diff --git a/src/main/java/fr/igred/ij/macro/package-info.java b/src/main/java/fr/igred/ij/macro/package-info.java index 821d0a7..d0189b7 100644 --- a/src/main/java/fr/igred/ij/macro/package-info.java +++ b/src/main/java/fr/igred/ij/macro/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 MICA & GReD + * Copyright (C) 2021-2023 MICA & GReD * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software diff --git a/src/main/java/fr/igred/ij/plugin/frame/OMEROBatchPlugin.java b/src/main/java/fr/igred/ij/plugin/frame/OMEROBatchPlugin.java index e6f8792..ed111c1 100644 --- a/src/main/java/fr/igred/ij/plugin/frame/OMEROBatchPlugin.java +++ b/src/main/java/fr/igred/ij/plugin/frame/OMEROBatchPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 MICA & GReD + * Copyright (C) 2021-2023 MICA & GReD * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software @@ -16,9 +16,13 @@ */ package fr.igred.ij.plugin.frame; + import fr.igred.ij.gui.OMEROConnectDialog; import fr.igred.ij.gui.ProgressDialog; +import fr.igred.ij.io.BatchImage; +import fr.igred.ij.io.ROIMode; import fr.igred.ij.macro.BatchListener; +import fr.igred.ij.macro.BatchParameters; import fr.igred.ij.macro.OMEROBatchRunner; import fr.igred.ij.macro.ScriptRunner; import fr.igred.omero.Client; @@ -58,90 +62,123 @@ import java.util.logging.Logger; import java.util.stream.Collectors; +import static fr.igred.ij.io.LocalBatchImage.listImages; +import static fr.igred.ij.io.OMEROBatchImage.listImages; import static javax.swing.JOptionPane.showMessageDialog; + /** * Main window for the OMERO batch plugin. */ public class OMEROBatchPlugin extends PlugInFrame implements BatchListener { + /** The logger. */ private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass().getName()); + /** The format used to display OMERO objects. */ private static final String FORMAT = "%%-%ds (ID:%%%dd)"; - // minimum window size + /** The minimum window size */ private final Dimension minimumSize = new Dimension(640, 480); // connection management + /** The connection status. */ private final JLabel connectionStatus = new JLabel("Disconnected"); + /** The connection button. */ private final JButton connect = new JButton("Connect"); + /** The disconnection button. */ private final JButton disconnect = new JButton("Disconnect"); // source selection + /** The OMERO input button. */ private final JRadioButton omero = new JRadioButton("OMERO"); + /** The local input button. */ private final JRadioButton local = new JRadioButton("Local"); - // choices of input images - private final JPanel input1a = new JPanel(); - private final JPanel input1b = new JPanel(); - private final JPanel input1c = new JPanel(); - private final JPanel input2 = new JPanel(); - // group and user selection + /** The list of groups. */ private final JComboBox groupList = new JComboBox<>(); + /** The list of users. */ private final JComboBox userList = new JComboBox<>(); // choice of the dataSet + /** The list of projects. */ private final JComboBox projectListIn = new JComboBox<>(); + /** The list of datasets. */ private final JComboBox datasetListIn = new JComboBox<>(); - private final JCheckBox checkDelROIs = new JCheckBox(" Clear ROIs each time "); - private final JCheckBox checkLoadROIs = new JCheckBox(" Load ROIs "); + /** The checkbox to delete ROIs. */ + private final JCheckBox checkDelROIs = new JCheckBox("Clear ROIs each time"); + /** The list of possible output projects. */ + private final JComboBox roiMode = new JComboBox<>(ROIMode.values()); // choice of the record + /** The input folder. */ private final JTextField inputFolder = new JTextField(20); + /** The checkbox to analyse subfolders. */ private final JCheckBox recursive = new JCheckBox("Recursive"); + /** The macro file. */ private final JTextField macro = new JTextField(20); + /** The macro language label. */ private final JLabel labelLanguage = new JLabel(); + /** The macro arguments label. */ private final JLabel labelArguments = new JLabel(); + /** The checkbox to save images. */ private final JCheckBox checkImage = new JCheckBox("New image(s)"); + /** The checkbox to save results. */ private final JCheckBox checkResults = new JCheckBox("Results table(s)"); + /** The checkbox to save ROIs. */ private final JCheckBox checkROIs = new JCheckBox("ROIs"); + /** The checkbox to save the log. */ private final JCheckBox checkLog = new JCheckBox("Log file"); - private final JPanel output2 = new JPanel(); + /** The suffix of the output files. */ private final JTextField suffix = new JTextField(10); // Omero or local => checkbox + /** The checkbox to save to OMERO. */ private final JCheckBox onlineOutput = new JCheckBox("OMERO"); + /** The checkbox to save locally. */ private final JCheckBox localOutput = new JCheckBox("Local"); - // existing dataset - private final JPanel output3a = new JPanel(); + /** The list of possible output projects. */ private final JComboBox projectListOut = new JComboBox<>(); + /** The list of possible output datasets. */ private final JComboBox datasetListOut = new JComboBox<>(); - private final JButton newDatasetBtn = new JButton("New"); - // local - private final JPanel output3b = new JPanel(); + /** The output folder. */ private final JTextField outputFolder = new JTextField(20); - // start button + /** The start button. */ private final JButton start = new JButton("Start"); //variables to keep + /** The OMERO client. */ private transient Client client; + /** The script runner. */ private transient ScriptRunner script; + /** The groups. */ private transient List groups; + /** The group projects. */ private transient List groupProjects; + /** The selected user's projects. */ private transient List userProjects; + /** The datasets. */ private transient List datasets; + /** The current user's projects. */ private transient List myProjects; + /** The current user's datasets. */ private transient List myDatasets; + /** The users. */ private transient List users; + /** The current user. */ private transient ExperimenterWrapper exp; + /** The output directory. */ private String directoryOut = null; + /** The input directory. */ private String directoryIn = null; + /** The output dataset ID. */ private Long outputDatasetId = null; + /** The output project ID. */ private Long outputProjectId = null; @@ -152,8 +189,8 @@ public OMEROBatchPlugin() { super("OMERO Batch Plugin"); super.setMinimumSize(minimumSize); - final String projectName = "Project Name: "; - final String datasetName = "Dataset Name: "; + final String projectName = "Project: "; + final String datasetName = "Dataset: "; final String browse = "Browse"; final Font nameFont = new Font("Arial", Font.ITALIC, 10); @@ -205,8 +242,9 @@ public OMEROBatchPlugin() { source.setBorder(BorderFactory.createTitledBorder("Source")); super.add(source); - JLabel labelGroup = new JLabel("Group Name: "); - JLabel labelUser = new JLabel("User Name: "); + JPanel input1a = new JPanel(); + JLabel labelGroup = new JLabel("Group: "); + JLabel labelUser = new JLabel("User: "); labelGroup.setLabelFor(groupList); labelUser.setLabelFor(userList); input1a.add(labelGroup); @@ -219,6 +257,7 @@ public OMEROBatchPlugin() { groupList.setFont(listFont); userList.setFont(listFont); + JPanel input1b = new JPanel(); JLabel labelProjectIn = new JLabel(projectName); JLabel labelDatasetIn = new JLabel(datasetName); JButton preview = new JButton("Preview"); @@ -237,9 +276,7 @@ public OMEROBatchPlugin() { projectListIn.setFont(listFont); datasetListIn.setFont(listFont); - input1c.add(checkLoadROIs); - input1c.add(checkDelROIs); - + JPanel input2 = new JPanel(); JLabel inputFolderLabel = new JLabel("Images folder: "); JButton inputFolderBtn = new JButton(browse); inputFolderLabel.setLabelFor(inputFolder); @@ -251,11 +288,18 @@ public OMEROBatchPlugin() { input2.add(recursive); inputFolderBtn.addActionListener(e -> chooseDirectory(inputFolder)); + JPanel input3 = new JPanel(); + JLabel labelROIMode = new JLabel("Load ROIs: "); + labelROIMode.setLabelFor(roiMode); + input3.add(labelROIMode); + input3.add(roiMode); + input3.add(checkDelROIs); + JPanel panelInput = new JPanel(); panelInput.add(input1a); panelInput.add(input1b); - panelInput.add(input1c); panelInput.add(input2); + panelInput.add(input3); panelInput.setLayout(new BoxLayout(panelInput, BoxLayout.PAGE_AXIS)); panelInput.setBorder(BorderFactory.createTitledBorder("Input")); super.add(panelInput); @@ -320,29 +364,34 @@ public OMEROBatchPlugin() { onlineOutput.addActionListener(this::updateOutput); localOutput.addActionListener(this::updateOutput); + JPanel output2 = new JPanel(); JLabel labelExtension = new JLabel("Suffix of output files:"); labelExtension.setLabelFor(suffix); suffix.setText("_macro"); output2.add(labelExtension); output2.add(suffix); + JPanel output3a = new JPanel(); + JPanel output3a1 = new JPanel(); + JButton newDatasetBtn = new JButton("New"); JLabel labelProjectOut = new JLabel(projectName); JLabel labelDatasetOut = new JLabel(datasetName); labelProjectOut.setLabelFor(projectListOut); labelDatasetOut.setLabelFor(datasetListOut); output3a.add(labelProjectOut); output3a.add(projectListOut); - output3a.add(Box.createRigidArea(smallHorizontal)); - output3a.add(labelDatasetOut); - output3a.add(datasetListOut); - output3a.add(Box.createRigidArea(smallHorizontal)); - output3a.add(newDatasetBtn); + output3a1.add(labelDatasetOut); + output3a1.add(datasetListOut); + output3a1.add(Box.createRigidArea(smallHorizontal)); + output3a1.add(newDatasetBtn); + output3a.add(output3a1); projectListOut.addItemListener(this::updateOutputProject); datasetListOut.addItemListener(this::updateOutputDataset); newDatasetBtn.addActionListener(this::createNewDataset); projectListOut.setFont(listFont); datasetListOut.setFont(listFont); + JPanel output3b = new JPanel(); JLabel outputFolderLabel = new JLabel("Output folder: "); JButton directoryBtn = new JButton(browse); outputFolderLabel.setLabelFor(outputFolder); @@ -355,8 +404,8 @@ public OMEROBatchPlugin() { // choice of output JPanel panelOutput = new JPanel(); - panelOutput.add(output2); panelOutput.add(output1); + panelOutput.add(output2); panelOutput.add(output3a); panelOutput.add(output3b); panelOutput.setLayout(new BoxLayout(panelOutput, BoxLayout.PAGE_AXIS)); @@ -373,12 +422,12 @@ public OMEROBatchPlugin() { input2.setVisible(false); input1a.setVisible(true); input1b.setVisible(true); - input1c.setVisible(true); + input3.setVisible(true); super.pack(); input1a.setMaximumSize(new Dimension(input1a.getMaximumSize().width, input1a.getHeight())); input1b.setMaximumSize(new Dimension(input1b.getMaximumSize().width, input1b.getHeight())); - input1c.setMaximumSize(new Dimension(input1c.getMaximumSize().width, input1c.getHeight())); + input3.setMaximumSize(new Dimension(input3.getMaximumSize().width, input3.getHeight())); local.setSelected(true); @@ -470,9 +519,13 @@ public static void errorWindow(String message) { */ private static void chooseDirectory(JTextField textField) { String pref = textField.getName(); - if (pref.isEmpty()) pref = "omero.batch." + Prefs.DIR_IMAGE; + if (pref.isEmpty()) { + pref = "omero.batch." + Prefs.DIR_IMAGE; + } String previousDir = textField.getText(); - if (previousDir.isEmpty()) previousDir = Prefs.get(pref, previousDir); + if (previousDir.isEmpty()) { + previousDir = Prefs.get(pref, previousDir); + } JFileChooser outputChoice = new JFileChooser(previousDir); outputChoice.setDialogTitle("Choose the directory"); @@ -511,7 +564,7 @@ public void run(String arg) { * @param username The OMERO user. * @param userId The user ID. */ - public void userProjectsAndDatasets(String username, long userId) { + private void userProjects(String username, long userId) { if ("All members".equals(username)) { userProjects = groupProjects; } else { @@ -555,7 +608,9 @@ private void updateInputProject(ItemEvent e) { for (DatasetWrapper d : this.datasets) { datasetListIn.addItem(format(d.getName(), d.getId(), padName, padId)); } - if (!this.datasets.isEmpty()) datasetListIn.setSelectedIndex(0); + if (!this.datasets.isEmpty()) { + datasetListIn.setSelectedIndex(0); + } } } } @@ -590,7 +645,9 @@ private void updateOutputProject(ItemEvent e) { for (DatasetWrapper d : this.myDatasets) { datasetListOut.addItem(format(d.getName(), d.getId(), padName, padId)); } - if (!this.datasets.isEmpty()) datasetListOut.setSelectedIndex(0); + if (!this.datasets.isEmpty()) { + datasetListOut.setSelectedIndex(0); + } } } } @@ -612,7 +669,9 @@ private void createNewDataset(ActionEvent e) { null, null, null); - if (name == null) return; + if (name == null) { + return; + } try { DatasetWrapper newDataset = project.addDataset(client, name, ""); id = newDataset.getId(); @@ -654,8 +713,10 @@ private void updateUser(ItemEvent e) { int index = userList.getSelectedIndex(); String username = userList.getItemAt(index); long userId = -1; - if (index >= 1) userId = users.get(index - 1).getId(); - userProjectsAndDatasets(username, userId); + if (index >= 1) { + userId = users.get(index - 1).getId(); + } + userProjects(username, userId); projectListIn.removeAllItems(); projectListOut.removeAllItems(); datasetListIn.removeAllItems(); @@ -670,8 +731,12 @@ private void updateUser(ItemEvent e) { for (ProjectWrapper project : myProjects) { projectListOut.addItem(format(project.getName(), project.getId(), padMyName, padMyId)); } - if (!userProjects.isEmpty()) projectListIn.setSelectedIndex(0); - if (!myProjects.isEmpty()) projectListOut.setSelectedIndex(0); + if (!userProjects.isEmpty()) { + projectListIn.setSelectedIndex(0); + } + if (!myProjects.isEmpty()) { + projectListOut.setSelectedIndex(0); + } } } @@ -732,20 +797,16 @@ private void updateInput(ItemEvent e) { connected = connect(); } if (connected) { - input1a.setVisible(true); - input1b.setVisible(true); - input1c.setVisible(true); - input2.setVisible(false); + groupList.getParent().setVisible(true); + projectListIn.getParent().setVisible(true); + inputFolder.getParent().setVisible(false); } else { local.setSelected(true); } } else { //local.isSelected() - input2.setVisible(true); - checkDelROIs.setSelected(false); - checkLoadROIs.setSelected(false); - input1c.setVisible(false); - input1b.setVisible(false); - input1a.setVisible(false); + inputFolder.getParent().setVisible(true); + projectListIn.getParent().setVisible(false); + groupList.getParent().setVisible(false); } this.repack(); } @@ -813,7 +874,9 @@ public void onThreadFinished() { private boolean connect() { final Color green = new Color(0, 153, 0); boolean connected = false; - if (client == null) client = new Client(); + if (client == null) { + client = new Client(); + } OMEROConnectDialog connectDialog = new OMEROConnectDialog(); connectDialog.connect(client); if (!connectDialog.wasCancelled()) { @@ -845,7 +908,9 @@ private boolean connect() { int index = -1; for (int i = 0; index < 0 && i < groups.size(); i++) { - if (groups.get(i).getId() == groupId) index = i; + if (groups.get(i).getId() == groupId) { + index = i; + } } groupList.setSelectedIndex(-1); groupList.setSelectedIndex(index); @@ -920,57 +985,62 @@ private void previewDataset() { */ public void start(ActionEvent e) { ProgressDialog progress = new ProgressDialog(); - OMEROBatchRunner runner = new OMEROBatchRunner(script, client, progress); - runner.setListener(this); + BatchParameters params = new BatchParameters(); - // initiation of success variables - boolean checkInput; - boolean checkMacro = getMacro(); - boolean checkOutput = getOutput(); + // initialization of success variables + boolean badInput; + boolean badMacro = !getMacro(); + boolean badOutput = !getOutput(); // input data - if (omero.isSelected()) { - runner.setInputOnOMERO(true); - int index = datasetListIn.getSelectedIndex(); - DatasetWrapper dataset = datasets.get(index); - long inputDatasetId = dataset.getId(); - runner.setInputDatasetId(inputDatasetId); - runner.setOutputDatasetId(inputDatasetId); - checkInput = true; - } else { // local.isSelected() - runner.setInputOnOMERO(false); - checkInput = getLocalInput(); - runner.setDirectoryIn(directoryIn); - runner.setRecursive(recursive.isSelected()); + params.setSuffix(suffix.getText()); + params.setROIMode(roiMode.getItemAt(roiMode.getSelectedIndex())); + params.setClearROIS(checkDelROIs.isSelected()); + params.setSaveImages(checkImage.isSelected()); + params.setSaveResults(checkResults.isSelected()); + params.setSaveROIs(checkROIs.isSelected()); + params.setSaveLog(checkLog.isSelected()); + + List images; + long inputDatasetId = -1L; + try { + if (omero.isSelected()) { + int index = datasetListIn.getSelectedIndex(); + DatasetWrapper dataset = datasets.get(index); + inputDatasetId = dataset.getId(); + List imageWrappers = dataset.getImages(client); + images = listImages(client, imageWrappers); + badInput = false; + } else { // local.isSelected() + badInput = !getLocalInput(); + images = listImages(directoryIn, recursive.isSelected()); + } + params.setOutputDatasetId(inputDatasetId); + } catch (ServiceException | AccessException | ExecutionException | IOException exception) { + IJ.error(exception.getMessage()); + return; } - if (!checkInput || !checkMacro || !checkOutput) { + if (badInput || badMacro || badOutput) { return; } - // suffix - runner.setSuffix(suffix.getText()); - - runner.setLoadROIS(checkLoadROIs.isSelected()); - runner.setClearROIS(checkDelROIs.isSelected()); - runner.setSaveImage(checkImage.isSelected()); - runner.setSaveResults(checkResults.isSelected()); - runner.setSaveROIs(checkROIs.isSelected()); - runner.setSaveLog(checkLog.isSelected()); if (onlineOutput.isSelected()) { - runner.setOutputOnOMERO(true); + params.setOutputOnOMERO(true); if (checkResults.isSelected()) { - runner.setOutputProjectId(outputProjectId); + params.setOutputProjectId(outputProjectId); } if (checkImage.isSelected()) { - runner.setOutputDatasetId(outputDatasetId); + params.setOutputDatasetId(outputDatasetId); } } if (localOutput.isSelected()) { - runner.setOutputOnLocal(true); - runner.setDirectoryOut(directoryOut); + params.setOutputOnLocal(true); + params.setDirectoryOut(directoryOut); } + OMEROBatchRunner runner = new OMEROBatchRunner(script, images, params, client, progress); + runner.setListener(this); start.setEnabled(false); try { runner.start(); @@ -996,14 +1066,13 @@ private void updateOutput(ActionEvent e) { onlineOutput.setSelected(outputOnline); } - output2.setVisible(outputImage); - output3a.setVisible(outputOnline && (outputImage || outputResults)); - datasetListOut.setVisible(outputOnline && outputImage); - newDatasetBtn.setVisible(outputOnline && outputImage); + suffix.getParent().setVisible(outputImage); + projectListOut.getParent().setVisible(outputOnline && (outputImage || outputResults)); + datasetListOut.getParent().setVisible(outputOnline && outputImage); if (outputOnline && userProjects.equals(myProjects)) { projectListOut.setSelectedIndex(projectListIn.getSelectedIndex()); } - output3b.setVisible(outputLocal); + outputFolder.getParent().setVisible(outputLocal); repack(); } @@ -1113,8 +1182,8 @@ private boolean getLocalOutput() { */ private boolean checkDeleteROIs() { boolean check = true; - if (checkDelROIs.isSelected() && (!onlineOutput.isSelected() || !checkROIs.isSelected())) { - errorWindow(String.format("ROIs:%nYou can't clear ROIs if you don't save ROIs on OMERO")); + if (checkDelROIs.isSelected() && !checkROIs.isSelected()) { + errorWindow(String.format("ROIs:%nYou can't clear ROIs if you don't save ROIs")); check = false; } return check; @@ -1129,7 +1198,8 @@ private boolean checkDeleteROIs() { private boolean checkUploadLocalInput() { boolean check = true; if (local.isSelected() && onlineOutput.isSelected() && !checkImage.isSelected()) { - errorWindow(String.format("Output:%nYou can't upload results file or ROIs on OMERO if your image isn't in OMERO")); + errorWindow(String.format( + "Output:%nYou can't upload results file or ROIs on OMERO if your image isn't in OMERO")); check = false; } return check; @@ -1189,11 +1259,12 @@ private void repack() { this.pack(); this.setMinimumSize(bestSize); - Container inputPanel = input1b.getParent(); + Container inputPanel = projectListIn.getParent().getParent(); inputPanel.setMinimumSize(inputPanel.getPreferredSize()); - inputPanel.setMaximumSize(new Dimension(inputPanel.getMaximumSize().width, inputPanel.getPreferredSize().height)); + inputPanel.setMaximumSize( + new Dimension(inputPanel.getMaximumSize().width, inputPanel.getPreferredSize().height)); - Container outputPanel = output2.getParent(); + Container outputPanel = onlineOutput.getParent().getParent(); outputPanel.setMinimumSize(outputPanel.getPreferredSize()); outputPanel.setMaximumSize(new Dimension(outputPanel.getMaximumSize().width, outputPanel.getHeight())); } @@ -1201,16 +1272,16 @@ private void repack() { private class ClientDisconnector extends WindowAdapter { - ClientDisconnector() { - super(); - } + ClientDisconnector() {} @Override public void windowClosing(WindowEvent e) { super.windowClosing(e); Client c = client; - if (c != null) c.disconnect(); + if (c != null) { + c.disconnect(); + } } } diff --git a/src/main/java/fr/igred/ij/plugin/frame/package-info.java b/src/main/java/fr/igred/ij/plugin/frame/package-info.java index fe5949d..f0c7ee6 100644 --- a/src/main/java/fr/igred/ij/plugin/frame/package-info.java +++ b/src/main/java/fr/igred/ij/plugin/frame/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 MICA & GReD + * Copyright (C) 2021-2023 MICA & GReD * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software diff --git a/src/test/java/fr/igred/ij/plugin/frame/OMEROBatchPluginTest.java b/src/test/java/fr/igred/ij/plugin/frame/OMEROBatchPluginTest.java index 1c20d2e..6869f83 100644 --- a/src/test/java/fr/igred/ij/plugin/frame/OMEROBatchPluginTest.java +++ b/src/test/java/fr/igred/ij/plugin/frame/OMEROBatchPluginTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 MICA & GReD + * Copyright (C) 2021-2023 MICA & GReD * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software