diff --git a/screening/dist/etc/service.properties b/screening/dist/etc/service.properties
index 6901f3595d13c171207ad897f6baf6c5f358c055..0fa1001749a7d6ae2541b6a062bd0170b6c5a521 100644
--- a/screening/dist/etc/service.properties
+++ b/screening/dist/etc/service.properties
@@ -137,6 +137,23 @@ tabular-data-graph-servlet.properties-file = etc/tabular-data-graph.properties
 screening-dss-api-exporter-servlet.class = ch.systemsx.cisd.openbis.dss.generic.server.DssScreeningApiServlet
 screening-dss-api-exporter-servlet.path = /rmi-datastore-server-screening-api-v1/*
 
+# ---------------------------------------------------------------------------
+# image overview plugins configuration
+# ---------------------------------------------------------------------------
+
+# Comma separated names of image overview plugins. 
+# Each plugin should have configuration properties prefixed with its name.
+# Generic properties for each <plugin> include: 
+#   <plugin>.class   - Fully qualified plugin class name (mandatory).
+#   <plugin>.default - If true all data set types not handled by other plugins should be handled 
+#                      by the plugin (default = false). 
+#   <plugin>.dataset-types - Comma separated list of data set types handled by the plugin 
+#                      (optional and ignored if default is true, otherwise mandatory). 
+overview-plugins = microscopy-image-overview
+
+microscopy-image-overview.class = ch.systemsx.cisd.openbis.dss.generic.server.MergingImagesDownloadServlet
+microscopy-image-overview.dataset-types = MICROSCOPY_IMAGE
+
 # ---------------------------------------------------------------------------
 
 maintenance-plugins=data-set-clean-up
@@ -289,3 +306,51 @@ image-analysis-results.storage-processor.ignore-comments = true
 image-analysis-results.storage-processor.well-name-row = row
 image-analysis-results.storage-processor.well-name-col = col
 image-analysis-results.storage-processor.well-name-col-is-alphanum = true
+
+# --- Example configuration of a dropbox for images which are not connected to wells on the plate
+
+# The directory to watch for incoming data.
+#microscopy-dropbox.incoming-dir = ${incoming-root-dir}/incoming-microscopy
+#microscopy-dropbox.incoming-data-completeness-condition = auto-detection
+
+# The extractor class to use for code extraction
+#microscopy-dropbox.data-set-info-extractor = ch.systemsx.cisd.etlserver.DefaultDataSetInfoExtractor
+#microscopy-dropbox.data-set-info-extractor.entity-separator = .
+#microscopy-dropbox.data-set-info-extractor.index-of-sample-code = 0
+#microscopy-dropbox.data-set-info-extractor.space-code = ${import-space-code}
+
+# The extractor class to use for type extraction
+#microscopy-dropbox.type-extractor = ch.systemsx.cisd.etlserver.SimpleTypeExtractor
+#microscopy-dropbox.type-extractor.file-format-type = TIFF
+#microscopy-dropbox.type-extractor.locator-type = RELATIVE_LOCATION
+#microscopy-dropbox.type-extractor.data-set-type = MICROSCOPY_IMAGE
+#microscopy-dropbox.type-extractor.is-measured = true
+
+#microscopy-dropbox.storage-processor = ch.systemsx.cisd.openbis.dss.etl.MicroscopyStorageProcessor
+#microscopy-dropbox.storage-processor.file-extractor = ch.systemsx.cisd.openbis.dss.etl.MicroscopyImageFileExtractor
+#microscopy-dropbox.storage-processor.data-source = imaging-db
+#microscopy-dropbox.storage-processor.channel-names = BLUE, GREEN, RED
+#microscopy-dropbox.storage-processor.well_geometry = 2x3
+#microscopy-dropbox.storage-processor.tile_mapping = 1,2,3;4,5,6
+
+# --- Microscopy dropbox with a series of images with any names ---------------------------
+
+# The directory to watch for incoming data.
+#microscopy-series-dropbox.incoming-dir = ${incoming-root-dir}/incoming-microscopy-series
+#microscopy-series-dropbox.incoming-data-completeness-condition = auto-detection
+
+# The extractor class to use for code extraction
+#microscopy-series-dropbox.data-set-info-extractor = ch.systemsx.cisd.etlserver.DefaultDataSetInfoExtractor
+#microscopy-series-dropbox.data-set-info-extractor.entity-separator = .
+#microscopy-series-dropbox.data-set-info-extractor.index-of-sample-code = 0
+#microscopy-series-dropbox.data-set-info-extractor.space-code = ${import-space-code}
+
+# The extractor class to use for type extraction
+#microscopy-series-dropbox.type-extractor = ch.systemsx.cisd.etlserver.SimpleTypeExtractor
+#microscopy-series-dropbox.type-extractor.file-format-type = TIFF
+#microscopy-series-dropbox.type-extractor.locator-type = RELATIVE_LOCATION
+#microscopy-series-dropbox.type-extractor.data-set-type = MICROSCOPY_IMAGE
+#microscopy-series-dropbox.type-extractor.is-measured = true
+
+#microscopy-series-dropbox.storage-processor = ch.systemsx.cisd.openbis.dss.etl.MicroscopyBlackboxSeriesStorageProcessor
+#microscopy-series-dropbox.storage-processor.data-source = imaging-db
diff --git a/screening/dist/server/web-client.properties b/screening/dist/server/web-client.properties
index ae923f04e69c29cb0d43c19d4a9a148ce9e89f94..58d0637fdfd83c1fc2c81cea5e0cade6bb87c6f5 100644
--- a/screening/dist/server/web-client.properties
+++ b/screening/dist/server/web-client.properties
@@ -3,6 +3,12 @@
 #
 #default-view-mode = SIMPLE
 
+# (optional) List of data set types for which there should be an image overview shown in dataset tables.
+# If not specified image overview will not be shown for any datasets 
+# even if some overview plugins have been configured. 
+#
+data-set-types-with-image-overview = MICROSCOPY_IMAGE
+
 # Configuration of entity (experiment, sample, data set, material) detail views.
 # Allows to hide chosen sections.
 #
@@ -19,6 +25,7 @@
 #   	data-set-parents-section
 #   	data-set-children-section
 #    	plate-layout-dataset-section
+#     logical-image-dataset-section (valid only for microscopy datasets)
 #			query-section
 #   generic_experiment_viewer
 #   	data-sets-section
@@ -35,11 +42,12 @@
 #   	data-sets-section
 #   	attachment-section
 #			query-section
+#     logical-image-well-section (valid only for HCS well samples)
 #   generic_material_viewer
 #   	plate-locations-material-section
 #			query-section
 #
-detail-views = plate-or-well-view, image-data-view, image-analysis-data-view 
+detail-views = plate-or-well-view, image-data-view, image-analysis-data-view, microscopy-dataset-view
 
 #experiment-view.view = generic_experiment_viewer
 #experiment-view.types = SIRNA_HCS
@@ -61,5 +69,11 @@ image-analysis-data-view.hide-sections = data-set-parents-section, data-set-chil
 image-analysis-data-view.hide-smart-view = true
 image-analysis-data-view.hide-file-view = true
 
+microscopy-dataset-view.view = generic_dataset_viewer
+microscopy-dataset-view.types = MICROSCOPY_IMAGE
+microscopy-dataset-view.hide-sections = data-set-children-section, data-set-parents-section
+microscopy-dataset-view.hide-smart-view = false
+microscopy-dataset-view.hide-file-view = true
+
 technologies = screening
-#screening.image-viewer-enabled = true
+screening.image-viewer-enabled = true
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AbstractImageDatasetUploader.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AbstractImageDatasetUploader.java
index 6350058ffff0664af20d1013f50ae5e598486f21..32d0e9b160ec38fffcf42953cefe8d3a89132fea 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AbstractImageDatasetUploader.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AbstractImageDatasetUploader.java
@@ -17,12 +17,16 @@
 package ch.systemsx.cisd.openbis.dss.etl;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
 
+import ch.rinn.restrictions.Private;
 import ch.systemsx.cisd.openbis.dss.etl.ImagingDatabaseHelper.ImagingChannelsMap;
 import ch.systemsx.cisd.openbis.dss.etl.dataaccess.IImagingQueryDAO;
 import ch.systemsx.cisd.openbis.plugin.screening.shared.imaging.dataaccess.ImgAcquiredImageDTO;
@@ -105,10 +109,23 @@ abstract class AbstractImageDatasetUploader
             stackImages.add(makeAcquiredImageInStack(image));
             map.put(stackDTO, stackImages);
         }
-        setChannelStackIds(map.keySet());
+        Set<ImgChannelStackDTO> channelStacks = map.keySet();
+        setChannelStackIds(channelStacks);
+        setChannelStackRepresentatives(channelStacks);
         return map;
     }
 
+    private void setChannelStackRepresentatives(Set<ImgChannelStackDTO> channelStacks)
+    {
+        Set<ImgChannelStackDTO> representatives =
+                ChannelStackRepresentativesOracle.calculateRepresentatives(channelStacks);
+        for (ImgChannelStackDTO channelStack : channelStacks)
+        {
+            boolean isRepresentative = representatives.contains(channelStack);
+            channelStack.setRepresentative(isRepresentative);
+        }
+    }
+
     private void setChannelStackIds(Set<ImgChannelStackDTO> channelStacks)
     {
         for (ImgChannelStackDTO channelStack : channelStacks)
@@ -129,7 +146,8 @@ abstract class AbstractImageDatasetUploader
         Long spotId = spotProvider.tryGetSpotId(image);
         int dummyId = 0;
         return new ImgChannelStackDTO(dummyId, image.getTileRow(), image.getTileColumn(),
-                datasetId, spotId, image.tryGetTimePoint(), image.tryGetDepth());
+                datasetId, spotId, image.tryGetTimePoint(), image.tryGetDepth(),
+                image.tryGetSeriesNumber(), false);
     }
 
     private void createImages(Map<ImgChannelStackDTO, List<AcquiredImageInStack>> stackImagesMap,
@@ -229,4 +247,86 @@ abstract class AbstractImageDatasetUploader
                         imageReferenceOrNull.tryGetColorComponent());
         return dto;
     }
+
+    @Private
+    static final class ChannelStackRepresentativesOracle
+    {
+        /**
+         * For all images of one spot chooses the 'smallest' element as an representative. If there
+         * are no spots than the smallest element of all specified images is chosen.
+         */
+        public static Set<ImgChannelStackDTO> calculateRepresentatives(
+                Set<ImgChannelStackDTO> images)
+        {
+            Map<Long/* spot or null */, List<ImgChannelStackDTO>> mapBySpot =
+                    createMapBySpot(images);
+            Comparator<? super ImgChannelStackDTO> spotChannelStacksComparator =
+                    createChannelStacksComparator();
+            for (List<ImgChannelStackDTO> spotChannelStacks : mapBySpot.values())
+            {
+                Collections.sort(spotChannelStacks, spotChannelStacksComparator);
+            }
+
+            Set<ImgChannelStackDTO> representatives = new HashSet<ImgChannelStackDTO>();
+            for (List<ImgChannelStackDTO> spotChannelStacks : mapBySpot.values())
+            {
+                representatives.add(spotChannelStacks.get(0));
+            }
+            return representatives;
+        }
+
+        private static Comparator<? super ImgChannelStackDTO> createChannelStacksComparator()
+        {
+            return new Comparator<ImgChannelStackDTO>()
+                {
+                    public int compare(ImgChannelStackDTO o1, ImgChannelStackDTO o2)
+                    {
+                        int cmp = compareNullable(o1.getRow(), o2.getRow());
+                        if (cmp != 0)
+                            return cmp;
+                        cmp = compareNullable(o1.getColumn(), o2.getColumn());
+                        if (cmp != 0)
+                            return cmp;
+                        cmp = compareNullable(o1.getT(), o2.getT());
+                        if (cmp != 0)
+                            return cmp;
+                        cmp = compareNullable(o1.getZ(), o2.getZ());
+                        if (cmp != 0)
+                            return cmp;
+                        cmp = compareNullable(o1.getSeriesNumber(), o2.getSeriesNumber());
+                        return cmp;
+                    }
+
+                    private <T extends Comparable<T>> int compareNullable(T v1OrNull, T v2OrNull)
+                    {
+                        if (v1OrNull == null)
+                        {
+                            return v2OrNull == null ? 0 : -1;
+                        } else
+                        {
+                            return v2OrNull == null ? 1 : v1OrNull.compareTo(v2OrNull);
+                        }
+                    }
+                };
+        }
+
+        private static Map<Long/* spot or null */, List<ImgChannelStackDTO>> createMapBySpot(
+                Set<ImgChannelStackDTO> channelStacks)
+        {
+            Map<Long, List<ImgChannelStackDTO>> mapBySpot =
+                    new HashMap<Long, List<ImgChannelStackDTO>>();
+            for (ImgChannelStackDTO channelStack : channelStacks)
+            {
+                Long spotId = channelStack.getSpotId();
+                List<ImgChannelStackDTO> spotChannelStacks = mapBySpot.get(spotId);
+                if (spotChannelStacks == null)
+                {
+                    spotChannelStacks = new ArrayList<ImgChannelStackDTO>();
+                }
+                spotChannelStacks.add(channelStack);
+                mapBySpot.put(spotId, spotChannelStacks);
+            }
+            return mapBySpot;
+        }
+    }
 }
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AbstractImageFileExtractor.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AbstractImageFileExtractor.java
index 90ff5dcebaafc6d2ab5242a45c784604583eb039..572255e05ab8f1bd9f0c0d72fdc96c35d00cf795 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AbstractImageFileExtractor.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AbstractImageFileExtractor.java
@@ -96,12 +96,21 @@ abstract public class AbstractImageFileExtractor implements IImageFileExtractor
 
     protected final Geometry wellGeometry;
 
