diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3d47f986c41db29ec6dc0d5036bf760b3a1cf366 --- /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 0000000000000000000000000000000000000000..a0e91f16fba6dbe004302456ac236144c7d2b216 --- /dev/null +++ b/pom.xml @@ -0,0 +1,89 @@ +<?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>protractor</artifactId> + <version>1.1.0</version> + + <packaging>jar</packaging> + + <name>Protractor</name> + <description>A protractor to measure angles on images </description> + <url>http://icy.bioimageanalysis.org/plugin/protractor/</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> + + <dependency> + <groupId>org.bioimageanalysis.icy</groupId> + <artifactId>ruler-helper</artifactId> + <version>1.2.1</version> + </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> diff --git a/src/main/java/plugins/fab/protractor/Protractor.class b/src/main/java/plugins/fab/protractor/Protractor.class new file mode 100644 index 0000000000000000000000000000000000000000..4ea6bec10a84e84a8c6e6ce2dd3da5fa604a7183 Binary files /dev/null and b/src/main/java/plugins/fab/protractor/Protractor.class differ diff --git a/src/main/java/plugins/fab/protractor/Protractor.java b/src/main/java/plugins/fab/protractor/Protractor.java new file mode 100644 index 0000000000000000000000000000000000000000..d746afd7a6622808064df0848cfd71a25c375cd6 --- /dev/null +++ b/src/main/java/plugins/fab/protractor/Protractor.java @@ -0,0 +1,30 @@ +/** + * by Fabrice de Chaumont + */ + +package plugins.fab.protractor; + +import icy.gui.dialog.MessageDialog; +import icy.plugin.abstract_.Plugin; +import icy.plugin.interface_.PluginImageAnalysis; +import icy.sequence.Sequence; + +public class Protractor extends Plugin implements PluginImageAnalysis { + + @Override + public void compute() { + + + Sequence sequence = getFocusedSequence(); + + if ( sequence!=null ) + { + new ProtractorPainter( getFocusedSequence() ); + }else + { + MessageDialog.showDialog("Please open an image first.", MessageDialog.INFORMATION_MESSAGE ); + } + + } + +} diff --git a/src/main/java/plugins/fab/protractor/ProtractorPainter.class b/src/main/java/plugins/fab/protractor/ProtractorPainter.class new file mode 100644 index 0000000000000000000000000000000000000000..375e3f6504d4a3fbf9f104f757d81707c770e23c Binary files /dev/null and b/src/main/java/plugins/fab/protractor/ProtractorPainter.class differ diff --git a/src/main/java/plugins/fab/protractor/ProtractorPainter.java b/src/main/java/plugins/fab/protractor/ProtractorPainter.java new file mode 100644 index 0000000000000000000000000000000000000000..9a5ace94cbce92e2d09166de8fc6247838a955fd --- /dev/null +++ b/src/main/java/plugins/fab/protractor/ProtractorPainter.java @@ -0,0 +1,415 @@ +/** + * by Fabrice de Chaumont + */ +package plugins.fab.protractor; + +import icy.canvas.IcyCanvas; +import icy.main.Icy; +import icy.math.Scaler; +import icy.painter.Anchor2D; +import icy.painter.Anchor2D.Anchor2DListener; +import icy.painter.Overlay; +import icy.painter.PainterEvent; +import icy.sequence.Sequence; +import icy.type.point.Point5D; +import icy.type.point.Point5D.Double; +import icy.util.GraphicsUtil; + +import java.awt.AlphaComposite; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Line2D; +import java.awt.geom.NoninvertibleTransformException; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; + +import plugins.fab.Ruler.Anchor2DTarget; +import plugins.fab.Ruler.ShapeDefinition; + +public class ProtractorPainter extends Overlay implements Anchor2DListener { + + Anchor2DTarget a1 = new Anchor2DTarget( 0 , 0 ); + Anchor2DTarget a2 = new Anchor2DTarget( 0 , 0 ); + ArrayList<ShapeDefinition> shapeDefinitionList = new ArrayList<ShapeDefinition>(); + ArrayList<AffineTransform> affineTransformList = new ArrayList<AffineTransform>(); + + + public ProtractorPainter(Sequence sequence) { + + super("Protractor"); + if ( sequence == null ) return; + + sequence.addOverlay( this ); + + a1.setPosition( sequence.getWidth() / 2 , sequence.getHeight() / 2 ); + a2.setPosition( 3 * sequence.getWidth() / 4 , sequence.getHeight() / 2 ); + +// sequence.addPainter( this ); + a1.addAnchorListener( this ); + a2.addAnchorListener( this ); + + } + + // Listener of Anchor + + @Override + public void painterChanged(PainterEvent event) { + + for ( Sequence sequence : Icy.getMainInterface().getSequencesContaining( this ) ) + { + sequence.overlayChanged( this ); +// sequence.painterChanged( this ); + } + + } + + @Override + public void positionChanged(Anchor2D source) { + + + } + + + int findBestMajTickSpace(int sliderSize, int delta) + { + + final int values[] = { 5, 10, 20, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000}; + + + int wantedMajTickSpace; + // wanted a major tick each ~40 pixels + try + { + wantedMajTickSpace = delta / (sliderSize / 40); + } + catch( ArithmeticException e ) + { + return values[0]; + } + + int min = Integer.MAX_VALUE; + int bestValue = 5; + + // try with our predefined values + for (int value : values) + { + final int dx = Math.abs(value - wantedMajTickSpace); + + if (dx < min) + { + min = dx; + bestValue = value; + } + } + + return bestValue; + } + + void pushTransform( Graphics2D g ) + { + affineTransformList.add( g.getTransform() ); + } + + void popTransform( Graphics2D g ) + { + g.setTransform( affineTransformList.get( affineTransformList.size() -1 ) ); + affineTransformList.remove( affineTransformList.size() -1 ); + } + + + public Point2D getPointLocation( double angle, double distance2 ) + { + angle = -angle * ( Math.PI / 180.0 ); + //distance2 = 100; + + Line2D line = new Line2D.Double( a1.getPosition() , a2.getPosition() ); + + // perform rotation. + + AffineTransform transform = new AffineTransform(); + + double distance = line.getP1().distance( line.getP2() ); + double vx = ( line.getP2().getX() - line.getP1().getX() ) / distance; + double vy = ( line.getP2().getY() - line.getP1().getY() ) / distance; + + //transform.translate( line.getX1() , line.getY1() ); + transform.rotate( -Math.atan2( vy , vx ) , 0 , 0 ); + + Point2D searchedPoint = new Point2D.Double( Math.cos( angle ) * distance2 , Math.sin( angle ) * distance2 ); + Point2D p = new Point2D.Double( 0,0 ); + + try { + transform.inverseTransform( searchedPoint , p ); + } catch (NoninvertibleTransformException e) { + e.printStackTrace(); + } + + p.setLocation( p.getX() + a1.getPosition().getX() , p.getY() + a1.getPosition().getY() ); + + return p; + } + + // Painter Section: + + @Override + public void paint(Graphics2D g, Sequence sequence, IcyCanvas canvas) { + + g= (Graphics2D) g.create(); + + 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 ) ); + + Line2D line = new Line2D.Double( a1.getPosition() , a2.getPosition() ); + + // transform and display ticks + + shapeDefinitionList.clear(); + + pushTransform( g ); + + double distance = line.getP1().distance( line.getP2() ); + double vx = ( line.getP2().getX() - line.getP1().getX() ) / distance; + double vy = ( line.getP2().getY() - line.getP1().getY() ) / distance; + + g.translate( line.getX1() , line.getY1() ); + g.rotate( Math.atan2( vy , vx ) , 0 , 0 ); + + double minorTickDistance = 9d * distance / 10d; + double subMinorTickDistance = 95d * distance / 100d; + double firstDistance = distance / 10d; + double secondDistance = 3* distance / 10d; + + // each 90° lines + + for ( int i=0 ; i< 360 ; i+=90 ) + { + double angle = i * ( Math.PI / 180.0 ); + + shapeDefinitionList.add( new ShapeDefinition( 1 , + new Line2D.Double( Math.cos( angle ) * firstDistance , Math.sin( angle ) * firstDistance , + Math.cos( angle ) * ( 3d * distance / 2d ) , Math.sin( angle ) * ( 3d * distance / 2d ) ) + ) ); + } + + // each 45° lines + + for ( int i=45 ; i< 360 ; i+=90 ) + { + double angle = i * ( Math.PI / 180.0 ); + + shapeDefinitionList.add( new ShapeDefinition( 2 , + new Line2D.Double( Math.cos( angle ) * secondDistance , Math.sin( angle ) * secondDistance , + Math.cos( angle ) * distance , Math.sin( angle ) * distance ) + ) ); + } + + // circles + + shapeDefinitionList.add( new ShapeDefinition( 1 , + new Ellipse2D.Double( -distance , -distance , distance*2 , distance*2 ) + ) ); + + shapeDefinitionList.add( new ShapeDefinition( 1 , + new Ellipse2D.Double( -firstDistance , -firstDistance , firstDistance*2 , firstDistance*2 ) + ) ); + + // ticks each 5 degrees + + for ( int i=0 ; i< 360 ; i+=5 ) + { + double angle = i * ( Math.PI / 180.0 ); + + shapeDefinitionList.add( new ShapeDefinition( 1 , + new Line2D.Double( Math.cos( angle ) * minorTickDistance , Math.sin( angle ) * minorTickDistance , + Math.cos( angle ) * distance , Math.sin( angle ) * distance ) + ) ); + } + + double distanceBetweenTicks; + { + double a = 0; + Point2D p1 = new Point2D.Double( Math.cos( a ) * distance , Math.sin( a ) * distance ); + a+=Math.PI / 18d; + Point2D p2 = new Point2D.Double( Math.cos( a ) * distance , Math.sin( a ) * distance ); + distanceBetweenTicks = canvas.getScaleX() * p1.distance( p2 ); + } + + // ticks each 1 degree + + Scaler scalerSubMinorTickAlpha = new Scaler( 40 , 50 , 0d , 1d , false ); + float alphaSubMinor = (float) scalerSubMinorTickAlpha.scale( distanceBetweenTicks ); + + if ( distanceBetweenTicks > 40 ) + { + for ( int i=0 ; i< 360 ; i++ ) + { + if ( i%5 == 0 ) continue; + + double angle = i * ( Math.PI / 180.0 ); + + shapeDefinitionList.add( new ShapeDefinition( 1 , + new Line2D.Double( Math.cos( angle ) * subMinorTickDistance , Math.sin( angle ) * subMinorTickDistance , + Math.cos( angle ) * distance , Math.sin( angle ) * distance ) , alphaSubMinor + ) ); + } + } + + + // draw lines ( black background, then white ) + + float oldAlpha= -1; + g.setColor( Color.black ); + for ( ShapeDefinition ld : shapeDefinitionList ) + { + if (oldAlpha!=ld.alpha) + { + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, ld.alpha ) ); + oldAlpha = ld.alpha; + } + g.setStroke( stroke[ld.stroke] ); + g.draw( ld.shape ); + } + + g.setColor( Color.white ); + for ( ShapeDefinition ld : shapeDefinitionList ) + { + if (oldAlpha!=ld.alpha) + { + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, ld.alpha ) ); + oldAlpha = ld.alpha; + } + g.setStroke( stroke[ld.stroke-1] ); + g.draw( ld.shape ); + } + + // draw degrees text + + Scaler scalerMajorTickAlpha = new Scaler( 12 , 20 , 0d , 1d , false ); + float alphaMajor = (float) scalerMajorTickAlpha.scale( distanceBetweenTicks ); + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alphaMajor ) ); + + if ( distanceBetweenTicks > 12 ) + { + int fontSize = (int)convertScale(canvas, 15 ); + Font font = new Font( "Arial" , Font.PLAIN , fontSize ); + for ( int i = 0 ; i < 360 ; i+=45 ) + { + pushTransform( g ); + String pixelString = " "+ (int)i + "° "; + Rectangle2D pixelBounds = GraphicsUtil.getStringBounds( g , font, pixelString ); + g.rotate( -i * Math.PI / 180d ); + g.translate( distance/2 - pixelBounds.getWidth()/2 , pixelBounds.getHeight()/4 ) ; //-convertScale( canvas, 20 ) ); + g.setFont( font ); + g.setColor( Color.white ); + g.fill( pixelBounds ); + g.setColor( Color.black ); + g.drawString( pixelString , 0, 0 ); + popTransform(g); + } + } + + Scaler scalerMinorTickAlpha = new Scaler( 18 , 30 , 0d , 1d , false ); + float alphaMinor = (float) scalerMinorTickAlpha.scale( distanceBetweenTicks ); + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alphaMinor ) ); + + if ( distanceBetweenTicks > 18 ) + { + int fontSize = (int)convertScale(canvas, 10 ); + Font font = new Font( "Arial" , Font.PLAIN , fontSize ); + for ( int i = 0 ; i < 360 ; i+=10 ) + { + pushTransform( g ); + String pixelString = " "+ (int)i + "° "; + Rectangle2D pixelBounds = GraphicsUtil.getStringBounds( g , font, pixelString ); + g.rotate( -i * Math.PI / 180d ); + g.translate( 5d*distance/6d - pixelBounds.getWidth()/2 , pixelBounds.getHeight()/4 ) ; //-convertScale( canvas, 20 ) ); + g.setFont( font ); + g.setColor( Color.white ); + g.fill( pixelBounds ); + g.setColor( Color.black ); + g.drawString( pixelString , 0, 0 ); + popTransform( g ); + } + } + + // get back to original transform + + popTransform( g ); + + // display anchors + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1 ) ); + a1.paint( g, sequence, canvas); + a2.paint( g, sequence, canvas); + } + + double convertScale( IcyCanvas canvas , double value ) + { + return canvas.canvasToImageLogDeltaX( (int) value ); + } + + + @Override + public void mousePressed(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { + a1.mousePressed(e, imagePoint, canvas); + a2.mousePressed(e, imagePoint, canvas); + } + + @Override + public void mouseReleased(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { + a1.mouseReleased(e, imagePoint, canvas); + a2.mouseReleased(e, imagePoint, canvas); + } + + @Override + public void mouseClick(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { + a1.mouseClick(e, imagePoint, canvas); + a2.mouseClick(e, imagePoint, canvas); + } + + @Override + public void mouseMove(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { + a1.mouseMove(e, imagePoint, canvas); + a2.mouseMove(e, imagePoint, canvas); + } + + @Override + public void mouseDrag(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { + a1.mouseDrag(e, imagePoint, canvas); + a2.mouseDrag(e, imagePoint, canvas); + } + + @Override + public void keyPressed(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { + + if ( ( e.getKeyCode() == KeyEvent.VK_DELETE && a1.isSelected() ) || ( e.getKeyCode() == KeyEvent.VK_DELETE && a2.isSelected() ) ) + { + for ( Sequence sequence : Icy.getMainInterface().getSequencesContaining( this ) ) + { + sequence.removePainter( this ); + } + + } + + a1.keyPressed(e, imagePoint, canvas); + a2.keyPressed(e, imagePoint, canvas); + } + + @Override + public void keyReleased(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { + a1.keyReleased(e, imagePoint, canvas); + a2.keyReleased(e, imagePoint, canvas); + } + +}