Revert "Work around annoying bug in Graphics2D, in which composite modes"
[trakem2.git] / ini / trakem2 / display / Stack.java
blobd507396fcfd7d3f8ff4ea54e1a25ba8d59976650
1 /**
2 * License: GPL
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License 2
6 * as published by the Free Software Foundation.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program; if not, write to the Free Software
15 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18 package ini.trakem2.display;
20 import ij.IJ;
21 import ij.ImagePlus;
22 import ij.process.ImageProcessor;
23 import ini.trakem2.Project;
24 import ini.trakem2.utils.Utils;
25 import ini.trakem2.utils.IJError;
27 import java.awt.Color;
28 import java.awt.Composite;
29 import java.awt.Graphics2D;
30 import java.awt.Image;
31 import java.awt.Polygon;
32 import java.awt.Rectangle;
33 import java.awt.geom.AffineTransform;
34 import java.awt.geom.Area;
35 import java.util.ArrayList;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.Iterator;
39 import java.util.Map;
40 import java.util.concurrent.Callable;
41 import java.util.concurrent.Future;
43 import mpicbg.ij.stack.InverseTransformMapping;
44 import mpicbg.ij.util.Filter;
45 import mpicbg.models.AffineModel2D;
46 import mpicbg.models.AffineModel3D;
47 import mpicbg.models.Boundable;
48 import mpicbg.models.InvertibleCoordinateTransformList;
49 import mpicbg.models.TranslationModel3D;
50 import mpicbg.trakem2.transform.InvertibleCoordinateTransform;
51 import mpicbg.util.Util;
53 /**
54 * @author Stephan Saalfeld <saalfeld@mpi-cbg.de>
56 public class Stack extends ZDisplayable implements ImageData
58 private String file_path;
59 private double depth = -1;
60 private double min;
61 private double max;
62 private InvertibleCoordinateTransform ict;
63 final private float[] boundsMin = new float[]{ 0, 0, 0 };
64 final private float[] boundsMax = new float[]{ 0, 0, 0 };
65 private double ictScale = 1.0;
68 * References cached images in Loader whose unique identifier from the
69 * prespective of the stack are double:magnification and double:z.
71 final private HashMap< SliceViewKey, Long > cachedImages = new HashMap< SliceViewKey, Long >();
73 final private HashMap< Long, Future< Image > > futureImages = new HashMap< Long, Future<Image> >();
75 private static class SliceViewKey
77 final double magnification;
78 final double z;
80 SliceViewKey( final double magnification, final double z )
82 this.magnification = magnification;
83 this.z = z;
86 @Override
87 final public boolean equals( Object o )
89 final SliceViewKey k = ( SliceViewKey )o;
90 return k.magnification == magnification && k.z == z;
93 @Override
94 final public int hashCode()
96 return 0;
100 public Stack( final Project project, final String title, double x, double y, Layer initial_layer, final String file_path )
102 super( project, title, x, y );
103 this.file_path = file_path;
104 // ct ==> initial_layer;
105 final ImagePlus imp = project.getLoader().fetchImagePlus( this );
107 /* TODO scale regarding the Calibration and shift regarding x, y and the initial_layer */
108 depth = imp.getNSlices();
109 width = imp.getWidth();
110 height = imp.getHeight();
111 min = imp.getDisplayRangeMin();
112 max = imp.getDisplayRangeMax();
113 //at.translate( x, y ); // No need: the call to the super constructor already translated the affine transform.
115 boundsMin[ 0 ] = 0;
116 boundsMin[ 1 ] = 0;
117 boundsMin[ 2 ] = ( float )initial_layer.getZ();
119 boundsMax[ 0 ] = ( float ) width;
120 boundsMax[ 1 ] = ( float ) height;
121 boundsMax[ 2 ] = ( float )( boundsMin[ 2 ] + depth );
123 addToDatabase();
126 /** For cloning purposes. */
127 private Stack(Project project, long id, String title,
128 AffineTransform at, double width, double height,
129 float alpha, boolean visible, Color color, boolean locked,
130 double depth, double min, double max,
131 float[] boundsMin, float[] boundsMax,
132 InvertibleCoordinateTransform ict, double ictScale, String file_path) {
133 super(project, id, title, locked, at, 0, 0);
134 this.title = title;
135 this.alpha = alpha;
136 this.visible = visible;
137 this.color = color;
138 this.boundsMin[0] = boundsMin[0];
139 this.boundsMin[1] = boundsMin[1];
140 this.boundsMin[2] = boundsMin[2];
141 this.boundsMax[0] = boundsMax[0];
142 this.boundsMax[1] = boundsMax[1];
143 this.boundsMax[2] = boundsMax[2];
144 this.width = width;
145 this.height = height;
146 this.depth = depth;
147 this.min = min;
148 this.max = max;
149 this.ict = null == ict ? null : this.ict.clone();
150 this.file_path = file_path;
153 /** Construct a Stack from an XML entry. */
154 public Stack(Project project, long id, HashMap ht, HashMap ht_links) {
155 super(project, id, ht, ht_links);
156 // parse specific fields
158 final Iterator it = ht.entrySet().iterator();
159 while (it.hasNext()) {
160 final Map.Entry entry = (Map.Entry)it.next();
161 final String key = (String)entry.getKey();
162 final String data = (String)entry.getValue();
163 if (key.equals("min")) {
164 this.min = Double.parseDouble(data);
165 } else if (key.equals("max")) {
166 this.max = Double.parseDouble(data);
167 } else if (key.equals("file_path")) {
168 this.file_path = data;
169 } else if (key.equals("depth")) {
170 this.depth = Double.parseDouble(data);
173 boundsMin[ 0 ] = 0;
174 boundsMin[ 1 ] = 0;
175 boundsMin[ 2 ] = 0;
177 boundsMax[ 0 ] = ( float )width;
178 boundsMax[ 1 ] = ( float )height;
179 boundsMax[ 2 ] = ( float )depth;
182 public InvertibleCoordinateTransform getInvertibleCoordinateTransform()
184 return ict;
187 /* (non-Javadoc)
188 * @see ini.trakem2.display.ZDisplayable#getFirstLayer()
190 @Override
191 public Layer getFirstLayer()
193 // TODO Auto-generated method stub
194 return null;
197 /* (non-Javadoc)
198 * @see ini.trakem2.display.ZDisplayable#intersects(java.awt.geom.Area, double, double)
200 @Override
201 public boolean intersects( Area area, double z_first, double z_last )
203 // TODO Auto-generated method stub
204 return false;
207 /* (non-Javadoc)
208 * @see ini.trakem2.display.ZDisplayable#linkPatches()
210 @Override
211 public boolean linkPatches()
213 return false;
216 /* (non-Javadoc)
217 * @see ini.trakem2.display.Displayable#clone(ini.trakem2.Project, boolean)
219 @Override
220 public Displayable clone( Project pr, boolean copy_id )
222 final long nid = copy_id ? this.id : pr.getLoader().getNextId();
223 final Stack copy = new Stack( pr, nid, null != title ? title.toString() : null,
224 new AffineTransform(at), width, height,
225 alpha, visible, color, locked,
226 depth, min, max,
227 boundsMin, boundsMax,
228 ict, ictScale, file_path );
229 // Copy references to cached images
230 copy.cachedImages.putAll(cachedImages);
231 copy.futureImages.putAll(futureImages);
232 copy.addToDatabase();
233 return copy;
236 /* (non-Javadoc)
237 * @see ini.trakem2.display.Displayable#isDeletable()
239 @Override
240 public boolean isDeletable()
242 return 0 == width && 0 == height;
245 public String getFilePath(){
246 return this.file_path;
249 public long estimateImageFileSize()
251 if ( -1 == depth )
252 return IJ.maxMemory() / 2;
253 return (long) (width * height * depth * 4);
257 * Estimate the scale of atp to apply the
258 * appropriate smoothing to the image.
260 final static private float estimateAffineScale( final AffineTransform atp )
262 final float dxx = ( float )atp.getScaleX();
263 final float dxy = ( float )atp.getShearY();
264 final float dxs = dxx * dxx + dxy * dxy;
266 final float dyx = ( float )atp.getShearX();
267 final float dyy = ( float )atp.getScaleY();
268 final float dys = dyx * dyx + dyy * dyy;
270 return ( float )Math.sqrt( Math.max( dxs, dys ) );
273 /** Slow paint: will wait until the image is generated and cached, then paint it. */
274 @Override
275 public void paint(final Graphics2D g, final double magnification, final boolean active, final int channels, final Layer active_layer) {
276 Image image = null;
277 Future< Image > fu = null;
278 final SliceViewKey sliceViewKey = new SliceViewKey( magnification, active_layer.getZ() );
279 synchronized ( cachedImages )
281 final long imageId;
282 Long imageIdL = cachedImages.get( sliceViewKey );
283 if ( imageIdL == null )
285 imageId = project.getLoader().getNextTempId();
286 cachedImages.put( sliceViewKey, imageId );
288 else
290 /* fetch the image from cache---still, it may be that it is not there... */
291 imageId = imageIdL;
292 image = project.getLoader().getCached( cachedImages.get( sliceViewKey ), 0 );
294 if ( image == null )
296 /* image has to be generated */
297 fu = fetchFutureImage( imageId, magnification, active_layer, false ); // do not trigger repaint event
301 // Paint outside the synchronization block:
302 if (null != image) {
303 paint(g, image);
304 } else if (null != fu) {
305 try {
306 image = fu.get();
307 } catch (Throwable ie) {
308 IJ.log("Could not paint Stack " + this);
309 IJError.print(ie);
310 return;
312 // If I put the fu.get() where image is, it fails to compile. I had to separate it!
313 paint(g, image); // will wait until present
314 } else {
315 Utils.log2("Stack.paint ERROR: no image to paint!");
319 final private Future< Image > fetchFutureImage(final Long imageId, final double magnification, final Layer active_layer, final boolean trigger_repaint_event) {
320 synchronized ( futureImages )
322 Future< Image > fu = futureImages.get( imageId );
323 if ( null == fu )
325 fu = project.getLoader().doLater( new Callable< Image >()
327 public Image call()
329 final InvertibleCoordinateTransformList< mpicbg.models.InvertibleCoordinateTransform > ictl = new InvertibleCoordinateTransformList< mpicbg.models.InvertibleCoordinateTransform >();
330 if ( ict != null )
332 // Utils.log2( "ictScale of " + getTitle() + " is " + ictScale );
333 ictl.add( ict );
335 /* Remove boundingBox shift ict ... */
336 final TranslationModel3D unShiftBounds = new TranslationModel3D();
337 unShiftBounds.set( -boundsMin[ 0 ], -boundsMin[ 1 ], 0 );
338 ictl.add( unShiftBounds );
340 if ( ictScale != 1.0 )
342 final AffineModel3D unScaleXY = new AffineModel3D();
343 unScaleXY.set(
344 1.0f / ( float )ictScale, 0, 0, 0,
345 0, 1.0f / ( float )ictScale, 0, 0,
346 0, 0, 1.0f, 0 );
347 ictl.add( unScaleXY );
351 /* TODO remove that scale from ict and put it into atp */
353 final ImagePlus imp = project.getLoader().fetchImagePlus( Stack.this );
354 final ImageProcessor ip = imp.getStack().getProcessor( 1 ).createProcessor( ( int )Math.ceil( ( boundsMax[ 0 ] - boundsMin[ 0 ] ) / ictScale ), ( int )Math.ceil( ( boundsMax[ 1 ] - boundsMin[ 1 ] ) / ictScale ) );
356 //Utils.log2( "ictScale is " + ictScale );
357 //Utils.log2( "rendering an image of " + ip.getWidth() + " x " + ip.getHeight() + " px" );
359 final double currentZ = active_layer.getZ();
361 final TranslationModel3D sliceShift = new TranslationModel3D();
362 sliceShift.set( 0, 0, ( float )-currentZ );
363 ictl.add( sliceShift );
365 /* optimization: if ict is affine, reduce ictl into a single affine */
366 final InverseTransformMapping< mpicbg.models.InvertibleCoordinateTransform > mapping;
367 if ( AffineModel3D.class.isInstance( ict ) )
369 final AffineModel3D ictAffine = new AffineModel3D();
370 boolean isAffine = true;
371 for ( final mpicbg.models.InvertibleCoordinateTransform t : ictl.getList( null ) )
373 if ( AffineModel3D.class.isInstance( t ) )
374 ictAffine.preConcatenate( ( AffineModel3D )t );
375 else if ( TranslationModel3D.class.isInstance( t ) )
376 ictAffine.preConcatenate( ( TranslationModel3D )t );
377 else
379 isAffine = false;
380 break;
383 if ( isAffine )
384 mapping = new InverseTransformMapping< mpicbg.models.InvertibleCoordinateTransform >( ictAffine );
385 else
386 mapping = new InverseTransformMapping< mpicbg.models.InvertibleCoordinateTransform >( ictl );
388 else
389 mapping = new InverseTransformMapping< mpicbg.models.InvertibleCoordinateTransform >( ictl );
390 mapping.mapInterpolated( imp.getStack(), ip );
392 final float s = estimateAffineScale( new AffineTransform( at ) ); // wast: atp
394 final float smoothMag = ( float )magnification * s * ( float )ictScale;
395 if ( smoothMag < 1.0f )
397 Filter.smoothForScale( ip, smoothMag, 0.5f, 0.5f );
400 final Image image = ip.createImage();
402 if ( null == image )
404 Utils.log2( "Stack.paint: null image, returning" );
405 return null; // TEMPORARY from lazy
406 // repaints after closing a
407 // Project
410 project.getLoader().cacheAWT( imageId, image );
412 synchronized ( futureImages )
414 futureImages.remove( imageId );
417 if ( trigger_repaint_event )
419 // Display.repaint( active_layer, Stack.this );
420 Display.repaint( active_layer );
423 return image;
426 } // else {
427 // Utils.log2( "fu is not null" );
428 // // We don't do anything: we wait for itself to launch a
429 // repaint event
430 // }
432 futureImages.put( imageId, fu );
433 return fu;
437 /** Will not paint but fork a task to create an image to paint later, when not already cached. */
438 @Override
439 public void prePaint(
440 final Graphics2D g,
441 final double magnification,
442 final boolean active,
443 final int channels,
444 final Layer active_layer )
447 //final Image image = project.getLoader().fetchImage(this,0);
448 //Utils.log2("Patch " + id + " painted image " + image);
450 final double currentZ = active_layer.getZ();
451 Image image = null;
452 synchronized ( cachedImages )
454 final SliceViewKey sliceViewKey = new SliceViewKey( magnification, currentZ );
455 final long imageId;
456 Long imageIdL = cachedImages.get( sliceViewKey );
457 if ( imageIdL == null )
459 imageId = project.getLoader().getNextTempId();
460 cachedImages.put( sliceViewKey, imageId );
462 else
464 /* fetch the image from cache---still, it may be that it is not there... */
465 imageId = imageIdL;
466 image = project.getLoader().getCached( cachedImages.get( sliceViewKey ), 0 );
468 if ( image == null )
470 /* image has to be generated */
471 fetchFutureImage( imageId, magnification, active_layer, true );
472 return;
476 if ( image != null) {
477 paint( g, image );
481 final private void paint( final Graphics2D g, final Image image )
483 final AffineTransform atp = new AffineTransform( this.at );
485 /* Put boundShift into atp */
486 final AffineTransform shiftBounds = new AffineTransform( 1, 0, 0, 1, boundsMin[ 0 ], boundsMin[ 1 ] );
487 atp.concatenate( shiftBounds );
489 /* If available, incorporate the involved x,y-scale of ict in the AffineTransform */
490 final AffineTransform asict = new AffineTransform( ictScale, 0, 0, ictScale, 0, 0 );
491 atp.concatenate( asict );
493 final Composite original_composite = g.getComposite();
494 // Fail gracefully for graphics cards that don't support custom composites, like ATI cards:
495 try {
496 g.setComposite( getComposite() );
497 g.drawImage( image, atp, null );
498 } catch (Throwable t) {
499 Utils.log(new StringBuilder("Cannot paint Stack with composite type ").append(compositeModes[getCompositeMode()]).append("\nReason:\n").append(t.toString()).toString());
500 g.setComposite( getComposite( COMPOSITE_NORMAL ) );
501 g.drawImage( image, atp, null );
504 g.setComposite( original_composite );
507 static public final void exportDTD( final StringBuffer sb_header, final HashSet hs, final String indent ) {
508 String type = "t2_stack";
509 if (hs.contains(type)) return;
510 hs.add( type );
511 sb_header.append(indent).append("<!ELEMENT t2_stack (").append(Displayable.commonDTDChildren()).append(",(iict_transform|iict_transform_list)?)>\n");
512 Displayable.exportDTD( type, sb_header, hs, indent );
513 sb_header.append(indent).append(TAG_ATTR1).append(type).append(" file_path CDATA #REQUIRED>\n")
514 .append(indent).append(TAG_ATTR1).append(type).append(" depth CDATA #REQUIRED>\n");
517 /** Opens and closes the tag and exports data. The image is saved in the directory provided in @param any as a String. */
518 public void exportXML(StringBuffer sb_body, String indent, Object any) { // TODO the Loader should handle the saving of images, not this class.
519 String in = indent + "\t";
520 sb_body.append(indent).append("<t2_stack\n");
522 super.exportXML(sb_body, in, any);
523 String[] RGB = Utils.getHexRGBColor(color);
525 sb_body.append(in).append("file_path=\"").append(file_path).append("\"\n")
526 .append(in).append("style=\"fill-opacity:").append(alpha).append(";stroke:#").append(RGB[0]).append(RGB[1]).append(RGB[2]).append(";\"\n")
527 .append(in).append("depth=\"").append(depth).append("\"\n")
528 .append(in).append("min=\"").append(min).append("\"\n")
529 .append(in).append("max=\"").append(max).append("\"\n")
532 sb_body.append(indent).append(">\n");
534 if (null != ict) {
535 sb_body.append(ict.toXML(in)).append('\n');
538 super.restXML(sb_body, in, any);
540 sb_body.append(indent).append("</t2_stack>\n");
543 @Override
544 protected Rectangle getBounds( final Rectangle rect )
546 final AffineModel2D a = new AffineModel2D();
547 a.set( at );
549 final float[] rMin = new float[]{ Float.MAX_VALUE, Float.MAX_VALUE };
550 final float[] rMax = new float[]{ -Float.MAX_VALUE, -Float.MAX_VALUE };
552 final float[] l = new float[]{ boundsMin[ 0 ], boundsMin[ 1 ] };
554 a.applyInPlace( l );
555 Util.min( rMin, l );
556 Util.max( rMax, l );
558 l[ 0 ] = boundsMin[ 0 ];
559 l[ 1 ] = boundsMax[ 1 ];
560 a.applyInPlace( l );
561 Util.min( rMin, l );
562 Util.max( rMax, l );
564 l[ 0 ] = boundsMax[ 0 ];
565 l[ 1 ] = boundsMin[ 1 ];
566 a.applyInPlace( l );
567 Util.min( rMin, l );
568 Util.max( rMax, l );
570 l[ 0 ] = boundsMax[ 0 ];
571 l[ 1 ] = boundsMax[ 1 ];
572 a.applyInPlace( l );
573 Util.min( rMin, l );
574 Util.max( rMax, l );
576 rect.x = ( int )rMin[ 0 ];
577 rect.y = ( int )rMin[ 1 ];
578 rect.width = ( int )Math.ceil( rMax[ 0 ] - rect.x );
579 rect.height = ( int )Math.ceil( rMax[ 1 ] - rect.y );
581 return rect;
584 private void update()
586 boundsMin[ 0 ] = 0;
587 boundsMin[ 1 ] = 0;
588 boundsMin[ 2 ] = 0;
590 boundsMax[ 0 ] = ( float )width;
591 boundsMax[ 1 ] = ( float )height;
592 boundsMax[ 2 ] = ( float )depth;
594 if ( ict == null )
596 // Utils.log2( "ict is null" );
597 return;
599 else if ( Boundable.class.isInstance( ict ) )
601 // Utils.log2( ict + " is a boundable" );
602 ( ( Boundable )ict ).estimateBounds( boundsMin, boundsMax );
603 // Utils.log2( ict + "its bounds are (" + boundsMin[ 0 ] + ", " + boundsMin[ 1 ] + ", " + boundsMin[ 2 ] + ") -> (" + boundsMax[ 0 ] + ", " + boundsMax[ 1 ] + ", " + boundsMax[ 2 ] + ")" );
605 else
607 Utils.log2( ict + " is not a boundable" );
608 final ArrayList< Layer > layers = layer_set.getLayers();
609 boundsMax[ 0 ] = ( float )layer_set.width;
610 boundsMax[ 1 ] = ( float )layer_set.height;
611 boundsMax[ 2 ] = ( float )( layers.get( layers.size() - 1 ).getZ() - layers.get( 0 ).getZ() );
614 if ( ict != null )
616 if ( AffineModel3D.class.isInstance( ict ) )
618 final float[] m = ( ( AffineModel3D )ict ).getMatrix( null );
620 final float dxs = m[ 0 ] * m[ 0 ] + m[ 4 ] * m[ 4 ];
621 final float dys = m[ 1 ] * m[ 1 ] + m[ 5 ] * m[ 5 ];
622 final float dzs = m[ 2 ] * m[ 2 ] + m[ 6 ] * m[ 6 ];
624 ictScale = ( float )Math.sqrt( Math.max( dxs, Math.max( dys, dzs ) ) );
630 * For now, just returns the bounding box---we can refine this later
632 public Polygon getPerimeter()
634 final Rectangle r = getBoundingBox();
635 return new Polygon(
636 new int[]{ r.x, r.x + r.width, r.x + r.width, r.x },
637 new int[]{ r.y, r.y, r.y + r.height, r.y + r.height },
638 4 );
641 /** For reconstruction purposes, overwrites the present InvertibleCoordinateTransform, if any, with the given one. */
642 public void setInvertibleCoordinateTransformSilently( final InvertibleCoordinateTransform ict )
644 this.ict = ict;
645 update();
648 private void invalidateCache()
650 cachedImages.clear();
653 public void setInvertibleCoordinateTransform( final InvertibleCoordinateTransform ict )
655 invalidateCache();
656 setInvertibleCoordinateTransformSilently( ict );
659 public void setAffineTransform( final AffineTransform at )
661 invalidateCache();
662 super.setAffineTransform( at );
665 /** Avoid calling the trees: the stack exists only in the LayerSet ZDisplayable's list. */
666 public boolean remove2(boolean check) {
667 return remove(check);