-    protected AbstractImageFileExtractor(final Properties properties)
+    protected AbstractImageFileExtractor(Properties properties)
     {
-        this.channelDescriptions = tryExtractChannelDescriptions(properties);
+        this(extractChannelDescriptions(properties), getWellGeometry(properties), properties);
+    }
+
+    protected AbstractImageFileExtractor(List<ChannelDescription> channelDescriptions,
+            Geometry wellGeometry, Properties properties)
+    {
+        assert wellGeometry != null : "wel geometry is null";
+        assert channelDescriptions != null : "channelDescriptions is null";
+
+        this.channelDescriptions = channelDescriptions;
+        this.wellGeometry = wellGeometry;
         this.channelColorComponentsOrNull = tryGetChannelComponents(properties);
         checkChannelsAndColorComponents();
-        this.wellGeometry = getWellGeometry(properties);
         this.tileMapperOrNull =
                 TileMapper.tryCreate(properties.getProperty(TILE_MAPPING_PROPERTY), wellGeometry);
     }
@@ -252,7 +261,7 @@ abstract public class AbstractImageFileExtractor implements IImageFileExtractor
         return channels;
     }
 
-    protected final static List<ChannelDescription> tryExtractChannelDescriptions(
+    protected final static List<ChannelDescription> extractChannelDescriptions(
             final Properties properties)
     {
         return PlateStorageProcessor.extractChannelDescriptions(properties);
@@ -289,7 +298,7 @@ abstract public class AbstractImageFileExtractor implements IImageFileExtractor
                         colorComponentOrNull);
         return new AcquiredSingleImage(imageInfo.tryGetWellLocation(), imageInfo.getTileLocation(),
                 imageInfo.getChannelCode(), imageInfo.tryGetTimepoint(), imageInfo.tryGetDepth(),
-                relativeImageRef);
+                imageInfo.tryGetSeriesNumber(), relativeImageRef);
     }
 
     protected static Integer tryAsInt(String valueOrNull)
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AbstractImageStorageProcessor.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AbstractImageStorageProcessor.java
index e2dc3e06eb01982c271aac3a7ef9eaa229435930..506221fdea355304766a11ceee86ce2227a07dbd 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AbstractImageStorageProcessor.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AbstractImageStorageProcessor.java
@@ -184,28 +184,42 @@ abstract class AbstractImageStorageProcessor extends AbstractStorageProcessor
     // ---
 
     public AbstractImageStorageProcessor(final Properties properties)
+    {
+        this(getMandatorySpotGeometry(properties), extractChannelDescriptions(properties),
+                tryCreateImageExtractor(properties), properties);
+    }
+
+    protected AbstractImageStorageProcessor(Geometry spotGeometry,
+            List<ChannelDescription> channelDescriptions, IImageFileExtractor imageFileExtractor,
+            Properties properties)
     {
         super(properties);
-        String spotGeometryText = getMandatoryProperty(SPOT_GEOMETRY_PROPERTY);
-        this.spotGeometry = Geometry.createFromString(spotGeometryText);
-        channelDescriptions = extractChannelDescriptions(properties);
-        thumbnailMaxWidth =
+        this.spotGeometry = spotGeometry;
+        this.channelDescriptions = channelDescriptions;
+        this.imageFileExtractor = imageFileExtractor;
+        this.thumbnailMaxWidth =
                 PropertyUtils.getInt(properties, THUMBNAIL_MAX_WIDTH_PROPERTY,
                         DEFAULT_THUMBNAIL_MAX_WIDTH);
-        thumbnailMaxHeight =
+        this.thumbnailMaxHeight =
                 PropertyUtils.getInt(properties, THUMBNAIL_MAX_HEIGHT_PROPERTY,
                         DEFAULT_THUMBNAIL_MAX_HEIGHT);
-        generateThumbnails =
+        this.generateThumbnails =
                 PropertyUtils.getBoolean(properties, GENERATE_THUMBNAILS_PROPERTY, false);
-        areThumbnailsCompressed =
+        this.areThumbnailsCompressed =
                 PropertyUtils.getBoolean(properties, COMPRESS_THUMBNAILS_PROPERTY, false);
-        originalDataStorageFormat = getOriginalDataStorageFormat(properties);
+        this.originalDataStorageFormat = getOriginalDataStorageFormat(properties);
 
-        this.imageFileExtractor = tryCreateImageExtractor(properties);
         this.dataSource = ServiceProvider.getDataSourceProvider().getDataSource(properties);
         this.currentTransaction = null;
     }
 
+    private static Geometry getMandatorySpotGeometry(Properties properties)
+    {
+        String spotGeometryText =
+                PropertyUtils.getMandatoryProperty(properties, SPOT_GEOMETRY_PROPERTY);
+        return Geometry.createFromString(spotGeometryText);
+    }
+
     private static IImageFileExtractor tryCreateImageExtractor(final Properties properties)
     {
         String fileExtractorClass = PropertyUtils.getProperty(properties, FILE_EXTRACTOR_PROPERTY);
@@ -720,7 +734,8 @@ abstract class AbstractImageStorageProcessor extends AbstractStorageProcessor
     {
         for (AcquiredSingleImage image : images)
         {
-            if (image.tryGetTimePoint() != null || image.tryGetDepth() != null)
+            if (image.tryGetTimePoint() != null || image.tryGetDepth() != null
+                    || image.tryGetSeriesNumber() != null)
             {
                 return true;
             }
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AcquiredSingleImage.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AcquiredSingleImage.java
index 2bc1f80f4298c632a98515a4ceac32b28f51def5..745daa8167c6a7257d2d8db5fa7f9385309031a4 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AcquiredSingleImage.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/AcquiredSingleImage.java
@@ -37,13 +37,16 @@ public class AcquiredSingleImage extends AbstractHashable
     // can be null
     private final Float timePointOrNull, depthOrNull;
 
+    // can be null
+    private Integer seriesNumberOrNull;
+
     private final RelativeImageReference imageFilePath; // relative to the original dataset
                                                         // directory
 
     private RelativeImageReference thumbnailFilePathOrNull;
 
     public AcquiredSingleImage(Location wellLocationOrNull, Location tileLocation,
-            String channelCode, Float timePointOrNull, Float depthOrNull,
+            String channelCode, Float timePointOrNull, Float depthOrNull, Integer seriesNumberOrNull,
             RelativeImageReference imageFilePath)
     {
         this.wellLocationOrNull = wellLocationOrNull;
@@ -51,9 +54,15 @@ public class AcquiredSingleImage extends AbstractHashable
         this.channelCode = channelCode.toUpperCase();
         this.timePointOrNull = timePointOrNull;
         this.depthOrNull = depthOrNull;
+        this.seriesNumberOrNull = seriesNumberOrNull;
         this.imageFilePath = imageFilePath;
     }
 
+    public Location tryGetWellLocation()
+    {
+        return wellLocationOrNull;
+    }
+
     /** Valid only in HCS case, do not call this method for microscopy images. */
     public int getWellRow()
     {
@@ -103,9 +112,20 @@ public class AcquiredSingleImage extends AbstractHashable
         return thumbnailFilePathOrNull;
     }
 
+    public Integer tryGetSeriesNumber()
+    {
+        return seriesNumberOrNull;
+    }
+
+    // ---- setters
+
     public final void setThumbnailFilePathOrNull(RelativeImageReference thumbnailFilePathOrNull)
     {
         this.thumbnailFilePathOrNull = thumbnailFilePathOrNull;
     }
 
+    public void setSeriesNumber(int seriesNumber)
+    {
+        this.seriesNumberOrNull = seriesNumber;
+    }
 }
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/HCSImageCheckList.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/HCSImageCheckList.java
index 43a590c245855954cba063d4e5ca8e9f1a58ae3c..05ebadacd74581458d29732fb4e6f2258c51ee70 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/HCSImageCheckList.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/HCSImageCheckList.java
@@ -23,11 +23,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import org.apache.log4j.Logger;
-
 import ch.systemsx.cisd.bds.hcs.Geometry;
-import ch.systemsx.cisd.common.logging.LogCategory;
-import ch.systemsx.cisd.common.logging.LogFactory;
 import ch.systemsx.cisd.common.utilities.AbstractHashable;
 import ch.systemsx.cisd.openbis.plugin.screening.shared.dto.PlateDimension;
 
@@ -42,9 +38,6 @@ import ch.systemsx.cisd.openbis.plugin.screening.shared.dto.PlateDimension;
  */
 public final class HCSImageCheckList
 {
-    private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION,
-            HCSImageCheckList.class);
-
     private final Map<FullLocation, Check> imageMap;
 
     public HCSImageCheckList(final List<String> channelCodes, final PlateDimension plateGeometry,
@@ -95,16 +88,12 @@ public final class HCSImageCheckList
         }
         Float timepointOrNull = image.tryGetTimePoint();
         Float depthOrNull = image.tryGetDepth();
-        if (check.isCheckedOff(timepointOrNull, depthOrNull))
+        Integer seriesNumberOrNull = image.tryGetSeriesNumber();
+        if (check.isCheckedOff(timepointOrNull, depthOrNull, seriesNumberOrNull))
         {
             throw new IllegalArgumentException("Image already handled: " + image);
         }
-        if (operationLog.isDebugEnabled())
-        {
-            operationLog.debug("Checking location " + location
-                    + (timepointOrNull == null ? "" : " timepoint " + timepointOrNull));
-        }
-        check.checkOff(timepointOrNull, depthOrNull);
+        check.checkOff(timepointOrNull, depthOrNull, seriesNumberOrNull);
     }
 
     private static FullLocation createLocation(AcquiredSingleImage image)
@@ -118,7 +107,7 @@ public final class HCSImageCheckList
         final List<FullLocation> fullLocations = new ArrayList<FullLocation>();
         for (final Map.Entry<FullLocation, Check> entry : imageMap.entrySet())
         {
-            if (entry.getValue().isCheckedOff(null, null) == false)
+            if (entry.getValue().isCheckedOff(null, null, null) == false)
             {
                 fullLocations.add(entry.getKey());
             }
@@ -136,10 +125,13 @@ public final class HCSImageCheckList
 
         private final Float depthOrNull;
 
-        public CheckDimension(Float timeOrNull, Float depthOrNull)
+        private final Integer seriesNumberOrNull;
+
+        public CheckDimension(Float timeOrNull, Float depthOrNull, Integer seriesNumberOrNull)
         {
             this.timeOrNull = timeOrNull;
             this.depthOrNull = depthOrNull;
+            this.seriesNumberOrNull = seriesNumberOrNull;
         }
 
         @Override
@@ -149,6 +141,9 @@ public final class HCSImageCheckList
             int result = 1;
             result = prime * result + ((depthOrNull == null) ? 0 : depthOrNull.hashCode());
             result = prime * result + ((timeOrNull == null) ? 0 : timeOrNull.hashCode());
+            result =
+                    prime * result
+                            + ((seriesNumberOrNull == null) ? 0 : seriesNumberOrNull.hashCode());
             return result;
         }
 
@@ -174,6 +169,12 @@ public final class HCSImageCheckList
                     return false;
             } else if (!timeOrNull.equals(other.timeOrNull))
                 return false;
