From 8ad1ce2217e67bb9ba9faccd84593cf8eb1c21df Mon Sep 17 00:00:00 2001
From: Thomas <thomas.musset@pasteur.fr>
Date: Sun, 7 Jul 2024 19:06:57 +0200
Subject: [PATCH] updated pom to v2.0.0-a.1, fix classes accordingly to new
 architecture, added icon, updated .gitignore

---
 .gitignore                                    |  46 ++-
 pom.xml                                       |  11 +-
 .../icy/image/projection/ProjectionAxis.java  |  11 +-
 .../projection/ProjectionCalculator.java      | 290 ++++++++++--------
 .../projection/ProjectionOperationType.java   |  18 +-
 .../util/MessageProgressListener.java         |   6 +-
 .../image/projection/util/SequenceCursor.java |  17 +-
 .../util/VolumetricImageCursor.java           |  21 +-
 .../adufour/projection/Projection.java        | 145 ++++-----
 .../adufour/projection/icon/icon_32.png       | Bin 0 -> 10473 bytes
 10 files changed, 328 insertions(+), 237 deletions(-)
 create mode 100644 src/main/resources/plugins/adufour/projection/icon/icon_32.png

diff --git a/.gitignore b/.gitignore
index 30c9a54..57f16fb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,41 @@
+/build*
+/workspace
+setting.xml
+release/
 target/
-workspace/
-.settings/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+icy.log
+
+### IntelliJ IDEA ###
 .idea/
-setting.xml
-.project
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
 .classpath
-*.prefs
-*.jardesc
-*.jar
-**/.DS_Store
\ No newline at end of file
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+**/.DS_Store
+Icon?
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 8f4dc7a..09be7c0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,12 +8,12 @@
     <parent>
         <groupId>org.bioimageanalysis.icy</groupId>
         <artifactId>pom-icy</artifactId>
-        <version>2.2.0</version>
+        <version>3.0.0-a.1</version>
     </parent>
 
     <!-- Project Information -->
     <artifactId>intensity-projection</artifactId>
-    <version>2.0.0</version>
+    <version>2.0.0-a.1</version>
 
     <name>Intensity Projection plugin</name>
     <description>Intensity projection along depth or time with multiple algorithms: mean, max, median, variance, standard deviation, saturated sum. Projection can be restricted to ROI.</description>
@@ -47,15 +47,11 @@
         </developer>
     </developers>
 
-    <!-- List of project's dependencies -->
     <dependencies>
-        <!-- The EzPlug library, simplifies writing UI for Icy plugins. -->
         <dependency>
             <groupId>org.bioimageanalysis.icy</groupId>
             <artifactId>ezplug</artifactId>
         </dependency>
-
-        <!-- The EzPlug library, simplifies writing UI for Icy plugins. -->
         <dependency>
             <groupId>org.bioimageanalysis.icy</groupId>
             <artifactId>protocols</artifactId>
@@ -66,8 +62,7 @@
     <repositories>
         <repository>
             <id>icy</id>
-            <name>Icy's Nexus</name>
-            <url>https://icy-nexus.pasteur.fr/repository/Icy/</url>
+            <url>https://nexus-icy.pasteur.cloud/repository/icy/</url>
         </repository>
     </repositories>
 </project>
diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionAxis.java b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionAxis.java
index bc9d7de..a3a3799 100644
--- a/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionAxis.java
+++ b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionAxis.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2023. Institut Pasteur.
+ * Copyright (c) 2010-2024. Institut Pasteur.
  *
  * This file is part of Icy.
  * Icy is free software: you can redistribute it and/or modify
