Repository: mobie/mobie-viewer-fiji (language: python)

Issue:
Navigation does not work for segmentation table in mergedGrid
Navigating to segments via selecting rows in the tables or segments in the image does not work for segmentations in a merged grid. There are two issues:
- when selecting a segment the viewer navigates to a wrong location (outside the image space), see second screenshot below
- the selection applies to all cells with the same id, not just the cell with this id in the image at hand. we somehow need to take the image-id into account here.

The example is from https://github.com/mobie/covid-if-project using the "cell segmentation".

![aaa-nav1](https://user-images.githubusercontent.com/4263537/142634667-fc17d7c1-afdd-421b-9422-7dbab874fd11.png)
![aaa-nav2](https://user-images.githubusercontent.com/4263537/142634674-61211599-13b6-4001-9bbb-c889f50a448a.png)
![aaa-nav3](https://user-images.githubusercontent.com/4263537/142634690-a08b59db-6462-4bf7-b7d0-ca5a8f78e7d5.png)


Navigation does not work for segmentation table in mergedGrid
Navigating to segments via selecting rows in the tables or segments in the image does not work for segmentations in a merged grid. There are two issues:
- when selecting a segment the viewer navigates to a wrong location (outside the image space), see second screenshot below
- the selection applies to all cells with the same id, not just the cell with this id in the image at hand. we somehow need to take the image-id into account here.

The example is from https://github.com/mobie/covid-if-project using the "cell segmentation".

![aaa-nav1](https://user-images.githubusercontent.com/4263537/142634667-fc17d7c1-afdd-421b-9422-7dbab874fd11.png)
![aaa-nav2](https://user-images.githubusercontent.com/4263537/142634674-61211599-13b6-4001-9bbb-c889f50a448a.png)
![aaa-nav3](https://user-images.githubusercontent.com/4263537/142634690-a08b59db-6462-4bf7-b7d0-ca5a8f78e7d5.png)




Discussion / hints:

Fix patch:
diff --git a/src/main/java/org/embl/mobie/viewer/MoBIE.java b/src/main/java/org/embl/mobie/viewer/MoBIE.java
index 9329bc9c0..c670ccfea 100644
--- a/src/main/java/org/embl/mobie/viewer/MoBIE.java
+++ b/src/main/java/org/embl/mobie/viewer/MoBIE.java
@@ -202,6 +202,7 @@ private void openDataset( String datasetName ) throws IOException
 		sourceNameToSourceAndConverter = new ConcurrentHashMap<>();
 		setDatasetName( datasetName );
 		dataset = new DatasetJsonParser().parseDataset( getDatasetPath( "dataset.json" ) );
+		//dataset.sources
 
 		userInterface = new UserInterface( this );
 		viewManager = new ViewManager( this, userInterface, dataset.is2D, dataset.timepoints );
@@ -362,7 +363,7 @@ private List< TableRowImageSegment > loadImageSegmentsTable( String sourceName,
 
 		final String defaultTablePath = getTablePath( tableSource, tableName );
 
-		final List< TableRowImageSegment > segments = MoBIEUtils.createAnnotatedImageSegmentsFromTableFile( defaultTablePath, displaySourceName );
+		final List< TableRowImageSegment > segments = MoBIEUtils.createAnnotatedImageSegmentsFromTableFile( defaultTablePath, sourceName );
 
 		return segments;
 	}
@@ -398,17 +399,21 @@ private List< Map< String, List< String > > > loadAdditionalTables( List<String>
 		return additionalTables;
 	}
 
+	public Map< String, SourceAndConverter< ? > > getSourceNameToSourceAndConverter()
+	{
+		return sourceNameToSourceAndConverter;
+	}
 
-	private ArrayList< List< TableRowImageSegment > > loadPrimarySegmentsTables(SegmentationSourceDisplay segmentationDisplay, String table )
+	private ArrayList< List< TableRowImageSegment > > loadPrimarySegmentsTables( SegmentationSourceDisplay segmentationDisplay, String table )
 	{
 		final List< String > segmentationDisplaySources = segmentationDisplay.getSources();
 		final ConcurrentHashMap< String, Set< Source > > sourceNameToRootSources = new ConcurrentHashMap();
 
 		for ( String sourceName : segmentationDisplaySources )
 		{
-			Set< Source > rootSourceNames = ConcurrentHashMap.newKeySet();
-			BdvPlaygroundUtils.fetchRootSources( getSourceAndConverter( sourceName ).getSpimSource(), rootSourceNames );
-			sourceNameToRootSources.put( sourceName, rootSourceNames );
+			Set< Source > rootSources = ConcurrentHashMap.newKeySet();
+			BdvPlaygroundUtils.fetchRootSources( getSourceAndConverter( sourceName ).getSpimSource(), rootSources );
+			sourceNameToRootSources.put( sourceName, rootSources );
 		}
 
 		// TODO: make parallel
diff --git a/src/main/java/org/embl/mobie/viewer/SourceNameEncoder.java b/src/main/java/org/embl/mobie/viewer/SourceNameEncoder.java
new file mode 100644
index 000000000..6094fd69a
--- /dev/null
+++ b/src/main/java/org/embl/mobie/viewer/SourceNameEncoder.java
@@ -0,0 +1,68 @@
+package org.embl.mobie.viewer;
+
+import net.imglib2.type.numeric.integer.UnsignedIntType;
+import net.imglib2.type.volatiles.VolatileUnsignedIntType;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+public abstract class SourceNameEncoder
+{
+	private static Map< String, Long > nameToLong = new HashMap< String, Long >();
+	private static Map< Long, String > longToName = new HashMap< Long, String >();
+	private static long count = 0;
+
+	public static Integer valueBits = 16;
+
+	public static synchronized void addNames( Collection< String > names )
+	{
+		for ( String name : names )
+		{
+			addName( name );
+		}
+	}
+
+	public static void addName( String name )
+	{
+		if ( ! nameToLong.containsKey( name ) )
+		{
+			nameToLong.put( name, count );
+			longToName.put( count, name );
+			count++;
+		}
+	}
+
+	public static String getName( final UnsignedIntType unsignedIntType )
+	{
+		final long imageIndex = unsignedIntType.get() >> valueBits;
+		final String name = longToName.get( imageIndex );
+		return name;
+	}
+
+	public static String getName( final VolatileUnsignedIntType unsignedIntType )
+	{
+		final long imageIndex = unsignedIntType.get().get() >> valueBits;
+		final String name = longToName.get( imageIndex );
+		return name;
+	}
+
+	public static long getValue( final UnsignedIntType unsignedIntType )
+	{
+		final long value = unsignedIntType.get() & 0x0000FFFF;
+		return value;
+	}
+
+	public static long getValue( final VolatileUnsignedIntType unsignedIntType )
+	{
+		final long value = unsignedIntType.get().get() & 0x0000FFFF;
+		return value;
+	}
+
+	public static void encodeName( final UnsignedIntType value, final String name )
+	{
+		final long encoded = value.get() + ( nameToLong.get( name ) << valueBits );
+		value.set( encoded );
+	}
+
+}
diff --git a/src/main/java/org/embl/mobie/viewer/bdv/view/SegmentationSliceView.java b/src/main/java/org/embl/mobie/viewer/bdv/view/SegmentationSliceView.java
index 79f2e5aa7..abc0f1f46 100644
--- a/src/main/java/org/embl/mobie/viewer/bdv/view/SegmentationSliceView.java
+++ b/src/main/java/org/embl/mobie/viewer/bdv/view/SegmentationSliceView.java
@@ -14,6 +14,7 @@
 import de.embl.cba.tables.select.SelectionListener;
 import mpicbg.spim.data.sequence.VoxelDimensions;
 import net.imglib2.realtransform.AffineTransform3D;
+import org.embl.mobie.viewer.transform.MergedGridSource;
 import sc.fiji.bdvpg.bdv.BdvHandleHelper;
 import sc.fiji.bdvpg.bdv.navigate.ViewerTransformChanger;
 import sc.fiji.bdvpg.scijava.services.SourceAndConverterBdvDisplayService;
@@ -81,10 +82,8 @@ private void show( )
 		for ( String name: sourceNameToSourceAndConverter.keySet() )
 		{
 			SourceAndConverter< ? > sourceAndConverter = sourceNameToSourceAndConverter.get( name );
-			LabelConverter< S > labelConverter = new LabelConverter(
-					display.segmentAdapter,
-					sourceAndConverter.getSpimSource().getName(),
-					display.coloringModel );
+
+			LabelConverter< S > labelConverter = getLabelConverter( sourceAndConverter );
 
 			SourceAndConverter< ? > sourceAndLabelConverter = asSourceAndLabelConverter( sourceAndConverter, labelConverter );
 
@@ -94,6 +93,27 @@ private void show( )
 		return sourceNameToLabelSourceAndConverter;
 	}
 
+	private LabelConverter< S > getLabelConverter( SourceAndConverter< ? > sourceAndConverter )
+	{
+		if ( MergedGridSource.instanceOf( sourceAndConverter ) )
+		{
+			// The source name is not the one from which the
+			// image segments should be fetched.
+			// Thus, the constructor where the source name
+			// is determined from encoding in the label is chosen.
+			return new LabelConverter(
+					display.segmentAdapter,
+					display.coloringModel );
+		}
+		else
+		{
+			return new LabelConverter(
+					display.segmentAdapter,
+					sourceAndConverter.getSpimSource().getName(),
+					display.coloringModel );
+		}
+	}
+
 	private SourceAndConverter asSourceAndLabelConverter( SourceAndConverter< ? > sourceAndConverter, LabelConverter labelConverter )
 	{
 		LabelSource volatileLabelSource = new LabelSource( sourceAndConverter.asVolatile().getSpimSource() );
@@ -151,7 +171,7 @@ public synchronized void focusEvent( S selection )
 	private void adaptPosition( double[] position, String sourceName )
 	{
 		// get source transform
-		final SourceAndConverter< ? > sourceAndConverter = display.sourceNameToSourceAndConverter.get( sourceName );
+		final SourceAndConverter< ? > sourceAndConverter = moBIE.getSourceAndConverter( sourceName );
 		AffineTransform3D sourceTransform = new AffineTransform3D();
 		sourceAndConverter.getSpimSource().getSourceTransform( 0,0, sourceTransform );
 
diff --git a/src/main/java/org/embl/mobie/viewer/bdv/view/SliceViewer.java b/src/main/java/org/embl/mobie/viewer/bdv/view/SliceViewer.java
index 1027cdb6d..1265ed3f9 100644
--- a/src/main/java/org/embl/mobie/viewer/bdv/view/SliceViewer.java
+++ b/src/main/java/org/embl/mobie/viewer/bdv/view/SliceViewer.java
@@ -2,6 +2,7 @@
 
 import bdv.util.BdvHandle;
 import bdv.viewer.SourceAndConverter;
+import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.bdv.MobieBdvSupplier;
 import org.embl.mobie.viewer.bdv.MobieSerializableBdvOptions;
 import org.embl.mobie.viewer.bdv.SourcesAtMousePositionSupplier;
diff --git a/src/main/java/org/embl/mobie/viewer/color/LabelConverter.java b/src/main/java/org/embl/mobie/viewer/color/LabelConverter.java
index 6b97b512a..801ef45de 100644
--- a/src/main/java/org/embl/mobie/viewer/color/LabelConverter.java
+++ b/src/main/java/org/embl/mobie/viewer/color/LabelConverter.java
@@ -29,6 +29,9 @@
 package org.embl.mobie.viewer.color;
 
 import bdv.viewer.TimePointListener;
+import net.imglib2.type.numeric.integer.UnsignedIntType;
+import net.imglib2.type.volatiles.VolatileUnsignedIntType;
+import org.embl.mobie.viewer.SourceNameEncoder;
 import org.embl.mobie.viewer.segment.SegmentAdapter;
 import de.embl.cba.tables.imagesegment.ImageSegment;
 import net.imglib2.Volatile;
@@ -45,6 +48,16 @@ public class LabelConverter< S extends ImageSegment > implements Converter< Real
 	private int timePointIndex = 0;
 	private double opacity = 1.0;
 
+	// No imageId given => decode from pixel value
+	public LabelConverter(
+			SegmentAdapter< S > segmentAdapter,
+			MoBIEColoringModel< S > coloringModel )
+	{
+		this.segmentAdapter = segmentAdapter;
+		this.imageId = null;
+		this.coloringModel = coloringModel;
+	}
+
 	public LabelConverter(
 			SegmentAdapter< S > segmentAdapter,
 			String imageId,
@@ -60,7 +73,7 @@ public void convert( RealType label, ARGBType color )
 	{
 		if ( label instanceof Volatile )
 		{
-			if ( ! ( ( Volatile ) label ).isValid() )
+			if ( !( ( Volatile ) label ).isValid() )
 			{
 				color.set( 0 );
 				return;
@@ -73,7 +86,7 @@ public void convert( RealType label, ARGBType color )
 			return;
 		}
 
-		final S imageSegment = segmentAdapter.getSegment( label.getRealDouble(), timePointIndex, imageId );
+		S imageSegment = getImageSegment( label );
 
 		if ( imageSegment == null )
 		{
@@ -90,6 +103,25 @@ public void convert( RealType label, ARGBType color )
 		color.mul( opacity );
 	}
 
+	// TODO: figure out how to make this work for more types
+	private S getImageSegment( RealType label )
+	{
+		if ( imageId == null )
+		{
+			final long value = SourceNameEncoder.getValue( ( VolatileUnsignedIntType ) label );
+			if ( value == 0 )
+			{
+				return null; // background
+			}
+			final String imageId = SourceNameEncoder.getName( ( VolatileUnsignedIntType ) label );
+			return segmentAdapter.getSegmentCreateIfNotExist( value, timePointIndex, imageId );
+		}
+		else
+		{
+			return segmentAdapter.getSegmentCreateIfNotExist( label.getRealDouble(), timePointIndex, imageId );
+		}
+	}
+
 	@Override
 	public void timePointChanged( int timePointIndex )
 	{
diff --git a/src/main/java/org/embl/mobie/viewer/segment/BdvSegmentSelector.java b/src/main/java/org/embl/mobie/viewer/segment/BdvSegmentSelector.java
index f1d1fef77..f17487519 100644
--- a/src/main/java/org/embl/mobie/viewer/segment/BdvSegmentSelector.java
+++ b/src/main/java/org/embl/mobie/viewer/segment/BdvSegmentSelector.java
@@ -1,17 +1,24 @@
 package org.embl.mobie.viewer.segment;
 
+import bdv.tools.transformation.TransformedSource;
 import bdv.util.BdvHandle;
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
+import ij.IJ;
+import org.embl.mobie.io.n5.source.LabelSource;
+import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.bdv.BdvMousePositionProvider;
 import org.embl.mobie.viewer.display.SegmentationSourceDisplay;
 import de.embl.cba.tables.tablerow.TableRowImageSegment;
 import net.imglib2.RandomAccess;
 import net.imglib2.RealPoint;
 import net.imglib2.type.numeric.RealType;
+import org.embl.mobie.viewer.transform.MergedGridSource;
 import sc.fiji.bdvpg.sourceandconverter.SourceAndConverterHelper;
 
 import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
 import java.util.function.Supplier;
 
 public class BdvSegmentSelector implements Runnable
@@ -47,26 +54,81 @@ private synchronized void toggleSelectionAtMousePosition()
 
 		for ( SegmentationSourceDisplay segmentationDisplay : segmentationDisplays )
 		{
-			for ( SourceAndConverter< ? > sourceAndConverter : segmentationDisplay.sourceNameToSourceAndConverter.values() )
+			final Collection< SourceAndConverter< ? > > displayedSourceAndConverters = segmentationDisplay.sourceNameToSourceAndConverter.values();
+			for ( SourceAndConverter< ? > displayedSourceAndConverter : displayedSourceAndConverters )
 			{
-				if ( SourceAndConverterHelper.isPositionWithinSourceInterval( sourceAndConverter, position, timePoint, is2D ) )
+				final boolean sourceVisible = bdvHandle.getViewerPanel().state().isSourceVisible( displayedSourceAndConverter );
+				if ( ! sourceVisible )
 				{
-					final Source< ? > source = sourceAndConverter.getSpimSource();
-					final double labelIndex = getPixelValue( timePoint, position, source );
-					if ( labelIndex == 0 ) return;
+					continue;
+				}
 
-					final TableRowImageSegment segment = segmentationDisplay.segmentAdapter.getSegment( labelIndex, timePoint, source.getName() );
+				// The source might be a MergedGridSource and thereby represent several sources that
+				// need to be inspected for whether they contain selectable image segments.
+				// TODO: Probably more efficient and consistent to simplify this,
+				//   using the image name encoding instead.
+				final Collection< SourceAndConverter< ? > > sourceAndConverters = getContainedSourceAndConverters( displayedSourceAndConverter );
 
-					segmentationDisplay.selectionModel.toggle( segment );
-					if ( segmentationDisplay.selectionModel.isSelected( segment ) )
+				for ( SourceAndConverter< ? > sourceAndConverter : sourceAndConverters )
+				{
+					if ( SourceAndConverterHelper.isPositionWithinSourceInterval( sourceAndConverter, position, timePoint, is2D ) )
 					{
-						segmentationDisplay.selectionModel.focus( segment );
+						final Source< ? > source = sourceAndConverter.getSpimSource();
+
+						final double labelIndex = getPixelValue( timePoint, position, source );
+						if ( labelIndex == 0 ) continue; // background
+
+						final String sourceName = source.getName();
+
+						final boolean containsSegment = segmentationDisplay.segmentAdapter.containsSegment( labelIndex, timePoint, sourceName );
+
+						if ( ! containsSegment )
+						{
+							continue;
+						}
+
+						final TableRowImageSegment segment = segmentationDisplay.segmentAdapter.getSegment( labelIndex, timePoint, sourceName );
+
+						segmentationDisplay.selectionModel.toggle( segment );
+						if ( segmentationDisplay.selectionModel.isSelected( segment ) )
+						{
+							segmentationDisplay.selectionModel.focus( segment );
+						}
 					}
 				}
 			}
 		}
 	}
 
+	private Collection< SourceAndConverter< ? > > getContainedSourceAndConverters( SourceAndConverter< ? > sourceAndConverter )
+	{
+		final Collection< SourceAndConverter< ? > > containedSourceAndConverters = new HashSet<>();
+
+		Source< ? > source = sourceAndConverter.getSpimSource();
+
+		if ( source instanceof LabelSource )
+		{
+			source = ( ( LabelSource ) source ).getWrappedSource();
+		}
+
+		if ( source instanceof TransformedSource )
+		{
+			source = ( ( TransformedSource< ? > ) source ).getWrappedSource();
+		}
+
+		if ( source instanceof MergedGridSource )
+		{
+			containedSourceAndConverters.addAll( ( ( MergedGridSource ) source ).getContainedSourceAndConverters() );
+		}
+		else
+		{
+			// not a MergedGridSource, just return the initial source
+			containedSourceAndConverters.add( sourceAndConverter );
+		}
+
+		return containedSourceAndConverters;
+	}
+
 	private static double getPixelValue( int timePoint, RealPoint position, Source< ? > source )
 	{
 		final RandomAccess< RealType > randomAccess = ( RandomAccess< RealType > ) source.getSource( timePoint, 0 ).randomAccess();
diff --git a/src/main/java/org/embl/mobie/viewer/segment/SegmentAdapter.java b/src/main/java/org/embl/mobie/viewer/segment/SegmentAdapter.java
index 1e4cd8758..2ba81de7a 100644
--- a/src/main/java/org/embl/mobie/viewer/segment/SegmentAdapter.java
+++ b/src/main/java/org/embl/mobie/viewer/segment/SegmentAdapter.java
@@ -28,15 +28,17 @@ public SegmentAdapter( List< T > segments )
 			labelFrameAndImageToSegment.put( new LabelFrameAndImage( segment ), segment );
 	}
 
-	public T getSegment( double label, int t, String imageId )
+	public synchronized boolean containsSegment( double label, int t, String imageId )
 	{
 		final LabelFrameAndImage labelFrameAndImage = new LabelFrameAndImage( label, t, imageId  );
 
-		return getSegment( labelFrameAndImage );
+		return labelFrameAndImageToSegment.containsKey( labelFrameAndImage );
 	}
 
-	private T getSegment( LabelFrameAndImage labelFrameAndImage )
+	public synchronized T getSegmentCreateIfNotExist( double label, int t, String imageId )
 	{
+		final LabelFrameAndImage labelFrameAndImage = new LabelFrameAndImage( label, t, imageId  );
+
 		if ( ! labelFrameAndImageToSegment.containsKey( labelFrameAndImage ) )
 		{
 			final DefaultImageSegment defaultImageSegment = new DefaultImageSegment( labelFrameAndImage.getImage(), labelFrameAndImage.getLabel(), labelFrameAndImage.getFrame(), 0, 0, 0, null );
@@ -46,6 +48,17 @@ private T getSegment( LabelFrameAndImage labelFrameAndImage )
 		return labelFrameAndImageToSegment.get( labelFrameAndImage );
 	}
 
+	public synchronized T getSegment( double label, int t, String imageId )
+	{
+		final LabelFrameAndImage labelFrameAndImage = new LabelFrameAndImage( label, t, imageId );
+		return getSegment( labelFrameAndImage );
+	}
+
+	public synchronized T getSegment( LabelFrameAndImage labelFrameAndImage )
+	{
+		return labelFrameAndImageToSegment.get( labelFrameAndImage  );
+	}
+
 	// deserialize
 	public List< T > getSegments( List< String > serialisedSegments )
 	{
diff --git a/src/main/java/org/embl/mobie/viewer/transform/MergedGridSource.java b/src/main/java/org/embl/mobie/viewer/transform/MergedGridSource.java
index 336e4c376..30e6180a8 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/MergedGridSource.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/MergedGridSource.java
@@ -1,8 +1,11 @@
 package org.embl.mobie.viewer.transform;
 
+import bdv.tools.transformation.TransformedSource;
 import bdv.util.DefaultInterpolators;
 import bdv.viewer.Interpolation;
 import bdv.viewer.Source;
+import bdv.viewer.SourceAndConverter;
+import net.imglib2.type.numeric.integer.UnsignedIntType;
 import org.embl.mobie.viewer.MoBIEUtils;
 import mpicbg.spim.data.sequence.VoxelDimensions;
 import net.imglib2.Cursor;
@@ -21,6 +24,7 @@
 import net.imglib2.type.numeric.NumericType;
 import net.imglib2.util.Util;
 import net.imglib2.view.Views;
+import org.embl.mobie.viewer.SourceNameEncoder;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -28,6 +32,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
+import java.util.Set;
 
 public class MergedGridSource< T extends NativeType< T > & NumericType< T > > implements Source< T >
 {
@@ -39,16 +44,19 @@ public class MergedGridSource< T extends NativeType< T > & NumericType< T > > im
 	private final List< Source< T > > gridSources;
 	private final List< int[] > positions;
 	private final double relativeCellMargin;
+	private final boolean encodeSource;
 	private int currentTimepoint = 0;
 	private Map< String, long[] > sourceNameToVoxelTranslation;
 	private int[][] cellDimensions;
 	private double[] cellRealDimensions;
+	private Set< SourceAndConverter > containedSourceAndConverters;
 
-	public MergedGridSource( List< Source< T > > gridSources, List< int[] > positions, String mergedGridSourceName, double relativeCellMargin )
+	public MergedGridSource( List< Source< T > > gridSources, List< int[] > positions, String mergedGridSourceName, double relativeCellMargin, boolean encodeSource )
 	{
 		this.gridSources = gridSources;
 		this.positions = positions;
 		this.relativeCellMargin = relativeCellMargin;
+		this.encodeSource = encodeSource;
 		this.interpolators = new DefaultInterpolators<>();
 		this.referenceSource = gridSources.get( 0 );
 		this.mergedGridSourceName = mergedGridSourceName;
@@ -57,6 +65,11 @@ public MergedGridSource( List< Source< T > > gridSources, List< int[] > position
 		mergedRandomAccessibleIntervals = createMergedRandomAccessibleIntervals();
 	}
 
+	public static boolean instanceOf( SourceAndConverter< ? > sourceAndConverter )
+	{
+		return (( TransformedSource ) sourceAndConverter.getSpimSource() ).getWrappedSource() instanceof MergedGridSource || sourceAndConverter.getSpimSource() instanceof MergedGridSource;
+	}
+
 	public List< Source< T > > getGridSources()
 	{
 		return gridSources;
@@ -241,6 +254,7 @@ private long[] computeTranslation( int[] cellDimensions, long[] cellMin, long[]
 		return translation;
 	}
 
+	// TODO: not used
 	public Map< String, long[] > getSourceNameToVoxelTranslation()
 	{
 		return sourceNameToVoxelTranslation;
@@ -309,6 +323,16 @@ public int getNumMipmapLevels()
 		return referenceSource.getNumMipmapLevels();
 	}
 
+	public void setContainedSourceAndConverters( Set< SourceAndConverter > containedSourceAndConverters )
+	{
+		this.containedSourceAndConverters = containedSourceAndConverters;
+	}
+
+	public Set< SourceAndConverter > getContainedSourceAndConverters()
+	{
+		return containedSourceAndConverters;
+	}
+
 	class RandomAccessibleIntervalCellLoader< T extends NativeType< T > > implements CellLoader< T >
 	{
 		private final Map< String, Source< T > > cellKeyToSource;
@@ -339,15 +363,28 @@ public void load( SingleCellArrayImg< T, ? > cell ) throws Exception
 				final long[] offset = computeTranslation( MoBIEUtils.asInts( cell.dimensionsAsLongArray() ), cell.minAsLongArray(), data.dimensionsAsLongArray() );
 				data = Views.translate( Views.zeroMin( data ), offset );
 
-
 				// copy RAI into cell
 				Cursor< T > sourceCursor = Views.iterable( data ).cursor();
 				RandomAccess< T > targetAccess = cell.randomAccess();
 
-				while ( sourceCursor.hasNext() )
+				if ( encodeSource )
+				{
+					final String name = source.getName();
+					while ( sourceCursor.hasNext() )
+					{
+						sourceCursor.fwd();
+						// copy the sourceCursor in order not to modify it by the source name encoding
+						targetAccess.setPositionAndGet( sourceCursor ).set( sourceCursor.get().copy() );
+						SourceNameEncoder.encodeName( ( UnsignedIntType ) targetAccess.get(), name );
+					}
+				}
+				else
 				{
-					sourceCursor.fwd();
-					targetAccess.setPositionAndGet( sourceCursor ).set( sourceCursor.get() );
+					while ( sourceCursor.hasNext() )
+					{
+						sourceCursor.fwd();
+						targetAccess.setPositionAndGet( sourceCursor ).set( sourceCursor.get() );
+					}
 				}
 			}
 		}
diff --git a/src/main/java/org/embl/mobie/viewer/transform/MergedGridSourceTransformer.java b/src/main/java/org/embl/mobie/viewer/transform/MergedGridSourceTransformer.java
index 6470912d1..1f27b7ca1 100644
--- a/src/main/java/org/embl/mobie/viewer/transform/MergedGridSourceTransformer.java
+++ b/src/main/java/org/embl/mobie/viewer/transform/MergedGridSourceTransformer.java
@@ -1,9 +1,11 @@
 package org.embl.mobie.viewer.transform;
 
+import bdv.tools.transformation.TransformedSource;
 import bdv.util.VolatileSource;
 import bdv.viewer.Source;
 import bdv.viewer.SourceAndConverter;
 import de.embl.cba.tables.Logger;
+import ij.IJ;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIEUtils;
 import net.imglib2.FinalRealInterval;
@@ -13,6 +15,8 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.stream.Collectors;
@@ -24,10 +28,12 @@ public class MergedGridSourceTransformer extends AbstractSourceTransformer
 	protected String mergedGridSourceName;
 	protected List< int[] > positions;
 	protected boolean centerAtOrigin = false;
+	protected boolean encodeSource = false;
 
 	// Runtime
 	private transient MergedGridSource< ? > mergedGridSource;
 	private transient double[] translationRealOffset;
+	private transient Set< SourceAndConverter > transformedSourceAndConverters;
 
 	@Override
 	public void transform( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter )
@@ -43,19 +49,21 @@ public void transform( Map< String, SourceAndConverter< ? > > sourceNameToSource
 
 		sourceNameToSourceAndConverter.put( mergedSourceAndConverter.getSpimSource().getName(), mergedSourceAndConverter );
 
-		// needed to know where the individual sources are in space:
-		transformGridSourcesIndividually( sourceNameToSourceAndConverter, gridSources );
+		// Transform (i.e. adapt the positions) all contained sources,
+		// because several parts of the code refer to them and their
+		// positions.
+		transformedSourceAndConverters = ConcurrentHashMap.newKeySet();
+		transformContainedSources( sourceNameToSourceAndConverter, gridSources );
+		mergedGridSource.setContainedSourceAndConverters( transformedSourceAndConverters );
 	}
 
-	private void transformGridSourcesIndividually( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, List< SourceAndConverter< ? > > gridSources )
+	private void transformContainedSources( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, List< SourceAndConverter< ? > > gridSources )
 	{
 		final long start = System.currentTimeMillis();
 
 		final ArrayList< SourceAndConverter< ? > > referenceSources = new ArrayList<>();
 		referenceSources.add( gridSources.get( 0 ) );
 
-		// final double[] gridCellRealDimensions = TransformedGridSourceTransformer.computeGridCellRealDimensions( referenceSources, TransformedGridSourceTransformer.RELATIVE_CELL_MARGIN );
-
 		final double[] gridCellRealDimensions = mergedGridSource.getCellRealDimensions();
 
 		// due to margin...
@@ -71,18 +79,14 @@ private void transformGridSourcesIndividually( Map< String, SourceAndConverter<
 
 			final ArrayList< String > sourceNamesAtGridPosition = getSourcesAtGridPosition( gridSources, finalPositionIndex );
 
-			// translate the source(s) at this grid position
-			// (in fact, here it can only be one source per grid position)
 			executorService.execute( () -> {
-				TransformedGridSourceTransformer.translate( sourceNameToSourceAndConverter, sourceNamesAtGridPosition, null, centerAtOrigin, gridCellRealDimensions[ 0 ] * positions.get( finalPositionIndex )[ 0 ] + translationRealOffset[ 0 ], gridCellRealDimensions[ 1 ] * positions.get( finalPositionIndex )[ 1 ] + translationRealOffset[ 1 ]);
+				recursivelyTransformSources( sourceNameToSourceAndConverter, gridCellRealDimensions, finalPositionIndex, sourceNamesAtGridPosition );
 			} );
 
-			translateSourcesWithinMergedSources( sourceNameToSourceAndConverter, gridCellRealDimensions, executorService, finalPositionIndex, sourceNamesAtGridPosition );
-
 		}
 		MoBIEUtils.waitUntilFinishedAndShutDown( executorService );
 
-		// System.out.println( "Transformed " + sourceNameToSourceAndConverter.size() + " image source(s) in " + (System.currentTimeMillis() - start) + " ms, using " + nThreads + " thread(s)." );
+		IJ.log( "Transformed " + transformedSourceAndConverters.size() + " source(s) in " + (System.currentTimeMillis() - start) + " ms, using " + nThreads + " thread(s)." );
 	}
 
 	private double[] computeTranslationOffset( List< SourceAndConverter< ? > > gridSources, double[] gridCellRealDimensions )
@@ -100,25 +104,43 @@ private double[] computeTranslationOffset( List< SourceAndConverter< ? > > gridS
 		return translationOffset;
 	}
 
-	private void translateSourcesWithinMergedSources( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, double[] gridCellRealDimensions, ExecutorService executorService, int finalPositionIndex, ArrayList< String > sourceNamesAtGridPosition )
+	private void recursivelyTransformSources( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, double[] gridCellRealDimensions, int finalPositionIndex, ArrayList< String > transformedSourceNames )
 	{
-		final ArrayList< String > baseSourceNames = new ArrayList<>();
-		for ( String sourceName : sourceNamesAtGridPosition )
-		{
-			final Source< ? > spimSource = sourceNameToSourceAndConverter.get( sourceName ).getSpimSource();
-			if ( spimSource instanceof MergedGridSource )
-			{
-				baseSourceNames.addAll( ( ( MergedGridSource< ? > ) spimSource ).getGridSources().stream().map( s -> s.getName() ).collect( Collectors.toList() ) ) ;
+		// transform the sources
+		TransformedGridSourceTransformer.translate( sourceNameToSourceAndConverter, transformedSourceNames, null, centerAtOrigin, gridCellRealDimensions[ 0 ] * positions.get( finalPositionIndex )[ 0 ] + translationRealOffset[ 0 ], gridCellRealDimensions[ 1 ] * positions.get( finalPositionIndex )[ 1 ] + translationRealOffset[ 1 ]);
+		addTransformedSources( sourceNameToSourceAndConverter, transformedSourceNames );
 
-			}
+		// if there are any, also transform contained sources
+		final ArrayList< String > containedSourceNames = fetchContainedSourceNames( sourceNameToSourceAndConverter, transformedSourceNames );
+		if ( containedSourceNames.size() > 0 )
+		{
+			recursivelyTransformSources( sourceNameToSourceAndConverter, gridCellRealDimensions, finalPositionIndex, containedSourceNames );
 		}
+	}
+
+	private void addTransformedSources( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, ArrayList< String > containedSourceNames )
+	{
+		final List< SourceAndConverter< ? > > sourceAndConverters = sourceNameToSourceAndConverter.values().stream().filter( sac -> containedSourceNames.contains( sac.getSpimSource().getName() ) ).collect( Collectors.toList() );
+		transformedSourceAndConverters.addAll( sourceAndConverters );
+	}
 
-		if ( baseSourceNames.size() > 0 )
+	private ArrayList< String > fetchContainedSourceNames( Map< String, SourceAndConverter< ? > > sourceNameToSourceAndConverter, ArrayList< String > sourceNames )
+	{
+		final ArrayList< String > containedSourceNames = new ArrayList<>();
+		for ( String sourceName : sourceNames )
 		{
-			executorService.execute( () -> {
-				TransformedGridSourceTransformer.translate( sourceNameToSourceAndConverter, baseSourceNames, null, centerAtOrigin, gridCellRealDimensions[ 0 ] * positions.get( finalPositionIndex )[ 0 ] + translationRealOffset[ 0 ], gridCellRealDimensions[ 1 ] * positions.get( finalPositionIndex )[ 1 ] + translationRealOffset[ 1 ]);
-			} );
+			Source< ? > source = sourceNameToSourceAndConverter.get( sourceName ).getSpimSource();
+			if ( source instanceof TransformedSource )
+			{
+				source = ( ( TransformedSource< ? > ) source ).getWrappedSource();
+			}
+
+			if ( source instanceof MergedGridSource )
+			{
+				containedSourceNames.addAll( ( ( MergedGridSource< ? > ) source ).getGridSources().stream().map( s -> s.getName() ).collect( Collectors.toList() ) ) ;
+			}
 		}
+		return containedSourceNames;
 	}
 
 	private ArrayList< String > getSourcesAtGridPosition( List< SourceAndConverter< ? > > gridSources, int finalPositionIndex )
@@ -136,7 +158,7 @@ public List< String > getSources()
 
 	private SourceAndConverter< ? > createMergedSourceAndConverter( List< Source< ? > > gridSources, Converter< ?, ARGBType > volatileConverter, Converter< ?, ARGBType > converter )
 	{
-		mergedGridSource = new MergedGridSource( gridSources, positions, mergedGridSourceName, TransformedGridSourceTransformer.RELATIVE_CELL_MARGIN );
+		mergedGridSource = new MergedGridSource( gridSources, positions, mergedGridSourceName, TransformedGridSourceTransformer.RELATIVE_CELL_MARGIN, encodeSource );
 
 		final VolatileSource< ?, ? > volatileMergedGridSource = new VolatileSource<>( mergedGridSource, MoBIE.sharedQueue );
 
diff --git a/src/main/java/org/embl/mobie/viewer/view/ViewManager.java b/src/main/java/org/embl/mobie/viewer/view/ViewManager.java
index c1c42f53b..4b30c9a07 100644
--- a/src/main/java/org/embl/mobie/viewer/view/ViewManager.java
+++ b/src/main/java/org/embl/mobie/viewer/view/ViewManager.java
@@ -15,6 +15,7 @@
 import org.embl.mobie.io.n5.source.LabelSource;
 import org.embl.mobie.viewer.MoBIE;
 import org.embl.mobie.viewer.MoBIEUtils;
+import org.embl.mobie.viewer.SourceNameEncoder;
 import org.embl.mobie.viewer.annotate.AnnotatedIntervalAdapter;
 import org.embl.mobie.viewer.annotate.AnnotatedIntervalTableRow;
 import org.embl.mobie.viewer.bdv.view.AnnotatedIntervalSliceView;
@@ -275,6 +276,7 @@ public synchronized void show( View view )
 
 		// fetch the names of all sources that are either shown or to be transformed
 		final Set< String > sources = fetchSources( view );
+		SourceNameEncoder.addNames( sources );
 		final Set< String > datasetSources = sources.stream().filter( s -> moBIE.getDataset().sources.containsKey( s ) ).collect( Collectors.toSet() );
 
 		// open all raw sources
@@ -297,7 +299,7 @@ public synchronized void show( View view )
 			sourceNameToSourceAndConverters.put( sourceName, sourceAndConverter );
 		}
 
-		// register all available sources
+		// register all available sources in MoBIE
 		moBIE.addSourceAndConverters( sourceNameToSourceAndConverters );
 
 		// show the displays


Test patch:
diff --git a/src/test/java/develop/DevelopSourceNameBitEncoding.java b/src/test/java/develop/DevelopSourceNameBitEncoding.java
new file mode 100644
index 000000000..51c44e4e9
--- /dev/null
+++ b/src/test/java/develop/DevelopSourceNameBitEncoding.java
@@ -0,0 +1,42 @@
+package develop;
+
+import net.imglib2.type.numeric.integer.UnsignedIntType;
+import org.embl.mobie.viewer.SourceNameEncoder;
+
+public class DevelopSourceNameBitEncoding
+{
+	public static void main( String[] args )
+	{
+		final UnsignedIntType unsignedIntType = new UnsignedIntType();
+		unsignedIntType.set( 1024 );
+
+		final long label = unsignedIntType.get();
+		System.out.println(Long.toBinaryString((long)label));
+
+		final long image = 10;
+		final long l1 = image << 16;
+		System.out.println(l1);
+		System.out.println(Long.toBinaryString(l1));
+		final long both = label + ( image << 16 );
+		System.out.println(Long.toBinaryString(both));
+		// image
+		final long decodedImage = both >> 16;
+		System.out.println(Long.toBinaryString( decodedImage ));
+		System.out.println("image id: " + decodedImage);
+		// label
+		final long decodedLabel = both & 0x0000FFFF;
+		System.out.println(Long.toBinaryString( decodedLabel ));
+		System.out.println("label id: " + decodedLabel);
+
+		System.out.printf( "----------------------\n" );
+
+		SourceNameEncoder.addName( "hello" );
+		SourceNameEncoder.addName( "world" );
+		final UnsignedIntType labelIndex = new UnsignedIntType( 133 );
+		SourceNameEncoder.encodeName( labelIndex, "hello" );
+		System.out.println(labelIndex.get());
+		System.out.println("name: " + SourceNameEncoder.getName( labelIndex ));
+		System.out.println("value: " + SourceNameEncoder.getValue( labelIndex ));
+
+	}
+}