+            if (seriesNumberOrNull == null)
+            {
+                if (other.seriesNumberOrNull != null)
+                    return false;
+            } else if (!seriesNumberOrNull.equals(other.seriesNumberOrNull))
+                return false;
             return true;
         }
     }
@@ -184,18 +185,19 @@ public final class HCSImageCheckList
 
         private final Set<CheckDimension> dimensions = new HashSet<CheckDimension>();
 
-        final void checkOff(Float timepointOrNull, Float depthOrNull)
+        final void checkOff(Float timepointOrNull, Float depthOrNull, Integer seriesNumberOrNull)
         {
-            dimensions.add(new CheckDimension(timepointOrNull, depthOrNull));
+            dimensions.add(new CheckDimension(timepointOrNull, depthOrNull, seriesNumberOrNull));
             checkedOff = true;
         }
 
-        final boolean isCheckedOff(Float timepointOrNull, Float depthOrNull)
+        final boolean isCheckedOff(Float timepointOrNull, Float depthOrNull,
+                Integer seriesNumberOrNull)
         {
             CheckDimension dim = null;
-            if (timepointOrNull != null || depthOrNull != null)
+            if (timepointOrNull != null || depthOrNull != null || seriesNumberOrNull != null)
             {
-                dim = new CheckDimension(timepointOrNull, depthOrNull);
+                dim = new CheckDimension(timepointOrNull, depthOrNull, seriesNumberOrNull);
             }
             return checkedOff && (dim == null || dimensions.contains(dim));
         }
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/HCSImageFileExtractor.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/HCSImageFileExtractor.java
index b02502a15e9034811c2812843a17f7052c82b92c..adcaf45b70636e3f0f67a5751f21e613728caaa2 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/HCSImageFileExtractor.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/HCSImageFileExtractor.java
@@ -172,9 +172,10 @@ public class HCSImageFileExtractor extends AbstractImageFileExtractor
 
         Float timepointOrNull = tryAsFloat(unparsedInfo.getTimepointToken());
         Float depthOrNull = tryAsFloat(unparsedInfo.getDepthToken());
+        Integer seriesNumberOrNull = tryAsInt(unparsedInfo.getSeriesNumberToken());
         String imageRelativePath = getRelativeImagePath(incomingDataSetDirectory, imageFile);
 
         return new ImageFileInfo(wellLocation, channelCode, tileLocation, imageRelativePath,
-                timepointOrNull, depthOrNull);
+                timepointOrNull, depthOrNull, seriesNumberOrNull);
     }
 }
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/IImagingDatasetLoader.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/IImagingDatasetLoader.java
index 0fcc5948895a954df41e0fdac9f07094da8f9cc4..79a3ed836795619744c63b18e13c7178b473e94b 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/IImagingDatasetLoader.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/IImagingDatasetLoader.java
@@ -16,6 +16,7 @@
 
 package ch.systemsx.cisd.openbis.dss.etl;
 
+import ch.systemsx.cisd.bds.hcs.Location;
 import ch.systemsx.cisd.openbis.dss.generic.server.images.ImageChannelStackReference;
 import ch.systemsx.cisd.openbis.dss.generic.shared.dto.Size;
 import ch.systemsx.cisd.openbis.plugin.screening.shared.imaging.IImageDatasetLoader;
@@ -34,4 +35,16 @@ public interface IImagingDatasetLoader extends IImageDatasetLoader
      */
     AbsoluteImageReference tryGetImage(String chosenChannelCode,
             ImageChannelStackReference channelStackReference, Size thumbnailSizeOrNull);
+
+    /**
+     * Finds representative image of this dataset in a given channel.
+     * 
+     * @param channelCode channel code for which representative image is requested
+     * @param wellLocationOrNull if not null the returned images are restricted to one well.
+     *            Otherwise the dataset is assumed to have no container and spots.
+     * @param thumbnailSizeOrNull if not null the thumbnail in th especified size will be returned.
+     */
+    AbsoluteImageReference tryGetRepresentativeImage(String channelCode,
+            Location wellLocationOrNull, Size thumbnailSizeOrNull);
+
 }
\ No newline at end of file
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/MicroscopyBlackboxSeriesStorageProcessor.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/MicroscopyBlackboxSeriesStorageProcessor.java
new file mode 100644
index 0000000000000000000000000000000000000000..c0ea63d0ab1963fdb3179767e0ebb0143f89060c
--- /dev/null
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/MicroscopyBlackboxSeriesStorageProcessor.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2010 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.openbis.dss.etl;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Properties;
+
+import ch.systemsx.cisd.bds.hcs.Geometry;
+import ch.systemsx.cisd.bds.hcs.Location;
+import ch.systemsx.cisd.common.mail.IMailClient;
+import ch.systemsx.cisd.openbis.dss.etl.dataaccess.IImagingQueryDAO;
+import ch.systemsx.cisd.openbis.dss.etl.dto.ImageFileInfo;
+import ch.systemsx.cisd.openbis.dss.generic.shared.dto.DataSetInformation;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifier;
+import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.ChannelDescription;
+
+/**
+ * Storage processor which stores microscopy images in a special-purpose imaging database. Image
+ * files do not have to adhere to any naming convention, it's assumed that there is exactly one
+ * tile, one channel, no time or depth dimensions. Images are sorted lexicographically and
+ * subsequent series numbers are assigned to them.
+ * <p>
+ * In this storage processor one can set neither well geometry, channels nor image file extractor.<br>
+ * See {@link AbstractImageStorageProcessor} documentation to check how to configure thumbnails
+ * generation.
+ * 
+ * @author Tomasz Pylak
+ */
+public class MicroscopyBlackboxSeriesStorageProcessor extends AbstractImageStorageProcessor
+{
+    private static final String DEFAULT_CHANNEL_CODE = "DEFAULT";
+
+    private static final String DEFAULT_CHANNEL_LABEL = "Default";
+
+    private static final Location DEFAULT_TILE = new Location(1, 1);
+
+    private static final Geometry DEFAULT_WELL_GEOMETRY = new Geometry(1, 1);
+
+    private static final List<ChannelDescription> DEFAULT_CHANNELS = Arrays
+            .asList(new ChannelDescription(DEFAULT_CHANNEL_CODE, DEFAULT_CHANNEL_LABEL));
+
+    public MicroscopyBlackboxSeriesStorageProcessor(Properties properties)
+    {
+        super(DEFAULT_WELL_GEOMETRY, DEFAULT_CHANNELS, new BlackboxSeriesImageFileExtractor(
+                properties), properties);
+    }
+
+    private static class BlackboxSeriesImageFileExtractor extends AbstractImageFileExtractor
+    {
+        protected BlackboxSeriesImageFileExtractor(Properties properties)
+        {
+            super(DEFAULT_CHANNELS, DEFAULT_WELL_GEOMETRY, properties);
+        }
+
+        @Override
+        protected ImageFileInfo tryExtractImageInfo(File imageFile, File incomingDataSetDirectory,
+                SampleIdentifier datasetSample)
+        {
+            String imageRelativePath = getRelativeImagePath(incomingDataSetDirectory, imageFile);
+            // we postpone assigning series numbers until all images are extracted
+            return new ImageFileInfo(null, DEFAULT_CHANNEL_CODE, DEFAULT_TILE, imageRelativePath,
+                    null, null, null);
+        }
+    }
+
+    @Override
+    protected void storeInDatabase(IImagingQueryDAO dao, DataSetInformation dataSetInformation,
+            ImageFileExtractionResult extractedImages)
+    {
+        List<AcquiredSingleImage> images = extractedImages.getImages();
+        setSeriesNumber(images);
+        MicroscopyImageDatasetInfo dataset =
+                createMicroscopyImageDatasetInfo(dataSetInformation, images);
+
+        MicroscopyImageDatasetUploader.upload(dao, dataset, images, extractedImages.getChannels());
+    }
+
+    private void setSeriesNumber(List<AcquiredSingleImage> images)
+    {
+        Collections.sort(images, createPathComparator());
+        int seriesNumber = 1;
+        for (AcquiredSingleImage image : images)
+        {
+            image.setSeriesNumber(seriesNumber++);
+        }
+    }
+
+    private Comparator<AcquiredSingleImage> createPathComparator()
+    {
+        return new Comparator<AcquiredSingleImage>()
+            {
+                public int compare(AcquiredSingleImage o1, AcquiredSingleImage o2)
+                {
+                    return getPath(o1).compareTo(getPath(o2));
+                }
+
+                private String getPath(AcquiredSingleImage o1)
+                {
+                    return o1.getImageReference().getRelativeImagePath();
+                }
+            };
+    }
+
+    private MicroscopyImageDatasetInfo createMicroscopyImageDatasetInfo(
+            DataSetInformation dataSetInformation, List<AcquiredSingleImage> images)
+    {
+        boolean hasImageSeries = hasImageSeries(images);
+        return new MicroscopyImageDatasetInfo(dataSetInformation.getDataSetCode(),
+                spotGeometry.getRows(), spotGeometry.getColumns(), hasImageSeries);
+    }
+
+    @Override
+    protected void validateImages(DataSetInformation dataSetInformation, IMailClient mailClient,
+            File incomingDataSetDirectory, ImageFileExtractionResult extractionResult)
+    {
+        // do nothing - for now we do not have good examples of real data
+    }
+
+}
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/MicroscopyImageFileExtractor.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/MicroscopyImageFileExtractor.java
index c908f2c3fdef402d9021c3191a898a590ac5420c..d1bf1fbe627d09410a820772924bec9f990a2be6 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/MicroscopyImageFileExtractor.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/MicroscopyImageFileExtractor.java
@@ -74,10 +74,11 @@ public class MicroscopyImageFileExtractor extends AbstractImageFileExtractor
 
         Float timepointOrNull = tryAsFloat(unparsedInfo.getTimepointToken());
         Float depthOrNull = tryAsFloat(unparsedInfo.getDepthToken());
+        Integer seriesNumberOrNull = tryAsInt(unparsedInfo.getSeriesNumberToken());
         String imageRelativePath = getRelativeImagePath(incomingDataSetDirectory, imageFile);
 
         return new ImageFileInfo(null, channelCode, tileLocation, imageRelativePath,
-                timepointOrNull, depthOrNull);
+                timepointOrNull, depthOrNull, seriesNumberOrNull);
     }
 
 }
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/PlateStorageProcessor.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/PlateStorageProcessor.java
index 85caba84f479cb6aa811068e125b80e3190bd3aa..d02e7bad0743067a656803dbc5e6b215f73be5e4 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/PlateStorageProcessor.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/PlateStorageProcessor.java
@@ -97,7 +97,7 @@ public final class PlateStorageProcessor extends AbstractImageStorageProcessor
             String channelCode = getChannelCodeOrLabel(channelCodes, channel);
             AcquiredSingleImage imageDesc =
                     new AcquiredSingleImage(wellLocation, tileLocation, channelCode, null, null,
-                            new RelativeImageReference(imageRelativePath, null, null));
+                            null, new RelativeImageReference(imageRelativePath, null, null));
             images.add(imageDesc);
         }
 
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/UnparsedImageFileInfoLexer.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/UnparsedImageFileInfoLexer.java
index f32bcbc8b68a80322c1b9fc00f867b1fafe0ee62..f7b3521f22c7c305310ada602ebea8d9de4c6c09 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/UnparsedImageFileInfoLexer.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/UnparsedImageFileInfoLexer.java
@@ -59,6 +59,8 @@ public class UnparsedImageFileInfoLexer
 
     private static final char TIME_MARKER = 't';
 
+    private static final char SERIES_NUMBER_MARKER = 'n';
+
     public static UnparsedImageFileInfo tryExtractHCSImageFileInfo(File imageFile,
             File incomingDataSetPath)
     {
@@ -107,6 +109,7 @@ public class UnparsedImageFileInfoLexer
         final String channelToken = tokensMap.get(CHANNEL_MARKER);
         final String timepointToken = tokensMap.get(TIME_MARKER);
         final String depthToken = tokensMap.get(DEPTH_MARKER);
+        final String seriesNumberToken = tokensMap.get(SERIES_NUMBER_MARKER);
 
         UnparsedImageFileInfo info = new UnparsedImageFileInfo();
         info.setWellLocationToken(wellLocationToken);
@@ -114,6 +117,7 @@ public class UnparsedImageFileInfoLexer
         info.setChannelToken(channelToken);
         info.setTimepointToken(timepointToken);
         info.setDepthToken(depthToken);
+        info.setSeriesNumberToken(seriesNumberToken);
         return info;
     }
 
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dataaccess/IImagingQueryDAO.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dataaccess/IImagingQueryDAO.java
index 2932b505145f24ace96cbb3c4b98e805f7516a04..417eef63693ab7f65a2bf74e5e81a8d8357eed5e 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dataaccess/IImagingQueryDAO.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dataaccess/IImagingQueryDAO.java
@@ -52,8 +52,8 @@ public interface IImagingQueryDAO extends TransactionQuery, IImagingReadonlyQuer
 
     // batch updates
 
