001/*
002 * Copyright 2010-2018 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package icy.file;
020
021import java.awt.Point;
022import java.awt.Rectangle;
023import java.io.IOException;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029
030import javax.swing.filechooser.FileFilter;
031
032import icy.common.exception.UnsupportedFormatException;
033import icy.file.SequenceFileSticher.SequenceFileGroup;
034import icy.file.SequenceFileSticher.SequenceIdent;
035import icy.file.SequenceFileSticher.SequencePosition;
036import icy.file.SequenceFileSticher.SequenceType;
037import icy.gui.dialog.LoaderDialog;
038import icy.image.AbstractImageProvider;
039import icy.image.IcyBufferedImage;
040import icy.image.ImageUtil;
041import icy.image.colormap.IcyColorMap;
042import icy.sequence.MetaDataUtil;
043import icy.system.IcyExceptionHandler;
044import icy.type.collection.CollectionUtil;
045import icy.type.collection.array.Array1DUtil;
046import icy.util.OMEUtil;
047import icy.util.StringUtil;
048import loci.formats.FormatTools;
049import loci.formats.MetadataTools;
050import loci.formats.ome.OMEXMLMetadataImpl;
051import ome.xml.meta.OMEXMLMetadata;
052import ome.xml.model.Channel;
053import ome.xml.model.Pixels;
054import ome.xml.model.Plane;
055import plugins.kernel.importer.LociImporterPlugin;
056
057/**
058 * Special importer able to group a list of path ({@link SequenceFileGroup}) to build a single Sequence out of it.<br>
059 * Note that this importer is limited to single series group, we don't allow group mixing several series.
060 */
061public class SequenceFileGroupImporter extends AbstractImageProvider implements SequenceFileImporter
062{
063    static final int MAX_IMPORTER = 16;
064
065    class FileCursor
066    {
067        public final SequencePosition position;
068        public final int index;
069        public final int internalZ;
070        public final int internalT;
071        public final int internalC;
072
073        public FileCursor(SequencePosition position, int index, int z, int t, int c)
074        {
075            super();
076
077            this.position = position;
078            this.index = index;
079            this.internalZ = z;
080            this.internalT = t;
081            this.internalC = c;
082        }
083    }
084
085    class TileIndex
086    {
087        public final Rectangle region;
088        public final int index;
089
090        public TileIndex(Rectangle region, int index)
091        {
092            super();
093
094            this.region = region;
095            this.index = index;
096        }
097    }
098
099    protected SequenceFileGroup currentGroup;
100    protected OMEXMLMetadata currentMetadata;
101    // position index array (stored in XYZTC order) to quickly find an image given its (XY)ZTC position
102    protected SequencePosition[] positions;
103
104    protected int openFlags;
105
106    // internals
107    protected int indYMul;
108    protected int indZMul;
109    protected int indTMul;
110    protected int indCMul;
111    protected int indSMul;
112
113    /**
114     * Shared importer for multi threading
115     */
116    protected final Map<String, SequenceFileImporter> importersPool;
117
118    public SequenceFileGroupImporter()
119    {
120        super();
121
122        currentGroup = null;
123        currentMetadata = null;
124        importersPool = new HashMap<String, SequenceFileImporter>();
125    }
126
127    @Override
128    public boolean acceptFile(String path)
129    {
130        boolean result = false;
131
132        if ((currentGroup != null) && (currentGroup.ident.importer != null))
133            result = currentGroup.ident.importer.acceptFile(path);
134
135        // use Loader to test it
136        if (!result)
137            result = Loader.isSupportedImageFile(path);
138
139        return result;
140    }
141
142    @Override
143    public List<FileFilter> getFileFilters()
144    {
145        // return a generic image file filter here
146        final List<FileFilter> result = new ArrayList<FileFilter>();
147        result.add(LoaderDialog.allImagesFileFilter);
148        return result;
149    }
150
151    /**
152     * @return <code>true</code> if a opened is currently opened
153     */
154    public boolean isOpen()
155    {
156        return getOpenedGroup() != null;
157    }
158
159    /**
160     * Return the current / last loaded image without the image group
161     */
162    @Override
163    public String getOpened()
164    {
165        if (currentGroup != null)
166            return currentGroup.ident.base;
167
168        return null;
169    }
170
171    /**
172     * @return current opened group
173     */
174    public SequenceFileGroup getOpenedGroup()
175    {
176        return currentGroup;
177    }
178
179    /**
180     * @deprecated Better to use {@link #open(Collection, int)} or {@link #open(SequenceFileGroup, int)} for this importer
181     */
182    @Deprecated
183    @Override
184    public boolean open(String path, int flags) throws UnsupportedFormatException, IOException
185    {
186        open(CollectionUtil.createArrayList(path), flags);
187
188        return true;
189    }
190
191    /**
192     * Open a list of ids
193     * 
194     * @param ids
195     */
196    public void open(Collection<String> ids, int flags)
197    {
198        open(SequenceFileSticher.groupFiles(null, ids, true, null), flags);
199    }
200
201    /**
202     * Open a SequenceFileGroup
203     */
204    public void open(SequenceFileGroup group, int flags)
205    {
206        try
207        {
208            close();
209        }
210        catch (IOException e)
211        {
212            // should not prevent from opening
213            IcyExceptionHandler.showErrorMessage(e, true, true);
214        }
215
216        // can't open null group
217        if (group == null)
218            return;
219
220        currentGroup = group;
221        // we need to rebuild metadata
222        currentMetadata = null;
223
224        // store flags for later
225        openFlags = flags;
226
227        buildIndexes();
228    }
229
230    /**
231     * Open the specified importer to the given path (internal use only).
232     * 
233     * @throws IOException
234     * @throws UnsupportedFormatException
235     * @see #releaseImporter(String, SequenceFileImporter)
236     */
237    protected boolean openImporter(SequenceFileImporter result, String path)
238            throws UnsupportedFormatException, IOException
239    {
240        // importer is already opened ? --> close it first
241        if (!StringUtil.isEmpty(result.getOpened()))
242            result.close();
243
244        // disable grouping for LOCI importer as we do it here
245        if (result instanceof LociImporterPlugin)
246            ((LociImporterPlugin) result).setGroupFiles(false);
247
248        return result.open(path, openFlags);
249    }
250
251    /**
252     * Create and open a new importer for the given path (internal use only)
253     */
254    protected SequenceFileImporter createImporter(String path)
255            throws InstantiationException, IllegalAccessException, UnsupportedFormatException, IOException
256    {
257        if (!isOpen())
258            return null;
259
260        // we should always use the same importer for a file group
261        final SequenceFileImporter result = currentGroup.ident.importer.getClass().newInstance();
262
263        // open the importer for given path
264        openImporter(result, path);
265
266        // // try to use the default group importer
267        // SequenceFileImporter result = currentGroup.ident.importer;
268        //
269        // // can use default one ? --> clone it
270        // if ((result != null) && result.acceptFile(path))
271        // result = result.getClass().newInstance();
272        // else
273        // {
274        // // get importer for this path
275        // result = Loader.getSequenceFileImporter(path, true);
276        // }
277        //
278        // // try to open it
279        // if (result != null)
280        // openImporter(result, path);
281
282        return result;
283    }
284
285    /**
286     * Return the path of the first available importer (internal use only)
287     * 
288     * @throws IOException
289     * @throws UnsupportedFormatException
290     */
291    protected String getFirstImporterPath() throws IOException, UnsupportedFormatException
292    {
293        synchronized (importersPool)
294        {
295            if (!importersPool.isEmpty())
296                return importersPool.keySet().iterator().next();
297        }
298
299        for (SequencePosition pos : currentGroup.positions)
300        {
301            if (pos != null)
302                return pos.getPath();
303        }
304
305        return null;
306    }
307
308    /**
309     * Return an opened importer for given path.<br>
310     * Note that you should call {@link #releaseImporter(String, SequenceFileImporter)} when you're done with it.
311     * 
312     * @throws IOException
313     * @throws UnsupportedFormatException
314     * @see #releaseImporter(String, SequenceFileImporter)
315     */
316    @SuppressWarnings("resource")
317    public SequenceFileImporter getImporter(String path) throws IOException, UnsupportedFormatException
318    {
319        if (StringUtil.isEmpty(path))
320            return null;
321
322        try
323        {
324            final int numImporter;
325            SequenceFileImporter result;
326
327            synchronized (importersPool)
328            {
329                numImporter = importersPool.size();
330                result = importersPool.remove(path);
331            }
332
333            // no available importer for this path ?
334            if (result == null)
335            {
336                // we have already enough importers (we don't want to create too much of them)
337                if (numImporter >= MAX_IMPORTER)
338                {
339                    // recycle first importer found one
340                    result = getImporter(getFirstImporterPath());
341
342                    // correctly
343                    if (result != null)
344                        openImporter(result, path);
345                }
346
347                // need to create a new importer
348                if (result == null)
349                    result = createImporter(path);
350            }
351
352            return result;
353        }
354        catch (InstantiationException e)
355        {
356            // better to re-throw as RuntimeException
357            throw new RuntimeException(e.getMessage());
358        }
359        catch (IllegalAccessException e)
360        {
361            // better to re-throw as RuntimeException
362            throw new RuntimeException(e.getMessage());
363        }
364    }
365
366    /**
367     * Release the importer obtained through {@link #getImporter(String)} to the importer pool.
368     * 
369     * @see #getImporter(String)
370     */
371    public void releaseImporter(String path, SequenceFileImporter importer)
372    {
373        synchronized (importersPool)
374        {
375            // it's better to specify path instead of using SequenceFileImporter.getOpened() as internally format can change
376            importersPool.put(path, importer);
377        }
378    }
379
380    protected void buildIndexes()
381    {
382        final SequenceFileGroup group = currentGroup;
383        final SequenceType baseType = group.ident.baseType;
384
385        // compute index multipliers
386        indYMul = group.totalSizeX / baseType.sizeX;
387        indZMul = indYMul * (group.totalSizeY / baseType.sizeY);
388        indTMul = indZMul * (group.totalSizeZ / baseType.sizeZ);
389        indCMul = indTMul * (group.totalSizeT / baseType.sizeT);
390
391        final int totalLen = indCMul * (group.totalSizeC / baseType.sizeC);
392
393        // allocate index array
394        positions = new SequencePosition[totalLen];
395
396        // find maxX and maxY
397        for (SequencePosition pos : currentGroup.positions)
398        {
399            // compute index
400            final int ind = getIdIndex(pos.getIndexX(), pos.getIndexY(), pos.getIndexZ(), pos.getIndexT(),
401                    pos.getIndexC());
402            // set path for this index
403            positions[ind] = pos;
404        }
405    }
406
407    /**
408     * @return <code>true</code> if this image group requires several images to build a full XY plan image.
409     */
410    public boolean isStitchedImage()
411    {
412        if (!isOpen())
413            return false;
414
415        // if total size XY != single image size XY --> we have a stitched image
416        return (currentGroup.totalSizeX != currentGroup.ident.baseType.sizeX)
417                || (currentGroup.totalSizeY != currentGroup.ident.baseType.sizeY);
418    }
419
420    /**
421     * @return cursor for the given [z,t,c] position
422     */
423    protected FileCursor getCursor(int z, int t, int c)
424    {
425        final SequenceType baseType = currentGroup.ident.baseType;
426
427        final int externalZ = z / baseType.sizeZ;
428        final int externalT = t / baseType.sizeT;
429        final int externalC = c / baseType.sizeC;
430
431        final int internalZ = z % baseType.sizeZ;
432        final int internalT = t % baseType.sizeT;
433        final int internalC = c % baseType.sizeC;
434
435        final int idInd = getIdIndex(0, 0, externalZ, externalT, externalC);
436
437        return new FileCursor(positions[idInd], idInd, internalZ, internalT, internalC);
438    }
439
440    /**
441     * @return all required TileIndex for the given XY region
442     */
443    protected List<TileIndex> getTileIndexes(Rectangle xyRegion)
444    {
445        if (!isStitchedImage())
446            return CollectionUtil.createArrayList(new TileIndex(xyRegion, 0));
447
448        final List<TileIndex> result = new ArrayList<TileIndex>();
449
450        final SequenceType baseType = currentGroup.ident.baseType;
451        final int tsx = baseType.sizeX;
452        final int tsy = baseType.sizeY;
453
454        // get tile list
455        final List<Rectangle> tiles = ImageUtil.getTileList(xyRegion, tsx, tsy);
456
457        // each tile represent a single image
458        for (Rectangle tile : tiles)
459        {
460            // find the image index
461            final int x = tile.x / tsx;
462            final int y = tile.y / tsy;
463
464            // add tile info
465            result.add(new TileIndex(tile.intersection(xyRegion), x + (y * indYMul)));
466        }
467
468        return result;
469    }
470
471    /**
472     * @return file path for the given [z,t,c] position
473     */
474    public String getPath(int z, int t, int c)
475    {
476        if (!isOpen())
477            return "";
478
479        final SequencePosition pos = getCursor(z, t, c).position;
480
481        if (pos != null)
482            return pos.getPath();
483
484        return "";
485    }
486
487    protected int getIdIndex(int x, int y, int z, int t, int c)
488    {
489        return x + (y * indYMul) + (z * indZMul) + (t * indTMul) + (c * indCMul);
490    }
491
492    /**
493     * Close all opened internals importer without closing the Group Importer itself (importer remaing active)
494     */
495    public void closeInternalsImporters() throws IOException
496    {
497        synchronized (importersPool)
498        {
499            // close all importers
500            for (SequenceFileImporter imp : importersPool.values())
501                imp.close();
502
503            importersPool.clear();
504        }
505    }
506
507    @Override
508    public void close() throws IOException
509    {
510        closeInternalsImporters();
511
512        // release position indexes array
513        positions = null;
514        // release everything
515        currentMetadata = null;
516        currentGroup = null;
517    }
518
519    @SuppressWarnings("deprecation")
520    @Override
521    public OMEXMLMetadataImpl getMetaData() throws UnsupportedFormatException, IOException
522    {
523        return (OMEXMLMetadataImpl) getOMEXMLMetaData();
524    }
525
526    @Override
527    public OMEXMLMetadata getOMEXMLMetaData() throws UnsupportedFormatException, IOException
528    {
529        if (!isOpen())
530            return null;
531
532        // need to build metadata ?
533        if (currentMetadata == null)
534            currentMetadata = buildMetaData();
535
536        return currentMetadata;
537    }
538
539    @SuppressWarnings({"static-access", "resource"})
540    protected OMEXMLMetadata buildMetaData() throws UnsupportedFormatException, IOException
541    {
542        final SequenceFileGroup group = currentGroup;
543        final SequenceIdent ident = group.ident;
544        final SequenceType baseType = ident.baseType;
545        final String name = FileUtil.getFileName(ident.base, false);
546        final OMEXMLMetadata result = MetaDataUtil.createMetadata(name);
547
548        // minimum metadata
549        MetaDataUtil.setMetaData(result, currentGroup.totalSizeX, currentGroup.totalSizeY, currentGroup.totalSizeC,
550                currentGroup.totalSizeZ, currentGroup.totalSizeT, baseType.dataType, true);
551        // pixel size & time interval
552        if (baseType.pixelSizeX > 0d)
553            MetaDataUtil.setPixelSizeX(result, 0, baseType.pixelSizeX);
554        if (baseType.pixelSizeY > 0d)
555            MetaDataUtil.setPixelSizeY(result, 0, baseType.pixelSizeY);
556        if (baseType.pixelSizeZ > 0d)
557            MetaDataUtil.setPixelSizeZ(result, 0, baseType.pixelSizeZ);
558        if (baseType.timeInterval > 0d)
559            MetaDataUtil.setTimeInterval(result, 0, baseType.timeInterval);
560
561        // get OME Pixels object (easier to deal with)
562        final Pixels resultPixels = MetaDataUtil.getPixels(result, 0);
563        // allocate Plane objects
564        MetaDataUtil.ensurePlane(resultPixels, group.totalSizeT - 1, group.totalSizeZ - 1, group.totalSizeC - 1);
565
566        // default metadata
567        if ((openFlags & SequenceFileImporter.FLAG_METADATA_MASK) != SequenceFileImporter.FLAG_METADATA_MINIMUM)
568        {
569            // iterate over all dimension
570            for (int c = 0; c < group.totalSizeC; c++)
571            {
572                for (int z = 0; z < group.totalSizeZ; z++)
573                {
574                    for (int t = 0; t < group.totalSizeT; t++)
575                    {
576                        final FileCursor cursor = getCursor(z, t, c);
577                        final SequencePosition position = cursor.position;
578
579                        // do we have an image for this position ?
580                        if (position != null)
581                        {
582                            // first ZT plane ? --> fill channel data
583                            if ((z == 0) && (t == 0))
584                            {
585                                final SequenceFileImporter imp = getImporter(position.getPath());
586
587                                if (imp != null)
588                                {
589                                    try
590                                    {
591                                        // get metadata for this file
592                                        final OMEXMLMetadata meta = imp.getOMEXMLMetaData();
593                                        // get OME Pixels object (easier to deal with)
594                                        final Pixels metaPixels = MetaDataUtil.getPixels(meta, 0);
595
596                                        final Channel metaChannel;
597
598                                        // get origin channel (or create it if needed)
599                                        if (cursor.internalC < metaPixels.sizeOfChannelList())
600                                            metaChannel = metaPixels.getChannel(cursor.internalC);
601                                        else
602                                            metaChannel = new Channel();
603
604                                        // get Channel object directly from source metadata
605                                        resultPixels.setChannel(c, metaChannel);
606                                        // change the path
607                                        result.setChannelID(MetadataTools.createLSID("Channel", 0, c), 0, c);
608                                    }
609                                    finally
610                                    {
611                                        releaseImporter(position.getPath(), imp);
612                                    }
613                                }
614                            }
615
616                            Plane metaPlane = null;
617
618                            // first ZT plane or supplementary metadata wanted ? --> get original plane metadata
619                            if (((z == 0) && (t == 0)) || ((openFlags
620                                    & SequenceFileImporter.FLAG_METADATA_MASK) == SequenceFileImporter.FLAG_METADATA_ALL))
621                            {
622                                final SequenceFileImporter imp = getImporter(position.getPath());
623
624                                if (imp != null)
625                                {
626                                    try
627                                    {
628                                        // get metadata for this file
629                                        final OMEXMLMetadata meta = imp.getOMEXMLMetaData();
630                                        // get OME Pixels object (easier to deal with)
631                                        final Pixels metaPixels = MetaDataUtil.getPixels(meta, 0);
632
633                                        // retrieve the original Plane object
634                                        metaPlane = MetaDataUtil.getPlane(metaPixels, cursor.internalT,
635                                                cursor.internalZ, cursor.internalC);
636
637                                        if (metaPlane != null)
638                                        {
639                                            // remove linked annotation from plane
640                                            for (int a = (metaPlane.sizeOfLinkedAnnotationList() - 1); a >= 0; a--)
641                                                metaPlane.unlinkAnnotation(metaPlane.getLinkedAnnotation(a));
642                                        }
643                                    }
644                                    finally
645                                    {
646                                        releaseImporter(position.getPath(), imp);
647                                    }
648                                }
649                            }
650
651                            // plane not retrieved from original metadata ? --> create a new empty one
652                            if (metaPlane == null)
653                                metaPlane = new Plane();
654
655                            // retrieve plane index (use FormatsTools to get it as metadata is not yet complete here)
656                            final int resultPlaneInd = FormatTools.getIndex(
657                                    result.getPixelsDimensionOrder(0).getValue(), group.totalSizeZ, group.totalSizeC,
658                                    group.totalSizeT, group.totalSizeZ * group.totalSizeC * group.totalSizeT, z, c, t);
659
660                            // set plane
661                            resultPixels.setPlane(resultPlaneInd, metaPlane);
662
663                            // adjust CZT info
664                            metaPlane.setTheC(OMEUtil.getNonNegativeInteger(c));
665                            metaPlane.setTheZ(OMEUtil.getNonNegativeInteger(z));
666                            metaPlane.setTheT(OMEUtil.getNonNegativeInteger(t));
667                        }
668                    }
669                }
670            }
671        }
672
673        return result;
674    }
675
676    @SuppressWarnings("resource")
677    @Override
678    public int getTileHeight(int series) throws UnsupportedFormatException, IOException
679    {
680        if (!isOpen())
681            return 0;
682
683        // total size Y
684        final int tsy = currentGroup.totalSizeY;
685        // image size Y
686        final int isy = currentGroup.ident.baseType.sizeY;
687
688        // single image for XY plane or stitched image with sizeY > 2048 --> use image tile height
689        if ((tsy == isy) || (isy > 2048))
690        {
691            final String path = getFirstImporterPath();
692            final SequenceFileImporter imp = getImporter(path);
693
694            if (imp != null)
695            {
696                try
697                {
698                    return imp.getTileHeight(series);
699                }
700                finally
701                {
702                    releaseImporter(path, imp);
703                }
704            }
705        }
706
707        // use image size Y as tile height
708        return isy;
709    }
710
711    @SuppressWarnings("resource")
712    @Override
713    public int getTileWidth(int series) throws UnsupportedFormatException, IOException
714    {
715        if (!isOpen())
716            return 0;
717
718        // total size X
719        final int tsx = currentGroup.totalSizeX;
720        // image size X
721        final int isx = currentGroup.ident.baseType.sizeX;
722
723        // single image for XY plane or stitched image with sizeX > 2048 --> use image tile width
724        if ((tsx == isx) || (isx > 2048))
725        {
726            final String path = getFirstImporterPath();
727            final SequenceFileImporter imp = getImporter(path);
728
729            if (imp != null)
730            {
731                try
732                {
733                    return imp.getTileWidth(series);
734                }
735                finally
736                {
737                    releaseImporter(path, imp);
738                }
739            }
740        }
741
742        // use image size X as tile width
743        return isx;
744    }
745
746    // internal use only
747    @SuppressWarnings("resource")
748    private Object getPixelsInternal(SequencePosition pos, int series, int resolution, Rectangle region, int z, int t,
749            int c) throws UnsupportedFormatException, IOException
750    {
751        if (pos == null)
752        {
753            final SequenceType bt = currentGroup.ident.baseType;
754            System.err.println("SequenceIdGroupImporter.getPixelsInternal: no image for tile [" + (region.x / bt.sizeX)
755                    + "," + (region.y / bt.sizeY) + "] !");
756            return null;
757        }
758
759        // get importer for this image
760        final SequenceFileImporter imp = getImporter(pos.getPath());
761
762        if (imp == null)
763        {
764            System.err.println("SequenceIdGroupImporter.getPixelsInternal: cannot get importer for image '"
765                    + pos.getPath() + "' !");
766            return null;
767        }
768
769        try
770        {
771            // get pixels from importer (it actually represents a tile of the resulting image)
772            return imp.getPixels(series, resolution, region, z, t, c);
773        }
774        finally
775        {
776            // release importer
777            releaseImporter(pos.getPath(), imp);
778        }
779    }
780
781    @Override
782    public Object getPixels(int series, int resolution, Rectangle rectangle, int z, int t, int c)
783            throws UnsupportedFormatException, IOException
784    {
785        if (!isOpen())
786            return null;
787
788        final SequenceFileGroup group = currentGroup;
789        final SequenceIdent ident = group.ident;
790        final SequenceType baseType = ident.baseType;
791
792        // define XY region to load (original resolution)
793        Rectangle region = new Rectangle(group.totalSizeX, group.totalSizeY);
794        if (rectangle != null)
795            region = region.intersection(rectangle);
796
797        // get cursor and tile indexes
798        final FileCursor cursor = getCursor(z, t, c);
799        final List<TileIndex> tiles = getTileIndexes(region);
800
801        // single tile ?
802        if (tiles.size() == 1)
803            return getPixelsInternal(positions[cursor.index + tiles.get(0).index], series, resolution, region,
804                    cursor.internalZ, cursor.internalT, cursor.internalC);
805
806        // define XY region to load (wanted resolution)
807        final Rectangle finalRegion = new Rectangle(region.x >> resolution, region.y >> resolution,
808                region.width >> resolution, region.height >> resolution);
809
810        // multiple tiles, create result buffer
811        final Object result = Array1DUtil.createArray(baseType.dataType, finalRegion.width * finalRegion.height);
812        final boolean signed = baseType.dataType.isSigned();
813        // deltas to put tile to region origin
814        final int dx = -finalRegion.x;
815        final int dy = -finalRegion.y;
816
817        // each tile represent a single image
818        for (TileIndex tile : tiles)
819        {
820            // adjusted tile region
821            final Rectangle tileRegion = tile.region.intersection(region);
822            // get tile pixels
823            final Object pixels = getPixelsInternal(positions[cursor.index + tile.index], series, resolution,
824                    tileRegion, cursor.internalZ, cursor.internalT, cursor.internalC);
825
826            // cannot retrieve pixels for this tile ? --> ignore
827            if (pixels == null)
828                continue;
829
830            // tile region (wanted resolution)
831            final Rectangle finalTileRegion = new Rectangle(tileRegion.x >> resolution, tileRegion.y >> resolution,
832                    tileRegion.width >> resolution, tileRegion.height >> resolution);
833            // destination
834            final Point pt = finalTileRegion.getLocation();
835            pt.translate(dx, dy);
836
837            // copy tile to result
838            Array1DUtil.copyRect(pixels, finalTileRegion.getSize(), null, result, finalRegion.getSize(), pt, signed);
839        }
840
841        // return full region pixels object
842        return result;
843    }
844
845    // internal use only
846    private IcyBufferedImage getImageInternal(SequencePosition pos, int series, int resolution, Rectangle region, int z,
847            int t, int c) throws UnsupportedFormatException, IOException
848    {
849        if (pos == null)
850        {
851            final SequenceType bt = currentGroup.ident.baseType;
852            System.err.println("SequenceIdGroupImporter.getImageInternal: no image for tile [" + (region.x / bt.sizeX)
853                    + "," + (region.y / bt.sizeY) + "] !");
854            return null;
855        }
856
857        // get importer for this image
858        final SequenceFileImporter imp = getImporter(pos.getPath());
859
860        if (imp == null)
861        {
862            System.err.println("SequenceIdGroupImporter.getImageInternal: cannot get importer for image '"
863                    + pos.getPath() + "' !");
864            return null;
865        }
866
867        try
868        {
869            // get image from importer (it actually represents a tile of the resulting image)
870            return imp.getImage(series, resolution, region, z, t, c);
871        }
872        finally
873        {
874            // release importer
875            releaseImporter(pos.getPath(), imp);
876        }
877    }
878
879    // internal use only, at this point c cannot be -1
880    private IcyBufferedImage getImageInternal(int series, int resolution, Rectangle rectangle, int z, int t, int c)
881            throws UnsupportedFormatException, IOException
882    {
883        if (!isOpen())
884            return null;
885
886        final SequenceFileGroup group = currentGroup;
887        final SequenceIdent ident = group.ident;
888        final SequenceType baseType = ident.baseType;
889
890        // define XY region to load
891        Rectangle region = new Rectangle(group.totalSizeX, group.totalSizeY);
892        if (rectangle != null)
893            region = region.intersection(rectangle);
894
895        // get cursor and tile indexes
896        final FileCursor cursor = getCursor(z, t, c);
897        final List<TileIndex> tiles = getTileIndexes(region);
898
899        // single tile ?
900        if (tiles.size() == 1)
901            return getImageInternal(positions[cursor.index + tiles.get(0).index], series, resolution, region,
902                    cursor.internalZ, cursor.internalT, cursor.internalC);
903
904        // define XY region to load (wanted resolution)
905        final Rectangle finalRegion = new Rectangle(region.x >> resolution, region.y >> resolution,
906                region.width >> resolution, region.height >> resolution);
907
908        // multiple tiles, create result image
909        final IcyBufferedImage result = new IcyBufferedImage(finalRegion.width, finalRegion.height, 1,
910                baseType.dataType);
911        // colormap save
912        IcyColorMap colormap = null;
913        // deltas to put tile to region origin
914        final int dx = -finalRegion.x;
915        final int dy = -finalRegion.y;
916
917        // each tile represent a single image
918        for (TileIndex tile : tiles)
919        {
920            // adjusted tile region
921            final Rectangle tileRegion = tile.region.intersection(region);
922            // get tile pixels
923            final IcyBufferedImage image = getImageInternal(positions[cursor.index + tile.index], series, resolution,
924                    tileRegion, cursor.internalZ, cursor.internalT, cursor.internalC);
925
926            // cannot retrieve pixels for this tile ? --> ignore
927            if (image == null)
928                continue;
929
930            // store colormap
931            if (colormap == null)
932                colormap = image.getColorMap(0);
933
934            // destination
935            final Point pt = new Point(tileRegion.x >> resolution, tileRegion.y >> resolution);
936            pt.translate(dx, dy);
937
938            // copy tile to image result
939            result.copyData(image, null, pt);
940        }
941
942        // set colormap
943        if (colormap != null)
944            result.setColorMap(0, colormap);
945
946        // return full image region
947        return result;
948    }
949
950    @Override
951    public IcyBufferedImage getImage(int series, int resolution, Rectangle rectangle, int z, int t, int c)
952            throws UnsupportedFormatException, IOException
953    {
954        if (!isOpen())
955            return null;
956
957        final SequenceFileGroup group = currentGroup;
958        final int sizeC = (c == -1) ? group.totalSizeC : 1;
959        final List<IcyBufferedImage> result = new ArrayList<IcyBufferedImage>();
960
961        // multi channel ?
962        if (sizeC > 1)
963        {
964            // handle channel independently we can have channel in separate file
965            for (int ch = 0; ch < sizeC; ch++)
966                result.add(getImageInternal(series, resolution, rectangle, z, t, ch));
967        }
968        // single channel
969        else
970            result.add(getImageInternal(series, resolution, rectangle, z, t, (c == -1) ? 0 : c));
971
972        // we have a null image in the result ?
973        if (result.contains(null))
974        {
975            final SequenceIdent ident = group.ident;
976            final SequenceType baseType = ident.baseType;
977
978            // define XY region to load
979            Rectangle region = new Rectangle(group.totalSizeX, group.totalSizeY);
980            if (rectangle != null)
981                region = region.intersection(rectangle);
982
983            final int sizeX = region.width >> resolution;
984            final int sizeY = region.height >> resolution;
985
986            // replace null by empty image
987            for (int i = 0; i < result.size(); i++)
988                if (result.get(i) == null)
989                    result.set(i, new IcyBufferedImage(sizeX, sizeY, 1, baseType.dataType));
990        }
991
992        // then build a single image from all channels
993        return IcyBufferedImage.createFrom(result);
994    }
995
996    @SuppressWarnings("resource")
997    @Override
998    public IcyBufferedImage getThumbnail(int series) throws UnsupportedFormatException, IOException
999    {
1000        final OMEXMLMetadata meta = getOMEXMLMetaData();
1001
1002        // probably no group opened
1003        if (meta == null)
1004            return null;
1005
1006        // stitched image ? --> use default implementation (downscale middle image)
1007        if (isStitchedImage())
1008            return super.getThumbnail(series);
1009
1010        final int sizeZ = MetaDataUtil.getSizeZ(meta, series);
1011        final int sizeT = MetaDataUtil.getSizeT(meta, series);
1012
1013        // get cursor for image at middle position
1014        final FileCursor cursor = getCursor(sizeZ / 2, sizeT / 2, 0);
1015        final String path;
1016
1017        if (cursor.position != null)
1018            // get path for this image
1019            path = cursor.position.getPath();
1020        else
1021            // no image for this position ? --> get first available path
1022            path = getFirstImporterPath();
1023
1024        final SequenceFileImporter imp = getImporter(path);
1025
1026        if (imp == null)
1027        {
1028            // should not happen
1029            System.err.println("SequenceIdGroupImporter.getThumbnail: cannot find importer...");
1030            return null;
1031        }
1032
1033        try
1034        {
1035            return imp.getThumbnail(series);
1036        }
1037        finally
1038        {
1039            releaseImporter(path, imp);
1040        }
1041    }
1042}