@@ -18,6 +18,9 @@
 
 package org.bioimageanalysis.icy.image.projection;
 
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+
 /**
  * Indicates the direction on which the projection should be performed.
  *
@@ -28,12 +31,14 @@ public enum ProjectionAxis {
 
     private final String name;
 
-    ProjectionAxis(final String name) {
+    @Contract(pure = true)
+    ProjectionAxis(final @NotNull String name) {
         this.name = name;
     }
 
+    @Contract(pure = true)
     @Override
-    public String toString() {
+    public @NotNull String toString() {
         return name;
     }
 }
diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionCalculator.java b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionCalculator.java
index d96316f..e744a89 100644
--- a/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionCalculator.java
+++ b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionCalculator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2023. Institut Pasteur.
+ * Copyright (c) 2010-2024. Institut Pasteur.
  *
  * This file is part of Icy.
  * Icy is free software: you can redistribute it and/or modify
@@ -18,15 +18,17 @@
 
 package org.bioimageanalysis.icy.image.projection;
 
-import icy.image.IcyBufferedImage;
-import icy.image.IcyBufferedImageCursor;
-import icy.math.ArrayMath;
-import icy.roi.ROI;
-import icy.sequence.Sequence;
-import icy.sequence.VolumetricImage;
-import icy.type.DataType;
-import icy.util.OMEUtil;
+import org.bioimageanalysis.icy.common.math.ArrayMath;
+import org.bioimageanalysis.icy.common.type.DataType;
 import org.bioimageanalysis.icy.image.projection.util.MessageProgressListener;
+import org.bioimageanalysis.icy.model.OMEUtil;
+import org.bioimageanalysis.icy.model.image.IcyBufferedImage;
+import org.bioimageanalysis.icy.model.image.IcyBufferedImageCursor;
+import org.bioimageanalysis.icy.model.roi.ROI;
+import org.bioimageanalysis.icy.model.sequence.Sequence;
+import org.bioimageanalysis.icy.model.sequence.VolumetricImage;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.*;
 import java.util.concurrent.*;
@@ -38,7 +40,6 @@ import java.util.concurrent.*;
  * @author Daniel Felipe Gonzalez Obando
  */
 public class ProjectionCalculator implements Callable<Sequence> {
-
     /**
      * Builder class for {@link ProjectionCalculator}s. This allows users to specify the sequence, ROIs, axis and operation before creating the calculator.
      *
@@ -57,6 +58,7 @@ public class ProjectionCalculator implements Callable<Sequence> {
          *
          * @param s Target sequence for the projection.
          */
+        @Contract("null -> fail")
         public Builder(final Sequence s) {
             if (s == null || s.isEmpty())
                 throw new IllegalArgumentException("Input sequence is null or empty.");
@@ -138,6 +140,7 @@ public class ProjectionCalculator implements Callable<Sequence> {
     private final List<MessageProgressListener> progressListeners;
     private boolean computed;
 
+    @Contract(pure = true)
     private ProjectionCalculator() {
         progressListeners = new ArrayList<>();
         computed = false;
@@ -184,15 +187,17 @@ public class ProjectionCalculator implements Callable<Sequence> {
     private Sequence result;
 
     private void createResultSequence() throws InterruptedException {
-        result = new Sequence(OMEUtil.createOMEXMLMetadata(seq.getOMEXMLMetadata()),
-                op + " " + axis + "-projection of " + seq.getName());
+        result = new Sequence(
+                OMEUtil.createOMEXMLMetadata(seq.getOMEXMLMetadata()),
+                op + " " + axis + "-projection of " + seq.getName()
+        );
 
         final int width = axis == ProjectionAxis.X ? 1 : seq.getSizeX();
         final int height = axis == ProjectionAxis.Y ? 1 : seq.getSizeY();
         final int depth = axis == ProjectionAxis.Z ? 1 : seq.getSizeZ();
         final int frames = axis == ProjectionAxis.T ? 1 : seq.getSizeT();
         final int channels = axis == ProjectionAxis.C ? 1 : seq.getSizeC();
-        final DataType dataType = seq.getDataType_();
+        final DataType dataType = seq.getDataType();
 
         for (int t = 0; t < frames; t++) {
             final VolumetricImage vol = new VolumetricImage();
@@ -209,18 +214,14 @@ public class ProjectionCalculator implements Callable<Sequence> {
             for (int c = 0; c < seq.getSizeC(); c++)
                 result.getColorModel().setColorMap(c, seq.getColorMap(c), true);
         }
-        notifyProgress(0.01, "Result sequence instatiated");
+        notifyProgress(0.01, "Result sequence instantiated");
     }
 
     private void computeProjection() throws InterruptedException, ExecutionException {
 
         switch (axis) {
             case X:
-                //startProjectionOnPlane();
-                //break;
             case C:
-                //startProjectionOnPlane();
-                //break;
             case Y:
                 startProjectionOnPlane();
                 break;
@@ -252,54 +253,52 @@ public class ProjectionCalculator implements Callable<Sequence> {
             }
         }
 
-        final ForkJoinPool generalTaskPool = (ForkJoinPool) Executors
-                .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1));
-
-        final List<Future<IcyBufferedImage>> futures = new ArrayList<>(frames * depth);
-        for (t = 0; t < frames; t++) {
-            tOff = t * depth;
-            for (z = 0; z < depth; z++) {
-                final Callable<IcyBufferedImage> task;
-                switch (axis) {
-                    case X:
-                        task = getImageXProjectionTask(inputPlanes[tOff + z], outputPlanes[tOff + z], t, z, channels,
+        try (final ForkJoinPool generalTaskPool = (ForkJoinPool) Executors.newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1))) {
+            final List<Future<IcyBufferedImage>> futures = new ArrayList<>(frames * depth);
+            for (t = 0; t < frames; t++) {
+                tOff = t * depth;
+                for (z = 0; z < depth; z++) {
+                    final Callable<IcyBufferedImage> task = switch (axis) {
+                        case X -> getImageXProjectionTask(inputPlanes[tOff + z], outputPlanes[tOff + z], t, z, channels,
                                 height, width);
-                        break;
-                    case Y:
-                        task = getImageYProjectionTask(inputPlanes[tOff + z], outputPlanes[tOff + z], t, z, channels,
+                        case Y -> getImageYProjectionTask(inputPlanes[tOff + z], outputPlanes[tOff + z], t, z, channels,
                                 height, width);
-                        break;
-                    case C:
-                        task = getImageCProjectionTask(inputPlanes[tOff + z], outputPlanes[tOff + z], t, z, channels,
+                        case C -> getImageCProjectionTask(inputPlanes[tOff + z], outputPlanes[tOff + z], t, z, channels,
                                 height, width);
-                        break;
-                    default:
-                        throw new IllegalArgumentException("Wrong axis parameter");
+                        default -> throw new IllegalArgumentException("Wrong axis parameter");
+                    };
+                    futures.add(generalTaskPool.submit(task));
                 }
-                futures.add(generalTaskPool.submit(task));
             }
-        }
 
-        generalTaskPool.shutdown();
+            generalTaskPool.shutdown();
 
-        int pos;
-        for (pos = 0, t = 0; t < frames; t++) {
-            for (z = 0; z < depth; z++) {
-                try {
-                    futures.get(pos++).get();
-                }
-                catch (final InterruptedException | ExecutionException e) {
-                    generalTaskPool.shutdownNow();
-                    throw e;
+            int pos;
+            for (pos = 0, t = 0; t < frames; t++) {
+                for (z = 0; z < depth; z++) {
+                    try {
+                        futures.get(pos++).get();
+                    }
+                    catch (final InterruptedException | ExecutionException e) {
+                        generalTaskPool.shutdownNow();
+                        throw e;
+                    }
                 }
+                notifyProgress(0.01 + 0.99 * ((double) t / frames), axis + "-projection: Processed t=" + t);
             }
-            notifyProgress(0.01 + 0.99 * ((double) t / frames), axis + "-projection: Processed t=" + t);
         }
     }
 
-    private Callable<IcyBufferedImage> getImageXProjectionTask(final IcyBufferedImage inputPlane,
-                                                               final IcyBufferedImage outputPlane, final int t, final int z, final int channels, final int height,
-                                                               final int width) {
+    @Contract(pure = true)
+    private @NotNull Callable<IcyBufferedImage> getImageXProjectionTask(
+            final IcyBufferedImage inputPlane,
+            final IcyBufferedImage outputPlane,
+            final int t,
+            final int z,
+            final int channels,
+            final int height,
+            final int width
+    ) {
         return () -> {
             int channel, line, column, elementsCount;
             final double[] elements = new double[width];
@@ -336,9 +335,16 @@ public class ProjectionCalculator implements Callable<Sequence> {
         };
     }
 
-    private Callable<IcyBufferedImage> getImageYProjectionTask(final IcyBufferedImage inputPlane,
-                                                               final IcyBufferedImage outputPlane, final int t, final int z, final int channels, final int height,
-                                                               final int width) {
+    @Contract(pure = true)
+    private @NotNull Callable<IcyBufferedImage> getImageYProjectionTask(
+            final IcyBufferedImage inputPlane,
+            final IcyBufferedImage outputPlane,
+            final int t,
+            final int z,
+            final int channels,
+            final int height,
+            final int width
+    ) {
         return () -> {
             int channel, line, column, elementsCount;
             final double[] elements = new double[height];
@@ -376,8 +382,16 @@ public class ProjectionCalculator implements Callable<Sequence> {
         };
     }
 
-    private Callable<IcyBufferedImage> getImageCProjectionTask(final IcyBufferedImage inputPlane,
-                                                               final IcyBufferedImage outputPlane, final int t, final int z, final int channels, final int height, final int width) {
+    @Contract(pure = true)
+    private @NotNull Callable<IcyBufferedImage> getImageCProjectionTask(
+            final IcyBufferedImage inputPlane,
+            final IcyBufferedImage outputPlane,
+            final int t,
+            final int z,
+            final int channels,
+            final int height,
+            final int width
+    ) {
         return () -> {
             int channel, line, column, elementsCount;
             final double[] elements = new double[height];
@@ -430,46 +444,57 @@ public class ProjectionCalculator implements Callable<Sequence> {
         final int frames = seq.getSizeT();
         final int channels = seq.getSizeC();
 
-        final ForkJoinPool generalTaskPool = (ForkJoinPool) Executors
-                .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() / 2));
-        final ForkJoinPool volumeTaskPool = (ForkJoinPool) Executors
-                .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1));
-        int t, c;
-        VolumetricImage inputVolume, resultVolume;
-        final List<Future<VolumetricImage>> futures = new ArrayList<>(frames * depth);
-        for (t = 0; t < frames; t++) {
-            inputVolume = seq.getVolumetricImage(t);
-            resultVolume = result.getVolumetricImage(t);
-            for (c = 0; c < channels; c++) {
-                futures.add(generalTaskPool.submit(getImageZProjectionTask(t, c, inputVolume, resultVolume, channels,
-                        height, width, depth, volumeTaskPool)));
+        try (
+                final ForkJoinPool generalTaskPool = (ForkJoinPool) Executors.newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() / 2));
+                final ForkJoinPool volumeTaskPool = (ForkJoinPool) Executors.newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1))
+        ) {
+            int t, c;
+            VolumetricImage inputVolume, resultVolume;
+            final List<Future<VolumetricImage>> futures = new ArrayList<>(frames * depth);
+            for (t = 0; t < frames; t++) {
+                inputVolume = seq.getVolumetricImage(t);
+                resultVolume = result.getVolumetricImage(t);
+                for (c = 0; c < channels; c++) {
+                    futures.add(generalTaskPool.submit(getImageZProjectionTask(t, c, inputVolume, resultVolume, channels,
+                            height, width, depth, volumeTaskPool)));
 
+                }
             }
-        }
 
-        generalTaskPool.shutdown();
+            generalTaskPool.shutdown();
 
-        try {
-            int pos;
-            for (pos = 0, t = 0; t < frames; t++) {
-                for (c = 0; c < channels; c++) {
-                    futures.get(pos++).get();
+            try {
+                int pos;
+                for (pos = 0, t = 0; t < frames; t++) {
+                    for (c = 0; c < channels; c++) {
+                        futures.get(pos++).get();
+                    }
+                    notifyProgress(0.01 + 0.99 * ((double) t / frames), axis + "-projection: Processed t=" + t);
                 }
-                notifyProgress(0.01 + 0.99 * ((double) t / frames), axis + "-projection: Processed t=" + t);
+            }
+            catch (final InterruptedException | ExecutionException e) {
+                generalTaskPool.shutdownNow();
+                throw e;
+            }
+            finally {
+                volumeTaskPool.shutdown();
             }
         }
-        catch (final InterruptedException | ExecutionException e) {
-            generalTaskPool.shutdownNow();
-            throw e;
-        }
-        finally {
-            volumeTaskPool.shutdown();
-        }
-
     }
 
-    private Callable<VolumetricImage> getImageZProjectionTask(final int t, final int c, final VolumetricImage inputVolume,
-                                                              final VolumetricImage resultVolume, final int channels, final int height, final int width, final int depth, final ForkJoinPool volumeTaskPool) {
+    @Contract(pure = true)
+    @SuppressWarnings("StatementWithEmptyBody")
+    private @NotNull Callable<VolumetricImage> getImageZProjectionTask(
+            final int t,
+            final int c,
+            final VolumetricImage inputVolume,
+            final VolumetricImage resultVolume,
+            final int channels,
+            final int height,
+            final int width,
+            final int depth,
+            final ForkJoinPool volumeTaskPool
+    ) {
         return () -> {
             int slice, line, column;
 
@@ -517,7 +542,10 @@ public class ProjectionCalculator implements Callable<Sequence> {
 
             try {
                 for (line = 0; line < height; line++) {
-                    final double[] lineElements = futureLines.get(line).get();
+                    notifyProgress(0.01 + 0.99 * ((double) line / height), axis + "-projection: Processed line=" + line);
+                    final Future<double[]> future = futureLines.get(line);
+                    while (!future.isDone()) ;
+                    final double[] lineElements = future.get();
                     for (column = 0; column < width; column++) {
                         outputCursor.setSafe(column, line, c, lineElements[column]);
                     }
@@ -539,46 +567,58 @@ public class ProjectionCalculator implements Callable<Sequence> {
         final int frames = seq.getSizeT();
         final int channels = seq.getSizeC();
 
-        final ForkJoinPool generalTaskPool = (ForkJoinPool) Executors
-                .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() / 2));
-        final ForkJoinPool temporalTaskPool = (ForkJoinPool) Executors
-                .newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1));
-        int z, c;
-        Sequence inputS, resultS;
-        final List<Future<Sequence>> futures = new ArrayList<>(depth);
-        for (z = 0; z < depth; z++) {
-            for (c = 0; c < channels; c++) {
-                inputS = seq;
-                resultS = result;
-                {
-                    futures.add(generalTaskPool.submit(getImageTProjectionTask(z, c, inputS, resultS, channels, height,
-                            width, depth, frames, temporalTaskPool)));
+        try (
+                final ForkJoinPool generalTaskPool = (ForkJoinPool) Executors.newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() / 2));
+                final ForkJoinPool temporalTaskPool = (ForkJoinPool) Executors.newWorkStealingPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1))
+        ) {
+            int z, c;
+            Sequence inputS, resultS;
+            final List<Future<Sequence>> futures = new ArrayList<>(depth);
+            for (z = 0; z < depth; z++) {
+                for (c = 0; c < channels; c++) {
+                    inputS = seq;
+                    resultS = result;
+                    {
+                        futures.add(generalTaskPool.submit(getImageTProjectionTask(z, c, inputS, resultS, channels, height, width, depth, frames, temporalTaskPool)));
+                    }
                 }
             }
-        }
 
-        generalTaskPool.shutdown();
+            generalTaskPool.shutdown();
 
-        try {
-            int pos;
-            for (pos = 0, z = 0; z < depth; z++) {
-                for (c = 0; c < channels; c++) {
-                    futures.get(pos++).get();
+            try {
+                int pos;
+                for (pos = 0, z = 0; z < depth; z++) {
+                    for (c = 0; c < channels; c++) {
+                        futures.get(pos++).get();
+                    }
+                    notifyProgress(0.01 + 0.99 * ((double) z / depth), axis + "-projection: Processed z=" + z);
                 }
-                notifyProgress(0.01 + 0.99 * ((double) z / depth), axis + "-projection: Processed z=" + z);
             }
-        }
-        catch (final InterruptedException | ExecutionException e) {
-            generalTaskPool.shutdownNow();
-            throw e;
-        }
-        finally {
-            temporalTaskPool.shutdown();
+            catch (final InterruptedException | ExecutionException e) {
+                generalTaskPool.shutdownNow();
+                throw e;
+            }
+            finally {
+                temporalTaskPool.shutdown();
+            }
         }
     }
 
-    private Callable<Sequence> getImageTProjectionTask(final int z, final int c, final Sequence inputS, final Sequence resultS, final int channels,
-                                                       final int height, final int width, final int depth, final int frames, final ForkJoinPool temporalTaskPool) {
+    @Contract(pure = true)
+    @SuppressWarnings("StatementWithEmptyBody")
+    private @NotNull Callable<Sequence> getImageTProjectionTask(
+            final int z,
+            final int c,
+            final Sequence inputS,
+            final Sequence resultS,
+            final int channels,
+            final int height,
+            final int width,
+            final int depth,
+            final int frames,
+            final ForkJoinPool temporalTaskPool
+    ) {
         return () -> {
             int frame, line, column;
             final IcyBufferedImageCursor[] inputCursors = new IcyBufferedImageCursor[frames];
@@ -624,7 +664,10 @@ public class ProjectionCalculator implements Callable<Sequence> {
 
             try {
                 for (line = 0; line < height; line++) {
-                    final double[] lineElements = futureLines.get(line).get();
+                    notifyProgress(0.01 + 0.99 * ((double) line / height), axis + "-projection: Processed line=" + line);
+                    final Future<double[]> future = futureLines.get(line);
+                    while (!future.isDone()) ;
+                    final double[] lineElements = future.get();
                     for (column = 0; column < width; column++) {
                         outputCursor.setSafe(column, line, c, lineElements[column]);
                     }
@@ -663,6 +706,7 @@ public class ProjectionCalculator implements Callable<Sequence> {
         progressListeners.forEach(l -> l.onProgress(progress, message));
     }
 
+    @Contract(pure = true)
     private Sequence getResultSequence() {
         return result;
     }
diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionOperationType.java b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionOperationType.java
index 49f4cfd..12ba559 100644
--- a/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionOperationType.java
+++ b/src/main/java/org/bioimageanalysis/icy/image/projection/ProjectionOperationType.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2023. Institut Pasteur.
+ * Copyright (c) 2010-2024. Institut Pasteur.
  *
  * This file is part of Icy.
  * Icy is free software: you can redistribute it and/or modify
@@ -18,22 +18,32 @@
 
 package org.bioimageanalysis.icy.image.projection;
 
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+
 /**
  * Represents the operation to be applied when performing an intensity projection on a sequence.
  *
  * @author Daniel Felipe Gonzalez Obando
  */
 public enum ProjectionOperationType {
-    MAX("Maximum"), MEAN("Average"), MED("Median"), MIN("Minimum"), STD("Standard Deviation"), SATSUM("Saturated Sum");
+    MAX("Maximum"),
+    MEAN("Average"),
+    MED("Median"),
+    MIN("Minimum"),
+    STD("Standard Deviation"),
+    SATSUM("Saturated Sum");
 
     private final String description;
 
-    ProjectionOperationType(final String description) {
+    @Contract(pure = true)
+    ProjectionOperationType(final @NotNull String description) {
         this.description = description;
     }
 
+    @Contract(pure = true)
     @Override
-    public String toString() {
+    public @NotNull String toString() {
         return description;
     }
 }
diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/util/MessageProgressListener.java b/src/main/java/org/bioimageanalysis/icy/image/projection/util/MessageProgressListener.java
index a1e70cd..ef2088a 100644
--- a/src/main/java/org/bioimageanalysis/icy/image/projection/util/MessageProgressListener.java
+++ b/src/main/java/org/bioimageanalysis/icy/image/projection/util/MessageProgressListener.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2023. Institut Pasteur.
+ * Copyright (c) 2010-2024. Institut Pasteur.
  *
  * This file is part of Icy.
  * Icy is free software: you can redistribute it and/or modify
@@ -18,6 +18,8 @@
 
 package org.bioimageanalysis.icy.image.projection.util;
 
+import org.jetbrains.annotations.Nullable;
+
 /**
  * A Progress event listener that handles events associating a value and a message to them.
  *
@@ -30,5 +32,5 @@ public interface MessageProgressListener {
      * @param progress The progress value from 0.0 to 1.0. {@link Double#NaN} value should be taken as no change and -1.0 as indeterminate progress.
      * @param message  The event message. A null value should be taken as no change on the message.
      */
-    void onProgress(double progress, String message);
+    void onProgress(double progress, @Nullable String message);
 }
diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/util/SequenceCursor.java b/src/main/java/org/bioimageanalysis/icy/image/projection/util/SequenceCursor.java
index 166ae99..a0255ef 100644
--- a/src/main/java/org/bioimageanalysis/icy/image/projection/util/SequenceCursor.java
+++ b/src/main/java/org/bioimageanalysis/icy/image/projection/util/SequenceCursor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2023. Institut Pasteur.
+ * Copyright (c) 2010-2024. Institut Pasteur.
  *
  * This file is part of Icy.
  * Icy is free software: you can redistribute it and/or modify
@@ -18,7 +18,8 @@
 
 package org.bioimageanalysis.icy.image.projection.util;
 
-import icy.sequence.Sequence;
+import org.bioimageanalysis.icy.model.sequence.Sequence;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -40,7 +41,7 @@ public class SequenceCursor {
      *
      * @param seq Sequence.
      */
-    public SequenceCursor(final Sequence seq) {
+    public SequenceCursor(final @NotNull Sequence seq) {
         this.seq = seq;
         this.volumeCursors = new VolumetricImageCursor[seq.getSizeT()];
         this.sequenceChanged = new AtomicBoolean();
@@ -75,8 +76,7 @@ public class SequenceCursor {
      * @throws IndexOutOfBoundsException If the position is not in the image.
      * @throws RuntimeException          If the data type is not a valid format.
      */
-    public synchronized void set(final int x, final int y, final int z, final int t, final int c, final double val)
-            throws IndexOutOfBoundsException, RuntimeException {
+    public synchronized void set(final int x, final int y, final int z, final int t, final int c, final double val) throws IndexOutOfBoundsException, RuntimeException {
         getVolumeCursor(t).set(x, y, z, c, val);
         sequenceChanged.set(true);
     }
@@ -94,8 +94,7 @@ public class SequenceCursor {
      * @throws IndexOutOfBoundsException If the position is not in the image.
      * @throws RuntimeException          If the data type is not a valid format.
      */
-    public synchronized void setSafe(final int x, final int y, final int z, final int t, final int c, final double val)
-            throws IndexOutOfBoundsException, RuntimeException {
+    public synchronized void setSafe(final int x, final int y, final int z, final int t, final int c, final double val) throws IndexOutOfBoundsException, RuntimeException {
         getVolumeCursor(t).setSafe(x, y, z, c, val);
         sequenceChanged.set(true);
     }
@@ -103,7 +102,7 @@ public class SequenceCursor {
     private int currentT;
     private VolumetricImageCursor currentCursor;
 
-    private synchronized VolumetricImageCursor getVolumeCursor(final int t) throws IndexOutOfBoundsException {
+    private synchronized @NotNull VolumetricImageCursor getVolumeCursor(final int t) throws IndexOutOfBoundsException {
         if (currentT != t) {
             if (volumeCursors[t] == null) {
                 volumeCursors[t] = new VolumetricImageCursor(seq, t);
@@ -129,7 +128,7 @@ public class SequenceCursor {
     }
 
     @Override
-    public String toString() {
+    public @NotNull String toString() {
         return "last T=" + currentT + " " + ((currentCursor != null) ? currentCursor.toString() : "");
     }
 }
diff --git a/src/main/java/org/bioimageanalysis/icy/image/projection/util/VolumetricImageCursor.java b/src/main/java/org/bioimageanalysis/icy/image/projection/util/VolumetricImageCursor.java
index 7d3f108..c12d681 100644
--- a/src/main/java/org/bioimageanalysis/icy/image/projection/util/VolumetricImageCursor.java
+++ b/src/main/java/org/bioimageanalysis/icy/image/projection/util/VolumetricImageCursor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2023. Institut Pasteur.
+ * Copyright (c) 2010-2024. Institut Pasteur.
  *
  * This file is part of Icy.
  * Icy is free software: you can redistribute it and/or modify
@@ -18,9 +18,10 @@
 
 package org.bioimageanalysis.icy.image.projection.util;
 
-import icy.image.IcyBufferedImageCursor;
-import icy.sequence.Sequence;
-import icy.sequence.VolumetricImage;
+import org.bioimageanalysis.icy.model.image.IcyBufferedImageCursor;
+import org.bioimageanalysis.icy.model.sequence.Sequence;
+import org.bioimageanalysis.icy.model.sequence.VolumetricImage;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -44,7 +45,7 @@ public class VolumetricImageCursor {
      *
      * @param vol Target volume.
      */
-    public VolumetricImageCursor(final VolumetricImage vol) {
+    public VolumetricImageCursor(final @NotNull VolumetricImage vol) {
         this.vol = vol;
         planeCursors = new IcyBufferedImageCursor[vol.getSize()];
         volumeChanged = new AtomicBoolean(false);
@@ -57,7 +58,7 @@ public class VolumetricImageCursor {
      * @param seq Target sequence.
      * @param t   Time position where the volume is located in {@code seq}.
      */
-    public VolumetricImageCursor(final Sequence seq, final int t) {
+    public VolumetricImageCursor(final @NotNull Sequence seq, final int t) {
         this(seq.getVolumetricImage(t));
     }
 
@@ -88,8 +89,7 @@ public class VolumetricImageCursor {
      * @throws IndexOutOfBoundsException If the position is not in the image.
      * @throws RuntimeException          If the data type is not a valid format.
      */
-    public synchronized void set(final int x, final int y, final int z, final int c, final double val)
-            throws IndexOutOfBoundsException, RuntimeException {
+    public synchronized void set(final int x, final int y, final int z, final int c, final double val) throws IndexOutOfBoundsException, RuntimeException {
         getPlaneCursor(z).set(x, y, c, val);
         volumeChanged.set(true);
     }
@@ -106,8 +106,7 @@ public class VolumetricImageCursor {
      * @throws IndexOutOfBoundsException If the position is not in the image.
      * @throws RuntimeException          If the data type is not a valid format.
      */
-    public synchronized void setSafe(final int x, final int y, final int z, final int c, final double val)
-            throws IndexOutOfBoundsException, RuntimeException {
+    public synchronized void setSafe(final int x, final int y, final int z, final int c, final double val) throws IndexOutOfBoundsException, RuntimeException {
         getPlaneCursor(z).setSafe(x, y, c, val);
         volumeChanged.set(true);
     }
@@ -115,7 +114,7 @@ public class VolumetricImageCursor {
     private IcyBufferedImageCursor currentCursor;
     private int currentZ;
 
-    private synchronized IcyBufferedImageCursor getPlaneCursor(final int z) throws IndexOutOfBoundsException {
+    private synchronized @NotNull IcyBufferedImageCursor getPlaneCursor(final int z) throws IndexOutOfBoundsException {
         if (currentZ != z) {
             if (planeCursors[z] == null) {
                 planeCursors[z] = new IcyBufferedImageCursor(vol.getImage(z));
diff --git a/src/main/java/plugins/adufour/projection/Projection.java b/src/main/java/plugins/adufour/projection/Projection.java
index 6a5e9a0..b8640ee 100644
--- a/src/main/java/plugins/adufour/projection/Projection.java
+++ b/src/main/java/plugins/adufour/projection/Projection.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2023. Institut Pasteur.
+ * Copyright (c) 2010-2024. Institut Pasteur.
  *
  * This file is part of Icy.
  * Icy is free software: you can redistribute it and/or modify
@@ -18,16 +18,17 @@
 
 package plugins.adufour.projection;
 
-import icy.main.Icy;
-import icy.plugin.PluginLauncher;
-import icy.plugin.PluginLoader;
-import icy.roi.ROI;
-import icy.sequence.Sequence;
-import icy.system.IcyExceptionHandler;
-import icy.system.IcyHandledException;
+import org.bioimageanalysis.icy.extension.plugin.annotation_.IcyPluginIcon;
+import org.bioimageanalysis.icy.extension.plugin.annotation_.IcyPluginName;
 import org.bioimageanalysis.icy.image.projection.ProjectionAxis;
 import org.bioimageanalysis.icy.image.projection.ProjectionCalculator;
 import org.bioimageanalysis.icy.image.projection.ProjectionOperationType;
+import org.bioimageanalysis.icy.model.roi.ROI;
+import org.bioimageanalysis.icy.model.sequence.Sequence;
+import org.bioimageanalysis.icy.system.logging.IcyLogger;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import plugins.adufour.blocks.lang.Block;
 import plugins.adufour.blocks.util.VarList;
 import plugins.adufour.ezplug.*;
@@ -36,21 +37,29 @@ import plugins.adufour.vars.lang.VarSequence;
 import java.util.Collections;
 import java.util.List;
 
+@IcyPluginName("Intensity Projection")
+@IcyPluginIcon(path = "/plugins/adufour/projection/icon/icon_32.png")
 public class Projection extends EzPlug implements Block, EzStoppable {
     public enum ProjectionDirection {
         Z, T, C, Y, X
     }
 
     public enum ProjectionType {
-        MAX("Maximum"), MEAN("Average"), MED("Median"), MIN("Minimum"), STD("Standard Deviation"),
+        MAX("Maximum"),
+        MEAN("Average"),
+        MED("Median"),
+        MIN("Minimum"),
+        STD("Standard Deviation"),
         SATSUM("Saturated Sum");
 
         private final String description;
 
+        @Contract(pure = true)
         ProjectionType(final String description) {
             this.description = description;
         }
 
+        @Contract(pure = true)
         @Override
         public String toString() {
             return description;
@@ -79,7 +88,7 @@ public class Projection extends EzPlug implements Block, EzStoppable {
     }
 
     @Override
-    public void declareInput(final VarList inputMap) {
+    public void declareInput(final @NotNull VarList inputMap) {
         initCommonVars();
         inputMap.add("input", input.getVariable());
         inputMap.add("projection direction", projectionDir.getVariable());
@@ -96,7 +105,7 @@ public class Projection extends EzPlug implements Block, EzStoppable {
     }
 
     @Override
-    public void declareOutput(final VarList outputMap) {
+    public void declareOutput(final @NotNull VarList outputMap) {
         output = new VarSequence("projected sequence", null);
         outputMap.add("projection output", output);
     }
@@ -113,8 +122,8 @@ public class Projection extends EzPlug implements Block, EzStoppable {
             computeCalculator();
         }
         catch (final Exception e) {
-            IcyExceptionHandler.showErrorMessage(e, true);
-            throw new IcyHandledException("Error while projecting: " + e.getMessage(), e);
+            IcyLogger.error(this.getClass(), e, "Error while projecting.");
+            return;
         }
         setOutput();
         internalClean();
@@ -132,42 +141,27 @@ public class Projection extends EzPlug implements Block, EzStoppable {
         rois = restrictToROI.getValue(true) ? inputSequence.getROIs() : Collections.emptyList();
     }
 
-    private static ProjectionAxis getAxis(final ProjectionDirection direction) {
-        switch (direction) {
-            case X:
-                return ProjectionAxis.X;
-            case Y:
-                return ProjectionAxis.Y;
-            case C:
-                return ProjectionAxis.C;
-            case Z:
-                return ProjectionAxis.Z;
-            case T:
-                return ProjectionAxis.T;
-
-            default:
-                throw new IllegalArgumentException("" + direction);
-        }
+    @Contract(pure = true)
+    private static @NotNull ProjectionAxis getAxis(final @NotNull ProjectionDirection direction) {
+        return switch (direction) {
+            case X -> ProjectionAxis.X;
+            case Y -> ProjectionAxis.Y;
+            case C -> ProjectionAxis.C;
+            case Z -> ProjectionAxis.Z;
+            case T -> ProjectionAxis.T;
+        };
     }
 
-    private static ProjectionOperationType getOperation(final ProjectionType type) {
-        switch (type) {
-            case MAX:
-                return ProjectionOperationType.MAX;
-            case MEAN:
-                return ProjectionOperationType.MEAN;
-            case MED:
-                return ProjectionOperationType.MED;
-            case MIN:
-                return ProjectionOperationType.MIN;
-            case SATSUM:
-                return ProjectionOperationType.SATSUM;
-            case STD:
-                return ProjectionOperationType.STD;
-
-            default:
-                throw new IllegalArgumentException("" + type);
-        }
+    @Contract(pure = true)
+    private static @NotNull ProjectionOperationType getOperation(final @NotNull ProjectionType type) {
+        return switch (type) {
+            case MAX -> ProjectionOperationType.MAX;
+            case MEAN -> ProjectionOperationType.MEAN;
+            case MED -> ProjectionOperationType.MED;
+            case MIN -> ProjectionOperationType.MIN;
+            case SATSUM -> ProjectionOperationType.SATSUM;
+            case STD -> ProjectionOperationType.STD;
+        };
     }
 
     private Sequence resultSequence;
@@ -188,7 +182,7 @@ public class Projection extends EzPlug implements Block, EzStoppable {
         }
     }
 
-    private void onProgress(final double progress, final String message) {
+    private void onProgress(final double progress, final @Nullable String message) {
         if (!isHeadLess()) {
             getUI().setProgressBarValue(progress);
             getUI().setProgressBarMessage(message);
@@ -242,7 +236,11 @@ public class Projection extends EzPlug implements Block, EzStoppable {
      * @throws Exception If the projection cannot be correctly done.
      * @see ProjectionCalculator
      */
-    public static Sequence zProjection(final Sequence in, final ProjectionType projection, final boolean multiThread) throws Exception {
+    public static Sequence zProjection(
+            final Sequence in,
+            final ProjectionType projection,
+            final boolean multiThread
+    ) throws Exception {
         return zProjection(in, projection, multiThread, false);
     }
 
@@ -259,10 +257,19 @@ public class Projection extends EzPlug implements Block, EzStoppable {
      * @throws Exception If the projection cannot be correctly done.
      * @see ProjectionCalculator
      */
-    public static Sequence zProjection(final Sequence in, final ProjectionType projection, final boolean multiThread, final boolean restrictToROI) throws Exception {
+    public static Sequence zProjection(
+            final Sequence in,
+            final ProjectionType projection,
+            final boolean multiThread,
+            final boolean restrictToROI
+    ) throws Exception {
         final List<ROI> rois = restrictToROI ? in.getROIs() : Collections.emptyList();
-        final ProjectionCalculator calculator = new ProjectionCalculator.Builder(in).axis(ProjectionAxis.Z)
-                .operation(getOperation(projection)).addRois(rois).build();
+        final ProjectionCalculator calculator = new ProjectionCalculator
+                .Builder(in)
+                .axis(ProjectionAxis.Z)
+                .operation(getOperation(projection))
+                .addRois(rois)
+                .build();
         return calculator.call();
     }
 
@@ -277,7 +284,11 @@ public class Projection extends EzPlug implements Block, EzStoppable {
      * @throws Exception If the projection cannot be correctly done.
      * @see ProjectionCalculator
      */
-    public static Sequence tProjection(final Sequence in, final ProjectionType projection, final boolean multiThread) throws Exception {
+    public static Sequence tProjection(
+            final Sequence in,
+            final ProjectionType projection,
+            final boolean multiThread
+    ) throws Exception {
         return tProjection(in, projection, multiThread, false);
     }
 
@@ -294,23 +305,19 @@ public class Projection extends EzPlug implements Block, EzStoppable {
      * @throws Exception If the projection cannot be correctly done.
      * @see ProjectionCalculator
      */
-    public static Sequence tProjection(final Sequence in, final ProjectionType projection, final boolean multiThread, final boolean restrictToROI) throws Exception {
+    public static Sequence tProjection(
+            final Sequence in,
+            final ProjectionType projection,
+            final boolean multiThread,
+            final boolean restrictToROI
+    ) throws Exception {
         final List<ROI> rois = restrictToROI ? in.getROIs() : Collections.emptyList();
-        final ProjectionCalculator calculator = new ProjectionCalculator.Builder(in).axis(ProjectionAxis.T)
-                .operation(getOperation(projection)).addRois(rois).build();
+        final ProjectionCalculator calculator = new ProjectionCalculator
+                .Builder(in)
+                .axis(ProjectionAxis.T)
+                .operation(getOperation(projection))
+                .addRois(rois)
+                .build();
         return calculator.call();
     }
-
-    /**
-     * Main method used for testing.
-     *
-     * @param args Input arguments.
-     */
-    public static void main(final String[] args) {
-        // start icy
-        Icy.main(args);
-
-        // then start plugin
-        PluginLauncher.start(PluginLoader.getPlugin(Projection.class.getName()));
-    }
 }
diff --git a/src/main/resources/plugins/adufour/projection/icon/icon_32.png b/src/main/resources/plugins/adufour/projection/icon/icon_32.png
new file mode 100644
index 0000000000000000000000000000000000000000..5dc83272bac2de94fcc0cd3feba5bdb227752022
GIT binary patch
literal 10473
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

literal 0
HcmV?d00001

-- 
GitLab