-    @Update(sql = "insert into CHANNEL_STACKS (ID, X, Y, Z_in_M, T_in_SEC, DS_ID, SPOT_ID) values "
-            + "(?{1.id}, ?{1.column}, ?{1.row}, ?{1.z}, ?{1.t}, ?{1.datasetId}, ?{1.spotId})", batchUpdate = true)
+    @Update(sql = "insert into CHANNEL_STACKS (ID, X, Y, Z_in_M, T_in_SEC, SERIES_NUMBER, IS_REPRESENTATIVE, DS_ID, SPOT_ID) values "
+            + "(?{1.id}, ?{1.column}, ?{1.row}, ?{1.z}, ?{1.t}, ?{1.seriesNumber}, ?{1.isRepresentative}, ?{1.datasetId}, ?{1.spotId})", batchUpdate = true)
     public void addChannelStacks(List<ImgChannelStackDTO> channelStacks);
 
     @Update(sql = "insert into IMAGES (ID, PATH, PAGE, COLOR) values "
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dataaccess/ImagingDatasetLoader.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dataaccess/ImagingDatasetLoader.java
index 7dca0ee839fc05a3126beabc4e111fb18d5f8bd7..29740923cde967e380561200d7ab2be63caa4241 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dataaccess/ImagingDatasetLoader.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dataaccess/ImagingDatasetLoader.java
@@ -80,16 +80,23 @@ public class ImagingDatasetLoader extends HCSDatasetLoader implements IImagingDa
         {
             return null;
         }
+        AbsoluteImageReference imgRef =
+                createAbsoluteImageReference(imageDTO, channel, thumbnailSizeOrNull);
+
+        return imgRef;
+    }
+
+    private AbsoluteImageReference createAbsoluteImageReference(ImgImageDTO imageDTO,
+            ImgChannelDTO channel, Size thumbnailSizeOrNull)
+    {
         String path = imageDTO.getFilePath();
         IContent content = contentRepository.getContent(path);
         ColorComponent colorComponent = imageDTO.getColorComponent();
         AbsoluteImageReference imgRef =
                 new AbsoluteImageReference(content, path, imageDTO.getPage(), colorComponent,
                         thumbnailSizeOrNull);
-
-        imgRef.setTransformerFactory(channel.getImageTransformerFactory());
         imgRef.setTransformerFactoryForMergedChannels(tryGetImageTransformerFactoryForMergedChannels());
-
+        imgRef.setTransformerFactory(channel.getImageTransformerFactory());
         return imgRef;
     }
 
@@ -205,4 +212,54 @@ public class ImagingDatasetLoader extends HCSDatasetLoader implements IImagingDa
             return query.tryGetThumbnail(channelId, channelStackId, datasetId);
         }
     }
+
+    private ImgImageDTO tryGetRepresentativeImageDTO(long channelId, Location wellLocationOrNull,
+            boolean thumbnailWanted)
+    {
+        long datasetId = dataset.getId();
+        ImgImageDTO image = null;
+        if (wellLocationOrNull == null)
+        {
+            if (thumbnailWanted)
+            {
+                image = query.tryGetMicroscopyRepresentativeThumbnail(datasetId, channelId);
+            }
+            if (image == null)
+            {
+                image = query.tryGetMicroscopyRepresentativeImage(datasetId, channelId);
+            }
+        } else
+        {
+            if (thumbnailWanted)
+            {
+                image =
+                        query.tryGetHCSRepresentativeThumbnail(datasetId, wellLocationOrNull,
+                                channelId);
+            }
+            if (image == null)
+            {
+                image =
+                        query.tryGetHCSRepresentativeImage(datasetId, wellLocationOrNull, channelId);
+            }
+        }
+        return image;
+    }
+
+    public AbsoluteImageReference tryGetRepresentativeImage(String channelCode,
+            Location wellLocationOrNull, Size thumbnailSizeOrNull)
+    {
+        ImgChannelDTO channel = tryLoadChannel(channelCode);
+        if (channel == null)
+        {
+            return null;
+        }
+        ImgImageDTO imageDTO =
+                tryGetRepresentativeImageDTO(channel.getId(), wellLocationOrNull,
+                        thumbnailSizeOrNull != null);
+        if (imageDTO == null)
+        {
+            return null;
+        }
+        return createAbsoluteImageReference(imageDTO, channel, thumbnailSizeOrNull);
+    }
 }
\ No newline at end of file
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/ImageFileInfo.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/ImageFileInfo.java
index 12ff5b1cac3cf08d9527d3943417a023a2860449..d4dfb34702d9d7ccabdbf6318ccb32a5e7799bf5 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/ImageFileInfo.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/ImageFileInfo.java
@@ -21,8 +21,10 @@ public final class ImageFileInfo
 
     private final Float depthOrNull;
 
+    private final Integer seriesNumber;
+
     public ImageFileInfo(Location wellLocationOrNull, String channelCode, Location tileLocation,
-            String imageRelativePath, Float timepointOrNull, Float depthOrNull)
+            String imageRelativePath, Float timepointOrNull, Float depthOrNull, Integer seriesNumber)
     {
         assert channelCode != null;
         assert tileLocation != null;
@@ -34,6 +36,7 @@ public final class ImageFileInfo
         this.imageRelativePath = imageRelativePath;
         this.timepointOrNull = timepointOrNull;
         this.depthOrNull = depthOrNull;
+        this.seriesNumber = seriesNumber;
     }
 
     public Location tryGetWellLocation()
@@ -66,6 +69,11 @@ public final class ImageFileInfo
         return depthOrNull;
     }
 
+    public Integer tryGetSeriesNumber()
+    {
+        return seriesNumber;
+    }
+
     public void setChannelCode(String channelCode)
     {
         this.channelCode = channelCode;
@@ -76,7 +84,8 @@ public final class ImageFileInfo
     {
         return "ImageFileInfo [well=" + wellLocationOrNull + ", tile=" + tileLocation
                 + ", channel=" + channelCode + ", path=" + imageRelativePath + ", timepoint="
-                + timepointOrNull + ", depth=" + depthOrNull + "]";
+                + timepointOrNull + ", depth=" + depthOrNull + ", seriesNumber=" + seriesNumber
+                + "]";
     }
 
 }
\ No newline at end of file
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/UnparsedImageFileInfo.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/UnparsedImageFileInfo.java
index df02548da37b63d90c25dd03f385d7b9bd437bff..bb02be652e00e63c79d3fd606f79945b5a67ccfc 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/UnparsedImageFileInfo.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dto/UnparsedImageFileInfo.java
@@ -23,6 +23,9 @@ public class UnparsedImageFileInfo extends AbstractHashable
     // can be null
     private String depthToken;
 
+    // can be null
+    private String seriesNumberToken;
+
     /** can be null */
     public String getWellLocationToken()
     {
@@ -75,4 +78,14 @@ public class UnparsedImageFileInfo extends AbstractHashable
     {
         this.depthToken = depthToken;
     }
+
+    public String getSeriesNumberToken()
+    {
+        return seriesNumberToken;
+    }
+
+    public void setSeriesNumberToken(String seriesNumberToken)
+    {
+        this.seriesNumberToken = seriesNumberToken;
+    }
 }
\ No newline at end of file
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dynamix/HCSImageFileExtractor.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dynamix/HCSImageFileExtractor.java
index 3802e195bcbce39dc9eaadb64636fdf7936684d3..53f6afcace0eb4bb69dd4b13ba2d8110919541ba 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dynamix/HCSImageFileExtractor.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/etl/dynamix/HCSImageFileExtractor.java
@@ -84,7 +84,7 @@ public class HCSImageFileExtractor extends AbstractImageFileExtractor
         String imageRelativePath = getRelativeImagePath(incomingDataSetDirectory, imageFile);
 
         return new ImageFileInfo(asLocation(wellLocation), channelCode, tileLocation,
-                imageRelativePath, timepoint, null);
+                imageRelativePath, timepoint, null, null);
     }
 
     private long getSecondsFromFirstMeasurement(File imageFile, String[] tokens)
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/MergingImagesDownloadServlet.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/MergingImagesDownloadServlet.java
index 96e91344f5a5718b6478b6e6807357e833c9263f..a97bc7d6a3844dfe4cf73af71c1fc5743005dbf6 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/MergingImagesDownloadServlet.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/MergingImagesDownloadServlet.java
@@ -18,26 +18,36 @@ package ch.systemsx.cisd.openbis.dss.generic.server;
 
 import java.io.File;
 import java.io.IOException;
+import java.util.Properties;
 
 import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
-import ch.systemsx.cisd.common.io.IContent;
-import ch.systemsx.cisd.openbis.dss.etl.HCSImageDatasetLoaderFactory;
-import ch.systemsx.cisd.openbis.dss.etl.IImagingDatasetLoader;
 import ch.systemsx.cisd.openbis.dss.generic.server.images.ImageChannelsUtils;
 import ch.systemsx.cisd.openbis.dss.generic.server.images.TileImageReference;
+import ch.systemsx.cisd.openbis.dss.generic.shared.dto.Size;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ImageResolutionKind;
 
 /**
- * Allows to download screening images in a chosen size for a specified channels or with all
+ * Allows to:<br>
+ * - download screening and microscopy images in a chosen size for a specified channels or with all
  * channels merged.<br>
- * Assumes that originally there is one image for each channel and no image with all the channels
- * merged exist.
+ * - fetch representative microscopy dataset image
  * 
  * @author Tomasz Pylak
  */
-public class MergingImagesDownloadServlet extends AbstractImagesDownloadServlet
+public class MergingImagesDownloadServlet extends AbstractImagesDownloadServlet implements
+        IDatasetImageOverviewPlugin
 {
     private static final long serialVersionUID = 1L;
 
+    /** Used to construct {@link IDatasetImageOverviewPlugin}. */
+    public MergingImagesDownloadServlet(Properties pluginProperties)
+    {
+    }
+
+    public MergingImagesDownloadServlet()
+    {
+    }
+
     /**
      * @throws EnvironmentFailureException if image does not exist
      **/
@@ -45,10 +55,28 @@ public class MergingImagesDownloadServlet extends AbstractImagesDownloadServlet
     protected final ResponseContentStream createImageResponse(TileImageReference params,
             File datasetRoot, String datasetCode) throws IOException, EnvironmentFailureException
     {
-        IImagingDatasetLoader imageAccessor =
-                HCSImageDatasetLoaderFactory.create(datasetRoot, datasetCode);
-        IContent image = ImageChannelsUtils.getImage(imageAccessor, params);
-        return ResponseContentStream.create(image.getInputStream(), image.getSize(),
-                ImageChannelsUtils.IMAGES_CONTENT_TYPE, image.tryGetName());
+        return ImageChannelsUtils.getImageStream(datasetRoot, datasetCode, params);
+    }
+
+    private static final Size DEFAULT_THUMBNAIL_SIZE = new Size(200, 120);
+
+    /** Provides overview of microscopy datasets. */
+    public ResponseContentStream createImageOverview(String datasetCode, String datasetTypeCode,
+            File datasetRoot, ImageResolutionKind resolution)
+    {
+        Size thumbnailSize = tryGetThumbnailSize(resolution);
+        return ImageChannelsUtils.getRepresentativeImageStream(datasetRoot, datasetCode, null,
+                thumbnailSize);
+    }
+
+    private static Size tryGetThumbnailSize(ImageResolutionKind resolution)
+    {
+        if (resolution == ImageResolutionKind.NORMAL)
+        {
+            return null;
+        } else
+        {
+            return DEFAULT_THUMBNAIL_SIZE;
+        }
     }
 }
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageChannelsUtils.java b/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageChannelsUtils.java
index cc6a31b01b566aa23f3a09561b70d01413c981c9..7d395c6cea76325f1695296bdbb479db8d9715db 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageChannelsUtils.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageChannelsUtils.java
@@ -20,6 +20,7 @@ import java.awt.Color;
 import java.awt.image.BufferedImage;
 import java.awt.image.RenderedImage;
 import java.io.ByteArrayOutputStream;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
@@ -29,14 +30,18 @@ import javax.imageio.ImageIO;
 
 import org.apache.log4j.Logger;
 
