From be2675886104ea3540a4cd04b07a5997d93a1fc9 Mon Sep 17 00:00:00 2001 From: Amandine Tournay <amandine.tournay@pasteur.fr> Date: Tue, 9 Mar 2021 19:07:46 +0100 Subject: [PATCH] Added project --- .gitignore | 6 + pom.xml | 92 ++++++++++ .../java/plugins/tprovoost/scale/Scale.java | 56 ++++++ .../plugins/tprovoost/scale/ScalePainter.java | 159 ++++++++++++++++++ .../tprovoost/scale/ScalePainterOld.java | 105 ++++++++++++ src/main/resources/Scale.png | Bin 0 -> 758 bytes src/main/resources/Scale.xml | 78 +++++++++ src/main/resources/Scale_icon.png | Bin 0 -> 5132 bytes 8 files changed, 496 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/plugins/tprovoost/scale/Scale.java create mode 100644 src/main/java/plugins/tprovoost/scale/ScalePainter.java create mode 100644 src/main/java/plugins/tprovoost/scale/ScalePainterOld.java create mode 100644 src/main/resources/Scale.png create mode 100644 src/main/resources/Scale.xml create mode 100644 src/main/resources/Scale_icon.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d47f98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea/ +target/ +.settings/ +*.iml +.project +.classpath \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e61cd16 --- /dev/null +++ b/pom.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <!-- Inherited Icy Parent POM --> + <parent> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>parent-pom-plugin</artifactId> + <version>1.0.3</version> + </parent> + + <!-- Project Information --> + <artifactId>scale-bar</artifactId> + <version>3.1.6</version> + + <packaging>jar</packaging> + + <name>Scale Bar</name> + <description> + Displays a scale bar overlay on the sequence. + Warning: this plugin needs correct sequence metadata to be effective. Otherwise it will display wrong values. + </description> + <url>http://icy.bioimageanalysis.org/plugin/scale-bar/</url> + <inceptionYear>2020</inceptionYear> + + <organization> + <name>Institut Pasteur</name> + <url>https://pasteur.fr</url> + </organization> + + <licenses> + <license> + <name>GNU GPLv3</name> + <url>https://www.gnu.org/licenses/gpl-3.0.en.html</url> + <distribution>repo</distribution> + </license> + </licenses> + + <developers> + <developer> + <id>sdallongeville</id> + <name>Stéphane Dallongeville</name> + <url>https://research.pasteur.fr/fr/member/stephane-dallongeville/</url> + <roles> + <role>founder</role> + <role>lead</role> + <role>architect</role> + <role>developer</role> + <role>debugger</role> + <role>tester</role> + <role>maintainer</role> + <role>support</role> + </roles> + </developer> + </developers> + + <!-- Project properties --> + <properties> + + </properties> + + <!-- Project build configuration --> + <build> + + </build> + + <!-- List of project's dependencies --> + <dependencies> + <!-- The core of Icy --> + <dependency> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>icy-kernel</artifactId> + </dependency> + + <!-- The EzPlug library, simplifies writing UI for Icy plugins. --> + <dependency> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>ezplug</artifactId> + </dependency> + </dependencies> + + <!-- Icy Maven repository (to find parent POM) --> + <repositories> + <repository> + <id>icy</id> + <name>Icy's Nexus</name> + <url>https://icy-nexus.pasteur.fr/repository/Icy/</url> + </repository> + </repositories> +</project> \ No newline at end of file diff --git a/src/main/java/plugins/tprovoost/scale/Scale.java b/src/main/java/plugins/tprovoost/scale/Scale.java new file mode 100644 index 0000000..af5f315 --- /dev/null +++ b/src/main/java/plugins/tprovoost/scale/Scale.java @@ -0,0 +1,56 @@ +package plugins.tprovoost.scale; + +import icy.gui.main.GlobalSequenceListener; +import icy.main.Icy; +import icy.plugin.abstract_.Plugin; +import icy.plugin.interface_.PluginDaemon; +import icy.sequence.Sequence; + +public class Scale extends Plugin implements PluginDaemon +{ + ScalePainter sp; + GlobalSequenceListener globalSequenceListener; + + @Override + public void run() + { + // nothing to do here + } + + @Override + public void stop() + { + for (Sequence sequence : Icy.getMainInterface().getSequences()) + sequence.removeOverlay(sp); + + Icy.getMainInterface().removeGlobalSequenceListener(globalSequenceListener); + } + + @Override + public void init() + { + // create painter + sp = new ScalePainter(); + globalSequenceListener = new GlobalSequenceListener() + { + @Override + public void sequenceOpened(Sequence sequence) + { + sequence.addOverlay(sp); + } + + @Override + public void sequenceClosed(Sequence sequence) + { + sequence.removeOverlay(sp); + } + }; + + // add the Scale Bar overlay on opened sequences + for (Sequence sequence : Icy.getMainInterface().getSequences()) + sequence.addOverlay(sp); + + // listen new opened sequence + Icy.getMainInterface().addGlobalSequenceListener(globalSequenceListener); + } +} diff --git a/src/main/java/plugins/tprovoost/scale/ScalePainter.java b/src/main/java/plugins/tprovoost/scale/ScalePainter.java new file mode 100644 index 0000000..af4f9ce --- /dev/null +++ b/src/main/java/plugins/tprovoost/scale/ScalePainter.java @@ -0,0 +1,159 @@ +package plugins.tprovoost.scale; + +import icy.canvas.IcyCanvas; +import icy.canvas.IcyCanvas2D; +import icy.math.MathUtil; +import icy.math.UnitUtil; +import icy.math.UnitUtil.UnitPrefix; +import icy.painter.Overlay; +import icy.sequence.Sequence; +import icy.util.GraphicsUtil; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.Stroke; + +public class ScalePainter extends Overlay +{ + /** + * Possible rounded scale factors : 1 --> 900 + */ + final static double[] scaleRoundedFactors = new double[] {1d, 2d, 3d, 4d, 5d, 6d, 7d, 8d, 9d, 10d, 20d, 30d, 40d, + 50d, 60d, 70d, 80d, 90d, 100d, 200d, 300d, 400d, 500d, 600d, 700d, 800d, 900d}; + + public ScalePainter() + { + super("Scale Bar"); + } + + @Override + public void paint(Graphics2D g2, Sequence sequence, IcyCanvas canvas) + { + if (g2 == null) + return; + if (!(canvas instanceof IcyCanvas2D)) + return; + + final IcyCanvas2D c2d = (IcyCanvas2D) canvas; + final Graphics2D g = (Graphics2D) g2.create(); + + double scaleFactor = c2d.getScaleX(); + g.transform(c2d.getInverseTransform()); + + BasicStroke[] stroke = new BasicStroke[4]; + stroke[0] = new BasicStroke(2f); + stroke[1] = new BasicStroke(3f); + stroke[2] = new BasicStroke(4f); + stroke[3] = new BasicStroke(5f); + int w = canvas.getCanvasSizeX(); + int h = canvas.getCanvasSizeY(); + double pxSize = sequence.getPixelSizeX(); + int space = 20; // spacing from borders + + int sizeW = w / 8; + // get the value with the best display possible + double valueReal = sizeW * pxSize / scaleFactor / 10; + UnitPrefix bestUnit = UnitUtil.getBestUnit(valueReal, UnitPrefix.MICRO, 1); + double valueRealBestUnit = UnitUtil.getValueInUnit(valueReal * 10, UnitPrefix.MICRO, bestUnit); + + double closestScale = MathUtil.closest(valueRealBestUnit, scaleRoundedFactors); + + // closest scale in px + int closestScPx = (int) (UnitUtil.getValueInUnit(closestScale, bestUnit, UnitPrefix.MICRO) / pxSize * scaleFactor); + // int closestScPx = closestScale; + + g.setColor(Color.BLACK); + g.setStroke(stroke[3]); + // g.drawLine(w - space - closestScPx, h - space, w - space, h - space); + g.drawLine(space, h - space, space + closestScPx, h - space); + + g.setColor(Color.WHITE); + g.setStroke(stroke[2]); + g.drawLine(space, h - space, space + closestScPx, h - space); + + int fontSize = 14; + Font font = new Font("Arial", Font.PLAIN, fontSize); + g.setFont(font); + String pixelString = "" + closestScale + " " + bestUnit + "m"; + int txW = g.getFontMetrics().charsWidth(pixelString.toCharArray(), 0, pixelString.length()); + int txH = g.getFontMetrics().getHeight(); + + g.setColor(Color.black); + // GraphicsUtil.drawString(g, pixelString, w - space - closestScPx / 2 - + // txW / 2, h - space - txH, true); + GraphicsUtil.drawString(g, pixelString, space + closestScPx / 2 - txW / 2, h - space - txH, true); + + g.dispose(); + } + + double convertScale(IcyCanvas canvas, double value) + { + return canvas.canvasToImageLogDeltaX((int) value); + } + + public class CompositeStroke implements Stroke + { + private Stroke stroke1, stroke2; + + public CompositeStroke(Stroke stroke1, Stroke stroke2) + { + this.stroke1 = stroke1; + this.stroke2 = stroke2; + } + + @Override + public Shape createStrokedShape(Shape shape) + { + return stroke2.createStrokedShape(stroke1.createStrokedShape(shape)); + } + } + +// private int findBestScale(IcyCanvas2D canvas, Sequence s) +// { +// int w = s.getWidth(); +// int minSize = w / 8; +// int maxSize = w / 4; +// double pxSize = s.getPixelSizeX(); +// double tab[] = new double[maxSize - minSize]; +// double scaleFactor = canvas.getScaleX(); +// +// // generate all values +// for (int i = minSize; i < maxSize; ++i) +// { +// +// // get the value with the best display possible +// double valueReal = i * pxSize / scaleFactor; +// UnitPrefix bestUnit = UnitUtil.getBestUnit(valueReal, UnitPrefix.MICRO, 1); +// double bestValue = UnitUtil.getValueInUnit(valueReal, UnitPrefix.MICRO, bestUnit); +// bestValue = ((int) (bestValue * 100)) / 100d; +// +// tab[i - minSize] = bestValue; +// } +// +// // return the first value with a round value +// for (int i = 0; i < maxSize - minSize; ++i) +// { +// double bestValue = tab[i]; +// if (bestValue == (int) bestValue) +// { +// return i + minSize; +// } +// } +// +// // otherwise, return first value with only one decimal +// for (int i = 0; i < maxSize - minSize; ++i) +// { +// double bestValue = tab[i]; +// if (bestValue == ((int) (bestValue * 10)) / 10d) +// { +// return i + minSize; +// } +// } +// +// // otherwise, return w / 4 +// return w / 4; +// } +} diff --git a/src/main/java/plugins/tprovoost/scale/ScalePainterOld.java b/src/main/java/plugins/tprovoost/scale/ScalePainterOld.java new file mode 100644 index 0000000..caf0c37 --- /dev/null +++ b/src/main/java/plugins/tprovoost/scale/ScalePainterOld.java @@ -0,0 +1,105 @@ +package plugins.tprovoost.scale; + +import icy.canvas.IcyCanvas; +import icy.canvas.IcyCanvas2D; +import icy.math.UnitUtil; +import icy.math.UnitUtil.UnitPrefix; +import icy.painter.Overlay; +import icy.roi.ROI2D; +import icy.sequence.Sequence; +import icy.util.GraphicsUtil; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.Stroke; + +public class ScalePainterOld extends Overlay +{ + public ScalePainterOld() + { + super("Scale Bar"); + } + + @Override + public void paint(Graphics2D g2, Sequence sequence, IcyCanvas canvas) + { + if (g2 == null) + return; + Graphics2D g = (Graphics2D) g2.create(); + // g.transform(((IcyCanvas2D) canvas).getInverseTransform()); + int w = sequence.getWidth(); + int h = sequence.getHeight(); + int sizeW = w / 10; + double pxSize = sequence.getPixelSizeX(); + int spaceFBords = 20; // spacing from borders + + BasicStroke[] stroke = new BasicStroke[4]; + stroke[0] = new BasicStroke((float) canvas.canvasToImageLogDeltaX(2)); + stroke[1] = new BasicStroke((float) canvas.canvasToImageLogDeltaX(3)); + stroke[2] = new BasicStroke((float) canvas.canvasToImageLogDeltaX(4)); + stroke[3] = new BasicStroke((float) canvas.canvasToImageLogDeltaX(5)); + + // get the value with the best display possible + double valueReal = sizeW * pxSize / 10; + UnitPrefix bestUnit = UnitUtil.getBestUnit(valueReal, UnitPrefix.MICRO, 1); + double bestValue = UnitUtil.getValueInUnit(valueReal * 10, UnitPrefix.MICRO, bestUnit); + + // get the best scale possible + int closestScale = 0; + if (bestValue < 10) + { + closestScale = (int) bestValue; + } else + closestScale = (int) (bestValue / 10) * 10; + + // closest scale in px + int closestScPx = (int) (UnitUtil.getValueInUnit(closestScale, bestUnit, UnitPrefix.MICRO) / pxSize); + + g.setColor(Color.BLACK); + g.setStroke(stroke[3]); + g.drawLine(w - spaceFBords - closestScPx, h - spaceFBords, w - spaceFBords, h - spaceFBords); + + g.setColor(Color.WHITE); + g.setStroke(stroke[2]); + g.drawLine(w - spaceFBords - closestScPx, h - spaceFBords, w - spaceFBords, h - spaceFBords); + + int fontSize = (int) canvas.canvasToImageLogDeltaX(14); + Font font = new Font("Arial", Font.PLAIN, fontSize); + g.setFont(font); + String pixelString = "" + closestScale + " " + bestUnit + "m"; + int txW = g.getFontMetrics().charsWidth(pixelString.toCharArray(), 0, pixelString.length()); + int txH = g.getFontMetrics().getHeight(); + + // g.fillRect(w - spaceFBords - closestScPx / 2 - txW / 2, h - + // spaceFBords - txH, txW, txH); + + g.setColor(Color.black); + GraphicsUtil.drawString(g, pixelString, w - spaceFBords - closestScPx / 2 - txW / 2, h - spaceFBords - txH, true); + + g.dispose(); + } + + double convertScale(IcyCanvas canvas, double value) + { + return canvas.canvasToImageLogDeltaX((int) value); + } + + public class CompositeStroke implements Stroke + { + private Stroke stroke1, stroke2; + + public CompositeStroke(Stroke stroke1, Stroke stroke2) + { + this.stroke1 = stroke1; + this.stroke2 = stroke2; + } + + public Shape createStrokedShape(Shape shape) + { + return stroke2.createStrokedShape(stroke1.createStrokedShape(shape)); + } + } +} diff --git a/src/main/resources/Scale.png b/src/main/resources/Scale.png new file mode 100644 index 0000000000000000000000000000000000000000..2227c16fad9f21113d628a11d5b912a4f42f1431 GIT binary patch literal 758 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911L)MWvCLm<VGm>l8*76lSpbYCt2Qc`IU zF{m&|6BIBqFfuZ{QkNHJU|?$Yba4!+nDh3=-Ylg=5w--!rijkH8wH%YL>zPW@H+-a zF5#YJRm-(;E62{B30kdlmuwQ%&Oe}~py|YM(DqG(_UfOJC*S2vdal`a*nqF*!@m1x zW}JlCGpOkgdfoM^Z(9$k37)-u`}X0(hv&|no3+(yrq853ea9c~-MjZmO3VU=8ehS) z9KQ-IdLBM{v?y$~YGv;B+kflq+w!(omy|34O0Slzt*h(PwV$ay_0;3X$voxd<?QV2 zuQTK3C-2y^M@NL~Xwt`5ud)mzrg$yg@$l@~UZxN8qk6yA*!7n$OgZ=b^Xbp4Kfh+R zu(hqNs_J@i{q@(15|_nFe?3<Q#i{3Pk$N4szPz?}uiVF{$q&ym-MN2X|9QrtSFD+O zLUEhUd@QlDwYI*!<MsakoF7iHu3RS+HzB3OZ1&tab9BDH*|2}Ve7=j}k!5o6CttIA z>=Sr2Bap#veaEu`iyRXvet!Pdg7$WHebav&Jzw8=>+QEH#&hS-CwH{FxiS9OqcrPj z(a-J1i6&B^T2ph(q;LPPu!+-mSbn)Pzu6*9q~Uz_#;Lk>Grl<`PJYWZ;q+6HhV==% znQ|0C0m^k?`!a?P-m9<vny|xs_Sr4dnd<!*e_SgoFP|;1_-^tyU<yWn2l>Alj|ttd TJ9X;fH;|~OtDnm{r-UW|Jxl<8 literal 0 HcmV?d00001 diff --git a/src/main/resources/Scale.xml b/src/main/resources/Scale.xml new file mode 100644 index 0000000..9a16e0b --- /dev/null +++ b/src/main/resources/Scale.xml @@ -0,0 +1,78 @@ +<?xml version='1.0' encoding='UTF-8' standalone='no'?> +<root> +<url><![CDATA[http://icy.bioimageanalysis.org/repository/getXMLPluginFile.php?pluginId=179&beta=0]]></url><name>Scale Bar</name><version>2.0.0.0</version><required_kernel_version>1.3.6.0</required_kernel_version><kernel_ver>1.3.6.0</kernel_ver><jar_url><![CDATA[http://icy.bioimageanalysis.org/repository/getJarFile.php?pluginId=179&beta=0]]></jar_url><icon_url><![CDATA[http://bioimageanalysis.org/icy/image.php?idAttach=1229]]></icon_url><image_url><![CDATA[http://bioimageanalysis.org/icy/image.php?idAttach=2020]]></image_url><description><![CDATA[Displays a scale bar overlay on the sequence. Be advised, this plugin needs correct Metadata on the sequence to be effective or it will display wrong values.]]></description><classname><![CDATA[plugins.tprovoost.scale.Scale]]></classname><author><![CDATA[tprovoost - Thomas Provoost]]></author><changelog><![CDATA[- +Version 2.0.0.0 +Date 2013-07-03 12:15:38 + +* Bottom left now. This will make it avoid being invisible because of the zoom factor is displayed at the same time +* the scale bar now fully works + +- +Version 1.6.0.0 +Date 2013-07-03 12:02:48 + +* Uses orders of magnitude instead of precise values. +* size of scale bar never changes now: will always be : size of the canvas / 8. + +- +Version 1.5.0.0 +Date 2013-06-26 16:52:26 + +* Now only updates text while zooming, and change the size and value after the zoom + +- +Version 1.4.0.0 +Date 2013-06-26 11:20:13 + +* fixed bar location issue + +- +Version 1.3.1.0 +Date 2013-06-17 15:18:23 + +* removed debug information + +- +Version 1.3.0.0 +Date 2013-06-17 15:17:46 + +* now scales with images zoom + +- +Version 1.2.0.0 +Date 2013-06-17 14:15:50 + +* removed white rectangle +* now shadow on text + +- +Version 1.1.0.0 +Date 2013-01-11 16:30:30 + +Updated to 1.3.0.0 + +- +Version 1.0.0.2 +Date 2012-10-22 17:10:43 + +* corrected 3D issue. + +- +Version 1.0.0.1 +Date 2012-07-03 09:23:11 + +Corrected issue with compilation. + +- +Version 1.0.0.0 +Date 2012-07-02 21:17:10 + +Updated to Icy last version. + +- +Version 0.0.1.0 +Date 2012-06-20 14:33:49 + + + +]]></changelog><web><![CDATA[http://icy.bioimageanalysis.org/plugin/Scale_Bar]]></web><dependencies></dependencies></root> \ No newline at end of file diff --git a/src/main/resources/Scale_icon.png b/src/main/resources/Scale_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..46567ed34a2c5de77c00a72963814c1866260f07 GIT binary patch literal 5132 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 literal 0 HcmV?d00001 -- GitLab