+import ch.rinn.restrictions.Private;
 import ch.systemsx.cisd.base.image.IImageTransformerFactory;
+import ch.systemsx.cisd.bds.hcs.Location;
 import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
 import ch.systemsx.cisd.common.io.ByteArrayBasedContent;
 import ch.systemsx.cisd.common.io.IContent;
 import ch.systemsx.cisd.common.logging.LogCategory;
 import ch.systemsx.cisd.common.logging.LogFactory;
 import ch.systemsx.cisd.openbis.dss.etl.AbsoluteImageReference;
+import ch.systemsx.cisd.openbis.dss.etl.HCSImageDatasetLoaderFactory;
 import ch.systemsx.cisd.openbis.dss.etl.IImagingDatasetLoader;
+import ch.systemsx.cisd.openbis.dss.generic.server.ResponseContentStream;
 import ch.systemsx.cisd.openbis.dss.generic.shared.dto.Size;
 import ch.systemsx.cisd.openbis.dss.generic.shared.utils.ImageUtil;
 import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.ScreeningConstants;
@@ -56,16 +61,46 @@ public class ImageChannelsUtils
     // MIME type of the images which are produced by this class
     public static final String IMAGES_CONTENT_TYPE = "image/png";
 
-    
     /**
      * Returns content of image for the specified tile in the specified size and for the requested
      * channel or with all channels merged.
      */
-    public static IContent getImage(IImagingDatasetLoader imageAccessor, TileImageReference params)
+    public static ResponseContentStream getImageStream(File datasetRoot, String datasetCode,
+            TileImageReference params)
+    {
+        IImagingDatasetLoader imageAccessor =
+                HCSImageDatasetLoaderFactory.create(datasetRoot, datasetCode);
+        IContent image = getImage(imageAccessor, params);
+        return asResponseContentStream(image);
+    }
+
+    /**
+     * Returns content of the image which is representative for the given dataset.
+     */
+    public static ResponseContentStream getRepresentativeImageStream(File datasetRoot,
+            String datasetCode, Location wellLocationOrNull, Size thumbnailSizeOrNull)
+    {
+        IImagingDatasetLoader imageAccessor =
+                HCSImageDatasetLoaderFactory.create(datasetRoot, datasetCode);
+        List<AbsoluteImageReference> imageReferences =
+                getRepresentativeImageReferences(imageAccessor, wellLocationOrNull,
+                        thumbnailSizeOrNull);
+        IContent imageContent = mergeAllChannels(imageReferences, true, true);
+        return asResponseContentStream(imageContent);
+    }
+
+    private static ResponseContentStream asResponseContentStream(IContent image)
+    {
+        return ResponseContentStream.create(image.getInputStream(), image.getSize(),
+                ImageChannelsUtils.IMAGES_CONTENT_TYPE, image.tryGetName());
+    }
+
+    @Private
+    static IContent getImage(IImagingDatasetLoader imageAccessor, TileImageReference params)
     {
         return getImage(imageAccessor, params, true, true);
     }
-    
+
     private static IContent getImage(IImagingDatasetLoader imageAccessor,
             TileImageReference params, boolean transform, boolean convertToPng)
     {
@@ -93,10 +128,48 @@ public class ImageChannelsUtils
         tileImageReference.setChannelStack(channelStackReference);
         tileImageReference.setChannel(chosenChannelCode);
         tileImageReference.setThumbnailSizeOrNull(thumbnailSizeOrNull);
-        tileImageReference.setMergeAllChannels(ScreeningConstants.MERGED_CHANNELS.equalsIgnoreCase(chosenChannelCode));
+        tileImageReference.setMergeAllChannels(ScreeningConstants.MERGED_CHANNELS
+                .equalsIgnoreCase(chosenChannelCode));
         return getImage(imageAccessor, tileImageReference, false, convertToPng);
     }
 
+    private static List<AbsoluteImageReference> getRepresentativeImageReferences(
+            IImagingDatasetLoader imageAccessor, Location wellLocationOrNull,
+            Size thumbnailSizeOrNull)
+    {
+        List<AbsoluteImageReference> images = new ArrayList<AbsoluteImageReference>();
+
+        for (String chosenChannel : imageAccessor.getImageParameters().getChannelsCodes())
+        {
+            AbsoluteImageReference image =
+                    getRepresentativeImageReference(imageAccessor, chosenChannel,
+                            wellLocationOrNull, thumbnailSizeOrNull);
+            images.add(image);
+        }
+        return images;
+    }
+
+    /**
+     * @throw {@link EnvironmentFailureException} when image does not exist
+     */
+    private static AbsoluteImageReference getRepresentativeImageReference(
+            IImagingDatasetLoader imageAccessor, String channelCode, Location wellLocationOrNull,
+            Size thumbnailSizeOrNull)
+    {
+        AbsoluteImageReference image =
+                imageAccessor.tryGetRepresentativeImage(channelCode, wellLocationOrNull,
+                        thumbnailSizeOrNull);
+        if (image != null)
+        {
+            return image;
+        } else
+        {
+            throw EnvironmentFailureException.fromTemplate("No representative "
+                    + (thumbnailSizeOrNull != null ? "thumbnail" : "image")
+                    + " found for well %s and channel %s", wellLocationOrNull, channelCode);
+        }
+    }
+
     private static List<AbsoluteImageReference> getImageReferences(
             IImagingDatasetLoader imageAccessor, TileImageReference params)
     {
@@ -128,7 +201,8 @@ public class ImageChannelsUtils
         final IContent content;
         if (convertToPng || imageReference.tryGetColorComponent() != null)
         {
-            final BufferedImage image = transform(calculateSingleImage(imageReference), transformerFactoryOrNull);
+            final BufferedImage image =
+                    transform(calculateSingleImage(imageReference), transformerFactoryOrNull);
 
             long start = operationLog.isDebugEnabled() ? System.currentTimeMillis() : 0;
             content = createPngContent(image, imageReference.getContent().tryGetName());
@@ -181,7 +255,8 @@ public class ImageChannelsUtils
             image = transformToChannel(image, colorComponentOrNull);
             if (operationLog.isDebugEnabled())
             {
-                operationLog.debug("Select single channel: " + (System.currentTimeMillis() - start));
+                operationLog
+                        .debug("Select single channel: " + (System.currentTimeMillis() - start));
             }
         }
         return image;
@@ -199,7 +274,8 @@ public class ImageChannelsUtils
         return image;
     }
 
-    private static IContent mergeAllChannels(List<AbsoluteImageReference> imageReferences, boolean transform, boolean convertToPng)
+    private static IContent mergeAllChannels(List<AbsoluteImageReference> imageReferences,
+            boolean transform, boolean convertToPng)
     {
         AbsoluteImageReference allChannelsImageReference =
                 tryCreateAllChannelsImageReference(imageReferences);
@@ -215,13 +291,15 @@ public class ImageChannelsUtils
         {
             List<BufferedImage> images = calculateSingleImages(imageReferences);
             BufferedImage mergedImage = mergeChannels(images);
-            IImageTransformerFactory transformerFactory = transform ? 
-                    imageReferences.get(0).getTransformerFactoryForMergedChannels() : null;
+            IImageTransformerFactory transformerFactory =
+                    transform ? imageReferences.get(0).getTransformerFactoryForMergedChannels()
+                            : null;
             return createPngContent(transform(mergedImage, transformerFactory), null);
         }
     }
-    
-    private static BufferedImage transform(BufferedImage input, IImageTransformerFactory factoryOrNull)
+
+    private static BufferedImage transform(BufferedImage input,
+            IImageTransformerFactory factoryOrNull)
     {
         if (factoryOrNull == null)
         {
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/LogicalImageSeriesViewer.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/LogicalImageSeriesViewer.java
index 11561437dcd598bfa97a6e4aee6f1924363f747a..2ca9e031b6c8e050630985d4db2e71c8d5e93116 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/LogicalImageSeriesViewer.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/LogicalImageSeriesViewer.java
@@ -17,9 +17,10 @@
 package ch.systemsx.cisd.openbis.plugin.screening.client.web.client.application.detailviewers;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
+import java.util.Set;
 import java.util.TreeMap;
 
 import com.extjs.gxt.ui.client.event.Events;
@@ -40,23 +41,203 @@ import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.ImageChannelSt
 class LogicalImageSeriesViewer
 {
 
+    static class ImageSeriesPoint implements Comparable<ImageSeriesPoint>
+    {
+        private final Float tOrNull, zOrNull;
+
+        private final Integer seriesNumberOrNull;
+
+        public ImageSeriesPoint(ImageChannelStack stack)
+        {
+            this.tOrNull = stack.tryGetTimepoint();
+            this.zOrNull = stack.tryGetDepth();
+            this.seriesNumberOrNull = stack.tryGetSeriesNumber();
+        }
+
+        public ImageSeriesPoint(Float tOrNull, Float zOrNull, Integer seriesNumberOrNull)
+        {
+            this.tOrNull = tOrNull;
+            this.zOrNull = zOrNull;
+            this.seriesNumberOrNull = seriesNumberOrNull;
+        }
+
+        public String getLabel()
+        {
+            String desc = "";
+            if (tOrNull != null)
+            {
+                if (desc.length() > 0)
+                {
+                    desc += ". ";
+                }
+                desc += "Time: " + tOrNull + " sec";
+            }
+            if (zOrNull != null)
+            {
+                if (desc.length() > 0)
+                {
+                    desc += ". ";
+                }
+                desc += "Depth: " + zOrNull;
+            }
+            if (seriesNumberOrNull != null)
+            {
+                if (desc.length() > 0)
+                {
+                    desc += ". ";
+                }
+                desc += "Series: " + seriesNumberOrNull;
+            }
+            return desc;
+        }
+
+        public int compareTo(ImageSeriesPoint o)
+        {
+            int cmp;
+            cmp = compareNullable(seriesNumberOrNull, o.seriesNumberOrNull);
+            if (cmp != 0)
+                return cmp;
+            cmp = compareNullable(tOrNull, o.tOrNull);
+            if (cmp != 0)
+                return cmp;
+            return compareNullable(zOrNull, o.zOrNull);
+        }
+
+        private static <T extends Comparable<T>> int compareNullable(T v1OrNull, T v2OrNull)
+        {
+            if (v1OrNull == null)
+            {
+                return v2OrNull == null ? 0 : -1;
+            } else
+            {
+                return v2OrNull == null ? 1 : v1OrNull.compareTo(v2OrNull);
+            }
+        }
+
+        @Override
+        public int hashCode()
+        {
+            final int prime = 31;
+            int result = 1;
+            result =
+                    prime * result
+                            + ((seriesNumberOrNull == null) ? 0 : seriesNumberOrNull.hashCode());
+            result = prime * result + ((tOrNull == null) ? 0 : tOrNull.hashCode());
+            result = prime * result + ((zOrNull == null) ? 0 : zOrNull.hashCode());
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj)
+        {
+            if (this == obj)
+                return true;
+            if (obj == null)
+                return false;
+            if (getClass() != obj.getClass())
+                return false;
+            ImageSeriesPoint other = (ImageSeriesPoint) obj;
+            if (seriesNumberOrNull == null)
+            {
+                if (other.seriesNumberOrNull != null)
+                    return false;
+            } else if (!seriesNumberOrNull.equals(other.seriesNumberOrNull))
+                return false;
+            if (tOrNull == null)
+            {
+                if (other.tOrNull != null)
+                    return false;
+            } else if (!tOrNull.equals(other.tOrNull))
+                return false;
+            if (zOrNull == null)
+            {
+                if (other.zOrNull != null)
+                    return false;
+            } else if (!zOrNull.equals(other.zOrNull))
+                return false;
+            return true;
+        }
+    }
+
+    static class LogicalImageSeriesViewerModel
+    {
+        private final List<ImageSeriesPoint> sortedPoints;
+
+        private final List<List<ImageChannelStack>> sortedChannelStackSeriesPoints;
+
+        public LogicalImageSeriesViewerModel(List<ImageChannelStack> channelStackImages)
+        {
+            Map<ImageSeriesPoint, List<ImageChannelStack>> channelStackImagesBySeries =
+                    groupImagesBySeries(channelStackImages);
+            this.sortedPoints = sortPoints(channelStackImagesBySeries.keySet());
+            this.sortedChannelStackSeriesPoints =
+                    getSortedSeries(channelStackImagesBySeries, sortedPoints);
+        }
+
+        private static Map<ImageSeriesPoint, List<ImageChannelStack>> groupImagesBySeries(
+                List<ImageChannelStack> channelStackImages)
+        {
+            Map<ImageSeriesPoint, List<ImageChannelStack>> result =
+                    new TreeMap<ImageSeriesPoint, List<ImageChannelStack>>();
+            for (ImageChannelStack ref : channelStackImages)
+            {
+                ImageSeriesPoint point = new ImageSeriesPoint(ref);
+                List<ImageChannelStack> imageReferences = result.get(point);
+                if (imageReferences == null)
+                {
+                    imageReferences = new ArrayList<ImageChannelStack>();
+                    result.put(point, imageReferences);
+                }
+                imageReferences.add(ref);
+            }
+            return result;
+        }
+
+        public List<ImageSeriesPoint> getSortedPoints()
+        {
+            return sortedPoints;
+        }
+
+        public List<List<ImageChannelStack>> getSortedChannelStackSeriesPoints()
+        {
+            return sortedChannelStackSeriesPoints;
+        }
+    }
+
     public static LayoutContainer create(String sessionId,
             List<ImageChannelStack> channelStackImages, LogicalImageReference images,
             String channel, int imageWidth, int imageHeight)
     {
-        Map<Float, List<ImageChannelStack>> channelStackImagesByTimepoint =
-                groupImagesByTimepoint(channelStackImages);
-
+        LogicalImageSeriesViewerModel model = new LogicalImageSeriesViewerModel(channelStackImages);
         List<LayoutContainer> frames =
-                createTimepointFrames(channelStackImagesByTimepoint, images, channel, sessionId,
-                        imageWidth, imageHeight);
+                createTimepointFrames(model.getSortedChannelStackSeriesPoints(), images, channel,
+                        sessionId, imageWidth, imageHeight);
+
+        return createMoviePlayer(frames, model.getSortedPoints());
+    }
 
-        Float[] timepoints = channelStackImagesByTimepoint.keySet().toArray(new Float[0]);
-        return createMoviePlayer(frames, timepoints);
+    private static List<List<ImageChannelStack>> getSortedSeries(
+            Map<ImageSeriesPoint, List<ImageChannelStack>> channelStackImagesBySeries,
+            List<ImageSeriesPoint> sortedPoints)
+    {
+        List<List<ImageChannelStack>> sortedSeries = new ArrayList<List<ImageChannelStack>>();
+        for (ImageSeriesPoint point : sortedPoints)
+        {
+            List<ImageChannelStack> series = channelStackImagesBySeries.get(point);
+            sortedSeries.add(series);
+        }
+        return sortedSeries;
+    }
+
+    private static List<ImageSeriesPoint> sortPoints(Set<ImageSeriesPoint> points)
+    {
+        ArrayList<ImageSeriesPoint> pointsList = new ArrayList<ImageSeriesPoint>(points);
+        Collections.sort(pointsList);
+        return pointsList;
     }
 
     private static LayoutContainer createMoviePlayer(final List<LayoutContainer> frames,
-            final Float[] timepoints)
+            final List<ImageSeriesPoint> sortedPoints)
     {
         final LayoutContainer mainContainer = new LayoutContainer();
         addAll(mainContainer, frames);
@@ -73,29 +254,33 @@ class LogicalImageSeriesViewer
                     }
                     frames.get(newValue - 1).show();
                     mainContainer.remove(mainContainer.getItem(0));
-                    mainContainer.insert(new Label(createTimepointLabel(timepoints, newValue)), 0);
+                    mainContainer
+                            .insert(new Label(createTimepointLabel(sortedPoints, newValue)), 0);
                     mainContainer.layout();
                 }
             });
         mainContainer.insert(slider, 0);
-        mainContainer.insert(new Label(createTimepointLabel(timepoints, 1)), 0);
+        mainContainer.insert(new Label(createTimepointLabel(sortedPoints, 1)), 0);
         slider.setValue(1);
 
         return mainContainer;
     }
 
+    /**
+     * @param sortedChannelStackSeriesPoints - one element on the list are all tiles for a fixed
+     *            series point
+     */
     private static List<LayoutContainer> createTimepointFrames(
-            Map<Float, List<ImageChannelStack>> channelStackImagesByTimepoint,
+            List<List<ImageChannelStack>> sortedChannelStackSeriesPoints,
             LogicalImageReference images, String channel, String sessionId, int imageWidth,
             int imageHeight)
     {
         final List<LayoutContainer> frames = new ArrayList<LayoutContainer>();
         int counter = 0;
-        for (Entry<Float, List<ImageChannelStack>> entry : channelStackImagesByTimepoint.entrySet())
+        for (List<ImageChannelStack> seriesPointStacks : sortedChannelStackSeriesPoints)
         {
-            List<ImageChannelStack> imageReferences = entry.getValue();
             final LayoutContainer container =
-                    createTilesGridForTimepoint(imageReferences, images, channel, sessionId,
+                    createTilesGridForTimepoint(seriesPointStacks, images, channel, sessionId,
                             imageWidth, imageHeight);
             frames.add(container);
             if (counter > 0)
@@ -116,14 +301,14 @@ class LogicalImageSeriesViewer
     }
 
     private static LayoutContainer createTilesGridForTimepoint(
-            List<ImageChannelStack> channelStackReferences, LogicalImageReference images,
+            List<ImageChannelStack> seriesPointStacks, LogicalImageReference images,
             String channel, String sessionId, int imageWidth, int imageHeight)
     {
         final LayoutContainer container =
                 new LayoutContainer(new TableLayout(images.getTileColsNum()));
 
         ImageChannelStack[/* tileRow */][/* tileCol */] tilesMap =
-                createTilesMap(channelStackReferences, images);
+                createTilesMap(seriesPointStacks, images);
         for (int row = 1; row <= images.getTileRowsNum(); row++)
         {
             for (int col = 1; col <= images.getTileColsNum(); col++)
@@ -163,29 +348,12 @@ class LogicalImageSeriesViewer
         container.add(dummy);
     }
 
-    private static String createTimepointLabel(Float[] timepoints, int sequenceNumber)
-    {
-        Float timepoint = timepoints[sequenceNumber - 1];
-        int numberOfSequences = timepoints.length;
-        return "Timepoint: " + timepoint + "sec (" + sequenceNumber + "/" + numberOfSequences + ")";
-    }
-
-    private static Map<Float, List<ImageChannelStack>> groupImagesByTimepoint(
-            List<ImageChannelStack> channelStackImages)
+    private static String createTimepointLabel(List<ImageSeriesPoint> sortedPoints,
+            int sequenceNumber)
     {
-        Map<Float, List<ImageChannelStack>> result = new TreeMap<Float, List<ImageChannelStack>>();
-        for (ImageChannelStack ref : channelStackImages)
-        {
-            Float t = ref.tryGetTimepoint();
-            List<ImageChannelStack> imageReferences = result.get(t);
-            if (imageReferences == null)
-            {
-                imageReferences = new ArrayList<ImageChannelStack>();
-                result.put(t, imageReferences);
-            }
-            imageReferences.add(ref);
-        }
-        return result;
+        ImageSeriesPoint point = sortedPoints.get(sequenceNumber - 1);
+        int numberOfSequences = sortedPoints.size();
+        return point.getLabel() + " (" + sequenceNumber + "/" + numberOfSequences + ")";
     }
 
     private static final Slider createTimepointsSlider(int maxValue, Listener<SliderEvent> listener)
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/basic/dto/ImageChannelStack.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/basic/dto/ImageChannelStack.java
index 0a403091cb834b667bbeb2791e9ba2a9cea22b88..6b97736df150047c66e99563221a567ad5aef39d 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/basic/dto/ImageChannelStack.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/basic/dto/ImageChannelStack.java
@@ -37,6 +37,8 @@ public class ImageChannelStack implements ISerializable
 
     private Float tOrNull, zOrNull;
 
+    private Integer seriesNumberOrNull;
+
     // GWT only
     @SuppressWarnings("unused")
     private ImageChannelStack()
@@ -44,13 +46,14 @@ public class ImageChannelStack implements ISerializable
     }
 
     public ImageChannelStack(long channelStackTechId, int tileRow, int tileCol, Float tOrNull,
-            Float zOrNull)
+            Float zOrNull, Integer seriesNumberOrNull)
     {
         this.channelStackTechId = channelStackTechId;
         this.tileRow = tileRow;
         this.tileCol = tileCol;
         this.tOrNull = tOrNull;
         this.zOrNull = zOrNull;
+        this.seriesNumberOrNull = seriesNumberOrNull;
     }
 
     public long getChannelStackTechId()
@@ -78,6 +81,11 @@ public class ImageChannelStack implements ISerializable
         return zOrNull;
     }
 
+    public Integer tryGetSeriesNumber()
+    {
+        return seriesNumberOrNull;
+    }
+
     @Override
     public String toString()
     {
@@ -90,7 +98,10 @@ public class ImageChannelStack implements ISerializable
         {
             desc += ", z=" + zOrNull;
         }
-
+        if (seriesNumberOrNull != null)
+        {
+            desc += ", series=" + seriesNumberOrNull;
+        }
         return "channelStack=" + channelStackTechId + ", tile[" + tileRow + "," + tileCol + "]"
                 + desc;
     }
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/HCSDatasetLoader.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/HCSDatasetLoader.java
index 68e1df0c4d892c4defbb6b35a29e4fb0e96679d2..d0e56ba874790e79073f953a00c381c8c79d729d 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/HCSDatasetLoader.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/HCSDatasetLoader.java
@@ -144,7 +144,7 @@ public class HCSDatasetLoader implements IImageDatasetLoader
     private static ImageChannelStack convert(ImgChannelStackDTO stack)
     {
         return new ImageChannelStack(stack.getId(), stack.getRow(), stack.getColumn(),
-                stack.getT(), stack.getZ());
+                stack.getT(), stack.getZ(), stack.getSeriesNumber());
     }
 
     public ImageDatasetParameters getImageParameters()
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/dataaccess/IImagingReadonlyQueryDAO.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/dataaccess/IImagingReadonlyQueryDAO.java
index 79194a2867df09b5a5401f811161faacfc5e506b..c9084979a196da6fcc1eaf6d4c4b17d335c80942 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/dataaccess/IImagingReadonlyQueryDAO.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/dataaccess/IImagingReadonlyQueryDAO.java
@@ -54,13 +54,35 @@ public interface IImagingReadonlyQueryDAO extends BaseQuery
                     // joins
                     + "ACQUIRED_IMAGES.CHANNEL_STACK_ID = CHANNEL_STACKS.ID ";
 
-    // FIXME 2010-12-10, Tomasz Pylak: uncomment when we are able to show a representative image
+    public static final String SQL_HCS_IMAGE_REPRESENTATIVE =
+            "select i.* from CHANNEL_STACKS, SPOTS, ACQUIRED_IMAGES, IMAGES as i "
+                    + "where                                        "
+                    + "CHANNEL_STACKS.is_representative = 'T' and   "
+                    + "CHANNEL_STACKS.DS_ID = ?{1} and              "
+                    + "ACQUIRED_IMAGES.channel_id = ?{3} and        "
+                    + "SPOTS.x = ?{2.x} and SPOTS.y = ?{2.y} and    "
+                    // joins
+                    + "ACQUIRED_IMAGES.CHANNEL_STACK_ID = CHANNEL_STACKS.ID and "
+                    + "CHANNEL_STACKS.SPOT_ID = SPOTS.ID ";
+
+    public static final String SQL_MICROSCOPY_IMAGE_REPRESENTATIVE =
+            "select i.* from CHANNEL_STACKS, ACQUIRED_IMAGES, IMAGES as i "
+                    + "where                                         "
+                    + "CHANNEL_STACKS.is_representative = 'T' and    "
+                    + "CHANNEL_STACKS.DS_ID = ?{1} and               "
+                    + "ACQUIRED_IMAGES.channel_id = ?{2} and         "
+                    // joins
+                    + "ACQUIRED_IMAGES.CHANNEL_STACK_ID = CHANNEL_STACKS.ID ";
+
+    // TODO 2010-12-10, Tomasz Pylak: uncomment when we are able to show a representative image
     public static final String SQL_NO_MULTIDIMENTIONAL_DATA_COND =
             " order by CHANNEL_STACKS.T_in_SEC, CHANNEL_STACKS.Z_in_M limit 1";
 
     // " and CHANNEL_STACKS.T_in_SEC IS NULL                        "
     // + " and CHANNEL_STACKS.Z_in_M IS NULL                ";
 
+    // ---------------- HCS ---------------------------------
+
     /**
      * @return an HCS image for the specified chanel, well and tile. If many images (e.g. for
      *         different timepoints or depths) exist, null is returned.
@@ -79,6 +101,24 @@ public interface IImagingReadonlyQueryDAO extends BaseQuery
     public ImgImageDTO tryGetHCSThumbnail(long channelId, long datasetId, Location tileLocation,
             Location wellLocation);
 
+    /**
+     * @return an representative HCS image for the specified dataset and well. Returns null if there
+     *         are no images at all for the specified well.
+     */
+    @Select(SQL_HCS_IMAGE_REPRESENTATIVE + " and ACQUIRED_IMAGES.IMG_ID = i.ID ")
+    public ImgImageDTO tryGetHCSRepresentativeImage(long datasetId, Location wellLocation,
+            long channelId);
+
+    /**
+     * @return an representative HCS thumbnail for the specified dataset and well. Returns null if
+     *         there are no images at all for the specified well.
+     */
+    @Select(SQL_HCS_IMAGE_REPRESENTATIVE + " and ACQUIRED_IMAGES.THUMBNAIL_ID = i.ID ")
+    public ImgImageDTO tryGetHCSRepresentativeThumbnail(long datasetId, Location wellLocation,
+            long channelId);
+
+    // ---------------- Microscopy ---------------------------------
+
     /**
      * @return an microscopy image for the specified channel and tile. If many images (e.g. for
      *         different timepoints or depths) exist, null is returned.
@@ -96,6 +136,33 @@ public interface IImagingReadonlyQueryDAO extends BaseQuery
     public ImgImageDTO tryGetMicroscopyThumbnail(long channelId, long datasetId,
             Location tileLocation);
 
+    /**
+     * @return an representative microscopy image for the specified dataset.
+     */
+    @Select(SQL_MICROSCOPY_IMAGE_REPRESENTATIVE + " and ACQUIRED_IMAGES.IMG_ID = i.ID ")
+    public ImgImageDTO tryGetMicroscopyRepresentativeImage(long datasetId, long channelId);
+
+    /**
+     * @return an representative microscopy thumbnail for the specified dataset. Can be null if
+     *         thumbnail has not been generated.
+     */
+    @Select(SQL_MICROSCOPY_IMAGE_REPRESENTATIVE + " and ACQUIRED_IMAGES.THUMBNAIL_ID = i.ID ")
+    public ImgImageDTO tryGetMicroscopyRepresentativeThumbnail(long datasetId, long channelId);
+
+    // ---------------- Microscopy - channels ---------------------------------
+
+    @Select("select cs.* from CHANNEL_STACKS cs               "
+            + "where cs.ds_id = ?{1} and cs.spot_id is NULL")
+    public List<ImgChannelStackDTO> listSpotlessChannelStacks(long datasetId);
+
+    @Select("select * from CHANNELS where DS_ID = ?{1} order by ID")
+    public List<ImgChannelDTO> getChannelsByDatasetId(long datasetId);
+
+    @Select("select * from CHANNELS where (DS_ID = ?{1}) and CODE = upper(?{2})")
+    public ImgChannelDTO tryGetChannelForDataset(long datasetId, String chosenChannelCode);
+
+    // ---------------- Generic ---------------------------------
+
     /** @return an image for the specified channel and channel stack or null */
     @Select("select i.* from IMAGES as i "
             + "join ACQUIRED_IMAGES on ACQUIRED_IMAGES.IMG_ID = i.ID "
@@ -120,6 +187,11 @@ public interface IImagingReadonlyQueryDAO extends BaseQuery
 
     // simple getters
 
+    @Select("select * from DATA_SETS where PERM_ID = ?{1}")
+    public ImgDatasetDTO tryGetDatasetByPermId(String datasetPermId);
+
+    // ---------------- HCS - experiments, containers, channels ---------------------------------
+
     @Select("select * from EXPERIMENTS where PERM_ID = ?{1}")
     public ImgExperimentDTO tryGetExperimentByPermId(String experimentPermId);
 
@@ -129,9 +201,6 @@ public interface IImagingReadonlyQueryDAO extends BaseQuery
     @Select("select ID from CONTAINERS where PERM_ID = ?{1}")
     public Long tryGetContainerIdPermId(String containerPermId);
 
-    @Select("select * from DATA_SETS where PERM_ID = ?{1}")
-    public ImgDatasetDTO tryGetDatasetByPermId(String datasetPermId);
-
     @Select("select * from CONTAINERS where ID = ?{1}")
     public ImgContainerDTO getContainerById(long containerId);
 
@@ -142,19 +211,22 @@ public interface IImagingReadonlyQueryDAO extends BaseQuery
             + "where cs.ds_id = ?{1} and s.x = ?{2} and s.y = ?{3}")
     public List<ImgChannelStackDTO> listChannelStacks(long datasetId, int spotX, int spotY);
 
-    @Select("select cs.* from CHANNEL_STACKS cs               "
-            + "where cs.ds_id = ?{1} and cs.spot_id is NULL")
-    public List<ImgChannelStackDTO> listSpotlessChannelStacks(long datasetId);
-
-    @Select("select * from CHANNELS where DS_ID = ?{1} order by ID")
-    public List<ImgChannelDTO> getChannelsByDatasetId(long datasetId);
-
     @Select(sql = "select * from CHANNELS where EXP_ID = ?{1} order by ID", fetchSize = FETCH_SIZE)
     public List<ImgChannelDTO> getChannelsByExperimentId(long experimentId);
 
     @Select("select * from SPOTS where cont_id = ?{1}")
     public List<ImgSpotDTO> listSpots(long contId);
 
+    @Select("select * from CHANNELS where (EXP_ID = ?{1}) and CODE = upper(?{2})")
+    public ImgChannelDTO tryGetChannelForExperiment(long experimentId, String chosenChannelCode);
+
+    @Select("select * from channels where code = ?{2} and "
+            + "exp_id in (select id from experiments where perm_id = ?{1})")
+    public ImgChannelDTO tryGetChannelForExperimentPermId(String experimentPermId,
+            String chosenChannelCode);
+
+    // ---------------- HCS - feature vectors ---------------------------------
+
     @Select("select * from FEATURE_DEFS where DS_ID = ?{1}")
     public List<ImgFeatureDefDTO> listFeatureDefsByDataSetId(long dataSetId);
 
@@ -166,15 +238,4 @@ public interface IImagingReadonlyQueryDAO extends BaseQuery
     @Select(sql = "select * from FEATURE_VALUES where FD_ID = ?{1.id} order by T_in_SEC, Z_in_M", resultSetBinding = FeatureVectorDataObjectBinding.class)
     public List<ImgFeatureValuesDTO> getFeatureValues(ImgFeatureDefDTO featureDef);
 
-    @Select("select * from CHANNELS where (EXP_ID = ?{1}) and CODE = upper(?{2})")
-    public ImgChannelDTO tryGetChannelForExperiment(long experimentId, String chosenChannelCode);
-
-    @Select("select * from CHANNELS where (DS_ID = ?{1}) and CODE = upper(?{2})")
-    public ImgChannelDTO tryGetChannelForDataset(long datasetId, String chosenChannelCode);
-
-    @Select("select * from channels where code = ?{2} and "
-            + "exp_id in (select id from experiments where perm_id = ?{1})")
-    public ImgChannelDTO tryGetChannelForExperimentPermId(String experimentPermId,
-            String chosenChannelCode);
-
 }
diff --git a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/dataaccess/ImgChannelStackDTO.java b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/dataaccess/ImgChannelStackDTO.java
index f262b732a3f21c4f1ae93225f8353e83a75a3898..eb973b183d0a88850fd86c4bc6c6bf1431452573 100644
--- a/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/dataaccess/ImgChannelStackDTO.java
+++ b/screening/source/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/dataaccess/ImgChannelStackDTO.java
@@ -47,12 +47,18 @@ public class ImgChannelStackDTO
     @ResultColumn("T_in_SEC")
     private Float t;
 
+    @ResultColumn("SERIES_NUMBER")
+    private Integer seriesNumber;
+
     @ResultColumn("DS_ID")
     private long datasetId;
 
     @ResultColumn("SPOT_ID")
     private Long spotId;
 
+    @ResultColumn("IS_REPRESENTATIVE")
+    private boolean isRepresentative;
+
     @SuppressWarnings("unused")
     private ImgChannelStackDTO()
     {
@@ -60,7 +66,7 @@ public class ImgChannelStackDTO
     }
 
     public ImgChannelStackDTO(long id, int row, int column, long datasetId, Long spotIdOrNull,
-            Float tOrNull, Float zOrNull)
+            Float tOrNull, Float zOrNull, Integer seriesNumber, boolean isRepresentative)
     {
         this.id = id;
         this.row = row;
@@ -69,6 +75,8 @@ public class ImgChannelStackDTO
         this.spotId = spotIdOrNull;
         this.t = tOrNull;
         this.z = zOrNull;
+        this.seriesNumber = seriesNumber;
+        this.isRepresentative = isRepresentative;
     }
 
     public long getId()
@@ -111,6 +119,16 @@ public class ImgChannelStackDTO
         this.t = t;
     }
 
+    public Integer getSeriesNumber()
+    {
+        return seriesNumber;
+    }
+
+    public void setSeriesNumber(Integer seriesNumber)
+    {
+        this.seriesNumber = seriesNumber;
+    }
+
     public long getDatasetId()
     {
         return datasetId;
@@ -132,10 +150,21 @@ public class ImgChannelStackDTO
         this.spotId = spotId;
     }
 
+    public boolean getIsRepresentative()
+    {
+        return isRepresentative;
+    }
+
+    public void setRepresentative(boolean isRepresentative)
+    {
+        this.isRepresentative = isRepresentative;
+    }
+
     @Override
     // use all fields besides id
     public int hashCode()
     {
+        // NOTE: isRepresentative field is derived and can be skipped
         final int prime = 31;
         int result = 1;
         result = prime * result + (int) (datasetId ^ (datasetId >>> 32));
@@ -144,6 +173,7 @@ public class ImgChannelStackDTO
         result = prime * result + ((spotId == null) ? 0 : spotId.hashCode());
         result = prime * result + ((t == null) ? 0 : t.hashCode());
         result = prime * result + ((z == null) ? 0 : z.hashCode());
+        result = prime * result + ((seriesNumber == null) ? 0 : seriesNumber.hashCode());
         return result;
     }
 
@@ -151,6 +181,7 @@ public class ImgChannelStackDTO
     // use all fields besides id
     public boolean equals(Object obj)
     {
+        // NOTE: isRepresentative field is derived and can be skipped
         if (obj == null)
             return false;
         if (this == obj)
@@ -192,6 +223,12 @@ public class ImgChannelStackDTO
                 return false;
         } else if (!z.equals(other.z))
             return false;
+        if (seriesNumber == null)
+        {
+            if (other.seriesNumber != null)
+                return false;
+        } else if (!seriesNumber.equals(other.seriesNumber))
+            return false;
         return true;
     }
 
diff --git a/screening/source/sql/postgresql/009/schema-009.sql b/screening/source/sql/postgresql/009/schema-009.sql
index 834d82dbf0e26750adc8b42c6e639e5271d78010..a94fe7dceadada7ec33792d554f474255de81770 100644
--- a/screening/source/sql/postgresql/009/schema-009.sql
+++ b/screening/source/sql/postgresql/009/schema-009.sql
@@ -112,13 +112,9 @@ CREATE TABLE CHANNEL_STACKS (
 		X INTEGER,
 		Y INTEGER,
 		-- We use the fixed dimension meters here.
-		-- Not null only if SERIES_NUMBER is null.
 		Z_in_M REAL,
 		-- We use the fixed dimension seconds here.
-		-- Not null only if SERIES_NUMBER is null.
 		T_in_SEC REAL,
-		-- not null if and only if t and z are null
-		-- TODO: write constraint which checks this
 		SERIES_NUMBER INTEGER,
 		
 		-- For all channel stacks of a well (HCS) or image dataset (microscopy) there should be exactly 
diff --git a/screening/source/sql/postgresql/migration/migration-008-009.sql b/screening/source/sql/postgresql/migration/migration-008-009.sql
index 65f164222760cf2fb165b6b12aada8366b1f59d2..54d146739b65a4115b32e4e70ad2b102c6ce879b 100644
--- a/screening/source/sql/postgresql/migration/migration-008-009.sql
+++ b/screening/source/sql/postgresql/migration/migration-008-009.sql
@@ -33,3 +33,11 @@ $$ LANGUAGE 'plpgsql';
 CREATE TRIGGER CHANNEL_STACKS_CHECK BEFORE INSERT OR UPDATE ON CHANNEL_STACKS
     FOR EACH ROW EXECUTE PROCEDURE CHANNEL_STACKS_CHECK();
     
+--- for each spot set exactly one representative channel_stacks record (with minimal id) ---------
+
+update channel_stacks as cs
+   set cs.is_representative = 'T'
+ where cs.id in (select min(cs.id) 
+		 from channel_stacks cs
+		 join spots on spots.id = cs.spot_id
+		 group by spots.id)
\ No newline at end of file
diff --git a/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/etl/ImagesRepresentativesOracleTest.java b/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/etl/ImagesRepresentativesOracleTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1b77f1efa8dd03f7b117b4314c2ab0e7cd3c1472
--- /dev/null
+++ b/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/etl/ImagesRepresentativesOracleTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2010 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.openbis.dss.etl;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.testng.AssertJUnit;
+import org.testng.annotations.Test;
+
+import ch.rinn.restrictions.Friend;
+import ch.systemsx.cisd.openbis.dss.etl.AbstractImageDatasetUploader.ChannelStackRepresentativesOracle;
+import ch.systemsx.cisd.openbis.plugin.screening.shared.imaging.dataaccess.ImgChannelStackDTO;
+
+/**
+ * @author Tomasz Pylak
+ */
+@Friend(toClasses = ChannelStackRepresentativesOracle.class)
+public class ImagesRepresentativesOracleTest extends AssertJUnit
+{
+    @Test
+    public void testWithSpots()
+    {
+        ImgChannelStackDTO img1 = createStack(2, 2, 2L, 1f, 1f);
+        ImgChannelStackDTO img2 = createStack(1, 1, 1L, 200f, 200f);
+        ImgChannelStackDTO img3 = createStack(1, 1, 1L, 1f, 1f);
+
+        Set<ImgChannelStackDTO> representatives = calculateRepresentatives(img1, img2, img3);
+        assertTrue(representatives.contains(img1));
+        assertFalse(representatives.contains(img2));
+        assertTrue(representatives.contains(img3));
+    }
+
+    private Set<ImgChannelStackDTO> calculateRepresentatives(ImgChannelStackDTO... stacks)
+    {
+        Set<ImgChannelStackDTO> stacksSet = new HashSet<ImgChannelStackDTO>();
+        for (ImgChannelStackDTO stack : stacks)
+        {
+            stacksSet.add(stack);
+        }
+        Set<ImgChannelStackDTO> representatives =
+                ChannelStackRepresentativesOracle.calculateRepresentatives(stacksSet);
+        return representatives;
+    }
+
+    @Test
+    public void testWithoutSpots()
+    {
+        ImgChannelStackDTO img1 = createStack(2, 2, null, 1f, 1f);
+        ImgChannelStackDTO img2 = createStack(1, 1, null, 200f, 200f);
+        ImgChannelStackDTO img3 = createStack(1, 1, null, 1f, 1f);
+
+        Set<ImgChannelStackDTO> representatives = calculateRepresentatives(img1, img2, img3);
+        assertFalse(representatives.contains(img1));
+        assertFalse(representatives.contains(img2));
+        assertTrue(representatives.contains(img3));
+    }
+
+    private ImgChannelStackDTO createStack(int row, int column, Long spotIdOrNull, Float tOrNull,
+            Float zOrNull)
+    {
+        return new ImgChannelStackDTO(0L, row, column, 0L, spotIdOrNull, tOrNull, zOrNull, null,
+                false);
+    }
+}
diff --git a/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/etl/dataaccess/ImagingQueryDAOTest.java b/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/etl/dataaccess/ImagingQueryDAOTest.java
index 1994fed759163733b9c37ac79facc1f14e66d66b..f12cfbaf16820089276f1f8d63fca3ef059478e2 100644
--- a/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/etl/dataaccess/ImagingQueryDAOTest.java
+++ b/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/etl/dataaccess/ImagingQueryDAOTest.java
@@ -129,12 +129,20 @@ public class ImagingQueryDAOTest extends AbstractDBTest
         assertEquals(MICROSCOPY_IMAGE_PATH1, image1.getFilePath());
         assertEquals(ColorComponent.BLUE, image1.getColorComponent());
 
+        ImgImageDTO representativeImage =
+                dao.tryGetMicroscopyRepresentativeImage(datasetId, channelId);
+        assertEquals(image1, representativeImage);
+
         ImgImageDTO image1bis = dao.tryGetImage(channelId, channelStackId, datasetId);
         assertEquals(image1, image1bis);
 
         ImgImageDTO thumbnail1 = dao.tryGetMicroscopyThumbnail(channelId, datasetId, tileLocation);
         assertEquals(image1, thumbnail1);
 
+        ImgImageDTO representativeThumbnail =
+                dao.tryGetMicroscopyRepresentativeThumbnail(datasetId, channelId);
+        assertEquals(image1, representativeThumbnail);
+
         ImgImageDTO thumbnail1bis = dao.tryGetThumbnail(channelId, channelStackId, datasetId);
         assertEquals(thumbnail1, thumbnail1bis);
     }
@@ -249,6 +257,10 @@ public class ImagingQueryDAOTest extends AbstractDBTest
         assertEquals(PATH1, image1.getFilePath());
         assertEquals(ColorComponent.BLUE, image1.getColorComponent());
 
+        ImgImageDTO representativeImage =
+                dao.tryGetHCSRepresentativeImage(datasetId, wellLocation, channelId1);
+        assertEquals(image1, representativeImage);
+
         ImgImageDTO image1bis = dao.tryGetImage(channelId1, channelStackId, datasetId);
         assertEquals(image1, image1bis);
 
@@ -256,6 +268,10 @@ public class ImagingQueryDAOTest extends AbstractDBTest
                 dao.tryGetHCSThumbnail(channelId1, datasetId, tileLocation, wellLocation);
         assertEquals(image1, thumbnail1);
 
+        ImgImageDTO representativeThumbnail =
+                dao.tryGetHCSRepresentativeThumbnail(datasetId, wellLocation, channelId1);
+        assertEquals(thumbnail1, representativeThumbnail);
+
         ImgImageDTO thumbnail1bis = dao.tryGetThumbnail(channelId1, channelStackId, datasetId);
         assertEquals(thumbnail1, thumbnail1bis);
 
@@ -379,7 +395,7 @@ public class ImagingQueryDAOTest extends AbstractDBTest
     {
         final ImgChannelStackDTO channelStack =
                 new ImgChannelStackDTO(dao.createChannelStackId(), Y_TILE_ROW, X_TILE_COLUMN,
-                        datasetId, spotIdOrNull, timeOrNull, depthOrNull);
+                        datasetId, spotIdOrNull, timeOrNull, depthOrNull, null, true);
         dao.addChannelStacks(Arrays.asList(channelStack));
         return channelStack.getId();
     }
diff --git a/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageChannelsUtilsTest.java b/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageChannelsUtilsTest.java
index 8b4e89434c1c60d29bb8c51f05c425d6b305720f..d1cd52963926070df64b0d7f5d473942d0b1b74a 100644
--- a/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageChannelsUtilsTest.java
+++ b/screening/sourceTest/java/ch/systemsx/cisd/openbis/dss/generic/server/images/ImageChannelsUtilsTest.java
@@ -29,6 +29,7 @@ import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import ch.rinn.restrictions.Friend;
 import ch.systemsx.cisd.base.exceptions.CheckedExceptionTunnel;
 import ch.systemsx.cisd.base.image.IImageTransformer;
 import ch.systemsx.cisd.base.image.IImageTransformerFactory;
@@ -43,6 +44,7 @@ import ch.systemsx.cisd.openbis.dss.generic.shared.utils.ImageUtil;
 /**
  * @author Franz-Josef Elmer
  */
+@Friend(toClasses = ImageChannelsUtils.class)
 public class ImageChannelsUtilsTest extends AssertJUnit
 {
     public static final File TEST_IMAGE_FOLDER = new File("../screening/sourceTest/java/"
diff --git a/screening/sourceTest/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/LogicalImageSeriesViewerModelTest.java b/screening/sourceTest/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/LogicalImageSeriesViewerModelTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..04fc91d9732325bbf35a58248ebdf764a45eb229
--- /dev/null
+++ b/screening/sourceTest/java/ch/systemsx/cisd/openbis/plugin/screening/client/web/client/application/detailviewers/LogicalImageSeriesViewerModelTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2010 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.openbis.plugin.screening.client.web.client.application.detailviewers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.testng.AssertJUnit;
+import org.testng.annotations.Test;
+
+import ch.systemsx.cisd.openbis.plugin.screening.client.web.client.application.detailviewers.LogicalImageSeriesViewer.ImageSeriesPoint;
+import ch.systemsx.cisd.openbis.plugin.screening.client.web.client.application.detailviewers.LogicalImageSeriesViewer.LogicalImageSeriesViewerModel;
+import ch.systemsx.cisd.openbis.plugin.screening.shared.basic.dto.ImageChannelStack;
+
+/**
+ * Unit tests of {@link LogicalImageSeriesViewerModel}.
+ * 
+ * @author Tomasz Pylak
+ */
+public class LogicalImageSeriesViewerModelTest extends AssertJUnit
+{
+
+    @Test
+    public void testModel()
+    {
+        Float times[] = new Float[]
+            { 3.4f, null, 1.2f, 6.6f };
+        Float depths[] = new Float[]
+            { 66.6f, 33.4f, null, 11.2f };
+        Integer series[] = new Integer[]
+            { 1, 2, 3, null };
+
+        LogicalImageSeriesViewerModel model = createModel(times, depths, series);
+
+        List<ImageSeriesPoint> sortedPoints = model.getSortedPoints();
+        ImageSeriesPoint firstPoint = new ImageSeriesPoint(null, null, null);
+        assertEquals(firstPoint, sortedPoints.get(0));
+        ImageSeriesPoint lastPoint = new ImageSeriesPoint(6.6f, 66.6f, 3);
+        assertEquals(lastPoint, sortedPoints.get(sortedPoints.size() - 1));
+
+        List<List<ImageChannelStack>> stackSeriesPoints = model.getSortedChannelStackSeriesPoints();
+        List<ImageChannelStack> firstList = stackSeriesPoints.get(0);
+        assertEquals(firstPoint, new ImageSeriesPoint(firstList.get(0)));
+
+        List<ImageChannelStack> lastList = stackSeriesPoints.get(stackSeriesPoints.size() - 1);
+        assertEquals(lastPoint, new ImageSeriesPoint(lastList.get(0)));
+    }
+
+    private LogicalImageSeriesViewerModel createModel(Float[] times, Float[] depths,
+            Integer[] series)
+    {
+        List<ImageChannelStack> stacks = new ArrayList<ImageChannelStack>();
+        for (int time = 0; time < times.length; time++)
+        {
+            for (int depth = 0; depth < depths.length; depth++)
+            {
+                for (int seriesNum = 0; seriesNum < series.length; seriesNum++)
+                {
+                    stacks.add(mkStack(1, 1, times[time], depths[depth], series[seriesNum]));
+                    stacks.add(mkStack(2, 2, times[time], depths[depth], series[seriesNum]));
+                }
+            }
+        }
+        return new LogicalImageSeriesViewerModel(stacks);
+    }
+
+    private static ImageChannelStack mkStack(int row, int col, Float tOrNull, Float zOrNull,
+            Integer seriesNumberOrNull)
+    {
+        return new ImageChannelStack(0, row, col, tOrNull, zOrNull, seriesNumberOrNull);
+    }
+}
diff --git a/screening/sourceTest/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/dataaccess/ImgChannelStackDTOTest.java b/screening/sourceTest/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/dataaccess/ImgChannelStackDTOTest.java
index 0748606ef44f2bb63652cc6dc8a0453ed157f59f..fb1e839134b01802e56ea1e4b89e368d336e1378 100644
--- a/screening/sourceTest/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/dataaccess/ImgChannelStackDTOTest.java
+++ b/screening/sourceTest/java/ch/systemsx/cisd/openbis/plugin/screening/shared/imaging/dataaccess/ImgChannelStackDTOTest.java
@@ -54,7 +54,7 @@ public class ImgChannelStackDTOTest
 
     private ImgChannelStackDTO createStackChannel(Long spotId)
     {
-        return new ImgChannelStackDTO(0, 1, 1, 1, spotId, 123F, null);
+        return new ImgChannelStackDTO(0, 1, 1, 1, spotId, 123F, null, null, false);
     }
 
 }