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

Issue:
Error opening remote dataset on branch
In MoBIE 2 (latest playground branch), opening a remote dataset on a branch other than master fails.

e.g. Running 'OpenRemoteYeastCLEM' with the branch spec-v2 errors with:
```
Exception in thread "main" java.io.FileNotFoundException: https://raw.githubusercontent.com/mobie/yeast-clem-datasets/data/spec-v2/project.json
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1896)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1498)
	at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:268)
	at java.net.URL.openStream(URL.java:1067)
	at de.embl.cba.tables.FileAndUrlUtils.getInputStream(FileAndUrlUtils.java:160)
	at de.embl.cba.tables.FileAndUrlUtils.read(FileAndUrlUtils.java:174)
	at de.embl.cba.mobie2.serialize.ProjectJsonParser.parseProject(ProjectJsonParser.java:19)
	at de.embl.cba.mobie2.MoBIE2.<init>(MoBIE2.java:59)
	at projects.OpenRemoteYeastCLEM.main(OpenRemoteYeastCLEM.java:16)
```
@tischi  I think this might be related to the changes you made to deal with presence / absence of the 'data' folder. The url generated should be ```https://raw.githubusercontent.com/mobie/yeast-clem-datasets/spec-v2/data/project.json``` rather than ```https://raw.githubusercontent.com/mobie/yeast-clem-datasets/data/spec-v2/project.json```
merge packages
Removes the 'mobie' package, and renames 'mobie2' back to 'mobie'

@tischi Can you take a quick look through, and check I haven't deleted anything you still need? I removed everything that wasn't used by any classes in MoBIE2.
I think there may have been some classes that are yet to be implemented in MoBIE2 e.g. the gene search for platybrowser that maybe you still want to keep for now? https://github.com/mobie/mobie-viewer-fiji/blob/playground/src/main/java/de/embl/cba/mobie/platybrowser/GeneSearch.java

Also, there are some old tests, that don't fit the new layout. For now, I just commented out the ones that had errors.


Discussion / hints:

Fix patch:
diff --git a/pom.xml b/pom.xml
index 7eb1b193f..dcc6b92c8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,7 +8,7 @@
 	</parent>
 
 	<artifactId>mobie</artifactId>
-	<version>0.6.2-SNAPSHOT</version>
+	<version>2.0.0-SNAPSHOT</version>
 
 	<url>https://github.com/mobie/mobie-viewer-fiji</url>
 	<name>MoBIE Fiji Viewer</name>
@@ -72,7 +72,7 @@
 		<package-name>de.embl.cba.mobie</package-name>
 		<license.licenseName>bsd_2</license.licenseName>
 		<license.copyrightOwners>EMBL</license.copyrightOwners>
-		<scijava.app.directory>/Users/tischer/Desktop/Fiji/Fiji-MoBIE.app</scijava.app.directory>
+		<scijava.app.directory>/Users/tischer/Desktop/Fiji/Fiji-MoBIE-beta.app</scijava.app.directory>
 		<enforcer.skip>true</enforcer.skip>
 
 		<!-- Java 8	Update Site	-->
@@ -82,9 +82,10 @@
 		<bigdataviewer-vistools.version>1.0.0-beta-28</bigdataviewer-vistools.version>
 
 		<!-- BIOP -->
+		<bigdataviewer-playground.version>0.2.14-SNAPSHOT</bigdataviewer-playground.version>
 
 		<!-- EMBL CBA -->
-		<imagej-utils.version>0.6.3-SNAPSHOT</imagej-utils.version>
+		<imagej-utils.version>0.6.4-SNAPSHOT</imagej-utils.version>
 
 		<!-- Version 1.6.0-scijava-3-SNAPSHOT of j3dcore addresses a minor issue
 		https://github.com/fiji/3D_Viewer/issues/26
@@ -97,8 +98,8 @@
 		on a central repository, and which we will use here for the ome.zarr branch
 		-->
 		<j3dcore.version>1.6.0-scijava-2</j3dcore.version>
-
 	</properties>
+
 	<build>
 		<plugins>
 			<plugin>
@@ -111,6 +112,7 @@
 			</plugin>
 		</plugins>
 	</build>
+
 	<repositories>
 		<repository>
 			<id>imagej.public</id>
@@ -118,6 +120,11 @@
 		</repository>
 	</repositories>
 	<dependencies>
+		<dependency>
+			<groupId>sc.fiji</groupId>
+			<artifactId>bigdataviewer-playground</artifactId>
+			<version>${bigdataviewer-playground.version}</version>
+		</dependency>
 		<dependency>
 			<groupId>net.imglib2</groupId>
 			<artifactId>imglib2</artifactId>
@@ -173,5 +180,10 @@
             <artifactId>annotations</artifactId>
             <version>16.0.2</version>
         </dependency>
+		<dependency>
+			<groupId>com.formdev</groupId>
+			<artifactId>flatlaf</artifactId>
+			<version>1.0</version>
+		</dependency>
 	</dependencies>
 </project>
\ No newline at end of file
diff --git a/src/main/java/de/embl/cba/mobie/BdvPlaygroundHelpers.java b/src/main/java/de/embl/cba/mobie/BdvPlaygroundHelpers.java
new file mode 100644
index 000000000..6113f8a52
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/BdvPlaygroundHelpers.java
@@ -0,0 +1,15 @@
+package de.embl.cba.mobie;
+
+import bdv.util.BdvHandle;
+import bdv.viewer.SourceAndConverter;
+import sc.fiji.bdvpg.services.SourceAndConverterServices;
+
+import java.util.List;
+
+public class BdvPlaygroundHelpers
+{
+	public static BdvHandle getBdvHandle( List< SourceAndConverter< ? > > sourceAndConverters )
+	{
+		return SourceAndConverterServices.getSourceAndConverterDisplayService().getDisplaysOf( sourceAndConverters.get( 0 ) ).iterator().next();
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/Dataset.java b/src/main/java/de/embl/cba/mobie/Dataset.java
new file mode 100644
index 000000000..9c71da471
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/Dataset.java
@@ -0,0 +1,14 @@
+package de.embl.cba.mobie;
+
+import de.embl.cba.mobie.source.SourceSupplier;
+import de.embl.cba.mobie.view.View;
+
+import java.util.Map;
+
+public class Dataset
+{
+	public boolean is2D = false;
+	public int timepoints = 1;
+	public Map< String, SourceSupplier > sources;
+	public Map< String, View > views;
+}
diff --git a/src/main/java/de/embl/cba/mobie/MoBIE.java b/src/main/java/de/embl/cba/mobie/MoBIE.java
new file mode 100644
index 000000000..5bb5afe18
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/MoBIE.java
@@ -0,0 +1,267 @@
+package de.embl.cba.mobie;
+
+import bdv.viewer.SourceAndConverter;
+import de.embl.cba.bdv.utils.BdvUtils;
+import de.embl.cba.mobie.serialize.DatasetJsonParser;
+import de.embl.cba.mobie.serialize.ProjectJsonParser;
+import de.embl.cba.mobie.source.ImageSource;
+import de.embl.cba.mobie.source.SegmentationSource;
+import de.embl.cba.mobie.ui.UserInterface;
+import de.embl.cba.mobie.ui.WindowArrangementHelper;
+import de.embl.cba.mobie.view.View;
+import de.embl.cba.mobie.view.ViewerManager;
+import de.embl.cba.tables.FileAndUrlUtils;
+import de.embl.cba.tables.github.GitHubUtils;
+import ij.IJ;
+import mpicbg.spim.data.SpimData;
+import sc.fiji.bdvpg.PlaygroundPrefs;
+import sc.fiji.bdvpg.sourceandconverter.importer.SourceAndConverterFromSpimDataCreator;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import static de.embl.cba.mobie.Utils.getName;
+
+public class MoBIE
+{
+	public static final int N_THREADS = 8;
+	public static final String PROTOTYPE_DISPLAY_VALUE = "01234567890123456789";
+
+	private final String projectName;
+	private MoBIESettings settings;
+	private String datasetName;
+	private Dataset dataset;
+	private ViewerManager viewerManager;
+	private Project project;
+	private UserInterface userInterface;
+	private String projectRoot;
+	private String imageRoot;
+	private String tableRoot;
+
+	public MoBIE(String projectRoot ) throws IOException
+	{
+		this( projectRoot, MoBIESettings.settings() );
+	}
+
+	public MoBIE(String projectLocation, MoBIESettings settings ) throws IOException
+	{
+		IJ.log("MoBIE");
+		this.settings = settings.projectLocation( projectLocation );
+		setProjectImageAndTableRootLocations( this.settings );
+		projectName = getName( projectLocation );
+		PlaygroundPrefs.setSourceAndConverterUIVisibility( false );
+		project = new ProjectJsonParser().parseProject( FileAndUrlUtils.combinePath( projectRoot,  "project.json" ) );
+		openDataset( project.getDefaultDataset() );
+	}
+
+	private void setProjectImageAndTableRootLocations( MoBIESettings settings )
+	{
+		projectRoot = createPath(
+				settings.values.getProjectLocation(),
+				settings.values.getProjectBranch() );
+
+		if( ! FileAndUrlUtils.exists( FileAndUrlUtils.combinePath( projectRoot, "project.json" ) ) )
+		{
+			projectRoot = FileAndUrlUtils.combinePath( projectRoot, "data" );
+		}
+
+		imageRoot = createPath(
+				settings.values.getImageDataLocation(),
+				settings.values.getImageDataBranch() );
+
+		if( ! FileAndUrlUtils.exists( FileAndUrlUtils.combinePath( imageRoot, "project.json" ) ) )
+		{
+			imageRoot = FileAndUrlUtils.combinePath( imageRoot, "data" );
+		}
+
+		tableRoot = createPath(
+				settings.values.getTableDataLocation(),
+				settings.values.getTableDataBranch() );
+
+		if( ! FileAndUrlUtils.exists( FileAndUrlUtils.combinePath( tableRoot, "project.json" ) ) )
+		{
+			tableRoot = FileAndUrlUtils.combinePath( tableRoot, "data" );
+		}
+	}
+
+	public List< SourceAndConverter< ? > > openSourceAndConverters( List< String > sources )
+	{
+		List< SourceAndConverter< ? > > sourceAndConverters = new CopyOnWriteArrayList<>();
+		final long start = System.currentTimeMillis();
+		final int nThreads = N_THREADS;
+		final ExecutorService executorService = Executors.newFixedThreadPool( nThreads );
+		for ( String sourceName : sources )
+		{
+			executorService.execute( () -> {
+				sourceAndConverters.add( openSourceAndConverter( sourceName ) );
+			} );
+		}
+
+		executorService.shutdown();
+		try {
+			executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
+		} catch (InterruptedException e) {
+		}
+
+		System.out.println( "Fetched " + sourceAndConverters.size() + " image source(s) in " + (System.currentTimeMillis() - start) + " ms, using " + nThreads + " thread(s).");
+
+		return sourceAndConverters;
+	}
+
+	private void openDataset( String datasetName ) throws IOException
+	{
+		setDatasetName( datasetName );
+		dataset = new DatasetJsonParser().parseDataset( getDatasetPath( "dataset.json" ) );
+
+		userInterface = new UserInterface( this );
+		viewerManager = new ViewerManager( this, userInterface, dataset.is2D, dataset.timepoints );
+		viewerManager.show( dataset.views.get( "default" ) );
+
+		// arrange windows
+		WindowArrangementHelper.setLogWindowPositionAndSize( userInterface.getWindow() );
+		WindowArrangementHelper.rightAlignWindow( userInterface.getWindow(), viewerManager.getSliceViewer().getWindow(), false, true );
+	}
+
+	private void setDatasetName( String datasetName )
+	{
+		this.datasetName = datasetName;
+	}
+
+	private String createPath( String rootLocation, String githubBranch, String... files )
+	{
+		if ( rootLocation.contains( "github.com" ) )
+		{
+			rootLocation = GitHubUtils.createRawUrl( rootLocation, githubBranch );
+		}
+
+		final ArrayList< String > strings = new ArrayList<>();
+		strings.add( rootLocation );
+
+		for ( int i = 0; i < files.length; i++ )
+		{
+			strings.add( files[ i ] );
+		}
+
+		final String path = FileAndUrlUtils.combinePath( strings.toArray( new String[0] ) );
+
+		return path;
+	}
+
+	public ViewerManager getViewerManager()
+	{
+		return viewerManager;
+	}
+
+	public String getProjectName()
+	{
+		return projectName;
+	}
+
+	public MoBIESettings getSettings()
+	{
+		return settings;
+	}
+
+	public Dataset getDataset()
+	{
+		return dataset;
+	}
+
+	public String getDatasetName()
+	{
+		return datasetName;
+	}
+
+	public List< String > getDatasets()
+	{
+		return project.getDatasets();
+	}
+
+	public UserInterface getUserInterface() { return userInterface; }
+
+	public void close()
+	{
+		// TODO
+//		sourcesDisplayManager.removeAllSourcesFromViewers();
+//		sourcesDisplayManager.getBdv().close();
+//		userInterface.dispose();
+	}
+
+	public synchronized ImageSource getSource( String sourceName )
+	{
+		return dataset.sources.get( sourceName ).get();
+	}
+
+	public SourceAndConverter openSourceAndConverter( String sourceName )
+	{
+		final ImageSource source = getSource( sourceName );
+		final String imagePath = getImagePath( source );
+		new Thread( () -> IJ.log( "Opening image:\n" + imagePath ) ).start();
+		final SpimData spimData = BdvUtils.openSpimData( imagePath );
+		final SourceAndConverterFromSpimDataCreator creator = new SourceAndConverterFromSpimDataCreator( spimData );
+		final SourceAndConverter sourceAndConverter = creator.getSetupIdToSourceAndConverter().values().iterator().next();
+		return sourceAndConverter;
+	}
+
+	public void setDataset( String dataset )
+	{
+		setDatasetName( dataset );
+		viewerManager.close();
+		userInterface.close();
+
+		try
+		{
+			openDataset( datasetName );
+		}
+		catch ( IOException e )
+		{
+			e.printStackTrace();
+		}
+	}
+
+	public Map< String, View > getViews()
+	{
+		return dataset.views;
+	}
+
+	public synchronized String getImagePath( ImageSource source )
+	{
+		final String relativeImagePath = source.imageDataLocations.get( settings.values.getImageDataStorageModality().toString() );
+		return FileAndUrlUtils.combinePath( imageRoot, getDatasetName(), relativeImagePath );
+	}
+
+	public String getDefaultTablePath( SegmentationSource source )
+	{
+		return getTablePath( source.tableDataLocation, "default.tsv" );
+	}
+
+	public String getDefaultTablePath( String relativeTableLocation )
+	{
+		return getTablePath( relativeTableLocation, "default.tsv" );
+	}
+
+	public String getTablePath( String relativeTableLocation, String table )
+	{
+		return FileAndUrlUtils.combinePath( tableRoot, getDatasetName(), relativeTableLocation, table );
+	}
+
+	public String getDatasetPath( String... files )
+	{
+		final String datasetRoot = FileAndUrlUtils.combinePath( projectRoot, getDatasetName() );
+		return createPath( datasetRoot, files );
+	}
+
+	private String createPath( String root, String[] files )
+	{
+		String location = root;
+		for ( String file : files )
+			location = FileAndUrlUtils.combinePath( location, file );
+		return location;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/ui/MoBIEInfo.java b/src/main/java/de/embl/cba/mobie/MoBIEInfo.java
similarity index 98%
rename from src/main/java/de/embl/cba/mobie/ui/MoBIEInfo.java
rename to src/main/java/de/embl/cba/mobie/MoBIEInfo.java
index d5e5bb72c..796731dec 100644
--- a/src/main/java/de/embl/cba/mobie/ui/MoBIEInfo.java
+++ b/src/main/java/de/embl/cba/mobie/MoBIEInfo.java
@@ -1,4 +1,4 @@
-package de.embl.cba.mobie.ui;
+package de.embl.cba.mobie;
 
 import bdv.tools.HelpDialog;
 import de.embl.cba.tables.FileAndUrlUtils;
diff --git a/src/main/java/de/embl/cba/mobie/ui/MoBIEOptions.java b/src/main/java/de/embl/cba/mobie/MoBIESettings.java
similarity index 54%
rename from src/main/java/de/embl/cba/mobie/ui/MoBIEOptions.java
rename to src/main/java/de/embl/cba/mobie/MoBIESettings.java
index 04fa3f93a..eb37310e1 100644
--- a/src/main/java/de/embl/cba/mobie/ui/MoBIEOptions.java
+++ b/src/main/java/de/embl/cba/mobie/MoBIESettings.java
@@ -1,57 +1,76 @@
-package de.embl.cba.mobie.ui;
+package de.embl.cba.mobie;
 
-public class MoBIEOptions
+public class MoBIESettings
 {
 	public final Values values = new Values();
 
 	public enum ImageDataStorageModality
 	{
 		FileSystem,
-		S3
+		S3;
+
+		@Override
+		public String toString()
+		{
+			if ( this.equals( S3 ) )
+			{
+				return "s3store";
+			}
+			else
+			{
+				return "fileSystem";
+			}
+		}
+	}
+
+	public static MoBIESettings settings()
+	{
+		return new MoBIESettings();
 	}
 
-	public static MoBIEOptions options()
+	public MoBIESettings projectLocation( String projectLocation )
 	{
-		return new MoBIEOptions();
+		this.values.projectLocation = projectLocation;
+		return this;
 	}
 
-	public MoBIEOptions dataset( String dataset )
+	public MoBIESettings dataset( String dataset )
 	{
 		this.values.dataset = dataset;
 		return this;
 	}
 
-	public MoBIEOptions gitProjectBranch( String gitBranch )
+	public MoBIESettings gitProjectBranch( String gitBranch )
 	{
 		this.values.projectBranch = gitBranch;
 		return this;
 	}
 
-	public MoBIEOptions imageDataStorageModality( ImageDataStorageModality imageDataStorageModality )
+	public MoBIESettings imageDataStorageModality( ImageDataStorageModality imageDataStorageModality )
 	{
 		this.values.imageDataStorageModality = imageDataStorageModality;
 		return this;
 	}
 
-	public MoBIEOptions imageDataLocation( String imageDataLocation )
+	public MoBIESettings imageDataLocation( String imageDataLocation )
 	{
 		this.values.imageDataLocation = imageDataLocation;
 		return this;
 	}
 
-	public MoBIEOptions tableDataLocation( String tableDataLocation )
+	public MoBIESettings tableDataLocation( String tableDataLocation )
 	{
 		this.values.tableDataLocation = tableDataLocation;
 		return this;
 	}
 
-	public MoBIEOptions gitTablesBranch( String tableDataBranch )
+	public MoBIESettings gitTablesBranch( String tableDataBranch )
 	{
 		this.values.tableDataBranch = tableDataBranch;
 		return this;
 	}
 
-	public MoBIEOptions pulicationURL( String publicationURL )
+	public MoBIESettings publicationURL( String publicationURL )
 	{
 		this.values.publicationURL = publicationURL;
 		return this;
@@ -64,6 +83,7 @@ public static class Values
 		private String projectBranch = "master"; // project and images
 		private String tableDataBranch;
 		private ImageDataStorageModality imageDataStorageModality = ImageDataStorageModality.S3;
+		private String projectLocation;
 		private String imageDataLocation;
 		private String tableDataLocation;
 
@@ -71,20 +91,22 @@ public String getDataset()
 		{
 			return dataset;
 		}
+
 		public String getProjectBranch()
 		{
 			return projectBranch;
 		}
+
 		public ImageDataStorageModality getImageDataStorageModality() { return imageDataStorageModality; }
 
 		public String getImageDataLocation()
 		{
-			return imageDataLocation;
+			return imageDataLocation != null ? imageDataLocation : projectLocation;
 		}
 
 		public String getTableDataLocation()
 		{
-			return tableDataLocation;
+			return tableDataLocation != null ? tableDataLocation : projectLocation;
 		}
 
 		public String getTableDataBranch()
@@ -92,9 +114,19 @@ public String getTableDataBranch()
 			return tableDataBranch != null ? tableDataBranch : projectBranch;
 		}
 
+		public String getImageDataBranch()
+		{
+			return projectBranch;
+		}
+
 		public String getPublicationURL()
 		{
 			return publicationURL;
 		}
+
+		public String getProjectLocation()
+		{
+			return projectLocation;
+		}
 	}
 }
diff --git a/src/main/java/de/embl/cba/mobie/Project.java b/src/main/java/de/embl/cba/mobie/Project.java
new file mode 100644
index 000000000..394fa0c90
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/Project.java
@@ -0,0 +1,31 @@
+package de.embl.cba.mobie;
+
+import java.util.List;
+
+public class Project
+{
+	private List< String > datasets;
+	private String defaultDataset;
+	private String specVersion;
+
+	public List< String > getDatasets()
+	{
+		return datasets;
+	}
+
+	public String getDefaultDataset()
+	{
+		return defaultDataset;
+	}
+
+	public String getSpecVersion()
+	{
+		return specVersion;
+	}
+
+	public void setDatasets( List<String> datasets ) { this.datasets = datasets; }
+
+	public void setDefaultDataset( String defaultDataset ) { this.defaultDataset = defaultDataset; }
+
+	public void setSpecVersion( String specVersion ) { this.specVersion = specVersion; }
+}
diff --git a/src/main/java/de/embl/cba/mobie/Utils.java b/src/main/java/de/embl/cba/mobie/Utils.java
new file mode 100644
index 000000000..8b7f254e0
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/Utils.java
@@ -0,0 +1,202 @@
+package de.embl.cba.mobie;
+
+import bdv.util.BdvHandle;
+import bdv.viewer.Source;
+import bdv.viewer.SourceAndConverter;
+import de.embl.cba.bdv.utils.BdvUtils;
+import de.embl.cba.tables.FileUtils;
+import de.embl.cba.tables.TableColumns;
+import de.embl.cba.tables.imagesegment.SegmentProperty;
+import de.embl.cba.tables.imagesegment.SegmentUtils;
+import de.embl.cba.tables.tablerow.TableRowImageSegment;
+import ij.IJ;
+import net.imglib2.*;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.realtransform.Scale3D;
+import java.io.*;
+import java.net.URI;
+import java.util.*;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static de.embl.cba.bdv.utils.BdvUtils.getBdvWindowCenter;
+
+public abstract class Utils
+{
+	public static < T > SourceAndConverter< T > getSource( List< SourceAndConverter< T > > sourceAndConverters, String name )
+	{
+		for ( SourceAndConverter< T > sourceAndConverter : sourceAndConverters )
+		{
+			if ( sourceAndConverter.getSpimSource().getName().equals( name ) )
+				return sourceAndConverter;
+		}
+
+		return null;
+	}
+
+	public static double[] delimitedStringToDoubleArray( String s, String delimiter) {
+
+		String[] sA = s.split(delimiter);
+		double[] nums = new double[sA.length];
+		for (int i = 0; i < nums.length; i++) {
+			nums[i] = Double.parseDouble(sA[i].trim());
+		}
+
+		return nums;
+	}
+
+	public static void log( String text )
+	{
+		IJ.log( text );
+	}
+
+	public static List< TableRowImageSegment > createAnnotatedImageSegmentsFromTableFile(
+			String tablePath,
+			String imageId )
+	{
+		IJ.log( "Opening table:\n" + tablePath );
+
+		tablePath = resolveTablePath( tablePath );
+
+		Map< String, List< String > > columns =
+				TableColumns.stringColumnsFromTableFile( tablePath );
+
+		TableColumns.addLabelImageIdColumn(
+				columns,
+				Constants.LABEL_IMAGE_ID,
+				imageId );
+
+		final Map< SegmentProperty, List< String > > segmentPropertyToColumn
+				= createSegmentPropertyToColumn( columns );
+
+		final List< TableRowImageSegment > segments
+				= SegmentUtils.tableRowImageSegmentsFromColumns(
+						columns, segmentPropertyToColumn, false );
+
+		return segments;
+	}
+
+	public static String resolveTablePath( String tablePath )
+	{
+		if ( tablePath.startsWith( "http" ) ) {
+			tablePath = FileUtils.resolveTableURL( URI.create(tablePath) );
+		} else {
+			tablePath = FileUtils.resolveTablePath( tablePath );
+		}
+		return tablePath;
+	}
+
+	public static Map< SegmentProperty, List< String > > createSegmentPropertyToColumn(
+			Map< String, List< String > > columns )
+	{
+		final HashMap< SegmentProperty, List< String > > segmentPropertyToColumn
+				= new HashMap<>();
+
+		segmentPropertyToColumn.put(
+				SegmentProperty.LabelImage,
+				columns.get( Constants.LABEL_IMAGE_ID ));
+
+		segmentPropertyToColumn.put(
+				SegmentProperty.ObjectLabel,
+				columns.get( Constants.SEGMENT_LABEL_ID ) );
+
+		segmentPropertyToColumn.put(
+				SegmentProperty.X,
+				columns.get( Constants.ANCHOR_X ) );
+
+		segmentPropertyToColumn.put(
+				SegmentProperty.Y,
+				columns.get( Constants.ANCHOR_Y ) );
+
+		segmentPropertyToColumn.put(
+				SegmentProperty.Z,
+				columns.get( Constants.ANCHOR_Z ) );
+
+		segmentPropertyToColumn.put(
+				SegmentProperty.T,
+				columns.get( Constants.TIMEPOINT ) );
+
+		SegmentUtils.putDefaultBoundingBoxMapping( segmentPropertyToColumn, columns );
+
+		return segmentPropertyToColumn;
+	}
+
+	public static String createNormalisedViewerTransformString( BdvHandle bdv, double[] position )
+	{
+		final AffineTransform3D view = createNormalisedViewerTransform( bdv, position );
+		final String replace = view.toString().replace( "3d-affine: (", "" ).replace( ")", "" );
+		final String collect = Arrays.stream( replace.split( "," ) ).map( x -> "n" + x.trim() ).collect( Collectors.joining( "," ) );
+		return collect;
+	}
+
+	public static AffineTransform3D createNormalisedViewerTransform( BdvHandle bdv, double[] position )
+	{
+		final AffineTransform3D view = new AffineTransform3D();
+		bdv.getViewerPanel().state().getViewerTransform( view );
+
+		// translate position to upper left corner of the Window (0,0)
+		final AffineTransform3D translate = new AffineTransform3D();
+		translate.translate( position );
+		view.preConcatenate( translate.inverse() );
+
+		// divide by window width
+		final int bdvWindowWidth = BdvUtils.getBdvWindowWidth( bdv );
+		final Scale3D scale = new Scale3D( 1.0 / bdvWindowWidth, 1.0 / bdvWindowWidth, 1.0 / bdvWindowWidth );
+		view.preConcatenate( scale );
+
+		return view;
+	}
+
+	public static double[] getMousePosition( BdvHandle bdv )
+	{
+		final RealPoint realPoint = new RealPoint( 2 );
+		bdv.getViewerPanel().getMouseCoordinates( realPoint );
+		final double[] doubles = new double[ 3 ];
+		realPoint.localize( doubles );
+		return doubles;
+	}
+
+	public static AffineTransform3D createUnnormalizedViewerTransform( AffineTransform3D normalisedTransform, BdvHandle bdv )
+	{
+		final AffineTransform3D transform = normalisedTransform.copy();
+
+		final int bdvWindowWidth = BdvUtils.getBdvWindowWidth( bdv );
+		final Scale3D scale = new Scale3D( 1.0 / bdvWindowWidth, 1.0 / bdvWindowWidth, 1.0 / bdvWindowWidth );
+		transform.preConcatenate( scale.inverse() );
+
+		AffineTransform3D translate = new AffineTransform3D();
+		translate.translate( getBdvWindowCenter( bdv ) );
+
+		transform.preConcatenate( translate );
+
+		return transform;
+	}
+
+	public static AffineTransform3D asAffineTransform3D( double[] doubles )
+	{
+		final AffineTransform3D view = new AffineTransform3D( );
+		view.set( doubles );
+		return view;
+	}
+
+	public static FinalRealInterval estimateBounds( Source< ? > source )
+	{
+		final AffineTransform3D affineTransform3D = new AffineTransform3D();
+		source.getSourceTransform( 0, 0, affineTransform3D );
+		final FinalRealInterval bounds = affineTransform3D.estimateBounds( source.getSource( 0, 0 ) );
+		return bounds;
+	}
+
+	public static String getName( String path )
+	{
+		if ( path.startsWith( "http" ) )
+		{
+			final String[] split = path.split( "/" );
+			return split[ split.length - 1 ];
+		}
+		else
+		{
+			return new File( path ).getName();
+		}
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/VisibilityListener.java b/src/main/java/de/embl/cba/mobie/VisibilityListener.java
new file mode 100644
index 000000000..6d78d2180
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/VisibilityListener.java
@@ -0,0 +1,6 @@
+package de.embl.cba.mobie;
+
+public interface VisibilityListener
+{
+	void visibility( boolean isVisible );
+}
diff --git a/src/main/java/de/embl/cba/mobie/annotate/Annotator.java b/src/main/java/de/embl/cba/mobie/annotate/Annotator.java
new file mode 100644
index 000000000..5caa780d2
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/annotate/Annotator.java
@@ -0,0 +1,461 @@
+/*-
+ * #%L
+ * Various Java code for ImageJ
+ * %%
+ * Copyright (C) 2018 - 2021 EMBL
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+package de.embl.cba.mobie.annotate;
+
+import de.embl.cba.tables.Logger;
+import de.embl.cba.tables.SwingUtils;
+import de.embl.cba.tables.color.CategoryTableRowColumnColoringModel;
+import de.embl.cba.tables.color.ColorUtils;
+import de.embl.cba.tables.select.SelectionModel;
+import de.embl.cba.tables.tablerow.TableRow;
+import de.embl.cba.tables.tablerow.TableRowImageSegment;
+import ij.IJ;
+import net.imglib2.type.numeric.ARGBType;
+
+import javax.swing.*;
+import javax.swing.table.TableModel;
+import java.awt.*;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class Annotator< T extends TableRow > extends JFrame
+{
+	public static final String LAST = "You are already at the last object in table.";
+	public static final String NO_MORE_SEGMENTS = "No more segments.";
+	private final String annotationColumnName;
+	private final List< T > tableRows;
+	private final SelectionModel< T > selectionModel;
+	private final CategoryTableRowColumnColoringModel< T > coloringModel;
+	private final RowSorter< ? extends TableModel > rowSorter;
+	private final JPanel panel;
+	private boolean skipNone;
+	private boolean isSingleRowBrowsingMode = false; // TODO: think about how to get out of this mode!
+	private JTextField goToRowIndexTextField;
+	private HashMap< String, T > annotationToTableRow;
+	private JPanel annotationButtonsContainer;
+	private JScrollPane annotationButtonsScrollPane;
+	private T currentlySelectedRow;
+	private Set< String > annotationNames;
+
+	public Annotator( String columnName, List< T > tableRows, SelectionModel< T > selectionModel, CategoryTableRowColumnColoringModel< T > coloringModel, RowSorter< ? extends TableModel > rowSorter )
+	{
+		super("");
+		this.annotationColumnName = columnName;
+		this.annotationNames = new HashSet<>();
+		this.tableRows = tableRows;
+		this.selectionModel = selectionModel;
+		this.coloringModel = coloringModel;
+		this.rowSorter = rowSorter;
+		this.currentlySelectedRow = tableRows.get( rowSorter.convertRowIndexToModel( 0 ) );
+		this.coloringModel.fixedColorMode( true );
+		this.panel = new JPanel();
+
+	}
+
+	public void showDialog()
+	{
+		createDialog();
+		showFrame();
+	}
+
+	private void createDialog()
+	{
+		this.setContentPane( panel );
+		panel.setOpaque( true ); //content panes must be opaque
+		panel.setLayout( new BoxLayout( panel, BoxLayout.Y_AXIS ) );
+		addCreateCategoryButton();
+		panel.add( new JSeparator( SwingConstants.HORIZONTAL ) );
+		addAnnotationButtons();
+		panel.add( new JSeparator( SwingConstants.HORIZONTAL ) );
+		addTableRowBrowserSelectPanel();
+		addTableRowBrowserSelectPreviousAndNextPanel();
+		addSkipNonePanel();
+		// this has to be done at the end, to make the packing work correctly
+		// otherwise, continuing an annotation with many categories will be
+		// packed to a size too large for the screen
+		addAnnotationButtonPanels();
+	}
+
+	private void showFrame()
+	{
+		this.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
+		this.setLocation( MouseInfo.getPointerInfo().getLocation().x, MouseInfo.getPointerInfo().getLocation().y );
+		this.setVisible( true );
+	}
+
+	private void addCreateCategoryButton()
+	{
+		final JPanel panel = SwingUtils.horizontalLayoutPanel();
+		final JButton button = new JButton( "Create new category" );
+		final JTextField textField = new JTextField( "Class A" );
+		panel.add( button );
+		panel.add( textField );
+		button.addActionListener( e -> {
+			String newClassName = textField.getText();
+			if ( getAnnotations().containsKey( newClassName ) || annotationNames.contains( newClassName )  )
+			{
+				Logger.error( "Category " + newClassName + " exists already." );
+				return;
+			}
+			addAnnotationButtonPanel( newClassName, null );
+		} );
+		this.panel.add( panel );
+	}
+
+	private void addAnnotationButtons()
+	{
+		JPanel annotationButtonsPanel = new JPanel( );
+		annotationButtonsPanel.setLayout( new BoxLayout(annotationButtonsPanel, BoxLayout.Y_AXIS ) );
+		annotationButtonsPanel.setBorder( BorderFactory.createEmptyBorder(0,10,10,10) );
+		panel.add( annotationButtonsPanel );
+
+		final JPanel panel = SwingUtils.horizontalLayoutPanel();
+		panel.add( new JLabel( "Annotate selected segment(s) as:" ) );
+		panel.add( new JLabel( "      " ) );
+		annotationButtonsPanel.add( panel );
+
+		annotationButtonsScrollPane = new JScrollPane( JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER );
+		annotationButtonsScrollPane.setBorder( BorderFactory.createEmptyBorder() );
+		annotationButtonsPanel.add( annotationButtonsScrollPane );
+
+		annotationButtonsContainer = new JPanel();
+		annotationButtonsContainer.setLayout( new BoxLayout( annotationButtonsContainer, BoxLayout.PAGE_AXIS ));
+		annotationButtonsContainer.setBorder( BorderFactory.createEmptyBorder() );
+		annotationButtonsScrollPane.setViewportView( annotationButtonsContainer );
+	}
+
+	private void addAnnotationButtonPanels() {
+		final HashMap< String, T > annotations = getAnnotations();
+		for ( String annotation : annotations.keySet() )
+			addAnnotationButtonPanel( annotation, annotations.get( annotation ) );
+	}
+
+	private void addAnnotationButtonPanel( String annotationName, T tableRow )
+	{
+		annotationNames.add( annotationName );
+		final JPanel panel = SwingUtils.horizontalLayoutPanel();
+
+		final JButton annotateButton = new JButton( String.format("%1$15s", annotationName) );
+		annotateButton.setFont( new Font("monospaced", Font.PLAIN, 12) );
+		annotateButton.setOpaque( true );
+		setButtonColor( annotateButton, tableRow );
+		annotateButton.setAlignmentX( Component.CENTER_ALIGNMENT );
+
+		final ARGBType argbType = new ARGBType();
+		coloringModel.convert( annotationName, argbType );
+		annotateButton.setBackground( ColorUtils.getColor( argbType ) );
+
+		annotateButton.addActionListener( e ->
+		{
+			if ( selectionModel.isEmpty() ) return; // nothing selected to be annotated
+
+			final Set< T > selected = selectionModel.getSelected();
+
+			for ( T row : selected )
+			{
+				row.setCell( annotationColumnName, annotationName );
+			}
+
+			if ( selected.size() > 1 ) isSingleRowBrowsingMode = false;
+
+			if( isSingleRowBrowsingMode )
+			{
+				selectionModel.clearSelection(); // Hack to notify all listeners that the coloring might have changed.
+				// select again such that the user could still change its mind
+				selectionModel.setSelected( selected, true );
+			}
+			else
+			{
+				selectionModel.clearSelection();
+			}
+		} );
+
+		final JButton changeColor = new JButton( "C" );
+		changeColor.addActionListener( e -> {
+			Color color = JColorChooser.showDialog( this.panel, "", null );
+			if ( color == null ) return;
+			annotateButton.setBackground( color );
+			coloringModel.putInputToFixedColor( annotationName, ColorUtils.getARGBType( color ) );
+		} );
+
+		panel.add( annotateButton );
+		panel.add( changeColor );
+		annotationButtonsContainer.add( panel );
+		refreshDialog();
+	}
+
+	private void addTableRowBrowserSelectPreviousAndNextPanel( )
+	{
+		final JPanel panel = SwingUtils.horizontalLayoutPanel();
+		final JButton previous = createSelectPreviousButton();
+		final JButton next = createSelectNextButton();
+		panel.add( previous );
+		panel.add( next );
+		this.panel.add( panel );
+	}
+
+	private void addTableRowBrowserSelectPanel( )
+	{
+		final JPanel panel = SwingUtils.horizontalLayoutPanel();
+		final JButton button = createSelectButton();
+		goToRowIndexTextField = new JTextField( "" );
+		goToRowIndexTextField.setText( "1" );
+		panel.add( button );
+		panel.add( goToRowIndexTextField );
+		this.panel.add( panel );
+	}
+
+	private JButton createSelectNextButton()
+	{
+		final JButton next = new JButton( "Select next" );
+		//next.setFont( new Font("monospaced", Font.PLAIN, 12) );
+		next.setAlignmentX( Component.CENTER_ALIGNMENT );
+
+		next.addActionListener( e ->
+		{
+			isSingleRowBrowsingMode = true;
+
+			// rowIndex in sorted "units"
+			int rowIndex = rowSorter.convertRowIndexToView( currentlySelectedRow.rowIndex() );
+			if ( rowIndex < tableRows.size() - 1 )
+			{
+				T row = null;
+				if ( skipNone )
+				{
+					while ( rowIndex < tableRows.size() - 1 )
+					{
+						row = tableRows.get( rowSorter.convertRowIndexToModel( ++rowIndex ) );
+						if ( isNoneOrNan( row ) )
+						{
+							row = null;
+							continue;
+						}
+						else
+						{
+							break;
+						}
+					}
+
+					if ( row == null )
+					{
+						IJ.showMessage( NO_MORE_SEGMENTS );
+						return; // All following rows are None or NaN
+					}
+
+					selectRow( row );
+				}
+				else
+				{
+					row = tableRows.get( rowSorter.convertRowIndexToModel( ++rowIndex ) );
+					selectRow( row );
+				}
+			}
+			else
+			{
+				IJ.showMessage( NO_MORE_SEGMENTS );
+			}
+		} );
+		return next;
+	}
+
+	private JButton createSelectPreviousButton()
+	{
+		final JButton previous = new JButton( "Select previous" );
+		//previous.setFont( new Font("monospaced", Font.PLAIN, 12) );
+		previous.setAlignmentX( Component.CENTER_ALIGNMENT );
+
+		previous.addActionListener( e ->
+		{
+			isSingleRowBrowsingMode = true;
+
+			// row index in sorted "units"
+			int rowIndex = rowSorter.convertRowIndexToView( currentlySelectedRow.rowIndex() );
+			if ( rowIndex > 0 )
+			{
+				T row = null;
+				if ( skipNone )
+				{
+					while ( rowIndex > 0 )
+					{
+						row = tableRows.get( rowSorter.convertRowIndexToModel( --rowIndex ) );
+						if ( isNoneOrNan( row ) )
+						{
+							row = null;
+							continue;
+						}
+						else
+							break;
+					}
+
+					if ( row == null )
+					{
+						IJ.showMessage( NO_MORE_SEGMENTS );
+						return; // None of the previous rows is not None
+					}
+
+					selectRow( row );
+				}
+				else
+				{
+					row = tableRows.get( rowSorter.convertRowIndexToModel( --rowIndex ) );
+					selectRow( row );
+				}
+			}
+			else
+			{
+				IJ.showMessage( NO_MORE_SEGMENTS );
+			}
+		} );
+
+		return previous;
+	}
+
+	private JButton createSelectButton()
+	{
+		final JButton button = new JButton( "Select segment with label id" );
+		button.setAlignmentX( Component.CENTER_ALIGNMENT );
+
+		button.addActionListener( e ->
+		{
+			isSingleRowBrowsingMode = true;
+			T selectedRow = getSelectedRow();
+			if ( selectedRow != null ) selectRow( selectedRow );
+		} );
+		return button;
+	}
+
+	private T getSelectedRow()
+	{
+		// TODO: in principle a flaw in logic as it assumes that all tableRows are of same type...
+		if ( tableRows.get( 0 ) instanceof TableRowImageSegment )
+		{
+			final double selectedLabelId = Double.parseDouble( goToRowIndexTextField.getText() );
+			for ( T tableRow : tableRows )
+			{
+				final double labelId = ( ( TableRowImageSegment ) tableRow ).labelId();
+				if ( labelId == selectedLabelId )
+				{
+					return tableRow;
+				}
+			}
+			throw new UnsupportedOperationException( "Could not find segment with label " + selectedLabelId );
+		}
+		else
+		{
+			final int rowIndex = Integer.parseInt( goToRowIndexTextField.getText() );
+			return tableRows.get( rowSorter.convertRowIndexToModel( rowIndex ) );
+		}
+	}
+
+	private boolean isNoneOrNan( T row )
+	{
+		return row.getCell( annotationColumnName ).toLowerCase().equals( "none" )
+				|| row.getCell( annotationColumnName ).toLowerCase().equals( "nan" );
+	}
+
+	private void selectRow( T row )
+	{
+		//currentlySelectedRowIndex = sortedRowIndex;
+		currentlySelectedRow = row;
+
+
+//		if ( isNoneOrNan( row ) )
+//		{
+//			selectionColoringModel.setSelectionColoringMode( SelectionColoringModel.SelectionColoringMode.SelectionColor );
+//		}
+//		else
+//		{
+//			selectionColoringModel.setSelectionColoringMode( SelectionColoringModel.SelectionColoringMode.OnlyShowSelected );
+//		}
+
+		selectionModel.clearSelection();
+		selectionModel.setSelected( row, true );
+		selectionModel.focus( row );
+	}
+
+	private void addSkipNonePanel( )
+	{
+		final JPanel panel = SwingUtils.horizontalLayoutPanel();
+
+		final JCheckBox checkBox = new JCheckBox( "Skip \"None\" & \"NaN\"" );
+		checkBox.setSelected( false );
+		skipNone = checkBox.isSelected();
+
+		checkBox.addActionListener( e -> {
+			skipNone = checkBox.isSelected();
+		}  );
+
+		panel.add( checkBox );
+		this.panel.add( panel );
+	}
+
+	private void setButtonColor( JButton button, T tableRow )
+	{
+		if ( tableRow != null )
+		{
+			final ARGBType argbType = new ARGBType();
+			coloringModel.convert( tableRow, argbType );
+			button.setBackground( new Color( argbType.get() ) );
+		}
+	}
+
+	private HashMap< String, T > getAnnotations()
+	{
+		if ( annotationToTableRow == null )
+		{
+			annotationToTableRow = new HashMap<>();
+
+			for ( int row = 0; row < tableRows.size(); row++ )
+			{
+				final T tableRow = tableRows.get( row );
+				annotationToTableRow.put( tableRow.getCell( annotationColumnName ), tableRow );
+			}
+		}
+
+		return annotationToTableRow;
+	}
+
+	private void refreshDialog()
+	{
+		panel.revalidate();
+		panel.repaint();
+
+		// scroll to bottom, so any new panels are visible
+		annotationButtonsScrollPane.validate();
+		JScrollBar vertical = annotationButtonsScrollPane.getVerticalScrollBar();
+		vertical.setValue( vertical.getMaximum() );
+
+		// scroll pane resizes up to five annotations, then requires user resizing
+		if ( annotationButtonsContainer.getComponentCount() < 6 ) {
+			this.pack();
+		}
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/bdv/BdvLocationLogger.java b/src/main/java/de/embl/cba/mobie/bdv/BdvLocationLogger.java
new file mode 100644
index 000000000..b8fd9c7c1
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/bdv/BdvLocationLogger.java
@@ -0,0 +1,28 @@
+package de.embl.cba.mobie.bdv;
+
+import bdv.util.BdvHandle;
+import de.embl.cba.bdv.utils.BdvUtils;
+import de.embl.cba.bdv.utils.Logger;
+import de.embl.cba.mobie.Utils;
+import org.scijava.plugin.Parameter;
+import org.scijava.plugin.Plugin;
+import sc.fiji.bdvpg.scijava.command.BdvPlaygroundActionCommand;
+
+@Plugin( type=BdvPlaygroundActionCommand.class, name = BdvLocationLogger.NAME, menuPath = BdvLocationLogger.NAME )
+public class BdvLocationLogger implements BdvPlaygroundActionCommand
+{
+	public static final String NAME = "BDV - Log Current Location";
+
+	@Parameter
+	BdvHandle bdvh;
+
+	@Override
+	public void run()
+	{
+		new Thread( () -> {
+			Logger.log( "\nPosition:\n" + BdvUtils.getGlobalMousePositionString( bdvh ) );
+			Logger.log( "View:\n" + BdvUtils.getBdvViewerTransformString( bdvh ) );
+			Logger.log( "Normalised view:\n" + Utils.createNormalisedViewerTransformString( bdvh, Utils.getMousePosition( bdvh ) ) );
+		} ).start();
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/bdv/BdvMousePositionProvider.java b/src/main/java/de/embl/cba/mobie/bdv/BdvMousePositionProvider.java
new file mode 100644
index 000000000..6a1d4792c
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/bdv/BdvMousePositionProvider.java
@@ -0,0 +1,28 @@
+package de.embl.cba.mobie.bdv;
+
+import bdv.util.BdvHandle;
+import net.imglib2.RealPoint;
+
+public class BdvMousePositionProvider
+{
+	private final RealPoint position;
+	private final int timePoint;
+
+	public BdvMousePositionProvider( BdvHandle bdvHandle )
+	{
+		// Gets mouse location in space (global 3D coordinates) and time
+		position = new RealPoint( 3 );
+		bdvHandle.getBdvHandle().getViewerPanel().getGlobalMouseCoordinates( position );
+		timePoint = bdvHandle.getViewerPanel().state().getCurrentTimepoint();
+	}
+
+	public RealPoint getPosition()
+	{
+		return position;
+	}
+
+	public int getTimePoint()
+	{
+		return timePoint;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/bdv/ImageSliceView.java b/src/main/java/de/embl/cba/mobie/bdv/ImageSliceView.java
new file mode 100644
index 000000000..b4a2ac261
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/bdv/ImageSliceView.java
@@ -0,0 +1,124 @@
+package de.embl.cba.mobie.bdv;
+
+import bdv.tools.brightness.ConverterSetup;
+import bdv.util.BdvHandle;
+import bdv.viewer.SourceAndConverter;
+import de.embl.cba.bdv.utils.lut.GlasbeyARGBLut;
+import de.embl.cba.mobie.color.opacity.AdjustableOpacityColorConverter;
+import de.embl.cba.mobie.color.opacity.VolatileAdjustableOpacityColorConverter;
+import de.embl.cba.mobie.display.ImageSourceDisplay;
+import de.embl.cba.mobie.open.SourceAndConverterSupplier;
+import de.embl.cba.mobie.transform.TransformerHelper;
+import de.embl.cba.tables.color.ColorUtils;
+import ij.IJ;
+import net.imglib2.Volatile;
+import net.imglib2.converter.Converter;
+import net.imglib2.type.numeric.ARGBType;
+import net.imglib2.type.numeric.RealType;
+import sc.fiji.bdvpg.bdv.projector.BlendingMode;
+import sc.fiji.bdvpg.scijava.services.SourceAndConverterBdvDisplayService;
+import sc.fiji.bdvpg.scijava.services.SourceAndConverterService;
+import sc.fiji.bdvpg.services.SourceAndConverterServices;
+import sc.fiji.bdvpg.sourceandconverter.display.ColorChanger;
+import sc.fiji.bdvpg.sourceandconverter.display.ConverterChanger;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static de.embl.cba.bdv.utils.converters.RandomARGBConverter.goldenRatio;
+
+public class ImageSliceView
+{
+	private final SourceAndConverterBdvDisplayService displayService;
+	private final ImageSourceDisplay imageDisplay;
+	private final BdvHandle bdvHandle;
+	private final SourceAndConverterSupplier sourceAndConverterSupplier;
+	private final SourceAndConverterService sacService;
+
+	public ImageSliceView( ImageSourceDisplay imageDisplay, BdvHandle bdvHandle, SourceAndConverterSupplier sourceAndConverterSupplier  )
+	{
+		this.imageDisplay = imageDisplay;
+		this.bdvHandle = bdvHandle;
+		this.sourceAndConverterSupplier = sourceAndConverterSupplier;
+
+		displayService = SourceAndConverterServices.getSourceAndConverterDisplayService();
+		sacService = ( SourceAndConverterService ) SourceAndConverterServices.getSourceAndConverterService();
+
+		show();
+	}
+
+	private void show( )
+	{
+		List< SourceAndConverter< ? > > sourceAndConverters = sourceAndConverterSupplier.get( imageDisplay.getSources() );
+
+		// transform
+		sourceAndConverters = TransformerHelper.transformSourceAndConverters( sourceAndConverters, imageDisplay.sourceTransformers );
+
+		// show
+		List< SourceAndConverter< ? > > displayedSourceAndConverters = new ArrayList<>();
+		for ( SourceAndConverter< ? > sourceAndConverter : sourceAndConverters )
+		{
+			// replace converter such that one can change the opacity
+			// (this changes the hash-code of the sourceAndConverter)
+
+			// TODO: understand this madness
+			final Converter< RealType, ARGBType > converter = ( Converter< RealType, ARGBType > ) sourceAndConverter.getConverter();
+			final Converter< ? extends Volatile< ? >, ARGBType > volatileConverter = sourceAndConverter.asVolatile().getConverter();
+			sourceAndConverter = new ConverterChanger( sourceAndConverter, new AdjustableOpacityColorConverter(  converter ), new VolatileAdjustableOpacityColorConverter( volatileConverter ) ).get();
+
+			// adapt color
+			adaptImageColor( sourceAndConverter );
+
+			// set blending mode
+			if ( imageDisplay.getBlendingMode() != null )
+				SourceAndConverterServices.getSourceAndConverterService().setMetadata( sourceAndConverter, BlendingMode.BLENDING_MODE, imageDisplay.getBlendingMode());
+
+			// show
+			displayService.show( bdvHandle, sourceAndConverter );
+
+			// adapt contrast limits
+			final ConverterSetup converterSetup = displayService.getConverterSetup( sourceAndConverter );
+			converterSetup.setDisplayRange( imageDisplay.getContrastLimits()[ 0 ], imageDisplay.getContrastLimits()[ 1 ] );
+
+			displayedSourceAndConverters.add( sourceAndConverter );
+		}
+
+		imageDisplay.sourceAndConverters = displayedSourceAndConverters;
+	}
+
+	private void adaptImageColor( SourceAndConverter< ? > sourceAndConverter )
+	{
+		if ( imageDisplay.getColor() != null )
+		{
+			final String color = imageDisplay.getColor();
+
+			if ( color.equals( "randomFromGlasbey" ) )
+			{
+				final GlasbeyARGBLut glasbeyARGBLut = new GlasbeyARGBLut();
+				double random = sourceAndConverter.getSpimSource().getName().hashCode() * goldenRatio;
+				random = random - ( long ) Math.floor( random );
+				final int argb = glasbeyARGBLut.getARGB( random );
+				new ColorChanger( sourceAndConverter, new ARGBType( argb ) ).run();
+			}
+			else
+			{
+				final ARGBType argbType = ColorUtils.getARGBType( color );
+				if ( argbType == null )
+				{
+					IJ.log( "[WARN] Could not parse color: " + color );
+				} else
+				{
+					new ColorChanger( sourceAndConverter, argbType ).run();
+				}
+			}
+		}
+	}
+
+	public void close( )
+	{
+		for ( SourceAndConverter< ? > sourceAndConverter : imageDisplay.sourceAndConverters )
+		{
+			SourceAndConverterServices.getSourceAndConverterDisplayService().removeFromAllBdvs( sourceAndConverter );
+		}
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/bdv/SegmentationImageSliceView.java b/src/main/java/de/embl/cba/mobie/bdv/SegmentationImageSliceView.java
new file mode 100644
index 000000000..fcf842c59
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/bdv/SegmentationImageSliceView.java
@@ -0,0 +1,164 @@
+package de.embl.cba.mobie.bdv;
+
+import bdv.util.BdvHandle;
+import bdv.viewer.SourceAndConverter;
+import de.embl.cba.mobie.n5.source.LabelSource;
+import de.embl.cba.mobie.color.LabelConverter;
+import de.embl.cba.mobie.display.SegmentationSourceDisplay;
+import de.embl.cba.mobie.display.SourceDisplay;
+import de.embl.cba.mobie.open.SourceAndConverterSupplier;
+import de.embl.cba.mobie.transform.SourceTransformer;
+import de.embl.cba.mobie.transform.TransformerHelper;
+import de.embl.cba.tables.color.ColoringListener;
+import de.embl.cba.tables.imagesegment.ImageSegment;
+import de.embl.cba.tables.select.SelectionListener;
+import net.imglib2.realtransform.AffineTransform3D;
+import sc.fiji.bdvpg.bdv.BdvHandleHelper;
+import sc.fiji.bdvpg.bdv.navigate.ViewerTransformChanger;
+import sc.fiji.bdvpg.scijava.services.SourceAndConverterBdvDisplayService;
+import sc.fiji.bdvpg.services.SourceAndConverterServices;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SegmentationImageSliceView< S extends ImageSegment > implements ColoringListener, SelectionListener< S >
+{
+	private final SourceAndConverterBdvDisplayService displayService;
+	private final SegmentationSourceDisplay segmentationDisplay;
+	private BdvHandle bdvHandle;
+	private final SourceAndConverterSupplier sourceAndConverterSupplier;
+
+	public SegmentationImageSliceView( SegmentationSourceDisplay segmentationDisplay, BdvHandle bdvHandle, SourceAndConverterSupplier sourceAndConverterSupplier  )
+	{
+		this.segmentationDisplay = segmentationDisplay;
+		this.bdvHandle = bdvHandle;
+		this.sourceAndConverterSupplier = sourceAndConverterSupplier;
+
+		displayService = SourceAndConverterServices.getSourceAndConverterDisplayService();
+		show();
+	}
+
+	private void show( )
+	{
+		segmentationDisplay.selectionModel.listeners().add( this );
+		segmentationDisplay.coloringModel.listeners().add( this );
+
+		// open
+		List< SourceAndConverter< ? > > sourceAndConverters = sourceAndConverterSupplier.get( segmentationDisplay.getSources() );
+
+		// transform
+		sourceAndConverters = TransformerHelper.transformSourceAndConverters( sourceAndConverters, segmentationDisplay.sourceTransformers );
+
+		// convert to labelSource
+		sourceAndConverters = asLabelSources( sourceAndConverters );
+
+		// adjust opacity and show in BDV
+		for ( SourceAndConverter< ? > sourceAndConverter : sourceAndConverters )
+		{
+			( ( LabelConverter ) sourceAndConverter.getConverter() ).setOpacity( segmentationDisplay.getOpacity() );
+			displayService.show( bdvHandle, sourceAndConverter );
+		}
+
+		segmentationDisplay.sourceAndConverters = sourceAndConverters;
+	}
+
+	private List< SourceAndConverter< ? > > asLabelSources( List< SourceAndConverter< ? > > sourceAndConverters )
+	{
+		List< SourceAndConverter< ? > > labelSourceAndConverters = new ArrayList<>( );
+		for ( SourceAndConverter< ? > sourceAndConverter : sourceAndConverters )
+		{
+			LabelConverter< S > labelConverter = new LabelConverter(
+					segmentationDisplay.segmentAdapter,
+					sourceAndConverter.getSpimSource().getName(),
+					segmentationDisplay.coloringModel );
+
+			SourceAndConverter< ? > labelSourceAndConverter = asLabelSourceAndConverter( sourceAndConverter, labelConverter );
+
+
+
+			labelSourceAndConverters.add( labelSourceAndConverter );
+		}
+
+		return labelSourceAndConverters;
+	}
+
+	private SourceAndConverter asLabelSourceAndConverter( SourceAndConverter< ? > sourceAndConverter, LabelConverter labelConverter )
+	{
+		LabelSource volatileLabelSource = new LabelSource( sourceAndConverter.asVolatile().getSpimSource() );
+		SourceAndConverter volatileSourceAndConverter = new SourceAndConverter( volatileLabelSource, labelConverter );
+		LabelSource labelSource = new LabelSource( sourceAndConverter.getSpimSource() );
+		return new SourceAndConverter( labelSource, labelConverter, volatileSourceAndConverter );
+	}
+
+	public void close()
+	{
+		for ( SourceAndConverter< ? > sourceAndConverter : segmentationDisplay.sourceAndConverters )
+		{
+			SourceAndConverterServices.getSourceAndConverterDisplayService().removeFromAllBdvs( sourceAndConverter );
+		}
+	};
+
+	@Override
+	public synchronized void coloringChanged()
+	{
+		bdvHandle.getViewerPanel().requestRepaint();
+	}
+
+	@Override
+	public synchronized void selectionChanged()
+	{
+		bdvHandle.getViewerPanel().requestRepaint();
+	}
+
+	@Override
+	public synchronized void focusEvent( S selection )
+	{
+		if ( selection.timePoint() != getBdvHandle().getViewerPanel().state().getCurrentTimepoint() )
+		{
+			getBdvHandle().getViewerPanel().state().setCurrentTimepoint( selection.timePoint() );
+		}
+
+		final double[] position = new double[ 3 ];
+		selection.localize( position );
+
+		adaptPosition( position, selection.imageId() );
+
+		new ViewerTransformChanger(
+				bdvHandle,
+				BdvHandleHelper.getViewerTransformWithNewCenter( bdvHandle, position ),
+				false,
+				500 ).run();
+	}
+
+	private void adaptPosition( double[] position, String sourceName )
+	{
+		if ( segmentationDisplay.sourceTransformers != null )
+		{
+			for ( SourceTransformer sourceTransformer : segmentationDisplay.sourceTransformers )
+			{
+				final AffineTransform3D transform = sourceTransformer.getTransform( sourceName );
+				transform.apply( position, position );
+			}
+		}
+	}
+
+	public BdvHandle getBdvHandle()
+	{
+		return bdvHandle;
+	}
+
+	public Window getWindow()
+	{
+		return SwingUtilities.getWindowAncestor( bdvHandle.getViewerPanel() );
+	}
+
+	public void removeSourceDisplay( SourceDisplay sourceDisplay )
+	{
+		for ( SourceAndConverter< ? > sourceAndConverter : sourceDisplay.sourceAndConverters )
+		{
+			SourceAndConverterServices.getSourceAndConverterDisplayService().removeFromAllBdvs( sourceAndConverter );
+		}
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/bdv/SliceViewer.java b/src/main/java/de/embl/cba/mobie/bdv/SliceViewer.java
new file mode 100644
index 000000000..cbc753e54
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/bdv/SliceViewer.java
@@ -0,0 +1,149 @@
+package de.embl.cba.mobie.bdv;
+
+import bdv.util.BdvHandle;
+import de.embl.cba.mobie.segment.BdvSegmentSelector;
+import de.embl.cba.mobie.color.RandomColorSeedChangerCommand;
+import de.embl.cba.mobie.view.ViewerManager;
+import org.scijava.ui.behaviour.ClickBehaviour;
+import org.scijava.ui.behaviour.io.InputTriggerConfig;
+import org.scijava.ui.behaviour.util.Behaviours;
+import sc.fiji.bdvpg.bdv.MinimalBdvCreator;
+import sc.fiji.bdvpg.bdv.projector.Projector;
+import sc.fiji.bdvpg.behaviour.SourceAndConverterContextMenuClickBehaviour;
+import sc.fiji.bdvpg.scijava.command.bdv.ScreenShotMakerCommand;
+import sc.fiji.bdvpg.scijava.command.source.SourceAndConverterBlendingModeChangerCommand;
+import sc.fiji.bdvpg.scijava.services.SourceAndConverterBdvDisplayService;
+import sc.fiji.bdvpg.scijava.services.SourceAndConverterService;
+import sc.fiji.bdvpg.services.SourceAndConverterServices;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.*;
+import java.util.function.Supplier;
+
+public class SliceViewer implements Supplier< BdvHandle >
+{
+	public static final String UNDO_SEGMENT_SELECTIONS = "Undo segment selections [ Ctrl Shift N ]";
+	public static final String CHANGE_RANDOM_COLOR_SEED = "Change random color seed";
+	public static final String LOAD_ADDITIONAL_VIEWS = "Load additional views";
+	public static final String SAVE_CURRENT_SETTINGS_AS_VIEW = "Save current settings as view";
+	private final SourceAndConverterBdvDisplayService sacDisplayService;
+	private BdvHandle bdvHandle;
+	private final boolean is2D;
+	private final ViewerManager viewerManager;
+	private final int timepoints;
+
+	private SourceAndConverterContextMenuClickBehaviour contextMenu;
+	private final SourceAndConverterService sacService;
+
+	public SliceViewer( boolean is2D, ViewerManager viewerManager, int timepoints )
+	{
+		this.is2D = is2D;
+		this.viewerManager = viewerManager;
+		this.timepoints = timepoints;
+
+		sacService = ( SourceAndConverterService ) SourceAndConverterServices.getSourceAndConverterService();
+		sacDisplayService = SourceAndConverterServices.getSourceAndConverterDisplayService();
+
+		bdvHandle = createBdv( timepoints );
+		sacDisplayService.registerBdvHandle( bdvHandle );
+
+		installContextMenuAndKeyboardShortCuts();
+	}
+
+	@Override
+	public BdvHandle get()
+	{
+		if ( bdvHandle == null )
+		{
+			bdvHandle = createBdv( timepoints );
+			sacDisplayService.registerBdvHandle( bdvHandle );
+		}
+		return bdvHandle;
+	}
+
+	private void installContextMenuAndKeyboardShortCuts( )
+	{
+		final BdvSegmentSelector segmentBdvSelector = new BdvSegmentSelector( bdvHandle, is2D, () -> viewerManager.getSegmentationDisplays() );
+
+		final Set< String > actionsKeys = sacService.getActionsKeys();
+
+		sacService.registerAction( UNDO_SEGMENT_SELECTIONS, sourceAndConverters -> {
+			// TODO: Maybe only do this for the sacs at the mouse position
+			segmentBdvSelector.clearSelection();
+		} );
+
+		sacService.registerAction( CHANGE_RANDOM_COLOR_SEED, sourceAndConverters -> {
+			// TODO: Maybe only do this for the sacs at the mouse position
+			RandomColorSeedChangerCommand.incrementRandomColorSeed( sourceAndConverters );
+		} );
+
+		sacService.registerAction( LOAD_ADDITIONAL_VIEWS, sourceAndConverters -> {
+			// TODO: Maybe only do this for the sacs at the mouse position
+			viewerManager.getAdditionalViewsLoader().loadAdditionalViewsDialog();
+		} );
+
+		sacService.registerAction( SAVE_CURRENT_SETTINGS_AS_VIEW, sourceAndConverters -> {
+			// TODO: Maybe only do this for the sacs at the mouse position
+			viewerManager.getViewsSaver().saveCurrentSettingsAsViewDialog();
+		} );
+
+		final String[] actions = {
+				sacService.getCommandName( ScreenShotMakerCommand.class ),
+				sacService.getCommandName( BdvLocationLogger.class ),
+				sacService.getCommandName( SourceAndConverterBlendingModeChangerCommand.class ),
+				sacService.getCommandName( RandomColorSeedChangerCommand.class ),
+				UNDO_SEGMENT_SELECTIONS,
+				LOAD_ADDITIONAL_VIEWS,
+				SAVE_CURRENT_SETTINGS_AS_VIEW
+		};
+
+		contextMenu = new SourceAndConverterContextMenuClickBehaviour( bdvHandle, new SourcesAtMousePositionSupplier( bdvHandle, is2D ), actions );
+
+		Behaviours behaviours = new Behaviours( new InputTriggerConfig() );
+		behaviours.behaviour( contextMenu, "Context menu", "button3", "shift P");
+		behaviours.install( bdvHandle.getTriggerbindings(), "MoBIE" );
+
+		behaviours.behaviour(
+				( ClickBehaviour ) ( x, y ) ->
+						new Thread( () -> segmentBdvSelector.run() ).start(),
+				"Toggle selection", "ctrl button1" ) ;
+
+		behaviours.behaviour(
+				( ClickBehaviour ) ( x, y ) ->
+						new Thread( () ->
+						{
+							segmentBdvSelector.clearSelection();
+						}).start(),
+				"Clear selection", "ctrl shift N" ) ;
+	}
+
+	public SourceAndConverterService getSacService()
+	{
+		return sacService;
+	}
+
+	public SourceAndConverterContextMenuClickBehaviour getContextMenu()
+	{
+		return contextMenu;
+	}
+
+	private BdvHandle createBdv( int numTimepoints )
+	{
+		// create Bdv
+		final MinimalBdvCreator bdvCreator = new MinimalBdvCreator( "MoBIE", is2D, Projector.MIXED_PROJECTOR, true, numTimepoints );
+		final BdvHandle bdvHandle = bdvCreator.get();
+
+		return bdvHandle;
+	}
+
+	public BdvHandle getBdvHandle()
+	{
+		return bdvHandle;
+	}
+
+	public Window getWindow()
+	{
+		return SwingUtilities.getWindowAncestor( bdvHandle.getViewerPanel() );
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/bdv/SourcesAtMousePositionSupplier.java b/src/main/java/de/embl/cba/mobie/bdv/SourcesAtMousePositionSupplier.java
new file mode 100644
index 000000000..12be003c8
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/bdv/SourcesAtMousePositionSupplier.java
@@ -0,0 +1,37 @@
+package de.embl.cba.mobie.bdv;
+
+import bdv.util.BdvHandle;
+import bdv.viewer.SourceAndConverter;
+import sc.fiji.bdvpg.services.SourceAndConverterServices;
+import sc.fiji.bdvpg.sourceandconverter.SourceAndConverterHelper;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public class SourcesAtMousePositionSupplier implements Supplier< Collection< SourceAndConverter< ? > > >
+{
+	BdvHandle bdvHandle;
+	boolean is2D;
+
+	public SourcesAtMousePositionSupplier( BdvHandle bdvHandle, boolean is2D )
+	{
+		this.bdvHandle = bdvHandle;
+		this.is2D = is2D;
+	}
+
+	@Override
+	public Collection< SourceAndConverter< ? > > get()
+	{
+		final BdvMousePositionProvider positionProvider = new BdvMousePositionProvider( bdvHandle );
+
+		final List< SourceAndConverter< ? > > sourceAndConverters = SourceAndConverterServices.getSourceAndConverterDisplayService().getSourceAndConverterOf( bdvHandle )
+				.stream()
+				.filter( sac -> SourceAndConverterHelper.isPositionWithinSourceInterval( sac, positionProvider.getPosition(), positionProvider.getTimePoint(), is2D ) )
+				.filter( sac -> SourceAndConverterServices.getSourceAndConverterDisplayService().isVisible( sac, bdvHandle ) )
+				.collect( Collectors.toList() );
+
+		return sourceAndConverters;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/bookmark/Bookmark.java b/src/main/java/de/embl/cba/mobie/bookmark/Bookmark.java
deleted file mode 100644
index 619a3ce18..000000000
--- a/src/main/java/de/embl/cba/mobie/bookmark/Bookmark.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package de.embl.cba.mobie.bookmark;
-
-import de.embl.cba.mobie.image.MutableImageProperties;
-
-import java.util.HashMap;
-
-public class Bookmark
-{
-	public String name;
-	public HashMap< String, MutableImageProperties > layers;
-	public double[] view; // ViewerTransform
-	public String[] normView; // ViewerTransform, normalised for BDV window size
-	public double[] position;
-	public HashMap< String, Layout > layouts;
-}
diff --git a/src/main/java/de/embl/cba/mobie/bookmark/BookmarkManager.java b/src/main/java/de/embl/cba/mobie/bookmark/BookmarkManager.java
deleted file mode 100644
index 018748d6c..000000000
--- a/src/main/java/de/embl/cba/mobie/bookmark/BookmarkManager.java
+++ /dev/null
@@ -1,275 +0,0 @@
-package de.embl.cba.mobie.bookmark;
-
-import bdv.util.BdvHandle;
-import bdv.viewer.Source;
-import de.embl.cba.bdv.utils.BdvUtils;
-import de.embl.cba.bdv.utils.sources.Metadata;
-import de.embl.cba.mobie.bookmark.write.BookmarkFileWriter;
-import de.embl.cba.mobie.bookmark.write.BookmarkWriter;
-import de.embl.cba.mobie.bookmark.write.NameAndFileLocation;
-import de.embl.cba.mobie.image.ImagePropertiesToMetadataAdapter;
-import de.embl.cba.mobie.image.MutableImageProperties;
-import de.embl.cba.mobie.image.SourceGroupLabelSourceCreator;
-import de.embl.cba.mobie.image.SourceGroups;
-import de.embl.cba.mobie.ui.MoBIE;
-import de.embl.cba.mobie.ui.SourcesDisplayManager;
-import de.embl.cba.mobie.bdv.BdvViewChanger;
-import de.embl.cba.mobie.utils.Utils;
-import de.embl.cba.tables.FileAndUrlUtils;
-import de.embl.cba.tables.FileUtils.FileLocation;
-import de.embl.cba.tables.image.SourceAndMetadata;
-import net.imglib2.FinalRealInterval;
-import net.imglib2.realtransform.AffineTransform3D;
-import net.imglib2.type.numeric.integer.IntType;
-
-import javax.swing.*;
-import java.io.IOException;
-import java.util.*;
-
-public class BookmarkManager
-{
-	private final SourcesDisplayManager sourcesDisplayManager;
-	private final MoBIE moBIE;
-	private final Map< String, Bookmark > nameToBookmark;
-	private final BookmarkReader bookmarkReader;
-	private JComboBox<String> bookmarkDropDown;
-	private final String datasetLocation;
-
-	public BookmarkManager( MoBIE moBIE, Map< String, Bookmark > nameToBookmark, BookmarkReader bookmarkReader )
-	{
-		this.moBIE = moBIE;
-		this.sourcesDisplayManager = moBIE.getSourcesDisplayManager();
-		this.nameToBookmark = nameToBookmark;
-		this.bookmarkReader = bookmarkReader;
-		this.datasetLocation = bookmarkReader.getDatasetLocation();
-	}
-
-	public void setBookmarkDropDown ( JComboBox<String> bookmarkDropDown) {
-		this.bookmarkDropDown = bookmarkDropDown;
-	}
-
-	public void setView( String bookmarkId )
-	{
-		final Bookmark bookmark = nameToBookmark.get( bookmarkId );
-
-		if ( bookmark.layers != null && bookmark.layers.size() > 0 )
-		{
-			sourcesDisplayManager.removeAllSourcesFromViewers();
-			show( bookmark );
-		}
-
-		// note: if this is trying to restore the default bookmark
-		// it may not do anything because bdv already automatically
-		// adapts the viewer transform when restoring the default view
-		// in case only one source was added
-		adaptViewerTransform( bookmark );
-	}
-
-	public void show( Bookmark bookmark )
-	{
-		final HashMap< String, SourceAndMetadata > sourcesAndMetadata = createSourcesAndMetadata( bookmark );
-
-		if ( bookmark.layouts != null )
-		{
-			for ( String layoutName : bookmark.layouts.keySet() )
-			{
-				final Layout layout = bookmark.layouts.get( layoutName );
-
-				final String name = bookmark.name + "-" + layoutName;
-				adjustMetadata( sourcesAndMetadata, layout, name );
-
-				final SourceGroupLabelSourceCreator creator = new SourceGroupLabelSourceCreator( sourcesAndMetadata, name + "-labels", layout );
-				final SourceAndMetadata< IntType > sam = creator.create();
-				// FileAndUrlUtils.combinePath( tableDataLocation,
-				sam.metadata().segmentsTablePath = FileAndUrlUtils.combinePath( moBIE.getTablesLocation(), layout.sourceTable );
-
-				sourcesDisplayManager.show( sam );
-			}
-		}
-
-		for ( SourceAndMetadata< ? > sam : sourcesAndMetadata.values() )
-		{
-			// display the source
-			sourcesDisplayManager.show( sam );
-
-			if ( sam.metadata().groupId != null )
-				SourceGroups.addSourceToGroup( sam );
-		}
-	}
-
-	// TODO: Make own Layout class
-	protected void adjustMetadata( HashMap< String, SourceAndMetadata > sourceNameToSourceAndMetadata, Layout layout, String groupName )
-	{
-		for ( String layer : layout.layers )
-		{
-			final SourceAndMetadata sourceAndMetadata = sourceNameToSourceAndMetadata.get( layer );
-			sourceAndMetadata.metadata().groupId = groupName;
-		}
-
-		if ( layout.layoutType.equals( LayoutType.AutoGrid ) )
-		{
-			final int numSources = layout.layers.size();
-			final int numColumns = ( int ) Math.ceil( Math.sqrt( numSources ) );
-			FinalRealInterval bounds = Utils.estimateBounds( sourceNameToSourceAndMetadata.get( layout.layers.get( 0 ) ).source() );
-			final double spacingFactor = 0.1;
-			double border = spacingFactor * ( bounds.realMax( 0 ) - bounds.realMin( 0 ) );
-			double offsetX = 0;
-			double offsetY = 0;
-
-			for ( int sourceIndex = 0, columnIndex = 0; sourceIndex < numSources; sourceIndex++ )
-			{
-				final SourceAndMetadata sam = sourceNameToSourceAndMetadata.get( layout.layers.get( sourceIndex ) );
-
-				final AffineTransform3D transform3D = new AffineTransform3D();
-				transform3D.translate( offsetX, offsetY, 0 );
-				sam.metadata().addedTransform = transform3D.getRowPackedCopy();
-
-				if ( ++columnIndex == numColumns )
-				{
-					offsetY += bounds.realMax( 1 ) + border;
-					offsetX = 0;
-					columnIndex = 0;
-				}
-				else
-				{
-					offsetX += bounds.realMax( 1 ) + border;
-				}
-			}
-		}
-	}
-
-	private HashMap< String, SourceAndMetadata > createSourcesAndMetadata( Bookmark bookmark )
-	{
-		final HashMap< String, SourceAndMetadata > sourceNameToSourceAndMetadata = new HashMap<>();
-		for ( String sourceName : bookmark.layers.keySet() )
-		{
-			if ( sourcesDisplayManager.getVisibleSourceNames().contains( sourceName ) )
-				continue;
-
-			final Source< ? > source
-					= sourcesDisplayManager.getSourceAndDefaultMetadata( sourceName ).source();
-
-			final Metadata metadata
-					= sourcesDisplayManager.getSourceAndDefaultMetadata( sourceName ).metadata().copy();
-
-			final MutableImageProperties mutableImageProperties
-					= bookmark.layers.get( sourceName );
-
-			// write mutable image properties from bookmark into default metadata
-			updateSourceMetadata( metadata, mutableImageProperties );
-
-			final SourceAndMetadata< ? > sam
-					= new SourceAndMetadata( source, metadata );
-
-			sourceNameToSourceAndMetadata.put( sourceName, sam );
-		}
-
-		return sourceNameToSourceAndMetadata;
-	}
-
-	public void updateSourceMetadata( Metadata sourceMetadata, MutableImageProperties value )
-	{
-		final ImagePropertiesToMetadataAdapter adapter = new ImagePropertiesToMetadataAdapter();
-		adapter.setMetadataFromMutableImageProperties( sourceMetadata, value );
-	}
-
-	public void adaptViewerTransform( Bookmark bookmark )
-	{
-		final Location location = getLocationFromBookmark( bookmark );
-
-		if ( location != null )
-		{
-			BdvViewChanger.moveToLocation( sourcesDisplayManager.getBdv(), location );
-		}
-	}
-
-	public void loadAdditionalBookmarks() {
-			Map<String, Bookmark> additionalBookmarks = bookmarkReader.selectAndReadBookmarks();
-			if (additionalBookmarks != null) {
-				nameToBookmark.putAll(additionalBookmarks);
-				bookmarkDropDown.removeAllItems();
-				for ( String bookmarkName : nameToBookmark.keySet()) {
-					bookmarkDropDown.addItem(bookmarkName);
-				}
-			}
-	}
-
-	public void saveCurrentSettingsAsBookmark() {
-		NameAndFileLocation bookmarkNameAndFileLocation = BookmarkWriter.bookmarkSaveDialog();
-		Bookmark currentBookmark = createBookmarkFromCurrentSettings( bookmarkNameAndFileLocation.name);
-		ArrayList<Bookmark> bookmarks = new ArrayList<>();
-		bookmarks.add(currentBookmark);
-
-		if ( bookmarkNameAndFileLocation.location.equals( FileLocation.Project ) &&
-				datasetLocation.contains( "raw.githubusercontent" )) {
-			BookmarkWriter.saveBookmarksToGithub(bookmarks, bookmarkReader );
-		} else {
-			try {
-				BookmarkFileWriter.saveBookmarksToFile(bookmarks, bookmarkNameAndFileLocation.location, datasetLocation );
-			} catch (IOException e) {
-				e.printStackTrace();
-			}
-		}
-	}
-
-	public Bookmark createBookmarkFromCurrentSettings(String bookmarkName) {
-		HashMap< String, MutableImageProperties > layers = new HashMap<>();
-		Set<String> visibleSourceNames = sourcesDisplayManager.getVisibleSourceNames();
-
-		for (String sourceName : visibleSourceNames) {
-			sourcesDisplayManager.updateCurrentMetadata( sourceName );
-			MutableImageProperties imageProperties = getMutableImagePropertiesFromCurrentMetadata(sourceName);
-			layers.put(sourceName, imageProperties);
-		}
-
-		BdvHandle bdv = sourcesDisplayManager.getBdv();
-		Bookmark currentBookmark = new Bookmark();
-		currentBookmark.name = bookmarkName;
-		currentBookmark.layers = layers;
-		double[] currentPosition = new double[3];
-		BdvUtils.getGlobalMouseCoordinates(bdv).localize(currentPosition);
-		currentBookmark.position = currentPosition;
-		currentBookmark.normView = Utils.createNormalisedViewerTransformString( bdv, Utils.getMousePosition( bdv ) ).split(",");
-		currentBookmark.view = null;
-
-		return currentBookmark;
-	}
-
-	private MutableImageProperties getMutableImagePropertiesFromCurrentMetadata( String sourceName )
-	{
-		Metadata metadata = sourcesDisplayManager.getSourceAndCurrentMetadata( sourceName ).metadata();
-
-		MutableImageProperties imageProperties = new MutableImageProperties();
-		final ImagePropertiesToMetadataAdapter adapter = new ImagePropertiesToMetadataAdapter();
-		adapter.setMutableImagePropertiesFromMetadata( imageProperties, metadata );
-
-		return imageProperties;
-	}
-
-	public static Location getLocationFromBookmark( Bookmark bookmark )
-	{
-		if ( bookmark.normView != null )
-		{
-			final double[] doubles = Arrays.stream( bookmark.normView ).mapToDouble( x -> Double.parseDouble( x.replace( "n", "" ) ) ).toArray();
-
-			return new Location( LocationType.NormalisedViewerTransform, doubles );
-		}
-		else if ( bookmark.view != null  )
-		{
-			return new Location( LocationType.ViewerTransform, bookmark.view );
-		}
-		else if ( bookmark.position != null )
-		{
-			return new Location( LocationType.Position3d, bookmark.position );
-		}
-		else
-		{
-			return null;
-		}
-	}
-
-	public Set< String > getBookmarkNames()
-	{
-		return nameToBookmark.keySet();
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/bookmark/BookmarkReader.java b/src/main/java/de/embl/cba/mobie/bookmark/BookmarkReader.java
deleted file mode 100644
index 6ba3e850c..000000000
--- a/src/main/java/de/embl/cba/mobie/bookmark/BookmarkReader.java
+++ /dev/null
@@ -1,93 +0,0 @@
-package de.embl.cba.mobie.bookmark;
-
-import com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
-import com.google.gson.stream.JsonReader;
-import de.embl.cba.tables.FileAndUrlUtils;
-
-import java.io.*;
-import java.lang.reflect.Type;
-import java.util.*;
-
-import static de.embl.cba.tables.FileUtils.selectPathFromProjectOrFileSystem;
-
-public class BookmarkReader
-{
-	private final String datasetLocation;
-
-	public BookmarkReader( String datasetLocation ) {
-		this.datasetLocation = datasetLocation;
-	}
-
-	public static Map< String, Bookmark > readBookmarksFromFile( Gson gson, Type type, String bookmarksLocation ) throws IOException
-	{
-		try ( InputStream inputStream = FileAndUrlUtils.getInputStream( bookmarksLocation );
-			  final JsonReader reader = new JsonReader( new InputStreamReader( inputStream, "UTF-8" )) ) {
-			final Map<String, Bookmark> stringBookmarkMap = gson.fromJson(reader, type);
-			return stringBookmarkMap;
-		}
-	}
-
-	public String getDatasetLocation() {
-		return datasetLocation;
-	}
-
-	public Map<String, Bookmark> readDefaultBookmarks() {
-		try {
-			ArrayList<String> filePaths = new ArrayList<>();
-			String bookmarkPath = FileAndUrlUtils.combinePath(datasetLocation, "misc", "bookmarks", "default.json");
-			filePaths.add(bookmarkPath);
-			return parseBookmarks( filePaths );
-		} catch (IOException e) {
-			e.printStackTrace();
-			return null;
-		}
-	}
-
-	public Map<String, Bookmark> selectAndReadBookmarks() {
-		try {
-			ArrayList<String> filePaths = new ArrayList<>();
-			String bookmarksDirectory = FileAndUrlUtils.combinePath(datasetLocation, "misc", "bookmarks");
-			String selectedFilePath = selectPathFromProjectOrFileSystem( bookmarksDirectory, "Bookmark" );
-			if (selectedFilePath != null) {
-				filePaths.add(selectedFilePath);
-				return parseBookmarks(filePaths);
-			} else {
-				return null;
-			}
-		} catch (IOException e) {
-			e.printStackTrace();
-			return null;
-		}
-	}
-
-	public Map< String, Bookmark > parseBookmarks( ArrayList< String > bookmarksFiles ) throws IOException
-	{
-		Map< String, Bookmark > nameToBookmark = new TreeMap<>();
-
-		Gson gson = new Gson();
-		Type type = new TypeToken< Map< String, Bookmark > >() {}.getType();
-
-		// parse bookmark files
-		// each file can contain multiple bookmarks
-		for ( String bookmarksFile : bookmarksFiles )
-		{
-			if ( ! bookmarksFile.endsWith( ".json" ) )
-			{
-				System.out.println("Found invalid bookmarks file: " + bookmarksFile );
-				continue;
-			}
-
-			final Map< String, Bookmark > bookmarks = readBookmarksFromFile( gson, type, bookmarksFile );
-
-			for ( Map.Entry< String, Bookmark > entry : bookmarks.entrySet() )
-			{
-				nameToBookmark.put( entry.getKey(), entry.getValue() );
-				entry.getValue().name = entry.getKey();
-			}
-		}
-
-		return nameToBookmark;
-	}
-
-}
diff --git a/src/main/java/de/embl/cba/mobie/bookmark/Layout.java b/src/main/java/de/embl/cba/mobie/bookmark/Layout.java
deleted file mode 100644
index 71fccdf3e..000000000
--- a/src/main/java/de/embl/cba/mobie/bookmark/Layout.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package de.embl.cba.mobie.bookmark;
-
-import java.util.ArrayList;
-
-public class Layout
-{
-	public ArrayList< String > layers;
-	public LayoutType layoutType;
-	public String sourceTable;
-}
diff --git a/src/main/java/de/embl/cba/mobie/bookmark/LayoutType.java b/src/main/java/de/embl/cba/mobie/bookmark/LayoutType.java
deleted file mode 100644
index 1b40c5973..000000000
--- a/src/main/java/de/embl/cba/mobie/bookmark/LayoutType.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package de.embl.cba.mobie.bookmark;
-
-public enum LayoutType
-{
-	AutoGrid
-}
diff --git a/src/main/java/de/embl/cba/mobie/bookmark/write/BookmarkFileWriter.java b/src/main/java/de/embl/cba/mobie/bookmark/write/BookmarkFileWriter.java
deleted file mode 100644
index d9e977ffa..000000000
--- a/src/main/java/de/embl/cba/mobie/bookmark/write/BookmarkFileWriter.java
+++ /dev/null
@@ -1,104 +0,0 @@
-package de.embl.cba.mobie.bookmark.write;
-
-import com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
-import com.google.gson.stream.JsonWriter;
-import de.embl.cba.mobie.bookmark.Bookmark;
-import de.embl.cba.mobie.bookmark.BookmarkReader;
-import de.embl.cba.tables.FileAndUrlUtils;
-import de.embl.cba.tables.FileUtils;
-
-import javax.swing.*;
-import javax.swing.filechooser.FileNameExtensionFilter;
-import java.io.*;
-import java.lang.reflect.Type;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
-
-public class BookmarkFileWriter
-{
-	public static boolean appendToFileDialog ()
-	{
-		int result = JOptionPane.showConfirmDialog(null,
-				"This Json file already exists - append bookmark to this file?", "Append to file?",
-				JOptionPane.YES_NO_OPTION,
-				JOptionPane.QUESTION_MESSAGE);
-		if (result != JOptionPane.YES_OPTION) {
-			return false;
-		} else {
-			return true;
-		}
-	}
-
-	public static void writeBookmarksToFile ( Gson gson, Type type, File jsonFile, Map< String, Bookmark > bookmarks) throws IOException
-	{
-		Map<String, Bookmark> bookmarksInFile = new HashMap<>();
-		// If json already exists, read existing bookmarks to append new ones
-		if (jsonFile.exists()) {
-			bookmarksInFile.putAll( BookmarkReader.readBookmarksFromFile(gson, type, jsonFile.getAbsolutePath()));
-		}
-		bookmarksInFile.putAll(bookmarks);
-
-		try ( OutputStream outputStream = new FileOutputStream( jsonFile );
-			  final JsonWriter writer = new JsonWriter( new OutputStreamWriter(outputStream, "UTF-8")) ) {
-			writer.setIndent("	");
-			gson.toJson(bookmarksInFile, type, writer);
-		}
-	}
-
-	public static void saveBookmarksToFile( Map<String, Bookmark> bookmarks, File jsonFile ) throws IOException {
-		Gson gson = BookmarkGsonBuilderCreator.createGsonBuilder(false);
-		Type type = new TypeToken<Map<String, Bookmark>>() {
-		}.getType();
-
-		writeBookmarksToFile(gson, type, jsonFile, bookmarks);
-	}
-
-	public static String chooseJsonFileLocation( FileUtils.FileLocation fileLocation, String datasetLocation ) {
-		String jsonPath = null;
-		final JFileChooser jFileChooser;
-		if (fileLocation.equals( FileUtils.FileLocation.FileSystem )) {
-			jFileChooser = new JFileChooser();
-		} else {
-			String bookmarksDirectory = FileAndUrlUtils.combinePath( datasetLocation, "misc", "bookmarks");
-			jFileChooser = new JFileChooser(bookmarksDirectory);
-		}
-		jFileChooser.setFileFilter(new FileNameExtensionFilter("json", "json"));
-		if (jFileChooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
-			jsonPath = jFileChooser.getSelectedFile().getAbsolutePath();
-		}
-
-		if (jsonPath != null) {
-
-			if (!jsonPath.endsWith(".json")) {
-				jsonPath += ".json";
-			}
-
-			File jsonFile = new File(jsonPath);
-			if (jsonFile.exists()) {
-				// check if want to append to existing file, otherwise abort
-				if (! appendToFileDialog() ) {
-					jsonPath = null;
-				}
-			}
-		}
-
-		return jsonPath;
-	}
-
-	public static void saveBookmarksToFile( ArrayList< Bookmark > bookmarks, FileUtils.FileLocation fileLocation, String datasetLocation ) throws IOException {
-		HashMap<String, Bookmark> namesToBookmarks = new HashMap<>();
-		for (Bookmark bookmark : bookmarks) {
-			namesToBookmarks.put(bookmark.name, bookmark);
-		}
-
-		String jsonPath = chooseJsonFileLocation( fileLocation, datasetLocation );
-
-		if ( jsonPath != null ) {
-			File jsonFile = new File( jsonPath );
-
-			saveBookmarksToFile( namesToBookmarks, jsonFile );
-		}
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/bookmark/write/BookmarkGithubWriter.java b/src/main/java/de/embl/cba/mobie/bookmark/write/BookmarkGithubWriter.java
deleted file mode 100644
index ee69c7e10..000000000
--- a/src/main/java/de/embl/cba/mobie/bookmark/write/BookmarkGithubWriter.java
+++ /dev/null
@@ -1,159 +0,0 @@
-package de.embl.cba.mobie.bookmark.write;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.internal.LinkedTreeMap;
-import com.google.gson.reflect.TypeToken;
-import de.embl.cba.mobie.bookmark.Bookmark;
-import de.embl.cba.mobie.bookmark.BookmarkReader;
-import de.embl.cba.tables.github.GitHubContentGetter;
-import de.embl.cba.tables.github.GitHubFileCommitter;
-import de.embl.cba.tables.github.GitLocation;
-import ij.Prefs;
-import ij.gui.GenericDialog;
-import org.apache.commons.compress.utils.FileNameUtils;
-
-import java.io.IOException;
-import java.lang.reflect.Type;
-import java.nio.charset.StandardCharsets;
-import java.util.*;
-
-public class BookmarkGithubWriter {
-
-    public static final String ACCESS_TOKEN = "MoBIE.GitHub access token";
-    private String accessToken;
-    private String bookmarkFileName;
-    private GitLocation bookmarkGitLocation;
-    private BookmarkReader bookmarkReader;
-
-    BookmarkGithubWriter(GitLocation bookmarkGitLocation, BookmarkReader bookmarkReader ) {
-        this.bookmarkGitLocation = bookmarkGitLocation;
-        this.bookmarkReader = bookmarkReader;
-    }
-
-    private static String writeBookmarksToBase64String ( Map<String, Bookmark > bookmarks) {
-        Gson gson = BookmarkGsonBuilderCreator.createGsonBuilder(true);
-        Type type = new TypeToken<Map<String, Bookmark>>() {
-        }.getType();
-        String jsonString = gson.toJson(bookmarks, type);
-        byte[] jsonBytes = jsonString.getBytes( StandardCharsets.UTF_8);
-        return Base64.getEncoder().encodeToString(jsonBytes);
-        // TODO - add new line at end?
-    }
-
-    private Map< String, String > getFilePathsToSha()
-    {
-        final GitHubContentGetter contentGetter =
-                new GitHubContentGetter( bookmarkGitLocation.repoUrl, bookmarkGitLocation.path, bookmarkGitLocation.branch, null );
-        final String json = contentGetter.getContent();
-
-        GsonBuilder builder = new GsonBuilder();
-
-        final Map< String, String > bookmarkPathsToSha = new HashMap<>();
-        ArrayList<LinkedTreeMap> linkedTreeMaps = ( ArrayList< LinkedTreeMap >) builder.create().fromJson( json, Object.class );
-        for ( LinkedTreeMap linkedTreeMap : linkedTreeMaps )
-        {
-            final String downloadUrl = ( String ) linkedTreeMap.get( "download_url" );
-            final String sha = (String) linkedTreeMap.get( "sha" );
-            bookmarkPathsToSha.put( downloadUrl, sha );
-        }
-        return bookmarkPathsToSha;
-    }
-
-    private Map<String, String> getBookmarkFileNamesToPaths(Set<String> bookmarkPaths) {
-        Map<String, String> bookmarkNamesToPaths = new HashMap<>();
-        for (String path : bookmarkPaths) {
-            bookmarkNamesToPaths.put(FileNameUtils.getBaseName(path), path);
-        }
-        return bookmarkNamesToPaths;
-    }
-
-    class FilePathAndSha {
-        String filePath;
-        String sha;
-    }
-
-    private FilePathAndSha getMatchingBookmarkFilePathAndSha () {
-        Map<String, String> bookmarkPathsToSha = getFilePathsToSha();
-        Map<String, String> bookmarkFileNamesToPaths = getBookmarkFileNamesToPaths(bookmarkPathsToSha.keySet());
-
-        for (String bookmarkFileNameGithub : bookmarkFileNamesToPaths.keySet()) {
-            if (bookmarkFileNameGithub.equals(bookmarkFileName)) {
-                FilePathAndSha matchingFileAndSha = new FilePathAndSha();
-                String matchingPath = bookmarkFileNamesToPaths.get(bookmarkFileNameGithub);
-                matchingFileAndSha.filePath = matchingPath;
-                matchingFileAndSha.sha = bookmarkPathsToSha.get(matchingPath);
-                return matchingFileAndSha;
-            }
-        }
-
-        return null;
-    }
-
-    public void writeBookmarksToGithub(ArrayList<Bookmark> bookmarks) {
-        if (showDialog()) {
-            try {
-                HashMap<String, Bookmark> namesToBookmarks = new HashMap<>();
-                for (Bookmark bookmark : bookmarks) {
-                    namesToBookmarks.put(bookmark.name, bookmark);
-                }
-
-                // check for matching bookmark file on github
-                FilePathAndSha matchingFilePathAndSha = getMatchingBookmarkFilePathAndSha();
-
-                boolean appendToFile = false;
-                if (matchingFilePathAndSha != null) {
-                    appendToFile = BookmarkFileWriter.appendToFileDialog();
-                }
-
-                // don't continue if matching file was found, but user does not want to append to it
-                if (!(matchingFilePathAndSha != null && !appendToFile)) {
-
-                    Map<String, Bookmark> bookmarksInFile = new HashMap<>();
-
-                    if (appendToFile) {
-                        ArrayList<String> matchingFilePathsFromGithub = new ArrayList<>();
-                        matchingFilePathsFromGithub.add(matchingFilePathAndSha.filePath);
-                        Map<String, Bookmark> existingBookmarks = bookmarkReader.parseBookmarks(matchingFilePathsFromGithub);
-                        bookmarksInFile.putAll(existingBookmarks);
-                    }
-                    bookmarksInFile.putAll(namesToBookmarks);
-
-                    final String bookmarkJsonBase64String = writeBookmarksToBase64String(bookmarksInFile);
-
-                    final GitHubFileCommitter fileCommitter;
-                    if (appendToFile) {
-                        fileCommitter = new GitHubFileCommitter(
-                                bookmarkGitLocation.repoUrl, accessToken, bookmarkGitLocation.branch,
-                                bookmarkGitLocation.path + "/" + bookmarkFileName + ".json", matchingFilePathAndSha.sha);
-                    } else {
-                        fileCommitter = new GitHubFileCommitter(
-                                bookmarkGitLocation.repoUrl, accessToken, bookmarkGitLocation.branch,
-                                bookmarkGitLocation.path + "/" + bookmarkFileName + ".json");
-                    }
-                    fileCommitter.commitStringAsFile("Add new bookmarks from UI", bookmarkJsonBase64String);
-                }
-            } catch (IOException e) {
-                e.printStackTrace();
-            }
-        }
-    }
-
-    private boolean showDialog()
-    {
-        final GenericDialog gd = new GenericDialog( "Save to github" );
-
-        gd.addStringField( "GitHub access token", Prefs.get( ACCESS_TOKEN, "1234567890" ));
-        gd.addStringField( "Bookmark file name", "");
-        gd.showDialog();
-
-        if ( gd.wasCanceled() ) return false;
-
-        accessToken = gd.getNextString();
-        bookmarkFileName = gd.getNextString();
-
-        Prefs.set( ACCESS_TOKEN, accessToken );
-
-        return true;
-    }
-}
diff --git a/src/main/java/de/embl/cba/mobie/bookmark/write/BookmarkGsonBuilderCreator.java b/src/main/java/de/embl/cba/mobie/bookmark/write/BookmarkGsonBuilderCreator.java
deleted file mode 100644
index 74608fcdf..000000000
--- a/src/main/java/de/embl/cba/mobie/bookmark/write/BookmarkGsonBuilderCreator.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package de.embl.cba.mobie.bookmark.write;
-
-import com.google.gson.ExclusionStrategy;
-import com.google.gson.FieldAttributes;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-
-public class BookmarkGsonBuilderCreator
-{
-	public static Gson createGsonBuilder( boolean usePrettyPrinting) {
-		// exclude the name field from json
-		ExclusionStrategy strategy = new ExclusionStrategy() {
-			@Override
-			public boolean shouldSkipField( FieldAttributes f) {
-				if (f.getName().equals("name") || f.getName().equals("type") ||
-						f.getName().equals("storage") || f.getName().equals("tableFolder")) {
-					return true;
-				}
-				return false;
-			}
-
-			@Override
-			public boolean shouldSkipClass(Class<?> clazz) {
-				return false;
-			}
-		};
-
-		Gson gson;
-		if (usePrettyPrinting) {
-			gson = new GsonBuilder().setPrettyPrinting().addSerializationExclusionStrategy(strategy).create();
-		} else {
-			gson = new GsonBuilder().addSerializationExclusionStrategy(strategy).create();
-		}
-		return gson;
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/bookmark/write/BookmarkWriter.java b/src/main/java/de/embl/cba/mobie/bookmark/write/BookmarkWriter.java
deleted file mode 100644
index d2d6f6721..000000000
--- a/src/main/java/de/embl/cba/mobie/bookmark/write/BookmarkWriter.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package de.embl.cba.mobie.bookmark.write;
-
-import de.embl.cba.mobie.bookmark.Bookmark;
-import de.embl.cba.mobie.bookmark.BookmarkReader;
-import de.embl.cba.tables.FileUtils;
-import de.embl.cba.tables.github.GitHubUtils;
-import de.embl.cba.tables.github.GitLocation;
-import ij.gui.GenericDialog;
-
-import java.util.ArrayList;
-
-public class BookmarkWriter
-{
-	public static void saveBookmarksToGithub( ArrayList< Bookmark > bookmarks, BookmarkReader bookmarkReader ) {
-		final GitLocation gitLocation = GitHubUtils.rawUrlToGitLocation( bookmarkReader.getDatasetLocation() );
-		gitLocation.path += "misc/bookmarks";
-
-		BookmarkGithubWriter bookmarkWriter = new BookmarkGithubWriter(gitLocation, bookmarkReader );
-		bookmarkWriter.writeBookmarksToGithub(bookmarks);
-	}
-
-	public static NameAndFileLocation bookmarkSaveDialog () {
-		FileUtils.FileLocation fileLocation = null;
-		String bookmarkName = null;
-		final GenericDialog gd = new GenericDialog( "Choose save location" );
-		gd.addStringField("Bookmark Name", "name");
-		gd.addChoice( "Save to", new String[]{ FileUtils.FileLocation.Project.toString(),
-				FileUtils.FileLocation.FileSystem.toString() }, FileUtils.FileLocation.Project.toString() );
-		gd.showDialog();
-
-		if ( gd.wasCanceled() ) return null;
-		bookmarkName = gd.getNextString();
-		fileLocation = FileUtils.FileLocation.valueOf( gd.getNextChoice() );
-
-		NameAndFileLocation bookmarkNameAndFileLocation = new NameAndFileLocation();
-		bookmarkNameAndFileLocation.name = bookmarkName;
-		bookmarkNameAndFileLocation.location = fileLocation;
-
-		return bookmarkNameAndFileLocation;
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/bookmark/write/NameAndFileLocation.java b/src/main/java/de/embl/cba/mobie/bookmark/write/NameAndFileLocation.java
deleted file mode 100644
index ef5051f59..000000000
--- a/src/main/java/de/embl/cba/mobie/bookmark/write/NameAndFileLocation.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package de.embl.cba.mobie.bookmark.write;
-
-import de.embl.cba.tables.FileUtils;
-
-public class NameAndFileLocation
-{
-	public String name;
-	public FileUtils.FileLocation location;
-}
diff --git a/src/main/java/de/embl/cba/mobie/color/ColoringModelHelper.java b/src/main/java/de/embl/cba/mobie/color/ColoringModelHelper.java
new file mode 100644
index 000000000..07054c8b4
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/color/ColoringModelHelper.java
@@ -0,0 +1,34 @@
+package de.embl.cba.mobie.color;
+
+import de.embl.cba.mobie.display.SegmentationSourceDisplay;
+import de.embl.cba.tables.color.ColoringModel;
+import de.embl.cba.tables.color.ColumnColoringModelCreator;
+import de.embl.cba.tables.tablerow.TableRowImageSegment;
+
+public class ColoringModelHelper
+{
+	public static void configureMoBIEColoringModel( SegmentationSourceDisplay segmentationDisplay )
+	{
+		if ( segmentationDisplay.getColorByColumn() != null )
+		{
+			final ColumnColoringModelCreator< TableRowImageSegment > modelCreator = new ColumnColoringModelCreator( segmentationDisplay.segments );
+
+			final ColoringModel< TableRowImageSegment > coloringModel;
+
+			String coloringLut = getColoringLut( segmentationDisplay );
+
+			coloringModel = modelCreator.createColoringModel( segmentationDisplay.getColorByColumn(), coloringLut, segmentationDisplay.getValueLimits()[ 0 ], segmentationDisplay.getValueLimits()[ 1 ] );
+
+			segmentationDisplay.coloringModel = new MoBIEColoringModel( coloringModel );
+		}
+		else
+		{
+			segmentationDisplay.coloringModel = new MoBIEColoringModel<>( segmentationDisplay.getLut() );
+		}
+	}
+
+	private static String getColoringLut( SegmentationSourceDisplay segmentationDisplay )
+	{
+		return segmentationDisplay.getLut() ;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/color/LabelConverter.java b/src/main/java/de/embl/cba/mobie/color/LabelConverter.java
new file mode 100644
index 000000000..483e54796
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/color/LabelConverter.java
@@ -0,0 +1,115 @@
+/*-
+ * #%L
+ * TODO
+ * %%
+ * Copyright (C) 2018 - 2020 EMBL
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+package de.embl.cba.mobie.color;
+
+import bdv.viewer.TimePointListener;
+import de.embl.cba.mobie.segment.SegmentAdapter;
+import de.embl.cba.tables.imagesegment.ImageSegment;
+import net.imglib2.Volatile;
+import net.imglib2.converter.Converter;
+import net.imglib2.type.numeric.ARGBType;
+import net.imglib2.type.numeric.RealType;
+
+public class LabelConverter< S extends ImageSegment > implements Converter< RealType, ARGBType >, TimePointListener, OpacityAdjuster
+{
+	private final SegmentAdapter< S > segmentAdapter;
+	private final String imageId;
+	private final MoBIEColoringModel< S > coloringModel;
+
+	private int frame;
+	private double opacity = 1.0;
+
+	public LabelConverter(
+			SegmentAdapter< S > segmentAdapter,
+			String imageId,
+			MoBIEColoringModel< S > coloringModel )
+	{
+		this.segmentAdapter = segmentAdapter;
+		this.imageId = imageId;
+		this.coloringModel = coloringModel;
+		this.frame = 0;
+	}
+
+	@Override
+	public void convert( RealType label, ARGBType color )
+	{
+		if ( label instanceof Volatile )
+		{
+			if ( ! ( ( Volatile ) label ).isValid() )
+			{
+				color.set( 0 );
+				return;
+			}
+		}
+
+		if ( label.getRealDouble() == 0 )
+		{
+			color.set( 0 );
+			return;
+		}
+
+		final S imageSegment = segmentAdapter.getSegment( label.getRealDouble(), frame, imageId );
+
+		if ( imageSegment == null )
+		{
+			color.set( 0 );
+			return;
+		}
+		else
+		{
+			coloringModel.convert( imageSegment, color );
+			final int alpha = ARGBType.alpha( color.get() );
+			color.mul( alpha / 255.0 );
+		}
+
+		color.mul( opacity );
+	}
+
+	public void timePointChanged( int timePointIndex )
+	{
+		this.frame = timePointIndex;
+	}
+
+	@Override
+	public void setOpacity( double opacity )
+	{
+		this.opacity = opacity;
+	}
+
+	@Override
+	public double getOpacity()
+	{
+		return opacity;
+	}
+
+	public MoBIEColoringModel< S > getColoringModel()
+	{
+		return coloringModel;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/color/ListItemsARGBConverter.java b/src/main/java/de/embl/cba/mobie/color/ListItemsARGBConverter.java
new file mode 100644
index 000000000..e66fe272b
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/color/ListItemsARGBConverter.java
@@ -0,0 +1,76 @@
+package de.embl.cba.mobie.color;
+
+import de.embl.cba.tables.color.ColoringModel;
+import net.imglib2.Volatile;
+import net.imglib2.converter.Converter;
+import net.imglib2.type.numeric.ARGBType;
+import net.imglib2.type.numeric.RealType;
+
+import java.util.HashMap;
+import java.util.List;
+
+public class ListItemsARGBConverter< T > implements Converter< RealType, ARGBType >, OpacityAdjuster
+{
+	public static final int OUT_OF_BOUNDS_ROW_INDEX = -1;
+	private final ColoringModel< T > coloringModel;
+	private final List< T > list;
+	private int backgroundARGBIndex; // default, background color
+	private final HashMap< Integer, Integer > indexToColor;
+	private double opacity = 0.5;
+
+	public ListItemsARGBConverter(
+			List< T > list,
+			ColoringModel< T > coloringModel )
+	{
+		this.list = list;
+		this.coloringModel = coloringModel;
+		backgroundARGBIndex = ARGBType.rgba( 0,0,0,255 );
+		indexToColor = new HashMap<>();
+	}
+
+	@Override
+	public void convert( RealType rowIndex, ARGBType color )
+	{
+		if ( rowIndex instanceof Volatile )
+		{
+			if ( ! ( ( Volatile ) rowIndex ).isValid() )
+			{
+				color.set( backgroundARGBIndex );
+				return;
+			}
+		}
+
+		final int index = ( int ) rowIndex.getRealDouble();
+
+		if ( index == OUT_OF_BOUNDS_ROW_INDEX )
+		{
+			color.set( backgroundARGBIndex );
+			return;
+		}
+
+		final T item = list.get( index );
+
+		if ( item == null )
+		{
+			throw new UnsupportedOperationException( "Item not found " + item );
+		}
+		else
+		{
+			coloringModel.convert( item, color );
+		}
+
+		color.mul( opacity );
+	}
+
+	@Override
+	public void setOpacity( double opacity )
+	{
+		this.opacity = opacity;
+	}
+
+	@Override
+	public double getOpacity()
+	{
+		return opacity;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/color/LutFactory.java b/src/main/java/de/embl/cba/mobie/color/LutFactory.java
new file mode 100644
index 000000000..2846a5d13
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/color/LutFactory.java
@@ -0,0 +1,27 @@
+package de.embl.cba.mobie.color;
+
+import de.embl.cba.bdv.utils.lut.ARGBLut;
+import de.embl.cba.bdv.utils.lut.BlueWhiteRedARGBLut;
+import de.embl.cba.bdv.utils.lut.GlasbeyARGBLut;
+import de.embl.cba.bdv.utils.lut.ViridisARGBLut;
+import de.embl.cba.tables.color.ColoringLuts;
+
+// TODO:
+//  Make the LUTs an inner enum class of the LutFactory
+public class LutFactory
+{
+	public ARGBLut get( String lut )
+	{
+		switch ( lut )
+		{
+			case ColoringLuts.BLUE_WHITE_RED:
+				return new BlueWhiteRedARGBLut( 1000 );
+			case ColoringLuts.VIRIDIS:
+				return new ViridisARGBLut();
+			case ColoringLuts.GLASBEY:
+				return new GlasbeyARGBLut();
+			default:
+				return new GlasbeyARGBLut();
+		}
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/color/MoBIEColoringModel.java b/src/main/java/de/embl/cba/mobie/color/MoBIEColoringModel.java
new file mode 100644
index 000000000..2dbc64ea3
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/color/MoBIEColoringModel.java
@@ -0,0 +1,217 @@
+/*-
+ * #%L
+ * Various Java code for ImageJ
+ * %%
+ * Copyright (C) 2018 - 2021 EMBL
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+package de.embl.cba.mobie.color;
+
+import de.embl.cba.tables.color.*;
+import de.embl.cba.tables.select.SelectionModel;
+import net.imglib2.type.numeric.ARGBType;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class MoBIEColoringModel< T > extends AbstractColoringModel< T >
+{
+	private ColoringModel< T > coloringModel;
+	private SelectionModel< T > selectionModel;
+
+	private SelectionColoringMode selectionColoringMode;
+	private ARGBType selectionColor;
+	private double opacityNotSelected;
+
+	public static final ARGBType YELLOW = new ARGBType( ARGBType.rgba( 255, 255, 0, 255 ) );
+	public static final ARGBType TRANSPARENT = new ARGBType( ARGBType.rgba( 0, 0, 0, 0 ) );
+
+	private List< SelectionColoringMode > selectionColoringModes;
+
+	public enum SelectionColoringMode
+	{
+		SelectionColor,
+		SelectionColorAndDimNotSelected,
+		DimNotSelected
+	}
+
+	public MoBIEColoringModel( ColoringModel< T > coloringModel )
+	{
+		setColoringModel( coloringModel );
+		init();
+	}
+
+	public MoBIEColoringModel( String lut )
+	{
+		setColoringModel( new LazyCategoryColoringModel<>( new LutFactory().get( lut ) ) );
+		init();
+	}
+
+	private void init()
+	{
+		this.selectionColoringModes = Arrays.asList( SelectionColoringMode.values() );
+		this.selectionColor = YELLOW;
+		this.opacityNotSelected = 0.15;
+		this.selectionColoringMode = SelectionColoringMode.DimNotSelected;
+	}
+
+	public void setSelectionModel( SelectionModel< T > selectionModel )
+	{
+		this.selectionModel = selectionModel;
+	}
+
+	@Override
+	public void convert( T input, ARGBType output )
+	{
+		coloringModel.convert( input, output );
+
+		if ( selectionModel == null ) return;
+
+		// TODO: this is very inefficient as it recomputes the colors all the time
+		//   implement a selectionChanged Listener!
+		if ( selectionModel.isEmpty() ) return;
+
+		final boolean isSelected = selectionModel.isSelected( input );
+
+		switch ( selectionColoringMode )
+		{
+			case DimNotSelected:
+				if ( ! isSelected )
+					dim( output, opacityNotSelected );
+				break;
+
+			case SelectionColor:
+				if ( isSelected )
+					output.set( selectionColor );
+				break;
+
+			case SelectionColorAndDimNotSelected:
+				if ( isSelected )
+					output.set( selectionColor );
+				else
+					dim( output, opacityNotSelected );
+				break;
+
+			default:
+				break;
+		}
+	}
+
+	/**
+	 * Implements dimming via alpha
+	 *
+	 * @param output
+	 * @param alpha
+	 */
+	private void dim( ARGBType output, double alpha )
+	{
+		final int colorIndex = output.get();
+
+		output.set(
+				ARGBType.rgba(
+						ARGBType.red( colorIndex ),
+						ARGBType.green( colorIndex ),
+						ARGBType.blue( colorIndex ),
+						alpha * 255 )
+		);
+	}
+
+	public void setSelectionColoringMode( SelectionColoringMode selectionColoringMode )
+	{
+		this.selectionColoringMode = selectionColoringMode;
+		notifyColoringListeners();
+	}
+
+	public void setSelectionColoringMode( SelectionColoringMode selectionColoringMode, double brightnessNotSelected )
+	{
+		this.selectionColoringMode = selectionColoringMode;
+
+		// ensure value between 0 and 1
+		brightnessNotSelected = Math.min( 1.0, brightnessNotSelected );
+		brightnessNotSelected = Math.max( 0.0, brightnessNotSelected );
+		this.opacityNotSelected = brightnessNotSelected;
+
+		notifyColoringListeners();
+	}
+
+	public SelectionColoringMode getSelectionColoringMode()
+	{
+		return selectionColoringMode;
+	}
+
+	public void setSelectionColor( ARGBType selectionColor )
+	{
+		this.selectionColor = selectionColor;
+		notifyColoringListeners();
+	}
+
+	public void setColoringModel( ColoringModel< T > coloringModel )
+	{
+		this.coloringModel = coloringModel;
+		notifyColoringListeners();
+
+		// chain event notification
+		coloringModel.listeners().add( () -> notifyColoringListeners() );
+	}
+
+	public ColoringModel< T > getWrappedColoringModel()
+	{
+		return coloringModel;
+	}
+
+	public SelectionModel< T > getSelectionModel()
+	{
+		return selectionModel;
+	}
+
+	public String getARGBLutName()
+	{
+		if ( coloringModel instanceof ARBGLutSupplier )
+		{
+			return ( ( ARBGLutSupplier ) coloringModel ).getARGBLut().getName();
+		}
+		else
+		{
+			return null;
+		}
+	}
+
+	public double getOpacityNotSelected()
+	{
+		return opacityNotSelected;
+	}
+
+	public void setOpacityNotSelected( double opacityNotSelected )
+	{
+		this.opacityNotSelected = opacityNotSelected;
+	}
+
+	public void incrementRandomColorSeed()
+	{
+		if ( coloringModel instanceof CategoryColoringModel )
+		{
+			( ( CategoryColoringModel<?> ) coloringModel ).incRandomSeed();
+		}
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/color/OpacityAdjuster.java b/src/main/java/de/embl/cba/mobie/color/OpacityAdjuster.java
new file mode 100644
index 000000000..ff734a0ce
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/color/OpacityAdjuster.java
@@ -0,0 +1,7 @@
+package de.embl.cba.mobie.color;
+
+public interface OpacityAdjuster
+{
+	void setOpacity( double opacity );
+	double getOpacity();
+}
diff --git a/src/main/java/de/embl/cba/mobie/color/RandomColorSeedChangerCommand.java b/src/main/java/de/embl/cba/mobie/color/RandomColorSeedChangerCommand.java
new file mode 100644
index 000000000..22aed7488
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/color/RandomColorSeedChangerCommand.java
@@ -0,0 +1,37 @@
+package de.embl.cba.mobie.color;
+
+import bdv.viewer.SourceAndConverter;
+import org.scijava.plugin.Parameter;
+import org.scijava.plugin.Plugin;
+import sc.fiji.bdvpg.scijava.ScijavaBdvDefaults;
+import sc.fiji.bdvpg.scijava.command.BdvPlaygroundActionCommand;
+
+@Plugin(type = BdvPlaygroundActionCommand.class, menuPath = ScijavaBdvDefaults.RootMenu+"Sources>Display>Change random coloring seed [ Ctrl L ]")
+public class RandomColorSeedChangerCommand implements BdvPlaygroundActionCommand
+{
+	// TODO: Maybe implement a setter (the Categorical coloring models have the method for this already)
+	//@Parameter( label = "Random color seed" )
+	//int seed = 42;
+
+	@Parameter
+	SourceAndConverter[] sourceAndConverters;
+
+	@Override
+	public void run()
+	{
+		incrementRandomColorSeed( sourceAndConverters );
+	}
+
+	public static void incrementRandomColorSeed( SourceAndConverter[] sourceAndConverters )
+	{
+		for ( SourceAndConverter sourceAndConverter : sourceAndConverters )
+		{
+			if ( sourceAndConverter.getConverter() instanceof LabelConverter< ? > )
+			{
+				final LabelConverter< ? > converter = ( LabelConverter< ? > ) sourceAndConverter.getConverter();
+
+				converter.getColoringModel().incrementRandomColorSeed();
+			}
+		}
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/color/VolatileRealARGBColorConverter.java b/src/main/java/de/embl/cba/mobie/color/VolatileRealARGBColorConverter.java
new file mode 100644
index 000000000..fa9fce1d2
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/color/VolatileRealARGBColorConverter.java
@@ -0,0 +1,11 @@
+package de.embl.cba.mobie.color;
+
+import net.imglib2.Volatile;
+import net.imglib2.converter.Converter;
+import net.imglib2.display.ColorConverter;
+import net.imglib2.type.numeric.ARGBType;
+import net.imglib2.type.numeric.RealType;
+
+public interface VolatileRealARGBColorConverter extends ColorConverter, Converter< Volatile< RealType >, ARGBType >
+{
+}
diff --git a/src/main/java/de/embl/cba/mobie/color/opacity/AdjustableOpacityColorConverter.java b/src/main/java/de/embl/cba/mobie/color/opacity/AdjustableOpacityColorConverter.java
new file mode 100644
index 000000000..0a657d3ef
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/color/opacity/AdjustableOpacityColorConverter.java
@@ -0,0 +1,107 @@
+/*-
+ * #%L
+ * TODO
+ * %%
+ * Copyright (C) 2018 - 2020 EMBL
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+package de.embl.cba.mobie.color.opacity;
+
+import de.embl.cba.mobie.color.OpacityAdjuster;
+import net.imglib2.converter.Converter;
+import net.imglib2.display.ColorConverter;
+import net.imglib2.type.numeric.ARGBType;
+import net.imglib2.type.numeric.RealType;
+
+public class AdjustableOpacityColorConverter implements OpacityAdjuster, ColorConverter, Converter< RealType, ARGBType >
+{
+	private final Converter< RealType, ARGBType > converter;
+	private double opacity = 1.0;
+
+	public AdjustableOpacityColorConverter( Converter< RealType, ARGBType > converter )
+	{
+		this.converter = converter;
+	}
+
+	@Override
+	public void convert( RealType realType, ARGBType output )
+	{
+		converter.convert( realType, output );
+		output.mul( opacity );
+	}
+
+	@Override
+	public void setOpacity( double opacity )
+	{
+		this.opacity = opacity;
+	}
+
+	@Override
+	public double getOpacity()
+	{
+		return opacity;
+	}
+
+	@Override
+	public ARGBType getColor()
+	{
+		return (( ColorConverter ) converter).getColor();
+	}
+
+	@Override
+	public void setColor( ARGBType c )
+	{
+		(( ColorConverter ) converter).setColor( c );
+	}
+
+	@Override
+	public boolean supportsColor()
+	{
+		return (( ColorConverter ) converter).supportsColor();
+	}
+
+	@Override
+	public double getMin()
+	{
+		return (( ColorConverter ) converter).getMin();
+	}
+
+	@Override
+	public double getMax()
+	{
+		return (( ColorConverter ) converter).getMax();
+	}
+
+	@Override
+	public void setMin( double min )
+	{
+		(( ColorConverter ) converter).setMin( min );
+	}
+
+	@Override
+	public void setMax( double max )
+	{
+		(( ColorConverter ) converter).setMax( max );
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/color/opacity/VolatileAdjustableOpacityColorConverter.java b/src/main/java/de/embl/cba/mobie/color/opacity/VolatileAdjustableOpacityColorConverter.java
new file mode 100644
index 000000000..1fd12b03d
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/color/opacity/VolatileAdjustableOpacityColorConverter.java
@@ -0,0 +1,110 @@
+/*-
+ * #%L
+ * TODO
+ * %%
+ * Copyright (C) 2018 - 2020 EMBL
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+package de.embl.cba.mobie.color.opacity;
+
+import de.embl.cba.mobie.color.OpacityAdjuster;
+import net.imglib2.Volatile;
+import net.imglib2.converter.Converter;
+import net.imglib2.display.ColorConverter;
+import net.imglib2.type.numeric.ARGBType;
+import net.imglib2.type.numeric.RealType;
+
+public class VolatileAdjustableOpacityColorConverter< V extends Volatile< RealType > > implements ColorConverter, Converter< V, ARGBType >, OpacityAdjuster
+{
+	private final Converter< V, ARGBType > converter;
+	private final ColorConverter colorConverter;
+	private double opacity = 1.0;
+
+	public VolatileAdjustableOpacityColorConverter( Converter< V, ARGBType > converter )
+	{
+		this.converter = converter;
+		this.colorConverter = ( ColorConverter ) converter;
+	}
+
+	@Override
+	public void convert( V realTypeVolatile, ARGBType output )
+	{
+		converter.convert( realTypeVolatile, output );
+		output.mul( opacity );
+	}
+
+	@Override
+	public void setOpacity( double opacity )
+	{
+		this.opacity = opacity;
+	}
+
+	@Override
+	public double getOpacity()
+	{
+		return opacity;
+	}
+
+	@Override
+	public ARGBType getColor()
+	{
+		return colorConverter.getColor();
+	}
+
+	@Override
+	public void setColor( ARGBType c )
+	{
+		colorConverter.setColor( c );
+	}
+
+	@Override
+	public boolean supportsColor()
+	{
+		return colorConverter.supportsColor();
+	}
+
+	@Override
+	public double getMin()
+	{
+		return colorConverter.getMin();
+	}
+
+	@Override
+	public double getMax()
+	{
+		return colorConverter.getMax();
+	}
+
+	@Override
+	public void setMin( double min )
+	{
+		colorConverter.setMin( min );
+	}
+
+	@Override
+	public void setMax( double max )
+	{
+		colorConverter.setMax( max );
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/ui/command/AddRemoteToMoBIEProjectCommand.java b/src/main/java/de/embl/cba/mobie/command/AddRemoteToMoBIEProjectCommand.java
similarity index 59%
rename from src/main/java/de/embl/cba/mobie/ui/command/AddRemoteToMoBIEProjectCommand.java
rename to src/main/java/de/embl/cba/mobie/command/AddRemoteToMoBIEProjectCommand.java
index 1a5394c80..8f2d01041 100644
--- a/src/main/java/de/embl/cba/mobie/ui/command/AddRemoteToMoBIEProjectCommand.java
+++ b/src/main/java/de/embl/cba/mobie/command/AddRemoteToMoBIEProjectCommand.java
@@ -1,8 +1,8 @@
-package de.embl.cba.mobie.ui.command;
+package de.embl.cba.mobie.command;
 
-import de.embl.cba.mobie.projects.projectsCreator.ProjectsCreator;
-import de.embl.cba.mobie.projects.projectsCreator.RemoteMetadataCreator;
-import de.embl.cba.mobie.utils.Utils;
+import de.embl.cba.mobie.projectcreator.ProjectCreator;
+import de.embl.cba.mobie.projectcreator.RemoteMetadataCreator;
+import ij.IJ;
 import net.imagej.ImageJ;
 import org.scijava.command.Command;
 import org.scijava.plugin.Parameter;
@@ -10,8 +10,10 @@
 
 import javax.swing.*;
 import java.io.File;
+import java.io.IOException;
 
-import static de.embl.cba.mobie.utils.ui.SwingUtils.resetSwingLookAndFeel;
+import static de.embl.cba.mobie.projectcreator.ProjectCreatorHelper.getDataLocation;
+import static de.embl.cba.mobie.ui.UserInterfaceHelper.setMoBIESwingLookAndFeel;
 
 @Plugin(type = Command.class, menuPath = "Plugins>MoBIE>Create>Add/Update MoBIE Project remote..." )
 public class AddRemoteToMoBIEProjectCommand implements Command {
@@ -45,21 +47,19 @@ public void run()
     {
 
         // using File script parameter changes the look and feel of swing, reset it to default here
-        resetSwingLookAndFeel();
-
-        File dataLocation = new File( projectLocation, "data" );
+        setMoBIESwingLookAndFeel();
 
         if ( !projectLocation.exists() ) {
-            Utils.log( "Add Remote failed - MoBIE project does not exist!" );
-        } else if ( !dataLocation.exists() ) {
-            Utils.log( "Add remote failed - this folder does not contain a valid MoBIE project structure. \n " +
-                    "Please choose a MoBIE project folder (this contains a 'data' folder at the top level)" );
+            IJ.log( "Add Remote failed - MoBIE project does not exist!" );
         } else {
-
             if ( continueDialog() ) {
-                ProjectsCreator projectsCreator = new ProjectsCreator(projectLocation);
-                RemoteMetadataCreator remoteMetadataCreator = projectsCreator.getRemoteMetadataCreator();
-                remoteMetadataCreator.createRemoteMetadata( signingRegion, serviceEndpoint, bucketName );
+                try {
+                    ProjectCreator projectsCreator = new ProjectCreator( getDataLocation( projectLocation ) );
+                    RemoteMetadataCreator remoteMetadataCreator = projectsCreator.getRemoteMetadataCreator();
+                    remoteMetadataCreator.createRemoteMetadata( signingRegion, serviceEndpoint, bucketName );
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
             }
         }
     }
diff --git a/src/main/java/de/embl/cba/mobie/ui/command/CreateNewMoBIEProjectCommand.java b/src/main/java/de/embl/cba/mobie/command/CreateNewMoBIEProjectCommand.java
similarity index 63%
rename from src/main/java/de/embl/cba/mobie/ui/command/CreateNewMoBIEProjectCommand.java
rename to src/main/java/de/embl/cba/mobie/command/CreateNewMoBIEProjectCommand.java
index 17f45c871..da49ca427 100644
--- a/src/main/java/de/embl/cba/mobie/ui/command/CreateNewMoBIEProjectCommand.java
+++ b/src/main/java/de/embl/cba/mobie/command/CreateNewMoBIEProjectCommand.java
@@ -1,18 +1,17 @@
-package de.embl.cba.mobie.ui.command;
+package de.embl.cba.mobie.command;
 
-import de.embl.cba.mobie.projects.projectsCreator.ProjectsCreator;
-import de.embl.cba.mobie.projects.projectsCreator.ui.ProjectsCreatorPanel;
-import de.embl.cba.mobie.utils.Utils;
-import ij.gui.GenericDialog;
+import de.embl.cba.mobie.projectcreator.ui.ProjectsCreatorPanel;
+import ij.IJ;
 import net.imagej.ImageJ;
 import org.scijava.command.Command;
 import org.scijava.plugin.Parameter;
 import org.scijava.plugin.Plugin;
 
 import java.io.File;
+import java.io.IOException;
 
-import static de.embl.cba.mobie.utils.Utils.tidyString;
-import static de.embl.cba.mobie.utils.ui.SwingUtils.resetSwingLookAndFeel;
+import static de.embl.cba.mobie.ui.UserInterfaceHelper.setMoBIESwingLookAndFeel;
+import static de.embl.cba.mobie.ui.UserInterfaceHelper.tidyString;
 
 @Plugin(type = Command.class, menuPath = "Plugins>MoBIE>Create>Create new MoBIE Project..." )
 public class CreateNewMoBIEProjectCommand implements Command {
@@ -32,16 +31,20 @@ public void run()
             File projectLocation = new File(folderLocation, tidyProjectName);
 
             if ( projectLocation.exists() ) {
-                Utils.log("Project creation failed - this project already exists!");
+                IJ.log("Project creation failed - this project already exists!");
             } else {
                 File dataDirectory = new File(projectLocation, "data");
                 dataDirectory.mkdirs();
 
                 // using File script parameter changes the look and feel of swing, reset it to default here
-                resetSwingLookAndFeel();
-
-                ProjectsCreatorPanel panel = new ProjectsCreatorPanel(projectLocation);
-                panel.showProjectsCreatorPanel();
+                setMoBIESwingLookAndFeel();
+
+                try {
+                    ProjectsCreatorPanel panel = new ProjectsCreatorPanel( projectLocation );
+                    panel.showProjectsCreatorPanel();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
             }
         }
 
diff --git a/src/main/java/de/embl/cba/mobie/ui/command/EditMoBIEProjectCommand.java b/src/main/java/de/embl/cba/mobie/command/EditMoBIEProjectCommand.java
similarity index 52%
rename from src/main/java/de/embl/cba/mobie/ui/command/EditMoBIEProjectCommand.java
rename to src/main/java/de/embl/cba/mobie/command/EditMoBIEProjectCommand.java
index e8af9927f..d128e2b66 100644
--- a/src/main/java/de/embl/cba/mobie/ui/command/EditMoBIEProjectCommand.java
+++ b/src/main/java/de/embl/cba/mobie/command/EditMoBIEProjectCommand.java
@@ -1,15 +1,16 @@
-package de.embl.cba.mobie.ui.command;
+package de.embl.cba.mobie.command;
 
-import de.embl.cba.mobie.projects.projectsCreator.ui.ProjectsCreatorPanel;
-import de.embl.cba.mobie.utils.Utils;
+import de.embl.cba.mobie.projectcreator.ui.ProjectsCreatorPanel;
+import ij.IJ;
 import net.imagej.ImageJ;
 import org.scijava.command.Command;
 import org.scijava.plugin.Parameter;
 import org.scijava.plugin.Plugin;
 
 import java.io.File;
+import java.io.IOException;
 
-import static de.embl.cba.mobie.utils.ui.SwingUtils.resetSwingLookAndFeel;
+import static de.embl.cba.mobie.ui.UserInterfaceHelper.setMoBIESwingLookAndFeel;
 import static org.scijava.ItemVisibility.MESSAGE;
 
 @Plugin(type = Command.class, menuPath = "Plugins>MoBIE>Create>Edit MoBIE Project..." )
@@ -25,18 +26,17 @@ public class EditMoBIEProjectCommand implements Command
     public void run()
     {
         // using File script parameter changes the look and feel of swing, reset it to default here
-        resetSwingLookAndFeel();
-
-        File dataLocation = new File( projectLocation, "data" );
+        setMoBIESwingLookAndFeel();
 
         if ( !projectLocation.exists() ) {
-            Utils.log( "Edit project failed - MoBIE project does not exist!" );
-        } else if ( !dataLocation.exists() ) {
-            Utils.log( "Edit project failed - this folder does not contain a valid MoBIE project structure. \n " +
-                    "Please choose a MoBIE project folder (this contains a 'data' folder at the top level)" );
+            IJ.log( "Edit project failed - MoBIE project does not exist!" );
         } else {
-            ProjectsCreatorPanel panel = new ProjectsCreatorPanel(projectLocation);
-            panel.showProjectsCreatorPanel();
+            try {
+                ProjectsCreatorPanel panel = new ProjectsCreatorPanel( projectLocation );
+                panel.showProjectsCreatorPanel();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
         }
     }
 
diff --git a/src/main/java/de/embl/cba/mobie/ui/command/OpenMoBIEProjectAdvancedCommand.java b/src/main/java/de/embl/cba/mobie/command/OpenMoBIEProjectAdvancedCommand.java
similarity index 56%
rename from src/main/java/de/embl/cba/mobie/ui/command/OpenMoBIEProjectAdvancedCommand.java
rename to src/main/java/de/embl/cba/mobie/command/OpenMoBIEProjectAdvancedCommand.java
index a0abef4e0..5f63b28d0 100644
--- a/src/main/java/de/embl/cba/mobie/ui/command/OpenMoBIEProjectAdvancedCommand.java
+++ b/src/main/java/de/embl/cba/mobie/command/OpenMoBIEProjectAdvancedCommand.java
@@ -1,12 +1,14 @@
-package de.embl.cba.mobie.ui.command;
+package de.embl.cba.mobie.command;
 
-import de.embl.cba.mobie.ui.MoBIEOptions;
-import de.embl.cba.mobie.ui.MoBIE;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
 import net.imagej.ImageJ;
 import org.scijava.command.Command;
 import org.scijava.plugin.Parameter;
 import org.scijava.plugin.Plugin;
 
+import java.io.IOException;
+
 @Plugin(type = Command.class, menuPath = "Plugins>MoBIE>Open>Advanced>Open MoBIE Project Expert Mode..." )
 public class OpenMoBIEProjectAdvancedCommand implements Command
 {
@@ -28,18 +30,24 @@ public class OpenMoBIEProjectAdvancedCommand implements Command
 	@Parameter ( label = "Table Data Branch" )
 	public String tableDataBranch = "master";
 
-
 	@Override
 	public void run()
 	{
-		new MoBIE(
-				projectLocation,
-				MoBIEOptions.options()
-						.gitProjectBranch( projectBranch )
-						.imageDataStorageModality( MoBIEOptions.ImageDataStorageModality.valueOf( imageDataStorageModality ) )
-						.imageDataLocation( imageDataLocation )
-						.tableDataLocation( tableDataLocation )
-						.gitTablesBranch( tableDataBranch ) );
+		try
+		{
+			new MoBIE(
+					projectLocation,
+					MoBIESettings.settings()
+							.gitProjectBranch( projectBranch )
+							.imageDataStorageModality( MoBIESettings.ImageDataStorageModality.valueOf( imageDataStorageModality ) )
+							.imageDataLocation( imageDataLocation )
+							.tableDataLocation( tableDataLocation )
+							.gitTablesBranch( tableDataBranch ) );
+		}
+		catch ( IOException e )
+		{
+			e.printStackTrace();
+		}
 	}
 
 	public static void main(final String... args)
@@ -57,14 +65,18 @@ public static void main(final String... args)
 		 * Table data branch (for github):"bookmarks"
 		 */
 
-//		new MoBIEViewer(
-//				"https://github.com/mobie/platybrowser-datasets",
-//				MoBIEOptions.options()
-//						.gitProjectBranch( "master" )
-//						.imageDataStorageModality( MoBIEOptions.ImageDataStorageModality.FileSystem )
-//						.imageDataLocation( "/g/arendt/EM_6dpf_segmentation/platy-browser-data" )
-//						.tableDataLocation( "https://github.com/vzinche/platybrowser-backend" )
-//						.gitTablesBranch( "bookmarks" ) );
+		// try {
+		// 	new MoBIE(
+		// 			"https://github.com/mobie/platybrowser-datasets",
+		// 			MoBIESettings.settings()
+		// 					.gitProjectBranch( "master" )
+		// 					.imageDataStorageModality( MoBIESettings.ImageDataStorageModality.FileSystem )
+		// 					.imageDataLocation( "/g/arendt/EM_6dpf_segmentation/platy-browser-data" )
+		// 					.tableDataLocation( "https://github.com/vzinche/platybrowser-backend" )
+		// 					.gitTablesBranch( "bookmarks" ) );
+		// } catch (IOException e) {
+		// 	e.printStackTrace();
+		// }
 
 		/**
 		 * Project location (github or local): "https://github.com/mobie/platybrowser-datasets"
@@ -75,15 +87,18 @@ public static void main(final String... args)
 		 * Table data branch (for github): "xray"
 		 */
 
-		new MoBIE(
-				"https://github.com/mobie/platybrowser-datasets",
-				MoBIEOptions.options()
-						.gitProjectBranch( "xray" )
-						.imageDataStorageModality( MoBIEOptions.ImageDataStorageModality.FileSystem )
-						.imageDataLocation( "/g/arendt/EM_6dpf_segmentation/platy-browser-data" )
-						.tableDataLocation( "https://github.com/mobie/platy-browser-data" )
-						.gitTablesBranch( "xray" ) );
-
+		try {
+			new MoBIE(
+					"https://github.com/mobie/platybrowser-datasets",
+					MoBIESettings.settings()
+							.gitProjectBranch( "xray" )
+							.imageDataStorageModality( MoBIESettings.ImageDataStorageModality.FileSystem )
+							.imageDataLocation( "/g/arendt/EM_6dpf_segmentation/platy-browser-data" )
+							.tableDataLocation( "https://github.com/mobie/platy-browser-data" )
+							.gitTablesBranch( "xray" ) );
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
 
 
 	}
diff --git a/src/main/java/de/embl/cba/mobie/ui/command/OpenMoBIEProjectBranchCommand.java b/src/main/java/de/embl/cba/mobie/command/OpenMoBIEProjectBranchCommand.java
similarity index 50%
rename from src/main/java/de/embl/cba/mobie/ui/command/OpenMoBIEProjectBranchCommand.java
rename to src/main/java/de/embl/cba/mobie/command/OpenMoBIEProjectBranchCommand.java
index a6410d3aa..4addb0e00 100644
--- a/src/main/java/de/embl/cba/mobie/ui/command/OpenMoBIEProjectBranchCommand.java
+++ b/src/main/java/de/embl/cba/mobie/command/OpenMoBIEProjectBranchCommand.java
@@ -1,12 +1,13 @@
-package de.embl.cba.mobie.ui.command;
+package de.embl.cba.mobie.command;
 
-import de.embl.cba.mobie.ui.MoBIEOptions;
-import de.embl.cba.mobie.ui.MoBIE;
-import net.imagej.ImageJ;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
 import org.scijava.command.Command;
 import org.scijava.plugin.Parameter;
 import org.scijava.plugin.Plugin;
 
+import java.io.IOException;
+
 @Plugin(type = Command.class, menuPath = "Plugins>MoBIE>Open>Advanced>Open MoBIE Project Branch..." )
 public class OpenMoBIEProjectBranchCommand implements Command
 {
@@ -19,18 +20,13 @@ public class OpenMoBIEProjectBranchCommand implements Command
 	@Override
 	public void run()
 	{
-		final MoBIE moBIE = new MoBIE(
-				projectLocation,
-				MoBIEOptions.options().gitProjectBranch( projectBranch ) );
-	}
-
-	public static void main(final String... args)
-	{
-		final ImageJ ij = new ImageJ();
-		ij.ui().showUI();
-
-		final MoBIE moBIE = new MoBIE(
-				"https://github.com/mobie/covid-tomo-datasets",
-				MoBIEOptions.options().gitProjectBranch( "norm-bookmarks" ) );
+		try
+		{
+			new MoBIE( projectLocation, MoBIESettings.settings().gitProjectBranch( projectBranch ) );
+		}
+		catch ( IOException e )
+		{
+			e.printStackTrace();
+		}
 	}
 }
diff --git a/src/main/java/de/embl/cba/mobie/command/OpenMoBIEProjectCommand.java b/src/main/java/de/embl/cba/mobie/command/OpenMoBIEProjectCommand.java
new file mode 100644
index 000000000..a8b2147db
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/command/OpenMoBIEProjectCommand.java
@@ -0,0 +1,38 @@
+package de.embl.cba.mobie.command;
+
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
+import org.scijava.command.Command;
+import org.scijava.plugin.Parameter;
+import org.scijava.plugin.Plugin;
+
+import java.io.IOException;
+
+
+@Plugin(type = Command.class, menuPath = "Plugins>MoBIE>Open>Open MoBIE Project..." )
+public class OpenMoBIEProjectCommand implements Command
+{
+	@Parameter ( label = "Project Location" )
+	public String projectLocation = "https://github.com/mobie/platybrowser-datasets";
+
+	@Override
+	public void run()
+	{
+		MoBIESettings options = MoBIESettings.settings();
+
+		if ( projectLocation.startsWith( "http" ) ) {
+			options = options.imageDataStorageModality(MoBIESettings.ImageDataStorageModality.S3);
+		} else {
+			options = options.imageDataStorageModality(MoBIESettings.ImageDataStorageModality.FileSystem);
+		}
+
+		try
+		{
+			new MoBIE( projectLocation, options );
+		}
+		catch ( IOException e )
+		{
+			e.printStackTrace();
+		}
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/ui/command/OpenMoBIEPublishedProjectCommand.java b/src/main/java/de/embl/cba/mobie/command/OpenMoBIEPublishedProjectCommand.java
similarity index 63%
rename from src/main/java/de/embl/cba/mobie/ui/command/OpenMoBIEPublishedProjectCommand.java
rename to src/main/java/de/embl/cba/mobie/command/OpenMoBIEPublishedProjectCommand.java
index 17e70b7fa..2a7cb6dff 100644
--- a/src/main/java/de/embl/cba/mobie/ui/command/OpenMoBIEPublishedProjectCommand.java
+++ b/src/main/java/de/embl/cba/mobie/command/OpenMoBIEPublishedProjectCommand.java
@@ -1,23 +1,21 @@
-package de.embl.cba.mobie.ui.command;
+package de.embl.cba.mobie.command;
 
-import de.embl.cba.mobie.projects.PublishedProject;
-import de.embl.cba.mobie.projects.PublishedProjectsCreator;
-import de.embl.cba.mobie.ui.MoBIEOptions;
-import de.embl.cba.mobie.ui.MoBIE;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
+import de.embl.cba.mobie.project.PublishedProject;
+import de.embl.cba.mobie.project.PublishedProjectsCreator;
 import ij.gui.GenericDialog;
 import net.imagej.ImageJ;
 import org.scijava.command.Command;
 import org.scijava.plugin.Plugin;
 
+import java.io.IOException;
 import java.util.HashMap;
 
 
 @Plugin(type = Command.class, menuPath = "Plugins>MoBIE>Open>Open Published MoBIE Project..." )
 public class OpenMoBIEPublishedProjectCommand implements Command
 {
-	//@Parameter ( label = "Select Project", callback = "selectProject" )
-	//public Button selectProjectButton;
-
 	@Override
 	public void run()
 	{
@@ -26,7 +24,7 @@ public void run()
 
 	private void selectProject()
 	{
-		final HashMap< String, PublishedProject > projects = new PublishedProjectsCreator().getPublishedProjects();
+		final HashMap< String, PublishedProject> projects = new PublishedProjectsCreator().getPublishedProjects();
 
 		final GenericDialog gd = new GenericDialog( "Please select a project" );
 
@@ -38,7 +36,14 @@ private void selectProject()
 
 		final PublishedProject project = projects.get( choice );
 
-		final MoBIE moBIE = new MoBIE( project.location, MoBIEOptions.options().pulicationURL( project.pulicationURL ) );
+		try
+		{
+			new MoBIE( project.location, MoBIESettings.settings().publicationURL( project.pulicationURL ) );
+		}
+		catch ( IOException e )
+		{
+			e.printStackTrace();
+		}
 	}
 
 	public static void main( String[] args )
diff --git a/src/main/java/de/embl/cba/mobie/command/OpenPlatyBrowserCommand.java b/src/main/java/de/embl/cba/mobie/command/OpenPlatyBrowserCommand.java
new file mode 100644
index 000000000..bb12d187e
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/command/OpenPlatyBrowserCommand.java
@@ -0,0 +1,28 @@
+package de.embl.cba.mobie.command;
+
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
+import org.scijava.command.Command;
+import org.scijava.plugin.Plugin;
+
+import java.io.IOException;
+
+
+@Plugin(type = Command.class, menuPath = "Plugins>MoBIE>Open>Open PlatyBrowser")
+public class OpenPlatyBrowserCommand implements Command
+{
+	@Override
+	public void run()
+	{
+		MoBIESettings options = MoBIESettings.settings().imageDataStorageModality(MoBIESettings.ImageDataStorageModality.S3);
+
+		try
+		{
+			new MoBIE( "https://github.com/mobie/platybrowser-datasets", options );
+		}
+		catch ( IOException e )
+		{
+			e.printStackTrace();
+		}
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/dataset/Datasets.java b/src/main/java/de/embl/cba/mobie/dataset/Datasets.java
deleted file mode 100644
index ec116da6d..000000000
--- a/src/main/java/de/embl/cba/mobie/dataset/Datasets.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package de.embl.cba.mobie.dataset;
-
-import java.util.ArrayList;
-
-public class Datasets
-{
-	public ArrayList< String  > datasets;
-	public String defaultDataset;
-}
diff --git a/src/main/java/de/embl/cba/mobie/dataset/DatasetsParser.java b/src/main/java/de/embl/cba/mobie/dataset/DatasetsParser.java
deleted file mode 100644
index a8b9a770a..000000000
--- a/src/main/java/de/embl/cba/mobie/dataset/DatasetsParser.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package de.embl.cba.mobie.dataset;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.reflect.TypeToken;
-import com.google.gson.stream.JsonReader;
-import com.google.gson.stream.JsonWriter;
-import de.embl.cba.mobie.bookmark.Bookmark;
-import de.embl.cba.mobie.utils.JsonUtils;
-import de.embl.cba.tables.FileAndUrlUtils;
-
-import java.io.*;
-import java.lang.reflect.Type;
-import java.util.HashMap;
-import java.util.Map;
-
-public class DatasetsParser
-{
-	public Datasets fetchProjectDatasets( String dataSourceLocation )
-	{
-		try
-		{
-			return datasetsFromFile( dataSourceLocation + "/datasets.json" );
-		}
-		catch ( Exception e )
-		{
-			try
-			{
-				return datasetsFromFile( dataSourceLocation + "/versions.json" );
-			}
-			catch ( Exception e2 )
-			{
-				e.printStackTrace();
-				throw new UnsupportedOperationException( "Could not open or parse datasets file: " + dataSourceLocation + "/datasets.json"  );
-			}
-		}
-	}
-
-	private Datasets datasetsFromFile( String path ) throws IOException
-	{
-		try (InputStream is = FileAndUrlUtils.getInputStream(path);
-			 final JsonReader reader = new JsonReader(new InputStreamReader(is, "UTF-8")) ) {
-
-			try {
-				GsonBuilder builder = new GsonBuilder();
-				Datasets datasets = builder.create().fromJson(reader, Datasets.class);
-				return datasets;
-			} catch ( Exception e ) {
-				// old version
-				final Datasets datasets = new Datasets();
-				datasets.datasets = JsonUtils.readStringArray( reader );
-				datasets.defaultDataset = datasets.datasets.get(0);
-				return datasets;
-			}
-		}
-	}
-
-	public void datasetsToFile( String path, Datasets datasets ) throws IOException
-	{
-		try ( OutputStream outputStream = new FileOutputStream( path );
-			  final JsonWriter writer = new JsonWriter( new OutputStreamWriter(outputStream, "UTF-8")) ) {
-			Gson gson = new GsonBuilder().create();
-			writer.setIndent("	");
-			gson.toJson(datasets, Datasets.class, writer);
-		}
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/display/ImageSourceDisplay.java b/src/main/java/de/embl/cba/mobie/display/ImageSourceDisplay.java
new file mode 100644
index 000000000..2d1421e5b
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/display/ImageSourceDisplay.java
@@ -0,0 +1,92 @@
+package de.embl.cba.mobie.display;
+
+import bdv.tools.brightness.ConverterSetup;
+import bdv.viewer.SourceAndConverter;
+import de.embl.cba.mobie.bdv.ImageSliceView;
+import de.embl.cba.mobie.color.opacity.AdjustableOpacityColorConverter;
+import net.imglib2.display.ColorConverter;
+import sc.fiji.bdvpg.bdv.projector.BlendingMode;
+import sc.fiji.bdvpg.services.SourceAndConverterServices;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static sc.fiji.bdvpg.bdv.projector.BlendingMode.BLENDING_MODE;
+
+public class ImageSourceDisplay extends SourceDisplay
+{
+	// Serialization
+	private String color;
+	private double[] contrastLimits;
+	private BlendingMode blendingMode;
+	private boolean showImagesIn3d;
+
+	// Runtime
+	public transient ImageSliceView imageSliceView;
+
+	public String getColor()
+	{
+		return color;
+	}
+
+	public double[] getContrastLimits()
+	{
+		return contrastLimits;
+	}
+
+	public BlendingMode getBlendingMode()
+	{
+		return blendingMode;
+	}
+
+	public ImageSourceDisplay( String name, double opacity, List< String > sources, String color,
+							   double[] contrastLimits, BlendingMode blendingMode, boolean showImagesIn3d ) {
+		this.name = name;
+		this.opacity = opacity;
+		this.sources = sources;
+		this.color = color;
+		this.contrastLimits = contrastLimits;
+		this.blendingMode = blendingMode;
+		this.showImagesIn3d = showImagesIn3d;
+	}
+
+	/**
+	 * Create a serializable copy
+	 *
+	 * @param imageDisplay
+	 */
+	public ImageSourceDisplay( ImageSourceDisplay imageDisplay )
+	{
+		this.name = imageDisplay.name;
+		this.sources = new ArrayList<>();
+		for ( SourceAndConverter< ? > sourceAndConverter : imageDisplay.sourceAndConverters )
+		{
+			sources.add( sourceAndConverter.getSpimSource().getName() );
+		}
+
+		final SourceAndConverter< ? > sourceAndConverter = imageDisplay.sourceAndConverters.get( 0 );
+		final ConverterSetup converterSetup = SourceAndConverterServices.getSourceAndConverterDisplayService().getConverterSetup( sourceAndConverter );
+
+		if( sourceAndConverter.getConverter() instanceof AdjustableOpacityColorConverter )
+		{
+			this.opacity = ( ( AdjustableOpacityColorConverter ) sourceAndConverter.getConverter() ).getOpacity();
+		}
+
+		if ( sourceAndConverter.getConverter() instanceof ColorConverter)
+		{
+			// needs to be of form r=(\\d+),g=(\\d+),b=(\\d+),a=(\\d+)"
+			String colorString = ( ( ColorConverter ) sourceAndConverter.getConverter() ).getColor().toString();
+			colorString.replaceAll("[()]", "");
+			this.color = colorString;
+		}
+
+		double[] contrastLimits = new double[2];
+		contrastLimits[0] = converterSetup.getDisplayRangeMin();
+		contrastLimits[1] = converterSetup.getDisplayRangeMax();
+		this.contrastLimits = contrastLimits;
+
+		this.blendingMode = (BlendingMode) SourceAndConverterServices.getSourceAndConverterService().getMetadata( sourceAndConverter, BLENDING_MODE );
+
+		// TODO - show images in 3d (currently not supported in viewer)
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/display/SegmentationSourceDisplay.java b/src/main/java/de/embl/cba/mobie/display/SegmentationSourceDisplay.java
new file mode 100644
index 000000000..66d21af70
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/display/SegmentationSourceDisplay.java
@@ -0,0 +1,155 @@
+package de.embl.cba.mobie.display;
+
+import bdv.viewer.SourceAndConverter;
+import de.embl.cba.mobie.Constants;
+import de.embl.cba.mobie.color.MoBIEColoringModel;
+import de.embl.cba.mobie.color.opacity.AdjustableOpacityColorConverter;
+import de.embl.cba.mobie.segment.SegmentAdapter;
+import de.embl.cba.mobie.plot.ScatterPlotViewer;
+import de.embl.cba.mobie.bdv.SegmentationImageSliceView;
+import de.embl.cba.mobie.volume.SegmentsVolumeView;
+import de.embl.cba.mobie.table.TableViewer;
+import de.embl.cba.tables.color.ColoringLuts;
+import de.embl.cba.tables.color.ColoringModel;
+import de.embl.cba.tables.color.ColumnColoringModel;
+import de.embl.cba.tables.color.NumericColoringModel;
+import de.embl.cba.tables.select.SelectionModel;
+import de.embl.cba.tables.tablerow.TableRowImageSegment;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class SegmentationSourceDisplay extends SourceDisplay
+{
+	// Serialization
+	private String lut = ColoringLuts.GLASBEY;
+	private String colorByColumn;
+	private Double[] valueLimits = new Double[]{ null, null };
+	private List< String > selectedSegmentIds;
+	private boolean showSelectedSegmentsIn3d = false;
+	private boolean showScatterPlot = false;
+	private String[] scatterPlotAxes = new String[]{ Constants.ANCHOR_X, Constants.ANCHOR_Y };
+	private List< String > tables;
+
+	// Runtime
+	public transient SelectionModel< TableRowImageSegment > selectionModel;
+	public transient MoBIEColoringModel< TableRowImageSegment > coloringModel;
+	public transient TableViewer< TableRowImageSegment > tableViewer;
+	public transient ScatterPlotViewer< TableRowImageSegment > scatterPlotViewer;
+	public transient List< TableRowImageSegment > segments;
+	public transient SegmentAdapter< TableRowImageSegment > segmentAdapter;
+	public transient SegmentsVolumeView< TableRowImageSegment > segmentsVolumeViewer;
+	public transient SegmentationImageSliceView< TableRowImageSegment > segmentationImageSliceView;
+
+	public String getLut()
+	{
+		return lut;
+	}
+
+	public String getColorByColumn()
+	{
+		return colorByColumn;
+	}
+
+	public Double[] getValueLimits()
+	{
+		return valueLimits;
+	}
+
+	public List< String > getSelectedSegmentIds()
+	{
+		return selectedSegmentIds;
+	}
+
+	public boolean showSelectedSegmentsIn3d()
+	{
+		return showSelectedSegmentsIn3d;
+	}
+
+	public boolean showScatterPlot()
+	{
+		return showScatterPlot;
+	}
+
+	public String[] getScatterPlotAxes()
+	{
+		return scatterPlotAxes;
+	}
+
+	public List< String > getTables()
+	{
+		return tables;
+	}
+
+	public SegmentationSourceDisplay( String name, double opacity, List< String > sources,
+									  String lut, String colorByColumn, Double[] valueLimits,
+									  List< String > selectedSegmentIds, boolean showSelectedSegmentsIn3d,
+									  boolean showScatterPlot, String[] scatterPlotAxes, List< String > tables )
+	{
+		this.name = name;
+		this.opacity = opacity;
+		this.sources = sources;
+		this.lut = lut;
+		this.colorByColumn = colorByColumn;
+		this.valueLimits = valueLimits;
+		this.selectedSegmentIds = selectedSegmentIds;
+		this.showSelectedSegmentsIn3d = showSelectedSegmentsIn3d;
+		this.showScatterPlot = showScatterPlot;
+		this.scatterPlotAxes = scatterPlotAxes;
+		this.tables = tables;
+	}
+
+	/**
+	 * Create a serializable copy
+	 *
+	 * @param segmentationDisplay
+	 */
+	public SegmentationSourceDisplay( SegmentationSourceDisplay segmentationDisplay )
+	{
+		this.name = segmentationDisplay.name;
+		this.sources = new ArrayList<>();
+		for ( SourceAndConverter< ? > sourceAndConverter : segmentationDisplay.sourceAndConverters )
+		{
+			sources.add( sourceAndConverter.getSpimSource().getName() );
+		}
+
+		final SourceAndConverter< ? > sourceAndConverter = segmentationDisplay.sourceAndConverters.get( 0 );
+
+		if( sourceAndConverter.getConverter() instanceof AdjustableOpacityColorConverter)
+		{
+			this.opacity = ( ( AdjustableOpacityColorConverter ) sourceAndConverter.getConverter() ).getOpacity();
+		}
+
+		this.lut = segmentationDisplay.coloringModel.getARGBLutName();
+
+		final ColoringModel< TableRowImageSegment > wrappedColoringModel = segmentationDisplay.coloringModel.getWrappedColoringModel();
+
+		if ( wrappedColoringModel instanceof ColumnColoringModel )
+		{
+			this.colorByColumn = (( ColumnColoringModel ) wrappedColoringModel).getColumnName();
+		}
+
+		if ( wrappedColoringModel instanceof NumericColoringModel )
+		{
+			this.valueLimits = new Double[2];
+			NumericColoringModel numericColoringModel = ( NumericColoringModel ) ( wrappedColoringModel );
+			valueLimits[0] = numericColoringModel.getMin();
+			valueLimits[1] = numericColoringModel.getMax();
+		}
+
+		Set<TableRowImageSegment> currentSelectedSegments = segmentationDisplay.selectionModel.getSelected();
+		if (currentSelectedSegments != null) {
+			ArrayList<String> selectedSegmentIds = new ArrayList<>();
+			for (TableRowImageSegment segment : currentSelectedSegments) {
+				selectedSegmentIds.add( String.valueOf( segment.labelId() ) );
+			}
+			this.selectedSegmentIds = selectedSegmentIds;
+		}
+
+		this.showSelectedSegmentsIn3d = segmentationDisplay.segmentsVolumeViewer.getShowSegments();
+		this.showScatterPlot = segmentationDisplay.scatterPlotViewer.isVisible();
+		this.scatterPlotAxes = segmentationDisplay.scatterPlotViewer.getSelectedColumns();
+		this.tables = segmentationDisplay.tableViewer.getAdditionalTables();
+	}
+
+}
diff --git a/src/main/java/de/embl/cba/mobie/display/SourceDisplay.java b/src/main/java/de/embl/cba/mobie/display/SourceDisplay.java
new file mode 100644
index 000000000..081db416d
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/display/SourceDisplay.java
@@ -0,0 +1,50 @@
+package de.embl.cba.mobie.display;
+
+import bdv.viewer.SourceAndConverter;
+import de.embl.cba.mobie.transform.SourceTransformer;
+import de.embl.cba.mobie.bdv.SliceViewer;
+
+import java.util.Collections;
+import java.util.List;
+
+public class SourceDisplay
+{
+	// Serialization
+	protected String name;
+	protected double opacity = 1.0;
+	protected List< String > sources;
+
+	// Runtime
+	public transient List< SourceAndConverter< ? > > sourceAndConverters;
+	public transient List< SourceTransformer > sourceTransformers;
+	public transient SliceViewer sliceViewer;
+
+	public String getName()
+	{
+		return name;
+	}
+
+	public List< String > getSources()
+	{
+		return Collections.unmodifiableList( sources );
+	}
+	public double getOpacity()
+	{
+		return opacity;
+	}
+
+	public List< SourceAndConverter< ? > > getSourceAndConverters()
+	{
+		return sourceAndConverters;
+	}
+
+	public List< SourceTransformer > getSourceTransformers()
+	{
+		return sourceTransformers;
+	}
+
+	public SliceViewer getSliceViewer()
+	{
+		return sliceViewer;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/grid/AnnotatedInterval.java b/src/main/java/de/embl/cba/mobie/grid/AnnotatedInterval.java
new file mode 100644
index 000000000..b89c9d3b2
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/grid/AnnotatedInterval.java
@@ -0,0 +1,9 @@
+package de.embl.cba.mobie.grid;
+
+import net.imglib2.RealInterval;
+
+public interface AnnotatedInterval
+{
+	RealInterval getInterval();
+	String getName();
+}
diff --git a/src/main/java/de/embl/cba/mobie/grid/AnnotatedIntervalCreator.java b/src/main/java/de/embl/cba/mobie/grid/AnnotatedIntervalCreator.java
new file mode 100644
index 000000000..ca986b5e4
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/grid/AnnotatedIntervalCreator.java
@@ -0,0 +1,45 @@
+package de.embl.cba.mobie.grid;
+
+import de.embl.cba.mobie.transform.GridSourceTransformer;
+import net.imglib2.FinalRealInterval;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class AnnotatedIntervalCreator
+{
+	private final Map< String, List< String > > columns;
+	private final GridSourceTransformer sourceTransformer;
+	private List< DefaultAnnotatedIntervalTableRow > tableRows;
+
+	public AnnotatedIntervalCreator( Map< String, List< String > > columns, GridSourceTransformer sourceTransformer )
+	{
+		this.columns = columns;
+		this.sourceTransformer = sourceTransformer;
+		create();
+	}
+
+	private void create()
+	{
+		tableRows = new ArrayList<>();
+		final int numRows = columns.values().iterator().next().size();
+		final List< FinalRealInterval > intervals = sourceTransformer.getIntervals();
+		for ( int row = 0; row < numRows; row++ )
+		{
+			intervals.get( row );
+			tableRows.add(
+					new DefaultAnnotatedIntervalTableRow(
+							"grid position " + row,
+							intervals.get( row ),
+							columns,
+							row )
+			);
+		}
+	}
+
+	public List< DefaultAnnotatedIntervalTableRow > getTableRows()
+	{
+		return tableRows;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/grid/AnnotatedIntervalTableRow.java b/src/main/java/de/embl/cba/mobie/grid/AnnotatedIntervalTableRow.java
new file mode 100644
index 000000000..a7b651a8e
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/grid/AnnotatedIntervalTableRow.java
@@ -0,0 +1,7 @@
+package de.embl.cba.mobie.grid;
+
+import de.embl.cba.tables.tablerow.TableRow;
+
+public interface AnnotatedIntervalTableRow extends AnnotatedInterval, TableRow
+{
+}
diff --git a/src/main/java/de/embl/cba/mobie/grid/DefaultAnnotatedIntervalTableRow.java b/src/main/java/de/embl/cba/mobie/grid/DefaultAnnotatedIntervalTableRow.java
new file mode 100644
index 000000000..0896253a7
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/grid/DefaultAnnotatedIntervalTableRow.java
@@ -0,0 +1,66 @@
+package de.embl.cba.mobie.grid;
+
+import de.embl.cba.tables.tablerow.AbstractTableRow;
+import net.imglib2.FinalRealInterval;
+import net.imglib2.RealInterval;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class DefaultAnnotatedIntervalTableRow extends AbstractTableRow implements AnnotatedIntervalTableRow
+{
+	protected final FinalRealInterval interval;
+	protected final Map< String, List< String > > columns;
+	protected final String siteName;
+	protected final int rowIndex;
+
+	public DefaultAnnotatedIntervalTableRow(
+			String siteName,
+			FinalRealInterval interval,
+			Map< String, List< String > > columns,
+			int rowIndex )
+	{
+		this.siteName = siteName;
+		this.interval = interval;
+		this.columns = columns;
+		this.rowIndex = rowIndex;
+	}
+
+	@Override
+	public RealInterval getInterval()
+	{
+		return interval;
+	}
+
+	@Override
+	public String getName()
+	{
+		return siteName;
+	}
+
+	@Override
+	public String getCell( String columnName )
+	{
+		return columns.get( columnName ).get( rowIndex );
+	}
+
+	@Override
+	public void setCell( String columnName, String value )
+	{
+		columns.get( columnName ).set( rowIndex, value );
+		notifyCellChangedListeners( columnName, value );
+	}
+
+	@Override
+	public Set< String > getColumnNames()
+	{
+		return columns.keySet();
+	}
+
+	@Override
+	public int rowIndex()
+	{
+		return rowIndex;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/grid/GridOverlaySourceDisplay.java b/src/main/java/de/embl/cba/mobie/grid/GridOverlaySourceDisplay.java
new file mode 100644
index 000000000..cb6be3c44
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/grid/GridOverlaySourceDisplay.java
@@ -0,0 +1,123 @@
+package de.embl.cba.mobie.grid;
+
+import bdv.util.BdvHandle;
+import bdv.viewer.SourceAndConverter;
+import de.embl.cba.bdv.utils.Logger;
+import de.embl.cba.mobie.transform.BdvLocation;
+import de.embl.cba.mobie.transform.BdvLocationType;
+import de.embl.cba.mobie.Utils;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.transform.BdvLocationChanger;
+import de.embl.cba.mobie.color.MoBIEColoringModel;
+import de.embl.cba.mobie.display.SourceDisplay;
+import de.embl.cba.mobie.transform.GridSourceTransformer;
+import de.embl.cba.mobie.table.TableViewer;
+import de.embl.cba.tables.TableColumns;
+import de.embl.cba.tables.color.ColoringListener;
+import de.embl.cba.tables.color.ColoringLuts;
+import de.embl.cba.tables.select.DefaultSelectionModel;
+import de.embl.cba.tables.select.SelectionListener;
+import net.imglib2.type.numeric.integer.IntType;
+import sc.fiji.bdvpg.services.SourceAndConverterServices;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class GridOverlaySourceDisplay extends SourceDisplay implements ColoringListener, SelectionListener< DefaultAnnotatedIntervalTableRow >
+{
+	private final MoBIEColoringModel< DefaultAnnotatedIntervalTableRow > coloringModel;
+	private final DefaultSelectionModel< DefaultAnnotatedIntervalTableRow > selectionModel;
+	private final TableViewer< DefaultAnnotatedIntervalTableRow > tableViewer;
+	private final BdvHandle bdvHandle;
+	private final String name;
+
+	// TODO: split in two classes: the GridOverlayDisplay and the GridOverlayView
+	public GridOverlaySourceDisplay(MoBIE moBIE2, BdvHandle bdvHandle, String name, String tableDataFolder, GridSourceTransformer sourceTransformer )
+	{
+		this.bdvHandle = bdvHandle;
+		this.name = name;
+
+		String tablePath = moBIE2.getDefaultTablePath( tableDataFolder );
+		tablePath = Utils.resolveTablePath( tablePath );
+		Logger.log( "Opening table:\n" + tablePath );
+
+		Map< String, List< String > > columns = TableColumns.stringColumnsFromTableFile( tablePath );
+		final AnnotatedIntervalCreator annotatedIntervalCreator = new AnnotatedIntervalCreator( columns, sourceTransformer );
+		final List< DefaultAnnotatedIntervalTableRow > tableRows = annotatedIntervalCreator.getTableRows();
+
+		coloringModel = new MoBIEColoringModel< DefaultAnnotatedIntervalTableRow >( ColoringLuts.GLASBEY );
+		selectionModel = new DefaultSelectionModel< DefaultAnnotatedIntervalTableRow >();
+		coloringModel.setSelectionModel( selectionModel );
+
+		tableViewer = new TableViewer<>( tableRows, selectionModel, coloringModel, name ).show();
+		coloringModel.listeners().add( tableViewer );
+		selectionModel.listeners().add( tableViewer );
+
+		sourceAndConverters = new ArrayList<>();
+		showGridImage( bdvHandle, name, tableRows );
+
+		coloringModel.listeners().add( this );
+		selectionModel.listeners().add( this );
+	}
+
+	private void showGridImage( BdvHandle bdvHandle, String name, List< DefaultAnnotatedIntervalTableRow > tableRows )
+	{
+		final TableRowsIntervalImage< DefaultAnnotatedIntervalTableRow > intervalImage = new TableRowsIntervalImage<>( tableRows, coloringModel, name );
+		SourceAndConverter< IntType > sourceAndConverter = intervalImage.getSourceAndConverter();
+		SourceAndConverterServices.getSourceAndConverterDisplayService().show( bdvHandle, sourceAndConverter );
+		sourceAndConverters.add( sourceAndConverter );
+	}
+
+	public TableViewer< DefaultAnnotatedIntervalTableRow > getTableViewer()
+	{
+		return tableViewer;
+	}
+
+	public BdvHandle getBdvHandle()
+	{
+		return bdvHandle;
+	}
+
+	@Override
+	public void coloringChanged()
+	{
+		bdvHandle.getViewerPanel().requestRepaint();
+	}
+
+	@Override
+	public void selectionChanged()
+	{
+
+	}
+
+	@Override
+	public void focusEvent( DefaultAnnotatedIntervalTableRow selection )
+	{
+		final double[] max = selection.interval.maxAsDoubleArray();
+		final double[] min = selection.interval.minAsDoubleArray();
+		final double[] center = new double[ min.length ];
+		for ( int d = 0; d < 3; d++ )
+		{
+			center[ d ] = ( max[ d ] + min[ d ] ) / 2;
+		}
+
+		BdvLocationChanger.moveToLocation( bdvHandle, new BdvLocation( BdvLocationType.Position3d, center ) );
+
+	}
+
+	public String getName()
+	{
+		return name;
+	}
+
+	public void close()
+	{
+		for ( SourceAndConverter< ? > sourceAndConverter : sourceAndConverters )
+		{
+			SourceAndConverterServices.getSourceAndConverterDisplayService().removeFromAllBdvs( sourceAndConverter );
+		}
+
+		tableViewer.close();
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/grid/TableRowsIntervalImage.java b/src/main/java/de/embl/cba/mobie/grid/TableRowsIntervalImage.java
new file mode 100644
index 000000000..f8727ad70
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/grid/TableRowsIntervalImage.java
@@ -0,0 +1,131 @@
+package de.embl.cba.mobie.grid;
+
+import bdv.util.BdvOverlay;
+import bdv.util.RealRandomAccessibleIntervalSource;
+import bdv.viewer.SourceAndConverter;
+import de.embl.cba.bdv.utils.sources.ARGBConvertedRealSource;
+import de.embl.cba.mobie.color.ListItemsARGBConverter;
+import de.embl.cba.tables.color.ColorUtils;
+import de.embl.cba.tables.color.ColoringModel;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.RealInterval;
+import net.imglib2.RealLocalizable;
+import net.imglib2.position.FunctionRealRandomAccessible;
+import net.imglib2.type.numeric.ARGBType;
+import net.imglib2.type.numeric.integer.IntType;
+import net.imglib2.util.Intervals;
+
+import java.awt.*;
+import java.util.HashMap;
+import java.util.List;
+import java.util.function.BiConsumer;
+
+public class TableRowsIntervalImage< T extends AnnotatedIntervalTableRow >
+{
+	private final List< T > tableRows;
+	private final ColoringModel< T > coloringModel;
+	private RandomAccessibleInterval< IntType > rai;
+	private double[] contrastLimits;
+	private HashMap< String, T > nameToTableRow;
+	private HashMap< String, Integer > nameToTableRowIndex;
+	private ARGBConvertedRealSource argbSource;
+	private String name;
+	private SourceAndConverter< IntType > sourceAndConverter;
+	private RealInterval union;
+
+	public TableRowsIntervalImage(
+			List< T > tableRows,
+			ColoringModel< T > coloringModel,
+			String name )
+	{
+		this.tableRows = tableRows;
+		this.coloringModel = coloringModel;
+		this.name = name;
+
+		init( tableRows );
+		createImage();
+	}
+
+	public void init( List< T > tableRows )
+	{
+		nameToTableRow = new HashMap<>();
+		nameToTableRowIndex = new HashMap();
+
+		int rowIndex = 0;
+		for ( T tableRow : tableRows )
+		{
+			nameToTableRow.put( tableRow.getName(), tableRow );
+			nameToTableRowIndex.put( tableRow.getName(), rowIndex++ );
+			if ( union == null )
+				union = tableRow.getInterval();
+			else
+				union = Intervals.union( tableRow.getInterval(), union );
+		}
+	}
+
+	private void createImage( )
+	{
+		BiConsumer< RealLocalizable, IntType > biConsumer = ( l, t ) ->
+		{
+			t.setInteger( ListItemsARGBConverter.OUT_OF_BOUNDS_ROW_INDEX );
+
+			for ( T annotatedIntervalTableRow : tableRows )
+			{
+				final RealInterval interval = annotatedIntervalTableRow.getInterval();
+
+				if ( Intervals.contains( interval, l ) )
+				{
+					final int rowIndex = annotatedIntervalTableRow.rowIndex();
+					t.setInteger( rowIndex );
+				}
+			}
+		};
+
+		final FunctionRealRandomAccessible< IntType > randomAccessible = new FunctionRealRandomAccessible( 3, biConsumer, IntType::new );
+
+		final IntType intType = randomAccessible.realRandomAccess().get();
+		final RealRandomAccessibleIntervalSource< IntType > source = new RealRandomAccessibleIntervalSource( randomAccessible, Intervals.smallestContainingInterval( union ), intType, name );
+
+		final ListItemsARGBConverter< T > argbConverter =
+				new ListItemsARGBConverter<>( tableRows, coloringModel );
+
+		sourceAndConverter = new SourceAndConverter( source, argbConverter );
+
+		contrastLimits = new double[]{ 0, 255 };
+	}
+
+	public String getName()
+	{
+		return name;
+	}
+
+	public ARGBType getColor()
+	{
+		return ColorUtils.getARGBType( Color.GRAY );
+	}
+
+	public double[] getContrastLimits()
+	{
+		return contrastLimits;
+	}
+
+	public RandomAccessibleInterval< ? > getRAI()
+	{
+		return rai;
+	}
+
+	public SourceAndConverter< IntType > getSourceAndConverter()
+	{
+		return sourceAndConverter;
+	}
+
+	public BdvOverlay getOverlay()
+	{
+		return null;
+	}
+
+	public boolean isInitiallyVisible()
+	{
+		return true;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/image/ImageProperties.java b/src/main/java/de/embl/cba/mobie/image/ImageProperties.java
deleted file mode 100644
index 57d97746f..000000000
--- a/src/main/java/de/embl/cba/mobie/image/ImageProperties.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package de.embl.cba.mobie.image;
-
-public class ImageProperties extends MutableImageProperties
-{
-	public String type;
-	public Storage storage;
-	public String tableFolder;
-}
diff --git a/src/main/java/de/embl/cba/mobie/image/ImagePropertiesToMetadataAdapter.java b/src/main/java/de/embl/cba/mobie/image/ImagePropertiesToMetadataAdapter.java
deleted file mode 100644
index e2056a475..000000000
--- a/src/main/java/de/embl/cba/mobie/image/ImagePropertiesToMetadataAdapter.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package de.embl.cba.mobie.image;
-
-import de.embl.cba.bdv.utils.sources.Metadata;
-import org.apache.commons.lang.WordUtils;
-
-import java.util.ArrayList;
-
-public class ImagePropertiesToMetadataAdapter
-{
-	public void setMetadataFromMutableImageProperties(Metadata metadata, MutableImageProperties imageProperties )
-	{
-		metadata.contrastLimits = imageProperties.contrastLimits != null
-				? imageProperties.contrastLimits : metadata.contrastLimits;
-		metadata.color = imageProperties.color != null
-				? imageProperties.color : metadata.color;
-		metadata.valueLimits = imageProperties.valueLimits != null
-				? imageProperties.valueLimits : metadata.valueLimits;
-		metadata.resolution3dView = imageProperties.resolution3dView;
-
-		if ( metadata.color != null )
-		{
-			// within MoBIE the convention is Captial first letter:
-			// see class ColoringLuts
-			metadata.color = WordUtils.capitalize( metadata.color );
-		}
-
-		metadata.colorByColumn = imageProperties.colorByColumn;
-		metadata.selectedSegmentIds = imageProperties.selectedLabelIds;
-		metadata.showImageIn3d = imageProperties.showImageIn3d;
-		metadata.showSelectedSegmentsIn3d = imageProperties.showSelectedSegmentsIn3d;
-		metadata.additionalSegmentTableNames = imageProperties.tables;
-		metadata.addedTransform = imageProperties.addedTransform;
-	}
-
-	public void setMutableImagePropertiesFromMetadata( MutableImageProperties imageProperties, Metadata metadata ) {
-		imageProperties.contrastLimits = metadata.contrastLimits != null
-				? metadata.contrastLimits : imageProperties.contrastLimits;
-		imageProperties.color = metadata.color != null
-				? metadata.color : imageProperties.color;
-		imageProperties.valueLimits = metadata.valueLimits != null
-				? metadata.valueLimits : imageProperties.valueLimits;
-		imageProperties.addedTransform = metadata.addedTransform != null
-				? metadata.addedTransform : imageProperties.addedTransform;
-
-		imageProperties.resolution3dView = metadata.resolution3dView;
-		imageProperties.colorByColumn = metadata.colorByColumn;
-		imageProperties.selectedLabelIds = metadata.selectedSegmentIds != null
-				? new ArrayList<>(metadata.selectedSegmentIds) : imageProperties.selectedLabelIds;
-		imageProperties.showImageIn3d = metadata.showImageIn3d;
-		imageProperties.showSelectedSegmentsIn3d = metadata.showSelectedSegmentsIn3d;
-		imageProperties.tables = metadata.additionalSegmentTableNames != null
-				? new ArrayList<>(metadata.additionalSegmentTableNames): imageProperties.tables;
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/image/ImagesJsonParser.java b/src/main/java/de/embl/cba/mobie/image/ImagesJsonParser.java
deleted file mode 100644
index 53c563d48..000000000
--- a/src/main/java/de/embl/cba/mobie/image/ImagesJsonParser.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package de.embl.cba.mobie.image;
-
-import com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
-import com.google.gson.stream.JsonReader;
-import com.google.gson.stream.JsonWriter;
-import de.embl.cba.mobie.bookmark.Bookmark;
-import de.embl.cba.tables.FileAndUrlUtils;
-
-import java.io.*;
-import java.lang.reflect.Type;
-import java.util.HashMap;
-import java.util.Map;
-
-public class ImagesJsonParser
-{
-	private final String imagesLocation;
-
-	public ImagesJsonParser( String imagesLocation )
-	{
-		this.imagesLocation = imagesLocation;
-	}
-
-	public Map< String, ImageProperties > getImagePropertiesMap()
-	{
-		try
-		{
-			return parseImagesJson();
-		}
-		catch ( IOException e )
-		{
-			e.printStackTrace();
-			return null;
-		}
-	}
-
-	// TODO - maybe fix rest of parsers this way too?
-	public void writeImagePropertiesMap( String path, Map<String, ImageProperties> imagePropertiesMap ) throws IOException {
-
-		try( OutputStream outputStream = new FileOutputStream( path );
-			 JsonWriter writer = new JsonWriter( new OutputStreamWriter(outputStream, "UTF-8")) ) {
-			writer.setIndent("	");
-			writeJson( writer, imagePropertiesMap );
-		}
-	}
-
-	private Map< String, ImageProperties > parseImagesJson() throws IOException
-	{
-		final String imagesJsonLocation = FileAndUrlUtils.combinePath( imagesLocation, "images/images.json" );
-
-		// swapped to using try-with-resources block, so streams don't remain open and interfere
-		// with file manipulation
-		try ( InputStream is = FileAndUrlUtils.getInputStream( imagesJsonLocation );
-			  JsonReader reader = new JsonReader( new InputStreamReader( is, "UTF-8" ) )) {
-				  return parseJson(reader);
-			  }
-	}
-
-	private Map< String, ImageProperties > parseJson( JsonReader reader )
-	{
-		Gson gson = new Gson();
-		Type type = new TypeToken< Map< String, ImageProperties > >() {}.getType();
-		return gson.fromJson( reader, type );
-	}
-
-	private void writeJson ( JsonWriter writer, Map<String, ImageProperties> imagePropertiesMap) {
-		Gson gson = new Gson();
-		Type type = new TypeToken< Map< String, ImageProperties > >() {}.getType();
-		gson.toJson( imagePropertiesMap, type, writer);
-	}
-
-}
diff --git a/src/main/java/de/embl/cba/mobie/image/MutableImageProperties.java b/src/main/java/de/embl/cba/mobie/image/MutableImageProperties.java
deleted file mode 100644
index 9548d11c9..000000000
--- a/src/main/java/de/embl/cba/mobie/image/MutableImageProperties.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package de.embl.cba.mobie.image;
-
-import java.util.ArrayList;
-
-public class MutableImageProperties
-{
-	public String color;
-	public String colorByColumn;
-	public double[] contrastLimits;
-	public double[] valueLimits;
-	public double resolution3dView;
-	public ArrayList< String > tables;
-	public ArrayList< Double > selectedLabelIds;
-	public boolean showSelectedSegmentsIn3d;
-	public boolean showImageIn3d;
-	public double[] addedTransform;
-}
diff --git a/src/main/java/de/embl/cba/mobie/image/SourceAndMetadataChangedListener.java b/src/main/java/de/embl/cba/mobie/image/SourceAndMetadataChangedListener.java
deleted file mode 100644
index 0aa5803e7..000000000
--- a/src/main/java/de/embl/cba/mobie/image/SourceAndMetadataChangedListener.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package de.embl.cba.mobie.image;
-
-import de.embl.cba.tables.image.SourceAndMetadata;
-
-public interface SourceAndMetadataChangedListener
-{
-	void addedToBDV( SourceAndMetadata< ? > sam );
-
-	void removedFromBDV( SourceAndMetadata< ? > sam );
-}
diff --git a/src/main/java/de/embl/cba/mobie/image/SourceGroupLabelSourceCreator.java b/src/main/java/de/embl/cba/mobie/image/SourceGroupLabelSourceCreator.java
deleted file mode 100644
index 81be7dd05..000000000
--- a/src/main/java/de/embl/cba/mobie/image/SourceGroupLabelSourceCreator.java
+++ /dev/null
@@ -1,132 +0,0 @@
-package de.embl.cba.mobie.image;
-
-import bdv.util.RandomAccessibleIntervalSource;
-import bdv.util.RandomAccessibleIntervalSource4D;
-import bdv.viewer.Source;
-import de.embl.cba.bdv.utils.sources.Metadata;
-import de.embl.cba.mobie.Constants;
-import de.embl.cba.mobie.bookmark.Layout;
-import de.embl.cba.mobie.utils.Utils;
-import de.embl.cba.tables.FileAndUrlUtils;
-import de.embl.cba.tables.image.SourceAndMetadata;
-import net.imglib2.*;
-import net.imglib2.position.FunctionRandomAccessible;
-import net.imglib2.realtransform.AffineTransform3D;
-import net.imglib2.type.numeric.integer.IntType;
-import net.imglib2.util.Intervals;
-import net.imglib2.util.Util;
-import net.imglib2.view.Views;
-
-import java.util.*;
-import java.util.function.BiConsumer;
-
-public class SourceGroupLabelSourceCreator
-{
-	public static final String SOURCE_GROUP_LABEL_IMAGE_METADATA = "SourceGroupLabelImageMetadata";
-	private final String name;
-	private final Layout layout;
-	private final HashMap< String, SourceAndMetadata > sourcesAndMetadata;
-	private Map< String, RealInterval > sourceNameToInterval;
-	private Map< String, Integer > sourceNameToLabelIndex;
-	private Source< IntType > labelSource;
-	private Metadata metadata;
-
-	public SourceGroupLabelSourceCreator( HashMap< String, SourceAndMetadata > sourcesAndMetadata, String name, Layout layout )
-	{
-		this.sourcesAndMetadata = sourcesAndMetadata;
-		this.name = name;
-		this.layout = layout;
-	}
-
-	private void prepare()
-	{
-		sourceNameToInterval = createIntervalMap( sourcesAndMetadata, layout.layers );
-
-		sourceNameToLabelIndex = new HashMap<>();
-
-		int labelIndex = 1;
-		for ( String sourceName : sourceNameToInterval.keySet() )
-		{
-			sourceNameToLabelIndex.put( sourceName, labelIndex++ );
-		}
-
-		final RealInterval union = createUnion( sourceNameToInterval );
-
-		BiConsumer< RealLocalizable, IntType > biConsumer = ( l, t ) ->
-		{
-			t.setInteger( 0 );
-
-			for ( Map.Entry< String, RealInterval > entry : sourceNameToInterval.entrySet() )
-			{
-				if ( Intervals.contains( entry.getValue(), l ) )
-				{
-					final Integer integer = sourceNameToLabelIndex.get( entry.getKey() );
-					t.setInteger( integer );
-					return;
-				}
-			}
-		};
-
-		final FunctionRandomAccessible< IntType > randomAccessible =
-				new FunctionRandomAccessible( 3, biConsumer, IntType::new );
-
-		RandomAccessibleInterval< IntType > rai = Views.interval( randomAccessible, Intervals.smallestContainingInterval( union ) );
-
-		// make 4D such that it is clear how many time points it has
-		// if this is not done things break during visualisation
-		rai = Views.addDimension( rai, 0 ,0  );
-
-		labelSource = new RandomAccessibleIntervalSource4D<>( rai, Util.getTypeFromInterval( rai ), name );
-
-		metadata = new Metadata( name );
-		metadata.type = Metadata.Type.Segmentation;
-		metadata.displayName = name;
-		metadata.color = Constants.RANDOM_FROM_GLASBEY;
-		metadata.contrastLimits = new double[]{ 0, 1000 }; // TODO: this is was we currently need for label overlay to be slightly transparent
-
-		final SourceGroupLabelSourceMetadata labelImageMetadata = new SourceGroupLabelSourceMetadata();
-		labelImageMetadata.sourceNameToInterval = sourceNameToInterval;
-		labelImageMetadata.sourceNameToLabelIndex = sourceNameToLabelIndex;
-		metadata.misc.put( SOURCE_GROUP_LABEL_IMAGE_METADATA, labelImageMetadata );
-	}
-
-	private RealInterval createUnion( Map< String, RealInterval > sourceNameToInterval )
-	{
-		RealInterval union = null;
-		for ( String sourceName : sourceNameToInterval.keySet() )
-		{
-			final RealInterval interval = sourceNameToInterval.get( sourceName );
-			if ( union == null )
-				union = interval;
-			else
-				union = Intervals.union( interval, union );
-		}
-
-		return union;
-	}
-
-	public SourceAndMetadata< IntType > create()
-	{
-		prepare();
-
-		final SourceAndMetadata< IntType > sourceAndMetadata = new SourceAndMetadata<>( labelSource, metadata );
-
-		return sourceAndMetadata;
-	}
-
-	private static HashMap< String, RealInterval > createIntervalMap( HashMap< String, SourceAndMetadata > sourceNameToSAM, ArrayList< String > sourceNames )
-	{
-		final HashMap< String, RealInterval > sourceNameToInterval = new HashMap<>();
-		for ( String sourceName : sourceNames )
-		{
-			final SourceAndMetadata< ? > sam = sourceNameToSAM.get( sourceName );
-			final FinalRealInterval sourceInterval = Utils.estimateBounds( sam.source() );
-			final AffineTransform3D transform3D = new AffineTransform3D();
-			transform3D.set( sam.metadata().addedTransform );
-
-			final FinalRealInterval translatedSourceInterval = transform3D.estimateBounds( sourceInterval );
-			sourceNameToInterval.put( sourceName, translatedSourceInterval );
-		}
-		return sourceNameToInterval;
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/image/SourceGroupLabelSourceMetadata.java b/src/main/java/de/embl/cba/mobie/image/SourceGroupLabelSourceMetadata.java
deleted file mode 100644
index 84052e6f5..000000000
--- a/src/main/java/de/embl/cba/mobie/image/SourceGroupLabelSourceMetadata.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package de.embl.cba.mobie.image;
-
-import net.imglib2.RealInterval;
-
-import java.util.Map;
-
-public class SourceGroupLabelSourceMetadata
-{
-	public Map< String, RealInterval > sourceNameToInterval;
-	public Map< String, Integer > sourceNameToLabelIndex;
-}
diff --git a/src/main/java/de/embl/cba/mobie/image/SourceGroups.java b/src/main/java/de/embl/cba/mobie/image/SourceGroups.java
deleted file mode 100644
index f784cade9..000000000
--- a/src/main/java/de/embl/cba/mobie/image/SourceGroups.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package de.embl.cba.mobie.image;
-
-import bdv.util.BdvHandle;
-import bdv.util.BdvStackSource;
-import bdv.viewer.SourceGroup;
-import bdv.viewer.SynchronizedViewerState;
-import de.embl.cba.tables.image.SourceAndMetadata;
-
-import java.util.*;
-
-public abstract class SourceGroups
-{
-	private static Map< String, SourceGroup > groupIdToSourceGroup = new HashMap<>();
-	private static Map< String, List< SourceAndMetadata< ? > > > groupIdToSourceAndMetadataList = new HashMap<>();
-
-	public static synchronized SourceGroup addSourceToGroup( SourceAndMetadata< ? > sourceAndMetadata )
-	{
-		final BdvStackSource< ? > bdvStackSource = sourceAndMetadata.metadata().bdvStackSource;
-		final BdvHandle bdvHandle = bdvStackSource.getBdvHandle();
-		final SynchronizedViewerState state = bdvHandle.getViewerPanel().state();
-
-		String groupId = sourceAndMetadata.metadata().groupId;
-		if ( ! groupIdToSourceGroup.containsKey( groupId ) )
-		{
-			SourceGroup sourceGroup = new SourceGroup();
-			groupIdToSourceGroup.put( groupId, sourceGroup );
-			state.addGroup( sourceGroup );
-			state.setGroupName( sourceGroup, groupId );
-			state.setGroupActive( sourceGroup, true );
-		}
-
-		if ( ! groupIdToSourceAndMetadataList.containsKey( groupId ) )
-		{
-			groupIdToSourceAndMetadataList.put( groupId, new ArrayList<>(  ) );
-		}
-
-		SourceGroup sourceGroup = groupIdToSourceGroup.get( groupId );
-		state.addSourceToGroup( bdvStackSource.getSources().get( 0 ), sourceGroup );
-
-		groupIdToSourceAndMetadataList.get( groupId ).add( sourceAndMetadata );
-
-		return sourceGroup;
-	}
-
-	public static SourceGroup getSourceGroup( String groupId )
-	{
-		return groupIdToSourceGroup.get( groupId );
-	}
-
-	public static List< SourceAndMetadata< ? > > getSourceAndMetadataList( String groupId )
-	{
-		return Collections.unmodifiableList( groupIdToSourceAndMetadataList.get( groupId ) );
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/image/SourcesModel.java b/src/main/java/de/embl/cba/mobie/image/SourcesModel.java
deleted file mode 100644
index b5eb89b28..000000000
--- a/src/main/java/de/embl/cba/mobie/image/SourcesModel.java
+++ /dev/null
@@ -1,143 +0,0 @@
-package de.embl.cba.mobie.image;
-
-import com.google.gson.internal.LinkedTreeMap;
-import com.google.gson.stream.JsonReader;
-import de.embl.cba.bdv.utils.sources.LazySpimSource;
-import de.embl.cba.bdv.utils.sources.Metadata;
-import de.embl.cba.bdv.utils.sources.Sources;
-import de.embl.cba.mobie.ui.MoBIEOptions;
-import de.embl.cba.mobie.utils.Enums;
-import de.embl.cba.tables.FileAndUrlUtils;
-import de.embl.cba.mobie.utils.Utils;
-import de.embl.cba.tables.image.ImageSourcesModel;
-import de.embl.cba.tables.image.SourceAndMetadata;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-
-public class SourcesModel implements ImageSourcesModel
-{
-	public static final String EM_ID = "em-";
-	public static final String XRAY_ID = "xray-";
-	private final MoBIEOptions.ImageDataStorageModality imageDataStorageModality;
-
-	private Map< String, SourceAndMetadata< ? > > nameToSourceAndDefaultMetadata;
-	private final String tableDataLocation;
-	private String imageStorageModality;
-	private String imageRootLocation;
-
-	public SourcesModel( String imageDataLocation, MoBIEOptions.ImageDataStorageModality imageDataStorageModality, String tableDataLocation )
-	{
-		this.imageDataStorageModality = imageDataStorageModality;
-		this.tableDataLocation = tableDataLocation;
-
-		nameToSourceAndDefaultMetadata = new HashMap<>();
-
-		fetchSources( imageDataLocation );
-	}
-
-	@Override
-	public Map< String, SourceAndMetadata< ? > > sources()
-	{
-		return nameToSourceAndDefaultMetadata;
-	}
-
-	@Override
-	public boolean is2D()
-	{
-		return false;
-	}
-
-	private void fetchSources( String imageDataLocation )
-	{
-		try
-		{
-			final ImagesJsonParser parser = new ImagesJsonParser( imageDataLocation );
-			final Map< String, ImageProperties > nameToImageProperties = parser.getImagePropertiesMap();
-			addSources( imageDataLocation, nameToImageProperties );
-		}
-		catch ( Exception e ) {
-			e.printStackTrace();
-			throw new UnsupportedOperationException();
-		}
-	}
-
-	private void addSources( String imageDataLocation, Map< String, ImageProperties > nameToImageProperties )
-	{
-		// TODO: this can probably be simplified somehow...
-		imageStorageModality = imageDataStorageModality.equals( MoBIEOptions.ImageDataStorageModality.S3 ) ? "remote" : "local";
-
-		imageRootLocation = FileAndUrlUtils.combinePath( imageDataLocation, "images" );
-
-		final Set< String > names = nameToImageProperties.keySet();
-
-		for ( String name : names )
-		{
-			final ImageProperties properties = nameToImageProperties.get( name );
-
-			final Metadata metadata = initMetadata( name );
-			setModality( name, metadata );
-			setXmlLocation( properties, metadata );
-			setDefaultTableLocation( properties, metadata );
-
-			metadata.type = Enums.valueOf( Metadata.Type.class, properties.type );
-
-			final ImagePropertiesToMetadataAdapter adapter = new ImagePropertiesToMetadataAdapter();
-			adapter.setMetadataFromMutableImageProperties( metadata, properties );
-
-			final LazySpimSource lazySpimSource = new LazySpimSource( name, metadata.xmlLocation );
-			nameToSourceAndDefaultMetadata.put( name, new SourceAndMetadata( lazySpimSource, metadata ) );
-			Sources.sourceToMetadata.put( lazySpimSource, metadata );
-		}
-
-		if ( nameToSourceAndDefaultMetadata.size() == 0)
-		{
-			throw new UnsupportedOperationException( "No image data found in: "
-					+ FileAndUrlUtils.combinePath( imageDataLocation, "images", imageStorageModality ) );
-		}
-
-		Utils.log("Found " + nameToSourceAndDefaultMetadata.size() + " image sources." );
-	}
-
-	private Metadata initMetadata( String name )
-	{
-		final Metadata metadata = new Metadata( name );
-		metadata.numSpatialDimensions = 3;
-		metadata.displayName = name;
-		return metadata;
-	}
-
-	private void setXmlLocation( ImageProperties properties, Metadata metadata )
-	{
-		if ( imageStorageModality == "remote")
-			metadata.xmlLocation = FileAndUrlUtils.combinePath( imageRootLocation, properties.storage.remote );
-		else
-			metadata.xmlLocation = FileAndUrlUtils.combinePath( imageRootLocation, properties.storage.local );
-	}
-
-	private void setDefaultTableLocation( ImageProperties properties, Metadata metadata )
-	{
-		if ( properties.tableFolder != null )
-		{
-			metadata.segmentsTablePath = FileAndUrlUtils.combinePath( tableDataLocation, properties.tableFolder, "default.csv" );
-		}
-	}
-
-	private void setModality( String imageId, Metadata metadata )
-	{
-		if ( imageId.contains( EM_ID ) )
-		{
-			metadata.modality = Metadata.Modality.EM;
-		}
-		else if ( imageId.contains( XRAY_ID ) )
-		{
-			metadata.modality = Metadata.Modality.XRay;
-		}
-		else
-		{
-			metadata.modality = Metadata.Modality.FM;
-		}
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/image/Storage.java b/src/main/java/de/embl/cba/mobie/image/Storage.java
deleted file mode 100644
index 073d59c36..000000000
--- a/src/main/java/de/embl/cba/mobie/image/Storage.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package de.embl.cba.mobie.image;
-
-public class Storage
-{
-	public String local;
-	public String remote;
-}
diff --git a/src/main/java/de/embl/cba/mobie/mesh/MeshCreator.java b/src/main/java/de/embl/cba/mobie/mesh/MeshCreator.java
new file mode 100644
index 000000000..713c73059
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/mesh/MeshCreator.java
@@ -0,0 +1,231 @@
+package de.embl.cba.mobie.mesh;
+
+import bdv.viewer.Source;
+import customnode.CustomTriangleMesh;
+import de.embl.cba.bdv.utils.objects3d.FloodFill;
+import de.embl.cba.tables.Logger;
+import de.embl.cba.tables.Utils;
+import de.embl.cba.tables.ij3d.UniverseUtils;
+import de.embl.cba.tables.imagesegment.ImageSegment;
+import de.embl.cba.tables.mesh.MeshExtractor;
+import isosurface.MeshEditor;
+import net.imglib2.FinalInterval;
+import net.imglib2.FinalRealInterval;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.algorithm.neighborhood.DiamondShape;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.type.numeric.RealType;
+import net.imglib2.util.Intervals;
+import net.imglib2.view.Views;
+import org.scijava.vecmath.Point3f;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class MeshCreator < S extends ImageSegment >
+{
+	private int meshSmoothingIterations;
+	private double maxNumSegmentVoxels;
+
+	public MeshCreator( int meshSmoothingIterations, double maxNumSegmentVoxels )
+	{
+		this.meshSmoothingIterations = meshSmoothingIterations;
+		this.maxNumSegmentVoxels = maxNumSegmentVoxels;
+	}
+
+	private float[] createMesh( ImageSegment segment, double voxelSpacing, Source< ? > labelsSource )
+	{
+		Integer level = getLevel( segment, labelsSource, voxelSpacing );
+		double[] voxelSpacings = Utils.getVoxelSpacings( labelsSource ).get( level );
+
+		final RandomAccessibleInterval< ? extends RealType< ? > > labelsRAI = ( RandomAccessibleInterval< ? extends RealType< ? > > ) labelsSource.getSource( segment.timePoint(), level  );
+
+		if ( segment.boundingBox() == null )
+			setSegmentBoundingBox( segment, labelsRAI, voxelSpacings );
+
+		FinalInterval boundingBox = getIntervalInVoxelUnits( segment.boundingBox(), voxelSpacings );
+		final long numElements = Intervals.numElements( boundingBox );
+
+		if ( voxelSpacing == 0 ) // auto-resolution
+		{
+			if ( numElements > maxNumSegmentVoxels )
+			{
+				Logger.info( "# 3D View:\n" +
+						"The bounding box of the selected segment has " + numElements + " voxels.\n" +
+						"The maximum recommended number is however only " + maxNumSegmentVoxels + ".\n" +
+						"It can take a bit of time to load...." );
+			}
+		}
+
+		if ( ! Intervals.contains( labelsRAI, boundingBox ) )
+		{
+			System.err.println( "The segment bounding box " + boundingBox + " is not fully contained in the image interval: " + Arrays.toString( Intervals.minAsLongArray( labelsRAI ) ) + "-" +  Arrays.toString( Intervals.maxAsDoubleArray( labelsRAI ) ));
+		}
+
+		final MeshExtractor meshExtractor = new MeshExtractor(
+				Views.extendZero( ( RandomAccessibleInterval ) labelsRAI ),
+				boundingBox,
+				new AffineTransform3D(),
+				new int[]{ 1, 1, 1 },
+				() -> false );
+
+		final float[] meshCoordinates = meshExtractor.generateMesh( segment.labelId() );
+
+		for ( int i = 0; i < meshCoordinates.length; )
+		{
+			meshCoordinates[ i++ ] *= voxelSpacings[ 0 ];
+			meshCoordinates[ i++ ] *= voxelSpacings[ 1 ];
+			meshCoordinates[ i++ ] *= voxelSpacings[ 2 ];
+		}
+
+		if ( meshCoordinates.length == 0 )
+		{
+			Logger.warn( "Could not find any pixels for segment with label " + segment.labelId()
+					+ "\nwithin bounding box " + boundingBox );
+			return null;
+		}
+
+		return meshCoordinates;
+	}
+
+	public CustomTriangleMesh createSmoothCustomTriangleMesh( S segment, double voxelSpacing, boolean recomputeMesh, Source< ? > source )
+	{
+		CustomTriangleMesh triangleMesh = createCustomTriangleMesh( segment, voxelSpacing, recomputeMesh, source );
+		MeshEditor.smooth2( triangleMesh, meshSmoothingIterations );
+		return triangleMesh;
+	}
+
+	private CustomTriangleMesh createCustomTriangleMesh( S segment, double voxelSpacing, boolean recomputeMesh, Source< ? > source )
+	{
+		if ( segment.getMesh() == null || recomputeMesh )
+		{
+			try
+			{
+				final float[] mesh = createMesh( segment, voxelSpacing, source );
+				if ( mesh == null )
+				{
+					throw new RuntimeException( "Could not create mesh for segment " + segment.labelId() + " at time point " + segment.timePoint() );
+				}
+				segment.setMesh( mesh );
+			}
+			catch ( Exception e )
+			{
+				e.printStackTrace();
+				throw new RuntimeException( "Could not create mesh for segment " + segment.labelId() + " at time point " + segment.timePoint() );
+			}
+		}
+
+		CustomTriangleMesh triangleMesh = asCustomTriangleMesh( segment.getMesh() );
+
+		return triangleMesh;
+	}
+
+	private static CustomTriangleMesh asCustomTriangleMesh( final float[] meshCoordinates )
+	{
+		final ArrayList< Point3f > points = new ArrayList<>();
+
+		for ( int i = 0; i < meshCoordinates.length; )
+		{
+			points.add( new Point3f(
+					meshCoordinates[ i++ ],
+					meshCoordinates[ i++ ],
+					meshCoordinates[ i++ ] ) );
+		}
+
+		CustomTriangleMesh mesh = new CustomTriangleMesh( points );
+
+		return mesh;
+	}
+
+	private Integer getLevel( ImageSegment segment, Source< ? > labelSource, double voxelSpacing )
+	{
+		Integer level;
+
+		if ( voxelSpacing != 0 )
+		{
+			level = UniverseUtils.getLevel( labelSource, voxelSpacing );
+		}
+		else // auto-resolution
+		{
+			if ( segment.boundingBox() == null )
+			{
+				Logger.error( "3D View:\n" +
+						"Automated resolution level selection is enabled, but the segment has no bounding box.\n" +
+						"This combination is currently not possible." );
+				level = null;
+			}
+			else
+			{
+				final ArrayList< double[] > voxelSpacings = Utils.getVoxelSpacings( labelSource );
+
+				final int numLevels = voxelSpacings.size();
+
+				for ( level = 0; level < numLevels; level++ )
+				{
+					FinalInterval boundingBox = getIntervalInVoxelUnits( segment.boundingBox(), voxelSpacings.get( level ) );
+
+					final long numElements = Intervals.numElements( boundingBox );
+
+					if ( numElements <= maxNumSegmentVoxels )
+						break;
+				}
+
+				if ( level == numLevels ) level = numLevels - 1;
+			}
+		}
+
+		return level;
+	}
+
+	private void setSegmentBoundingBox(
+			ImageSegment segment,
+			RandomAccessibleInterval< ? extends RealType< ? > > labelsRAI,
+			double[] voxelSpacing )
+	{
+		final long[] voxelCoordinate = getSegmentLocationInVoxelsUnits( segment, voxelSpacing );
+
+		final FloodFill floodFill = new FloodFill(
+				labelsRAI,
+				new DiamondShape( 1 ),
+				1000 * 1000 * 1000L );
+
+		floodFill.run( voxelCoordinate );
+		final RandomAccessibleInterval mask = floodFill.getCroppedRegionMask();
+
+		final int numDimensions = segment.numDimensions();
+		final double[] min = new double[ numDimensions ];
+		final double[] max = new double[ numDimensions ];
+		for ( int d = 0; d < numDimensions; d++ )
+		{
+			min[ d ] = mask.min( d ) * voxelSpacing[ d ];
+			max[ d ] = mask.max( d ) * voxelSpacing[ d ];
+		}
+
+		segment.setBoundingBox( new FinalRealInterval( min, max ) );
+	}
+
+	private static long[] getSegmentLocationInVoxelsUnits(
+			ImageSegment segment,
+			double[] calibration )
+	{
+		final long[] voxelCoordinate = new long[ segment.numDimensions() ];
+		for ( int d = 0; d < segment.numDimensions(); d++ )
+			voxelCoordinate[ d ] = ( long ) ( segment.getDoublePosition( d ) / calibration[ d ] );
+		return voxelCoordinate;
+	}
+
+	private FinalInterval getIntervalInVoxelUnits(
+			FinalRealInterval realInterval,
+			double[] calibration )
+	{
+		final long[] min = new long[ 3 ];
+		final long[] max = new long[ 3 ];
+		for ( int d = 0; d < 3; d++ )
+		{
+			min[ d ] = (long) ( realInterval.realMin( d ) / calibration[ d ] );
+			max[ d ] = (long) ( realInterval.realMax( d ) / calibration[ d ] );
+		}
+		return new FinalInterval( min, max );
+	}
+
+}
diff --git a/src/main/java/de/embl/cba/mobie/n5/N5ImageLoader.java b/src/main/java/de/embl/cba/mobie/n5/N5ImageLoader.java
index 1148fb299..477da72e2 100644
--- a/src/main/java/de/embl/cba/mobie/n5/N5ImageLoader.java
+++ b/src/main/java/de/embl/cba/mobie/n5/N5ImageLoader.java
@@ -36,7 +36,6 @@
 import bdv.img.cache.VolatileGlobalCellCache;
 import bdv.util.ConstantRandomAccessible;
 import bdv.util.MipmapTransforms;
-import com.amazonaws.SdkClientException;
 import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription;
 import mpicbg.spim.data.generic.sequence.BasicViewSetup;
 import mpicbg.spim.data.generic.sequence.ImgLoaderHint;
diff --git a/src/main/java/de/embl/cba/mobie/n5/source/LabelSource.java b/src/main/java/de/embl/cba/mobie/n5/source/LabelSource.java
index 604f4faa5..7dbfa13a9 100644
--- a/src/main/java/de/embl/cba/mobie/n5/source/LabelSource.java
+++ b/src/main/java/de/embl/cba/mobie/n5/source/LabelSource.java
@@ -4,12 +4,9 @@
 import bdv.viewer.Interpolation;
 import bdv.viewer.Source;
 import de.embl.cba.lazyalgorithm.RandomAccessibleIntervalFilter;
-import de.embl.cba.lazyalgorithm.converter.NeighborhoodNonZeroBoundariesConverter2;
-import de.embl.cba.lazyalgorithm.view.NeighborhoodViews;
 import mpicbg.spim.data.sequence.VoxelDimensions;
 import net.imglib2.RandomAccessibleInterval;
 import net.imglib2.RealRandomAccessible;
-import net.imglib2.algorithm.neighborhood.HyperSphereShape;
 import net.imglib2.realtransform.AffineTransform3D;
 import net.imglib2.type.numeric.NumericType;
 import net.imglib2.type.numeric.RealType;
@@ -46,19 +43,16 @@ public void showAsBoundary( boolean showAsBoundaries, int boundaryWidth )
 		this.boundaryWidth = boundaryWidth;
 	}
 
-//	@Override
-//	public boolean doBoundingBoxCulling()
-//	{
-////		System.out.println( "Source " + source.getName() + " culling: " + source.doBoundingBoxCulling() );
-//		return source.doBoundingBoxCulling();
-//	}
-
+	@Override
+	public boolean doBoundingBoxCulling()
+	{
+		return source.doBoundingBoxCulling();
+	}
 
 	@Override
 	public synchronized void getSourceTransform( final int t, final int level, final AffineTransform3D transform )
 	{
 		source.getSourceTransform( t, level, transform );
-//		System.out.println( "Source " + source.getName() + " transform: " + transform.toString() );
 	}
 
 	@Override
@@ -70,21 +64,24 @@ public boolean isPresent( final int t )
 	@Override
 	public RandomAccessibleInterval< T > getSource( final int t, final int level )
 	{
-		RandomAccessibleInterval< T > source = this.source.getSource( t, level );
-
-		if ( showAsBoundaries )
-		{
-			NeighborhoodNonZeroBoundariesConverter2< T > boundariesConverter = new NeighborhoodNonZeroBoundariesConverter2< T >( source );
-			RandomAccessibleInterval boundaries = NeighborhoodViews.neighborhoodConvertedView(
-					source,
-					boundariesConverter,
-					new HyperSphereShape( boundaryWidth ) );
-			return boundaries;
-		}
-		else
-		{
-			return source;
-		}
+		return source.getSource( t, level );
+
+		// below code is not needed, because BDV (I think) always shows the interpolated source
+//		RandomAccessibleInterval< T > source = this.source.getSource( t, level );
+//
+//		if ( showAsBoundaries )
+//		{
+//			NeighborhoodNonZeroBoundariesConverter2< T > boundariesConverter = new NeighborhoodNonZeroBoundariesConverter2< T >( source );
+//			RandomAccessibleInterval boundaries = NeighborhoodViews.neighborhoodConvertedView(
+//					source,
+//					boundariesConverter,
+//					new HyperSphereShape( boundaryWidth ) );
+//			return boundaries;
+//		}
+//		else
+//		{
+//			return source;
+//		}
 	}
 
 	@Override
diff --git a/src/main/java/de/embl/cba/mobie/n5/source/Sources.java b/src/main/java/de/embl/cba/mobie/n5/source/Sources.java
index 1042f1693..c586ccda2 100644
--- a/src/main/java/de/embl/cba/mobie/n5/source/Sources.java
+++ b/src/main/java/de/embl/cba/mobie/n5/source/Sources.java
@@ -2,11 +2,8 @@
 
 import bdv.util.BdvFunctions;
 import bdv.util.BdvHandle;
-import bdv.util.BdvOptions;
 import bdv.util.BdvStackSource;
 import bdv.viewer.SourceAndConverter;
-import de.embl.cba.bdv.utils.sources.ARGBConvertedRealSource;
-import de.embl.cba.tables.color.LazyLabelsARGBConverter;
 import net.imglib2.RandomAccessibleInterval;
 import net.imglib2.converter.Converter;
 import net.imglib2.img.display.imagej.ImageJFunctions;
diff --git a/src/main/java/de/embl/cba/mobie/n5/zarr/ui/OpenOMEZARRFromS3AdvancedCommand.java b/src/main/java/de/embl/cba/mobie/n5/zarr/ui/OpenOMEZARRFromS3AdvancedCommand.java
index f77b60bd3..7c317a414 100644
--- a/src/main/java/de/embl/cba/mobie/n5/zarr/ui/OpenOMEZARRFromS3AdvancedCommand.java
+++ b/src/main/java/de/embl/cba/mobie/n5/zarr/ui/OpenOMEZARRFromS3AdvancedCommand.java
@@ -1,10 +1,6 @@
 package de.embl.cba.mobie.n5.zarr.ui;
 
-import bdv.util.BdvFunctions;
 import de.embl.cba.mobie.n5.zarr.OMEZarrS3Reader;
-import de.embl.cba.mobie.n5.zarr.OMEZarrViewer;
-import ij.IJ;
-import mpicbg.spim.data.SpimData;
 import net.imagej.ImageJ;
 import org.scijava.command.Command;
 import org.scijava.plugin.Parameter;
diff --git a/src/main/java/de/embl/cba/mobie/n5/zarr/ui/OpenOMEZARRFromS3Command.java b/src/main/java/de/embl/cba/mobie/n5/zarr/ui/OpenOMEZARRFromS3Command.java
index 89d12c081..1bfd9f551 100644
--- a/src/main/java/de/embl/cba/mobie/n5/zarr/ui/OpenOMEZARRFromS3Command.java
+++ b/src/main/java/de/embl/cba/mobie/n5/zarr/ui/OpenOMEZARRFromS3Command.java
@@ -1,7 +1,5 @@
 package de.embl.cba.mobie.n5.zarr.ui;
 
-import bdv.util.BdvFunctions;
-import de.embl.cba.mobie.n5.zarr.N5OMEZarrImageLoader;
 import de.embl.cba.mobie.n5.zarr.OMEZarrS3Reader;
 import de.embl.cba.mobie.n5.zarr.OMEZarrViewer;
 import mpicbg.spim.data.SpimData;
@@ -11,8 +9,6 @@
 import org.scijava.plugin.Plugin;
 
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.stream.Collectors;
 
 
 @Plugin(type = Command.class, menuPath = "Plugins>BigDataViewer>OME ZARR>Open OME ZARR From S3..." )
diff --git a/src/main/java/de/embl/cba/mobie/open/SourceAndConverterSupplier.java b/src/main/java/de/embl/cba/mobie/open/SourceAndConverterSupplier.java
new file mode 100644
index 000000000..df7b09a92
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/open/SourceAndConverterSupplier.java
@@ -0,0 +1,10 @@
+package de.embl.cba.mobie.open;
+
+import bdv.viewer.SourceAndConverter;
+
+import java.util.List;
+
+public interface SourceAndConverterSupplier
+{
+	List< SourceAndConverter< ? > > get( List< String > sourceNames );
+}
diff --git a/src/main/java/de/embl/cba/mobie/platybrowser/GeneSearch.java b/src/main/java/de/embl/cba/mobie/platybrowser/GeneSearch.java
deleted file mode 100644
index 088d63b86..000000000
--- a/src/main/java/de/embl/cba/mobie/platybrowser/GeneSearch.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package de.embl.cba.mobie.platybrowser;
-
-import de.embl.cba.bdv.utils.BdvUtils;
-import de.embl.cba.bdv.utils.sources.Metadata;
-import de.embl.cba.mobie.Constants;
-import de.embl.cba.mobie.utils.Utils;
-import de.embl.cba.tables.Logger;
-import de.embl.cba.tables.image.ImageSourcesModel;
-import de.embl.cba.tables.image.SourceAndMetadata;
-import mpicbg.spim.data.sequence.VoxelDimensions;
-import net.imglib2.RandomAccessibleInterval;
-import net.imglib2.type.NativeType;
-import net.imglib2.type.numeric.RealType;
-
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-public class GeneSearch
-{
-	private final double micrometerRadius;
-	private final double[] micrometerPosition;
-	private final ImageSourcesModel imageSourcesModel;
-	private Map< String, Double > localExpression;
-
-	public GeneSearch( double micrometerRadius,
-					   double[] micrometerPosition,
-					   ImageSourcesModel imageSourcesModel )
-	{
-		this.micrometerRadius = micrometerRadius;
-		this.micrometerPosition = micrometerPosition;
-		this.imageSourcesModel = imageSourcesModel;
-	}
-
-	public void searchGenes( )
-	{
-		final Map< String, Double > geneExpressionLevels = runSearchAndGetLocalExpression();
-
-		GeneSearchResults.addRowToGeneExpressionTable(
-				micrometerPosition, micrometerRadius, geneExpressionLevels );
-
-		GeneSearchResults.logGeneExpression(
-				micrometerPosition, micrometerRadius, geneExpressionLevels );
-	}
-
-	private Map< String, Double > getExpressionLevelsSortedByValue()
-	{
-		Map< String, Double > localSortedExpression = Utils.sortByValue( localExpression );
-		removeGenesWithZeroExpression( localSortedExpression );
-		return localSortedExpression;
-	}
-
-	private Map< String, Double > runSearchAndGetLocalExpression()
-	{
-		final Map< String, SourceAndMetadata< ? > > sources = imageSourcesModel.sources();
-
-		localExpression = new LinkedHashMap<>(  );
-
-		Logger.info( "# Gene search" );
-		for ( String sourceName : sources.keySet() )
-		{
-			final Metadata.Type type = sources.get( sourceName ).metadata().type;
-			if ( ! type.equals( Metadata.Type.Image )  ) continue;
-			if ( ! sourceName.contains( Constants.PROSPR ) ) continue;
-
-			final SourceAndMetadata sourceAndMetadata =
-					sources.get( sourceName );
-
-			final RandomAccessibleInterval< ? > rai =
-					BdvUtils.getRealTypeNonVolatileRandomAccessibleInterval(
-							sourceAndMetadata.source(), 0, 0 );
-
-			final VoxelDimensions voxelDimensions = sourceAndMetadata.source().getVoxelDimensions();
-
-			final double fractionOfNonZeroVoxels = Utils.getFractionOfNonZeroVoxels(
-					( RandomAccessibleInterval ) rai,
-					micrometerPosition,
-					micrometerRadius,
-					voxelDimensions.dimension( 0 ) );
-
-			final String simplifiedSourceName = Utils.getSimplifiedSourceName( sourceName, true );
-			localExpression.put( simplifiedSourceName, fractionOfNonZeroVoxels );
-			Logger.info(simplifiedSourceName + ": " + fractionOfNonZeroVoxels );
-		}
-
-		return localExpression;
-	}
-
-	private void removeGenesWithZeroExpression( Map< String, Double > localSortedExpression)
-	{
-		ArrayList< String > sortedNames = new ArrayList( localSortedExpression.keySet() );
-
-		// remove entries with zero expression
-		for ( int i = sortedNames.size() - 1; i >= 0; --i )
-		{
-			String name = sortedNames.get( i );
-			double value = localSortedExpression.get( name ).doubleValue();
-
-			if ( value == 0.0 )
-			{
-				localSortedExpression.remove( name );
-			}
-		}
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/platybrowser/GeneSearchResults.java b/src/main/java/de/embl/cba/mobie/platybrowser/GeneSearchResults.java
deleted file mode 100644
index 86d2c9734..000000000
--- a/src/main/java/de/embl/cba/mobie/platybrowser/GeneSearchResults.java
+++ /dev/null
@@ -1,125 +0,0 @@
-package de.embl.cba.mobie.platybrowser;
-
-import de.embl.cba.mobie.utils.Utils;
-import de.embl.cba.tables.TableUIs;
-
-import javax.swing.*;
-import javax.swing.table.DefaultTableModel;
-import java.awt.*;
-import java.util.*;
-
-import static de.embl.cba.mobie.utils.Utils.combine;
-
-public class GeneSearchResults
-{
-	private static JTable table;
-	private static DefaultTableModel model;
-
-	public static void logGeneExpression(
-			double[] micrometerPosition,
-			double micrometerRadius,
-			Map< String, Double > geneExpressionLevels )
-	{
-
-		Utils.log( "\n# Expression levels [fraction of search volume]" );
-		Utils.logVector( "Center position [um]" , micrometerPosition, 2 );
-		Utils.log( "Radius [um]: " + micrometerRadius );
-
-		final LinkedHashMap< String, Double > sortedExpressionLevels = new LinkedHashMap<>( );
-
-		geneExpressionLevels.entrySet()
-				.stream()
-				.sorted(Map.Entry.comparingByValue((Comparator.reverseOrder())))
-				.forEachOrdered(x -> sortedExpressionLevels.put(x.getKey(), x.getValue()));
-
-		for ( String gene : sortedExpressionLevels.keySet() )
-		{
-			if ( sortedExpressionLevels.get( gene ) > 0.0 )
-				Utils.log( gene + ": " + String.format( "%.2f", sortedExpressionLevels.get( gene ) ) );
-		}
-	}
-
-	public static synchronized void addRowToGeneExpressionTable(
-			double[] micrometerPosition,
-			double micrometerRadius,
-			Map< String, Double > expressionLevels )
-	{
-
-		final ArrayList< String > sortedGeneNames = Utils.getSortedList( expressionLevels.keySet() );
-
-		if ( table == null )
-		{
-			initGeneExpressionTable( sortedGeneNames );
-			showGeneExpressionTable();
-		}
-
-		final Double[] position = { micrometerPosition [ 0 ], micrometerPosition[ 1 ], micrometerPosition[ 2 ], 0.0 };
-		final Double[] parameters = { micrometerRadius };
-
-		final ArrayList< Double > sortedExpressionLevels = new ArrayList<>();
-		for ( String geneName : sortedGeneNames )
-			sortedExpressionLevels.add( expressionLevels.get( geneName ) );
-
-		model.addRow( combine( combine( position, parameters ), sortedExpressionLevels.toArray( new Double[ expressionLevels.size() ] )));
-	}
-
-	public static void showGeneExpressionTable()
-	{
-		JFrame frame = new JFrame("Gene Expression");
-
-		JPanel panel = new JPanel();
-		panel.setLayout(new BorderLayout());
-
-		final JMenuBar jMenuBar = new JMenuBar();
-		final JMenu menu = new JMenu( "File" );
-		final JMenuItem saveAs = new JMenuItem( "Save as..." );
-		saveAs.addActionListener( e -> SwingUtilities.invokeLater( () -> TableUIs.saveTableUI( table ) ) );
-		menu.add( saveAs );
-		jMenuBar.add( menu );
-		frame.setJMenuBar( jMenuBar );
-
-		JScrollPane tableContainer = new JScrollPane(
-				table,
-				JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
-				JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED );
-
-		table.setAutoResizeMode( JTable.AUTO_RESIZE_OFF );
-
-		panel.add(tableContainer, BorderLayout.CENTER);
-		frame.getContentPane().add(panel);
-
-		frame.pack();
-		frame.setVisible(true);
-	}
-
-	public static void initGeneExpressionTable( Collection< String > geneNames )
-	{
-		final String[] position = { "X", "Y", "Z", "T" };
-		final String[] searchParameters = { "SearchRadius_um" };
-
-		String[] genes = geneNames.toArray( new String[ geneNames.size() ] );
-
-		model = new DefaultTableModel();
-		model.setColumnIdentifiers( combine( combine( position, searchParameters ), genes )  );
-		table = new JTable( model );
-	}
-
-	public static String[] extractGeneNamesFromImageSourcesNames( String[] genes )
-	{
-		for ( int i = 0; i < genes.length; i++ )
-			genes[ i ] = Utils.getSimplifiedSourceName( genes[i ], true );
-
-		return genes;
-	}
-
-	public static ArrayList< String > extractGeneNamesFromImageSourcesNames( Collection< String > sourceNames )
-	{
-		final ArrayList< String > geneNames = new ArrayList<>();
-
-		for ( String sourceName : sourceNames )
-			geneNames.add( Utils.getSimplifiedSourceName( sourceName, true ));
-
-		return geneNames;
-	}
-
-}
diff --git a/src/main/java/de/embl/cba/mobie/plot/ScatterPlotViewer.java b/src/main/java/de/embl/cba/mobie/plot/ScatterPlotViewer.java
new file mode 100644
index 000000000..f0d472b79
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/plot/ScatterPlotViewer.java
@@ -0,0 +1,378 @@
+/*-
+ * #%L
+ * Various Java code for ImageJ
+ * %%
+ * Copyright (C) 2018 - 2021 EMBL
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+package de.embl.cba.mobie.plot;
+
+import bdv.util.BdvFunctions;
+import bdv.util.BdvHandle;
+import bdv.util.BdvOptions;
+import bdv.util.BdvStackSource;
+import bdv.util.Prefs;
+import bdv.viewer.TimePointListener;
+import de.embl.cba.bdv.utils.BdvUtils;
+import de.embl.cba.bdv.utils.popup.BdvPopupMenus;
+import de.embl.cba.mobie.Constants;
+import de.embl.cba.mobie.VisibilityListener;
+import de.embl.cba.tables.color.ColoringListener;
+import de.embl.cba.tables.color.ColoringModel;
+import de.embl.cba.tables.plot.RealPointARGBTypeBiConsumerSupplier;
+import de.embl.cba.tables.plot.ScatterPlotDialog;
+import de.embl.cba.tables.plot.TableRowKDTreeSupplier;
+import de.embl.cba.tables.select.SelectionListener;
+import de.embl.cba.tables.select.SelectionModel;
+import de.embl.cba.tables.tablerow.TableRow;
+import ij.IJ;
+import net.imglib2.FinalInterval;
+import net.imglib2.KDTree;
+import net.imglib2.RealPoint;
+import net.imglib2.neighborsearch.NearestNeighborSearchOnKDTree;
+import net.imglib2.position.FunctionRealRandomAccessible;
+import net.imglib2.type.numeric.ARGBType;
+import org.scijava.ui.behaviour.ClickBehaviour;
+import org.scijava.ui.behaviour.io.InputTriggerConfig;
+import org.scijava.ui.behaviour.util.Behaviours;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public class ScatterPlotViewer< T extends TableRow > implements SelectionListener< T >, ColoringListener, TimePointListener
+{
+	private final List< T > tableRows;
+	private final ColoringModel< T > coloringModel;
+	private final SelectionModel< T > selectionModel;
+
+	private String[] selectedColumns;
+	private double[] scaleFactors;
+	private double dotSizeScaleFactor;
+	private BdvHandle bdvHandle;
+	private Map< T, RealPoint > tableRowToRealPoint;
+	private T recentFocus;
+	private Window window;
+	private NearestNeighborSearchOnKDTree< T > search;
+	private BdvStackSource< ARGBType > scatterPlotSource;
+	private int currentTimepoint;
+	private List< VisibilityListener > listeners = new ArrayList<>(  );
+	private boolean showColumnSelectionUI = true;
+
+	public ScatterPlotViewer(
+			List< T > tableRows,
+			SelectionModel< T > selectionModel,
+			ColoringModel< T > coloringModel,
+			String[] selectedColumns,
+			double[] scaleFactors,
+			double dotSizeScaleFactor )
+	{
+		this.tableRows = tableRows;
+		this.coloringModel = coloringModel;
+		this.selectionModel = selectionModel;
+		this.selectedColumns = selectedColumns;
+		this.scaleFactors = scaleFactors;
+		this.dotSizeScaleFactor = dotSizeScaleFactor;
+		this.currentTimepoint = 0;
+	}
+
+	public void show()
+	{
+		if ( window == null )
+		{
+			if ( showColumnSelectionUI )
+			{
+				ScatterPlotDialog dialog = new ScatterPlotDialog( tableRows.get( 0 ).getColumnNames().stream().toArray( String[]::new ), selectedColumns, scaleFactors, dotSizeScaleFactor );
+
+				if ( dialog.show() )
+				{
+					selectedColumns = dialog.getSelectedColumns();
+					scaleFactors = dialog.getScaleFactors();
+					dotSizeScaleFactor = dialog.getDotSizeScaleFactor();
+				}
+			}
+
+			showColumnSelectionUI = false; // only show the first time
+
+			updateScatterPlotSource();
+			installBdvBehaviours();
+			configureWindow();
+		}
+		else
+		{
+			window.setVisible( true );
+		}
+	}
+
+	public List< VisibilityListener > getListeners()
+	{
+		return listeners;
+	}
+
+	private void updateScatterPlotSource( )
+	{
+		List< T > tableRows = getTableRows( );
+
+		TableRowKDTreeSupplier< T > kdTreeSupplier = new TableRowKDTreeSupplier<>( tableRows, selectedColumns, scaleFactors );
+		KDTree< T > kdTree = kdTreeSupplier.get();
+		double[] min = kdTreeSupplier.getMin();
+		double[] max = kdTreeSupplier.getMax();
+		tableRowToRealPoint = kdTreeSupplier.getTableRowToRealPoint();
+		search = new NearestNeighborSearchOnKDTree<>( kdTree );
+
+		double aspectRatio = ( max[ 1 ] - min[ 1 ] ) / ( max[ 0 ] - min[ 0 ] );
+		if ( aspectRatio > 10 || aspectRatio < 0.1 )
+		{
+			IJ.showMessage( "The aspect ratio, (yMax-yMin)/(xMax-xMin), of your data is " + aspectRatio + "." +
+					"\nIn order to see anything you may have to scale either the x or y values" +
+					"\nsuch that this ratio becomes closer to one." +
+					"\nYou can do so by right-clicking into the scatter plot" +
+					"\nand selecting \"Reconfigure...\"" );
+		}
+
+		Supplier< BiConsumer< RealPoint, ARGBType > > biConsumerSupplier = new RealPointARGBTypeBiConsumerSupplier<>( kdTree, coloringModel, dotSizeScaleFactor * ( min[ 0 ] - max[ 0 ] ) / 100.0, ARGBType.rgba( 100,  100, 100, 255 ) );
+
+		FunctionRealRandomAccessible< ARGBType > randomAccessible = new FunctionRealRandomAccessible( 2, biConsumerSupplier, ARGBType::new );
+
+		showInBdv( randomAccessible, FinalInterval.createMinMax( ( long ) min[ 0 ], ( long ) min[ 1 ], 0, ( long ) Math.ceil( max[ 0 ] ), ( long ) Math.ceil( max[ 1 ] ), 0 ), selectedColumns );
+	}
+
+	private List< T > getTableRows( )
+	{
+		if ( tableRows.get( 0 ).getColumnNames().contains( Constants.TIMEPOINT  ) )
+		{
+			return tableRows.stream().filter( t -> Double.parseDouble( t.getCell( Constants.TIMEPOINT ) ) == currentTimepoint ).collect( Collectors.toList() );
+		}
+		else
+		{
+			return tableRows;
+		}
+	}
+
+	private void configureWindow()
+	{
+		window = SwingUtilities.getWindowAncestor( bdvHandle.getViewerPanel() );
+		window.addWindowListener(
+			new WindowAdapter() {
+				public void windowClosing( WindowEvent ev) {
+					for ( VisibilityListener listener : listeners )
+					{
+						BdvPopupMenus.removePopupMenu( bdvHandle );
+						listener.visibility( false );
+						// The window needs to be recreated because
+						// BDV seems to close things...
+						window = null;
+						bdvHandle = null;
+					}
+				}
+			});
+	}
+
+	private void installBdvBehaviours( )
+	{
+		Behaviours behaviours = new Behaviours( new InputTriggerConfig() );
+		behaviours.install( bdvHandle.getTriggerbindings(), getBehavioursName() );
+		behaviours.getBehaviourMap().clear();
+		BdvPopupMenus.addAction( bdvHandle,"Focus closest point [Left-Click ]",
+				( x, y ) -> focusAndSelectClosestPoint( search, true )
+		);
+
+		behaviours.behaviour( ( ClickBehaviour ) ( x, y ) -> focusAndSelectClosestPoint( search, true ), "Focus closest point", "button1" ) ;
+
+		BdvPopupMenus.addAction( bdvHandle,"Select closest point [ Ctrl Left-Click ]",
+				( x, y ) -> focusAndSelectClosestPoint( search, false )
+		);
+
+		behaviours.behaviour( ( ClickBehaviour ) ( x, y ) -> focusAndSelectClosestPoint( search, false ), "Select closest point", "ctrl button1" ) ;
+
+		BdvPopupMenus.addAction( bdvHandle,"Reconfigure...",
+			( x, y ) -> {
+				SwingUtilities.invokeLater( () ->  {
+
+					ScatterPlotDialog dialog = new ScatterPlotDialog( tableRows.get( 0 ).getColumnNames().stream().toArray( String[]::new ), selectedColumns, scaleFactors, dotSizeScaleFactor );
+
+					if ( dialog.show() )
+					{
+						selectedColumns = dialog.getSelectedColumns();
+						scaleFactors = dialog.getScaleFactors();
+						dotSizeScaleFactor = dialog.getDotSizeScaleFactor();
+						if ( scatterPlotSource != null)
+							scatterPlotSource.removeFromBdv();
+						updateScatterPlotSource();
+					}
+				});
+			}
+		);
+	}
+
+	private String getBehavioursName()
+	{
+		return "scatterplot" + selectedColumns[ 0 ] + selectedColumns[ 1 ];
+	}
+
+	private synchronized void focusAndSelectClosestPoint( NearestNeighborSearchOnKDTree< T > search, boolean focusOnly )
+	{
+		final T selection = searchClosestPoint( search );
+
+		if ( selection != null )
+		{
+			if ( focusOnly )
+			{
+				recentFocus = selection;
+				selectionModel.focus( selection );
+			}
+			else
+			{
+				selectionModel.toggle( selection );
+				if ( selectionModel.isSelected( selection ) )
+				{
+					recentFocus = selection;
+					selectionModel.focus( selection );
+				}
+			}
+		}
+		else
+			throw new RuntimeException( "No closest point found." );
+	}
+
+	private T searchClosestPoint( NearestNeighborSearchOnKDTree< T > search )
+	{
+		final RealPoint realPoint = new RealPoint( 3 );
+		bdvHandle.getViewerPanel().getGlobalMouseCoordinates( realPoint );
+		RealPoint realPoint2d = new RealPoint( realPoint.getDoublePosition( 0 ), realPoint.getDoublePosition( 1 ) );
+		search.search( realPoint2d );
+		return search.getSampler().get();
+	}
+
+	private void showInBdv( FunctionRealRandomAccessible< ARGBType > randomAccessible, FinalInterval interval, String[] selectedColumns )
+	{
+		Prefs.showMultibox( false );
+		Prefs.showScaleBar( false );
+
+		final BdvOptions bdvOptions = BdvOptions.options().is2D().frameTitle( createPlotName( selectedColumns ) ).addTo( bdvHandle );
+
+		scatterPlotSource = BdvFunctions.show(
+				randomAccessible,
+				interval,
+				createPlotName( selectedColumns ),
+				bdvOptions );
+
+		bdvHandle = scatterPlotSource.getBdvHandle();
+	}
+
+	private static String createPlotName( String[] selectedColumns )
+	{
+		return "x: " + selectedColumns[ 0 ] + ", y: " + selectedColumns[ 1 ];
+	}
+
+	public String[] getSelectedColumns()
+	{
+		return selectedColumns;
+	}
+
+	public boolean isVisible() { return (window != null) && window.isVisible(); }
+
+	@Override
+	public void timePointChanged( int timepoint )
+	{
+		this.currentTimepoint = timepoint;
+		if ( window == null )
+			return;
+		if ( scatterPlotSource != null)
+			scatterPlotSource.removeFromBdv();
+		updateScatterPlotSource();
+	}
+
+	@Override
+	public void coloringChanged()
+	{
+		if ( bdvHandle == null ) return;
+
+		bdvHandle.getViewerPanel().requestRepaint();
+	}
+
+	@Override
+	public void selectionChanged()
+	{
+		if ( bdvHandle == null ) return;
+
+		bdvHandle.getViewerPanel().requestRepaint();
+	}
+
+	@Override
+	public void focusEvent( T selection )
+	{
+		if ( bdvHandle == null ) return;
+
+		if ( selection.getColumnNames().contains( Constants.TIMEPOINT  ) )
+		{
+			int selectedTimepoint = (int) Double.parseDouble( selection.getCell( Constants.TIMEPOINT ) );
+			if ( selectedTimepoint != currentTimepoint )
+			{
+				currentTimepoint = selectedTimepoint;
+				updateScatterPlotSource();
+			}
+		}
+
+		if ( selection == recentFocus )
+		{
+			return;
+		}
+		else
+		{
+			recentFocus = selection;
+			double[] location = new double[ 3 ];
+			tableRowToRealPoint.get( selection ).localize( location );
+			BdvUtils.moveToPosition( bdvHandle, location, 0, 500 );
+		}
+	}
+
+	public Window getWindow()
+	{
+		return window;
+	}
+
+	public void hide()
+	{
+		if ( window != null )
+			window.setVisible( false );
+	}
+
+	public void close()
+	{
+		if ( bdvHandle != null )
+		{
+			BdvPopupMenus.removePopupMenu( bdvHandle );
+			bdvHandle.close();
+			window = null;
+		}
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/projects/PublishedProject.java b/src/main/java/de/embl/cba/mobie/project/PublishedProject.java
similarity index 75%
rename from src/main/java/de/embl/cba/mobie/projects/PublishedProject.java
rename to src/main/java/de/embl/cba/mobie/project/PublishedProject.java
index f67a5ff39..8815ceacb 100644
--- a/src/main/java/de/embl/cba/mobie/projects/PublishedProject.java
+++ b/src/main/java/de/embl/cba/mobie/project/PublishedProject.java
@@ -1,4 +1,4 @@
-package de.embl.cba.mobie.projects;
+package de.embl.cba.mobie.project;
 
 public class PublishedProject
 {
diff --git a/src/main/java/de/embl/cba/mobie/projects/PublishedProjectsCreator.java b/src/main/java/de/embl/cba/mobie/project/PublishedProjectsCreator.java
similarity index 92%
rename from src/main/java/de/embl/cba/mobie/projects/PublishedProjectsCreator.java
rename to src/main/java/de/embl/cba/mobie/project/PublishedProjectsCreator.java
index b7bea9bee..505293347 100644
--- a/src/main/java/de/embl/cba/mobie/projects/PublishedProjectsCreator.java
+++ b/src/main/java/de/embl/cba/mobie/project/PublishedProjectsCreator.java
@@ -1,4 +1,4 @@
-package de.embl.cba.mobie.projects;
+package de.embl.cba.mobie.project;
 
 import java.util.HashMap;
 
@@ -12,7 +12,7 @@ public PublishedProjectsCreator()
 
 		final PublishedProject platybrowserProject = new PublishedProject();
 		platybrowserProject.name = "Vergara et al. (2020) \"PlatyBrowser\", bioRxiv";
-		platybrowserProject.location = "https://github.com/platybrowser/platybrowser";
+		platybrowserProject.location = "https://github.com/mobie/platybrowser-datasets";
 		platybrowserProject.pulicationURL = "https://www.biorxiv.org/content/10.1101/2020.02.26.961037v1";
 
 		publishedProjects.put( platybrowserProject.name, platybrowserProject );
diff --git a/src/main/java/de/embl/cba/mobie/projectcreator/DatasetJsonCreator.java b/src/main/java/de/embl/cba/mobie/projectcreator/DatasetJsonCreator.java
new file mode 100644
index 000000000..c6de90271
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/projectcreator/DatasetJsonCreator.java
@@ -0,0 +1,144 @@
+package de.embl.cba.mobie.projectcreator;
+
+import de.embl.cba.mobie.Constants;
+import de.embl.cba.mobie.Dataset;
+import de.embl.cba.mobie.display.ImageSourceDisplay;
+import de.embl.cba.mobie.display.SegmentationSourceDisplay;
+import de.embl.cba.mobie.display.SourceDisplay;
+import de.embl.cba.mobie.serialize.DatasetJsonParser;
+import de.embl.cba.mobie.source.ImageSource;
+import de.embl.cba.mobie.source.SegmentationSource;
+import de.embl.cba.mobie.source.SourceSupplier;
+import de.embl.cba.mobie.view.View;
+import de.embl.cba.tables.FileAndUrlUtils;
+import de.embl.cba.tables.color.ColoringLuts;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+public class DatasetJsonCreator {
+
+    ProjectCreator projectCreator;
+
+    public DatasetJsonCreator( ProjectCreator projectCreator ) {
+        this.projectCreator = projectCreator;
+    }
+
+    public void addToDatasetJson( String imageName, String datasetName, ProjectCreator.ImageType imageType,
+                                  String uiSelectionGroup, boolean is2D, int nTimepoints ) {
+        Dataset dataset = projectCreator.getDataset( datasetName );
+        if ( dataset == null ) {
+            dataset = new Dataset();
+            dataset.sources = new HashMap<>();
+            dataset.views = new HashMap<>();
+            // start new datasets with is2D as true, then for the first 3D image added it can be set to false
+            dataset.is2D = true;
+        }
+
+        if ( !is2D ) {
+            dataset.is2D = false;
+        }
+
+        if ( nTimepoints > dataset.timepoints ) {
+            dataset.timepoints = nTimepoints;
+        }
+
+        addNewSource( dataset, imageName, imageType );
+        if ( uiSelectionGroup != null ) {
+            // add a view with the same name as the image, and sensible defaults
+            addNewImageView( dataset, imageName, imageType, uiSelectionGroup );
+        }
+
+        // if there is no default view, make one with this image and sensible defaults
+        if ( !dataset.views.containsKey("default")) {
+            addNewDefaultView( dataset, imageName, imageType );
+        }
+
+        writeDatasetJson( datasetName, dataset );
+    }
+
+    private void addNewSource( Dataset dataset, String imageName, ProjectCreator.ImageType imageType ) {
+        Map<String, String> imageDataLocations;
+        SourceSupplier sourceSupplier;
+
+        switch( imageType ) {
+            case segmentation:
+                SegmentationSource segmentationSource = new SegmentationSource();
+                segmentationSource.tableDataLocation = "tables/" + imageName;
+                imageDataLocations = new HashMap<>();
+                imageDataLocations.put( "fileSystem", "images/local/" + imageName + ".xml" );
+                segmentationSource.imageDataLocations = imageDataLocations;
+
+                sourceSupplier = new SourceSupplier( segmentationSource );
+
+                dataset.sources.put( imageName, sourceSupplier );
+                break;
+
+            case image:
+                ImageSource imageSource = new ImageSource();
+                imageDataLocations = new HashMap<>();
+                imageDataLocations.put( "fileSystem", "images/local/" + imageName + ".xml" );
+                imageSource.imageDataLocations = imageDataLocations;
+
+                sourceSupplier = new SourceSupplier( imageSource );
+                dataset.sources.put( imageName, sourceSupplier );
+                break;
+        }
+    }
+
+    private void addNewImageView( Dataset dataset, String imageName, ProjectCreator.ImageType imageType,
+                            String uiSelectionGroup ) {
+
+        View view = createView( imageName, imageType, uiSelectionGroup, false );
+        dataset.views.put( imageName, view );
+    }
+
+    private void addNewDefaultView( Dataset dataset, String imageName, ProjectCreator.ImageType imageType ) {
+
+        View view = createView( imageName, imageType, "views", true );
+        dataset.views.put( "default", view );
+    }
+
+    private View createView( String imageName, ProjectCreator.ImageType imageType, String uiSelectionGroup,
+                             boolean isExclusive ) {
+        ArrayList<SourceDisplay> sourceDisplays = new ArrayList<>();
+        ArrayList<String> sources = new ArrayList<>();
+        sources.add( imageName );
+        switch( imageType ) {
+            case segmentation:
+                SegmentationSourceDisplay segmentationSourceDisplay = new SegmentationSourceDisplay(
+                        imageName, 0.5, sources, ColoringLuts.GLASBEY,
+                        null, null, null, false,
+                        false, new String[]{ Constants.ANCHOR_X, Constants.ANCHOR_Y }, null );
+                sourceDisplays.add( segmentationSourceDisplay );
+                break;
+            case image:
+                ImageSourceDisplay imageSourceDisplay = new ImageSourceDisplay( imageName, 1.0, sources,
+                        "white", new double[] {0.0, 255.0}, null, false );
+                sourceDisplays.add( imageSourceDisplay );
+                break;
+        }
+
+        View view = new View( uiSelectionGroup, sourceDisplays, null, null, isExclusive );
+        return view;
+    }
+
+    public void writeDatasetJson ( String datasetName, Dataset dataset ) {
+        try {
+            String datasetJsonPath = FileAndUrlUtils.combinePath( projectCreator.getDataLocation().getAbsolutePath(),
+                    datasetName, "dataset.json" );
+            new DatasetJsonParser().saveDataset( dataset, datasetJsonPath );
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        // whether the dataset json saving succeeded or not, we reload the current dataset
+        try {
+            projectCreator.reloadCurrentDataset();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/src/main/java/de/embl/cba/mobie/projectcreator/DatasetsCreator.java b/src/main/java/de/embl/cba/mobie/projectcreator/DatasetsCreator.java
new file mode 100644
index 000000000..63afcf746
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/projectcreator/DatasetsCreator.java
@@ -0,0 +1,110 @@
+package de.embl.cba.mobie.projectcreator;
+
+import de.embl.cba.mobie.serialize.ProjectJsonParser;
+import de.embl.cba.tables.FileAndUrlUtils;
+import ij.IJ;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+public class DatasetsCreator {
+
+    private ProjectCreator projectCreator;
+
+    public DatasetsCreator(ProjectCreator projectCreator ) {
+        this.projectCreator = projectCreator;
+    }
+
+    public void addDataset ( String datasetName ) {
+        List<String> currentDatasets = projectCreator.getProject().getDatasets();
+        boolean contains = currentDatasets.contains(datasetName);
+        if ( !contains ) {
+            File datasetDir = new File ( FileAndUrlUtils.combinePath( projectCreator.getDataLocation().getAbsolutePath(),
+                    datasetName ) );
+
+            if ( !datasetDir.exists() ) {
+                datasetDir.mkdirs();
+
+                // make rest of folders required under dataset
+                new File(datasetDir, "images").mkdirs();
+                new File(datasetDir, "misc").mkdirs();
+                new File(datasetDir, "tables").mkdirs();
+                new File(FileAndUrlUtils.combinePath(datasetDir.getAbsolutePath(), "images", "local")).mkdirs();
+                new File(FileAndUrlUtils.combinePath(datasetDir.getAbsolutePath(), "images", "remote")).mkdirs();
+                new File(FileAndUrlUtils.combinePath(datasetDir.getAbsolutePath(), "misc", "views")).mkdirs();
+
+
+                // if this is the first dataset, then make this the default
+                if ( currentDatasets.size() == 0 ) {
+                    projectCreator.getProject().setDefaultDataset( datasetName );
+                }
+                currentDatasets.add( datasetName );
+                writeProjectJson();
+            } else {
+                IJ.log( "Dataset creation failed - this name already exists" );
+            }
+        } else {
+            IJ.log("Add dataset failed - dataset already exists");
+        }
+    }
+
+    public void renameDataset( String oldName, String newName ) {
+
+        if ( !newName.equals(oldName) ) {
+            // check not already in datasets
+            List<String> currentDatasets = projectCreator.getProject().getDatasets();
+            boolean contains = currentDatasets.contains( newName );
+            if ( !contains ) {
+                File oldDatasetDir = new File( FileAndUrlUtils.combinePath(
+                        projectCreator.getDataLocation().getAbsolutePath(), oldName) );
+                File newDatasetDir = new File( FileAndUrlUtils.combinePath(
+                        projectCreator.getDataLocation().getAbsolutePath(), newName ));
+
+                if (oldDatasetDir.exists()) {
+                    if (oldDatasetDir.renameTo(newDatasetDir)) {
+
+                        // update json
+                        if ( projectCreator.getProject().getDefaultDataset().equals(oldName) ) {
+                            projectCreator.getProject().setDefaultDataset( newName );
+                        }
+
+                        int indexOld = projectCreator.getProject().getDatasets().indexOf(oldName);
+                        projectCreator.getProject().getDatasets().set(indexOld, newName);
+
+                        writeProjectJson();
+
+                    } else {
+                        IJ.log("Rename directory failed");
+                    }
+                } else {
+                    IJ.log("Rename dataset failed - that dataset doesn't exist");
+                }
+            } else {
+                IJ.log( "Rename dataset failed - there is already a dataset of that name" );
+            }
+        }
+
+    }
+
+    public void makeDefaultDataset ( String datasetName ) {
+        projectCreator.getProject().setDefaultDataset( datasetName );
+        writeProjectJson();
+    }
+
+    private void writeProjectJson() {
+        try {
+            new ProjectJsonParser().saveProject( projectCreator.getProject(), projectCreator.getProjectJson().getAbsolutePath() );
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        // whether the project writing succeeded or not, we now read the current state of the project
+        try {
+            projectCreator.reloadProject();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+}
diff --git a/src/main/java/de/embl/cba/mobie/projectcreator/ImagesCreator.java b/src/main/java/de/embl/cba/mobie/projectcreator/ImagesCreator.java
new file mode 100644
index 000000000..fc662586c
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/projectcreator/ImagesCreator.java
@@ -0,0 +1,382 @@
+package de.embl.cba.mobie.projectcreator;
+
+import bdv.img.n5.N5ImageLoader;
+import bdv.spimdata.SequenceDescriptionMinimal;
+import bdv.spimdata.SpimDataMinimal;
+import bdv.spimdata.XmlIoSpimDataMinimal;
+import de.embl.cba.bdv.utils.sources.LazySpimSource;
+import de.embl.cba.mobie.projectcreator.n5.DownsampleBlock;
+import de.embl.cba.mobie.projectcreator.n5.WriteImgPlusToN5;
+import de.embl.cba.mobie.projectcreator.ui.ManualN5ExportPanel;
+import de.embl.cba.tables.FileAndUrlUtils;
+import de.embl.cba.tables.Tables;
+import ij.IJ;
+import ij.ImagePlus;
+import mpicbg.spim.data.SpimDataException;
+import mpicbg.spim.data.generic.base.Entity;
+import mpicbg.spim.data.generic.sequence.BasicImgLoader;
+import mpicbg.spim.data.generic.sequence.BasicViewSetup;
+import mpicbg.spim.data.registration.ViewRegistration;
+import mpicbg.spim.data.registration.ViewRegistrations;
+import mpicbg.spim.data.sequence.*;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.converter.RealTypeConverters;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.roi.labeling.ImgLabeling;
+import net.imglib2.roi.labeling.LabelRegion;
+import net.imglib2.roi.labeling.LabelRegions;
+import net.imglib2.type.numeric.integer.IntType;
+import net.imglib2.type.numeric.real.FloatType;
+import org.apache.commons.compress.utils.FileNameUtils;
+import org.apache.commons.io.FileUtils;
+import org.janelia.saalfeldlab.n5.GzipCompression;
+
+import javax.swing.*;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import static de.embl.cba.mobie.projectcreator.ProjectCreatorHelper.*;
+import static de.embl.cba.morphometry.Utils.labelMapAsImgLabeling;
+import static net.imglib2.util.Util.getTypeFromInterval;
+
+public class ImagesCreator {
+
+    ProjectCreator projectCreator;
+
+    public ImagesCreator( ProjectCreator projectCreator ) {
+        this.projectCreator = projectCreator;
+    }
+
+    private String getDefaultLocalImageXmlPath( String datasetName, String imageName ) {
+        return FileAndUrlUtils.combinePath(projectCreator.getDataLocation().getAbsolutePath(), datasetName,
+                "images", "local", imageName + ".xml");
+    }
+
+    private String getDefaultLocalImageDirPath( String datasetName ) {
+        return FileAndUrlUtils.combinePath(projectCreator.getDataLocation().getAbsolutePath(), datasetName,
+                "images", "local" );
+    }
+
+    private String getDefaultTableDirPath( String datasetName, String imageName ) {
+        return FileAndUrlUtils.combinePath( projectCreator.getDataLocation().getAbsolutePath(), datasetName, "tables", imageName );
+    }
+
+    public void addImage ( ImagePlus imp, String imageName, String datasetName,
+                          ProjectCreator.BdvFormat bdvFormat, ProjectCreator.ImageType imageType,
+                          AffineTransform3D sourceTransform, boolean useDefaultSettings, String uiSelectionGroup ) {
+        String xmlPath = getDefaultLocalImageXmlPath( datasetName, imageName );
+        File xmlFile = new File( xmlPath );
+
+        DownsampleBlock.DownsamplingMethod downsamplingMethod;
+        switch( imageType ) {
+            case image:
+                downsamplingMethod = DownsampleBlock.DownsamplingMethod.Average;
+                break;
+            default:
+                downsamplingMethod = DownsampleBlock.DownsamplingMethod.Centre;
+        }
+
+        if ( !xmlFile.exists() ) {
+            switch( bdvFormat ) {
+                case n5:
+                    if (!useDefaultSettings) {
+                        new ManualN5ExportPanel(imp, xmlPath, sourceTransform, downsamplingMethod, imageName).getManualExportParameters();
+                    } else {
+                        // gzip compression by default
+                        new WriteImgPlusToN5().export(imp, xmlPath, sourceTransform, downsamplingMethod,
+                                new GzipCompression(), new String[]{imageName} );
+                    }
+            }
+
+            // check image written successfully, before writing jsons
+            if ( xmlFile.exists() ) {
+                boolean is2D;
+                if ( imp.getNDimensions() <= 2 ) {
+                    is2D = true;
+                } else {
+                    is2D = false;
+                }
+                try {
+                    updateTableAndJsonsForNewImage( imageName, imageType, datasetName, uiSelectionGroup, is2D, imp.getNFrames() );
+                } catch (SpimDataException e) {
+                    e.printStackTrace();
+                }
+            }
+        } else {
+            IJ.log( "Adding image to project failed - this image name already exists" );
+        }
+    }
+
+    public void addBdvFormatImage ( File xmlLocation, String datasetName, ProjectCreator.ImageType imageType,
+                                   ProjectCreator.AddMethod addMethod, String uiSelectionGroup ) throws SpimDataException, IOException {
+        if ( xmlLocation.exists() ) {
+            SpimDataMinimal spimDataMinimal = new XmlIoSpimDataMinimal().load(xmlLocation.getAbsolutePath());
+            String imageName = FileNameUtils.getBaseName(xmlLocation.getAbsolutePath());
+            File newXmlDirectory = new File( getDefaultLocalImageDirPath( datasetName ));
+            File newXmlFile = new File(newXmlDirectory, imageName + ".xml");
+
+            if ( !newXmlFile.exists() ) {
+                // check n5 format (e.g. we no longer support hdf5)
+                ProjectCreator.BdvFormat bdvFormat = getBdvFormatFromSpimDataMinimal( spimDataMinimal );
+                if ( bdvFormat != null ) {
+                    // The view setup name must be the same as the image name
+                    spimDataMinimal = fixSetupName( spimDataMinimal, imageName );
+
+                    switch (addMethod) {
+                        case link:
+                            new XmlIoSpimDataMinimal().save(spimDataMinimal, newXmlFile.getAbsolutePath());
+                            break;
+                        case copy:
+                            copyImage(bdvFormat, spimDataMinimal, newXmlDirectory, imageName);
+                            break;
+                        case move:
+                            moveImage(bdvFormat, spimDataMinimal, newXmlDirectory, imageName);
+                            break;
+                    }
+                    updateTableAndJsonsForNewImage( imageName, imageType, datasetName, uiSelectionGroup,
+                            isSpimData2D( spimDataMinimal ), getNTimepointsFromSpimData( spimDataMinimal ) );
+                } else {
+                    IJ.log( "Image is of unsupported type. Must be n5.");
+                }
+            } else {
+                IJ.log("Adding image to project failed - this image name already exists");
+            }
+        } else {
+            IJ.log( "Adding image to project failed - xml does not exist" );
+        }
+    }
+
+    private ArrayList<Object[]> makeDefaultTableRowsForTimepoint( LazySpimSource labelsSource, int timepoint, boolean addTimepointColumn ) {
+
+        RandomAccessibleInterval rai = labelsSource.getNonVolatileSource( timepoint, 0 );
+        if ( getTypeFromInterval( rai ) instanceof FloatType ) {
+            rai = RealTypeConverters.convert( rai, new IntType() );
+        }
+
+        double[] dimensions = new double[ rai.numDimensions() ];
+        labelsSource.getVoxelDimensions().dimensions( dimensions );
+
+        ImgLabeling<Integer, IntType> imgLabeling = labelMapAsImgLabeling(rai);
+
+        LabelRegions labelRegions = new LabelRegions(imgLabeling);
+        Iterator<LabelRegion> labelRegionIterator = labelRegions.iterator();
+
+        ArrayList<Object[]> rows = new ArrayList<>();
+        int nColumns = 10;
+        if ( addTimepointColumn ) {
+            nColumns += 1;
+        }
+        while (labelRegionIterator.hasNext()) {
+            Object[] row = new Object[ nColumns ];
+            LabelRegion labelRegion = labelRegionIterator.next();
+
+            double[] centre = new double[rai.numDimensions()];
+            labelRegion.getCenterOfMass().localize(centre);
+            double[] bbMin = new double[rai.numDimensions()];
+            double[] bbMax = new double[rai.numDimensions()];
+            labelRegion.realMin(bbMin);
+            labelRegion.realMax(bbMax);
+
+            row[0] = labelRegion.getLabel();
+            row[1] = centre[0] * dimensions[0];
+            row[2] = centre[1] * dimensions[1];
+            row[3] = centre[2] * dimensions[2];
+            row[4] = bbMin[0] * dimensions[0];
+            row[5] = bbMin[1] * dimensions[1];
+            row[6] = bbMin[2] * dimensions[2];
+            row[7] = bbMax[0] * dimensions[0];
+            row[8] = bbMax[1] * dimensions[1];
+            row[9] = bbMax[2] * dimensions[2];
+
+            if ( addTimepointColumn ) {
+                row[10] = timepoint;
+            }
+
+            rows.add(row);
+        }
+
+        return rows;
+    }
+
+    // TODO - is this efficient for big images?
+    private void addDefaultTableForImage ( String imageName, String datasetName ) throws SpimDataException {
+        File tableFolder = new File( getDefaultTableDirPath( datasetName, imageName ) );
+        File defaultTable = new File( tableFolder, "default.tsv" );
+        if ( !tableFolder.exists() ){
+            tableFolder.mkdirs();
+        }
+
+        if ( !defaultTable.exists() ) {
+
+            IJ.log( " Creating default table... 0 label is counted as background" );
+
+            SpimDataMinimal spimDataMinimal = new XmlIoSpimDataMinimal().load(getDefaultLocalImageXmlPath(datasetName, imageName));
+
+            boolean hasTimeColumn = spimDataMinimal.getSequenceDescription().getTimePoints().size() > 1;
+            ArrayList<String> columnNames = new ArrayList<>();
+            columnNames.add( "label_id" );
+            columnNames.add( "anchor_x" );
+            columnNames.add( "anchor_y" );
+            columnNames.add( "anchor_z" );
+            columnNames.add( "bb_min_x" );
+            columnNames.add( "bb_min_y" );
+            columnNames.add( "bb_min_z" );
+            columnNames.add( "bb_max_x" );
+            columnNames.add( "bb_max_y" );
+            columnNames.add( "bb_max_z" );
+            if ( hasTimeColumn ) {
+                columnNames.add("timepoint");
+            }
+
+            final LazySpimSource labelsSource = new LazySpimSource("labelImage",
+                    getDefaultLocalImageXmlPath(datasetName, imageName));
+            ArrayList<Object[]> rows = new ArrayList<>();
+
+            for ( Integer timepoint: spimDataMinimal.getSequenceDescription().getTimePoints().getTimePoints().keySet() ) {
+                rows.addAll( makeDefaultTableRowsForTimepoint( labelsSource, timepoint, hasTimeColumn ) );
+            }
+
+            Object[][] rowArray = new Object[rows.size()][columnNames.size()];
+            rowArray = rows.toArray(rowArray);
+
+            JTable table = new JTable(rowArray, columnNames.toArray() );
+            Tables.saveTable( table, defaultTable );
+
+            IJ.log( "Default table complete" );
+        }
+    }
+
+    private void updateTableAndJsonsForNewImage ( String imageName, ProjectCreator.ImageType imageType,
+                                          String datasetName, String uiSelectionGroup, boolean is2D, int nTimepoints ) throws SpimDataException {
+        if ( imageType == ProjectCreator.ImageType.segmentation) {
+            addDefaultTableForImage( imageName, datasetName );
+        }
+        DatasetJsonCreator datasetJsonCreator = projectCreator.getDatasetJsonCreator();
+        datasetJsonCreator.addToDatasetJson( imageName, datasetName, imageType, uiSelectionGroup, is2D, nTimepoints );
+    }
+
+    private void copyImage ( ProjectCreator.BdvFormat bdvFormat, SpimDataMinimal spimDataMinimal, File newXmlDirectory, String imageName ) throws IOException, SpimDataException {
+        File newImageFile = new File( newXmlDirectory, imageName + "." + bdvFormat );
+        File imageLocation = getImageLocationFromSpimDataMinimal( spimDataMinimal, bdvFormat );
+
+        switch ( bdvFormat ) {
+            case n5:
+                FileUtils.copyDirectory(imageLocation, newImageFile );
+                break;
+        }
+
+        writeNewBdvXml( spimDataMinimal, newImageFile, newXmlDirectory, imageName, bdvFormat);
+    }
+
+    private void moveImage ( ProjectCreator.BdvFormat bdvFormat, SpimDataMinimal spimDataMinimal, File newXmlDirectory, String imageName ) throws IOException, SpimDataException {
+        File newImageFile = new File( newXmlDirectory, imageName + "." + bdvFormat );
+        File imageLocation = getImageLocationFromSpimDataMinimal( spimDataMinimal, bdvFormat );
+
+        // have to explicitly close the image loader, so we can delete the original file
+        closeImgLoader( spimDataMinimal, bdvFormat );
+
+        switch ( bdvFormat ) {
+            case n5:
+                FileUtils.moveDirectory( imageLocation, newImageFile );
+                break;
+        }
+
+        writeNewBdvXml( spimDataMinimal, newImageFile, newXmlDirectory, imageName, bdvFormat );
+    }
+
+    private void closeImgLoader ( SpimDataMinimal spimDataMinimal, ProjectCreator.BdvFormat bdvFormat ) {
+        BasicImgLoader imgLoader = spimDataMinimal.getSequenceDescription().getImgLoader();
+
+        switch ( bdvFormat ) {
+            case n5:
+                N5ImageLoader n5ImageLoader = (N5ImageLoader) imgLoader;
+                n5ImageLoader.close();
+                break;
+        }
+    }
+
+    private SpimDataMinimal fixSetupName( SpimDataMinimal spimDataMinimal, String imageName ) {
+        // The view setup name must be the same as the image name
+        BasicViewSetup firstSetup = spimDataMinimal.getSequenceDescription().getViewSetupsOrdered().get(0);
+        if ( !firstSetup.getName().equals(imageName) ) {
+
+            int numSetups = spimDataMinimal.getSequenceDescription().getViewSetups().size();
+            final HashMap< Integer, BasicViewSetup> setups = new HashMap<>( numSetups );
+            for ( int s = 0; s < numSetups; s++ )
+            {
+                final BasicViewSetup setup;
+                if ( s == 0 ) {
+                    setup = new BasicViewSetup( firstSetup.getId(), imageName,
+                            firstSetup.getSize(), firstSetup.getVoxelSize() );
+                    for ( Entity attribute: firstSetup.getAttributes().values() ) {
+                        setup.setAttribute( attribute );
+                    }
+                } else {
+                    setup = spimDataMinimal.getSequenceDescription().getViewSetupsOrdered().get( s );
+                }
+                setups.put( s, setup );
+            }
+
+            final SequenceDescriptionMinimal newSeq = new SequenceDescriptionMinimal(
+                    spimDataMinimal.getSequenceDescription().getTimePoints(), setups,
+                    spimDataMinimal.getSequenceDescription().getImgLoader(), null );
+
+            return new SpimDataMinimal( spimDataMinimal.getBasePath(), newSeq, spimDataMinimal.getViewRegistrations() );
+        } else {
+            return spimDataMinimal;
+        }
+    }
+
+    private void writeNewBdvXml ( SpimDataMinimal spimDataMinimal, File imageFile, File saveDirectory, String imageName,
+                                  ProjectCreator.BdvFormat bdvFormat ) throws SpimDataException {
+
+        ImgLoader imgLoader = null;
+        switch ( bdvFormat ) {
+            case n5:
+                imgLoader = new N5ImageLoader( imageFile, null);
+                break;
+        }
+
+        spimDataMinimal.setBasePath( saveDirectory );
+        spimDataMinimal.getSequenceDescription().setImgLoader(imgLoader);
+        new XmlIoSpimDataMinimal().save(spimDataMinimal, new File( saveDirectory, imageName + ".xml").getAbsolutePath() );
+    }
+
+    private void addAffineTransformToXml ( String xmlPath, String affineTransform )  {
+        if ( affineTransform != null ) {
+            try {
+                SpimDataMinimal spimDataMinimal = new XmlIoSpimDataMinimal().load( xmlPath );
+                int numTimepoints = spimDataMinimal.getSequenceDescription().getTimePoints().size();
+                int numSetups = spimDataMinimal.getSequenceDescription().getViewSetupsOrdered().size();
+
+                AffineTransform3D sourceTransform = new AffineTransform3D();
+                String[] splitAffineTransform = affineTransform.split(" ");
+                double[] doubleAffineTransform = new double[splitAffineTransform.length];
+                for ( int i = 0; i < splitAffineTransform.length; i++ ) {
+                    doubleAffineTransform[i] = Double.parseDouble( splitAffineTransform[i] );
+                }
+                sourceTransform.set( doubleAffineTransform );
+
+                final ArrayList<ViewRegistration> registrations = new ArrayList<>();
+                for ( int t = 0; t < numTimepoints; ++t ) {
+                    for (int s = 0; s < numSetups; ++s) {
+                        registrations.add(new ViewRegistration(t, s, sourceTransform));
+                    }
+                }
+
+                SpimDataMinimal updatedSpimDataMinimial = new SpimDataMinimal(spimDataMinimal.getBasePath(),
+                        spimDataMinimal.getSequenceDescription(), new ViewRegistrations( registrations) );
+
+                new XmlIoSpimDataMinimal().save( updatedSpimDataMinimial, xmlPath);
+            } catch (SpimDataException e) {
+                IJ.log( "Error adding affine transform to xml file. Check xml manually.");
+                e.printStackTrace();
+            }
+
+        }
+    }
+
+}
diff --git a/src/main/java/de/embl/cba/mobie/projectcreator/ProjectCreator.java b/src/main/java/de/embl/cba/mobie/projectcreator/ProjectCreator.java
new file mode 100644
index 000000000..39d13f975
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/projectcreator/ProjectCreator.java
@@ -0,0 +1,148 @@
+package de.embl.cba.mobie.projectcreator;
+
+import de.embl.cba.mobie.Dataset;
+import de.embl.cba.mobie.Project;
+import de.embl.cba.mobie.serialize.DatasetJsonParser;
+import de.embl.cba.mobie.serialize.ProjectJsonParser;
+import de.embl.cba.tables.FileAndUrlUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+
+import static de.embl.cba.mobie.projectcreator.ProjectCreatorHelper.getGroupToViewsMap;
+
+public class ProjectCreator {
+
+    private final File dataLocation;
+
+    private final File projectJson;
+    private Project project;
+
+    private File currentDatasetJson;
+    private String currentDatasetName;
+    private Dataset currentDataset;
+    private Map<String, ArrayList<String> > currentGrouptoViews;
+
+    private final DatasetsCreator datasetsCreator;
+    private final ImagesCreator imagesCreator;
+    private final DatasetJsonCreator datasetJsonCreator;
+    private final RemoteMetadataCreator remoteMetadataCreator;
+
+    public enum BdvFormat {
+        // TODO - add OME.ZARR
+        n5
+    }
+
+    public enum ImageType {
+        image,
+        segmentation
+    }
+
+    public enum AddMethod {
+        link,
+        copy,
+        move
+    }
+
+    // data location is the folder that contains the projects.json and the individual dataset folders
+    public ProjectCreator(File dataLocation ) throws IOException {
+        this.dataLocation = dataLocation;
+        projectJson = new File( FileAndUrlUtils.combinePath(  dataLocation.getAbsolutePath(), "project.json") );
+        if ( projectJson.exists() ) {
+            reloadProject();
+        } else {
+            this.project = new Project();
+            project.setDatasets( new ArrayList<>() );
+            project.setSpecVersion( "0.2.0" );
+        }
+
+        this.datasetsCreator = new DatasetsCreator( this );
+        this.datasetJsonCreator = new DatasetJsonCreator( this );
+        this.imagesCreator = new ImagesCreator( this );
+        this.remoteMetadataCreator = new RemoteMetadataCreator( this );
+    }
+
+    public File getDataLocation() { return dataLocation; }
+
+    public Project getProject() {
+        return project;
+    }
+
+    public File getProjectJson() { return projectJson; }
+
+    public void reloadProject() throws IOException {
+        this.project = new ProjectJsonParser().parseProject( projectJson.getAbsolutePath() );
+    }
+
+    public Dataset getDataset( String datasetName ) {
+        if ( datasetName.equals(currentDatasetName) ) {
+            return currentDataset;
+        } else {
+            File datasetJson = new File( FileAndUrlUtils.combinePath( dataLocation.getAbsolutePath(), datasetName, "dataset.json") );
+            if ( datasetJson.exists() ) {
+                try {
+                    currentDatasetJson = datasetJson;
+                    currentDatasetName = datasetName;
+                    reloadCurrentDataset();
+                    return currentDataset;
+                } catch (IOException e) {
+                    e.printStackTrace();
+                    return null;
+                }
+            } else {
+                return null;
+            }
+        }
+    }
+
+    public String[] getGroups( String datasetName ) {
+        if ( !datasetName.equals(currentDatasetName) ) {
+            getDataset( datasetName );
+        }
+
+        String[] groups = null;
+        if ( currentGrouptoViews != null && currentGrouptoViews.keySet().size() > 0 ) {
+            groups = new String[currentGrouptoViews.keySet().size()];
+            currentGrouptoViews.keySet().toArray(groups);
+        }
+        return groups;
+    }
+
+    public String[] getViews( String datasetName, String uiSelectionGroup ) {
+        if ( !datasetName.equals(currentDatasetName) ) {
+            getDataset( datasetName );
+        }
+
+        String[] views = null;
+        if ( currentGrouptoViews != null && currentGrouptoViews.keySet().size() > 0 ) {
+            views = currentGrouptoViews.get(uiSelectionGroup).toArray(new String[0]);
+        }
+
+        return views;
+    }
+
+    public void reloadCurrentDataset() throws IOException {
+        if ( currentDatasetName != null ) {
+            this.currentDataset = new DatasetJsonParser().parseDataset(currentDatasetJson.getAbsolutePath());
+            this.currentGrouptoViews = getGroupToViewsMap(this.currentDataset);
+        }
+    }
+
+    public DatasetsCreator getDatasetsCreator() {
+        return datasetsCreator;
+    }
+
+    public ImagesCreator getImagesCreator() {
+        return imagesCreator;
+    }
+
+    public DatasetJsonCreator getDatasetJsonCreator() {
+        return datasetJsonCreator;
+    }
+
+    public RemoteMetadataCreator getRemoteMetadataCreator() {
+        return remoteMetadataCreator;
+    }
+}
diff --git a/src/main/java/de/embl/cba/mobie/projectcreator/ProjectCreatorHelper.java b/src/main/java/de/embl/cba/mobie/projectcreator/ProjectCreatorHelper.java
new file mode 100644
index 000000000..c0079cb38
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/projectcreator/ProjectCreatorHelper.java
@@ -0,0 +1,251 @@
+package de.embl.cba.mobie.projectcreator;
+
+import bdv.img.n5.N5ImageLoader;
+import bdv.spimdata.SpimDataMinimal;
+import de.embl.cba.mobie.n5.N5FSImageLoader;
+import de.embl.cba.mobie.n5.N5S3ImageLoader;
+import de.embl.cba.mobie.Dataset;
+import de.embl.cba.mobie.view.View;
+import de.embl.cba.mobie.view.additionalviews.AdditionalViews;
+import de.embl.cba.tables.FileAndUrlUtils;
+import ij.IJ;
+import ij.ImagePlus;
+import ij.gui.GenericDialog;
+import mpicbg.spim.data.generic.sequence.BasicImgLoader;
+import mpicbg.spim.data.generic.sequence.BasicViewSetup;
+import mpicbg.spim.data.sequence.FinalVoxelDimensions;
+import net.imglib2.FinalDimensions;
+import net.imglib2.realtransform.AffineTransform3D;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import static de.embl.cba.mobie.ui.UserInterfaceHelper.tidyString;
+
+public class ProjectCreatorHelper {
+    public static boolean isImageSuitable(ImagePlus imp) {
+        // check the image type
+        switch (imp.getType()) {
+            case ImagePlus.GRAY8:
+            case ImagePlus.GRAY16:
+            case ImagePlus.GRAY32:
+                break;
+            default:
+                IJ.showMessage("Only 8, 16, 32-bit images are supported currently!");
+                return false;
+        }
+
+        // check the image dimensionality
+        if (imp.getNDimensions() < 2) {
+            IJ.showMessage("Image must be at least 2-dimensional!");
+            return false;
+        }
+
+        return true;
+    }
+
+    public static boolean isValidAffine(String affine) {
+        if (!affine.matches("^[0-9. ]+$")) {
+            IJ.log("Invalid affine transform - must contain only numbers and spaces");
+            return false;
+        }
+
+        String[] splitAffine = affine.split(" ");
+        if (splitAffine.length != 12) {
+            IJ.log("Invalid affine transform - must be of length 12");
+            return false;
+        }
+
+        return true;
+    }
+
+    public static AffineTransform3D parseAffineString(String affine) {
+        if (isValidAffine(affine)) {
+            AffineTransform3D sourceTransform = new AffineTransform3D();
+            String[] splitAffineTransform = affine.split(" ");
+            double[] doubleAffineTransform = new double[splitAffineTransform.length];
+            for (int i = 0; i < splitAffineTransform.length; i++) {
+                doubleAffineTransform[i] = Double.parseDouble(splitAffineTransform[i]);
+            }
+            sourceTransform.set(doubleAffineTransform);
+            return sourceTransform;
+        } else {
+            return null;
+        }
+    }
+
+    public static String generateDefaultAffine(ImagePlus imp) {
+        final double pixelWidth = imp.getCalibration().pixelWidth;
+        final double pixelHeight = imp.getCalibration().pixelHeight;
+        final double pixelDepth = imp.getCalibration().pixelDepth;
+
+        String defaultAffine = pixelWidth + " 0.0 0.0 0.0 0.0 " + pixelHeight + " 0.0 0.0 0.0 0.0 " + pixelDepth + " 0.0";
+        return defaultAffine;
+    }
+
+    public static FinalVoxelDimensions getVoxelSize(ImagePlus imp) {
+        final double pw = imp.getCalibration().pixelWidth;
+        final double ph = imp.getCalibration().pixelHeight;
+        final double pd = imp.getCalibration().pixelDepth;
+        String punit = imp.getCalibration().getUnit();
+        if (punit == null || punit.isEmpty())
+            punit = "px";
+        final FinalVoxelDimensions voxelSize = new FinalVoxelDimensions(punit, pw, ph, pd);
+        return voxelSize;
+    }
+
+    public static FinalDimensions getSize(ImagePlus imp) {
+        final int w = imp.getWidth();
+        final int h = imp.getHeight();
+        final int d = imp.getNSlices();
+        final FinalDimensions size = new FinalDimensions(w, h, d);
+        return size;
+    }
+
+    public static File getSeqFileFromPath(String seqFilename) {
+        final File seqFile = new File(seqFilename);
+        final File parent = seqFile.getParentFile();
+        if (parent == null || !parent.exists() || !parent.isDirectory()) {
+            IJ.showMessage("Invalid export filename " + seqFilename);
+            return null;
+        }
+        return seqFile;
+    }
+
+    public static AffineTransform3D generateSourceTransform(FinalVoxelDimensions voxelSize) {
+        // create SourceTransform from the images calibration
+        final AffineTransform3D sourceTransform = new AffineTransform3D();
+        sourceTransform.set(voxelSize.dimension(0), 0, 0, 0, 0, voxelSize.dimension(1),
+                0, 0, 0, 0, voxelSize.dimension(2), 0);
+        return sourceTransform;
+    }
+
+    public static File getN5FileFromXmlPath(String xmlPath) {
+        final String n5Filename = xmlPath.substring(0, xmlPath.length() - 4) + ".n5";
+        return new File(n5Filename);
+    }
+
+    public static ProjectCreator.BdvFormat getBdvFormatFromSpimDataMinimal(SpimDataMinimal spimDataMinimal) {
+        ProjectCreator.BdvFormat bdvFormat = null;
+        BasicImgLoader imgLoader = spimDataMinimal.getSequenceDescription().getImgLoader();
+        if (imgLoader instanceof N5ImageLoader | imgLoader instanceof N5FSImageLoader |
+                imgLoader instanceof N5S3ImageLoader | imgLoader instanceof de.embl.cba.mobie.n5.N5ImageLoader) {
+            bdvFormat = ProjectCreator.BdvFormat.n5;
+        }
+
+        return bdvFormat;
+    }
+
+    public static File getImageLocationFromSpimDataMinimal(SpimDataMinimal spimDataMinimal, ProjectCreator.BdvFormat bdvFormat) {
+        File imageLocation = null;
+
+        switch (bdvFormat) {
+            case n5:
+                // get image loader to find absolute image location
+                BasicImgLoader imgLoader = spimDataMinimal.getSequenceDescription().getImgLoader();
+                if (imgLoader instanceof N5ImageLoader) {
+                    N5ImageLoader n5ImageLoader = (N5ImageLoader) imgLoader;
+                    imageLocation = n5ImageLoader.getN5File();
+                } else if (imgLoader instanceof N5FSImageLoader) {
+                    N5FSImageLoader n5ImageLoader = (N5FSImageLoader) imgLoader;
+                    imageLocation = n5ImageLoader.getN5File();
+                }
+                break;
+        }
+
+        return imageLocation;
+    }
+
+    public static File getDataLocation( File projectLocation ) {
+        String dataDirectoryPath = FileAndUrlUtils.combinePath(projectLocation.getAbsolutePath(), "data");
+        File dataDirectory = new File( dataDirectoryPath );
+        if( !dataDirectory.exists() ) {
+            return projectLocation;
+        } else {
+            return dataDirectory;
+        }
+    }
+
+    public static boolean isSpimData2D( SpimDataMinimal spimDataMinimal ) {
+        BasicViewSetup firstSetup = spimDataMinimal.getSequenceDescription().getViewSetupsOrdered().get(0);
+        long[] dimensions = firstSetup.getSize().dimensionsAsLongArray();
+        if ( dimensions.length < 3 || dimensions[2] == 1 ) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public static Map<String, ArrayList<String>> getGroupToViewsMap( Dataset dataset ) {
+        Map<String, ArrayList<String>> groupToViewsMap = new HashMap<>();
+        for ( String viewName: dataset.views.keySet() ) {
+            View view = dataset.views.get( viewName );
+            String group = view.getUiSelectionGroup();
+            if ( !groupToViewsMap.containsKey( group ) ) {
+                ArrayList<String> views = new ArrayList<>();
+                views.add( viewName );
+                groupToViewsMap.put( group, views );
+            } else {
+                groupToViewsMap.get( group ).add( viewName );
+            }
+        }
+
+        return groupToViewsMap;
+    }
+
+    public static Map<String, ArrayList<String>> getGroupToViewsMap( AdditionalViews additionalViews ) {
+        Map<String, ArrayList<String>> groupToViewsMap = new HashMap<>();
+        for ( String viewName: additionalViews.views.keySet() ) {
+            View view = additionalViews.views.get( viewName );
+            String group = view.getUiSelectionGroup();
+            if ( !groupToViewsMap.containsKey( group ) ) {
+                ArrayList<String> views = new ArrayList<>();
+                views.add( viewName );
+                groupToViewsMap.put( group, views );
+            } else {
+                groupToViewsMap.get( group ).add( viewName );
+            }
+        }
+
+        return groupToViewsMap;
+    }
+
+    public static int getNTimepointsFromSpimData( SpimDataMinimal spimDataMinimal ) {
+        return spimDataMinimal.getSequenceDescription().getTimePoints().size();
+    }
+
+    public static String makeNewUiSelectionGroup( String[] currentUiSelectionGroups ) {
+        String newUiSelectionGroup = chooseNewSelectionGroupNameDialog();
+
+        // get rid of any spaces, warn for unusual characters
+        if ( newUiSelectionGroup != null ) {
+            newUiSelectionGroup = tidyString(newUiSelectionGroup);
+        }
+
+        if ( newUiSelectionGroup != null ) {
+            boolean alreadyExists = Arrays.asList(currentUiSelectionGroups).contains( newUiSelectionGroup );
+            if ( alreadyExists ) {
+                newUiSelectionGroup = null;
+                IJ.log("Saving view aborted - new ui selection group already exists");
+            }
+        }
+
+        return newUiSelectionGroup;
+    }
+
+    public static String chooseNewSelectionGroupNameDialog() {
+        final GenericDialog gd = new GenericDialog("Choose ui selection group Name:");
+
+        gd.addStringField("New ui selection group name:", "", 25 );
+        gd.showDialog();
+
+        if (!gd.wasCanceled()) {
+            return gd.getNextString();
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/RemoteMetadataCreator.java b/src/main/java/de/embl/cba/mobie/projectcreator/RemoteMetadataCreator.java
similarity index 56%
rename from src/main/java/de/embl/cba/mobie/projects/projectsCreator/RemoteMetadataCreator.java
rename to src/main/java/de/embl/cba/mobie/projectcreator/RemoteMetadataCreator.java
index acdb8e56e..eb7740cd5 100644
--- a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/RemoteMetadataCreator.java
+++ b/src/main/java/de/embl/cba/mobie/projectcreator/RemoteMetadataCreator.java
@@ -1,55 +1,78 @@
-package de.embl.cba.mobie.projects.projectsCreator;
+package de.embl.cba.mobie.projectcreator;
 
 import bdv.spimdata.SpimDataMinimal;
 import bdv.spimdata.XmlIoSpimDataMinimal;
 import de.embl.cba.mobie.n5.XmlIoN5S3ImageLoader;
-import de.embl.cba.mobie.utils.Utils;
+import de.embl.cba.mobie.Dataset;
+import de.embl.cba.mobie.source.ImageSource;
+import de.embl.cba.tables.FileAndUrlUtils;
+import ij.IJ;
 import mpicbg.spim.data.SpimDataException;
 import mpicbg.spim.data.SpimDataIOException;
-import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.FilenameUtils;
 import org.jdom2.Document;
 import org.jdom2.Element;
 import org.jdom2.output.Format;
 import org.jdom2.output.XMLOutputter;
+
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 
-import static de.embl.cba.mobie.utils.ExportUtils.getBdvFormatFromSpimDataMinimal;
-import static de.embl.cba.mobie.utils.ExportUtils.getImageLocationFromSpimDataMinimal;
+import static de.embl.cba.mobie.projectcreator.ProjectCreatorHelper.getBdvFormatFromSpimDataMinimal;
+import static de.embl.cba.mobie.projectcreator.ProjectCreatorHelper.getImageLocationFromSpimDataMinimal;
 
 public class RemoteMetadataCreator {
-    Project project;
+    ProjectCreator projectCreator;
     String signingRegion;
     String serviceEndpoint;
     String bucketName;
 
-    public RemoteMetadataCreator( Project project ) {
-        this.project = project;
+    public RemoteMetadataCreator( ProjectCreator projectCreator ) {
+        this.projectCreator = projectCreator;
     }
 
     private void deleteAllRemoteMetadata() throws IOException {
-        for ( String datasetName: project.getDatasetNames() ) {
+        for ( String datasetName: projectCreator.getProject().getDatasets() ) {
             if ( !datasetName.equals("") ) {
-                File remoteDir = new File( project.getRemoteImagesDirectoryPath( datasetName ) );
-                FileUtils.cleanDirectory( remoteDir );
+                Dataset dataset = projectCreator.getDataset( datasetName );
+                for ( String imageName: dataset.sources.keySet() ) {
+                    deleteRemoteMetadataForImage( datasetName, imageName );
+                }
+                projectCreator.getDatasetJsonCreator().writeDatasetJson( datasetName, dataset );
+            }
+        }
+    }
+
+    private void deleteRemoteMetadataForImage( String datasetName, String imageName ) throws IOException {
+        ImageSource imageSource = projectCreator.getDataset( datasetName ).sources.get( imageName ).get();
+        if ( imageSource.imageDataLocations.containsKey("s3store") ) {
+            // delete any existing remote metadata
+            File currentRemoteXmlLocation = new File( FileAndUrlUtils.combinePath( projectCreator.getDataLocation().getAbsolutePath(),
+                    datasetName, imageSource.imageDataLocations.get("s3store") ) );
+            if ( currentRemoteXmlLocation.exists() ) {
+                if ( !currentRemoteXmlLocation.delete() ) {
+                    String errorMessage = "Remote metadata for: " + imageName + " in dataset: " + datasetName + " could not be deleted.";
+                    IJ.log(errorMessage);
+                    throw new IOException(errorMessage);
+                }
             }
+            imageSource.imageDataLocations.remove( "s3store" );
         }
     }
 
     public String getRelativeKey( SpimDataMinimal spimDataMinimal, String datasetName, String imageName,
-                                  ProjectsCreator.BdvFormat bdvFormat ) throws IOException {
+                                  ProjectCreator.BdvFormat bdvFormat ) throws IOException {
         // check image is within the project folder (if people 'link' to bdv format images they may be outside)
         Path imagePath = Paths.get( getImageLocationFromSpimDataMinimal( spimDataMinimal, bdvFormat ).getAbsolutePath() ).normalize();
-        Path projectDataFolder = Paths.get( project.getDataLocation().getAbsolutePath() ).normalize();
+        Path projectDataFolder = Paths.get( projectCreator.getDataLocation().getAbsolutePath() ).normalize();
 
         if ( !imagePath.startsWith( projectDataFolder )) {
             String errorMesage = "Image: " + imageName + " for dataset:" + datasetName + " is not in project folder. \n" +
                     "You can't 'link' to bdv images outside the project folder, when uploading to s3";
-            Utils.log( errorMesage );
+            IJ.log( errorMesage );
             throw new IOException( errorMesage );
         }
 
@@ -59,7 +82,7 @@ public String getRelativeKey( SpimDataMinimal spimDataMinimal, String datasetNam
     }
 
     public Element createImageLoaderXmlElement ( SpimDataMinimal spimDataMinimal,
-                                                 ProjectsCreator.BdvFormat bdvFormat,
+                                                 ProjectCreator.BdvFormat bdvFormat,
                                                  String datasetName, String imageName ) throws IOException {
         String key = null;
         switch (bdvFormat) {
@@ -71,7 +94,7 @@ public Element createImageLoaderXmlElement ( SpimDataMinimal spimDataMinimal,
     }
 
     public void saveXml( final SpimDataMinimal spimData, String datasetName, String imagename,
-                         final String xmlFile, ProjectsCreator.BdvFormat bdvFormat ) throws SpimDataException, IOException {
+                         final String xmlFile, ProjectCreator.BdvFormat bdvFormat ) throws SpimDataException, IOException {
         XmlIoSpimDataMinimal io = new XmlIoSpimDataMinimal();
         final File xmlFileDirectory = new File( xmlFile ).getParentFile();
         final Document doc = new Document( io.toXml( spimData, xmlFileDirectory ) );
@@ -91,38 +114,48 @@ public void saveXml( final SpimDataMinimal spimData, String datasetName, String
     }
 
     private void addRemoteMetadataForImage( String datasetName, String imageName ) throws SpimDataException, IOException {
-        String localXmlLocation = project.getLocalImageXmlPath( datasetName, imageName );
-        String remoteXmlLocation = project.getRemoteImagesDirectoryPath( datasetName );
+        ImageSource imageSource = projectCreator.getDataset( datasetName ).sources.get( imageName ).get();
+        String localXmlLocation = FileAndUrlUtils.combinePath( projectCreator.getDataLocation().getAbsolutePath(),
+                datasetName, imageSource.imageDataLocations.get("fileSystem") );
+
+        deleteRemoteMetadataForImage( datasetName, imageName );
+
+        String remoteXmlLocation = FileAndUrlUtils.combinePath( projectCreator.getDataLocation().getAbsolutePath(),
+                datasetName, "images", "remote" );
 
         SpimDataMinimal spimDataMinimal = new XmlIoSpimDataMinimal().load( localXmlLocation );
-        ProjectsCreator.BdvFormat bdvFormat = getBdvFormatFromSpimDataMinimal( spimDataMinimal );
+        ProjectCreator.BdvFormat bdvFormat = getBdvFormatFromSpimDataMinimal( spimDataMinimal );
 
         if ( bdvFormat == null ) {
             String errorMesage = "Image:" + imageName + " in dataset:" + datasetName + " is of an unsupported format";
-            Utils.log( errorMesage );
+            IJ.log( errorMesage );
             throw new IOException( errorMesage );
         } else {
             spimDataMinimal.setBasePath( new File( remoteXmlLocation ) );
             saveXml( spimDataMinimal, datasetName, imageName,
                     new File(remoteXmlLocation, imageName + ".xml").getAbsolutePath(),
                     bdvFormat );
+            imageSource.imageDataLocations.put( "s3store", "images/remote/" + imageName + ".xml" );
         }
 
     }
 
     private void addRemoteMetadataForDataset( String datasetName ) throws SpimDataException, IOException {
-        for ( String imageName: project.getDataset( datasetName ).getImageNames() ) {
+        Dataset dataset = projectCreator.getDataset( datasetName );
+        for ( String imageName: dataset.sources.keySet() ) {
             if ( !imageName.equals("") ) {
-                Utils.log("Adding metadata for image: " + imageName );
+                IJ.log("Adding metadata for image: " + imageName );
                 addRemoteMetadataForImage( datasetName, imageName );
             }
         }
+
+        projectCreator.getDatasetJsonCreator().writeDatasetJson( datasetName, dataset );
     }
 
     private void addAllRemoteMetadata() throws SpimDataException, IOException {
-        for ( String datasetName: project.getDatasetNames() ) {
+        for ( String datasetName: projectCreator.getProject().getDatasets() ) {
             if ( !datasetName.equals("") ) {
-                Utils.log("Adding metadata for dataset: " + datasetName );
+                IJ.log("Adding metadata for dataset: " + datasetName );
                 addRemoteMetadataForDataset( datasetName );
             }
         }
@@ -140,7 +173,7 @@ public void createRemoteMetadata( String signingRegion, String serviceEndpoint,
             try {
                addAllRemoteMetadata();
             } catch (SpimDataException | IOException e) {
-                Utils.log( "Error - aborting, and removing all remote metadata" );
+                IJ.log( "Error - aborting, and removing all remote metadata" );
                 deleteAllRemoteMetadata();
                 e.printStackTrace();
             }
diff --git a/src/main/java/de/embl/cba/mobie/n5/DownsampleBlock.java b/src/main/java/de/embl/cba/mobie/projectcreator/n5/DownsampleBlock.java
similarity index 99%
rename from src/main/java/de/embl/cba/mobie/n5/DownsampleBlock.java
rename to src/main/java/de/embl/cba/mobie/projectcreator/n5/DownsampleBlock.java
index 2f1b2b88b..a13ce19b8 100644
--- a/src/main/java/de/embl/cba/mobie/n5/DownsampleBlock.java
+++ b/src/main/java/de/embl/cba/mobie/projectcreator/n5/DownsampleBlock.java
@@ -1,4 +1,4 @@
-package de.embl.cba.mobie.n5;
+package de.embl.cba.mobie.projectcreator.n5;
 
 /*-
  * #%L
@@ -29,11 +29,8 @@
  * #L%
  */
 
-import java.util.Arrays;
-
 import gnu.trove.iterator.TLongLongIterator;
 import gnu.trove.map.hash.TLongLongHashMap;
-import gnu.trove.set.TLongSet;
 import net.imglib2.*;
 import net.imglib2.img.array.ArrayImgs;
 import net.imglib2.loops.ClassCopyProvider;
@@ -41,6 +38,8 @@
 import net.imglib2.type.numeric.real.DoubleType;
 import net.imglib2.util.Intervals;
 
+import java.util.Arrays;
+
 public interface DownsampleBlock< T extends RealType< T > >
 {
     enum DownsamplingMethod
diff --git a/src/main/java/de/embl/cba/mobie/n5/ExportScalePyramid.java b/src/main/java/de/embl/cba/mobie/projectcreator/n5/ExportScalePyramid.java
similarity index 99%
rename from src/main/java/de/embl/cba/mobie/n5/ExportScalePyramid.java
rename to src/main/java/de/embl/cba/mobie/projectcreator/n5/ExportScalePyramid.java
index b392d58e6..67507fb61 100644
--- a/src/main/java/de/embl/cba/mobie/n5/ExportScalePyramid.java
+++ b/src/main/java/de/embl/cba/mobie/projectcreator/n5/ExportScalePyramid.java
@@ -1,4 +1,4 @@
-package de.embl.cba.mobie.n5;
+package de.embl.cba.mobie.projectcreator.n5;
 
 /*
  * #%L
@@ -29,15 +29,6 @@
  * #L%
  */
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-import java.util.concurrent.atomic.AtomicInteger;
-
 import bdv.export.*;
 import net.imglib2.FinalInterval;
 import net.imglib2.RandomAccess;
@@ -53,6 +44,15 @@
 import net.imglib2.util.Intervals;
 import net.imglib2.view.Views;
 
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicInteger;
+
 /**
  * Write an image to a chunked mipmap representation.
  */
diff --git a/src/main/java/de/embl/cba/mobie/n5/WriteImgPlusToN5.java b/src/main/java/de/embl/cba/mobie/projectcreator/n5/WriteImgPlusToN5.java
similarity index 77%
rename from src/main/java/de/embl/cba/mobie/n5/WriteImgPlusToN5.java
rename to src/main/java/de/embl/cba/mobie/projectcreator/n5/WriteImgPlusToN5.java
index 9318e1cc9..c3a336a09 100644
--- a/src/main/java/de/embl/cba/mobie/n5/WriteImgPlusToN5.java
+++ b/src/main/java/de/embl/cba/mobie/projectcreator/n5/WriteImgPlusToN5.java
@@ -1,6 +1,9 @@
-package de.embl.cba.mobie.n5;
+package de.embl.cba.mobie.projectcreator.n5;
 
-import bdv.export.*;
+import bdv.export.ExportMipmapInfo;
+import bdv.export.ProgressWriter;
+import bdv.export.ProposeMipmaps;
+import bdv.export.SubTaskProgressWriter;
 import bdv.ij.util.PluginHelper;
 import bdv.ij.util.ProgressWriterIJ;
 import bdv.img.imagestack.ImageStackImageLoader;
@@ -32,7 +35,7 @@
 import java.util.HashMap;
 import java.util.Map;
 
-import static de.embl.cba.mobie.utils.ExportUtils.*;
+import static de.embl.cba.mobie.projectcreator.ProjectCreatorHelper.*;
 
 // splits functionality from https://github.com/bigdataviewer/bigdataviewer_fiji/blob/master/src/main/java/bdv/ij/ExportImagePlusAsN5PlugIn.java
 // separating from GUI. Provides general export functions for ImagePlus.
@@ -55,6 +58,8 @@ protected static class Parameters
 
         final Compression compression;
 
+        final String[] viewSetupNames;
+
         public Parameters(
                 final int[][] resolutions, final int[][] subdivisions,
                 final File seqFile, final File n5File, final AffineTransform3D sourceTransform,
@@ -67,6 +72,23 @@ public Parameters(
             this.sourceTransform = sourceTransform;
             this.downsamplingMethod = downsamplingMethod;
             this.compression = compression;
+            this.viewSetupNames = null;
+        }
+
+        public Parameters(
+                final int[][] resolutions, final int[][] subdivisions,
+                final File seqFile, final File n5File, final AffineTransform3D sourceTransform,
+                final DownsampleBlock.DownsamplingMethod downsamplingMethod, final Compression compression,
+                final String[] viewSetupNames )
+        {
+            this.resolutions = resolutions;
+            this.subdivisions = subdivisions;
+            this.seqFile = seqFile;
+            this.n5File = n5File;
+            this.sourceTransform = sourceTransform;
+            this.downsamplingMethod = downsamplingMethod;
+            this.compression = compression;
+            this.viewSetupNames = viewSetupNames;
         }
     }
 
@@ -81,20 +103,34 @@ public void export( ImagePlus imp, String xmlPath, DownsampleBlock.DownsamplingM
         final AffineTransform3D sourceTransform = generateSourceTransform( voxelSize );
 
         Parameters defaultParameters = generateDefaultParameters( imp, xmlPath, sourceTransform, downsamplingMethod,
-                compression );
+                compression, null );
+
+        export( imp, defaultParameters );
+    }
+
+    // export, generating default resolutions / subdivisions
+    public void export(ImagePlus imp, String xmlPath, AffineTransform3D sourceTransform,
+                       DownsampleBlock.DownsamplingMethod downsamplingMethod, Compression compression ) {
+        if ( !isImageSuitable( imp ) ) {
+            return;
+        }
+
+        Parameters defaultParameters = generateDefaultParameters( imp, xmlPath, sourceTransform,
+                downsamplingMethod, compression, null );
 
         export( imp, defaultParameters );
     }
 
     // export, generating default resolutions / subdivisions
-    public void export( ImagePlus imp, String xmlPath, AffineTransform3D sourceTransform,
-                        DownsampleBlock.DownsamplingMethod downsamplingMethod, Compression compression ) {
+    public void export(ImagePlus imp, String xmlPath, AffineTransform3D sourceTransform,
+                       DownsampleBlock.DownsamplingMethod downsamplingMethod, Compression compression,
+                       String[] viewSetupNames ) {
         if ( !isImageSuitable( imp ) ) {
             return;
         }
 
         Parameters defaultParameters = generateDefaultParameters( imp, xmlPath, sourceTransform,
-                downsamplingMethod, compression );
+                downsamplingMethod, compression, viewSetupNames );
 
         export( imp, defaultParameters );
     }
@@ -102,6 +138,12 @@ public void export( ImagePlus imp, String xmlPath, AffineTransform3D sourceTrans
     public void export( ImagePlus imp, int[][] resolutions, int[][] subdivisions, String xmlPath,
                         AffineTransform3D sourceTransform, DownsampleBlock.DownsamplingMethod downsamplingMethod,
                         Compression compression ) {
+        export( imp, resolutions, subdivisions, xmlPath, sourceTransform, downsamplingMethod, compression, null );
+    }
+
+    public void export( ImagePlus imp, int[][] resolutions, int[][] subdivisions, String xmlPath,
+                        AffineTransform3D sourceTransform, DownsampleBlock.DownsamplingMethod downsamplingMethod,
+                        Compression compression, String[] viewSetupNames ) {
         if ( resolutions.length == 0 ) {
             IJ.showMessage( "Invalid resolutions - length 0" );
             return;
@@ -130,13 +172,14 @@ public void export( ImagePlus imp, int[][] resolutions, int[][] subdivisions, St
         // TODO - check transform and downsampling mode
 
         Parameters exportParameters = new Parameters( resolutions, subdivisions, seqFile, n5File, sourceTransform,
-                downsamplingMethod, compression );
+                downsamplingMethod, compression, viewSetupNames );
 
         export( imp, exportParameters );
     }
 
-    protected Parameters generateDefaultParameters( ImagePlus imp, String xmlPath, AffineTransform3D sourceTransform,
-                                                    DownsampleBlock.DownsamplingMethod downsamplingMethod, Compression compression ) {
+    protected Parameters generateDefaultParameters(ImagePlus imp, String xmlPath, AffineTransform3D sourceTransform,
+                                                   DownsampleBlock.DownsamplingMethod downsamplingMethod, Compression compression,
+                                                   String[] viewSetupNames ) {
         FinalVoxelDimensions voxelSize = getVoxelSize( imp );
         FinalDimensions size = getSize( imp );
 
@@ -165,7 +208,7 @@ protected Parameters generateDefaultParameters( ImagePlus imp, String xmlPath, A
         final File n5File = getN5FileFromXmlPath( seqFilename );
 
         return new Parameters( resolutions, subdivisions, seqFile, n5File, sourceTransform,
-                downsamplingMethod, compression );
+                downsamplingMethod, compression, viewSetupNames );
     }
 
     protected void export( ImagePlus imp, Parameters params ) {
@@ -224,9 +267,18 @@ protected void export( ImagePlus imp, Parameters params ) {
 
         // write n5
         final HashMap< Integer, BasicViewSetup> setups = new HashMap<>( numSetups );
+        if ( params.viewSetupNames != null && params.viewSetupNames.length != numSetups ) {
+            throw new RuntimeException( params.viewSetupNames.length + " view setup names were given, " +
+                    "but there are " + numSetups + "setups" );
+        }
         for ( int s = 0; s < numSetups; ++s )
         {
-            final BasicViewSetup setup = new BasicViewSetup( s, String.format( "channel %d", s + 1 ), size, voxelSize );
+            final BasicViewSetup setup;
+            if ( params.viewSetupNames != null ) {
+                setup = new BasicViewSetup(s, params.viewSetupNames[s], size, voxelSize);
+            } else {
+                setup = new BasicViewSetup(s, String.format("channel %d", s + 1), size, voxelSize);
+            }
             setup.setAttribute( new Channel( s + 1 ) );
             setups.put( s, setup );
         }
@@ -301,7 +353,7 @@ public void afterEachPlane( final boolean usedLoopBack )
                     new SubTaskProgressWriter( progressWriter, 0, 0.95 ) );
 
             // write xml sequence description
-            final bdv.img.n5.N5ImageLoader n5Loader = new N5ImageLoader( params.n5File, null );
+            final N5ImageLoader n5Loader = new N5ImageLoader( params.n5File, null );
             final SequenceDescriptionMinimal seqh5 = new SequenceDescriptionMinimal( seq, n5Loader );
 
             final ArrayList<ViewRegistration> registrations = new ArrayList<>();
diff --git a/src/main/java/de/embl/cba/mobie/n5/WriteSequenceToN5.java b/src/main/java/de/embl/cba/mobie/projectcreator/n5/WriteSequenceToN5.java
similarity index 95%
rename from src/main/java/de/embl/cba/mobie/n5/WriteSequenceToN5.java
rename to src/main/java/de/embl/cba/mobie/projectcreator/n5/WriteSequenceToN5.java
index 475ef7b80..87389b510 100644
--- a/src/main/java/de/embl/cba/mobie/n5/WriteSequenceToN5.java
+++ b/src/main/java/de/embl/cba/mobie/projectcreator/n5/WriteSequenceToN5.java
@@ -1,4 +1,4 @@
-package de.embl.cba.mobie.n5;
+package de.embl.cba.mobie.projectcreator.n5;
 /*-
  * #%L
  * BigDataViewer core classes with minimal dependencies.
@@ -28,18 +28,12 @@
  * #L%
  */
 
-import bdv.export.*;
+import bdv.export.ExportMipmapInfo;
+import bdv.export.ProgressWriter;
+import bdv.export.ProgressWriterNull;
+import bdv.export.SubTaskProgressWriter;
 import bdv.img.cache.SimpleCacheArrayLoader;
 import bdv.img.n5.N5ImageLoader;
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.function.Function;
-import java.util.stream.Collectors;
 import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription;
 import mpicbg.spim.data.generic.sequence.BasicImgLoader;
 import mpicbg.spim.data.generic.sequence.BasicSetupImgLoader;
@@ -54,23 +48,20 @@
 import net.imglib2.type.NativeType;
 import net.imglib2.type.numeric.RealType;
 import net.imglib2.util.Cast;
-import org.janelia.saalfeldlab.n5.ByteArrayDataBlock;
-import org.janelia.saalfeldlab.n5.Compression;
-import org.janelia.saalfeldlab.n5.DataBlock;
-import org.janelia.saalfeldlab.n5.DataType;
-import org.janelia.saalfeldlab.n5.DatasetAttributes;
-import org.janelia.saalfeldlab.n5.DoubleArrayDataBlock;
-import org.janelia.saalfeldlab.n5.FloatArrayDataBlock;
-import org.janelia.saalfeldlab.n5.IntArrayDataBlock;
-import org.janelia.saalfeldlab.n5.LongArrayDataBlock;
-import org.janelia.saalfeldlab.n5.N5FSWriter;
-import org.janelia.saalfeldlab.n5.N5Writer;
-import org.janelia.saalfeldlab.n5.ShortArrayDataBlock;
+import org.janelia.saalfeldlab.n5.*;
 import org.janelia.saalfeldlab.n5.imglib2.N5Utils;
 
-import static bdv.img.n5.BdvN5Format.DATA_TYPE_KEY;
-import static bdv.img.n5.BdvN5Format.DOWNSAMPLING_FACTORS_KEY;
-import static bdv.img.n5.BdvN5Format.getPathName;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static bdv.img.n5.BdvN5Format.*;
 import static net.imglib2.cache.img.ReadOnlyCachedCellImgOptions.options;
 
 /**
diff --git a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/ui/ManualN5ExportPanel.java b/src/main/java/de/embl/cba/mobie/projectcreator/ui/ManualN5ExportPanel.java
similarity index 95%
rename from src/main/java/de/embl/cba/mobie/projects/projectsCreator/ui/ManualN5ExportPanel.java
rename to src/main/java/de/embl/cba/mobie/projectcreator/ui/ManualN5ExportPanel.java
index 39d4f8690..98baffdd9 100644
--- a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/ui/ManualN5ExportPanel.java
+++ b/src/main/java/de/embl/cba/mobie/projectcreator/ui/ManualN5ExportPanel.java
@@ -1,8 +1,8 @@
-package de.embl.cba.mobie.projects.projectsCreator.ui;
+package de.embl.cba.mobie.projectcreator.ui;
 
 import bdv.ij.util.PluginHelper;
-import de.embl.cba.mobie.n5.DownsampleBlock;
-import de.embl.cba.mobie.n5.WriteImgPlusToN5;
+import de.embl.cba.mobie.projectcreator.n5.DownsampleBlock;
+import de.embl.cba.mobie.projectcreator.n5.WriteImgPlusToN5;
 import fiji.util.gui.GenericDialogPlus;
 import ij.IJ;
 import ij.ImagePlus;
@@ -20,6 +20,7 @@ public class ManualN5ExportPanel {
     String xmlPath;
     AffineTransform3D sourceTransform;
     DownsampleBlock.DownsamplingMethod downsamplingMethod;
+    String imageName;
 
     static String lastSubsampling = "{ {1,1,1} }";
     static String lastChunkSizes = "{ {64,64,64} }";
@@ -27,11 +28,12 @@ public class ManualN5ExportPanel {
     static boolean lastCompressionDefaultSettings = true;
 
     public ManualN5ExportPanel ( ImagePlus imp, String xmlPath, AffineTransform3D sourceTransform,
-                                 DownsampleBlock.DownsamplingMethod downsamplingMethod ) {
+                                 DownsampleBlock.DownsamplingMethod downsamplingMethod, String imageName ) {
         this.imp = imp;
         this.xmlPath = xmlPath;
         this.sourceTransform = sourceTransform;
         this.downsamplingMethod = downsamplingMethod;
+        this.imageName = imageName;
     }
 
     public void getManualExportParameters() {
@@ -97,7 +99,7 @@ private void parseInputAndWriteToN5 () {
             return;
 
         new WriteImgPlusToN5().export( imp, resolutions, subdivisions, xmlPath, sourceTransform,
-                downsamplingMethod, compression );
+                downsamplingMethod, compression, new String[]{imageName} );
     }
 
 
diff --git a/src/main/java/de/embl/cba/mobie/projectcreator/ui/ProjectsCreatorPanel.java b/src/main/java/de/embl/cba/mobie/projectcreator/ui/ProjectsCreatorPanel.java
new file mode 100644
index 000000000..a1510e111
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/projectcreator/ui/ProjectsCreatorPanel.java
@@ -0,0 +1,545 @@
+package de.embl.cba.mobie.projectcreator.ui;
+
+import de.embl.cba.mobie.Dataset;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.Project;
+import de.embl.cba.mobie.projectcreator.ProjectCreator;
+import de.embl.cba.tables.SwingUtils;
+import ij.IJ;
+import ij.ImagePlus;
+import ij.gui.GenericDialog;
+import mpicbg.spim.data.SpimDataException;
+import net.imglib2.realtransform.AffineTransform3D;
+import org.apache.commons.compress.utils.FileNameUtils;
+
+import javax.swing.*;
+import javax.swing.filechooser.FileNameExtensionFilter;
+import java.awt.*;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.io.File;
+import java.io.IOException;
+
+import static de.embl.cba.mobie.projectcreator.ProjectCreatorHelper.*;
+import static de.embl.cba.mobie.ui.SwingHelper.*;
+import static de.embl.cba.mobie.ui.UserInterfaceHelper.tidyString;
+
+public class ProjectsCreatorPanel extends JFrame {
+    private ProjectCreator projectsCreator;
+    private JComboBox<String> datasetComboBox;
+    private JComboBox<String> sourcesComboBox;
+    private JComboBox<String> groupsComboBox;
+    private JComboBox<String> viewsComboBox;
+
+    public ProjectsCreatorPanel ( File projectLocation ) throws IOException {
+
+        // account for projects with and without the top 'data' directory
+        File dataDirectory = getDataLocation( projectLocation );
+        this.projectsCreator = new ProjectCreator( dataDirectory );
+
+        addDatasetPanel();
+        addSourcesPanel();
+        this.getContentPane().add(new JSeparator(SwingConstants.HORIZONTAL));
+        this.getContentPane().add( Box.createVerticalStrut( 10 ) );
+        addViewsPanel();
+
+        String shortenedProjectName = projectLocation.getName();
+        if ( shortenedProjectName.length() > 50 ) {
+            shortenedProjectName = shortenedProjectName.substring( 0, 47 ) + "...";
+        }
+        this.setTitle( "Editing MoBIE Project: " + shortenedProjectName );
+        this.getContentPane().setLayout( new BoxLayout(this.getContentPane(), BoxLayout.Y_AXIS ) );
+        this.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
+    }
+
+    public void showProjectsCreatorPanel() {
+        this.pack();
+        this.setLocationRelativeTo(null);
+        this.setVisible( true );
+    }
+
+    public ProjectCreator getProjectsCreator() {
+        return projectsCreator;
+    }
+
+    private void addDatasetPanel() {
+        final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
+
+        final JButton addButton = createButton("Add");
+        final JButton editButton = createButton("Edit");
+
+        createDatasetComboBox();
+        addButton.addActionListener( e ->
+        {
+            new Thread( () -> { addDatasetDialog(); } ).start();
+        } );
+
+        editButton.addActionListener( e ->
+        {
+            new Thread( () -> { editDatasetDialog(); } ).start();
+        } );
+
+        horizontalLayoutPanel.add(getJLabel("dataset", 60, 10));
+        horizontalLayoutPanel.add(datasetComboBox);
+        horizontalLayoutPanel.add(addButton);
+        horizontalLayoutPanel.add(editButton);
+        horizontalLayoutPanel.setAlignmentX( Component.LEFT_ALIGNMENT );
+
+        this.getContentPane().add(horizontalLayoutPanel);
+    }
+
+    private String[] getDatasetNames() {
+        Project project = projectsCreator.getProject();
+        if ( project.getDatasets() != null && project.getDatasets().size() > 0 ) {
+            String[] datasetNames = new String[project.getDatasets().size()];
+            for (int i = 0; i < project.getDatasets().size(); i++) {
+                datasetNames[i] = project.getDatasets().get(i);
+            }
+            return datasetNames;
+        } else {
+            return new String[]{""};
+        }
+    }
+
+    private void createDatasetComboBox() {
+        String[] datasetNames = getDatasetNames();
+        datasetComboBox = new JComboBox<>( datasetNames );
+        datasetComboBox.setSelectedItem( datasetNames[0] );
+        setComboBoxDimensions(datasetComboBox);
+        datasetComboBox.setPrototypeDisplayValue( MoBIE.PROTOTYPE_DISPLAY_VALUE);
+        datasetComboBox.addItemListener( new SyncAllWithDatasetComboBox() );
+    }
+
+    private void createSoucesComboBox() {
+        String selectedDataset = (String) datasetComboBox.getSelectedItem();
+        Dataset dataset = projectsCreator.getDataset( selectedDataset );
+        if ( !selectedDataset.equals("") && dataset != null && dataset.sources.keySet().size() > 0 ) {
+            String[] imageNames = projectsCreator.getDataset( selectedDataset ).sources.keySet().toArray(new String[0]);
+            sourcesComboBox = new JComboBox<>( imageNames );
+            sourcesComboBox.setSelectedItem( imageNames[0] );
+        } else {
+            sourcesComboBox = new JComboBox<>( new String[] {""} );
+            sourcesComboBox.setSelectedItem( "" );
+        }
+        setComboBoxDimensions(sourcesComboBox);
+        sourcesComboBox.setPrototypeDisplayValue( MoBIE.PROTOTYPE_DISPLAY_VALUE);
+    }
+
+    private void createGroupsCombobox() {
+        String selectedDataset = (String) datasetComboBox.getSelectedItem();
+        String[] groupNames = null;
+        if ( !selectedDataset.equals("") ) {
+            groupNames = projectsCreator.getGroups( selectedDataset );
+        }
+
+        if ( groupNames != null && groupNames.length > 0 ) {
+            groupsComboBox = new JComboBox<>( groupNames );
+            groupsComboBox.setSelectedItem( groupNames[0] );
+        } else {
+            groupsComboBox = new JComboBox<>( new String[] {""} );
+            groupsComboBox.setSelectedItem( "" );
+        }
+        setComboBoxDimensions(groupsComboBox);
+        groupsComboBox.setPrototypeDisplayValue( MoBIE.PROTOTYPE_DISPLAY_VALUE);
+        groupsComboBox.addItemListener( new SyncGroupAndViewComboBox() );
+    }
+
+    private void createViewsCombobox() {
+        String selectedDataset = (String) datasetComboBox.getSelectedItem();
+        String selectedGroup = (String) groupsComboBox.getSelectedItem();
+        String[] viewNames = null;
+        if ( !selectedDataset.equals("") && !selectedGroup.equals("") ) {
+            viewNames = projectsCreator.getViews( selectedDataset, selectedGroup );
+        }
+
+        if ( viewNames != null && viewNames.length > 0 ) {
+            viewsComboBox = new JComboBox<>( viewNames );
+            viewsComboBox.setSelectedItem( viewNames[0] );
+        } else {
+            viewsComboBox = new JComboBox<>( new String[] {""} );
+            viewsComboBox.setSelectedItem( "" );
+        }
+
+        setComboBoxDimensions(viewsComboBox);
+        viewsComboBox.setPrototypeDisplayValue( MoBIE.PROTOTYPE_DISPLAY_VALUE);
+    }
+
+    public static void setComboBoxDimensions( JComboBox< String > comboBox )
+    {
+        comboBox.setPrototypeDisplayValue( MoBIE.PROTOTYPE_DISPLAY_VALUE );
+        comboBox.setPreferredSize( new Dimension( 350, 20 ) );
+        comboBox.setMaximumSize( new Dimension( 350, 20 ) );
+    }
+
+    private class SyncAllWithDatasetComboBox implements ItemListener {
+        @Override
+        public void itemStateChanged(ItemEvent event) {
+            if (event.getStateChange() == ItemEvent.SELECTED) {
+                updateSourcesComboBox();
+                updateGroupsComboBox();
+            }
+        }
+    }
+
+    private class SyncGroupAndViewComboBox implements ItemListener {
+        @Override
+        public void itemStateChanged(ItemEvent event) {
+            if (event.getStateChange() == ItemEvent.SELECTED) {
+                updateViewsComboBox();
+            }
+        }
+    }
+
+    private void addSourcesPanel() {
+        final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
+
+        final JButton addButton = createButton( "Add" );
+        // for now we don't support editing any image properties, but this is likely to change in future,
+        // so keep this code for now
+        // final JButton editButton = createButton("Edit");
+
+        createSoucesComboBox();
+        addButton.addActionListener( e ->
+        {
+            new Thread( () -> { addImageDialog(); } ).start();
+        } );
+
+        // editButton.addActionListener( e ->
+        // {
+        //     new Thread( () -> { editImageDialog(); } ).start();
+        // } );
+
+        horizontalLayoutPanel.add(getJLabel("source", 60, 10));
+        horizontalLayoutPanel.add(sourcesComboBox);
+        horizontalLayoutPanel.add( addButton );
+        horizontalLayoutPanel.add( Box.createHorizontalStrut( BUTTON_DIMENSION.width ) );
+        // horizontalLayoutPanel.add( editButton );
+        horizontalLayoutPanel.setAlignmentX( Component.LEFT_ALIGNMENT );
+
+        this.getContentPane().add(horizontalLayoutPanel);
+    }
+
+    private void addViewsPanel() {
+        final JPanel groupPanel = SwingUtils.horizontalLayoutPanel();
+        final JPanel viewsPanel = SwingUtils.horizontalLayoutPanel();
+
+        createGroupsCombobox();
+        createViewsCombobox();
+
+        groupPanel.add(getJLabel("group", 60, 10));
+        viewsPanel.add(getJLabel("view", 60, 10));
+        groupPanel.add( groupsComboBox );
+        viewsPanel.add( viewsComboBox );
+        groupPanel.add( Box.createHorizontalStrut( BUTTON_DIMENSION.width ) );
+        groupPanel.add( Box.createHorizontalStrut( BUTTON_DIMENSION.width ) );
+        viewsPanel.add( Box.createHorizontalStrut( BUTTON_DIMENSION.width ) );
+        viewsPanel.add( Box.createHorizontalStrut( BUTTON_DIMENSION.width ) );
+        groupPanel.setAlignmentX( Component.LEFT_ALIGNMENT );
+        viewsPanel.setAlignmentX( Component.LEFT_ALIGNMENT );
+
+        this.getContentPane().add( groupPanel );
+        this.getContentPane().add( viewsPanel );
+    }
+
+    public String chooseDatasetDialog() {
+        final GenericDialog gd = new GenericDialog( "Choose a dataset" );
+        String[] currentDatasets = getDatasetNames();
+        gd.addChoice("Dataset", currentDatasets, currentDatasets[0]);
+        gd.showDialog();
+
+        if ( !gd.wasCanceled() ) {
+            return gd.getNextChoice();
+
+        } else {
+            return null;
+        }
+
+    }
+
+    public void addImageDialog() {
+        final GenericDialog gd = new GenericDialog( "Add..." );
+        String[] addMethods = new String[] {"current open image", "bdv format image"};
+        gd.addChoice("Add:", addMethods, "current open image");
+        gd.showDialog();
+
+        if ( !gd.wasCanceled() ) {
+            String addMethod = gd.getNextChoice();
+            if (addMethod.equals("current open image")) {
+                addCurrentOpenImageDialog();
+            } else if (addMethod.equals("bdv format image")) {
+                addBdvFormatImageDialog();
+            }
+
+        }
+    }
+
+    private String selectUiSelectionGroupDialog( String datasetName ) {
+
+        String[] currentSelectionGroups = projectsCreator.getGroups( datasetName );
+
+        String chosenGroup = null;
+        if ( currentSelectionGroups == null || currentSelectionGroups.length == 0 ) {
+            chosenGroup = makeNewUiSelectionGroup( new String[]{} );
+        } else {
+            final GenericDialog gd = new GenericDialog( "Choose selection group for image view..." );
+            String[] choices = new String[currentSelectionGroups.length + 1];
+            choices[0] = "Make New Ui Selection Group";
+            for (int i = 0; i < currentSelectionGroups.length; i++) {
+                choices[i + 1] = currentSelectionGroups[i];
+            }
+            gd.addChoice("Ui Selection Group", choices, choices[0]);
+            gd.showDialog();
+
+            if ( !gd.wasCanceled() ) {
+                String choice = gd.getNextChoice();
+                if ( choice.equals("Make New Ui Selection Group") ) {
+                    chosenGroup = makeNewUiSelectionGroup( currentSelectionGroups );
+                } else {
+                    chosenGroup = choice;
+                }
+            }
+        }
+
+        return chosenGroup;
+    }
+
+    private void updateComboBoxesForNewImage( String imageName, String uiSelectionGroup ) {
+        updateSourcesComboBox( imageName );
+        if ( uiSelectionGroup != null ) {
+            updateGroupsComboBox( uiSelectionGroup );
+            viewsComboBox.setSelectedItem( imageName );
+        } else {
+            updateGroupsComboBox();
+        }
+    }
+
+    public void addCurrentOpenImageDialog() {
+        String datasetName = (String) datasetComboBox.getSelectedItem();
+
+        if ( !datasetName.equals("") ) {
+            ImagePlus currentImage = IJ.getImage();
+            String defaultAffineTransform = generateDefaultAffine( currentImage );
+
+            final GenericDialog gd = new GenericDialog( "Add Current Image To MoBIE Project..." );
+            gd.addMessage( "Make sure your pixel size, and unit,\n are set properly under Image > Properties...");
+            gd.addStringField( "Image Name", "", 35 );
+            String[] imageTypes = new String[]{ ProjectCreator.ImageType.image.toString(),
+                    ProjectCreator.ImageType.segmentation.toString() };
+            gd.addChoice( "Image Type", imageTypes, imageTypes[0] );
+            // TODO - add OME.ZARR
+            String[] bdvFormats = new String[]{ ProjectCreator.BdvFormat.n5.toString() };
+            gd.addChoice( "Bdv format", bdvFormats, bdvFormats[0] );
+            gd.addStringField("Affine", defaultAffineTransform, 35 );
+            gd.addCheckbox("Use default export settings", true);
+            gd.addCheckbox( "Create view for this image", true );
+
+            gd.showDialog();
+
+            if ( !gd.wasCanceled() ) {
+                String imageName = gd.getNextString();
+                ProjectCreator.ImageType imageType = ProjectCreator.ImageType.valueOf( gd.getNextChoice() );
+                ProjectCreator.BdvFormat bdvFormat = ProjectCreator.BdvFormat.valueOf( gd.getNextChoice() );
+                String affineTransform = gd.getNextString().trim();
+                boolean useDefaultSettings = gd.getNextBoolean();
+                boolean createView = gd.getNextBoolean();
+
+                // tidy up image name, remove any spaces
+                imageName = tidyString( imageName );
+
+                AffineTransform3D sourceTransform = parseAffineString( affineTransform );
+
+                if ( imageName != null && sourceTransform != null ) {
+                    String uiSelectionGroup = null;
+                    if ( createView ) {
+                        uiSelectionGroup = selectUiSelectionGroupDialog( datasetName );
+                        if ( uiSelectionGroup != null ) {
+                            projectsCreator.getImagesCreator().addImage( currentImage, imageName,
+                                    datasetName, bdvFormat, imageType, sourceTransform, useDefaultSettings, uiSelectionGroup );
+                            updateComboBoxesForNewImage( imageName, uiSelectionGroup );
+                        }
+                    } else {
+                        projectsCreator.getImagesCreator().addImage( currentImage, imageName,
+                                datasetName, bdvFormat, imageType, sourceTransform, useDefaultSettings, uiSelectionGroup );
+                        updateComboBoxesForNewImage( imageName, uiSelectionGroup );
+                    }
+                }
+            }
+
+        } else {
+            IJ.log( "Add image failed - create a dataset first" );
+        }
+    }
+
+    public void addBdvFormatImageDialog() {
+        String datasetName = (String) datasetComboBox.getSelectedItem();
+
+        if (!datasetName.equals("")) {
+
+            JFileChooser chooser = new JFileChooser();
+            chooser.setFileFilter(new FileNameExtensionFilter("xml", "xml"));
+            chooser.setDialogTitle( "Select bdv xml..." );
+            int returnVal = chooser.showOpenDialog(null );
+            if (returnVal == JFileChooser.APPROVE_OPTION) {
+                File xmlLocation = new File( chooser.getSelectedFile().getAbsolutePath() );
+                final GenericDialog gd = new GenericDialog("Add Bdv Format Image To Project...");
+                String[] addMethods = new String[]{ ProjectCreator.AddMethod.link.toString(),
+                        ProjectCreator.AddMethod.copy.toString(), ProjectCreator.AddMethod.move.toString() };
+                gd.addChoice("Add method:", addMethods, addMethods[0]);
+                String[] imageTypes = new String[]{ ProjectCreator.ImageType.image.toString(),
+                        ProjectCreator.ImageType.segmentation.toString() };
+                gd.addChoice("Image Type", imageTypes, imageTypes[0]);
+                gd.addCheckbox( "Create view for this image", true );
+                gd.addMessage( "Note: You can only 'link' to images outside \n" +
+                        "the project folder for local projects. \n " +
+                        "'copy' or 'move' if you wish to upload to s3");
+
+                gd.showDialog();
+
+                if (!gd.wasCanceled()) {
+                    ProjectCreator.AddMethod addMethod = ProjectCreator.AddMethod.valueOf( gd.getNextChoice() );
+                    ProjectCreator.ImageType imageType = ProjectCreator.ImageType.valueOf( gd.getNextChoice() );
+                    boolean createView = gd.getNextBoolean();
+                    String imageName = FileNameUtils.getBaseName(xmlLocation.getAbsolutePath());
+
+                    try {
+                        String uiSelectionGroup = null;
+                        if ( createView ) {
+                            uiSelectionGroup = selectUiSelectionGroupDialog( datasetName );
+                            if ( uiSelectionGroup != null ) {
+                                projectsCreator.getImagesCreator().addBdvFormatImage( xmlLocation, datasetName, imageType,
+                                        addMethod, uiSelectionGroup );
+                                updateComboBoxesForNewImage( imageName, uiSelectionGroup );
+                            }
+                        } else {
+                            projectsCreator.getImagesCreator().addBdvFormatImage( xmlLocation, datasetName, imageType,
+                                    addMethod, uiSelectionGroup );
+                            updateComboBoxesForNewImage( imageName, uiSelectionGroup );
+                        }
+                    } catch (SpimDataException | IOException e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+        } else {
+            IJ.log( "Add image failed - create a dataset first" );
+        }
+    }
+
+    public void addDatasetDialog () {
+        final GenericDialog gd = new GenericDialog( "Create a new dataset" );
+        gd.addStringField( "Name of dataset", "", 35 );
+        gd.showDialog();
+
+        if ( !gd.wasCanceled() ) {
+            String datasetName = gd.getNextString();
+            datasetName = tidyString( datasetName );
+
+            if ( datasetName != null ) {
+                projectsCreator.getDatasetsCreator().addDataset(datasetName);
+                updateDatasetsComboBox( datasetName );
+            }
+        }
+    }
+
+    private void editDatasetDialog() {
+        final GenericDialog gd = new GenericDialog( "Edit dataset..." );
+        String oldName = (String) datasetComboBox.getSelectedItem();
+        if ( !oldName.equals("") ) {
+            boolean isDefault = projectsCreator.getProject().getDefaultDataset().equals(oldName);
+
+            gd.addStringField("Dataset name", oldName, 35);
+            if (isDefault) {
+                gd.addMessage("     This dataset is the default");
+            } else {
+                gd.addCheckbox("Make default dataset", false);
+            }
+            gd.showDialog();
+
+            if (!gd.wasCanceled()) {
+                String newName = gd.getNextString();
+                newName = tidyString( newName );
+                if ( newName != null ) {
+                    projectsCreator.getDatasetsCreator().renameDataset( oldName, newName );
+                    updateDatasetsComboBox( newName );
+                }
+
+                if (!isDefault) {
+                    boolean makeDefault = gd.getNextBoolean();
+                    if (makeDefault) {
+                        projectsCreator.getDatasetsCreator().makeDefaultDataset( newName );
+                    }
+                }
+
+            }
+        }
+    }
+
+    private void updateDatasetsComboBox( String selection ) {
+        datasetComboBox.removeAllItems();
+        for ( String datasetName : projectsCreator.getProject().getDatasets() ) {
+            datasetComboBox.addItem(datasetName);
+        }
+
+        datasetComboBox.setSelectedItem( selection );
+    }
+
+    private void updateSourcesComboBox(String selection ) {
+        updateSourcesComboBox();
+        sourcesComboBox.setSelectedItem( selection );
+    }
+
+    private void updateSourcesComboBox() {
+        String currentDataset = (String) datasetComboBox.getSelectedItem();
+
+        if ( currentDataset != null && !currentDataset.equals("") ) {
+            sourcesComboBox.removeAllItems();
+            Dataset dataset = projectsCreator.getDataset( currentDataset );
+            if ( dataset != null && dataset.sources.keySet().size() > 0 ) {
+                for (String sourceName : dataset.sources.keySet() ) {
+                    sourcesComboBox.addItem( sourceName );
+                }
+            } else {
+                sourcesComboBox.addItem( "" );
+            }
+        }
+    }
+
+    private void updateGroupsComboBox( String selection ) {
+        updateGroupsComboBox();
+        groupsComboBox.setSelectedItem( selection );
+    }
+
+    private void updateGroupsComboBox() {
+        String currentDataset = (String) datasetComboBox.getSelectedItem();
+        if ( currentDataset != null && !currentDataset.equals("") ) {
+            groupsComboBox.removeAllItems();
+            String[] groups = projectsCreator.getGroups( currentDataset );
+            if ( groups != null && groups.length > 0 ) {
+                for ( String group : groups ) {
+                    groupsComboBox.addItem( group );
+                }
+            } else {
+                groupsComboBox.addItem( "" );
+            }
+        }
+    }
+
+    private void updateViewsComboBox() {
+        String currentDataset = (String) datasetComboBox.getSelectedItem();
+        String currentGroup = (String) groupsComboBox.getSelectedItem();
+
+        if ( currentDataset != null && !currentDataset.equals("") && currentGroup != null && !currentGroup.equals("") ) {
+            viewsComboBox.removeAllItems();
+            String[] views = projectsCreator.getViews( currentDataset, currentGroup );
+            if ( views != null && views.length > 0 ) {
+                for ( String view: views ) {
+                    viewsComboBox.addItem(view);
+                }
+            } else {
+                viewsComboBox.addItem( "" );
+            }
+        }
+    }
+
+
+}
diff --git a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/Dataset.java b/src/main/java/de/embl/cba/mobie/projects/projectsCreator/Dataset.java
deleted file mode 100644
index 7460e998c..000000000
--- a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/Dataset.java
+++ /dev/null
@@ -1,113 +0,0 @@
-package de.embl.cba.mobie.projects.projectsCreator;
-
-import de.embl.cba.mobie.bookmark.Bookmark;
-import de.embl.cba.mobie.bookmark.BookmarkReader;
-import de.embl.cba.mobie.image.ImageProperties;
-import de.embl.cba.mobie.image.ImagesJsonParser;
-import org.apache.commons.compress.utils.FileNameUtils;
-
-import java.io.File;
-import java.io.FilenameFilter;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-
-public class Dataset {
-
-    Project project;
-    String name;
-    Map<String, ImageProperties> imagePropertiesMap;
-    private Map<String, Bookmark> defaultBookmarks;
-
-    public Dataset( Project project, String name ) {
-        this.project = project;
-        this.name = name;
-        this.imagePropertiesMap = new HashMap<>();
-        this.defaultBookmarks = new HashMap<>();
-        updateImageProperties();
-        updateDefaultBookmarks();
-    }
-
-    public void updateImageProperties() {
-        File imagesJSON = new File( project.getImagesJsonPath( name ) );
-
-        if ( imagesJSON.exists() ) {
-            imagePropertiesMap = new ImagesJsonParser( project.getDatasetDirectoryPath( name ) ).getImagePropertiesMap();
-        } else {
-            imagePropertiesMap = new HashMap<>();
-        }
-    }
-
-    public void updateDefaultBookmarks() {
-        File defaultBookmarkJson = new File ( project.getDefaultBookmarkJsonPath( name ) );
-        if ( defaultBookmarkJson.exists() ) {
-            defaultBookmarks = new BookmarkReader( project.getDatasetDirectoryPath( name ) ).readDefaultBookmarks();
-        } else {
-            defaultBookmarks = new HashMap<>();
-        }
-    }
-
-    public ImageProperties getImageProperties( String imageName ) {
-        return imagePropertiesMap.get(imageName);
-    }
-
-    public Map<String, ImageProperties> getImagePropertiesMap () {
-        return imagePropertiesMap;
-    }
-
-    public String[] getImageNames() {
-        if ( imagePropertiesMap.size() > 0 ) {
-            Set<String> imageNames = imagePropertiesMap.keySet();
-            String[] imageNamesArray = new String[imageNames.size()];
-            imageNames.toArray( imageNamesArray );
-            return imageNamesArray;
-        } else {
-            return new String[] {""};
-        }
-    }
-
-    public Set<String> getImageNamesInDefaultBookmark() {
-        return defaultBookmarks.get( "default" ).layers.keySet();
-    }
-
-    public Map<String, Bookmark> getDefaultBookmarks() {
-        return defaultBookmarks;
-    }
-
-    public boolean isInDefaultBookmark (String imageName ) {
-        return getImageNamesInDefaultBookmark().contains( imageName );
-    }
-
-    public String[] getTableNames( String imageName ) {
-        File tableFolder = new File( project.getTablesDirectoryPath( name, imageName ) );
-        File[] tableFiles = tableFolder.listFiles(new FilenameFilter() {
-            public boolean accept(File dir, String name) {
-                return name.toLowerCase().endsWith(".csv") || name.toLowerCase().endsWith(".tsv");
-            }
-        });
-
-        if ( !(tableFiles.length > 0) ) {
-            return null;
-        }
-
-        // we don't include the default table here, as it is always shown
-        ArrayList<String> tableNames = new ArrayList<>();
-        for (int i = 0; i< tableFiles.length; i++) {
-            String tableName = FileNameUtils.getBaseName( tableFiles[i].getAbsolutePath() );
-            if (!tableName.equals("default")) {
-                tableNames.add( tableName );
-            }
-        }
-
-        if ( !(tableNames.size() > 0) ) {
-            return null;
-        }
-
-        String[] tableNamesArray = new String[tableNames.size()];
-        tableNamesArray = tableNames.toArray( tableNamesArray );
-
-        return tableNamesArray;
-    }
-
-}
diff --git a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/DatasetsCreator.java b/src/main/java/de/embl/cba/mobie/projects/projectsCreator/DatasetsCreator.java
deleted file mode 100644
index d95fa903b..000000000
--- a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/DatasetsCreator.java
+++ /dev/null
@@ -1,109 +0,0 @@
-package de.embl.cba.mobie.projects.projectsCreator;
-
-import de.embl.cba.mobie.dataset.Datasets;
-import de.embl.cba.mobie.dataset.DatasetsParser;
-import de.embl.cba.mobie.utils.Utils;
-import de.embl.cba.tables.FileAndUrlUtils;
-
-import java.io.File;
-import java.io.IOException;
-
-public class DatasetsCreator {
-
-    private Project project;
-
-    public DatasetsCreator( Project project ) {
-        this.project = project;
-    }
-
-    public void addDataset ( String datasetName ) {
-        boolean contains = project.isInDatasets(datasetName);
-        if (!contains) {
-            File datasetDir = new File ( project.getDatasetDirectoryPath( datasetName ) );
-
-            if ( !datasetDir.exists() ) {
-                datasetDir.mkdirs();
-
-                // make rest of folders required under dataset
-                new File(datasetDir, "images").mkdirs();
-                new File(datasetDir, "misc").mkdirs();
-                new File(datasetDir, "tables").mkdirs();
-                new File(FileAndUrlUtils.combinePath(datasetDir.getAbsolutePath(), "images", "local")).mkdirs();
-                new File(FileAndUrlUtils.combinePath(datasetDir.getAbsolutePath(), "images", "remote")).mkdirs();
-                new File(FileAndUrlUtils.combinePath(datasetDir.getAbsolutePath(), "misc", "bookmarks")).mkdirs();
-
-
-                // if this is the first dataset, then make this the default
-                Datasets currentDatasets = project.getCurrentDatasets();
-                if ( currentDatasets.datasets.size() == 0) {
-                    currentDatasets.defaultDataset = datasetName;
-                }
-                currentDatasets.datasets.add(datasetName);
-                try {
-                    writeDatasetsJson( currentDatasets );
-                } catch (IOException e) {
-                    e.printStackTrace();
-                }
-            } else {
-                Utils.log( "Dataset creation failed - this name already exists" );
-            }
-        } else {
-            Utils.log("Add dataset failed - dataset already exists");
-        }
-    }
-
-    public void renameDataset( String oldName, String newName ) {
-
-        if ( !newName.equals(oldName) ) {
-            // check not already in datasets
-            boolean contains = project.isInDatasets(newName);
-            if (!contains) {
-                File oldDatasetDir = new File(project.getDatasetDirectoryPath(oldName));
-                File newDatasetDir = new File(project.getDatasetDirectoryPath(newName));
-
-                if (oldDatasetDir.exists()) {
-                    if (oldDatasetDir.renameTo(newDatasetDir)) {
-
-                        Datasets currentDatasets = project.getCurrentDatasets();
-                        // update json
-                        if (currentDatasets.defaultDataset.equals(oldName)) {
-                            currentDatasets.defaultDataset = newName;
-                        }
-
-                        int indexOld = currentDatasets.datasets.indexOf(oldName);
-                        currentDatasets.datasets.set(indexOld, newName);
-
-                        try {
-                            writeDatasetsJson(currentDatasets);
-                        } catch (IOException e) {
-                            e.printStackTrace();
-                        }
-                    } else {
-                        Utils.log("Rename directory failed");
-                    }
-                } else {
-                    Utils.log("Rename dataset failed - that dataset doesn't exist");
-                }
-            } else {
-                Utils.log( "Rename dataset failed - there is already a dataset of that name" );
-            }
-        }
-
-    }
-
-    public void makeDefaultDataset ( String datasetName ) {
-        Datasets currentDatasets = project.getCurrentDatasets();
-        currentDatasets.defaultDataset = datasetName;
-        try {
-            writeDatasetsJson( currentDatasets );
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
-    }
-
-    private void writeDatasetsJson( Datasets datasets ) throws IOException {
-        new DatasetsParser().datasetsToFile( project.getDatasetsJsonPath(), datasets);
-        project.updateDatasets();
-    }
-
-}
diff --git a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/DefaultBookmarkCreator.java b/src/main/java/de/embl/cba/mobie/projects/projectsCreator/DefaultBookmarkCreator.java
deleted file mode 100644
index 01071511d..000000000
--- a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/DefaultBookmarkCreator.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package de.embl.cba.mobie.projects.projectsCreator;
-
-import de.embl.cba.mobie.bookmark.Bookmark;
-import de.embl.cba.mobie.bookmark.write.BookmarkFileWriter;
-import de.embl.cba.mobie.image.ImageProperties;
-import de.embl.cba.mobie.image.MutableImageProperties;
-import de.embl.cba.mobie.utils.Utils;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-
-public class DefaultBookmarkCreator {
-
-    Project project;
-
-    public DefaultBookmarkCreator( Project project ) {
-        this.project = project;
-    }
-
-    public void createDefaultBookmark ( String imageName, String datasetName ) {
-        File defaultBookmarkJson = new File( project.getDefaultBookmarkJsonPath(datasetName) );
-        if ( !defaultBookmarkJson.exists() ) {
-            HashMap<String, MutableImageProperties> layers = new HashMap<>();
-            layers.put(imageName, project.getDataset(datasetName).getImageProperties(imageName));
-
-            Bookmark defaultBookmark = new Bookmark();
-            defaultBookmark.name = "default";
-            defaultBookmark.layers = layers;
-
-            Map<String, Bookmark> defaultBookmarks = project.getDataset(datasetName).getDefaultBookmarks();
-            defaultBookmarks.put("default", defaultBookmark);
-            writeDefaultBookmarksJson( datasetName, defaultBookmarks );
-        }
-    }
-
-    public void addImageToDefaultBookmark( String imageName, String datasetName ) {
-        Map<String, Bookmark> defaultBookmarks = project.getDataset(datasetName).getDefaultBookmarks();
-        defaultBookmarks.get( "default" ).layers.put( imageName, project.getDataset( datasetName ).getImageProperties( imageName) );
-        writeDefaultBookmarksJson( datasetName, defaultBookmarks );
-    }
-
-    public void setImagePropertiesInDefaultBookmark ( String imageName, String datasetName,
-                                                      ImageProperties imageProperties ) {
-        Map<String, Bookmark> defaultBookmarks = project.getDataset(datasetName).getDefaultBookmarks();
-        defaultBookmarks.get( "default" ).layers.put( imageName, imageProperties );
-        writeDefaultBookmarksJson( datasetName, defaultBookmarks );
-    }
-
-    public void removeImageFromDefaultBookmark( String imageName, String datasetName ) {
-        Set<String> currentImages = project.getDataset(datasetName).getImageNamesInDefaultBookmark();
-        if ( currentImages.size() > 1 ) {
-            Map<String, Bookmark> defaultBookmarks = project.getDataset(datasetName).getDefaultBookmarks();
-            defaultBookmarks.get( "default" ).layers.remove( imageName );
-            writeDefaultBookmarksJson( datasetName, defaultBookmarks );
-        } else {
-            Utils.log( "can't make image non-default - you need at least one default image" );
-        }
-
-    }
-
-    private void writeDefaultBookmarksJson ( String datasetName, Map<String, Bookmark> defaultBookmarks ) {
-        try {
-            BookmarkFileWriter.saveBookmarksToFile( defaultBookmarks,
-                    new File ( project.getDefaultBookmarkJsonPath( datasetName )) );
-            project.getDataset( datasetName ).updateDefaultBookmarks();
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
-    }
-}
diff --git a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/ImagesCreator.java b/src/main/java/de/embl/cba/mobie/projects/projectsCreator/ImagesCreator.java
deleted file mode 100644
index 0717ca921..000000000
--- a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/ImagesCreator.java
+++ /dev/null
@@ -1,285 +0,0 @@
-package de.embl.cba.mobie.projects.projectsCreator;
-
-import bdv.img.n5.N5ImageLoader;
-import bdv.spimdata.SpimDataMinimal;
-import bdv.spimdata.XmlIoSpimDataMinimal;
-import de.embl.cba.bdv.utils.sources.LazySpimSource;
-import de.embl.cba.mobie.n5.DownsampleBlock;
-import de.embl.cba.mobie.n5.WriteImgPlusToN5;
-import de.embl.cba.mobie.projects.projectsCreator.ui.ManualN5ExportPanel;
-import de.embl.cba.mobie.utils.Utils;
-import de.embl.cba.tables.FileAndUrlUtils;
-import de.embl.cba.tables.Tables;
-import ij.ImagePlus;
-import mpicbg.spim.data.SpimDataException;
-import mpicbg.spim.data.generic.sequence.BasicImgLoader;
-import mpicbg.spim.data.registration.ViewRegistration;
-import mpicbg.spim.data.registration.ViewRegistrations;
-import mpicbg.spim.data.sequence.ImgLoader;
-import net.imglib2.RandomAccessibleInterval;
-import net.imglib2.realtransform.AffineTransform3D;
-import net.imglib2.roi.labeling.ImgLabeling;
-import net.imglib2.roi.labeling.LabelRegion;
-import net.imglib2.roi.labeling.LabelRegions;
-import net.imglib2.type.numeric.integer.IntType;
-import org.apache.commons.compress.utils.FileNameUtils;
-import org.apache.commons.io.FileUtils;
-import org.janelia.saalfeldlab.n5.GzipCompression;
-
-import javax.swing.*;
-import java.io.File;
-import java.io.IOException;
-import java.util.*;
-
-import static de.embl.cba.mobie.utils.ExportUtils.getBdvFormatFromSpimDataMinimal;
-import static de.embl.cba.mobie.utils.ExportUtils.getImageLocationFromSpimDataMinimal;
-import static de.embl.cba.morphometry.Utils.labelMapAsImgLabeling;
-
-public class ImagesCreator {
-
-    Project project;
-    ImagesJsonCreator imagesJsonCreator;
-    DefaultBookmarkCreator defaultBookmarkCreator;
-
-    public ImagesCreator( Project project, ImagesJsonCreator imagesJsonCreator,
-                          DefaultBookmarkCreator defaultBookmarkCreator ) {
-        this.project = project;
-        this.imagesJsonCreator = imagesJsonCreator;
-        this.defaultBookmarkCreator = defaultBookmarkCreator;
-    }
-
-    public void addImage ( ImagePlus imp, String imageName, String datasetName,
-                           ProjectsCreator.BdvFormat bdvFormat, ProjectsCreator.ImageType imageType,
-                          AffineTransform3D sourceTransform, boolean useDefaultSettings ) {
-        String xmlPath = project.getLocalImageXmlPath( datasetName, imageName);
-        File xmlFile = new File( xmlPath );
-
-        DownsampleBlock.DownsamplingMethod downsamplingMethod;
-        switch( imageType ) {
-            case image:
-                downsamplingMethod = DownsampleBlock.DownsamplingMethod.Average;
-                break;
-            default:
-                downsamplingMethod = DownsampleBlock.DownsamplingMethod.Centre;
-        }
-
-        if ( !xmlFile.exists() ) {
-            switch( bdvFormat ) {
-                case n5:
-                    if (!useDefaultSettings) {
-                        new ManualN5ExportPanel(imp, xmlPath, sourceTransform, downsamplingMethod).getManualExportParameters();
-                    } else {
-                        // gzip compression by default
-                        new WriteImgPlusToN5().export(imp, xmlPath, sourceTransform, downsamplingMethod,
-                                new GzipCompression());
-                    }
-            }
-
-            // check image written successfully, before writing jsons
-            if ( xmlFile.exists() ) {
-                updateTableAndJsonsForNewImage( imageName, imageType, datasetName );
-            }
-        } else {
-            Utils.log( "Adding image to project failed - this image name already exists" );
-        }
-    }
-
-    public void addBdvFormatImage ( File xmlLocation, String datasetName, ProjectsCreator.ImageType imageType,
-                                   ProjectsCreator.AddMethod addMethod ) throws SpimDataException, IOException {
-        if ( xmlLocation.exists() ) {
-            SpimDataMinimal spimDataMinimal = new XmlIoSpimDataMinimal().load(xmlLocation.getAbsolutePath());
-            String imageName = FileNameUtils.getBaseName(xmlLocation.getAbsolutePath());
-            File newXmlDirectory = new File(FileAndUrlUtils.combinePath(project.getImagesDirectoryPath( datasetName ), "local"));
-            File newXmlFile = new File(newXmlDirectory, imageName + ".xml");
-
-            if ( !newXmlFile.exists() ) {
-                // check n5 format (e.g. we no longer support hdf5)
-                ProjectsCreator.BdvFormat bdvFormat = getBdvFormatFromSpimDataMinimal( spimDataMinimal );
-                if ( bdvFormat != null ) {
-                    switch (addMethod) {
-                        case link:
-                            new XmlIoSpimDataMinimal().save(spimDataMinimal, newXmlFile.getAbsolutePath());
-                            break;
-                        case copy:
-                            copyImage(bdvFormat, spimDataMinimal, newXmlDirectory, imageName);
-                            break;
-                        case move:
-                            moveImage(bdvFormat, spimDataMinimal, newXmlDirectory, imageName);
-                            break;
-                    }
-                    updateTableAndJsonsForNewImage( imageName, imageType, datasetName );
-                } else {
-                    Utils.log( "Image is of unsupported type. Must be n5.");
-                }
-            } else {
-                Utils.log("Adding image to project failed - this image name already exists");
-            }
-        } else {
-            Utils.log( "Adding image to project failed - xml does not exist" );
-        }
-    }
-
-    // TODO - is this efficient for big images?
-    private void addDefaultTableForImage ( String imageName, String datasetName ) {
-        File tableFolder = new File( project.getTablesDirectoryPath( datasetName, imageName ));
-        File defaultTable = new File( tableFolder, "default.csv");
-        if ( !tableFolder.exists() ){
-            tableFolder.mkdirs();
-        }
-
-        if ( !defaultTable.exists() ) {
-
-            Utils.log( " Creating default table... 0 label is counted as background" );
-
-            String[] columnNames = {"label_id", "anchor_x", "anchor_y",
-                    "anchor_z", "bb_min_x", "bb_min_y", "bb_min_z", "bb_max_x",
-                    "bb_max_y", "bb_max_z"};
-
-            final LazySpimSource labelsSource = new LazySpimSource("labelImage",
-                    project.getLocalImageXmlPath(datasetName, imageName));
-
-            final RandomAccessibleInterval<IntType> rai = labelsSource.getNonVolatileSource(0, 0);
-            double[] dimensions = new double[ rai.numDimensions() ];
-            labelsSource.getVoxelDimensions().dimensions( dimensions );
-
-            ImgLabeling<Integer, IntType> imgLabeling = labelMapAsImgLabeling(rai);
-
-            LabelRegions labelRegions = new LabelRegions(imgLabeling);
-            Iterator<LabelRegion> labelRegionIterator = labelRegions.iterator();
-
-            ArrayList<Object[]> rows = new ArrayList<>();
-            while (labelRegionIterator.hasNext()) {
-                Object[] row = new Object[columnNames.length];
-                LabelRegion labelRegion = labelRegionIterator.next();
-
-                double[] centre = new double[rai.numDimensions()];
-                labelRegion.getCenterOfMass().localize(centre);
-                double[] bbMin = new double[rai.numDimensions()];
-                double[] bbMax = new double[rai.numDimensions()];
-                labelRegion.realMin(bbMin);
-                labelRegion.realMax(bbMax);
-
-                row[0] = labelRegion.getLabel();
-                row[1] = centre[0] * dimensions[0];
-                row[2] = centre[1] * dimensions[1];
-                row[3] = centre[2] * dimensions[2];
-                row[4] = bbMin[0] * dimensions[0];
-                row[5] = bbMin[1] * dimensions[1];
-                row[6] = bbMin[2] * dimensions[2];
-                row[7] = bbMax[0] * dimensions[0];
-                row[8] = bbMax[1] * dimensions[1];
-                row[9] = bbMax[2] * dimensions[2];
-
-                rows.add(row);
-            }
-
-            Object[][] rowArray = new Object[rows.size()][columnNames.length];
-            rowArray = rows.toArray(rowArray);
-
-            JTable table = new JTable(rowArray, columnNames);
-            Tables.saveTable(table, new File(FileAndUrlUtils.combinePath(tableFolder.getAbsolutePath(), "default.csv")));
-
-            Utils.log( "Default table complete" );
-        }
-    }
-
-    private void updateTableAndJsonsForNewImage ( String imageName, ProjectsCreator.ImageType imageType,
-                                          String datasetName ) {
-        if ( imageType == ProjectsCreator.ImageType.segmentation) {
-            addDefaultTableForImage( imageName, datasetName );
-        }
-        imagesJsonCreator.addToImagesJson( imageName, datasetName, imageType );
-        // if there's no default json, create one with this image
-        defaultBookmarkCreator.createDefaultBookmark( imageName, datasetName );
-    }
-
-    private void copyImage (ProjectsCreator.BdvFormat bdvFormat, SpimDataMinimal spimDataMinimal, File newXmlDirectory, String imageName ) throws IOException, SpimDataException {
-        File newImageFile = new File( newXmlDirectory, imageName + "." + bdvFormat );
-        File imageLocation = getImageLocationFromSpimDataMinimal( spimDataMinimal, bdvFormat );
-
-        switch ( bdvFormat ) {
-            case n5:
-                FileUtils.copyDirectory(imageLocation, newImageFile );
-                break;
-        }
-
-        writeNewBdvXml( spimDataMinimal, newImageFile, newXmlDirectory, imageName, bdvFormat);
-    }
-
-    private void moveImage (ProjectsCreator.BdvFormat bdvFormat, SpimDataMinimal spimDataMinimal, File newXmlDirectory, String imageName ) throws IOException, SpimDataException {
-        File newImageFile = new File( newXmlDirectory, imageName + "." + bdvFormat );
-        File imageLocation = getImageLocationFromSpimDataMinimal( spimDataMinimal, bdvFormat );
-
-        // have to explicitly close the image loader, so we can delete the original file
-        closeImgLoader( spimDataMinimal, bdvFormat );
-
-        switch ( bdvFormat ) {
-            case n5:
-                FileUtils.moveDirectory( imageLocation, newImageFile );
-                break;
-        }
-
-        writeNewBdvXml( spimDataMinimal, newImageFile, newXmlDirectory, imageName, bdvFormat );
-    }
-
-    private void closeImgLoader ( SpimDataMinimal spimDataMinimal, ProjectsCreator.BdvFormat bdvFormat ) {
-        BasicImgLoader imgLoader = spimDataMinimal.getSequenceDescription().getImgLoader();
-
-        switch ( bdvFormat ) {
-            case n5:
-                N5ImageLoader n5ImageLoader = (N5ImageLoader) imgLoader;
-                n5ImageLoader.close();
-                break;
-        }
-    }
-
-    private void writeNewBdvXml ( SpimDataMinimal spimDataMinimal, File imageFile, File saveDirectory, String imageName,
-                                  ProjectsCreator.BdvFormat bdvFormat ) throws SpimDataException {
-
-        ImgLoader imgLoader = null;
-        switch ( bdvFormat ) {
-            case n5:
-                imgLoader = new N5ImageLoader( imageFile, null);
-                break;
-        }
-
-        spimDataMinimal.setBasePath( saveDirectory );
-        spimDataMinimal.getSequenceDescription().setImgLoader(imgLoader);
-        new XmlIoSpimDataMinimal().save(spimDataMinimal, new File( saveDirectory, imageName + ".xml").getAbsolutePath() );
-    }
-
-    private void addAffineTransformToXml ( String xmlPath, String affineTransform )  {
-        if ( affineTransform != null ) {
-            try {
-                SpimDataMinimal spimDataMinimal = new XmlIoSpimDataMinimal().load( xmlPath );
-                int numTimepoints = spimDataMinimal.getSequenceDescription().getTimePoints().size();
-                int numSetups = spimDataMinimal.getSequenceDescription().getViewSetupsOrdered().size();
-
-                AffineTransform3D sourceTransform = new AffineTransform3D();
-                String[] splitAffineTransform = affineTransform.split(" ");
-                double[] doubleAffineTransform = new double[splitAffineTransform.length];
-                for ( int i = 0; i < splitAffineTransform.length; i++ ) {
-                    doubleAffineTransform[i] = Double.parseDouble( splitAffineTransform[i] );
-                }
-                sourceTransform.set( doubleAffineTransform );
-
-                final ArrayList<ViewRegistration> registrations = new ArrayList<>();
-                for ( int t = 0; t < numTimepoints; ++t ) {
-                    for (int s = 0; s < numSetups; ++s) {
-                        registrations.add(new ViewRegistration(t, s, sourceTransform));
-                    }
-                }
-
-                SpimDataMinimal updatedSpimDataMinimial = new SpimDataMinimal(spimDataMinimal.getBasePath(),
-                        spimDataMinimal.getSequenceDescription(), new ViewRegistrations( registrations) );
-
-                new XmlIoSpimDataMinimal().save( updatedSpimDataMinimial, xmlPath);
-            } catch (SpimDataException e) {
-                Utils.log( "Error adding affine transform to xml file. Check xml manually.");
-                e.printStackTrace();
-            }
-
-        }
-    }
-
-}
diff --git a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/ImagesJsonCreator.java b/src/main/java/de/embl/cba/mobie/projects/projectsCreator/ImagesJsonCreator.java
deleted file mode 100644
index c385a136d..000000000
--- a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/ImagesJsonCreator.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package de.embl.cba.mobie.projects.projectsCreator;
-
-import de.embl.cba.mobie.image.ImageProperties;
-import de.embl.cba.mobie.image.ImagesJsonParser;
-import de.embl.cba.mobie.image.Storage;
-import de.embl.cba.tables.color.ColoringLuts;
-
-import java.io.IOException;
-import java.util.Map;
-
-public class ImagesJsonCreator {
-
-    Project project;
-
-    public ImagesJsonCreator( Project project ) {
-        this.project = project;
-    }
-
-    public void addToImagesJson (String imageName, String datasetName, ProjectsCreator.ImageType imageType ) {
-
-        Map<String, ImageProperties> currentImageProperties = project.getDataset( datasetName ).getImagePropertiesMap();
-        ImageProperties newImageProperties = new ImageProperties();
-        newImageProperties.type = imageType.toString();
-
-        switch( imageType ) {
-            case segmentation:
-                newImageProperties.color = ColoringLuts.GLASBEY;
-                newImageProperties.tableFolder = "tables/" + imageName;
-                break;
-            default:
-                newImageProperties.color = "white";
-        }
-
-        newImageProperties.contrastLimits = new double[] {0, 255};
-
-        Storage storage = new Storage();
-        storage.local = "local/" + imageName + ".xml";
-        newImageProperties.storage = storage;
-
-        currentImageProperties.put( imageName, newImageProperties);
-
-        writeImagesJson( datasetName, currentImageProperties );
-    }
-
-    public void setImagePropertiesInImagesJson( String imageName, String datasetName, ImageProperties imageProperties ) {
-        Map<String, ImageProperties> currentImageProperties = project.getDataset( datasetName ).getImagePropertiesMap();
-        currentImageProperties.put( imageName, imageProperties );
-        writeImagesJson( datasetName, currentImageProperties );
-    }
-
-    private void writeImagesJson ( String datasetName, Map<String, ImageProperties> imagePropertiesMap ) {
-        try {
-            new ImagesJsonParser( project.getDatasetDirectoryPath( datasetName) ).writeImagePropertiesMap(
-                    project.getImagesJsonPath( datasetName),
-                    imagePropertiesMap );
-            project.getDataset( datasetName ).updateImageProperties();
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
-    }
-}
diff --git a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/Project.java b/src/main/java/de/embl/cba/mobie/projects/projectsCreator/Project.java
deleted file mode 100644
index 0830602da..000000000
--- a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/Project.java
+++ /dev/null
@@ -1,123 +0,0 @@
-package de.embl.cba.mobie.projects.projectsCreator;
-
-import de.embl.cba.mobie.dataset.Datasets;
-import de.embl.cba.mobie.dataset.DatasetsParser;
-import de.embl.cba.tables.FileAndUrlUtils;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-
-public class Project {
-    private final File projectLocation;
-    private final File dataLocation;
-
-    private Datasets currentDatasets;
-    private Map<String, Dataset> datasetNameToDataset;
-
-    public Project( File projectLocation ) {
-        this.projectLocation = projectLocation;
-        this.dataLocation = new File( projectLocation, "data");
-        this.datasetNameToDataset = new HashMap<>();
-        updateDatasets();
-    }
-
-    public void updateDatasets() {
-        File datasetJSON = new File( getDatasetsJsonPath() );
-
-        if ( datasetJSON.exists() ) {
-            currentDatasets = new DatasetsParser().fetchProjectDatasets( dataLocation.getAbsolutePath() );
-        } else {
-            currentDatasets = new Datasets();
-            currentDatasets.datasets = new ArrayList<>();
-        }
-    }
-
-    public Dataset getDataset( String datasetName ) {
-        if ( currentDatasets.datasets.contains(datasetName) ) {
-            // Datasets are created lazily, so only create if not already there
-            if ( !datasetNameToDataset.containsKey(datasetName) ) {
-                datasetNameToDataset.put( datasetName, new Dataset(this, datasetName ));
-            }
-            return datasetNameToDataset.get(datasetName);
-        } else {
-            return null;
-        }
-    }
-
-    public String[] getDatasetNames() {
-        if ( currentDatasets.datasets.size() > 0 ) {
-            ArrayList<String> datasetNames = currentDatasets.datasets;
-            String[] datasetNamesArray = new String[datasetNames.size()];
-            datasetNames.toArray( datasetNamesArray );
-            return datasetNamesArray;
-        } else {
-            return new String[] {""};
-        }
-    }
-
-    public Datasets getCurrentDatasets() {
-        return currentDatasets;
-    }
-
-    public int getNumberOfDatasets() {
-        return currentDatasets.datasets.size();
-    }
-
-    public boolean isInDatasets ( String datasetName ) {
-        return Arrays.stream( getDatasetNames() ).anyMatch(datasetName::equals);
-    }
-
-    public boolean isDefaultDataset( String datasetName ) {
-        return currentDatasets.defaultDataset.equals( datasetName );
-    }
-
-    public File getDataLocation() {
-        return dataLocation;
-    }
-
-    public String getDatasetDirectoryPath(String datasetName ) {
-        return FileAndUrlUtils.combinePath( dataLocation.getAbsolutePath(), datasetName);
-    }
-
-    public String getImagesDirectoryPath( String datasetName ) {
-        return FileAndUrlUtils.combinePath( dataLocation.getAbsolutePath(), datasetName, "images");
-    }
-
-    public String getLocalImagesDirectoryPath( String datasetName ) {
-        return FileAndUrlUtils.combinePath( dataLocation.getAbsolutePath(), datasetName, "images", "local" );
-    }
-
-    public String getRemoteImagesDirectoryPath( String datasetName ) {
-        return FileAndUrlUtils.combinePath( dataLocation.getAbsolutePath(), datasetName, "images", "remote" );
-    }
-
-    public String getImagesJsonPath( String datasetName ) {
-        return FileAndUrlUtils.combinePath( dataLocation.getAbsolutePath(), datasetName,
-                "images", "images.json");
-    }
-
-    public String getDatasetsJsonPath() {
-        File datasetJSON = new File( dataLocation, "datasets.json" );
-        return datasetJSON.getAbsolutePath();
-    }
-
-    public String getLocalImageXmlPath ( String datasetName, String imageName ) {
-        return FileAndUrlUtils.combinePath(dataLocation.getAbsolutePath(), datasetName, "images", "local", imageName + ".xml");
-    }
-
-    public String getRemoteImageXmlPath( String datasetName, String imageName ) {
-        return FileAndUrlUtils.combinePath(dataLocation.getAbsolutePath(), datasetName, "images", "remote", imageName + ".xml");
-    }
-
-    public String getTablesDirectoryPath( String datasetName, String imageName ) {
-        return FileAndUrlUtils.combinePath( getDatasetDirectoryPath( datasetName ), "tables", imageName);
-    }
-
-    public String getDefaultBookmarkJsonPath ( String datasetName ) {
-        return FileAndUrlUtils.combinePath( dataLocation.getAbsolutePath(), datasetName, "misc", "bookmarks",
-                "default.json");
-    }
-}
diff --git a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/ProjectsCreator.java b/src/main/java/de/embl/cba/mobie/projects/projectsCreator/ProjectsCreator.java
deleted file mode 100644
index b87dba50a..000000000
--- a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/ProjectsCreator.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package de.embl.cba.mobie.projects.projectsCreator;
-
-import java.io.File;
-
-public class ProjectsCreator {
-
-    private final Project project;
-    private final DatasetsCreator datasetsCreator;
-    private final ImagesCreator imagesCreator;
-    private final ImagesJsonCreator imagesJsonCreator;
-    private final DefaultBookmarkCreator defaultBookmarkCreator;
-    private final RemoteMetadataCreator remoteMetadataCreator;
-
-    public enum BdvFormat {
-        // TODO - add OME.ZARR
-        n5
-    }
-
-    public enum ImageType {
-        image,
-        segmentation,
-        mask
-    }
-
-    public enum AddMethod {
-        link,
-        copy,
-        move
-    }
-
-    public ProjectsCreator ( File projectLocation ) {
-        this.project = new Project( projectLocation );
-        this.datasetsCreator = new DatasetsCreator( project );
-        this.imagesJsonCreator = new ImagesJsonCreator( project );
-        this.defaultBookmarkCreator = new DefaultBookmarkCreator( project );
-        this.imagesCreator = new ImagesCreator( project, imagesJsonCreator, defaultBookmarkCreator );
-        this.remoteMetadataCreator = new RemoteMetadataCreator( project );
-    }
-
-    public Project getProject() {
-        return project;
-    }
-
-    public DatasetsCreator getDatasetsCreator() {
-        return datasetsCreator;
-    }
-
-    public ImagesCreator getImagesCreator() {
-        return imagesCreator;
-    }
-
-    public ImagesJsonCreator getImagesJsonCreator() {
-        return imagesJsonCreator;
-    }
-
-    public DefaultBookmarkCreator getDefaultBookmarkCreator() {
-        return defaultBookmarkCreator;
-    }
-
-    public RemoteMetadataCreator getRemoteMetadataCreator() {
-        return remoteMetadataCreator;
-    }
-}
diff --git a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/ui/ImagePropertiesEditor.java b/src/main/java/de/embl/cba/mobie/projects/projectsCreator/ui/ImagePropertiesEditor.java
deleted file mode 100644
index 316d460ec..000000000
--- a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/ui/ImagePropertiesEditor.java
+++ /dev/null
@@ -1,483 +0,0 @@
-package de.embl.cba.mobie.projects.projectsCreator.ui;
-
-import de.embl.cba.mobie.image.ImageProperties;
-import de.embl.cba.mobie.projects.projectsCreator.Dataset;
-import de.embl.cba.mobie.projects.projectsCreator.DefaultBookmarkCreator;
-import de.embl.cba.mobie.projects.projectsCreator.Project;
-import de.embl.cba.mobie.projects.projectsCreator.ProjectsCreator;
-import de.embl.cba.mobie.ui.MoBIE;
-import de.embl.cba.tables.SwingUtils;
-import de.embl.cba.tables.color.ColorUtils;
-import de.embl.cba.tables.color.ColoringLuts;
-import net.imglib2.type.numeric.ARGBType;
-import org.apache.commons.lang.WordUtils;
-
-import javax.swing.*;
-import java.awt.*;
-import java.text.Format;
-import java.text.NumberFormat;
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-
-import static de.embl.cba.mobie.utils.ui.SwingUtils.*;
-
-// TODO - add option to rename images? (more difficult as need to edit path inside the xml too)
-public class ImagePropertiesEditor {
-    private String datasetName;
-    private String imageName;
-    private ProjectsCreator projectsCreator;
-    private Project project;
-    private Dataset dataset;
-    private ImageProperties imageProperties;
-    private NumberFormat amountFormat;
-
-    private JComboBox<String> colorCombo;
-    private JCheckBox transparencyCheckBox;
-    private JTextField colorByColumnField;
-    private JFormattedTextField contrastLimitMinField;
-    private JFormattedTextField contrastLimitMaxField;
-    private JFormattedTextField valueLimitMinField;
-    private JFormattedTextField valueLimitMaxField;
-    private JFormattedTextField resolution3dViewField;
-    private JList tablesList;
-    private JTextField selectedLabelIdsField;
-    private JCheckBox showSelectedSegmentsIn3dCheckbox;
-    private JCheckBox showImageIn3dCheckbox;
-    private JCheckBox showByDefaultCheckbox;
-    private boolean zeroTransparent;
-
-
-    public ImagePropertiesEditor( String datasetName, String imageName, ProjectsCreator projectsCreator ) {
-        this.datasetName = datasetName;
-        this.imageName = imageName;
-        this.projectsCreator = projectsCreator;
-        this.project = projectsCreator.getProject();
-        this.dataset = project.getDataset( datasetName );
-        imageProperties = dataset.getImageProperties( imageName );
-
-        // Format to only accept numbers in text fields
-        amountFormat = NumberFormat.getNumberInstance();
-        amountFormat.setMaximumFractionDigits(5);
-        amountFormat.setGroupingUsed( false );
-
-        zeroTransparent = false;
-        editImagePropertiesDialog();
-    }
-
-    private JPanel createLabelPanel ( String label ) {
-        JPanel labelPanel = SwingUtils.horizontalLayoutPanel();
-        JLabel jlabel = getJLabel( label, 170, 20 );
-        jlabel.setBorder( BorderFactory.createEmptyBorder(10, 0, 10, 0) );
-        labelPanel.add( jlabel );
-        return labelPanel;
-    }
-
-    private JPanel createComboPanel (JComboBox<String> combo, String label) {
-        final JPanel comboPanel = SwingUtils.horizontalLayoutPanel();
-        setComboBoxDimensions( combo );
-        combo.setPrototypeDisplayValue( MoBIE.PROTOTYPE_DISPLAY_VALUE);
-        comboPanel.add(getJLabel(label));
-        comboPanel.add(combo);
-
-        return comboPanel;
-    }
-
-    private JPanel createComboPanel (JComboBox<String> combo, String label, JButton button) {
-        JPanel comboPanel = createComboPanel( combo, label );
-        comboPanel.add( button );
-
-        return comboPanel;
-    }
-
-    private JPanel createListPanel ( String label, JList list ) {
-        final JPanel listPanel = SwingUtils.horizontalLayoutPanel();
-        listPanel.add(getJLabel(label));
-        JScrollPane scroller = new JScrollPane( list );
-        scroller.setPreferredSize( new Dimension(250, 80));
-        listPanel.add(scroller);
-        return listPanel;
-    }
-
-    private JTextField createTextField ( String defaultValue ) {
-        JTextField textField;
-        if ( defaultValue == null ) {
-            textField = new JTextField(20);
-        } else {
-            textField = new JTextField( defaultValue, 20);
-        }
-
-        return textField;
-    }
-
-    private JPanel createTextPanel ( String label, JTextField textField ) {
-        final JPanel textPanel = SwingUtils.horizontalLayoutPanel();
-        textPanel.add(getJLabel(label));
-        textPanel.add(textField);
-
-        return textPanel;
-    }
-
-    private JFormattedTextField createFormattedTextField ( Format format, Object defaultValue ) {
-        JFormattedTextField textField = new JFormattedTextField(format);
-        if ( defaultValue != null ) {
-            textField.setValue( defaultValue );
-        }
-        textField.setFocusLostBehavior( JFormattedTextField.COMMIT_OR_REVERT );
-
-        return textField;
-    }
-
-    private JPanel createNumberPanel (String label, JFormattedTextField textField) {
-        final JPanel textPanel = SwingUtils.horizontalLayoutPanel();
-        textPanel.add(getJLabel(label));
-        textPanel.add(textField);
-
-        return textPanel;
-    }
-
-    private JButton createColorButton( JComboBox<String> colorCombo )
-    {
-        JButton colorButton;
-        colorButton = new JButton( "other color" );
-
-        colorButton.addActionListener( e ->
-        {
-            new Thread( () -> {
-                Color color = JColorChooser.showDialog( null, "", null );
-
-                if ( color == null ) return;
-
-                ARGBType colorARGB = ColorUtils.getARGBType( color );
-                colorCombo.addItem( colorARGB.toString() );
-                colorCombo.setSelectedItem( colorARGB.toString() );
-            } ).start();
-        } );
-
-        return colorButton;
-    }
-
-    private JPanel createAcceptCancelPanel( JFrame frame ) {
-        final JPanel acceptCancelPanel = SwingUtils.horizontalLayoutPanel();
-        JButton acceptButton = getButton( "Update properties", new Dimension(160, 20));
-        JButton cancelButton = getButton( "Cancel");
-
-        acceptButton.addActionListener( e ->
-        {
-            new Thread( () -> {
-                try {
-                    updateImageProperties();
-                    frame.dispose();
-                } catch (ParseException parseException) {
-                    parseException.printStackTrace();
-                }
-            } ).start();
-        } );
-
-        cancelButton.addActionListener( e ->
-        {
-            new Thread( () -> {
-                frame.dispose();
-            } ).start();
-        } );
-
-        acceptCancelPanel.add(acceptButton);
-        acceptCancelPanel.add(cancelButton);
-
-        return acceptCancelPanel;
-    }
-
-    private JPanel createColorPanel () {
-        JPanel colorPanel;
-
-        if ( imageProperties.type.equals("segmentation") ) {
-            String[] colorOptions = new String[] { ColoringLuts.GLASBEY, ColoringLuts.BLUE_WHITE_RED, ColoringLuts.VIRIDIS,
-                    ColoringLuts.ARGB_COLUMN };
-            colorCombo = new JComboBox<>( colorOptions );
-            String capitalisedColor = WordUtils.capitalize( imageProperties.color );
-
-            // deal with any 'zero transparent' options
-            if ( capitalisedColor.contains( ColoringLuts.ZERO_TRANSPARENT ) ) {
-                capitalisedColor = capitalisedColor.split( ColoringLuts.ZERO_TRANSPARENT )[0];
-                zeroTransparent = true;
-            }
-
-            colorCombo.setSelectedItem( capitalisedColor );
-            colorPanel = createComboPanel( colorCombo, "color");
-
-        } else {
-            ArrayList<String> colorOptions = new ArrayList<> ();
-            colorOptions.add("white");
-            colorOptions.add("randomFromGlasbey");
-            String currentColor = imageProperties.color;
-
-            if ( !colorOptions.contains( currentColor ) ) {
-                colorOptions.add(currentColor);
-            }
-
-            String[] colorArray = new String[ colorOptions.size() ];
-            colorOptions.toArray( colorArray );
-
-            colorCombo = new JComboBox<>( colorArray );
-            colorCombo.setSelectedItem( currentColor );
-            JButton otherColorButton = createColorButton( colorCombo );
-
-            colorPanel = createComboPanel( colorCombo, "color", otherColorButton );
-        }
-
-        return colorPanel;
-    }
-
-    private void makeTablesList ( String[] tableNamesArray ) {
-        tablesList = new JList(tableNamesArray);
-        tablesList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
-        tablesList.setLayoutOrientation(JList.VERTICAL);
-        if ( tableNamesArray.length < 3) {
-            tablesList.setVisibleRowCount(tableNamesArray.length);
-        } else {
-            tablesList.setVisibleRowCount(3);
-        }
-
-        ArrayList<String> selectedTables = imageProperties.tables;
-        if (selectedTables != null) {
-            if (selectedTables.size() > 0) {
-                ArrayList<Integer> selectedIndices = new ArrayList<>();
-                for (int i = 0; i < tablesList.getModel().getSize(); i++) {
-                    if (selectedTables.contains(tablesList.getModel().getElementAt(i))) {
-                        selectedIndices.add(i);
-                    }
-                }
-                int[] selectedIndicesArray = selectedIndices.stream().mapToInt(i -> i).toArray();
-                tablesList.setSelectedIndices(selectedIndicesArray);
-            }
-        }
-    }
-
-    private JPanel createTablesPanel() {
-        JPanel tables = null;
-        if ( imageProperties.type.equals("segmentation") ) {
-
-            String[] tableNamesArray = dataset.getTableNames( imageName );
-
-            if ( tableNamesArray != null) {
-                makeTablesList( tableNamesArray );
-                tables = createListPanel("tables", tablesList);
-            }
-        }
-
-        return tables;
-    }
-
-    public void editImagePropertiesDialog() {
-        JFrame editImageFrame = new JFrame("Edit image properties...");
-        editImageFrame.getContentPane().setLayout( new BoxLayout(editImageFrame.getContentPane(), BoxLayout.Y_AXIS ) );
-        editImageFrame.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
-
-        JPanel colorComboPanel = createColorPanel();
-
-        transparencyCheckBox = new JCheckBox("Paint Zero Transparent");
-        transparencyCheckBox.setSelected( zeroTransparent );
-
-        colorByColumnField = createTextField( imageProperties.colorByColumn );
-        final JPanel colorByColumnPanel = createTextPanel( "colorByColumn", colorByColumnField );
-
-        double[] currentContrastLimits = imageProperties.contrastLimits;
-        if ( currentContrastLimits != null ) {
-            contrastLimitMinField = createFormattedTextField( amountFormat, currentContrastLimits[0]);
-            contrastLimitMaxField = createFormattedTextField( amountFormat, currentContrastLimits[1] );
-        } else {
-            // if no contrast limits, suggest reasonable defaults
-            contrastLimitMinField = createFormattedTextField( amountFormat, 0.0);
-            contrastLimitMaxField = createFormattedTextField( amountFormat, 255.0);
-        }
-        JPanel contrastLimitMinPanel = createNumberPanel( "contrast limit min", contrastLimitMinField );
-        JPanel contrastLimitMaxPanel = createNumberPanel( "contrast limit max", contrastLimitMaxField );
-
-        double[] currentValueLimits = imageProperties.valueLimits;
-        if ( currentValueLimits != null ) {
-            valueLimitMinField = createFormattedTextField( amountFormat, currentValueLimits[0] );
-            valueLimitMaxField = createFormattedTextField( amountFormat, currentValueLimits[1] );
-        } else {
-            valueLimitMinField = createFormattedTextField( amountFormat, 0.0 );
-            valueLimitMaxField = createFormattedTextField( amountFormat, 0.0 );
-        }
-        JPanel valueLimitMinPanel = createNumberPanel( "value limit min", valueLimitMinField );
-        JPanel valueLimitMaxPanel = createNumberPanel( "value limit max", valueLimitMaxField );
-
-        resolution3dViewField = createFormattedTextField( amountFormat, imageProperties.resolution3dView );
-        final JPanel resolution3dViewPanel = createNumberPanel( "resolution 3d view", resolution3dViewField );
-
-        JPanel tablesPanel = createTablesPanel();
-
-        if ( imageProperties.selectedLabelIds != null ) {
-            ArrayList<Double> currentSelectedLabelIds = imageProperties.selectedLabelIds;
-            String currentIdsString = currentSelectedLabelIds.stream().map(Object::toString).collect(Collectors.joining(","));
-            selectedLabelIdsField = createTextField(currentIdsString);
-        } else {
-            selectedLabelIdsField = createTextField( "" );
-        }
-        JPanel selectedLabelIDsPanel = createTextPanel("selected label ids", selectedLabelIdsField);
-
-
-        showSelectedSegmentsIn3dCheckbox = new JCheckBox("Show selected segments in 3d");
-        showSelectedSegmentsIn3dCheckbox.setSelected( imageProperties.showSelectedSegmentsIn3d );
-
-        showImageIn3dCheckbox = new JCheckBox("Show image in 3d");
-        showImageIn3dCheckbox.setSelected( imageProperties.showImageIn3d );
-
-        showByDefaultCheckbox = new JCheckBox( "Show by default" );
-        showByDefaultCheckbox.setSelected( dataset.isInDefaultBookmark( imageName ));
-
-        JPanel editPropertiesPanel = new JPanel();
-        editPropertiesPanel.setLayout( new BoxLayout(editPropertiesPanel, BoxLayout.PAGE_AXIS) );
-        editPropertiesPanel.add(colorComboPanel);
-        if ( imageProperties.type.equals("segmentation")) {
-            editPropertiesPanel.add(transparencyCheckBox);
-            editPropertiesPanel.add(colorByColumnPanel);
-        }
-        editPropertiesPanel.add(contrastLimitMinPanel);
-        editPropertiesPanel.add(contrastLimitMaxPanel);
-        if ( imageProperties.type.equals("segmentation")) {
-            editPropertiesPanel.add(valueLimitMinPanel);
-            editPropertiesPanel.add(valueLimitMaxPanel);
-            if ( tablesPanel != null ) {
-                editPropertiesPanel.add(tablesPanel);
-            }
-            editPropertiesPanel.add(createLabelPanel( "List label ids e.g. 12,35,95,100"));
-            editPropertiesPanel.add(selectedLabelIDsPanel);
-        }
-        editPropertiesPanel.add(resolution3dViewPanel);
-        if ( imageProperties.type.equals("segmentation")) {
-            editPropertiesPanel.add(showSelectedSegmentsIn3dCheckbox);
-        }
-        editPropertiesPanel.add(showImageIn3dCheckbox);
-        editPropertiesPanel.add(showByDefaultCheckbox);
-        editPropertiesPanel.add( createAcceptCancelPanel( editImageFrame ));
-        editImageFrame.add(editPropertiesPanel);
-
-        editImageFrame.pack();
-        editImageFrame.setLocationRelativeTo(null);
-        editImageFrame.setVisible( true );
-
-    }
-
-    private void commitAllEdits() {
-        // ensure all edited values are commited in text fields, otherwise can have unexpected behaviour if
-        // user types and immediately clicks the accept button
-        try {
-            contrastLimitMinField.commitEdit();
-            contrastLimitMaxField.commitEdit();
-            valueLimitMinField.commitEdit();
-            valueLimitMaxField.commitEdit();
-            resolution3dViewField.commitEdit();
-        } catch (ParseException e) {
-            e.printStackTrace();
-        }
-
-    }
-
-    private boolean updateDefaultBookmarkSettingsDialog() {
-        int result = JOptionPane.showConfirmDialog(null,
-                "Update the default bookmark settings for this image?", "Update default bookmark too?",
-                JOptionPane.YES_NO_OPTION,
-                JOptionPane.QUESTION_MESSAGE);
-        if (result == JOptionPane.YES_OPTION) {
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    private void updateSelectedLabelIds() {
-        if ( !selectedLabelIdsField.getText().equals("") ) {
-            String selectedLabelIdsText = selectedLabelIdsField.getText().trim();
-            String selectedLabelIdsTextNoSpaces = selectedLabelIdsText.replaceAll("\\s+","");
-
-            // check contains nothing but numbers or . or ,
-            if ( Pattern.matches( "[0-9 | , | .]+", selectedLabelIdsTextNoSpaces )) {
-                String[] ids = selectedLabelIdsTextNoSpaces.split(",");
-                ArrayList<Double> selectedIds = new ArrayList<>();
-                for ( String id : ids ) {
-                    selectedIds.add( Double.valueOf( id ) );
-                }
-                imageProperties.selectedLabelIds = selectedIds;
-            }
-        } else {
-            imageProperties.selectedLabelIds = null;
-        }
-    }
-
-    private void updateDefaultBookmarkSettings () {
-
-        DefaultBookmarkCreator defaultBookmarkCreator = projectsCreator.getDefaultBookmarkCreator();
-        boolean imageIsInDefaultBookmark = dataset.isInDefaultBookmark( imageName );
-        // if modifying an image that is shown in default bookmark, give option to update that metadata
-        if ( showByDefaultCheckbox.isSelected() && imageIsInDefaultBookmark ) {
-            if ( updateDefaultBookmarkSettingsDialog() ) {
-                defaultBookmarkCreator.setImagePropertiesInDefaultBookmark( imageName, datasetName, imageProperties);
-            }
-        }
-
-        // if show by default has changed, modify the bookmarks json
-        if ( !(showByDefaultCheckbox.isSelected() == imageIsInDefaultBookmark) ) {
-            if ( showByDefaultCheckbox.isSelected() ) {
-                defaultBookmarkCreator.addImageToDefaultBookmark(imageName, datasetName);
-            } else {
-                defaultBookmarkCreator.removeImageFromDefaultBookmark( imageName, datasetName );
-            }
-        }
-    }
-
-    public void updateImageProperties () throws ParseException {
-
-        commitAllEdits();
-
-        String selectedColor = (String) colorCombo.getSelectedItem();
-        if ( transparencyCheckBox.isSelected() ) {
-            selectedColor = selectedColor + ColoringLuts.ZERO_TRANSPARENT;
-        }
-        imageProperties.color = selectedColor;
-
-        double contrastLimitMin = amountFormat.parse( contrastLimitMinField.getText() ).doubleValue();
-        double contrastLimitMax = amountFormat.parse( contrastLimitMaxField.getText() ).doubleValue();
-        imageProperties.contrastLimits = new double[] { contrastLimitMin, contrastLimitMax };
-
-        imageProperties.resolution3dView = amountFormat.parse( resolution3dViewField.getText() ).doubleValue();
-        imageProperties.showImageIn3d = showImageIn3dCheckbox.isSelected();
-
-        if ( imageProperties.type.equals("segmentation") ) {
-            String colorByColumn = colorByColumnField.getText().trim();
-            if ( !colorByColumn.equals("") ) {
-                imageProperties.colorByColumn = colorByColumn;
-            } else {
-                imageProperties.colorByColumn = null;
-            }
-
-            double valueLimitMin = amountFormat.parse( valueLimitMinField.getText() ).doubleValue();
-            double valueLimitMax = amountFormat.parse( valueLimitMaxField.getText() ).doubleValue();
-            if ( valueLimitMin != 0.0 && valueLimitMax != 0.0 ) {
-                imageProperties.valueLimits = new double[]{valueLimitMin, valueLimitMax};
-            } else {
-                // 0 is the default value in the text field, if both are left at zero, then this means no value limits
-                // are set i.e. null
-                imageProperties.valueLimits = null;
-            }
-
-            if ( tablesList != null ) {
-                if (tablesList.getSelectedIndices().length > 0) {
-                    imageProperties.tables = (ArrayList<String>) tablesList.getSelectedValuesList();
-                } else {
-                    imageProperties.tables = null;
-                }
-            } else {
-                imageProperties.tables = null;
-            }
-
-            updateSelectedLabelIds();
-            imageProperties.showSelectedSegmentsIn3d = showSelectedSegmentsIn3dCheckbox.isSelected();
-        }
-        projectsCreator.getImagesJsonCreator().setImagePropertiesInImagesJson( imageName, datasetName, imageProperties );
-        updateDefaultBookmarkSettings();
-    }
-}
diff --git a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/ui/ProjectsCreatorPanel.java b/src/main/java/de/embl/cba/mobie/projects/projectsCreator/ui/ProjectsCreatorPanel.java
deleted file mode 100644
index 6575daef6..000000000
--- a/src/main/java/de/embl/cba/mobie/projects/projectsCreator/ui/ProjectsCreatorPanel.java
+++ /dev/null
@@ -1,358 +0,0 @@
-package de.embl.cba.mobie.projects.projectsCreator.ui;
-
-import de.embl.cba.mobie.projects.projectsCreator.Project;
-import de.embl.cba.mobie.projects.projectsCreator.ProjectsCreator;
-import de.embl.cba.mobie.ui.MoBIE;
-import de.embl.cba.mobie.utils.Utils;
-import de.embl.cba.tables.SwingUtils;
-import ij.IJ;
-import ij.ImagePlus;
-import ij.gui.GenericDialog;
-import mpicbg.spim.data.SpimDataException;
-import net.imglib2.realtransform.AffineTransform3D;
-import org.apache.commons.compress.utils.FileNameUtils;
-
-import javax.swing.*;
-import javax.swing.filechooser.FileNameExtensionFilter;
-import java.awt.*;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.io.File;
-import java.io.IOException;
-
-import static de.embl.cba.mobie.utils.ExportUtils.generateDefaultAffine;
-import static de.embl.cba.mobie.utils.ExportUtils.parseAffineString;
-import static de.embl.cba.mobie.utils.Utils.tidyString;
-import static de.embl.cba.mobie.utils.ui.SwingUtils.*;
-
-public class ProjectsCreatorPanel extends JFrame {
-    private ProjectsCreator projectsCreator;
-    private Project project;
-    private JComboBox<String> datasetComboBox;
-    private JComboBox<String> imagesComboBox;
-
-    public ProjectsCreatorPanel ( File projectLocation ) {
-        this.projectsCreator = new ProjectsCreator( projectLocation );
-        this.project = projectsCreator.getProject();
-        addDatasetPanel();
-        addImagesPanel();
-        String shortenedProjectName = projectLocation.getName();
-        if ( shortenedProjectName.length() > 50 ) {
-            shortenedProjectName = shortenedProjectName.substring( 0, 47 ) + "...";
-        }
-        this.setTitle( "Editing MoBIE Project: " + shortenedProjectName );
-        this.getContentPane().setLayout( new BoxLayout(this.getContentPane(), BoxLayout.Y_AXIS ) );
-        this.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
-    }
-
-    public void showProjectsCreatorPanel() {
-        this.pack();
-        this.setLocationRelativeTo(null);
-        this.setVisible( true );
-    }
-
-    public ProjectsCreator getProjectsCreator() {
-        return projectsCreator;
-    }
-
-    private void addDatasetPanel() {
-        final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
-
-        final JButton addButton = getButton("Add");
-        final JButton editButton = getButton("Edit");
-
-        createDatasetComboBox();
-        addButton.addActionListener( e ->
-        {
-            new Thread( () -> { addDatasetDialog(); } ).start();
-        } );
-
-        editButton.addActionListener( e ->
-        {
-            new Thread( () -> { editDatasetDialog(); } ).start();
-        } );
-
-        horizontalLayoutPanel.add(getJLabel("dataset", 60, 10));
-        horizontalLayoutPanel.add(datasetComboBox);
-        horizontalLayoutPanel.add(addButton);
-        horizontalLayoutPanel.add(editButton);
-        horizontalLayoutPanel.setAlignmentX( Component.LEFT_ALIGNMENT );
-
-        this.getContentPane().add(horizontalLayoutPanel);
-    }
-
-    private void createDatasetComboBox() {
-        String[] datasetNames = project.getDatasetNames();
-        datasetComboBox = new JComboBox<>( datasetNames );
-        datasetComboBox.setSelectedItem( datasetNames[0] );
-        setComboBoxDimensions(datasetComboBox);
-        datasetComboBox.setPrototypeDisplayValue( MoBIE.PROTOTYPE_DISPLAY_VALUE);
-        datasetComboBox.addActionListener( new SyncImageAndDatasetComboBox() );
-    }
-
-    private void createImagesCombobox() {
-        String selectedDataset = (String) datasetComboBox.getSelectedItem();
-        if ( !selectedDataset.equals("") ) {
-            String[] imageNames = project.getDataset( selectedDataset ).getImageNames();
-            imagesComboBox = new JComboBox<>( imageNames );
-            imagesComboBox.setSelectedItem( imageNames[0] );
-        } else {
-            imagesComboBox = new JComboBox<>( new String[] {""} );
-            imagesComboBox.setSelectedItem( "" );
-        }
-        setComboBoxDimensions(imagesComboBox);
-        imagesComboBox.setPrototypeDisplayValue( MoBIE.PROTOTYPE_DISPLAY_VALUE);
-    }
-
-    public static void setComboBoxDimensions( JComboBox< String > comboBox )
-    {
-        comboBox.setPrototypeDisplayValue( MoBIE.PROTOTYPE_DISPLAY_VALUE );
-        comboBox.setPreferredSize( new Dimension( 350, 20 ) );
-        comboBox.setMaximumSize( new Dimension( 350, 20 ) );
-    }
-
-    private class SyncImageAndDatasetComboBox implements ActionListener {
-
-        @Override
-        public void actionPerformed(ActionEvent e) {
-            updateImagesComboBox();
-        }
-    }
-
-    private void addImagesPanel() {
-        final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
-
-        final JButton addButton = getButton( "Add" );
-        final JButton editButton = getButton("Edit");
-
-        createImagesCombobox();
-        addButton.addActionListener( e ->
-        {
-            new Thread( () -> { addImageDialog(); } ).start();
-        } );
-
-        editButton.addActionListener( e ->
-        {
-            new Thread( () -> { editImageDialog(); } ).start();
-        } );
-
-        horizontalLayoutPanel.add(getJLabel("image", 60, 10));
-        horizontalLayoutPanel.add(imagesComboBox);
-        horizontalLayoutPanel.add( addButton );
-        horizontalLayoutPanel.add( editButton );
-        horizontalLayoutPanel.setAlignmentX( Component.LEFT_ALIGNMENT );
-
-        this.getContentPane().add(horizontalLayoutPanel);
-    }
-
-    public String chooseDatasetDialog() {
-        final GenericDialog gd = new GenericDialog( "Choose a dataset" );
-        String[] currentDatasets = project.getDatasetNames();
-        gd.addChoice("Dataset", currentDatasets, currentDatasets[0]);
-        gd.showDialog();
-
-        if ( !gd.wasCanceled() ) {
-            return gd.getNextChoice();
-
-        } else {
-            return null;
-        }
-
-    }
-
-    public void addImageDialog() {
-        final GenericDialog gd = new GenericDialog( "Add..." );
-        String[] addMethods = new String[] {"current open image", "bdv format image"};
-        gd.addChoice("Add:", addMethods, "current open image");
-        gd.showDialog();
-
-        if ( !gd.wasCanceled() ) {
-            String addMethod = gd.getNextChoice();
-            if (addMethod.equals("current open image")) {
-                addCurrentOpenImageDialog();
-            } else if (addMethod.equals("bdv format image")) {
-                addBdvFormatImageDialog();
-            }
-
-        }
-    }
-
-    public void addCurrentOpenImageDialog() {
-        String datasetName = (String) datasetComboBox.getSelectedItem();
-
-        if ( !datasetName.equals("") ) {
-            ImagePlus currentImage = IJ.getImage();
-            String defaultAffineTransform = generateDefaultAffine( currentImage );
-
-            final GenericDialog gd = new GenericDialog( "Add Current Image To MoBIE Project..." );
-            gd.addMessage( "Make sure your pixel size, and unit,\n are set properly under Image > Properties...");
-            gd.addStringField( "Image Name", "", 35 );
-            String[] imageTypes = new String[]{ ProjectsCreator.ImageType.image.toString(),
-                    ProjectsCreator.ImageType.segmentation.toString(), ProjectsCreator.ImageType.mask.toString() };
-            gd.addChoice( "Image Type", imageTypes, imageTypes[0] );
-            // TODO - add OME.ZARR
-            String[] bdvFormats = new String[]{ ProjectsCreator.BdvFormat.n5.toString() };
-            gd.addChoice( "Bdv format", bdvFormats, bdvFormats[0] );
-            gd.addStringField("Affine", defaultAffineTransform, 35 );
-            gd.addCheckbox("Use default export settings", true);
-
-            gd.showDialog();
-
-            if ( !gd.wasCanceled() ) {
-                String imageName = gd.getNextString();
-                ProjectsCreator.ImageType imageType = ProjectsCreator.ImageType.valueOf( gd.getNextChoice() );
-                ProjectsCreator.BdvFormat bdvFormat = ProjectsCreator.BdvFormat.valueOf( gd.getNextChoice() );
-                String affineTransform = gd.getNextString().trim();
-                boolean useDefaultSettings = gd.getNextBoolean();
-
-                // tidy up image name, remove any spaces
-                imageName = tidyString( imageName );
-
-                AffineTransform3D sourceTransform = parseAffineString( affineTransform );
-
-                if ( imageName != null && sourceTransform != null ) {
-                    projectsCreator.getImagesCreator().addImage( currentImage, imageName,
-                            datasetName, bdvFormat, imageType, sourceTransform, useDefaultSettings );
-                    updateImagesComboBox( imageName );
-                }
-            }
-
-        } else {
-            Utils.log( "Add image failed - create a dataset first" );
-        }
-    }
-
-    public void addBdvFormatImageDialog() {
-        String datasetName = (String) datasetComboBox.getSelectedItem();
-
-        if (!datasetName.equals("")) {
-
-            JFileChooser chooser = new JFileChooser();
-            chooser.setFileFilter(new FileNameExtensionFilter("xml", "xml"));
-            chooser.setDialogTitle( "Select bdv xml..." );
-            int returnVal = chooser.showOpenDialog(null );
-            if (returnVal == JFileChooser.APPROVE_OPTION) {
-                File xmlLocation = new File( chooser.getSelectedFile().getAbsolutePath() );
-                final GenericDialog gd = new GenericDialog("Add Bdv Format Image To Project...");
-                String[] addMethods = new String[]{ ProjectsCreator.AddMethod.link.toString(),
-                        ProjectsCreator.AddMethod.copy.toString(), ProjectsCreator.AddMethod.move.toString() };
-                gd.addChoice("Add method:", addMethods, addMethods[0]);
-                String[] imageTypes = new String[]{ ProjectsCreator.ImageType.image.toString(),
-                        ProjectsCreator.ImageType.segmentation.toString(), ProjectsCreator.ImageType.mask.toString() };
-                gd.addChoice("Image Type", imageTypes, imageTypes[0]);
-                gd.addMessage( "Note: You can only 'link' to images outside \n" +
-                        "the project folder for local projects. \n " +
-                        "'copy' or 'move' if you wish to upload to s3");
-
-                gd.showDialog();
-
-                if (!gd.wasCanceled()) {
-                    ProjectsCreator.AddMethod addMethod = ProjectsCreator.AddMethod.valueOf( gd.getNextChoice() );
-                    ProjectsCreator.ImageType imageType = ProjectsCreator.ImageType.valueOf( gd.getNextChoice() );
-
-                    try {
-                        projectsCreator.getImagesCreator().addBdvFormatImage( xmlLocation, datasetName, imageType, addMethod );
-                    } catch (SpimDataException | IOException e) {
-                        e.printStackTrace();
-                    }
-
-                    String imageName = FileNameUtils.getBaseName(xmlLocation.getAbsolutePath());
-                    updateImagesComboBox( imageName );
-                }
-            }
-        } else {
-            Utils.log( "Add image failed - create a dataset first" );
-        }
-    }
-
-    public void addDatasetDialog () {
-        final GenericDialog gd = new GenericDialog( "Create a new dataset" );
-        gd.addStringField( "Name of dataset", "", 35 );
-        gd.showDialog();
-
-        if ( !gd.wasCanceled() ) {
-            String datasetName = gd.getNextString();
-            datasetName = tidyString( datasetName );
-
-            if ( datasetName != null ) {
-                projectsCreator.getDatasetsCreator().addDataset(datasetName);
-                updateDatasetsComboBox( datasetName );
-            }
-        }
-    }
-
-    private void editDatasetDialog() {
-        final GenericDialog gd = new GenericDialog( "Edit dataset..." );
-        String oldName = (String) datasetComboBox.getSelectedItem();
-        if ( !oldName.equals("") ) {
-            boolean isDefault = project.isDefaultDataset(oldName);
-
-            gd.addStringField("Dataset name", oldName, 35);
-            if (isDefault) {
-                gd.addMessage("     This dataset is the default");
-            } else {
-                gd.addCheckbox("Make default dataset", false);
-            }
-            gd.showDialog();
-
-            if (!gd.wasCanceled()) {
-                String newName = gd.getNextString();
-                newName = tidyString( newName );
-                if ( newName != null ) {
-                    projectsCreator.getDatasetsCreator().renameDataset( oldName, newName );
-                    updateDatasetsComboBox( newName );
-                }
-
-                if (!isDefault) {
-                    boolean makeDefault = gd.getNextBoolean();
-                    if (makeDefault) {
-                        projectsCreator.getDatasetsCreator().makeDefaultDataset( newName );
-                    }
-                }
-
-            }
-        }
-    }
-
-    private void editImageDialog() {
-        String datasetName = (String) datasetComboBox.getSelectedItem();
-        String imageName = (String) imagesComboBox.getSelectedItem();
-
-        if ( !datasetName.equals("") && !imageName.equals("") ) {
-            new ImagePropertiesEditor(datasetName,
-                    imageName, projectsCreator);
-        }
-    }
-
-    private void updateDatasetsComboBox( String selection ) {
-        updateDatasetsComboBox();
-        datasetComboBox.setSelectedItem( selection );
-    }
-
-    private void updateDatasetsComboBox () {
-        datasetComboBox.removeAllItems();
-        for ( String datasetName : project.getDatasetNames() ) {
-            datasetComboBox.addItem(datasetName);
-        }
-
-        updateImagesComboBox();
-    }
-
-    private void updateImagesComboBox( String selection ) {
-        updateImagesComboBox();
-        imagesComboBox.setSelectedItem( selection );
-    }
-
-    private void updateImagesComboBox () {
-        String currentDataset = (String) datasetComboBox.getSelectedItem();
-
-        if ( currentDataset != null && !currentDataset.equals("") ) {
-            imagesComboBox.removeAllItems();
-            for ( String imageName : project.getDataset(currentDataset).getImageNames() ) {
-                imagesComboBox.addItem(imageName);
-            }
-        }
-    }
-
-
-}
diff --git a/src/main/java/de/embl/cba/mobie/segment/BdvSegmentSelector.java b/src/main/java/de/embl/cba/mobie/segment/BdvSegmentSelector.java
new file mode 100644
index 000000000..993dd268b
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/segment/BdvSegmentSelector.java
@@ -0,0 +1,84 @@
+package de.embl.cba.mobie.segment;
+
+import bdv.util.BdvHandle;
+import bdv.viewer.Source;
+import bdv.viewer.SourceAndConverter;
+import de.embl.cba.mobie.bdv.BdvMousePositionProvider;
+import de.embl.cba.mobie.display.SegmentationSourceDisplay;
+import de.embl.cba.tables.tablerow.TableRowImageSegment;
+import net.imglib2.RandomAccess;
+import net.imglib2.RealPoint;
+import net.imglib2.type.numeric.RealType;
+import sc.fiji.bdvpg.sourceandconverter.SourceAndConverterHelper;
+
+import java.util.Collection;
+import java.util.function.Supplier;
+
+public class BdvSegmentSelector implements Runnable
+{
+	private BdvHandle bdvHandle;
+	private boolean is2D;
+	private Supplier< Collection< SegmentationSourceDisplay > > segmentationDisplaySupplier;
+
+	public BdvSegmentSelector( BdvHandle bdvHandle, boolean is2D, Supplier< Collection< SegmentationSourceDisplay > > segmentationDisplaySupplier )
+	{
+		this.bdvHandle = bdvHandle;
+		this.is2D = is2D;
+		this.segmentationDisplaySupplier = segmentationDisplaySupplier;
+	}
+
+	public synchronized void clearSelection()
+	{
+		final Collection< SegmentationSourceDisplay > segmentationDisplays = segmentationDisplaySupplier.get();
+
+		for ( SegmentationSourceDisplay segmentationDisplay : segmentationDisplays )
+		{
+			segmentationDisplay.selectionModel.clearSelection();
+		}
+	}
+
+	private synchronized void toggleSelectionAtMousePosition()
+	{
+		final BdvMousePositionProvider positionProvider = new BdvMousePositionProvider( bdvHandle );
+		final int timePoint = positionProvider.getTimePoint();
+		final RealPoint position = positionProvider.getPosition();
+
+		final Collection< SegmentationSourceDisplay > segmentationDisplays = segmentationDisplaySupplier.get();
+
+		for ( SegmentationSourceDisplay segmentationDisplay : segmentationDisplays )
+		{
+			for ( SourceAndConverter< ? > sourceAndConverter : segmentationDisplay.sourceAndConverters )
+			{
+				if ( SourceAndConverterHelper.isPositionWithinSourceInterval( sourceAndConverter, position, timePoint, is2D ) )
+				{
+					final Source< ? > source = sourceAndConverter.getSpimSource();
+					final double labelIndex = getPixelValue( timePoint, position, source );
+					if ( labelIndex == 0 ) return;
+
+					final TableRowImageSegment segment = segmentationDisplay.segmentAdapter.getSegment( labelIndex, timePoint, source.getName() );
+
+					segmentationDisplay.selectionModel.toggle( segment );
+					if ( segmentationDisplay.selectionModel.isSelected( segment ) )
+					{
+						segmentationDisplay.selectionModel.focus( segment );
+					}
+				}
+			}
+		}
+	}
+
+	private static double getPixelValue( int timePoint, RealPoint position, Source< ? > source )
+	{
+		final RandomAccess< RealType > randomAccess = ( RandomAccess< RealType > ) source.getSource( timePoint, 0 ).randomAccess();
+		final long[] positionInSource = SourceAndConverterHelper.getVoxelPositionInSource( source, position, timePoint, 0 );
+		randomAccess.setPosition( positionInSource );
+		final double labelIndex = randomAccess.get().getRealDouble();
+		return labelIndex;
+	}
+
+	@Override
+	public void run()
+	{
+		toggleSelectionAtMousePosition();
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/segment/SegmentAdapter.java b/src/main/java/de/embl/cba/mobie/segment/SegmentAdapter.java
new file mode 100644
index 000000000..d88cc6313
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/segment/SegmentAdapter.java
@@ -0,0 +1,42 @@
+package de.embl.cba.mobie.segment;
+
+import de.embl.cba.tables.imagesegment.ImageSegment;
+import de.embl.cba.tables.imagesegment.LabelFrameAndImage;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+public class SegmentAdapter< T extends ImageSegment >
+{
+	private final HashMap< LabelFrameAndImage, T > labelFrameAndImageToSegment;
+
+	public SegmentAdapter( List< T > segments )
+	{
+		labelFrameAndImageToSegment = new HashMap<>();
+
+		for ( T segment : segments )
+			labelFrameAndImageToSegment.put( new LabelFrameAndImage( segment ), segment );
+	}
+
+	public T getSegment( double label, int t, String imageId )
+	{
+		final LabelFrameAndImage labelFrameAndImage = new LabelFrameAndImage( label, t, imageId  );
+
+		return labelFrameAndImageToSegment.get( labelFrameAndImage );
+	}
+
+	// deserialize
+	public List< T > getSegments( List< String > serialisedSegments )
+	{
+		final ArrayList< T > segments = new ArrayList<>();
+		for ( String serialisedSegment : serialisedSegments )
+		{
+			final String[] split = serialisedSegment.split( ";" );
+			final LabelFrameAndImage labelFrameAndImage = new LabelFrameAndImage( Double.parseDouble( split[2] ), Integer.parseInt( split[1] ), split[0] );
+			segments.add( labelFrameAndImageToSegment.get( labelFrameAndImage ) );
+		}
+
+		return segments;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/segment/SegmentHelper.java b/src/main/java/de/embl/cba/mobie/segment/SegmentHelper.java
new file mode 100644
index 000000000..9e4eee4df
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/segment/SegmentHelper.java
@@ -0,0 +1,5 @@
+package de.embl.cba.mobie.segment;
+
+public class SegmentHelper
+{
+}
diff --git a/src/main/java/de/embl/cba/mobie/serialize/AdditionalViewsJsonParser.java b/src/main/java/de/embl/cba/mobie/serialize/AdditionalViewsJsonParser.java
new file mode 100644
index 000000000..d56f9232d
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/serialize/AdditionalViewsJsonParser.java
@@ -0,0 +1,42 @@
+package de.embl.cba.mobie.serialize;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonWriter;
+import de.embl.cba.mobie.view.additionalviews.AdditionalViews;
+import de.embl.cba.tables.FileAndUrlUtils;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.reflect.Type;
+
+import static de.embl.cba.mobie.serialize.JsonHelper.buildGson;
+
+public class AdditionalViewsJsonParser {
+    public AdditionalViews getViews( String path ) throws IOException
+    {
+        final String s = FileAndUrlUtils.read( path );
+        Gson gson = buildGson( false );
+        Type type = new TypeToken< AdditionalViews >() {}.getType();
+        return gson.fromJson( s, type );
+    }
+
+    public void saveViews( AdditionalViews additionalViews, String path ) throws IOException {
+        Gson gson = buildGson( false );
+        Type type = new TypeToken< AdditionalViews >() {}.getType();
+
+        try ( OutputStream outputStream = new FileOutputStream( path );
+             final JsonWriter writer = new JsonWriter( new OutputStreamWriter(outputStream, "UTF-8")) ) {
+            writer.setIndent("	");
+            gson.toJson(additionalViews, type, writer);
+        }
+    }
+
+    public String viewsToJsonString( AdditionalViews additionalViews, boolean prettyPrinting ) {
+        Gson gson = buildGson( true );
+        Type type = new TypeToken< AdditionalViews >() {}.getType();
+        return gson.toJson( additionalViews, type );
+    }
+}
diff --git a/src/main/java/de/embl/cba/mobie/serialize/DatasetJsonParser.java b/src/main/java/de/embl/cba/mobie/serialize/DatasetJsonParser.java
new file mode 100644
index 000000000..7f61ff197
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/serialize/DatasetJsonParser.java
@@ -0,0 +1,47 @@
+package de.embl.cba.mobie.serialize;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonWriter;
+import de.embl.cba.mobie.Dataset;
+import de.embl.cba.tables.FileAndUrlUtils;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.reflect.Type;
+
+import static de.embl.cba.mobie.serialize.JsonHelper.buildGson;
+
+public class DatasetJsonParser
+{
+	public Dataset parseDataset( String path ) throws IOException
+	{
+		final String datasetJson = FileAndUrlUtils.read( path );
+
+		Gson gson = buildGson( false );
+
+		Type type = new TypeToken< Dataset >() {}.getType();
+		Dataset dataset = gson.fromJson( datasetJson, type );
+		return dataset;
+	}
+
+	public void saveDataset( Dataset dataset, String path ) throws IOException {
+		Gson gson = buildGson( false );
+		Type type = new TypeToken< Dataset >() {}.getType();
+
+		try (OutputStream outputStream = new FileOutputStream( path );
+			 final JsonWriter writer = new JsonWriter( new OutputStreamWriter(outputStream, "UTF-8")) ) {
+			writer.setIndent("	");
+			gson.toJson(dataset, type, writer);
+		}
+	}
+
+	public String datasetToJsonString( Dataset dataset, boolean prettyPrinting ) {
+		Gson gson = buildGson( prettyPrinting );
+		Type type = new TypeToken<Dataset>() {}.getType();
+		return gson.toJson(dataset, type);
+	}
+
+}
diff --git a/src/main/java/de/embl/cba/mobie/serialize/JsonHelper.java b/src/main/java/de/embl/cba/mobie/serialize/JsonHelper.java
new file mode 100644
index 000000000..9d58a50f4
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/serialize/JsonHelper.java
@@ -0,0 +1,61 @@
+package de.embl.cba.mobie.serialize;
+
+import com.google.gson.*;
+import com.google.gson.internal.LinkedTreeMap;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import de.embl.cba.mobie.display.SourceDisplay;
+import de.embl.cba.mobie.transform.SourceTransformer;
+import de.embl.cba.tables.FileAndUrlUtils;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class JsonHelper
+{
+	public static double[] fetchLeveling( String dataLocation )
+	{
+		final String levelingFile = FileAndUrlUtils.combinePath( dataLocation, "misc/leveling.json" );
+		try
+		{
+			InputStream is = FileAndUrlUtils.getInputStream( levelingFile );
+			final JsonReader reader = new JsonReader( new InputStreamReader( is, "UTF-8" ) );
+			final GsonBuilder gsonBuilder = new GsonBuilder();
+			LinkedTreeMap linkedTreeMap = gsonBuilder.create().fromJson( reader, Object.class );
+			ArrayList< Double > normalVector = ( ArrayList< Double > ) linkedTreeMap.get( "NormalVector" );
+			final double[] doubles = normalVector.stream().mapToDouble( Double::doubleValue ).toArray();
+			return doubles;
+		}
+		catch ( Exception e )
+		{
+			return null; // new double[]{0.70,0.56,0.43};
+		}
+	}
+
+	public static Object getObject( JsonDeserializationContext context, JsonElement je, Map< String, Class > nameToClass )
+	{
+		final JsonObject jsonObject = je.getAsJsonObject();
+		final Map.Entry< String, JsonElement > jsonElementEntry = jsonObject.entrySet().iterator().next();
+		Class c = nameToClass.get( jsonElementEntry.getKey() );
+		if (c == null)
+			throw new RuntimeException("Unknown class: " + jsonElementEntry.getKey());
+		final Object deserialize = context.deserialize( jsonElementEntry.getValue(), c );
+		return deserialize;
+	}
+
+	public static Gson buildGson( boolean prettyPrinting )
+	{
+		GsonBuilder gb = new GsonBuilder();
+		gb.registerTypeAdapter( new TypeToken<List<SourceTransformer>>(){}.getType(), new SourceTransformerListAdapter());
+		gb.registerTypeAdapter( new TypeToken<List<SourceDisplay>>(){}.getType(), new SourceDisplayListAdapter());
+
+		if ( prettyPrinting ) {
+			gb.setPrettyPrinting();
+		}
+		Gson gson = gb.create();
+		return gson;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/serialize/ProjectJsonParser.java b/src/main/java/de/embl/cba/mobie/serialize/ProjectJsonParser.java
new file mode 100644
index 000000000..d40335d03
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/serialize/ProjectJsonParser.java
@@ -0,0 +1,36 @@
+package de.embl.cba.mobie.serialize;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonWriter;
+import de.embl.cba.mobie.Project;
+import de.embl.cba.tables.FileAndUrlUtils;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.reflect.Type;
+
+public class ProjectJsonParser
+{
+	public Project parseProject( String path ) throws IOException
+	{
+		final String s = FileAndUrlUtils.read( path );
+		Gson gson = new Gson();
+		Type type = new TypeToken< Project >() {}.getType();
+		Project project = gson.fromJson( s, type );
+		return project;
+	}
+
+	public void saveProject( Project project, String path ) throws IOException {
+		Gson gson = new Gson();
+		Type type = new TypeToken< Project >() {}.getType();
+
+		try (OutputStream outputStream = new FileOutputStream( path );
+			 final JsonWriter writer = new JsonWriter( new OutputStreamWriter(outputStream, "UTF-8")) ) {
+			writer.setIndent("	");
+			gson.toJson( project, type, writer );
+		}
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/serialize/SourceDisplayListAdapter.java b/src/main/java/de/embl/cba/mobie/serialize/SourceDisplayListAdapter.java
new file mode 100644
index 000000000..ba2af6fe4
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/serialize/SourceDisplayListAdapter.java
@@ -0,0 +1,57 @@
+package de.embl.cba.mobie.serialize;
+
+import com.google.gson.*;
+import com.google.gson.reflect.TypeToken;
+import de.embl.cba.mobie.display.ImageSourceDisplay;
+import de.embl.cba.mobie.display.SegmentationSourceDisplay;
+import de.embl.cba.mobie.display.SourceDisplay;
+import de.embl.cba.mobie.transform.SourceTransformer;
+
+import java.lang.reflect.Type;
+import java.util.*;
+
+public class SourceDisplayListAdapter implements JsonSerializer< List< SourceDisplay > >, JsonDeserializer< List< SourceDisplay > >
+{
+	private static Map<String, Class> nameToClass = new TreeMap<>();
+	private static Map<String, String> classToName = new TreeMap<>();
+
+	static {
+		nameToClass.put("imageDisplay", ImageSourceDisplay.class);
+		classToName.put(ImageSourceDisplay.class.getName(), "imageDisplay");
+		nameToClass.put("segmentationDisplay", SegmentationSourceDisplay.class);
+		classToName.put(SegmentationSourceDisplay.class.getName(), "segmentationDisplay");
+	}
+
+	@Override
+	public List< SourceDisplay > deserialize( JsonElement json, Type typeOfT, JsonDeserializationContext context ) throws JsonParseException
+	{
+		List list = new ArrayList<SourceTransformer>();
+		JsonArray ja = json.getAsJsonArray();
+
+		for (JsonElement je : ja)
+		{
+			list.add( JsonHelper.getObject( context, je, nameToClass ) );
+		}
+
+		return list;
+	}
+
+	@Override
+	public JsonElement serialize( List< SourceDisplay > sourceDisplays, Type type, JsonSerializationContext context ) {
+		JsonArray ja = new JsonArray();
+		for ( SourceDisplay sourceDisplay: sourceDisplays ) {
+			Map< String, SourceDisplay > nameToSourceDisplay = new HashMap<>();
+			nameToSourceDisplay.put( classToName.get( sourceDisplay.getClass().getName() ), sourceDisplay );
+
+			if ( sourceDisplay instanceof ImageSourceDisplay ) {
+				ja.add( context.serialize( nameToSourceDisplay, new TypeToken< Map< String, ImageSourceDisplay > >() {}.getType() ) );
+			} else if ( sourceDisplay instanceof  SegmentationSourceDisplay ) {
+				ja.add( context.serialize( nameToSourceDisplay , new TypeToken< Map< String, SegmentationSourceDisplay > >() {}.getType() ) );
+			} else {
+				throw new UnsupportedOperationException( "Could not serialise SourceDisplay of type: " + sourceDisplay.getClass().toString() );
+			}
+		}
+
+		return ja;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/serialize/SourceTransformerListAdapter.java b/src/main/java/de/embl/cba/mobie/serialize/SourceTransformerListAdapter.java
new file mode 100644
index 000000000..ded557598
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/serialize/SourceTransformerListAdapter.java
@@ -0,0 +1,61 @@
+package de.embl.cba.mobie.serialize;
+
+import com.google.gson.*;
+import com.google.gson.reflect.TypeToken;
+import de.embl.cba.mobie.transform.AffineSourceTransformer;
+import de.embl.cba.mobie.transform.CropSourceTransformer;
+import de.embl.cba.mobie.transform.GridSourceTransformer;
+import de.embl.cba.mobie.transform.SourceTransformer;
+
+import java.lang.reflect.Type;
+import java.util.*;
+
+public class SourceTransformerListAdapter implements JsonSerializer< List< SourceTransformer > >, JsonDeserializer< List< SourceTransformer > >
+{
+	private static Map<String, Class> nameToClass = new TreeMap<>();
+	private static Map<String, String> classToName = new TreeMap<>();
+
+	static {
+		nameToClass.put("grid", GridSourceTransformer.class);
+		classToName.put(GridSourceTransformer.class.getName(), "grid");
+		nameToClass.put("affine", AffineSourceTransformer.class);
+		classToName.put(AffineSourceTransformer.class.getName(), "affine");
+		nameToClass.put("crop", CropSourceTransformer.class);
+		classToName.put(CropSourceTransformer.class.getName(), "crop");
+	}
+
+	@Override
+	public List< SourceTransformer > deserialize( JsonElement json, Type typeOfT, JsonDeserializationContext context ) throws JsonParseException
+	{
+		List list = new ArrayList<SourceTransformer>();
+		JsonArray ja = json.getAsJsonArray();
+
+		for (JsonElement je : ja)
+		{
+			list.add( JsonHelper.getObject( context, je, nameToClass ));
+		}
+
+		return list;
+	}
+
+	@Override
+	public JsonElement serialize( List<SourceTransformer> sourceTransformers, Type type, JsonSerializationContext context ) {
+		JsonArray ja = new JsonArray();
+		for ( SourceTransformer sourceTransformer: sourceTransformers ) {
+			Map< String, SourceTransformer > nameToTransformer = new HashMap<>();
+			nameToTransformer.put( classToName.get( sourceTransformer.getClass().getName() ), sourceTransformer );
+
+			if ( sourceTransformer instanceof GridSourceTransformer ) {
+				ja.add( context.serialize( nameToTransformer, new TypeToken< Map< String, GridSourceTransformer > >() {}.getType() ) );
+			} else if ( sourceTransformer instanceof AffineSourceTransformer ) {
+				ja.add( context.serialize( nameToTransformer , new TypeToken< Map< String, AffineSourceTransformer > >() {}.getType() ) );
+			} else if ( sourceTransformer instanceof CropSourceTransformer ) {
+				ja.add( context.serialize( nameToTransformer , new TypeToken< Map< String, CropSourceTransformer > >() {}.getType() ) );
+			} else {
+				throw new UnsupportedOperationException( "Could not serialise SourceTransformer of type: " + sourceTransformer.getClass().toString() );
+			}
+		}
+
+		return ja;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/source/ImageSource.java b/src/main/java/de/embl/cba/mobie/source/ImageSource.java
new file mode 100644
index 000000000..3ca5b8691
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/source/ImageSource.java
@@ -0,0 +1,8 @@
+package de.embl.cba.mobie.source;
+
+import java.util.Map;
+
+public class ImageSource
+{
+	public Map< String, String > imageDataLocations;
+}
diff --git a/src/main/java/de/embl/cba/mobie/source/MoBIESource.java b/src/main/java/de/embl/cba/mobie/source/MoBIESource.java
new file mode 100644
index 000000000..0f08842a6
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/source/MoBIESource.java
@@ -0,0 +1,6 @@
+package de.embl.cba.mobie.source;
+
+public class MoBIESource
+{
+	public String menuItem;
+}
diff --git a/src/main/java/de/embl/cba/mobie/source/SegmentationSource.java b/src/main/java/de/embl/cba/mobie/source/SegmentationSource.java
new file mode 100644
index 000000000..b10ee39fb
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/source/SegmentationSource.java
@@ -0,0 +1,6 @@
+package de.embl.cba.mobie.source;
+
+public class SegmentationSource extends ImageSource
+{
+	public String tableDataLocation;
+}
diff --git a/src/main/java/de/embl/cba/mobie/source/SourceChanger.java b/src/main/java/de/embl/cba/mobie/source/SourceChanger.java
new file mode 100644
index 000000000..c77a7e335
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/source/SourceChanger.java
@@ -0,0 +1,65 @@
+/*-
+ * #%L
+ * BigDataViewer-Playground
+ * %%
+ * Copyright (C) 2019 - 2021 Nicolas Chiaruttini, EPFL - Robert Haase, MPI CBG - Christian Tischer, EMBL
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+package de.embl.cba.mobie.source;
+
+import bdv.viewer.Source;
+import bdv.viewer.SourceAndConverter;
+
+import java.util.function.Function;
+
+public class SourceChanger< T > implements Function< SourceAndConverter< ? >,  SourceAndConverter< ? > >
+{
+    private final Function< Source< ? >, Source< ? > > sourceConverter;
+
+    public SourceChanger( Function< Source< ? >, Source< ? > > sourceConverter )
+    {
+        this.sourceConverter = sourceConverter;
+    }
+
+    @Override
+    public SourceAndConverter< ? > apply( SourceAndConverter< ? > inputSourceAndConverter )
+    {
+        Source< ? > outputSource = sourceConverter.apply( inputSourceAndConverter.getSpimSource() );
+
+        if ( inputSourceAndConverter.asVolatile() != null )
+        {
+            final Source< ? > volatileOutputSource = sourceConverter.apply( inputSourceAndConverter.asVolatile().getSpimSource() );
+
+            final SourceAndConverter< ? > volatileOutputSourceAndConverter = new SourceAndConverter( volatileOutputSource, inputSourceAndConverter.asVolatile().getConverter() );
+
+            final SourceAndConverter< ? > outputSourceAndConverter = new SourceAndConverter( outputSource, inputSourceAndConverter.getConverter(), volatileOutputSourceAndConverter );
+
+            return outputSourceAndConverter;
+        }
+        else
+        {
+            return new SourceAndConverter( outputSource, inputSourceAndConverter.getConverter() );
+        }
+    }
+}
diff --git a/src/main/java/de/embl/cba/mobie/source/SourceSupplier.java b/src/main/java/de/embl/cba/mobie/source/SourceSupplier.java
new file mode 100644
index 000000000..4d40c0dc7
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/source/SourceSupplier.java
@@ -0,0 +1,22 @@
+package de.embl.cba.mobie.source;
+
+public class SourceSupplier
+{
+	private ImageSource image;
+	private SegmentationSource segmentation;
+
+	public SourceSupplier( ImageSource imageSource ) {
+		this.image = imageSource;
+	}
+
+	public SourceSupplier( SegmentationSource segmentationSource ) {
+		this.segmentation = segmentationSource;
+	}
+
+	public ImageSource get()
+	{
+		if ( image != null ) return image;
+		else if ( segmentation != null ) return segmentation;
+		else throw new RuntimeException( "Unsupported Source." );
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/source/SourceWrapper.java b/src/main/java/de/embl/cba/mobie/source/SourceWrapper.java
new file mode 100644
index 000000000..3b3b6e981
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/source/SourceWrapper.java
@@ -0,0 +1,8 @@
+package de.embl.cba.mobie.source;
+
+import bdv.viewer.Source;
+
+public interface SourceWrapper< T >
+{
+	Source< T > getWrappedSource();
+}
diff --git a/src/main/java/de/embl/cba/mobie/table/TableViewer.java b/src/main/java/de/embl/cba/mobie/table/TableViewer.java
new file mode 100644
index 000000000..a67f81aa5
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/table/TableViewer.java
@@ -0,0 +1,793 @@
+/*-
+ * #%L
+ * Various Java code for ImageJ
+ * %%
+ * Copyright (C) 2018 - 2021 EMBL
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+package de.embl.cba.mobie.table;
+
+import de.embl.cba.bdv.utils.lut.GlasbeyARGBLut;
+import de.embl.cba.mobie.annotate.Annotator;
+import de.embl.cba.mobie.color.MoBIEColoringModel;
+import de.embl.cba.tables.*;
+import de.embl.cba.tables.color.*;
+import de.embl.cba.tables.plot.ScatterPlotDialog;
+
+import de.embl.cba.tables.select.SelectionListener;
+import de.embl.cba.tables.select.SelectionModel;
+import de.embl.cba.tables.tablerow.JTableFromTableRowsModelCreator;
+import de.embl.cba.tables.tablerow.TableRow;
+import de.embl.cba.tables.tablerow.TableRowListener;
+import de.embl.cba.tables.tablerow.TableRows;
+import ij.gui.GenericDialog;
+import net.imglib2.type.numeric.ARGBType;
+import org.apache.commons.io.FilenameUtils;
+
+import javax.activation.UnsupportedDataTypeException;
+import javax.swing.*;
+import javax.swing.table.DefaultTableCellRenderer;
+import javax.swing.table.TableModel;
+import java.awt.*;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static de.embl.cba.tables.FileUtils.selectPathFromProjectOrFileSystem;
+import static de.embl.cba.tables.color.CategoryTableRowColumnColoringModel.DARK_GREY;
+import static de.embl.cba.tables.tablerow.TableRows.setTableCell;
+
+public class TableViewer< T extends TableRow > implements SelectionListener< T >, ColoringListener, TableRowListener
+{
+	private final List< T > tableRows;
+	private final SelectionModel< T > selectionModel;
+	private final MoBIEColoringModel< T > coloringModel;
+	private final String tableName;
+
+	private JTable table;
+
+	private int recentlySelectedRowInView;
+	private ColumnColoringModelCreator< T > columnColoringModelCreator;
+	private String mergeByColumnName; // for loading additional columns
+	private String tablesDirectory; // for loading additional columns
+	private ArrayList<String> additionalTables; // tables from which additional columns are loaded
+	private TableRowSelectionMode tableRowSelectionMode = TableRowSelectionMode.FocusOnly;
+
+	// TODO: this is only for the annotator (maybe move it there)
+	private Map< String, CategoryTableRowColumnColoringModel< T > > columnNameToColoringModel = new HashMap<>(  );
+
+	private boolean controlDown;
+	private JFrame frame;
+
+	public void close()
+	{
+		frame.dispose();
+	}
+
+	private enum TableRowSelectionMode
+	{
+		None,
+		FocusOnly,
+		ToggleSelectionAndFocusIfSelected
+	}
+
+	public TableViewer(
+			final List< T > tableRows,
+			final SelectionModel< T > selectionModel,
+			final MoBIEColoringModel< T > moBIEColoringModel,
+			String tableName )
+	{
+		this.tableRows = tableRows;
+		this.coloringModel = moBIEColoringModel;
+		this.selectionModel = selectionModel;
+		this.tableName = tableName;
+		this.recentlySelectedRowInView = -1;
+		this.additionalTables = new ArrayList<>();
+
+		// TODO: reconsider
+		registerAsTableRowListener( tableRows );
+	}
+
+	public TableViewer< T > show()
+	{
+		configureJTable();
+
+		if ( selectionModel != null )
+			installSelectionModelNotification();
+
+		if ( coloringModel != null)
+			configureTableRowColoring();
+
+		createAndShowMenu();
+
+		return this;
+	}
+
+	public void registerAsTableRowListener( List< T > tableRows )
+	{
+		for ( T tableRow : tableRows )
+		{
+			tableRow.listeners().add( new TableRowListener()
+			{
+				@Override
+				public void cellChanged( String columnName, String value )
+				{
+					setTableCell( tableRow.rowIndex(), columnName, value, getTable() );
+				}
+			} );
+		}
+	}
+
+	public List< T > getTableRows()
+	{
+		return tableRows;
+	}
+
+
+	private void configureTableRowColoring()
+	{
+		table.setDefaultRenderer( Double.class, new DefaultTableCellRenderer()
+		{
+			@Override
+			public Component getTableCellRendererComponent(
+					JTable table, Object value, boolean isSelected,
+					boolean hasFocus, int row, int column) {
+
+				Component c = super.getTableCellRendererComponent(
+						table,
+						value,
+						isSelected,
+						hasFocus,
+						row,
+						column);
+
+				c.setBackground( getColor(row, column) );
+
+				return c;
+			}
+		} );
+
+		table.setDefaultRenderer( String.class, new DefaultTableCellRenderer()
+		{
+			@Override
+			public Component getTableCellRendererComponent(
+					JTable table, Object value, boolean isSelected,
+					boolean hasFocus, int row, int column) {
+
+				Component c = super.getTableCellRendererComponent(
+						table,
+						value,
+						isSelected,
+						hasFocus,
+						row,
+						column);
+
+				c.setBackground( getColor(row, column) );
+
+				return c;
+			}
+
+		} );
+
+		table.setDefaultRenderer( Long.class, new DefaultTableCellRenderer()
+		{
+			@Override
+			public Component getTableCellRendererComponent(
+					JTable table, Object value, boolean isSelected,
+					boolean hasFocus, int row, int column )
+			{
+				Component c = super.getTableCellRendererComponent(
+						table,
+						value,
+						isSelected,
+						hasFocus,
+						row,
+						column );
+
+				c.setBackground( getColor( row, column ) );
+
+				return c;
+			}
+		});
+
+		table.setDefaultRenderer( Integer.class, new DefaultTableCellRenderer()
+		{
+			@Override
+			public Component getTableCellRendererComponent(
+					JTable table, Object value, boolean isSelected,
+					boolean hasFocus, int row, int column)
+			{
+				Component c = super.getTableCellRendererComponent(
+						table,
+						value,
+						isSelected,
+						hasFocus,
+						row,
+						column);
+
+				c.setBackground( getColor(row, column) );
+
+				return c;
+			}
+		} );
+
+		table.setDefaultRenderer( Object.class, new DefaultTableCellRenderer()
+		{
+			@Override
+			public Component getTableCellRendererComponent(
+					JTable table, Object value, boolean isSelected,
+					boolean hasFocus, int row, int column )
+			{
+				Component c = super.getTableCellRendererComponent(
+						table,
+						value,
+						isSelected,
+						hasFocus,
+						row,
+						column );
+
+				c.setBackground( getColor( row, column ) );
+
+				return c;
+			}
+		});
+	}
+
+	private Color getColor( int rowInView, int columnInView )
+	{
+		final int row = table.convertRowIndexToModel( rowInView );
+
+//		if ( selectionModel.isFocused( tableRows.getTableRows().get( row ) ) )
+//		{
+//			return Color.BLUE;
+//		}
+
+		final ARGBType argbType = new ARGBType();
+		coloringModel.convert( tableRows.get( row ), argbType );
+
+		if ( ARGBType.alpha( argbType.get() ) == 0 )
+			return Color.WHITE;
+		else
+			return ColorUtils.getColor( argbType );
+	}
+
+	private void registerAsColoringListener( SelectionColoringModel< T > selectionColoringModel )
+	{
+		selectionColoringModel.listeners().add( () -> SwingUtilities.invokeLater( () -> repaintTable() ) );
+	}
+
+	private synchronized void repaintTable()
+	{
+		table.repaint();
+	}
+
+	private void configureJTable()
+	{
+		table = new JTableFromTableRowsModelCreator( tableRows ).createJTable();
+		table.setPreferredScrollableViewportSize( new Dimension(500, 200) );
+		table.setFillsViewportHeight( true );
+		table.setAutoCreateRowSorter( true );
+		table.setRowSelectionAllowed( true );
+		table.setSelectionMode( ListSelectionModel.SINGLE_SELECTION );
+
+		columnColoringModelCreator = new ColumnColoringModelCreator( table );
+	}
+
+	private JMenuBar createMenuBar()
+	{
+		JMenuBar menuBar = new JMenuBar();
+		menuBar.add( createTableMenu() );
+
+		if ( selectionModel != null )
+			menuBar.add( createSelectionMenu() );
+
+		if ( coloringModel != null )
+		{
+			menuBar.add( createColoringMenu() );
+			menuBar.add( createAnnotateMenu() );
+			// menuBar.add( createPlotMenu() ); we have this already in the MoBIE UI
+		}
+
+		return menuBar;
+	}
+
+	private JMenu createSelectionMenu()
+	{
+		JMenu menu = new JMenu( "Select" );
+
+		menu.add( createSelectAllMenuItem() );
+
+		return menu;
+	}
+
+	private JMenu createAnnotateMenu()
+	{
+		JMenu menu = new JMenu( "Annotate" );
+
+		menu.add( createStartNewAnnotationMenuItem() );
+
+		menu.add( createContinueAnnotationMenuItem() );
+
+		return menu;
+	}
+
+	private JMenu createPlotMenu()
+	{
+		JMenu menu = new JMenu( "Plot" );
+		menu.add( createScatterPlotMenuItem() );
+		return menu;
+	}
+
+	private JMenuItem createScatterPlotMenuItem()
+	{
+		final JMenuItem menuItem = new JMenuItem( "2D Scatter Plot..." );
+		menuItem.addActionListener( e ->
+			{
+				SwingUtilities.invokeLater( () ->
+				{
+					String[] columnNames = getColumnNames().stream().toArray( String[]::new );
+					ScatterPlotDialog dialog = new ScatterPlotDialog( columnNames, new String[]{ columnNames[ 0 ], columnNames[ 1 ] }, new double[]{ 1.0, 1.0 }, 1.0 );
+
+					if ( dialog.show() )
+					{
+//						TableRowsScatterPlot< T > scatterPlot = new TableRowsScatterPlot<>( tableRows, coloringModel, dialog.getSelectedColumns(), dialog.getScaleFactors(), dialog.getDotSizeScaleFactor() );
+//						scatterPlot.show( null );
+					}
+				});
+			}
+		);
+		return menuItem;
+	}
+
+	private JMenu createTableMenu()
+    {
+        JMenu menu = new JMenu( "Table" );
+
+        menu.add( createSaveTableAsMenuItem() );
+
+		menu.add( createSaveColumnsAsMenuItem() );
+
+		menu.add( createLoadColumnsMenuItem() );
+
+		return menu;
+    }
+
+    public void addAdditionalTable(String tablePath) {
+		String tableName  = FilenameUtils.getBaseName(tablePath);
+		additionalTables.add(tableName);
+	}
+
+	private JMenuItem createLoadColumnsMenuItem()
+	{
+		final JMenuItem menuItem = new JMenuItem( "Load Columns..." );
+		menuItem.addActionListener( e ->
+				SwingUtilities.invokeLater( () ->
+				{
+					try
+					{
+						String mergeByColumnName = getMergeByColumnName();
+						String tablePath = selectPathFromProjectOrFileSystem( tablesDirectory, "Table");
+						addAdditionalTable(tablePath);
+						Map< String, List< String > > newColumnsOrdered = TableUIs.loadColumns( table, tablePath, mergeByColumnName );
+						if ( newColumnsOrdered == null ) return;
+						newColumnsOrdered.remove( mergeByColumnName );
+						addColumns( newColumnsOrdered );
+					} catch ( IOException ioOException )
+					{
+						ioOException.printStackTrace();
+					}
+				} ) );
+
+		return menuItem;
+	}
+
+	private String getMergeByColumnName()
+	{
+		String aMergeByColumnName;
+		if ( mergeByColumnName == null )
+			aMergeByColumnName = TableUIs.selectColumnNameUI( table, "Merge by " );
+		else
+			aMergeByColumnName = mergeByColumnName;
+		return aMergeByColumnName;
+	}
+
+	public ArrayList<String> getAdditionalTables() {
+		return additionalTables;
+	}
+
+	private JMenuItem createSaveTableAsMenuItem()
+	{
+		final JMenuItem menuItem = new JMenuItem( "Save Table as..." );
+		menuItem.addActionListener( e ->
+				SwingUtilities.invokeLater( () ->
+						TableUIs.saveTableUI( table ) ) );
+
+		return menuItem;
+	}
+
+	private JMenuItem createSaveColumnsAsMenuItem()
+	{
+		final JMenuItem menuItem = new JMenuItem( "Save Columns as..." );
+		menuItem.addActionListener( e ->
+				SwingUtilities.invokeLater( () -> TableUIs.saveColumns( table ) ) );
+
+		return menuItem;
+	}
+
+	private JMenuItem createSelectAllMenuItem()
+	{
+		final JMenuItem menuItem = new JMenuItem( "Select all" );
+
+		menuItem.addActionListener( e ->
+				SwingUtilities.invokeLater( () ->
+						selectAll() ) );
+
+		return menuItem;
+	}
+
+	private JMenuItem createStartNewAnnotationMenuItem()
+	{
+		final JMenuItem menuItem = new JMenuItem( "Start new annotation..." );
+
+		menuItem.addActionListener( e -> showNewAnnotationDialog() );
+
+		return menuItem;
+	}
+
+	private JMenuItem createContinueAnnotationMenuItem()
+	{
+		final JMenuItem menuItem = new JMenuItem( "Continue annotation..." );
+
+		menuItem.addActionListener( e -> showContinueAnnotationDialog() );
+
+		return menuItem;
+	}
+
+	public void showContinueAnnotationDialog()
+	{
+		SwingUtilities.invokeLater( () ->
+		{
+			final String annotationColumn = TableUIs.selectColumnNameUI( table, "Annotation column" );
+			continueAnnotation( annotationColumn );
+		});
+	}
+
+	private void selectAll()
+	{
+		selectionModel.setSelected( tableRows, true );
+//		for ( T tableRow : tableRows )
+//		{
+//			selectionModel.setSelected( tableRow, true );
+//			selectionModel.focus( tableRow );
+//		}
+
+	}
+
+	public void showNewAnnotationDialog()
+	{
+		final GenericDialog gd = new GenericDialog( "" );
+		gd.addStringField( "Annotation column name", "", 30 );
+		gd.showDialog();
+		if( gd.wasCanceled() ) return;
+		final String columnName = gd.getNextString();
+		if ( getColumnNames().contains( columnName ) )
+		{
+			Logger.error( "\"" +columnName + "\" exists already as a column name, please choose another one." );
+			return;
+		}
+		this.addColumn( columnName, "None" );
+
+		continueAnnotation( columnName );
+	}
+
+	public void continueAnnotation( String columnName )
+	{
+		if ( ! columnNameToColoringModel.containsKey( columnName ) )
+		{
+			final CategoryTableRowColumnColoringModel< T > categoricalColoringModel = columnColoringModelCreator.createCategoricalColoringModel( columnName, false, new GlasbeyARGBLut(), DARK_GREY );
+			columnNameToColoringModel.put( columnName, categoricalColoringModel );
+		}
+
+		coloringModel.setSelectionColoringMode( MoBIEColoringModel.SelectionColoringMode.DimNotSelected );
+		coloringModel.setColoringModel( columnNameToColoringModel.get( columnName ) );
+		final RowSorter< ? extends TableModel > rowSorter = table.getRowSorter();
+
+		final Annotator annotator = new Annotator(
+				columnName,
+				tableRows,
+				selectionModel,
+				columnNameToColoringModel.get( columnName ),
+				rowSorter
+		);
+
+		annotator.showDialog();
+	}
+
+	private void createAndShowMenu()
+	{
+		final JPanel panel = new JPanel( new GridLayout( 1, 0 ) );
+		JScrollPane scrollPane = new JScrollPane(
+				table,
+				JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
+				JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED );
+		panel.add( scrollPane );
+
+		table.setAutoResizeMode( JTable.AUTO_RESIZE_OFF );
+		panel.updateUI(); // TODO do we need this?
+		panel.setOpaque( true );
+
+		frame = new JFrame( tableName );
+		final JMenuBar menuBar = createMenuBar();
+		frame.setJMenuBar( menuBar );
+		frame.setContentPane( panel );
+
+		// Display the window.
+		frame.pack();
+
+		// Replace closing by making it invisible
+		frame.setDefaultCloseOperation( WindowConstants.DO_NOTHING_ON_CLOSE );
+		frame.addWindowListener( new WindowAdapter() {
+			public void windowClosing( WindowEvent ev) {
+				frame.setVisible( false );
+			}
+		});
+
+		SwingUtilities.invokeLater( () -> frame.setVisible( true ) );
+	}
+
+	public void addColumn( String column, Object defaultValue )
+	{
+		if ( getColumnNames().contains( column ) )
+			throw new RuntimeException( column + " exists already, please choose another name." );
+		Tables.addColumn( table.getModel(), column, defaultValue );
+		TableRows.addColumn( tableRows, column, defaultValue );
+	}
+
+	public void addColumn( String column, Object[] values )
+	{
+		Tables.addColumn( table.getModel(), column, values );
+		TableRows.addColumn( tableRows, column, values );
+	}
+
+	public void addColumns( Map< String, List< String > > columns )
+	{
+		for ( String columnName : columns.keySet() )
+		{
+			try
+			{
+				final Object[] values = TableColumns.asTypedArray( columns.get( columnName ) );
+				addColumn( columnName, values );
+			} catch ( UnsupportedDataTypeException e )
+			{
+				Logger.error( "Could not add column " + columnName + ", because the" +
+						" data type could not be determined.");
+			}
+		}
+	}
+
+	public List< String > getColumnNames()
+	{
+		return Tables.getColumnNames( table );
+	}
+
+	public JTable getTable()
+	{
+		return table;
+	}
+
+	private synchronized void moveToRowInView( int rowInView )
+	{
+		setRecentlySelectedRowInView( rowInView );
+		//table.getSelectionModel().setSelectionInterval( rowInView, rowInView );
+		final Rectangle visibleRect = table.getVisibleRect();
+		final Rectangle cellRect = table.getCellRect( rowInView, 0, true );
+		visibleRect.y = cellRect.y;
+		table.scrollRectToVisible( visibleRect );
+		table.repaint();
+	}
+
+	public void installSelectionModelNotification()
+	{
+		table.addMouseListener( new MouseAdapter()
+		{
+			@Override
+			public void mouseClicked( MouseEvent e )
+			{
+				controlDown = e.isControlDown();
+			}
+		} );
+
+		table.getSelectionModel().addListSelectionListener( e ->
+			SwingUtilities.invokeLater( () ->
+			{
+				if ( tableRowSelectionMode.equals( TableRowSelectionMode.None ) ) return;
+
+				if ( e.getValueIsAdjusting() ) return;
+
+				final int selectedRowInView = table.getSelectedRow();
+
+				if ( selectedRowInView == -1 ) return;
+
+				if ( selectedRowInView == recentlySelectedRowInView ) return;
+
+				setRecentlySelectedRowInView( selectedRowInView );
+
+				final int row = table.convertRowIndexToModel( recentlySelectedRowInView );
+
+				final T object = tableRows.get( row );
+
+				tableRowSelectionMode = controlDown ? TableRowSelectionMode.ToggleSelectionAndFocusIfSelected : TableRowSelectionMode.FocusOnly;
+
+				if ( tableRowSelectionMode.equals( TableRowSelectionMode.FocusOnly ) )
+				{
+					selectionModel.focus( object );
+				}
+				else if ( tableRowSelectionMode.equals( TableRowSelectionMode.ToggleSelectionAndFocusIfSelected ) )
+				{
+					selectionModel.toggle( object );
+					if ( selectionModel.isSelected( object ) )
+						selectionModel.focus( object );
+				}
+			})
+		);
+	}
+
+	private synchronized void setRecentlySelectedRowInView( int r )
+	{
+		recentlySelectedRowInView = r;
+	}
+
+	private synchronized void moveToSelectedTableRow( TableRow selection )
+	{
+		final int rowInView = table.convertRowIndexToView( selection.rowIndex() );
+
+		if ( rowInView == recentlySelectedRowInView ) return;
+
+		moveToRowInView( rowInView );
+	}
+
+	private JMenu createColoringMenu()
+	{
+		JMenu coloringMenu = new JMenu( "Color" );
+
+		addColorByColumnMenuItem( coloringMenu );
+
+		// TODO: add menu item to configure values that should be transparent
+
+		addColorLoggingMenuItem( coloringMenu );
+
+		return coloringMenu;
+	}
+
+	private void addColorLoggingMenuItem( JMenu coloringMenu )
+	{
+		final JMenuItem menuItem = new JMenuItem( "Log Current Value to Color Map" );
+
+		menuItem.addActionListener( e ->
+				new Thread( () ->
+						logCurrentValueToColorMap() ).start() );
+
+		coloringMenu.add( menuItem );
+	}
+
+	private void logCurrentValueToColorMap()
+	{
+		String coloringColumnName = getColoringColumnName();
+
+		if ( coloringColumnName == null )
+		{
+			Logger.error( "Please first use the [ Color > Color by Column ] menu item to configure the coloring." );
+			return;
+		}
+
+		Logger.info( " "  );
+		Logger.info( "Column used for coloring: " + coloringColumnName );
+		Logger.info( " "  );
+		Logger.info( "Value, R, G, B"  );
+
+		for ( T tableRow : tableRows )
+		{
+			final String value = tableRow.getCell( coloringColumnName );
+
+			final ARGBType argbType = new ARGBType();
+			coloringModel.convert( tableRow, argbType );
+			final int colorIndex = argbType.get();
+			Logger.info( value + ": " + ARGBType.red( colorIndex ) + ", " + ARGBType.green( colorIndex ) + ", " + ARGBType.blue( colorIndex ) );
+		}
+	}
+
+	public String getColoringColumnName()
+	{
+		final ColoringModel< T > coloringModel = this.coloringModel.getWrappedColoringModel();
+
+		if ( coloringModel instanceof ColumnColoringModel )
+		{
+			return (( ColumnColoringModel ) coloringModel).getColumnName();
+		}
+		else
+		{
+			return null;
+		}
+	}
+
+	private void addColorByColumnMenuItem( JMenu coloringMenu )
+	{
+		final JMenuItem menuItem = new JMenuItem( "Color by Column..." );
+
+		menuItem.addActionListener( e ->
+				new Thread( () -> showColorByColumnDialog()
+				).start() );
+
+		coloringMenu.add( menuItem );
+	}
+
+	public void showColorByColumnDialog()
+	{
+		final ColoringModel< T > coloringModel = columnColoringModelCreator.showDialog();
+
+		if ( coloringModel != null )
+			this.coloringModel.setColoringModel( coloringModel );
+	}
+
+	public Window getWindow()
+	{
+		return frame;
+	}
+
+	@Override
+	public synchronized void selectionChanged()
+	{
+		if ( selectionModel.isEmpty() )
+		{
+			setRecentlySelectedRowInView( -1 );
+			table.getSelectionModel().clearSelection();
+		}
+		SwingUtilities.invokeLater( () -> repaintTable() );
+	}
+
+	@Override
+	public synchronized void focusEvent( T selection )
+	{
+		SwingUtilities.invokeLater( () -> moveToSelectedTableRow( selection ) );
+	}
+
+	@Override
+	public void coloringChanged()
+	{
+		SwingUtilities.invokeLater( () -> repaintTable() );
+	}
+
+	@Override
+	public void cellChanged( String columnName, String value )
+	{
+		// TODO:
+	}
+
+}
diff --git a/src/main/java/de/embl/cba/mobie/transform/AbstractSourceTransformer.java b/src/main/java/de/embl/cba/mobie/transform/AbstractSourceTransformer.java
new file mode 100644
index 000000000..f8f4b7181
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/transform/AbstractSourceTransformer.java
@@ -0,0 +1,27 @@
+package de.embl.cba.mobie.transform;
+
+import bdv.viewer.SourceAndConverter;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.type.numeric.NumericType;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class AbstractSourceTransformer< T extends NumericType< T > > implements SourceTransformer< T >
+{
+	protected String name;
+	protected transient Map< String, AffineTransform3D > sourceNameToTransform = new HashMap();
+
+	@Override
+	public List< SourceAndConverter< T > > transform( List< SourceAndConverter< T > > sourceAndConverters )
+	{
+		return null;
+	}
+
+	@Override
+	public AffineTransform3D getTransform( String sourceName )
+	{
+		return sourceNameToTransform.get( sourceName );
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/transform/AffineSourceTransformer.java b/src/main/java/de/embl/cba/mobie/transform/AffineSourceTransformer.java
new file mode 100644
index 000000000..685578b8b
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/transform/AffineSourceTransformer.java
@@ -0,0 +1,40 @@
+package de.embl.cba.mobie.transform;
+
+import bdv.viewer.SourceAndConverter;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.type.numeric.NumericType;
+import sc.fiji.bdvpg.sourceandconverter.transform.SourceAffineTransformer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AffineSourceTransformer< T extends NumericType< T > > extends AbstractSourceTransformer< T >
+{
+	private List< String > sources;
+	private double[] parameters;
+
+	@Override
+	public List< SourceAndConverter< T > > transform( List< SourceAndConverter< T > > sources )
+	{
+		final ArrayList< SourceAndConverter< T > > transformedSources = new ArrayList<>( sources );
+
+		final AffineTransform3D affineTransform3D = new AffineTransform3D();
+		affineTransform3D.set( parameters );
+		final SourceAffineTransformer transformer = new SourceAffineTransformer( affineTransform3D );
+
+		for ( SourceAndConverter< ? > source : sources )
+		{
+			final String name = source.getSpimSource().getName();
+			if ( this.sources.contains( name ) )
+			{
+				final SourceAndConverter transformedSource = transformer.apply( source );
+				// replace the source in the list
+				transformedSources.remove( source );
+				transformedSources.add( transformedSource );
+				sourceNameToTransform.put( name, affineTransform3D );
+			}
+		}
+
+		return transformedSources;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/bookmark/Location.java b/src/main/java/de/embl/cba/mobie/transform/BdvLocation.java
similarity index 68%
rename from src/main/java/de/embl/cba/mobie/bookmark/Location.java
rename to src/main/java/de/embl/cba/mobie/transform/BdvLocation.java
index 0f2c98213..59fcae3e8 100644
--- a/src/main/java/de/embl/cba/mobie/bookmark/Location.java
+++ b/src/main/java/de/embl/cba/mobie/transform/BdvLocation.java
@@ -1,19 +1,19 @@
-package de.embl.cba.mobie.bookmark;
+package de.embl.cba.mobie.transform;
 
-import de.embl.cba.mobie.utils.Utils;
+import de.embl.cba.mobie.Utils;
 
-public class Location
+public class BdvLocation
 {
-	public LocationType type;
+	public BdvLocationType type;
 	public double[] doubles;
 
-	public Location( LocationType type, double[] doubles )
+	public BdvLocation( BdvLocationType type, double[] doubles )
 	{
 		this.type = type;
 		this.doubles = doubles;
 	}
 
-	public Location( String location )
+	public BdvLocation( String location )
 	{
 		boolean isNormalised = false;
 
@@ -44,15 +44,15 @@ public Location( String location )
 		this.doubles = Utils.delimitedStringToDoubleArray( location, "," );
 
 		if ( this.doubles.length == 3 )
-			this.type = LocationType.Position3d;
+			this.type = BdvLocationType.Position3d;
 		else if ( this.doubles.length == 4 )
-			this.type = LocationType.Position3d;
+			this.type = BdvLocationType.Position3d;
 		else if ( this.doubles.length == 12 )
 		{
 			if ( isNormalised )
-				this.type = LocationType.NormalisedViewerTransform;
+				this.type = BdvLocationType.NormalisedViewerTransform;
 			else
-				this.type = LocationType.ViewerTransform;
+				this.type = BdvLocationType.ViewerTransform;
 		}
 	}
 }
diff --git a/src/main/java/de/embl/cba/mobie/bdv/BdvViewChanger.java b/src/main/java/de/embl/cba/mobie/transform/BdvLocationChanger.java
similarity index 67%
rename from src/main/java/de/embl/cba/mobie/bdv/BdvViewChanger.java
rename to src/main/java/de/embl/cba/mobie/transform/BdvLocationChanger.java
index 714355180..f9eb527f2 100644
--- a/src/main/java/de/embl/cba/mobie/bdv/BdvViewChanger.java
+++ b/src/main/java/de/embl/cba/mobie/transform/BdvLocationChanger.java
@@ -1,20 +1,14 @@
-package de.embl.cba.mobie.bdv;
+package de.embl.cba.mobie.transform;
 
 import bdv.util.*;
 import de.embl.cba.bdv.utils.BdvUtils;
-import de.embl.cba.mobie.bookmark.Location;
-import de.embl.cba.mobie.utils.Utils;
+import de.embl.cba.mobie.bdv.BdvPointOverlay;
+import de.embl.cba.mobie.Utils;
 import net.imglib2.realtransform.AffineTransform3D;
 
 import java.util.Arrays;
 
-
-/**
- * TODO: probably move to bdv-utils
- *
- *
- */
-public abstract class BdvViewChanger
+public abstract class BdvLocationChanger
 {
 	public static int animationDurationMillis = 3000;
 
@@ -23,36 +17,36 @@ public abstract class BdvViewChanger
 	private static boolean pointOverlaySourceIsActive;
 	private static boolean isPointOverlayEnabled;
 
-	public static void moveToLocation( BdvHandle bdv, Location location )
+	public static void moveToLocation( BdvHandle bdv, BdvLocation bdvLocation )
 	{
-		switch ( location.type )
+		switch ( bdvLocation.type )
 		{
 			case Position3d:
 
-				BdvUtils.moveToPosition( bdv, location.doubles, 0, animationDurationMillis );
+				BdvUtils.moveToPosition( bdv, bdvLocation.doubles, 0, animationDurationMillis );
 
 				if ( isPointOverlayEnabled )
-					addPointOverlay( bdv, location.doubles );
+					addPointOverlay( bdv, bdvLocation.doubles );
 				break;
 
 			case Position3dAndTime:
 
 				final double[] position = new double[ 3 ];
 				for ( int d = 0; d < 3; d++ )
-					position[ d ] = location.doubles[ d ];
-				final int time = (int) location.doubles[ 3 ];
+					position[ d ] = bdvLocation.doubles[ d ];
+				final int time = (int) bdvLocation.doubles[ 3 ];
 
 				BdvUtils.moveToPosition( bdv, position, time, animationDurationMillis );
 				break;
 
 			case ViewerTransform:
 
-				BdvUtils.changeBdvViewerTransform( bdv, Utils.asAffineTransform3D( location.doubles ), animationDurationMillis );
+				BdvUtils.changeBdvViewerTransform( bdv, Utils.asAffineTransform3D( bdvLocation.doubles ), animationDurationMillis );
 				break;
 
 			case NormalisedViewerTransform:
 
-				final AffineTransform3D transform = Utils.createUnnormalizedViewerTransform( Utils.asAffineTransform3D( location.doubles ), bdv );
+				final AffineTransform3D transform = Utils.createUnnormalizedViewerTransform( Utils.asAffineTransform3D( bdvLocation.doubles ), bdv );
 				BdvUtils.changeBdvViewerTransform( bdv, transform, animationDurationMillis );
 				break;
 		}
@@ -66,7 +60,7 @@ public static void togglePointOverlay()
 		pointOverlaySource.setActive( pointOverlaySourceIsActive );
 	}
 
-	public static void addPointOverlay( Bdv bdv, double[] doubles )
+	private static void addPointOverlay( Bdv bdv, double[] doubles )
 	{
 		if ( bdvPointOverlay == null )
 		{
@@ -85,6 +79,6 @@ public static void addPointOverlay( Bdv bdv, double[] doubles )
 
 	public static void enablePointOverlay( boolean isPointOverlayEnabled )
 	{
-		BdvViewChanger.isPointOverlayEnabled = isPointOverlayEnabled;
+		BdvLocationChanger.isPointOverlayEnabled = isPointOverlayEnabled;
 	}
 }
diff --git a/src/main/java/de/embl/cba/mobie/transform/BdvLocationSupplier.java b/src/main/java/de/embl/cba/mobie/transform/BdvLocationSupplier.java
new file mode 100644
index 000000000..425f7fb61
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/transform/BdvLocationSupplier.java
@@ -0,0 +1,30 @@
+package de.embl.cba.mobie.transform;
+
+public class BdvLocationSupplier
+{
+	private double[] normalizedAffine;
+	private double[] position;
+
+	public BdvLocationSupplier( BdvLocation bdvLocation ) {
+		if ( bdvLocation.type == BdvLocationType.NormalisedViewerTransform ) {
+			this.normalizedAffine = bdvLocation.doubles;
+		} else if ( bdvLocation.type == BdvLocationType.Position3d ) {
+			this.position = bdvLocation.doubles;
+		}
+	}
+
+	public BdvLocation get()
+	{
+		if ( normalizedAffine != null )
+		{
+			final BdvLocation bdvLocation = new BdvLocation( BdvLocationType.NormalisedViewerTransform, normalizedAffine );
+			return bdvLocation;
+		}
+		else if ( position != null )
+		{
+			final BdvLocation bdvLocation = new BdvLocation( BdvLocationType.Position3d, position );
+			return bdvLocation;
+		}
+		else throw new UnsupportedOperationException( "No viewer transform found." );
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/bookmark/LocationType.java b/src/main/java/de/embl/cba/mobie/transform/BdvLocationType.java
similarity index 56%
rename from src/main/java/de/embl/cba/mobie/bookmark/LocationType.java
rename to src/main/java/de/embl/cba/mobie/transform/BdvLocationType.java
index 32d6a7539..4ec582d66 100644
--- a/src/main/java/de/embl/cba/mobie/bookmark/LocationType.java
+++ b/src/main/java/de/embl/cba/mobie/transform/BdvLocationType.java
@@ -1,6 +1,6 @@
-package de.embl.cba.mobie.bookmark;
+package de.embl.cba.mobie.transform;
 
-public enum LocationType
+public enum BdvLocationType
 {
 	Position3d,
 	Position3dAndTime,
diff --git a/src/main/java/de/embl/cba/mobie/transform/CropSourceTransformer.java b/src/main/java/de/embl/cba/mobie/transform/CropSourceTransformer.java
new file mode 100644
index 000000000..825c4706f
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/transform/CropSourceTransformer.java
@@ -0,0 +1,77 @@
+package de.embl.cba.mobie.transform;
+
+import bdv.viewer.Source;
+import bdv.viewer.SourceAndConverter;
+import de.embl.cba.mobie.source.SourceChanger;
+import net.imglib2.FinalRealInterval;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.type.numeric.NumericType;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+
+public class CropSourceTransformer< T extends NumericType< T > > extends AbstractSourceTransformer< T >
+{
+	private List< String > sources;
+	private List< String > names;
+	private double[] min;
+	private double[] max;
+	private boolean shiftToOrigin = true;
+
+	@Override
+	public List< SourceAndConverter< T > > transform( List< SourceAndConverter< T > > sourceAndConverters )
+	{
+		final ArrayList< SourceAndConverter< T > > transformedSources = new ArrayList<>( sourceAndConverters );
+
+		for ( SourceAndConverter< ? > sourceAndConverter : sourceAndConverters )
+		{
+			final String inputSourceName = sourceAndConverter.getSpimSource().getName();
+
+			if ( this.sources.contains( inputSourceName ) )
+			{
+				String transformedSourceName = getTransformedSourceName( inputSourceName );
+
+				// transform, i.e. crop
+//				final SourceAndConverter< T > transformedSourceOld = new SourceCropper( sourceAndConverter, transformedSourceName, new FinalRealInterval( min, max ), shiftToOrigin ).get();
+//
+//				final CroppedSource< T > croppedSource = ( CroppedSource< T > ) new CroppedSource( sourceAndConverter.getSpimSource(), transformedSourceName, new FinalRealInterval( min, max ), shiftToOrigin );
+
+				final SourceAndConverter< T > croppedSourceAndConverter = new SourceChanger( ( Function< Source< ? >, Source< ? > > ) source -> new CroppedSource( source, transformedSourceName, new FinalRealInterval( min, max ), shiftToOrigin  ) ).apply( sourceAndConverter );
+
+				// replace the source in the list
+				transformedSources.remove( sourceAndConverter );
+				transformedSources.add( croppedSourceAndConverter );
+
+				// store translation
+				final AffineTransform3D transform3D = new AffineTransform3D();
+				if ( shiftToOrigin == true )
+				{
+					transform3D.translate( Arrays.stream( min ).map( x -> -x ).toArray() );
+					sourceNameToTransform.put( transformedSourceName, transform3D );
+				}
+				else
+				{
+					sourceNameToTransform.put( transformedSourceName, transform3D );
+				}
+			}
+		}
+
+		return transformedSources;
+	}
+
+	private String getTransformedSourceName( String inputSourceName )
+	{
+		String transformedSourceName;
+		if ( names != null )
+		{
+			transformedSourceName = names.get( this.sources.indexOf( inputSourceName ) );
+		}
+		else
+		{
+			transformedSourceName = inputSourceName;
+		}
+		return transformedSourceName;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/transform/CroppedSource.java b/src/main/java/de/embl/cba/mobie/transform/CroppedSource.java
new file mode 100644
index 000000000..775c5e861
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/transform/CroppedSource.java
@@ -0,0 +1,163 @@
+/*-
+ * #%L
+ * BigDataViewer-Playground
+ * %%
+ * Copyright (C) 2019 - 2021 Nicolas Chiaruttini, EPFL - Robert Haase, MPI CBG - Christian Tischer, EMBL
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+package de.embl.cba.mobie.transform;
+
+import bdv.util.DefaultInterpolators;
+import bdv.viewer.Interpolation;
+import bdv.viewer.Source;
+import mpicbg.spim.data.sequence.VoxelDimensions;
+import net.imglib2.FinalInterval;
+import net.imglib2.Interval;
+import net.imglib2.RandomAccessibleInterval;
+import net.imglib2.RealInterval;
+import net.imglib2.RealRandomAccessible;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.type.numeric.NumericType;
+import net.imglib2.util.Intervals;
+import net.imglib2.view.ExtendedRandomAccessibleInterval;
+import net.imglib2.view.IntervalView;
+import net.imglib2.view.Views;
+
+import java.util.HashMap;
+import java.util.function.Function;
+
+public class CroppedSource< T extends NumericType<T> > implements Source< T >, Function< Source< T >, Source< T > >
+{
+    private final Source< T > source;
+    private final String name;
+    private final RealInterval crop;
+    private final boolean zeroMin;
+
+    protected transient final DefaultInterpolators< T > interpolators;
+    private transient HashMap< Integer, Interval > levelToVoxelInterval;
+
+    public CroppedSource( Source< T > source, RealInterval crop, boolean zeroMin )
+    {
+        this.source = source;
+        this.name = null;
+        this.crop = crop;
+        this.zeroMin = zeroMin;
+        this.interpolators = new DefaultInterpolators();
+
+        initCropIntervals( source, crop );
+    }
+
+    public CroppedSource( Source< T > source, String name, RealInterval crop, boolean zeroMin )
+    {
+        this.source = source;
+        this.name = name;
+        this.crop = crop;
+        this.zeroMin = zeroMin;
+        this.interpolators = new DefaultInterpolators();
+
+        initCropIntervals( source, crop );
+    }
+
+    private void initCropIntervals( Source< T > source, RealInterval crop )
+    {
+        final AffineTransform3D transform3D = new AffineTransform3D();
+        levelToVoxelInterval = new HashMap<>();
+        for ( int level = 0; level < source.getNumMipmapLevels(); level++ )
+        {
+            source.getSourceTransform( 0, level, transform3D );
+            final Interval voxelInterval = Intervals.smallestContainingInterval( transform3D.inverse().estimateBounds( crop ) );
+            // If the interval is outside the bounds of the RAI then there is nothing to show.
+            // Moreover the fetcher threads throw errors when trying to access pixels outside the RAI.
+            // Thus let's limit the interval to where there actually is data .
+            final RandomAccessibleInterval< T > rai = source.getSource( 0, level );
+            final FinalInterval intersect = Intervals.intersect( rai, voxelInterval );
+            levelToVoxelInterval.put( level, intersect );
+        }
+    }
+
+    public Source< ? > getWrappedSource() {
+        return source;
+    }
+
+    @Override
+    public boolean isPresent(int t) {
+        return source.isPresent(t);
+    }
+
+    @Override
+    public RandomAccessibleInterval< T > getSource(int t, int level)
+    {
+        final RandomAccessibleInterval< T > rai = source.getSource( t, level );
+        final IntervalView< T > croppedRai = Views.interval( rai, levelToVoxelInterval.get( level ) );
+
+        if ( zeroMin )
+            return Views.zeroMin( croppedRai );
+        else
+            return croppedRai;
+    }
+
+    @Override
+    public RealRandomAccessible< T > getInterpolatedSource( int t, int level, Interpolation method )
+    {
+        final RandomAccessibleInterval< T > croppedRai = getSource( t, level );
+        ExtendedRandomAccessibleInterval<T, RandomAccessibleInterval< T >>
+                extendedRai = Views.extendZero( croppedRai );
+        RealRandomAccessible< T > realRandomAccessible = Views.interpolate( extendedRai, interpolators.get(method) );
+        return realRandomAccessible;
+    }
+
+    @Override
+    public void getSourceTransform(int t, int level, AffineTransform3D transform)
+    {
+        source.getSourceTransform( t, level, transform );
+    }
+
+    @Override
+    public T getType() {
+        return source.getType();
+    }
+
+    @Override
+    public String getName()
+    {
+        if ( name != null ) return name;
+        else return source.getName();
+    }
+
+    @Override
+    public VoxelDimensions getVoxelDimensions() {
+        return source.getVoxelDimensions();
+    }
+
+    @Override
+    public int getNumMipmapLevels() {
+        return source.getNumMipmapLevels();
+    }
+
+    @Override
+    public Source< T > apply( Source< T > inputSource )
+    {
+        return new CroppedSource<>( inputSource, name, crop, zeroMin );
+    }
+}
diff --git a/src/main/java/de/embl/cba/mobie/transform/GridSourceTransformer.java b/src/main/java/de/embl/cba/mobie/transform/GridSourceTransformer.java
new file mode 100644
index 000000000..77c1210d1
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/transform/GridSourceTransformer.java
@@ -0,0 +1,150 @@
+package de.embl.cba.mobie.transform;
+
+import bdv.viewer.SourceAndConverter;
+import de.embl.cba.mobie.Utils;
+import de.embl.cba.mobie.MoBIE;
+import net.imglib2.FinalRealInterval;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.type.numeric.NumericType;
+import org.apache.commons.lang.ArrayUtils;
+import sc.fiji.bdvpg.sourceandconverter.transform.SourceAffineTransformer;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+public class GridSourceTransformer< T extends NumericType< T > > extends AbstractSourceTransformer< T >
+{
+	// Serialization
+	private List< List< String > > sources;
+	private String tableDataLocation;
+
+	// Runtime
+	private List< int[] > positions;
+	private transient List< FinalRealInterval > intervals;
+	private transient List< SourceAndConverter< T > > transformedSources;
+
+	@Override
+	public List< SourceAndConverter< T > > transform( List< SourceAndConverter< T > > sourceAndConverters )
+	{
+		// Make a copy because not all sources in the input list may be transformed
+		transformedSources = new CopyOnWriteArrayList<>( sourceAndConverters );
+		intervals = new CopyOnWriteArrayList<>();
+
+		if ( positions == null )
+		{
+			autoSetPositions();
+		}
+
+		final SourceAndConverter< T > reference = getReferenceSource( sourceAndConverters );
+
+		// TODO: make this work on the union of the sources
+		FinalRealInterval bounds = Utils.estimateBounds( reference.getSpimSource() );
+		final double spacingFactor = 0.1;
+		double spacingX = ( 1.0 + spacingFactor ) * ( bounds.realMax( 0 ) - bounds.realMin( 0 ) );
+		double spacingY = ( 1.0 + spacingFactor ) * ( bounds.realMax( 1 ) - bounds.realMin( 1 ) );
+
+		final long start = System.currentTimeMillis();
+
+		final int nThreads = MoBIE.N_THREADS;
+		final ExecutorService executorService = Executors.newFixedThreadPool( nThreads );
+		for ( int gridIndex = 0; gridIndex < positions.size(); gridIndex++ )
+		{
+			final int index = gridIndex;
+
+			executorService.execute( () -> {
+				transformSourcesAtGridPosition( sourceAndConverters, transformedSources, spacingX, spacingY, index );
+			} );
+		}
+		executorService.shutdown();
+		try {
+			executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
+		} catch (InterruptedException e) {
+		}
+
+		System.out.println( "Transformed " + sourceAndConverters.size() + " image source(s) in " + (System.currentTimeMillis() - start) + " ms, using " + nThreads + " thread(s)." );
+
+		return transformedSources;
+	}
+
+	private void transformSourcesAtGridPosition( List< SourceAndConverter< T > > sourceAndConverters, List< SourceAndConverter< T > > transformedSources, double spacingX, double spacingY, int i )
+	{
+		final List< String > sources = this.sources.get( i );
+
+		for ( String sourceName : sources )
+		{
+			final AffineTransform3D transform3D = new AffineTransform3D();
+			transform3D.translate( spacingX * positions.get( i )[ 0 ], spacingY * positions.get( i )[ 1 ], 0 );
+
+			final SourceAndConverter< T > sourceAndConverter = Utils.getSource( sourceAndConverters, sourceName );
+
+			if ( sourceAndConverter == null )
+			{
+				// This is OK, because the field `List< List< String > > sources`
+				// can contain more sources than the ones that should be
+				// transformed with `transform( List< SourceAndConverter< ? > > sourceAndConverters )`
+				// Examples are multi-color images where there is a separate imageDisplay
+				// for each color.
+				continue;
+			}
+
+			final SourceAndConverter< T > transformedSource = new SourceAffineTransformer( sourceAndConverter, transform3D ).getSourceOut();
+
+			// Replace the original source by the transformed one
+			transformedSources.remove( sourceAndConverter );
+			transformedSources.add( transformedSource );
+
+			sourceNameToTransform.put( sourceName, transform3D );
+			intervals.add( Utils.estimateBounds( transformedSource.getSpimSource() ) );
+		}
+	}
+
+	private SourceAndConverter< T > getReferenceSource( List< SourceAndConverter< T > > sourceAndConverters )
+	{
+		final List< String > sourcesAtFirstGridPosition = sources.get( 0 );
+
+		for ( String name : sourcesAtFirstGridPosition )
+		{
+			final SourceAndConverter< T > source = Utils.getSource( sourceAndConverters, name );
+			if ( source != null )
+			{
+				return source;
+			}
+		}
+
+		throw new UnsupportedOperationException( "None of the sources specified at the first grid position could be found at the list of the sources that are to be transformed. Names of sources at first grid position: " + ArrayUtils.toString( sourcesAtFirstGridPosition ) );
+	}
+
+	private void autoSetPositions()
+	{
+		final int numPositions = sources.size();
+		final int numX = ( int ) Math.ceil( Math.sqrt( numPositions ) );
+		positions = new ArrayList<>();
+		int xIndex = 0;
+		int yIndex = 0;
+		for ( int i = 0; i < numPositions; i++ )
+		{
+			if ( xIndex == numX )
+			{
+				xIndex = 0;
+				yIndex++;
+			}
+			positions.add( new int[]{ xIndex, yIndex }  );
+			xIndex++;
+		}
+	}
+
+	public String getTableDataLocation()
+	{
+		return tableDataLocation;
+	}
+
+	public List< FinalRealInterval > getIntervals()
+	{
+		return Collections.unmodifiableList( intervals );
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/transform/SourceCropper.java b/src/main/java/de/embl/cba/mobie/transform/SourceCropper.java
new file mode 100644
index 000000000..b1e00e3b4
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/transform/SourceCropper.java
@@ -0,0 +1,72 @@
+/*-
+ * #%L
+ * BigDataViewer-Playground
+ * %%
+ * Copyright (C) 2019 - 2021 Nicolas Chiaruttini, EPFL - Robert Haase, MPI CBG - Christian Tischer, EMBL
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+package de.embl.cba.mobie.transform;
+
+import bdv.viewer.SourceAndConverter;
+import net.imglib2.RealInterval;
+import net.imglib2.Volatile;
+import net.imglib2.type.numeric.NumericType;
+
+public class SourceCropper< T extends NumericType<T> > {
+
+    private final SourceAndConverter< T > sourceAndConverter;
+    private final String name;
+    private final RealInterval realInterval;
+    private final boolean zeroMin;
+
+    public SourceCropper( SourceAndConverter< T > sourceAndConverter, String name, RealInterval realInterval, boolean zeroMin )
+    {
+        this.sourceAndConverter = sourceAndConverter;
+        this.name = name;
+        this.realInterval = realInterval;
+        this.zeroMin = zeroMin;
+    }
+
+    public SourceAndConverter< T > get()
+    {
+        CroppedSource< T > src = new CroppedSource<>( sourceAndConverter.getSpimSource(), name, realInterval, zeroMin );
+
+        if ( sourceAndConverter.asVolatile() != null )
+        {
+            CroppedSource< ? extends Volatile< T > > vsrc = new CroppedSource( sourceAndConverter.asVolatile().getSpimSource(), name, realInterval, zeroMin );
+
+//            SourceAndConverter< ? extends Volatile< T > > vsac = new SourceAndConverter( vsrc, sourceAndConverter.asVolatile().getConverter(), sourceAndConverter.asVolatile() );
+
+            SourceAndConverter< ? extends Volatile< T > > vsac = new SourceAndConverter( vsrc, sourceAndConverter.asVolatile().getConverter() );
+
+            final SourceAndConverter< T > outputSourceAndConverter = new SourceAndConverter<>( src, sourceAndConverter.getConverter(), vsac );
+
+            return outputSourceAndConverter;
+        }
+        else
+        {
+            return new SourceAndConverter< T >( src, sourceAndConverter.getConverter() );
+        }
+    }
+}
diff --git a/src/main/java/de/embl/cba/mobie/transform/SourceTransformer.java b/src/main/java/de/embl/cba/mobie/transform/SourceTransformer.java
new file mode 100644
index 000000000..fa8c0b2b3
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/transform/SourceTransformer.java
@@ -0,0 +1,14 @@
+package de.embl.cba.mobie.transform;
+
+import bdv.viewer.SourceAndConverter;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.type.numeric.NumericType;
+
+import java.util.List;
+
+public interface SourceTransformer< T extends NumericType< T > >
+{
+	List< SourceAndConverter< T > > transform( List< SourceAndConverter< T > > sourceAndConverters  );
+
+	AffineTransform3D getTransform( String name );
+}
diff --git a/src/main/java/de/embl/cba/mobie/transform/TransformerHelper.java b/src/main/java/de/embl/cba/mobie/transform/TransformerHelper.java
new file mode 100644
index 000000000..8fafd0c2b
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/transform/TransformerHelper.java
@@ -0,0 +1,21 @@
+package de.embl.cba.mobie.transform;
+
+import bdv.viewer.SourceAndConverter;
+
+import java.util.List;
+
+public class TransformerHelper
+{
+	public static List< SourceAndConverter< ? > > transformSourceAndConverters( List< SourceAndConverter< ? > > sourceAndConverters, List< SourceTransformer > sourceTransformers )
+	{
+		if ( sourceTransformers != null )
+		{
+			for ( SourceTransformer sourceTransformer : sourceTransformers )
+			{
+				sourceAndConverters = sourceTransformer.transform( sourceAndConverters );
+			}
+		}
+
+		return sourceAndConverters;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/ui/BdvBehaviourInstaller.java b/src/main/java/de/embl/cba/mobie/ui/BdvBehaviourInstaller.java
deleted file mode 100644
index 5beea6232..000000000
--- a/src/main/java/de/embl/cba/mobie/ui/BdvBehaviourInstaller.java
+++ /dev/null
@@ -1,106 +0,0 @@
-package de.embl.cba.mobie.ui;
-
-import bdv.util.BdvHandle;
-import de.embl.cba.bdv.utils.BdvUtils;
-import de.embl.cba.bdv.utils.Logger;
-import de.embl.cba.bdv.utils.popup.BdvPopupMenus;
-import de.embl.cba.mobie.bdv.BdvViewChanger;
-import de.embl.cba.mobie.bookmark.BookmarkManager;
-import de.embl.cba.mobie.bookmark.Location;
-import de.embl.cba.mobie.bookmark.LocationType;
-import de.embl.cba.mobie.platybrowser.GeneSearch;
-import de.embl.cba.mobie.universe.UniverseConfigurationDialog;
-import de.embl.cba.mobie.utils.Utils;
-import de.embl.cba.mobie.utils.ui.BdvTextOverlay;
-import org.scijava.ui.behaviour.ClickBehaviour;
-import org.scijava.ui.behaviour.io.InputTriggerConfig;
-import org.scijava.ui.behaviour.util.Behaviours;
-
-import javax.swing.*;
-
-public class BdvBehaviourInstaller
-{
-	private static final String RESTORE_DEFAULT_VIEW_TRIGGER = "ctrl R";
-
-	private final MoBIE moBIE;
-	private final BdvHandle bdv;
-	private final BookmarkManager bookmarkManager;
-
-	public BdvBehaviourInstaller( MoBIE moBIE )
-	{
-		this.moBIE = moBIE;
-		this.bookmarkManager = moBIE.getBookmarkManager();
-		this.bdv = moBIE.getSourcesDisplayManager().getBdv();
-	}
-
-	public void run( )
-	{
-		Behaviours behaviours = new Behaviours( new InputTriggerConfig() );
-		behaviours.install( bdv.getBdvHandle().getTriggerbindings(), "MoBIE behaviours" );
-
-		// TODO: Use BDV-Playground instead
-		BdvPopupMenus.addScreenshotAction( bdv );
-
-		BdvPopupMenus.addAction( bdv, "Log Current Location",
-				() -> {
-					new Thread( () -> {
-						Logger.log( "\nPosition:\n" + BdvUtils.getGlobalMousePositionString( bdv ) );
-						Logger.log( "View:\n" + BdvUtils.getBdvViewerTransformString( bdv ) );
-						Logger.log( "Normalised view:\n" + Utils.createNormalisedViewerTransformString( bdv, Utils.getMousePosition( bdv ) ) );
-					} ).start();
-				});
-
-		BdvPopupMenus.addAction( bdv, "Load Additional Bookmarks",
-				() -> {
-					new Thread( () -> {
-						SwingUtilities.invokeLater( () -> bookmarkManager.loadAdditionalBookmarks() );
-					} ).start();
-				});
-
-		BdvPopupMenus.addAction( bdv, "Save Current Settings As Bookmark",
-				() -> {
-					new Thread( () -> {
-						SwingUtilities.invokeLater( () -> bookmarkManager.saveCurrentSettingsAsBookmark() );
-					} ).start();
-				});
-
-		BdvPopupMenus.addAction( bdv, "Restore Default View" + BdvUtils.getShortCutString( RESTORE_DEFAULT_VIEW_TRIGGER ) ,
-				() -> new Thread( () -> restoreDefaultView() ).start() );
-
-		BdvPopupMenus.addAction( bdv, "Configure 3D View...",
-				() -> new Thread( () -> new UniverseConfigurationDialog( moBIE.getSourcesDisplayManager() ).showDialog() ).start() );
-
-		if ( moBIE.getProjectLocation().contains( "platybrowser" ) )
-		{
-			BdvPopupMenus.addAction( bdv, "Search Genes...", ( x, y ) ->
-			{
-				double[] micrometerPosition = new double[ 3 ];
-				BdvUtils.getGlobalMouseCoordinates( bdv ).localize( micrometerPosition );
-
-				final BdvTextOverlay bdvTextOverlay
-						= new BdvTextOverlay( bdv,
-						"Searching expressed genes; please wait...", micrometerPosition );
-
-				new Thread( () ->
-				{
-					final GeneSearch geneSearch = new GeneSearch( 3.0, micrometerPosition, moBIE.getSourcesModel() );
-					geneSearch.searchGenes();
-					bdvTextOverlay.setText( "" );
-				}
-				).start();
-			} );
-		}
-
-		behaviours.behaviour( ( ClickBehaviour ) ( x, y ) -> {
-			(new Thread( () -> {
-				restoreDefaultView();
-			} )).start();
-		}, "Toggle point overlays", RESTORE_DEFAULT_VIEW_TRIGGER ) ;
-	}
-
-	private void restoreDefaultView()
-	{
-		final Location location = new Location( LocationType.NormalisedViewerTransform, moBIE.getDefaultNormalisedViewerTransform().getRowPackedCopy() );
-		BdvViewChanger.moveToLocation( bdv, location );
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/ui/MoBIE.java b/src/main/java/de/embl/cba/mobie/ui/MoBIE.java
deleted file mode 100644
index 79c5960ce..000000000
--- a/src/main/java/de/embl/cba/mobie/ui/MoBIE.java
+++ /dev/null
@@ -1,238 +0,0 @@
-package de.embl.cba.mobie.ui;
-
-import bdv.util.BdvHandle;
-import com.google.gson.GsonBuilder;
-import com.google.gson.internal.LinkedTreeMap;
-import com.google.gson.stream.JsonReader;
-import de.embl.cba.bdv.utils.BdvUtils;
-import de.embl.cba.mobie.dataset.Datasets;
-import de.embl.cba.mobie.dataset.DatasetsParser;
-import de.embl.cba.mobie.image.SourcesModel;
-import de.embl.cba.mobie.bookmark.Bookmark;
-import de.embl.cba.mobie.bookmark.BookmarkReader;
-import de.embl.cba.mobie.bookmark.BookmarkManager;
-import de.embl.cba.tables.FileAndUrlUtils;
-import de.embl.cba.mobie.utils.Utils;
-import net.imglib2.realtransform.AffineTransform3D;
-
-import javax.swing.*;
-import java.awt.*;
-import java.io.*;
-import java.net.HttpURLConnection;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Map;
-import java.net.URI;
-
-import static de.embl.cba.mobie.utils.FileAndUrlUtils.getName;
-
-public class MoBIE
-{
-	public static final String PROTOTYPE_DISPLAY_VALUE = "01234567890123456789";
-
-	private final SourcesDisplayManager sourcesDisplayManager;
-	private final SourcesModel sourcesModel;
-	private final MoBIEOptions options;
-	private UserInterface userInterface;
-	private String dataset;
-	private final String projectBaseLocation; // without branch, pure github address
-	private String projectLocation; // with branch, actual url to data
-	private String imagesLocation; // selected dataset
-	private String tablesLocation;
-
-	private BookmarkManager bookmarkManager;
-	private Datasets datasets;
-	private final double[] levelingVector;
-	private String projectName;
-	private AffineTransform3D defaultNormalisedViewerTransform;
-
-	public MoBIE( String projectLocation )
-	{
-		this( projectLocation, MoBIEOptions.options() );
-	}
-
-	@Deprecated
-	public MoBIE(
-			String projectLocation,
-			String tablesLocation )
-	{
-		this( projectLocation, MoBIEOptions.options().tableDataLocation( tablesLocation ) );
-	}
-
-	public MoBIE(
-			String projectBaseLocation,
-			MoBIEOptions options )
-	{
-		this.projectBaseLocation = projectBaseLocation;
-		this.options = options;
-		projectName = getName( this.projectBaseLocation );
-
-		configureDatasetsRootLocations();
-		appendSpecificDatasetLocations(); // TODO: separate this such that this MoBIE class does not need to be re-instantiated
-
-		sourcesModel = new SourcesModel( imagesLocation, options.values.getImageDataStorageModality(), tablesLocation );
-		sourcesDisplayManager = new SourcesDisplayManager( sourcesModel, projectName );
-		bookmarkManager = fetchBookmarks( projectLocation );
-		levelingVector = fetchLeveling( imagesLocation );
-
-		SwingUtilities.invokeLater( () -> {
-			userInterface = new UserInterface( this );
-			bookmarkManager.setView( "default" );
-			final BdvHandle bdvHandle = sourcesDisplayManager.getBdv();
-			userInterface.setBdvWindowPositionAndSize( bdvHandle );
-			defaultNormalisedViewerTransform = Utils.createNormalisedViewerTransform( bdvHandle, BdvUtils.getBdvWindowCenter( bdvHandle ) );
-			new BdvBehaviourInstaller( this ).run();
-		} );
-	}
-
-	public String getTablesLocation()
-	{
-		return tablesLocation;
-	}
-
-	public String getProjectName()
-	{
-		return projectName;
-	}
-
-	public MoBIEOptions getOptions()
-	{
-		return options;
-	}
-
-	public AffineTransform3D getDefaultNormalisedViewerTransform()
-	{
-		return defaultNormalisedViewerTransform;
-	}
-
-	public double[] getLevelingVector()
-	{
-		return levelingVector;
-	}
-
-	public SourcesModel getSourcesModel()
-	{
-		return sourcesModel;
-	}
-
-	public String getProjectLocation()
-	{
-		return projectBaseLocation; // without branch information, which is stored in the options
-	}
-
-	public String getDataset()
-	{
-		return dataset;
-	}
-
-	public ArrayList< String > getDatasets()
-	{
-		return datasets.datasets;
-	}
-
-	public BookmarkManager getBookmarkManager()
-	{
-		return bookmarkManager;
-	}
-
-	private double[] fetchLeveling( String dataLocation )
-	{
-		final String levelingFile = FileAndUrlUtils.combinePath( dataLocation, "misc/leveling.json" );
-		try
-		{
-			InputStream is = FileAndUrlUtils.getInputStream( levelingFile );
-			final JsonReader reader = new JsonReader( new InputStreamReader( is, "UTF-8" ) );
-			final GsonBuilder gsonBuilder = new GsonBuilder();
-			LinkedTreeMap linkedTreeMap = gsonBuilder.create().fromJson( reader, Object.class );
-			ArrayList< Double >  normalVector = ( ArrayList< Double > ) linkedTreeMap.get( "NormalVector" );
-			final double[] doubles = normalVector.stream().mapToDouble( Double::doubleValue ).toArray();
-			return doubles;
-		}
-		catch ( Exception e )
-		{
-			return null; // new double[]{0.70,0.56,0.43};
-		}
-	}
-
-	private void appendSpecificDatasetLocations()
-	{
-		this.datasets = new DatasetsParser().fetchProjectDatasets( projectLocation );
-		this.dataset = options.values.getDataset();
-
-		if ( dataset == null )
-		{
-			dataset = datasets.defaultDataset;
-		}
-
-		projectLocation = FileAndUrlUtils.combinePath( projectLocation, dataset );
-		imagesLocation = FileAndUrlUtils.combinePath( imagesLocation, dataset );
-		tablesLocation = FileAndUrlUtils.combinePath( tablesLocation, dataset );
-
-		Utils.log( "Fetching project from: " + projectLocation );
-		Utils.log( "Fetching images from: " + imagesLocation );
-		Utils.log( "Image data storage modality: " + options.values.getImageDataStorageModality() );
-		Utils.log( "Fetching tables from: " + tablesLocation );
-	}
-
-	private void configureDatasetsRootLocations( )
-	{
-		this.projectLocation = projectBaseLocation;
-		this.imagesLocation = options.values.getImageDataLocation() != null ? options.values.getImageDataLocation() : projectBaseLocation;
-		this.tablesLocation = options.values.getTableDataLocation() != null ? options.values.getTableDataLocation() : projectBaseLocation;
-
-		projectLocation = FileAndUrlUtils.removeTrailingSlash( projectLocation );
-		imagesLocation = FileAndUrlUtils.removeTrailingSlash( imagesLocation );
-		tablesLocation = FileAndUrlUtils.removeTrailingSlash( tablesLocation );
-
-		projectLocation = adaptUrl( projectLocation, options.values.getProjectBranch() );
-		imagesLocation = adaptUrl( imagesLocation, options.values.getProjectBranch() );
-		tablesLocation = adaptUrl( tablesLocation, options.values.getProjectBranch() );
-
-		// two different locations of the data w.r.t someLocation are supported:
-		// the data can either be directly underneath someLocation
-		// or in someLocation/data
-		// test if someLocation/data exists and set if to the new location if it does
-		// NOTE this produces a corner case for nested "data" folders, i.e. "data/data", but it's the best solution I came up with so far
-		if(FileAndUrlUtils.exists(projectLocation + "/data/datasets.json")) {
-			projectLocation = projectLocation + "/data";
-		}
-		if(FileAndUrlUtils.exists(imagesLocation + "/data/datasets.json")) {
-			imagesLocation = imagesLocation + "/data";
-		}
-		if(FileAndUrlUtils.exists(tablesLocation + "/data/datasets.json")) {
-			tablesLocation = tablesLocation + "/data";
-		}
-	}
-
-	private String adaptUrl( String url, String projectBranch )
-	{
-		if ( url.contains( "github.com" ) )
-		{
-			url = url.replace( "github.com", "raw.githubusercontent.com" );
-			url += "/" + projectBranch;
-		}
-		return url;
-	}
-
-	private BookmarkManager fetchBookmarks( String location )
-	{
-		BookmarkReader bookmarkParser = new BookmarkReader(location);
-		Map< String, Bookmark > nameToBookmark = bookmarkParser.readDefaultBookmarks();
-
-		return new BookmarkManager( this, nameToBookmark, bookmarkParser);
-	}
-
-	// TODO: This should be dataset dependent?
-	public SourcesDisplayManager getSourcesDisplayManager()
-	{
-		return sourcesDisplayManager;
-	}
-
-	public void close()
-	{
-		sourcesDisplayManager.removeAllSourcesFromViewers();
-		sourcesDisplayManager.getBdv().close();
-		userInterface.dispose();
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/ui/SourcesDisplayManager.java b/src/main/java/de/embl/cba/mobie/ui/SourcesDisplayManager.java
deleted file mode 100644
index 3a49c1512..000000000
--- a/src/main/java/de/embl/cba/mobie/ui/SourcesDisplayManager.java
+++ /dev/null
@@ -1,773 +0,0 @@
-package de.embl.cba.mobie.ui;
-
-import bdv.tools.transformation.TransformedSource;
-import bdv.util.*;
-import bdv.viewer.Source;
-import de.embl.cba.bdv.utils.BdvUtils;
-import de.embl.cba.bdv.utils.Logger;
-import de.embl.cba.bdv.utils.lut.GlasbeyARGBLut;
-import de.embl.cba.bdv.utils.sources.ARGBConvertedRealSource;
-import de.embl.cba.bdv.utils.sources.Metadata;
-import de.embl.cba.mobie.Constants;
-import de.embl.cba.mobie.image.SourceAndMetadataChangedListener;
-import de.embl.cba.mobie.image.SourceGroupLabelSourceMetadata;
-import de.embl.cba.mobie.image.SourceGroupLabelSourceCreator;
-import de.embl.cba.mobie.image.SourcesModel;
-import de.embl.cba.mobie.utils.Utils;
-import de.embl.cba.tables.FileAndUrlUtils;
-import de.embl.cba.tables.FileUtils;
-import de.embl.cba.tables.TableColumns;
-import de.embl.cba.tables.color.ColorUtils;
-import de.embl.cba.tables.color.LazyLabelsARGBConverter;
-import de.embl.cba.tables.ij3d.UniverseUtils;
-import de.embl.cba.tables.image.DefaultImageSourcesModel;
-import de.embl.cba.tables.image.SourceAndMetadata;
-import de.embl.cba.tables.tablerow.TableRowImageSegment;
-import de.embl.cba.tables.view.Segments3dView;
-import de.embl.cba.tables.view.SegmentsBdvView;
-import de.embl.cba.tables.view.TableRowsTableView;
-import de.embl.cba.tables.view.combined.SegmentsTableBdvAnd3dViews;
-import ij3d.ContentConstants;
-import ij3d.Image3DUniverse;
-import net.imglib2.realtransform.AffineTransform3D;
-import net.imglib2.type.numeric.ARGBType;
-import org.jetbrains.annotations.NotNull;
-import org.scijava.ui.behaviour.ClickBehaviour;
-import org.scijava.ui.behaviour.io.InputTriggerConfig;
-import org.scijava.ui.behaviour.util.Behaviours;
-import org.scijava.vecmath.Color3f;
-import weka.Run;
-
-import javax.swing.*;
-import java.awt.*;
-import java.net.URI;
-import java.util.List;
-import java.util.*;
-
-import static de.embl.cba.mobie.utils.Utils.*;
-
-public class SourcesDisplayManager extends JPanel
-{
-    private final Map< String, SegmentsTableBdvAnd3dViews > sourceNameToLabelViews;
-    private Map< String, SourceAndMetadata< ? > > sourceNameToSourceAndCurrentMetadata;
-    private BdvHandle bdv;
-    private final SourcesModel sourcesModel;
-    private final String projectName;
-    private Image3DUniverse universe;
-    private int meshSmoothingIterations = 5; // TODO: Why is this here?
-
-    private List< SourceAndMetadataChangedListener > listeners = new ArrayList<>(  );
-
-    public SourcesDisplayManager( SourcesModel sourcesModel, String projectName )
-    {
-        this.sourcesModel = sourcesModel;
-        this.projectName = projectName;
-        sourceNameToLabelViews = new LinkedHashMap<>();
-        sourceNameToSourceAndCurrentMetadata = new LinkedHashMap<>();
-    }
-
-    public static void updateSource3dView( SourceAndMetadata< ? > sam, SourcesDisplayManager sourcesDisplayManager, boolean forceRepaint )
-    {
-        if ( sam.metadata().type.equals( Metadata.Type.Segmentation ) ) return;
-
-        if ( sam.metadata().showImageIn3d )
-        {
-            sourcesDisplayManager.showSourceInVolumeViewer( sam, forceRepaint );
-        }
-        else
-        {
-            if ( sam.metadata().content != null )
-                sam.metadata().content.setVisible( false );
-        }
-    }
-
-    public static void updateSegments3dView( SourceAndMetadata< ? > sam, SourcesDisplayManager sourcesDisplayManager )
-    {
-        if ( sam.metadata().views != null )
-        {
-            final Segments3dView< TableRowImageSegment > segments3dView = sam.metadata().views.getSegments3dView();
-            segments3dView.setVoxelSpacing( sam.metadata().resolution3dView );
-            segments3dView.showSelectedSegments( sam.metadata().showSelectedSegmentsIn3d, false );
-
-            if ( sam.metadata().showSelectedSegmentsIn3d ) sourcesDisplayManager.setUniverse( segments3dView.getUniverse() );
-        }
-    }
-
-    public void setSourceColor( SourceAndMetadata< ? > sam, Color color )
-    {
-        sam.metadata().bdvStackSource.setColor( ColorUtils.getARGBType( color ) );
-
-        if ( sam.metadata().content != null )
-            sam.metadata().content.setColor( new Color3f( color ));
-
-        if ( sourceNameToLabelViews.containsKey( sam.metadata().displayName ) )
-        {
-            final SegmentsBdvView< TableRowImageSegment > segmentsBdvView
-                    = sourceNameToLabelViews.get( sam.metadata().displayName ).getSegmentsBdvView();
-
-            segmentsBdvView.setLabelSourceSingleColor( ColorUtils.getARGBType( color ) );
-        }
-    }
-
-    public void setSourceColor( String sourceName, Color color )
-    {
-        if ( ! sourceNameToSourceAndCurrentMetadata.containsKey( sourceName ) )
-        {
-            System.err.println( "Source not displayed: " + sourceName );
-            return;
-        }
-
-        final SourceAndMetadata< ? > sam = sourceNameToSourceAndCurrentMetadata.get(sourceName);
-
-        setSourceColor( sam, color );
-    }
-
-    public Image3DUniverse getUniverse()
-    {
-        return universe;
-    }
-
-    public int getMeshSmoothingIterations()
-    {
-        return meshSmoothingIterations;
-    }
-
-    public void setMeshSmoothingIterations( int meshSmoothingIterations )
-    {
-        this.meshSmoothingIterations = meshSmoothingIterations;
-
-        for ( SegmentsTableBdvAnd3dViews views : sourceNameToLabelViews.values() )
-            views.getSegments3dView().setMeshSmoothingIterations( meshSmoothingIterations );
-    }
-
-    public void setVoxelSpacing3DView( String sourceName, double voxelSpacing )
-    {
-        SourceAndMetadata< ? > sam = sourceNameToSourceAndCurrentMetadata.get( sourceName );
-
-        if ( sam.metadata().resolution3dView != voxelSpacing )
-        {
-            sam.metadata().resolution3dView = voxelSpacing;
-
-            if ( sam.metadata().content != null )
-                updateSource3dView( sam, this, true );
-
-            if ( sourceNameToLabelViews.containsKey( sourceName ) )
-            {
-                Segments3dView< TableRowImageSegment > segments3dView = sourceNameToLabelViews.get( sourceName ).getSegments3dView();
-                segments3dView.setVoxelSpacing( voxelSpacing );
-                segments3dView.showSelectedSegments( sam.metadata().showSelectedSegmentsIn3d, true );
-            }
-        }
-    }
-
-    public void updateCurrentMetadata( String sourceName )
-    {
-        final Metadata metadata = sourceNameToSourceAndCurrentMetadata.get( sourceName ).metadata();
-        final Source< ? > source = sourceNameToSourceAndCurrentMetadata.get( sourceName ).source();
-        final BdvStackSource< ? > bdvStackSource = metadata.bdvStackSource;
-
-        metadata.color = bdvStackSource.getConverterSetups().get( 0 ).getColor().toString();
-        metadata.addedTransform = getAddedSourceTransform( bdvStackSource, source ).getRowPackedCopy();
-
-        if (sourceNameToLabelViews.containsKey(sourceName)) {
-            TableRowsTableView<TableRowImageSegment> sourceTableRowsTableView = metadata.views.getTableRowsTableView();
-
-            if (!metadata.views.getSegmentsBdvView().isLabelMaskShownAsBinaryMask()) {
-                metadata.color = sourceTableRowsTableView.getColoringLUTName();
-                metadata.colorByColumn = sourceTableRowsTableView.getColoringColumnName();
-                metadata.valueLimits = sourceTableRowsTableView.getColorByColumnValueLimits();
-            }
-
-            ArrayList<TableRowImageSegment> selectedSegments = sourceTableRowsTableView.getSelectedLabelIds();
-            if (selectedSegments != null) {
-                ArrayList<Double> selectedLabelIds = new ArrayList<>();
-                for (TableRowImageSegment segment : selectedSegments) {
-                    selectedLabelIds.add(segment.labelId());
-                }
-                metadata.selectedSegmentIds = selectedLabelIds;
-            }
-
-            ArrayList<String> additionalTables = sourceTableRowsTableView.getAdditionalTables();
-            if (additionalTables != null & additionalTables.size() > 0 ) {
-                metadata.additionalSegmentTableNames = new ArrayList<>();
-                // ensure tables are unique
-                for (String tableName : sourceTableRowsTableView.getAdditionalTables()) {
-                    if (!metadata.additionalSegmentTableNames.contains(tableName)) {
-                        metadata.additionalSegmentTableNames.add(tableName);
-                    }
-                }
-            }
-
-            metadata.showSelectedSegmentsIn3d = metadata.views.getSegments3dView().showSelectedSegments();
-        }
-
-        if (metadata.content != null) {
-            if (metadata.content.isVisible()) {
-                metadata.showImageIn3d = true;
-            } else {
-                metadata.showImageIn3d = false;
-            }
-        } else {
-            metadata.showImageIn3d = false;
-        }
-
-        double[] currentContrastLimits = new double[2];
-        currentContrastLimits[0] = bdvStackSource.getConverterSetups().get(0).getDisplayRangeMin();
-        currentContrastLimits[1] = bdvStackSource.getConverterSetups().get(0).getDisplayRangeMax();
-        metadata.contrastLimits = currentContrastLimits;
-    }
-
-    @NotNull
-    protected AffineTransform3D getAddedSourceTransform( BdvStackSource< ? > bdvStackSource, Source< ? > source )
-    {
-        final int t = 0; // TODO: Once we have data with multiple time points we may have to rethink this...
-        final AffineTransform3D initialTransform = new AffineTransform3D();
-        source.getSourceTransform( t, 0, initialTransform );
-        final AffineTransform3D currentTransform = new AffineTransform3D();
-        bdvStackSource.getSources().get( 0 ).getSpimSource().getSourceTransform( t, 0, currentTransform );
-        return currentTransform.copy().preConcatenate( initialTransform.inverse() );
-    }
-
-    public void showSourceInVolumeViewer( SourceAndMetadata< ? > sam, boolean forceRepaint )
-    {
-    	if ( sam.metadata().showImageIn3d )
-        {
-            initAndShowUniverseIfNecessary();
-
-            if ( sam.metadata().content == null || forceRepaint )
-            {
-                DisplaySettings3DViewer settings = getDisplaySettings3DViewer( sam );
-
-                if ( forceRepaint )
-                {
-                    if ( universe.getContents().contains( sam.metadata().content ) )
-                    {
-                        universe.removeContent( sam.metadata().content.getName() );
-                    }
-                }
-
-                if ( sam.metadata().resolution3dView == 0 )
-                {
-                    // auto-adjust voxel spacing using maxNumVoxels
-                    sam.metadata().content = UniverseUtils.addSourceToUniverse(
-                            universe,
-                            sam.source(),
-                            300 * 300 * 300, // TODO: make adaptable
-                            settings.displayMode,
-                            settings.color,
-                            settings.transparency,
-                            0,
-                            settings.max );
-                }
-                else
-                {
-                    sam.metadata().content = UniverseUtils.addSourceToUniverse(
-                            universe,
-                            sam.source(),
-                            sam.metadata().resolution3dView,
-                            settings.displayMode,
-                            settings.color,
-                            settings.transparency,
-                            0,
-                            settings.max );
-                }
-            }
-            else
-            {
-                if ( ! universe.getContents().contains( sam.metadata().content ) )
-                    universe.addContent( sam.metadata().content );
-
-                sam.metadata().content.setVisible( true );
-            }
-        }
-    	else
-        {
-            return;
-        }
-    }
-
-    public void initAndShowUniverseIfNecessary()
-    {
-        if ( universe == null ) init3DUniverse();
-
-        UniverseUtils.showUniverseWindow( universe, bdv.getViewerPanel() );
-    }
-
-    private void init3DUniverse()
-	{
-		universe = new Image3DUniverse();
-	}
-
-    public void setUniverse( Image3DUniverse universe )
-    {
-        this.universe = universe;
-    }
-
-    public void close()
-    {
-
-    }
-
-    class DisplaySettings3DViewer
-    {
-        int displayMode;
-        float transparency;
-        int max;
-        ARGBType color = new ARGBType( 0xffffffff );
-    }
-
-    private DisplaySettings3DViewer getDisplaySettings3DViewer( SourceAndMetadata< ? > sam )
-    {
-        final DisplaySettings3DViewer settings = new DisplaySettings3DViewer();
-        if ( sam.metadata().displayName.contains( Constants.PROSPR ) // TODO: make more general
-                || sam.metadata().displayName.contains( Constants.SEGMENTED )  )
-        {
-            settings.displayMode = ContentConstants.SURFACE;
-            settings.max = 1;
-            settings.transparency = 0.3F;
-        }
-        else if ( sam.metadata().displayName.contains( Constants.EM_FILE_ID ) )
-        {
-            settings.displayMode = ContentConstants.ORTHO;
-            settings.max = 255;
-            settings.transparency = 0.0F;
-        }
-        else
-        {
-            // do nothing
-        }
-
-        if ( sam.metadata().content != null )
-        {
-            final ARGBType argbType = ColorUtils.getARGBType( sam.metadata().content.getColor().get() );
-            if ( argbType != null ) settings.color = argbType;
-        }
-        else
-        {
-            final ARGBType argbType = ColorUtils.getARGBType( sam.metadata().color );
-            if ( argbType != null ) settings.color = argbType;
-        }
-
-        return settings;
-    }
-
-    public void show( String sourceName )
-    {
-        final SourceAndMetadata< ? > samDefault = getSourceAndDefaultMetadata( sourceName );
-
-        // make a copy here so that changes to the current metadata, don't affect the default
-        // this means any changes to current metadata won't persist when sources are removed and added again
-        final SourceAndMetadata< ? > samCurrent = new SourceAndMetadata( samDefault.source(), samDefault.metadata().copy() );
-
-        show( samCurrent );
-    }
-
-    public SourceAndMetadata< ? > getSourceAndDefaultMetadata( String sourceName )
-    {
-        return sourcesModel.sources().get( sourceName );
-    }
-
-    // necessary as loading from bookmark creates a new sourceAndMetadata that is separate from the default
-    public SourceAndMetadata< ? > getSourceAndCurrentMetadata( String sourceName )
-    {
-        return sourceNameToSourceAndCurrentMetadata.get( sourceName );
-    }
-
-    public ArrayList< String > getSourceNames()
-    {
-        return new ArrayList<>( sourcesModel.sources().keySet() );
-    }
-
-    public Set< String > getVisibleSourceNames()
-    {
-        return Collections.unmodifiableSet( new HashSet<>( sourceNameToSourceAndCurrentMetadata.keySet() ) );
-    }
-
-    public void show( SourceAndMetadata< ? > sam )
-    {
-        final String sourceName = sam.metadata().displayName;
-
-        if ( sam.metadata().bdvStackSource != null  )
-        {
-            Logger.log( "Source is already shown: " + sourceName + "" );
-            return;
-        }
-
-        Logger.log( "Adding source: " + sourceName + "..." );
-        sourceNameToSourceAndCurrentMetadata.put( sourceName, sam );
-
-        Prefs.showScaleBar( true );
-        Prefs.showMultibox( false );
-
-        final Metadata metadata = sam.metadata();
-
-        if ( metadata.type.equals( Metadata.Type.Segmentation ) )
-        {
-            if ( metadata.segmentsTablePath != null )
-            {
-                showAnnotatedLabelsSource( sam );
-            }
-            else
-            {
-                throw new RuntimeException( "Visualisation of label images without tables is not supported." );
-                //showLabelsSource( sam );
-            }
-        }
-        else if ( metadata.type.equals( Metadata.Type.Image ) )
-        {
-            showIntensitySource( sam );
-        }
-        else if ( metadata.type.equals( Metadata.Type.Mask ) )
-        {
-            showIntensitySource( sam );
-        }
-
-        adjustSourceTransform( sam );
-        for ( SourceAndMetadataChangedListener listener : listeners )
-        {
-            listener.addedToBDV( sam );
-        }
-    }
-
-    private void adjustSourceTransform( SourceAndMetadata< ? > sam )
-    {
-        if ( sam.metadata().addedTransform != null )
-        {
-            final TransformedSource< ? > source = ( TransformedSource< ? > ) sam.metadata().bdvStackSource.getSources().get( 0 ).getSpimSource();
-            final AffineTransform3D transform = new AffineTransform3D();
-            transform.set( sam.metadata().addedTransform );
-            source.setFixedTransform( transform );
-        }
-    }
-
-    private void showIntensitySource( SourceAndMetadata< ? > sam )
-    {
-        final Metadata metadata = sam.metadata();
-
-        final BdvStackSource< ? > bdvStackSource = addSourceToBDV( sam );
-        bdvStackSource.setActive( true );
-
-        setDisplayRange( bdvStackSource, metadata );
-        setColor( bdvStackSource, metadata );
-
-        bdv = bdvStackSource.getBdvHandle();
-
-        metadata.bdvStackSource = bdvStackSource;
-    }
-
-    private BdvStackSource addSourceToBDV( SourceAndMetadata< ? > sam )
-    {
-        BdvOptions options = BdvOptions.options().addTo( bdv ).frameTitle( projectName ); //.numRenderingThreads( 1 );
-
-        return BdvFunctions.show(
-                sam.source(),
-                Utils.getNumTimePoints( sam.source() ),
-                options );
-    }
-
-    private void setColor( BdvStackSource bdvStackSource, Metadata metadata )
-    {
-        ARGBType argbType;
-        if ( metadata.color.equals( Constants.RANDOM_FROM_GLASBEY ) )
-        {
-            final GlasbeyARGBLut glasbeyARGBLut = new GlasbeyARGBLut();
-            final int argb = glasbeyARGBLut.getARGB( createRandom( metadata.imageId ) );
-            argbType = new ARGBType( argb );
-        }
-        else
-        {
-            argbType = ColorUtils.getARGBType( metadata.color );
-        }
-
-        if ( argbType != null ) bdvStackSource.setColor( argbType );
-    }
-
-    private void setDisplayRange( BdvStackSource bdvStackSource, Metadata metadata )
-    {
-        if ( metadata.contrastLimits != null )
-        {
-            bdvStackSource.setDisplayRange( metadata.contrastLimits[ 0 ], metadata.contrastLimits[ 1 ] );
-        }
-    }
-
-    // TODO: remove?!
-    @Deprecated
-    private void showLabelsSource( SourceAndMetadata< ? > sam )
-    {
-        final LazyLabelsARGBConverter lazyLabelsARGBConverter = new LazyLabelsARGBConverter();
-        // TODO: Bug: this is not used!!
-        final ARGBConvertedRealSource source =
-                new ARGBConvertedRealSource( sam.source(), lazyLabelsARGBConverter );
-
-        sam.metadata().bdvStackSource = addSourceToBDV( sam );
-
-        setDisplayRange( sam.metadata().bdvStackSource, sam.metadata() );
-
-        final Behaviours behaviours = new Behaviours( new InputTriggerConfig() );
-        behaviours.install( bdv.getBdvHandle().getTriggerbindings(), sam.metadata().displayName + "-bdv-select-handler" );
-
-    behaviours.behaviour( ( ClickBehaviour ) ( x, y ) ->
-            new Thread( () ->
-            {
-                lazyLabelsARGBConverter.getColoringModel().incRandomSeed();
-                BdvUtils.repaint( bdv );
-            } ).start(),
-            sam.metadata().displayName + "-change-color-random-seed",
-            "ctrl L" );
-    }
-
-    private void showAnnotatedLabelsSource( SourceAndMetadata< ? > sam )
-    {
-        if ( sam.metadata().misc.containsKey( SourceGroupLabelSourceCreator.SOURCE_GROUP_LABEL_IMAGE_METADATA ) )
-        {
-            List< TableRowImageSegment > segments = createGroupedSourcesSegmentsFromTableFile(
-                    sam.metadata().segmentsTablePath,
-                    sam.metadata().imageId,
-                    ( SourceGroupLabelSourceMetadata ) sam.metadata().misc.get( SourceGroupLabelSourceCreator.SOURCE_GROUP_LABEL_IMAGE_METADATA )
-                    );
-
-            final SegmentsTableBdvAnd3dViews views
-                    = new SegmentsTableBdvAnd3dViews(
-                    segments,
-                    createLabelsSourceModel( sam ),
-                    sam.metadata().imageId,
-                    bdv,
-                    universe );
-
-            sam.metadata().views = views;
-            configureSegments3dView( views, sam );
-            configureTableView( views, sam );
-
-            bdv = views.getSegmentsBdvView().getBdv();
-
-            sam.metadata().bdvStackSource = views
-                    .getSegmentsBdvView()
-                    .getCurrentSources().get( 0 )
-                    .metadata().bdvStackSource;
-
-            setDisplayRange( sam.metadata().bdvStackSource, sam.metadata() );
-
-            sourceNameToLabelViews.put( sam.metadata().displayName, views );
-        }
-        else
-        {
-            List< TableRowImageSegment > segments = createAnnotatedImageSegmentsFromTableFile(
-                    sam.metadata().segmentsTablePath,
-                    sam.metadata().imageId );
-
-            final SegmentsTableBdvAnd3dViews views
-                    = new SegmentsTableBdvAnd3dViews(
-                    segments,
-                    createLabelsSourceModel( sam ),
-                    sam.metadata().imageId,
-                    bdv,
-                    universe );
-
-            sam.metadata().views = views;
-            configureSegments3dView( views, sam );
-            configureTableView( views, sam );
-
-            bdv = views.getSegmentsBdvView().getBdv();
-
-            sam.metadata().bdvStackSource = views
-                    .getSegmentsBdvView()
-                    .getCurrentSources().get( 0 )
-                    .metadata().bdvStackSource;
-
-            setDisplayRange( sam.metadata().bdvStackSource, sam.metadata() );
-
-            sourceNameToLabelViews.put( sam.metadata().displayName, views );
-
-            setUniverse();
-        }
-    }
-
-	private void setUniverse()
-	{
-		for ( SegmentsTableBdvAnd3dViews views : sourceNameToLabelViews.values() )
-		{
-			if ( views.getSegments3dView().getUniverse() != null )
-			{
-				universe = views.getSegments3dView().getUniverse();
-				continue;
-			}
-		}
-	}
-
-	private void configureTableView( SegmentsTableBdvAnd3dViews views, SourceAndMetadata< ? > sam )
-    {
-        final TableRowsTableView< TableRowImageSegment > tableRowsTableView = views.getTableRowsTableView();
-
-        final String tablesLocation = FileAndUrlUtils.getParentLocation( sam.metadata().segmentsTablePath );
-        tableRowsTableView.setTablesDirectory( tablesLocation );
-        tableRowsTableView.setMergeByColumnName( Constants.SEGMENT_LABEL_ID );
-        //tableRowsTableView.setSelectionMode( TableRowsTableView.SelectionMode.FocusOnly );
-
-        mergeDefaultTableWithAdditionalTables( sam, tableRowsTableView, tablesLocation );
-
-        // select segments
-        if ( sam.metadata().selectedSegmentIds != null && sam.metadata().selectedSegmentIds.size() > 0 )
-        {
-            // TODO: in table-utils: the selection should not be part of any specific view
-            views.getSegmentsBdvView().select( sam.metadata().selectedSegmentIds );
-        }
-
-        // apply colorByColumn
-        if ( sam.metadata().colorByColumn != null && sam.metadata().color != null )
-        {
-            if ( sam.metadata().valueLimits != null ) {
-                views.getTableRowsTableView().colorByColumn(
-                        sam.metadata().colorByColumn,
-                        sam.metadata().color,
-                        sam.metadata().valueLimits[0],
-                        sam.metadata().valueLimits[1]);
-            } else {
-                views.getTableRowsTableView().colorByColumn(
-                        sam.metadata().colorByColumn,
-                        sam.metadata().color,
-                        null,
-                        null);
-            }
-        }
-    }
-
-    private void mergeDefaultTableWithAdditionalTables( SourceAndMetadata< ? > sam, TableRowsTableView< TableRowImageSegment > tableRowsTableView, String tablesLocation )
-    {
-        if ( sam.metadata().additionalSegmentTableNames == null ) return;
-
-        for ( String tableName : sam.metadata().additionalSegmentTableNames )
-        {
-            String newTablePath = FileAndUrlUtils.combinePath( tablesLocation, tableName + ".csv" );
-            tableRowsTableView.addAdditionalTable(newTablePath);
-
-            if ( newTablePath.startsWith( "http" ) )
-                newTablePath = FileUtils.resolveTableURL( URI.create( newTablePath ) );
-
-            final Map< String, List< String > > newColumns =
-                    TableColumns.openAndOrderNewColumns(
-                        tableRowsTableView.getTable(),
-                        Constants.SEGMENT_LABEL_ID,
-                        newTablePath );
-
-            newColumns.remove( Constants.SEGMENT_LABEL_ID );
-            tableRowsTableView.addColumns( newColumns );
-        }
-    }
-
-    private void configureSegments3dView( SegmentsTableBdvAnd3dViews views, SourceAndMetadata< ? > sam )
-    {
-        final Segments3dView< TableRowImageSegment > segments3dView = views.getSegments3dView();
-
-        segments3dView.setObjectsName( sam.metadata().imageId );
-        segments3dView.setSegmentFocusZoomLevel( 0.1 );
-        segments3dView.setMaxNumSegmentVoxels( 100 * 100 * 100 );
-
-        if ( sam.metadata().imageId.contains( "nuclei" ) )
-        {
-            //segments3dView.setVoxelSpacing3DView( voxelSpacing3DView );
-            segments3dView.setMeshSmoothingIterations( meshSmoothingIterations );
-            segments3dView.setSegmentFocusDxyMin( 0 );
-            segments3dView.setSegmentFocusDzMin( 0 );
-            segments3dView.setTransparency( 0.0 );
-        }
-        else if ( sam.metadata().imageId.contains( "cells" ) )
-        {
-            //segments3dView.setVoxelSpacing3DView( voxelSpacing3DView );
-            segments3dView.setMeshSmoothingIterations( meshSmoothingIterations );
-            segments3dView.setSegmentFocusDxyMin( 0 );
-            segments3dView.setSegmentFocusDzMin( 0 );
-            segments3dView.setTransparency( 0.4 );
-        }
-        else if ( sam.metadata().imageId.contains( "chromatin" ) )
-        {
-            //segments3dView.setVoxelSpacing3DView( voxelSpacing3DView );
-            segments3dView.setMeshSmoothingIterations( meshSmoothingIterations );
-            segments3dView.setSegmentFocusDxyMin( 0 );
-            segments3dView.setSegmentFocusDzMin( 0 );
-            segments3dView.setTransparency( 0.6 );
-        }
-        else
-        {
-            segments3dView.setMeshSmoothingIterations( meshSmoothingIterations );
-            segments3dView.setSegmentFocusDxyMin( 0 );
-            segments3dView.setSegmentFocusDzMin( 0 );
-            segments3dView.setTransparency( 0.3 );
-        }
-
-        segments3dView.setVoxelSpacing( sam.metadata().resolution3dView );
-        segments3dView.showSelectedSegments( sam.metadata().showSelectedSegmentsIn3d, true );
-    }
-
-    public Map< String, SegmentsTableBdvAnd3dViews > getSourceNameToLabelViews()
-    {
-        return sourceNameToLabelViews;
-    }
-
-    private DefaultImageSourcesModel createLabelsSourceModel(
-            SourceAndMetadata< ? > labelsSAM )
-    {
-        final DefaultImageSourcesModel imageSourcesModel
-                = new DefaultImageSourcesModel( false );
-
-        imageSourcesModel.addSourceAndMetadata(
-                labelsSAM.metadata().imageId,
-                labelsSAM );
-
-        return imageSourcesModel;
-    }
-
-    public void removeSourceFromViewers( SourceAndMetadata< ? > sam )
-    {
-        sam.metadata().showImageIn3d = false;
-        sam.metadata().showSelectedSegmentsIn3d = false;
-        updateSegments3dView( sam, this );
-        updateSource3dView( sam, this, false );
-
-		removeLabelViews( sam.metadata().displayName );
-		sourceNameToSourceAndCurrentMetadata.remove( sam.metadata().displayName );
-
-		sam.metadata().bdvStackSource.removeFromBdv();
-
-        for ( SourceAndMetadataChangedListener listener : listeners )
-        {
-            listener.removedFromBDV( sam );
-        }
-
-		if ( sam.metadata().content != null )
-        {
-            universe.removeContent( sam.metadata().content.getName() );
-        }
-    }
-
-	private void removeLabelViews( String sourceName )
-	{
-		if ( sourceNameToLabelViews.keySet().contains( sourceName ) )
-        {
-            // TODO work more on closing the views properly (also free the memory)
-			sourceNameToLabelViews.get( sourceName ).getTableRowsTableView().close();
-            sourceNameToLabelViews.get( sourceName ).getSegmentsBdvView().close();
-            sourceNameToLabelViews.remove( sourceName );
-        }
-	}
-
-    public void removeAllSourcesFromViewers()
-    {
-        final Set< String > visibleSourceNames = getVisibleSourceNames();
-
-        for ( String visibleSourceName : visibleSourceNames )
-        {
-            final SourceAndMetadata< ? > sam = getSourceAndCurrentMetadata( visibleSourceName );
-            removeSourceFromViewers( sam );
-        }
-    }
-
-    public BdvHandle getBdv()
-    {
-        return bdv;
-    }
-
-    public List< SourceAndMetadataChangedListener > listeners()
-    {
-        return listeners;
-    }
-}
\ No newline at end of file
diff --git a/src/main/java/de/embl/cba/mobie/utils/ui/SwingUtils.java b/src/main/java/de/embl/cba/mobie/ui/SwingHelper.java
similarity index 71%
rename from src/main/java/de/embl/cba/mobie/utils/ui/SwingUtils.java
rename to src/main/java/de/embl/cba/mobie/ui/SwingHelper.java
index 74e22394c..8475fa77d 100644
--- a/src/main/java/de/embl/cba/mobie/utils/ui/SwingUtils.java
+++ b/src/main/java/de/embl/cba/mobie/ui/SwingHelper.java
@@ -1,11 +1,11 @@
-package de.embl.cba.mobie.utils.ui;
+package de.embl.cba.mobie.ui;
 
-import de.embl.cba.mobie.ui.MoBIE;
+import de.embl.cba.mobie.MoBIE;
 
 import javax.swing.*;
 import java.awt.*;
 
-public class SwingUtils
+public class SwingHelper
 {
 	public static final int TEXT_FIELD_HEIGHT = 20;
 	public static final int COMBOBOX_WIDTH = 270;
@@ -22,15 +22,6 @@ public static JPanel horizontalLayoutPanel()
 		return panel;
 	}
 
-	public static void resetSwingLookAndFeel() {
-		try {
-			UIManager.setLookAndFeel(
-					UIManager.getCrossPlatformLookAndFeelClassName());
-		} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e) {
-			e.printStackTrace();
-		}
-	}
-
 	public static JLabel getJLabel( String text )
 	{
 		return getJLabel( text, 170, 10);
@@ -46,12 +37,12 @@ public static JLabel getJLabel( String text, int width, int height )
 		return comp;
 	}
 
-	public static JButton getButton( String buttonLabel )
+	public static JButton createButton( String buttonLabel )
 	{
-		return getButton( buttonLabel, BUTTON_DIMENSION );
+		return createButton( buttonLabel, BUTTON_DIMENSION );
 	}
 
-	public static JButton getButton( String buttonLabel, Dimension dimension )
+	public static JButton createButton( String buttonLabel, Dimension dimension )
 	{
 		final JButton button = new JButton( buttonLabel );
 		button.setPreferredSize( dimension );
diff --git a/src/main/java/de/embl/cba/mobie/ui/UserInterface.java b/src/main/java/de/embl/cba/mobie/ui/UserInterface.java
index 755e289e6..f2696c763 100644
--- a/src/main/java/de/embl/cba/mobie/ui/UserInterface.java
+++ b/src/main/java/de/embl/cba/mobie/ui/UserInterface.java
@@ -1,92 +1,51 @@
 package de.embl.cba.mobie.ui;
 
-import bdv.tools.brightness.ConverterSetup;
-import bdv.tools.brightness.SliderPanelDouble;
-import bdv.util.BdvHandle;
-import bdv.util.BoundedValueDouble;
-import bdv.viewer.Interpolation;
-import de.embl.cba.bdv.utils.BdvUtils;
-import de.embl.cba.bdv.utils.BrightnessUpdateListener;
-import de.embl.cba.mobie.image.SourceAndMetadataChangedListener;
-import de.embl.cba.tables.image.SourceAndMetadata;
-import ij.WindowManager;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.view.View;
+import de.embl.cba.mobie.display.ImageSourceDisplay;
+import de.embl.cba.mobie.display.SegmentationSourceDisplay;
+import de.embl.cba.mobie.display.SourceDisplay;
+import de.embl.cba.mobie.grid.GridOverlaySourceDisplay;
 
 import javax.swing.*;
 import java.awt.*;
 import java.util.HashMap;
-import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
-public class UserInterface implements SourceAndMetadataChangedListener
+import static de.embl.cba.mobie.ui.UserInterfaceHelper.resetSystemSwingLookAndFeel;
+import static de.embl.cba.mobie.ui.UserInterfaceHelper.setMoBIESwingLookAndFeel;
+
+public class UserInterface
 {
-	private final UserInterfaceComponentsProvider componentsProvider;
-	private final JPanel displaySettingsPanel;
-	private final SourcesDisplayManager displayManager;
-	private HashMap< Object, JPanel > sourceToPanel;
+	private final JPanel displaySettingsContainer;
 	private final JFrame frame;
-	private final JPanel actionPanel;
+	private final JPanel selectionContainer;
+	private final UserInterfaceHelper userInterfaceHelper;
+	private Map< Object, JPanel > displayToPanel;
+	private JSplitPane splitPane;
 
 	public UserInterface( MoBIE moBIE )
 	{
-		displayManager = moBIE.getSourcesDisplayManager();
-		displayManager.listeners().add( this );
-
-		componentsProvider = new UserInterfaceComponentsProvider( moBIE );
-
-		actionPanel = createActionsPanel( moBIE );
-		displaySettingsPanel = createDisplaySettingsPanel();
-
-		frame = createAndShowFrame( moBIE, actionPanel, displaySettingsPanel );
-		setImageJLogWindowPositionAndSize( frame );
-	}
-
-	public void dispose()
-	{
-		frame.dispose();
-	}
-
-	private JPanel createDisplaySettingsPanel()
-	{
-		final JPanel panel = new JPanel();
-		panel.setLayout( new BoxLayout(panel, BoxLayout.Y_AXIS ) );
-		panel.setAlignmentX( Component.LEFT_ALIGNMENT );
-
-		sourceToPanel = new HashMap<>();
-
-		return panel;
-	}
-
-	private JPanel createActionsPanel( MoBIE moBIE )
-	{
-		final JPanel actionPanel = new JPanel();
-		actionPanel.setLayout( new BoxLayout( actionPanel, BoxLayout.Y_AXIS ) );
-
-		actionPanel.add( componentsProvider.createInfoPanel( moBIE.getProjectLocation(), moBIE.getOptions().values.getPublicationURL() ) );
-		actionPanel.add( new JSeparator( SwingConstants.HORIZONTAL ) );
-
-		actionPanel.add( componentsProvider.createDatasetSelectionPanel() );
-		actionPanel.add( new JSeparator( SwingConstants.HORIZONTAL ) );
+		userInterfaceHelper = new UserInterfaceHelper( moBIE );
 
-		actionPanel.add( componentsProvider.createSourceSelectionPanel( moBIE.getSourcesDisplayManager() ) );
-		actionPanel.add( new JSeparator( SwingConstants.HORIZONTAL ) );
+		selectionContainer = userInterfaceHelper.createSelectionPanel();
+		displaySettingsContainer = userInterfaceHelper.createDisplaySettingsPanel();
+		displayToPanel = new HashMap<>();
 
-		actionPanel.add( componentsProvider.createBookmarksPanel( moBIE.getBookmarkManager() )  );
-		actionPanel.add( componentsProvider.createMoveToLocationPanel( )  );
-
-		if ( moBIE.getLevelingVector() != null )
-		{
-			actionPanel.add( componentsProvider.createLevelingPanel( moBIE.getLevelingVector() ) );
-		}
-		return actionPanel;
+		setMoBIESwingLookAndFeel();
+		frame = createAndShowFrame( selectionContainer, displaySettingsContainer, moBIE.getProjectName() + "-" + moBIE.getDatasetName() );
+		resetSystemSwingLookAndFeel();
 	}
 
-	private JFrame createAndShowFrame( MoBIE moBIE, JPanel actionPanel, JPanel displaySettingsPanel )
+	private JFrame createAndShowFrame( JPanel actionPanel, JPanel displaySettingsPanel, String panelName )
 	{
-		JFrame frame = new JFrame( "MoBIE: " + moBIE.getProjectName() + "-" + moBIE.getDataset() );
+		JFrame frame = new JFrame( "MoBIE: " + panelName );
 
-		JSplitPane splitPane = new JSplitPane();
+		splitPane = new JSplitPane();
 		splitPane.setOrientation( JSplitPane.VERTICAL_SPLIT );
-		final int sourceSelectionPanelHeight = componentsProvider.getSourceSelectionPanelHeight();
-		final int actionPanelHeight = sourceSelectionPanelHeight + 7 * 40;
+		final int actionPanelHeight = userInterfaceHelper.getActionPanelHeight();
+
 
 		splitPane.setDividerLocation( actionPanelHeight );
 		splitPane.setTopComponent( actionPanel );
@@ -94,7 +53,7 @@ private JFrame createAndShowFrame( MoBIE moBIE, JPanel actionPanel, JPanel displ
 		splitPane.setAutoscrolls( true );
 
 		// show frame
-		frame.setPreferredSize( new Dimension( 600, actionPanelHeight + 200 ) );
+		frame.setPreferredSize( new Dimension( 550, actionPanelHeight + 200 ) );
 		frame.getContentPane().setLayout( new GridLayout() );
 		frame.getContentPane().add( splitPane );
 
@@ -105,130 +64,97 @@ private JFrame createAndShowFrame( MoBIE moBIE, JPanel actionPanel, JPanel displ
 		return frame;
 	}
 
-	private void setImageJLogWindowPositionAndSize( JFrame parentComponent )
+	private void refreshDisplaySettings()
 	{
-		final Frame log = WindowManager.getFrame( "Log" );
-		if (log != null) {
-			Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
-			final int logWindowHeight = screenSize.height - ( parentComponent.getLocationOnScreen().y + parentComponent.getHeight() + 20 );
-			log.setSize( parentComponent.getWidth(), logWindowHeight  );
-			log.setLocation( parentComponent.getLocationOnScreen().x, parentComponent.getLocationOnScreen().y + parentComponent.getHeight() );
-		}
+		displaySettingsContainer.revalidate();
+		displaySettingsContainer.repaint();
+		frame.revalidate();
+		frame.repaint();
 	}
 
-	public void setBdvWindowPositionAndSize( BdvHandle bdvHandle )
+	private void refreshSelection()
 	{
-		BdvUtils.getViewerFrame( bdvHandle ).setLocation(
-				frame.getLocationOnScreen().x + frame.getWidth(),
-				frame.getLocationOnScreen().y );
-
-		BdvUtils.getViewerFrame( bdvHandle ).setSize( frame.getHeight(), frame.getHeight() );
+		selectionContainer.revalidate();
+		selectionContainer.repaint();
+		// update the location of the splitpane divider, so any new uiSelectionGroups are visible
+		final int actionPanelHeight = userInterfaceHelper.getActionPanelHeight();
+		splitPane.setDividerLocation( actionPanelHeight );
+		frame.revalidate();
+		frame.repaint();
+	}
 
-		bdvHandle.getViewerPanel().setInterpolation( Interpolation.NLINEAR );
+	public void addViews( Map<String, View> views ) {
+		userInterfaceHelper.addViewsToSelectionPanel(views);
+		refreshSelection();
 	}
 
-	private void refresh()
+	public void addSourceDisplay( SourceDisplay sourceDisplay )
 	{
-		displaySettingsPanel.revalidate();
-		displaySettingsPanel.repaint();
-		frame.revalidate();
-		frame.repaint();
+		final JPanel panel = createDisplaySettingPanel( sourceDisplay );
+		showDisplaySettingsPanel( sourceDisplay, panel );
+		refreshDisplaySettings();
 	}
 
-	@Override
-	public synchronized void addedToBDV( SourceAndMetadata< ? > sam )
+	private JPanel createDisplaySettingPanel( SourceDisplay sourceDisplay )
 	{
-		Object panelKey = getPanelKey( sam );
-
-		if ( sourceToPanel.containsKey( panelKey ) )
+		if ( sourceDisplay instanceof ImageSourceDisplay )
+		{
+			return userInterfaceHelper.createImageDisplaySettingsPanel( ( ImageSourceDisplay ) sourceDisplay );
+		}
+		else if ( sourceDisplay instanceof SegmentationSourceDisplay )
 		{
-			return;
+			return userInterfaceHelper.createSegmentationDisplaySettingsPanel( ( SegmentationSourceDisplay ) sourceDisplay );
 		}
 		else
 		{
-			final JPanel panel = componentsProvider.createDisplaySettingsPanel( sam, displayManager );
-			displaySettingsPanel.add( panel );
-			sourceToPanel.put( panelKey, panel );
-			refresh();
+			throw new UnsupportedOperationException();
 		}
 	}
 
-	protected Object getPanelKey( SourceAndMetadata< ? > sam )
+	public void addGridView( GridOverlaySourceDisplay gridOverlayDisplay )
 	{
-		Object panelKey;
-		if ( sam.metadata().groupId != null )
-			panelKey = sam.metadata().groupId;
-		else
-			panelKey = sam.metadata();
-		return panelKey;
+		final JPanel panel = userInterfaceHelper.createGridViewDisplaySettingsPanel( gridOverlayDisplay );
+		showDisplaySettingsPanel( gridOverlayDisplay, panel );
 	}
 
-	@Override
-	public void removedFromBDV( SourceAndMetadata< ? > sam )
+	public void removeDisplaySettingsPanel( Object display )
 	{
-		final JPanel panel = sourceToPanel.get( getPanelKey( sam ) );
-		displaySettingsPanel.remove( panel );
-		sourceToPanel.remove( sam );
-		refresh();
+		SwingUtilities.invokeLater( () -> {
+			final JPanel jPanel = displayToPanel.get( display );
+			displaySettingsContainer.remove( jPanel );
+			displayToPanel.remove( display );
+			refreshDisplaySettings();
+		} );
 	}
 
-	public static void showBrightnessDialog(
-			String name,
-			List< ConverterSetup > converterSetups,
-			double rangeMin,
-			double rangeMax )
+	protected void showDisplaySettingsPanel( Object display, JPanel panel )
 	{
-		JFrame frame = new JFrame( name );
-		frame.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
-
-		final double currentRangeMin = converterSetups.get( 0 ).getDisplayRangeMin();
-		final double currentRangeMax = converterSetups.get( 0 ).getDisplayRangeMax();
-
-		final BoundedValueDouble min =
-				new BoundedValueDouble(
-						rangeMin,
-						rangeMax,
-						currentRangeMin );
-
-		final BoundedValueDouble max =
-				new BoundedValueDouble(
-						rangeMin,
-						rangeMax,
-						currentRangeMax );
-
-		double spinnerStepSize = ( currentRangeMax - currentRangeMin ) / 100.0;
-
-		JPanel panel = new JPanel();
-		panel.setLayout( new BoxLayout( panel, BoxLayout.PAGE_AXIS ) );
-		final SliderPanelDouble minSlider =
-				new SliderPanelDouble( "Min", min, spinnerStepSize );
-		minSlider.setNumColummns( 7 );
-		minSlider.setDecimalFormat( "####E0" );
-
-		final SliderPanelDouble maxSlider =
-				new SliderPanelDouble( "Max", max, spinnerStepSize );
-		maxSlider.setNumColummns( 7 );
-		maxSlider.setDecimalFormat( "####E0" );
-
-		final BrightnessUpdateListener brightnessUpdateListener =
-				new BrightnessUpdateListener(
-						min, max, minSlider, maxSlider, converterSetups );
-
-		min.setUpdateListener( brightnessUpdateListener );
-		max.setUpdateListener( brightnessUpdateListener );
+		SwingUtilities.invokeLater( () -> {
+			displayToPanel.put( display, panel );
+			displaySettingsContainer.add( panel );
+			refreshDisplaySettings();
+		});
+	}
 
-		panel.add( minSlider );
-		panel.add( maxSlider );
+	public Window getWindow()
+	{
+		return frame;
+	}
 
-		frame.setContentPane( panel );
+	public String[] getUISelectionGroupNames() {
 
-		//Display the window.
-		frame.setBounds( MouseInfo.getPointerInfo().getLocation().x,
-				MouseInfo.getPointerInfo().getLocation().y,
-				120, 10);
-		frame.setResizable( false );
-		frame.pack();
-		frame.setVisible( true );
+		Set<String> groupings = userInterfaceHelper.getGroupings();
+		String[] groupNames = new String[groupings.size()];
+		int i = 0;
+		for ( String groupName: groupings ) {
+			groupNames[i] = groupName;
+			i++;
+		}
+		return groupNames;
+	}
 
+	public void close()
+	{
+		frame.dispose();
 	}
 }
diff --git a/src/main/java/de/embl/cba/mobie/ui/UserInterfaceComponentsProvider.java b/src/main/java/de/embl/cba/mobie/ui/UserInterfaceComponentsProvider.java
deleted file mode 100644
index 0365cbf0f..000000000
--- a/src/main/java/de/embl/cba/mobie/ui/UserInterfaceComponentsProvider.java
+++ /dev/null
@@ -1,524 +0,0 @@
-package de.embl.cba.mobie.ui;
-
-import bdv.tools.brightness.ConverterSetup;
-import de.embl.cba.bdv.utils.BdvUtils;
-import de.embl.cba.bdv.utils.sources.Metadata;
-import de.embl.cba.mobie.bdv.BdvViewChanger;
-import de.embl.cba.mobie.bookmark.BookmarkManager;
-import de.embl.cba.mobie.bookmark.Location;
-import de.embl.cba.mobie.image.SourceGroups;
-import de.embl.cba.mobie.utils.Utils;
-import de.embl.cba.tables.SwingUtils;
-import de.embl.cba.tables.color.ColorUtils;
-import de.embl.cba.tables.image.SourceAndMetadata;
-
-import javax.swing.*;
-import java.awt.*;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Set;
-
-import static de.embl.cba.mobie.utils.ui.SwingUtils.*;
-
-public class UserInterfaceComponentsProvider
-{
-	public static final String BUTTON_LABEL_VIEW = "view";
-	public static final String BUTTON_LABEL_MOVE = "move";
-	public static final String BUTTON_LABEL_HELP = "show";
-	public static final String BUTTON_LABEL_SWITCH = "switch";
-	public static final String BUTTON_LABEL_LEVEL = "level";
-	public static final String BUTTON_LABEL_ADD = "add";
-
-	private final SourcesDisplayManager displayManager;
-	private final MoBIE moBIE;
-	private final ArrayList< String > datasets;
-	private ArrayList< String > sortedModalities;
-	private int sourceSelectionPanelHeight;
-
-	public UserInterfaceComponentsProvider( MoBIE moBIE )
-	{
-		this.moBIE = moBIE;
-		this.displayManager = moBIE.getSourcesDisplayManager();
-		this.datasets = moBIE.getDatasets();
-	}
-
-	// TODO: too complex, make own source selection class
-	public JPanel createSourceSelectionPanel( SourcesDisplayManager sourcesDisplayManager )
-	{
-		HashMap< String, String > selectionNameAndModalityToSourceName = new HashMap<>();
-		HashMap< String, ArrayList< String > > modalityToSelectionNames = new HashMap<>();
-
-		for ( String sourceName : sourcesDisplayManager.getSourceNames() )
-		{
-			String modality = sourceName.split( "-" )[ 0 ];
-
-			String selectionName = sourceName.replace( modality + "-", "" );
-
-			final Metadata metadata = sourcesDisplayManager.getSourceAndDefaultMetadata( sourceName ).metadata();
-
-			if ( metadata.type.equals( Metadata.Type.Segmentation ) )
-			{
-				if ( ! modality.contains( " segmentation" ) )
-					modality += " segmentation";
-			}
-			else if ( metadata.type.equals( Metadata.Type.Mask ) )
-			{
-				if ( ! modality.contains( " segmentation" ) )
-					modality += " segmentation";
-			}
-
-			selectionName = Utils.getSimplifiedSourceName( selectionName, false );
-
-			selectionNameAndModalityToSourceName.put( selectionName + "-" + modality, sourceName  );
-
-			if ( ! modalityToSelectionNames.containsKey( modality ) )
-				modalityToSelectionNames.put( modality, new ArrayList<>(  ) );
-
-			modalityToSelectionNames.get( modality ).add( selectionName);
-		}
-
-		sortedModalities = Utils.getSortedList( modalityToSelectionNames.keySet() );
-
-		JPanel sourcesSelectionPanel = new JPanel( new BorderLayout() );
-		sourcesSelectionPanel.setLayout( new BoxLayout( sourcesSelectionPanel, BoxLayout.Y_AXIS ) );
-		for ( String modality : sortedModalities )
-		{
-			final String[] names = Utils.getSortedList( modalityToSelectionNames.get( modality ) ).toArray( new String[ 0 ] );
-			final JComboBox< String > comboBox = new JComboBox<>( names );
-			setComboBoxDimensions( comboBox );
-			final JPanel selectionComboBoxAndButtonPanel = createSourceSelectionComboBoxAndButtonPanel( selectionNameAndModalityToSourceName, comboBox, modality );
-			sourcesSelectionPanel.add( selectionComboBoxAndButtonPanel );
-		}
-
-		sourceSelectionPanelHeight = sortedModalities.size() * 40;
-
-		return sourcesSelectionPanel;
-	}
-
-	public int getSourceSelectionPanelHeight()
-	{
-		return sourceSelectionPanelHeight;
-	}
-
-	private JPanel createSourceSelectionComboBoxAndButtonPanel(
-			HashMap< String, String > selectionNameAndModalityToSourceName,
-			final JComboBox comboBox,
-			final String modality )
-	{
-		final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
-
-		if ( comboBox.getModel().getSize() == 0 ) return horizontalLayoutPanel;
-
-		final JButton button = getButton( BUTTON_LABEL_ADD );
-		button.addActionListener( e ->
-		{
-			SwingUtilities.invokeLater( () ->
-			{
-				final String selectedSource = ( String ) comboBox.getSelectedItem();
-				final String sourceName = selectionNameAndModalityToSourceName.get( selectedSource + "-" + modality );
-				displayManager.show( sourceName );
-			} );
-		} );
-
-		final JLabel comp = getJLabel( modality );
-
-		horizontalLayoutPanel.add( comp );
-		horizontalLayoutPanel.add( comboBox );
-		horizontalLayoutPanel.add( button );
-
-		return horizontalLayoutPanel;
-	}
-
-	public JPanel createLevelingPanel( double[] levelingVector )
-	{
-		final double[] targetNormalVector = Arrays.copyOf( levelingVector, 3 );
-
-		final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
-
-		final JButton button = getButton( BUTTON_LABEL_LEVEL );
-		horizontalLayoutPanel.add( button );
-
-		// TODO: if below code is needed make an own Levelling class
-//		final JButton changeReference = new JButton( "Set new level vector" );
-//		horizontalLayoutPanel.add( changeReference );
-
-//		final JButton defaultReference = new JButton( "Set default level vector" );
-//		horizontalLayoutPanel.add( defaultReference );
-
-//		changeReference.addActionListener( e -> {
-//			targetNormalVector = BdvUtils.getCurrentViewNormalVector( bdv );
-//			Utils.logVector( "New reference normal vector: ", targetNormalVector );
-//		} );
-
-//		defaultReference.addActionListener( e -> {
-//			targetNormalVector = Arrays.copyOf( levelingVector, 3);
-//			Utils.logVector( "New reference normal vector (default): ", levelingVector );
-//		} );
-
-		button.addActionListener( e -> BdvUtils.levelCurrentView( displayManager.getBdv(), targetNormalVector ) );
-
-		return horizontalLayoutPanel;
-	}
-
-	public JPanel createBookmarksPanel( final BookmarkManager bookmarkManager )
-	{
-		final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
-		final JButton button = getButton( BUTTON_LABEL_VIEW );
-		final Set< String > bookmarkNames = bookmarkManager.getBookmarkNames();
-		JComboBox comboBox = new JComboBox<>( bookmarkNames.toArray( new String[bookmarkNames.size()] ) );
-		setComboBoxDimensions( comboBox );
-		button.addActionListener( e -> bookmarkManager.setView( ( String ) comboBox.getSelectedItem() ) );
-		bookmarkManager.setBookmarkDropDown( comboBox );
-
-		horizontalLayoutPanel.add( getJLabel( "bookmark" ) );
-		horizontalLayoutPanel.add( comboBox );
-		horizontalLayoutPanel.add( button );
-
-		return horizontalLayoutPanel;
-	}
-
-	public JPanel createMoveToLocationPanel( )
-	{
-		final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
-
-		final JButton button = getButton( BUTTON_LABEL_MOVE );
-
-		final JTextField jTextField = new JTextField( "120.5,115.3,201.5" );
-		jTextField.setPreferredSize( new Dimension( COMBOBOX_WIDTH - 3, TEXT_FIELD_HEIGHT ) );
-		jTextField.setMaximumSize( new Dimension( COMBOBOX_WIDTH - 3, TEXT_FIELD_HEIGHT ) );
-		button.addActionListener( e -> BdvViewChanger.moveToLocation( displayManager.getBdv(), new Location( jTextField.getText() ) ) );
-
-		horizontalLayoutPanel.add( getJLabel( "location" ) );
-		horizontalLayoutPanel.add( jTextField );
-		horizontalLayoutPanel.add( button );
-
-		return horizontalLayoutPanel;
-	}
-
-	public JPanel createInfoPanel( String projectLocation, String publicationURL )
-	{
-		final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
-
-		final JButton button = getButton( BUTTON_LABEL_HELP );
-
-		final MoBIEInfo moBIEInfo = new MoBIEInfo( projectLocation, publicationURL );
-
-		final JComboBox< String > comboBox = new JComboBox<>( moBIEInfo.getInfoChoices() );
-		setComboBoxDimensions( comboBox );
-
-		button.addActionListener( e -> {
-			moBIEInfo.showInfo( ( String ) comboBox.getSelectedItem() );
-		} );
-		comboBox.setPrototypeDisplayValue( MoBIE.PROTOTYPE_DISPLAY_VALUE  );
-
-		horizontalLayoutPanel.setSize( 0, 80 );
-		final ImageIcon icon = createMobieIcon( 80 );
-		final JLabel moBIE = new JLabel( "                   " );
-		moBIE.setIcon( icon );
-
-		horizontalLayoutPanel.add( moBIE );
-		horizontalLayoutPanel.add( comboBox );
-		horizontalLayoutPanel.add( button );
-
-		return horizontalLayoutPanel;
-	}
-
-	public ImageIcon createMobieIcon( int size )
-	{
-		final URL resource = UserInterfaceComponentsProvider.class.getResource( "/mobie.jpeg" );
-		final ImageIcon imageIcon = new ImageIcon( resource );
-		final Image scaledInstance = imageIcon.getImage().getScaledInstance( size, size, Image.SCALE_SMOOTH );
-		return new ImageIcon( scaledInstance );
-	}
-
-	public JPanel createDatasetSelectionPanel( )
-	{
-		final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
-
-		final JButton button = getButton( BUTTON_LABEL_SWITCH );
-
-		final String[] choices = datasets.stream().toArray( String[]::new );
-		final JComboBox< String > comboBox = new JComboBox<>( choices );
-		comboBox.setSelectedItem( moBIE.getDataset() );
-		setComboBoxDimensions( comboBox );
-		button.addActionListener( e -> switchDataset( ( String ) comboBox.getSelectedItem() ) );
-		comboBox.setPrototypeDisplayValue( MoBIE.PROTOTYPE_DISPLAY_VALUE  );
-
-		horizontalLayoutPanel.add( getJLabel( "dataset" ) );
-		horizontalLayoutPanel.add( comboBox );
-		horizontalLayoutPanel.add( button );
-
-		return horizontalLayoutPanel;
-	}
-
-	// TODO: The project manager should do this!
-	private void switchDataset( String dataset )
-	{
-		// TODO: make sure the Swing UI sources panel is fully visible before instantiating the new BDV
-		moBIE.close();
-		new MoBIE( moBIE.getProjectLocation(), moBIE.getOptions().dataset( dataset ) );
-	}
-
-	// TODO: move this stuff to the UserInterfacePanelsProvider
-	public static JCheckBox createBigDataViewerVisibilityCheckbox(
-			int[] dims,
-			List< SourceAndMetadata< ? > > sourceAndMetadataList,
-			boolean isVisible )
-	{
-		JCheckBox checkBox = new JCheckBox( "S" );
-		checkBox.setSelected( isVisible );
-		checkBox.setPreferredSize( new Dimension( dims[ 0 ], dims[ 1 ] ) );
-
-		checkBox.addActionListener( new ActionListener()
-		{
-			@Override
-			public void actionPerformed( ActionEvent e )
-			{
-				final String groupId = sourceAndMetadataList.get( 0 ).metadata().groupId;
-
-				if ( groupId != null )
-				{
-					final List< SourceAndMetadata< ? > > sourceAndMetadataGroupList = SourceGroups.getSourceAndMetadataList( groupId );
-					for ( SourceAndMetadata< ? > sourceAndMetadata : sourceAndMetadataGroupList )
-					{
-						sourceAndMetadata.metadata().bdvStackSource.setActive( checkBox.isSelected() );
-					}
-
-					// TODO: below is not working...
-//					final SourceGroup sourceGroup = SourceGroupings.getSourceGroup( sourceAndMetadataList.get( 0 ).metadata().groupId );
-//
-//					final BdvHandle bdvHandle = sourceAndMetadataList.get( 0 ).metadata().bdvStackSource.getBdvHandle();
-//					bdvHandle.getViewerPanel().state().setGroupActive( sourceGroup, checkBox.isSelected() );
-				}
-				else
-				{
-					for ( SourceAndMetadata< ? > sourceAndMetadata : sourceAndMetadataList )
-					{
-						sourceAndMetadata.metadata().bdvStackSource.setActive( checkBox.isSelected() );
-					}
-				}
-			}
-		} );
-
-		return checkBox;
-	}
-
-	public static JCheckBox createVolumeViewVisibilityCheckbox(
-			SourcesDisplayManager sourcesDisplayManager,
-			int[] dims,
-			SourceAndMetadata< ? > sam,
-			boolean isVisible )
-	{
-		JCheckBox checkBox = new JCheckBox( "V" );
-		checkBox.setSelected( isVisible );
-		checkBox.setPreferredSize( new Dimension( dims[ 0 ], dims[ 1 ] ) );
-
-		checkBox.addActionListener( new ActionListener()
-		{
-			@Override
-			public void actionPerformed( ActionEvent e )
-			{
-				new Thread( () -> {
-					sam.metadata().showImageIn3d = checkBox.isSelected();
-					sam.metadata().showSelectedSegmentsIn3d = checkBox.isSelected();
-					sourcesDisplayManager.updateSegments3dView( sam, sourcesDisplayManager );
-					sourcesDisplayManager.updateSource3dView( sam, sourcesDisplayManager, false );
-				}).start();
-			}
-		} );
-
-		return checkBox;
-	}
-
-	public static JButton createBrightnessButton( String sourceName, int[] buttonDimensions, List< SourceAndMetadata< ? > > sourceAndMetadataList, final double rangeMin, final double rangeMax )
-	{
-		JButton button = new JButton( "B" );
-		button.setPreferredSize( new Dimension(
-				buttonDimensions[ 0 ],
-				buttonDimensions[ 1 ] ) );
-
-		button.addActionListener( e ->
-		{
-			final ArrayList< ConverterSetup > converterSetups = new ArrayList<>();
-
-			final String groupId = sourceAndMetadataList.get( 0 ).metadata().groupId;
-
-			if ( groupId != null )
-			{
-				final List< SourceAndMetadata< ? > > sourceAndMetadataGroupList = SourceGroups.getSourceAndMetadataList( groupId );
-				for ( SourceAndMetadata< ? > sourceAndMetadata : sourceAndMetadataGroupList )
-				{
-					final List< ConverterSetup > setups = sourceAndMetadata.metadata().bdvStackSource.getConverterSetups();
-					for ( ConverterSetup setup : setups )
-					{
-						converterSetups.add( setup );
-					}
-				}
-			}
-			else
-			{
-				for ( SourceAndMetadata< ? > sourceAndMetadata : sourceAndMetadataList )
-				{
-					final List< ConverterSetup > setups = sourceAndMetadata.metadata().bdvStackSource.getConverterSetups();
-					for ( ConverterSetup setup : setups )
-					{
-						converterSetups.add( setup );
-					}
-				}
-			}
-
-			UserInterface.showBrightnessDialog(
-					sourceName,
-					converterSetups,
-					rangeMin,
-					rangeMax );
-		} );
-
-		return button;
-	}
-
-	public JPanel createDisplaySettingsPanel( SourceAndMetadata< ? > sam, SourcesDisplayManager displayManager )
-	{
-		return createDisplaySettingsPanel( Arrays.asList( sam ), displayManager );
-	}
-
-	public JPanel createDisplaySettingsPanel( List< SourceAndMetadata< ? > > sourceAndMetadataList, SourcesDisplayManager displayManager )
-	{
-		final Metadata metadataFirstSource = sourceAndMetadataList.get( 0 ).metadata();
-		final String panelName = metadataFirstSource.groupId != null ? metadataFirstSource.groupId : metadataFirstSource.displayName;
-
-		JPanel panel = new JPanel();
-
-		panel.setLayout( new BoxLayout(panel, BoxLayout.LINE_AXIS) );
-		panel.setBorder( BorderFactory.createEmptyBorder( 0, 10, 0, 10 ) );
-		panel.add( Box.createHorizontalGlue() );
-		setPanelColor( metadataFirstSource, panel );
-
-		JLabel sourceNameLabel = new JLabel( panelName );
-		sourceNameLabel.setHorizontalAlignment( SwingUtilities.CENTER );
-
-		int[] buttonDimensions = new int[]{ 50, 30 };
-		int[] viewSelectionDimensions = new int[]{ 50, 30 };
-
-		panel.add( sourceNameLabel );
-
-		JButton colorButton = createColorButton( displayManager, panel, buttonDimensions, sourceAndMetadataList );
-
-		final JButton brightnessButton = createBrightnessButton( panelName, buttonDimensions, sourceAndMetadataList, 0.0, 65535.0);
-
-		final JButton removeButton = createRemoveButton( displayManager, sourceAndMetadataList, buttonDimensions );
-
-		final JCheckBox bigDataViewerVisibilityCheckbox = createBigDataViewerVisibilityCheckbox( viewSelectionDimensions, sourceAndMetadataList, true );
-
-		// TODO: Can we adapt this for source groups?
-		final JCheckBox volumeVisibilityCheckbox =
-			    createVolumeViewVisibilityCheckbox(
-						displayManager,
-						viewSelectionDimensions,
-						sourceAndMetadataList.get( 0 ),
-						sourceAndMetadataList.get( 0 ).metadata().showImageIn3d || sourceAndMetadataList.get( 0 ).metadata().showSelectedSegmentsIn3d );
-
-		panel.add( colorButton );
-		panel.add( brightnessButton );
-		panel.add( removeButton );
-		panel.add( volumeVisibilityCheckbox );
-		panel.add( bigDataViewerVisibilityCheckbox );
-
-		for ( SourceAndMetadata< ? > sam : sourceAndMetadataList )
-		{
-			sam.metadata().bdvStackSource.getConverterSetups().get( 0 ).setupChangeListeners().add( setup -> {
-				// color changed listener
-				sam.metadata().color = setup.getColor().toString();
-				final Color color = ColorUtils.getColor( sam.metadata().color );
-				if ( color != null )
-				{
-					panel.setOpaque( true );
-					panel.setBackground( color );
-				}
-			} );
-		}
-		return panel;
-	}
-
-	private JButton createColorButton( SourcesDisplayManager displayManager, JPanel parentPanel, int[] buttonDimensions, List< SourceAndMetadata< ? > > sams )
-	{
-		JButton colorButton = new JButton( "C" );
-
-		colorButton.setPreferredSize( new Dimension( buttonDimensions[ 0 ], buttonDimensions[ 1 ] ) );
-
-		colorButton.addActionListener( e -> {
-			Color color = JColorChooser.showDialog( null, "", null );
-
-			if ( color == null ) return;
-
-			final String groupId = sams.get( 0 ).metadata().groupId;
-
-			if ( groupId != null )
-			{
-				final List< SourceAndMetadata< ? > > sourceAndMetadataList = SourceGroups.getSourceAndMetadataList( groupId );
-				for ( SourceAndMetadata< ? > sourceAndMetadata : sourceAndMetadataList )
-				{
-					displayManager.setSourceColor( sourceAndMetadata, color );
-				}
-			}
-			else
-			{
-				for ( SourceAndMetadata< ? > sam : sams )
-				{
-					displayManager.setSourceColor( sam, color );
-				}
-			}
-
-			parentPanel.setBackground( color );
-		} );
-
-		return colorButton;
-	}
-
-	private void setPanelColor( Metadata metadata, JPanel panel )
-	{
-		final Color color = ColorUtils.getColor( metadata.color );
-		if ( color != null )
-		{
-			panel.setOpaque( true );
-			panel.setBackground( color );
-		}
-	}
-
-	private JButton createRemoveButton(
-			SourcesDisplayManager displayManager,
-			List< SourceAndMetadata< ? > > sourceAndMetadataList,
-			int[] buttonDimensions )
-	{
-		JButton removeButton = new JButton( "X" );
-		removeButton.setPreferredSize( new Dimension( buttonDimensions[ 0 ], buttonDimensions[ 1 ] ) );
-
-		removeButton.addActionListener( e ->
-		{
-			final String groupId = sourceAndMetadataList.get( 0 ).metadata().groupId;
-
-			if ( groupId != null )
-			{
-				final List< SourceAndMetadata< ? > > sourceAndMetadataGroupList = SourceGroups.getSourceAndMetadataList( groupId );
-				for ( SourceAndMetadata< ? > sourceAndMetadata : sourceAndMetadataGroupList )
-				{
-					displayManager.removeSourceFromViewers( sourceAndMetadata );
-				}
-			}
-			else
-			{
-				for ( SourceAndMetadata< ? > sourceAndMetadata : sourceAndMetadataList )
-				{
-					displayManager.removeSourceFromViewers( sourceAndMetadata );
-				}
-			}
-		} );
-
-		return removeButton;
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/ui/UserInterfaceHelper.java b/src/main/java/de/embl/cba/mobie/ui/UserInterfaceHelper.java
new file mode 100644
index 000000000..65942528e
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/ui/UserInterfaceHelper.java
@@ -0,0 +1,837 @@
+package de.embl.cba.mobie.ui;
+
+import bdv.tools.brightness.ConverterSetup;
+import bdv.tools.brightness.SliderPanelDouble;
+import bdv.util.BdvHandle;
+import bdv.util.BoundedValueDouble;
+import bdv.viewer.SourceAndConverter;
+import com.formdev.flatlaf.FlatLightLaf;
+import de.embl.cba.bdv.utils.BdvUtils;
+import de.embl.cba.bdv.utils.BrightnessUpdateListener;
+import de.embl.cba.mobie.Utils;
+import de.embl.cba.mobie.transform.BdvLocationChanger;
+import de.embl.cba.mobie.transform.BdvLocation;
+import de.embl.cba.mobie.MoBIEInfo;
+import de.embl.cba.mobie.*;
+import de.embl.cba.mobie.color.OpacityAdjuster;
+import de.embl.cba.mobie.display.ImageSourceDisplay;
+import de.embl.cba.mobie.display.SegmentationSourceDisplay;
+import de.embl.cba.mobie.display.SourceDisplay;
+import de.embl.cba.mobie.grid.GridOverlaySourceDisplay;
+import de.embl.cba.mobie.view.View;
+import de.embl.cba.tables.SwingUtils;
+import de.embl.cba.tables.color.ColorUtils;
+import net.imglib2.converter.Converter;
+import net.imglib2.display.ColorConverter;
+import net.imglib2.realtransform.AffineTransform3D;
+import net.imglib2.type.numeric.ARGBType;
+import sc.fiji.bdvpg.bdv.navigate.ViewerTransformAdjuster;
+import sc.fiji.bdvpg.bdv.navigate.ViewerTransformChanger;
+import sc.fiji.bdvpg.services.SourceAndConverterServices;
+import sc.fiji.bdvpg.sourceandconverter.display.ColorChanger;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.net.URL;
+import java.util.*;
+import java.util.List;
+
+import static de.embl.cba.mobie.ui.SwingHelper.*;
+
+public class UserInterfaceHelper
+{
+	private static final Dimension PREFERRED_BUTTON_SIZE = new Dimension( 30, 30 );
+	private static final Dimension PREFERRED_CHECKBOX_SIZE = new Dimension( 40, 30 );
+	private static final Dimension PREFERRED_SPACE_SIZE = new Dimension( 10, 30 );
+	private static final String VIEW = "view";
+	private static final String MOVE = "move";
+	private static final String HELP = "show";
+	private static final String SWITCH = "switch";
+	private static final String LEVEL = "level";
+	private static final String ADD = "view";
+	public static final int SPACING = 20;
+
+	private final MoBIE moBIE2;
+	private int viewsSelectionPanelHeight;
+	private JPanel viewSelectionPanel;
+	private Map< String, Map< String, View > > groupingsToViews;
+	private Map< String, JComboBox > groupingsToComboBox;
+
+	public UserInterfaceHelper( MoBIE moBIE2 )
+	{
+		this.moBIE2 = moBIE2;
+	}
+
+	public static JPanel createDisplaySettingsPanel()
+	{
+		final JPanel panel = new JPanel();
+		panel.setLayout( new BoxLayout(panel, BoxLayout.Y_AXIS ) );
+		panel.setAlignmentX( Component.LEFT_ALIGNMENT );
+		return panel;
+	}
+
+	public static void showBrightnessDialog(
+			String name,
+			List< ConverterSetup > converterSetups,
+			double rangeMin,
+			double rangeMax )
+	{
+		JFrame frame = new JFrame( name );
+		frame.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
+
+		final double currentRangeMin = converterSetups.get( 0 ).getDisplayRangeMin();
+		final double currentRangeMax = converterSetups.get( 0 ).getDisplayRangeMax();
+
+		final BoundedValueDouble min =
+				new BoundedValueDouble(
+						rangeMin,
+						rangeMax,
+						currentRangeMin );
+
+		final BoundedValueDouble max =
+				new BoundedValueDouble(
+						rangeMin,
+						rangeMax,
+						currentRangeMax );
+
+		double spinnerStepSize = Math.abs( currentRangeMax - currentRangeMin ) / 100.0;
+
+		JPanel panel = new JPanel();
+		panel.setLayout( new BoxLayout( panel, BoxLayout.PAGE_AXIS ) );
+		final SliderPanelDouble minSlider =
+				new SliderPanelDouble( "Min", min, spinnerStepSize );
+		minSlider.setNumColummns( 7 );
+		minSlider.setDecimalFormat( "####E0" );
+
+		final SliderPanelDouble maxSlider =
+				new SliderPanelDouble( "Max", max, spinnerStepSize );
+		maxSlider.setNumColummns( 7 );
+		maxSlider.setDecimalFormat( "####E0" );
+
+		final BrightnessUpdateListener brightnessUpdateListener = new BrightnessUpdateListener( min, max, minSlider, maxSlider, converterSetups );
+
+		min.setUpdateListener( brightnessUpdateListener );
+		max.setUpdateListener( brightnessUpdateListener );
+
+		panel.add( minSlider );
+		panel.add( maxSlider );
+
+		frame.setContentPane( panel );
+
+		//Display the window.
+		frame.setBounds( MouseInfo.getPointerInfo().getLocation().x,
+				MouseInfo.getPointerInfo().getLocation().y,
+				120, 10);
+		frame.setResizable( false );
+		frame.pack();
+		frame.setVisible( true );
+	}
+
+	public static void showOpacityDialog(
+			String name,
+			List< SourceAndConverter< ? > > sourceAndConverters,
+			BdvHandle bdvHandle )
+	{
+		JFrame frame = new JFrame( name );
+		frame.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
+
+		// TODO: This cast requires that the sourceAndConverter implements
+		//   an OpacityAdjuster; how to do this more cleanly?
+		final double current = ( (OpacityAdjuster) sourceAndConverters.get( 0 ).getConverter()).getOpacity();
+
+		final BoundedValueDouble selection =
+				new BoundedValueDouble(
+						0.0,
+						1.0,
+						current );
+
+		double spinnerStepSize = 0.05;
+
+		JPanel panel = new JPanel();
+		panel.setLayout( new BoxLayout( panel, BoxLayout.PAGE_AXIS ) );
+		final SliderPanelDouble slider = new SliderPanelDouble( "Opacity", selection, spinnerStepSize );
+		slider.setNumColummns( 3 );
+		slider.setDecimalFormat( "#.##" );
+
+		final OpacityUpdateListener opacityUpdateListener =
+				new OpacityUpdateListener( selection, slider, sourceAndConverters, bdvHandle );
+
+		selection.setUpdateListener( opacityUpdateListener );
+		panel.add( slider );
+
+		frame.setContentPane( panel );
+
+		//Display the window.
+		frame.setBounds( MouseInfo.getPointerInfo().getLocation().x,
+				MouseInfo.getPointerInfo().getLocation().y,
+				120, 10);
+		frame.setResizable( false );
+		frame.pack();
+		frame.setVisible( true );
+
+	}
+
+	public static void setMoBIESwingLookAndFeel() {
+		FlatLightLaf.install();
+		System.setProperty("apple.laf.useScreenMenuBar", "false");
+		try {
+			UIManager.setLookAndFeel( new FlatLightLaf() );
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+	}
+
+	public static void resetSystemSwingLookAndFeel() {
+		// TODO: reset where the menu bar is?
+		try {
+			UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+	}
+
+	public static void resetCrossPlatformSwingLookAndFeel() {
+		// TODO: reset where the menu bar is?
+		try {
+			UIManager.setLookAndFeel( UIManager.getCrossPlatformLookAndFeelClassName() );
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+	}
+
+	public JPanel createGridViewDisplaySettingsPanel( GridOverlaySourceDisplay gridOverlayDisplay )
+	{
+		JPanel panel = createDisplayPanel( gridOverlayDisplay.getName() );
+
+		// Buttons
+		panel.add( createSpace() );
+		panel.add( createButtonPlaceholder() );
+		panel.add( createOpacityButton( gridOverlayDisplay.getSourceAndConverters(), gridOverlayDisplay.getName(), gridOverlayDisplay.getBdvHandle() ) );
+		panel.add( createButtonPlaceholder() );
+		panel.add( createButtonPlaceholder() );
+		panel.add( createRemoveButton( gridOverlayDisplay ) );
+		// Checkboxes
+		panel.add( createSpace() );
+		panel.add( createSliceViewerVisibilityCheckbox( true,  gridOverlayDisplay.getSourceAndConverters() ) );
+		panel.add( createCheckboxPlaceholder() );
+		panel.add( createWindowVisibilityCheckbox( true, gridOverlayDisplay.getTableViewer().getWindow() ) );
+		panel.add( createCheckboxPlaceholder() ); //panel.add( createScatterPlotViewerVisibilityCheckbox( display, true ) );
+		return panel;
+	}
+
+
+	public static class OpacityUpdateListener implements BoundedValueDouble.UpdateListener
+	{
+		final private List< SourceAndConverter< ? > > sourceAndConverters;
+		private final BdvHandle bdvHandle;
+		final private BoundedValueDouble value;
+		private final SliderPanelDouble slider;
+
+		public OpacityUpdateListener( BoundedValueDouble value,
+									  SliderPanelDouble slider,
+									  List< SourceAndConverter< ? > > sourceAndConverters, BdvHandle bdvHandle )
+		{
+			this.value = value;
+			this.slider = slider;
+			this.sourceAndConverters = sourceAndConverters;
+			this.bdvHandle = bdvHandle;
+		}
+
+		@Override
+		public void update()
+		{
+			slider.update();
+
+			for ( SourceAndConverter< ? > sourceAndConverter : sourceAndConverters )
+			{
+				final double currentValue = value.getCurrentValue();
+
+				( ( OpacityAdjuster ) sourceAndConverter.getConverter() ).setOpacity( currentValue );
+				if ( sourceAndConverter.asVolatile() != null )
+					( ( OpacityAdjuster ) sourceAndConverter.asVolatile().getConverter() ).setOpacity( currentValue );
+			}
+
+			bdvHandle.getViewerPanel().requestRepaint();
+		}
+	}
+
+	public JPanel createSelectionPanel()
+	{
+		final JPanel panel = new JPanel();
+		panel.setLayout( new BoxLayout( panel, BoxLayout.Y_AXIS ) );
+
+		panel.add( createInfoPanel( moBIE2.getSettings().values.getProjectLocation(), moBIE2.getSettings().values.getPublicationURL() ) );
+		panel.add( new JSeparator( SwingConstants.HORIZONTAL ) );
+		panel.add( createDatasetSelectionPanel() );
+		panel.add( new JSeparator( SwingConstants.HORIZONTAL ) );
+		panel.add( createViewsSelectionPanel() );
+		panel.add( new JSeparator( SwingConstants.HORIZONTAL ) );
+		panel.add( createMoveToLocationPanel()  );
+
+		return panel;
+	}
+
+	public JPanel createImageDisplaySettingsPanel( ImageSourceDisplay display )
+	{
+		JPanel panel = createDisplayPanel( display.getName() );
+
+		// Set panel background color
+		final Converter< ?, ARGBType > converter = display.sourceAndConverters.get( 0 ).getConverter();
+		if ( converter instanceof ColorConverter )
+		{
+			setPanelColor( panel, ( ( ColorConverter ) converter ).getColor() );
+		}
+
+		// Buttons
+		panel.add( createSpace() );
+		panel.add( createFocusButton( display, display.sourceAndConverters, display.sliceViewer.getBdvHandle() ) );
+		panel.add( createOpacityButton( display.sourceAndConverters, display.getName(), display.sliceViewer.getBdvHandle() ) );
+		panel.add( createColorButton( panel, display.sourceAndConverters ) );
+		panel.add( createImageDisplayBrightnessButton( display ) );
+		panel.add( createRemoveButton( display ) );
+		// Checkboxes
+		panel.add( createSpace() );
+		panel.add( createSliceViewerVisibilityCheckbox( true, display.sourceAndConverters ) );
+		panel.add( createCheckboxPlaceholder() ); // TODO: createVolume...
+		panel.add( createCheckboxPlaceholder() );
+		panel.add( createCheckboxPlaceholder() );
+
+
+		// make the panel color listen to color changes of the sources
+		for ( SourceAndConverter< ? > sourceAndConverter : display.sourceAndConverters )
+		{
+			SourceAndConverterServices.getSourceAndConverterDisplayService().getConverterSetup( sourceAndConverter ).setupChangeListeners().add( setup -> {
+				// color changed listener
+				setPanelColor( panel, setup.getColor() );
+			} );
+		}
+
+		return panel;
+	}
+
+	private JPanel createDisplayPanel( String name )
+	{
+		JPanel panel = new JPanel();
+		panel.setLayout( new BoxLayout( panel, BoxLayout.LINE_AXIS ) );
+		panel.setBorder( BorderFactory.createEmptyBorder( 0, 10, 0, 10 ) );
+		panel.add( Box.createHorizontalGlue() );
+		JLabel label = new JLabel(name );
+		label.setHorizontalAlignment( SwingUtilities.LEFT );
+		panel.add( label );
+
+		return panel;
+	}
+
+	public JPanel createSegmentationDisplaySettingsPanel( SegmentationSourceDisplay display )
+	{
+		JPanel panel = createDisplayPanel( display.getName() );
+
+		panel.add( createSpace() );
+		panel.add( createFocusButton( display, display.sourceAndConverters, display.sliceViewer.getBdvHandle() ) );
+		panel.add( createOpacityButton( display.sourceAndConverters, display.getName(), display.sliceViewer.getBdvHandle() ) );
+		panel.add( createButtonPlaceholder() );
+		panel.add( createButtonPlaceholder() );
+		panel.add( createRemoveButton( display ) );
+		panel.add( createSpace() );
+		panel.add( createSliceViewerVisibilityCheckbox( true, display.sourceAndConverters ) );
+		panel.add( createVolumeViewerVisibilityCheckbox( display ) );
+		panel.add( createWindowVisibilityCheckbox( true, display.tableViewer.getWindow() ) );
+		panel.add( createScatterPlotViewerVisibilityCheckbox( display,  display.showScatterPlot() ) );
+
+		return panel;
+	}
+
+	public JPanel createViewsSelectionPanel( )
+	{
+		final Map< String, View > views = moBIE2.getViews();
+
+		groupingsToViews = new HashMap<>(  );
+		groupingsToComboBox = new HashMap<>( );
+		viewSelectionPanel = new JPanel( new BorderLayout() );
+		viewSelectionPanel.setLayout( new BoxLayout( viewSelectionPanel, BoxLayout.Y_AXIS ) );
+
+		addViewsToSelectionPanel( views );
+
+		return viewSelectionPanel;
+	}
+
+	public void addViewsToSelectionPanel( Map< String, View > views ) {
+		for ( String viewName : views.keySet() )
+		{
+			final View view = views.get( viewName );
+			final String uiSelectionGroup = view.getUiSelectionGroup();
+			if ( ! groupingsToViews.containsKey( uiSelectionGroup ) )
+				groupingsToViews.put( uiSelectionGroup, new LinkedHashMap<>( ));
+			groupingsToViews.get( uiSelectionGroup ).put( viewName, view );
+		}
+
+		final ArrayList< String > uiSelectionGroups = new ArrayList<>( groupingsToViews.keySet() );
+		// sort in alphabetical order, ignoring upper/lower case
+		Collections.sort( uiSelectionGroups, new Comparator<String>() {
+			@Override
+			public int compare(String s1, String s2) {
+				return s1.compareToIgnoreCase(s2);
+			}
+		});
+
+		// If it's the first time, just add all the panels in order
+		if ( groupingsToComboBox.keySet().size() == 0 ) {
+			for (String uiSelectionGroup : uiSelectionGroups) {
+				final JPanel selectionPanel = createViewSelectionPanel(moBIE2, uiSelectionGroup, groupingsToViews.get(uiSelectionGroup));
+				viewSelectionPanel.add(selectionPanel);
+			}
+		} else {
+			// If there are already panels, then add new ones at the correct index to maintain alphabetical order
+			Map< Integer, JPanel > indexToPanel = new HashMap<>();
+			for ( String viewName : views.keySet() ) {
+				String uiSelectionGroup = views.get( viewName ).getUiSelectionGroup();
+				if ( groupingsToComboBox.containsKey( uiSelectionGroup ) ) {
+					groupingsToComboBox.get( uiSelectionGroup ).addItem( viewName );
+				} else {
+					final JPanel selectionPanel = createViewSelectionPanel(moBIE2, uiSelectionGroup, groupingsToViews.get(uiSelectionGroup));
+					int alphabeticalIndex = uiSelectionGroups.indexOf( uiSelectionGroup );
+					indexToPanel.put( alphabeticalIndex, selectionPanel );
+				}
+			}
+
+			if ( indexToPanel.keySet().size() > 0 ) {
+				// add panels in ascending index order
+				final ArrayList< Integer > sortedIndices = new ArrayList<>( indexToPanel.keySet() );
+				Collections.sort( sortedIndices );
+				for ( Integer index: sortedIndices ) {
+					viewSelectionPanel.add( indexToPanel.get(index), index.intValue() );
+				}
+			}
+		}
+
+		viewsSelectionPanelHeight = groupingsToViews.keySet().size() * 40;
+	}
+
+	public int getViewsSelectionPanelHeight()
+	{
+		return viewsSelectionPanelHeight;
+	}
+
+	public int getActionPanelHeight()
+	{
+		return viewsSelectionPanelHeight + 4 * 40;
+	}
+
+	public Set<String> getGroupings() {
+		return groupingsToViews.keySet();
+	}
+
+	private JPanel createViewSelectionPanel(MoBIE moBIE2, String panelName, Map< String, View > views )
+	{
+		final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
+
+		final JComboBox< String > comboBox = new JComboBox<>( views.keySet().toArray( new String[ 0 ] ) );
+
+		final JButton button = createButton( ADD );
+		button.addActionListener( e ->
+		{
+			SwingUtilities.invokeLater( () ->
+			{
+				new Thread( () ->
+				{
+					final String viewName = ( String ) comboBox.getSelectedItem();
+					final View view = views.get( viewName );
+					moBIE2.getViewerManager().show( view );
+				}).start();
+			} );
+		} );
+
+		setComboBoxDimensions( comboBox );
+
+		horizontalLayoutPanel.add( getJLabel( panelName ) );
+		horizontalLayoutPanel.add( comboBox );
+		horizontalLayoutPanel.add( button );
+
+		groupingsToComboBox.put( panelName, comboBox );
+
+		return horizontalLayoutPanel;
+	}
+
+	public JPanel createLevelingPanel( double[] levelingVector )
+	{
+		final double[] targetNormalVector = Arrays.copyOf( levelingVector, 3 );
+
+		final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
+
+		final JButton button = createButton( LEVEL );
+		horizontalLayoutPanel.add( button );
+
+		// TODO: if below code is needed make an own Levelling class
+//		final JButton changeReference = new JButton( "Set new level vector" );
+//		horizontalLayoutPanel.add( changeReference );
+
+//		final JButton defaultReference = new JButton( "Set default level vector" );
+//		horizontalLayoutPanel.add( defaultReference );
+
+//		changeReference.addActionListener( e -> {
+//			targetNormalVector = BdvUtils.getCurrentViewNormalVector( bdv );
+//			Utils.logVector( "New reference normal vector: ", targetNormalVector );
+//		} );
+
+//		defaultReference.addActionListener( e -> {
+//			targetNormalVector = Arrays.copyOf( levelingVector, 3);
+//			Utils.logVector( "New reference normal vector (default): ", levelingVector );
+//		} );
+
+		button.addActionListener( e -> BdvUtils.levelCurrentView( moBIE2.getViewerManager().getSliceViewer().getBdvHandle(), targetNormalVector ) );
+
+		return horizontalLayoutPanel;
+	}
+
+	public JPanel createMoveToLocationPanel( )
+	{
+		final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
+		final JButton button = createButton( MOVE );
+
+		final JTextField jTextField = new JTextField( "120.5,115.3,201.5" );
+		jTextField.setPreferredSize( new Dimension( COMBOBOX_WIDTH - 3, TEXT_FIELD_HEIGHT ) );
+		jTextField.setMaximumSize( new Dimension( COMBOBOX_WIDTH - 3, TEXT_FIELD_HEIGHT ) );
+		button.addActionListener( e -> BdvLocationChanger.moveToLocation( moBIE2.getViewerManager().getSliceViewer().getBdvHandle(), new BdvLocation( jTextField.getText() ) ) );
+
+		horizontalLayoutPanel.add( getJLabel( "location" ) );
+		horizontalLayoutPanel.add( jTextField );
+		horizontalLayoutPanel.add( button );
+
+		return horizontalLayoutPanel;
+	}
+
+	public JPanel createInfoPanel( String projectLocation, String publicationURL )
+	{
+		final JPanel horizontalLayoutPanel = SwingUtils.horizontalLayoutPanel();
+
+		final JButton button = createButton( HELP );
+
+		final MoBIEInfo moBIEInfo = new MoBIEInfo( projectLocation, publicationURL );
+
+		final JComboBox< String > comboBox = new JComboBox<>( moBIEInfo.getInfoChoices() );
+		setComboBoxDimensions( comboBox );
+
+		button.addActionListener( e -> {
+			moBIEInfo.showInfo( ( String ) comboBox.getSelectedItem() );
+		} );
+		comboBox.setPrototypeDisplayValue( MoBIE.PROTOTYPE_DISPLAY_VALUE  );
+
+		horizontalLayoutPanel.setSize( 0, 80 );
+		final ImageIcon icon = createMobieIcon( 80 );
+		final JLabel moBIE = new JLabel( "                   " );
+		moBIE.setIcon( icon );
+
+		horizontalLayoutPanel.add( moBIE );
+		horizontalLayoutPanel.add( comboBox );
+		horizontalLayoutPanel.add( button );
+
+		return horizontalLayoutPanel;
+	}
+
+	public ImageIcon createMobieIcon( int size )
+	{
+		final URL resource = UserInterfaceHelper.class.getResource( "/mobie.jpeg" );
+		final ImageIcon imageIcon = new ImageIcon( resource );
+		final Image scaledInstance = imageIcon.getImage().getScaledInstance( size, size, Image.SCALE_SMOOTH );
+		return new ImageIcon( scaledInstance );
+	}
+
+	public JPanel createDatasetSelectionPanel( )
+	{
+		final JPanel panel = SwingUtils.horizontalLayoutPanel();
+
+		final JComboBox< String > comboBox = new JComboBox<>( moBIE2.getDatasets().toArray( new String[ 0 ] ) );
+
+		final JButton button = createButton( ADD );
+		button.addActionListener( e ->
+		{
+			SwingUtilities.invokeLater( () ->
+			{
+				final String dataset = ( String ) comboBox.getSelectedItem();
+				moBIE2.setDataset( dataset );
+			} );
+		} );
+
+		comboBox.setSelectedItem( moBIE2.getDatasetName() );
+		setComboBoxDimensions( comboBox );
+
+		panel.add( getJLabel( "dataset" ) );
+		panel.add( comboBox );
+		panel.add( button );
+
+		return panel;
+	}
+
+	private static Component createSpace()
+	{
+		return Box.createRigidArea( PREFERRED_SPACE_SIZE );
+	}
+	private static Component createButtonPlaceholder()
+	{
+		return Box.createRigidArea( PREFERRED_BUTTON_SIZE );
+	}
+
+	private static Component createCheckboxPlaceholder()
+	{
+		return Box.createRigidArea( PREFERRED_CHECKBOX_SIZE );
+	}
+
+	private static JCheckBox createSliceViewerVisibilityCheckbox(
+			boolean isVisible,
+			final List< SourceAndConverter< ? > > sourceAndConverters )
+	{
+		JCheckBox checkBox = new JCheckBox( "S" );
+		checkBox.setSelected( isVisible );
+		checkBox.setPreferredSize( PREFERRED_CHECKBOX_SIZE );
+
+		checkBox.addActionListener( new ActionListener()
+		{
+			@Override
+			public void actionPerformed( ActionEvent e )
+			{
+				for ( SourceAndConverter< ? > sourceAndConverter : sourceAndConverters )
+				{
+					SourceAndConverterServices.getSourceAndConverterDisplayService().setVisible( sourceAndConverter, checkBox.isSelected() );
+				}
+			}
+		} );
+
+		return checkBox;
+	}
+
+	private static JCheckBox createWindowVisibilityCheckbox(
+			boolean isVisible,
+			Window window )
+	{
+		JCheckBox checkBox = new JCheckBox( "T" );
+		checkBox.setSelected( isVisible );
+		checkBox.setPreferredSize( PREFERRED_CHECKBOX_SIZE );
+		checkBox.addActionListener( e -> SwingUtilities.invokeLater( () -> window.setVisible( checkBox.isSelected() ) ) );
+
+		window.addWindowListener(
+				new WindowAdapter() {
+					public void windowClosing( WindowEvent ev) {
+						checkBox.setSelected( false );
+					}
+		});
+
+		return checkBox;
+	}
+
+	private static JCheckBox createScatterPlotViewerVisibilityCheckbox(
+			SegmentationSourceDisplay display,
+			boolean isVisible )
+	{
+		JCheckBox checkBox = new JCheckBox( "P" );
+		checkBox.setSelected( isVisible );
+		checkBox.setPreferredSize( PREFERRED_CHECKBOX_SIZE );
+		checkBox.addActionListener( e ->
+			SwingUtilities.invokeLater( () ->
+				{
+					if ( checkBox.isSelected() )
+						display.scatterPlotViewer.show();
+					else
+						display.scatterPlotViewer.hide();
+				} ) );
+
+		display.scatterPlotViewer.getListeners().add( new VisibilityListener()
+		{
+			@Override
+			public void visibility( boolean isVisible )
+			{
+				SwingUtilities.invokeLater( () ->
+				{
+					checkBox.setSelected( isVisible );
+				});
+			}
+		} );
+
+		return checkBox;
+	}
+
+	public static JCheckBox createVolumeViewerVisibilityCheckbox( SegmentationSourceDisplay display )
+	{
+		JCheckBox checkBox = new JCheckBox( "V" );
+		checkBox.setSelected( display.showSelectedSegmentsIn3d() );
+		checkBox.setPreferredSize( PREFERRED_CHECKBOX_SIZE );
+
+		checkBox.addActionListener( new ActionListener()
+		{
+			@Override
+			public void actionPerformed( ActionEvent e )
+			{
+				new Thread( () -> {
+					if ( checkBox.isSelected() )
+					{
+						display.segmentsVolumeViewer.showSegments( true );
+					}
+					else
+					{
+						display.segmentsVolumeViewer.showSegments( false );
+					}
+				}).start();
+			}
+		} );
+
+		display.segmentsVolumeViewer.getListeners().add( new VisibilityListener()
+		{
+			@Override
+			public void visibility( boolean isVisible )
+			{
+				SwingUtilities.invokeLater( () ->
+				{
+					checkBox.setSelected( isVisible );
+				});
+			}
+		} );
+
+
+		return checkBox;
+	}
+
+	public static JButton createFocusButton( SourceDisplay sourceDisplay, List< SourceAndConverter< ? > > sourceAndConverters, BdvHandle bdvHandle )
+	{
+		JButton button = new JButton( "F" );
+		button.setPreferredSize( PREFERRED_BUTTON_SIZE );
+
+		button.addActionListener( e ->
+		{
+			for ( SourceAndConverter< ? > sourceAndConverter : sourceAndConverters )
+			{
+				// TODO: make this work for multiple!
+				final AffineTransform3D transform = new ViewerTransformAdjuster( sourceDisplay.sliceViewer.getBdvHandle(), sourceAndConverter ).getTransform();
+				new ViewerTransformChanger( bdvHandle, transform, false, 1000 ).run();
+			}
+		} );
+
+		return button;
+	}
+
+	public static JButton createImageDisplayBrightnessButton( ImageSourceDisplay imageDisplay )
+	{
+		JButton button = new JButton( "B" );
+		button.setPreferredSize( PREFERRED_BUTTON_SIZE );
+
+		button.addActionListener( e ->
+		{
+			final ArrayList< ConverterSetup > converterSetups = new ArrayList<>();
+			for ( SourceAndConverter< ? > sourceAndConverter : imageDisplay.sourceAndConverters )
+			{
+				converterSetups.add( SourceAndConverterServices.getSourceAndConverterDisplayService().getConverterSetup( sourceAndConverter ) );
+			}
+
+			UserInterfaceHelper.showBrightnessDialog(
+					imageDisplay.getName(),
+					converterSetups,
+					0,   // TODO: determine somehow...
+					65535 );
+		} );
+
+		return button;
+	}
+
+	public static JButton createDummyButton(  )
+	{
+		JButton button = new JButton( " " );
+		button.setPreferredSize( PREFERRED_BUTTON_SIZE );
+
+		button.addActionListener( e ->
+		{
+		} );
+
+		return button;
+	}
+
+	public static JButton createOpacityButton( List< SourceAndConverter< ? > > sourceAndConverters, String name, BdvHandle bdvHandle )
+	{
+		JButton button = new JButton( "O" );
+		button.setPreferredSize( PREFERRED_BUTTON_SIZE );
+
+		button.addActionListener( e ->
+		{
+			UserInterfaceHelper.showOpacityDialog(
+					name,
+					sourceAndConverters,
+					bdvHandle );
+		} );
+
+		return button;
+	}
+
+	private static JButton createColorButton( JPanel parentPanel, List< SourceAndConverter< ? > > sourceAndConverters )
+	{
+		JButton colorButton = new JButton( "C" );
+
+		colorButton.setPreferredSize( PREFERRED_BUTTON_SIZE);
+
+		colorButton.addActionListener( e ->
+		{
+			Color color = JColorChooser.showDialog( null, "", null );
+			if ( color == null ) return;
+
+			parentPanel.setBackground( color );
+
+			for ( SourceAndConverter< ? > sourceAndConverter : sourceAndConverters )
+			{
+				new ColorChanger( sourceAndConverter, ColorUtils.getARGBType( color ) ).run();
+			}
+		} );
+
+		return colorButton;
+	}
+
+	private void setPanelColor( JPanel panel, ARGBType argbType )
+	{
+		final Color color = ColorUtils.getColor( argbType );
+		if ( color != null )
+		{
+			panel.setOpaque( true );
+			panel.setBackground( color );
+		}
+	}
+
+	private void setPanelColor( JPanel panel, String colorString )
+	{
+		final Color color = ColorUtils.getColor( colorString );
+		if ( color != null )
+		{
+			panel.setOpaque( true );
+			panel.setBackground( color );
+		}
+	}
+
+	private JButton createRemoveButton( SourceDisplay sourceDisplay )
+	{
+		JButton removeButton = new JButton( "X" );
+		removeButton.setPreferredSize( PREFERRED_BUTTON_SIZE );
+
+		removeButton.addActionListener( e ->
+		{
+			moBIE2.getViewerManager().removeSourceDisplay( sourceDisplay );
+		} );
+
+		return removeButton;
+	}
+
+	public static String tidyString( String string ) {
+		string = string.trim();
+		String tidyString = string.replaceAll("\\s+","_");
+
+		if ( !string.equals(tidyString) ) {
+			Utils.log( "Spaces were removed from name, and replaced by _");
+		}
+
+		// check only contains alphanumerics, or _ -
+		if ( !tidyString.matches("^[a-zA-Z0-9_-]+$") ) {
+			Utils.log( "Names must only contain letters, numbers, _ or -. Please try again " +
+					"with a different name.");
+			tidyString = null;
+		}
+
+		return tidyString;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/ui/WindowArrangementHelper.java b/src/main/java/de/embl/cba/mobie/ui/WindowArrangementHelper.java
new file mode 100644
index 000000000..7c0e14ab7
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/ui/WindowArrangementHelper.java
@@ -0,0 +1,73 @@
+package de.embl.cba.mobie.ui;
+
+import bdv.util.BdvHandle;
+import bdv.viewer.Interpolation;
+import de.embl.cba.bdv.utils.BdvUtils;
+import ij.WindowManager;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class WindowArrangementHelper
+{
+	public static int getDefaultWindowWidth()
+	{
+		// TODO: does not work for multiple screens
+		return (int) ( Toolkit.getDefaultToolkit().getScreenSize().width / 3.1 );
+	}
+
+	@Deprecated
+	public static void setImageJLogWindowPositionAndSize( JFrame parentComponent )
+	{
+		final Frame log = WindowManager.getFrame( "Log" );
+		if (log != null) {
+			Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
+			final int logWindowHeight = screenSize.height - ( parentComponent.getLocationOnScreen().y + parentComponent.getHeight() + 20 );
+			log.setSize( parentComponent.getWidth(), logWindowHeight  );
+			log.setLocation( parentComponent.getLocationOnScreen().x, parentComponent.getLocationOnScreen().y + parentComponent.getHeight() );
+		}
+	}
+
+	public static void setBdvWindowPositionAndSize( BdvHandle bdvHandle, JFrame frame )
+	{
+		BdvUtils.getViewerFrame( bdvHandle ).setLocation(
+				frame.getLocationOnScreen().x + frame.getWidth(),
+				frame.getLocationOnScreen().y );
+
+		BdvUtils.getViewerFrame( bdvHandle ).setSize( frame.getHeight(), frame.getHeight() );
+
+		bdvHandle.getViewerPanel().setInterpolation( Interpolation.NLINEAR );
+	}
+
+	public static void setLogWindowPositionAndSize( Window reference )
+	{
+		final Frame log = WindowManager.getFrame( "Log" );
+		if (log != null) {
+			Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
+			final int logWindowHeight = screenSize.height - ( reference.getLocationOnScreen().y + reference.getHeight() + 2 * UserInterfaceHelper.SPACING );
+			log.setSize( reference.getWidth(), logWindowHeight  );
+			log.setLocation( reference.getLocationOnScreen().x, reference.getLocationOnScreen().y + UserInterfaceHelper.SPACING + reference.getHeight() );
+		}
+	}
+
+	public static void rightAlignWindow( Window reference, Window window, boolean adjustWidth, boolean adjustHeight )
+	{
+		window.setLocation(
+				reference.getLocationOnScreen().x + reference.getWidth() + UserInterfaceHelper.SPACING,
+				reference.getLocationOnScreen().y );
+
+		if ( adjustWidth )
+			window.setSize( reference.getWidth(), window.getHeight() );
+
+		if ( adjustHeight )
+			window.setSize( window.getWidth(), reference.getHeight() );
+	}
+
+	public static void bottomAlignWindow( Window reference, Window window )
+	{
+		window.setLocation(
+				reference.getLocationOnScreen().x,
+				reference.getLocationOnScreen().y + reference.getHeight() + UserInterfaceHelper.SPACING
+		);
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/ui/command/OpenMoBIEProjectCommand.java b/src/main/java/de/embl/cba/mobie/ui/command/OpenMoBIEProjectCommand.java
deleted file mode 100644
index c2143498b..000000000
--- a/src/main/java/de/embl/cba/mobie/ui/command/OpenMoBIEProjectCommand.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package de.embl.cba.mobie.ui.command;
-
-import de.embl.cba.mobie.ui.MoBIEOptions;
-import de.embl.cba.mobie.ui.MoBIE;
-import org.scijava.command.Command;
-import org.scijava.plugin.Parameter;
-import org.scijava.plugin.Plugin;
-
-
-@Plugin(type = Command.class, menuPath = "Plugins>MoBIE>Open>Open MoBIE Project..." )
-public class OpenMoBIEProjectCommand implements Command
-{
-	@Parameter ( label = "Project Location" )
-	public String projectLocation = "https://github.com/platybrowser/platybrowser";
-
-	@Override
-	public void run()
-	{
-		MoBIEOptions options = MoBIEOptions.options();
-
-		if ( ! projectLocation.startsWith( "http" ) )
-			options = options.imageDataStorageModality( MoBIEOptions.ImageDataStorageModality.FileSystem );
-
-		new MoBIE( projectLocation, options );
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/ui/command/OpenPlatyBrowserCommand.java b/src/main/java/de/embl/cba/mobie/ui/command/OpenPlatyBrowserCommand.java
deleted file mode 100644
index babf366fe..000000000
--- a/src/main/java/de/embl/cba/mobie/ui/command/OpenPlatyBrowserCommand.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package de.embl.cba.mobie.ui.command;
-
-import de.embl.cba.mobie.ui.MoBIE;
-import org.scijava.command.Command;
-import org.scijava.plugin.Plugin;
-
-
-@Plugin(type = Command.class, menuPath = "Plugins>MoBIE>Open>Open PlatyBrowser")
-public class OpenPlatyBrowserCommand implements Command
-{
-	public static final String githubProject = "https://github.com/platybrowser/platybrowser";
-
-	@Override
-	public void run()
-	{
-		new MoBIE( githubProject );
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/universe/UniverseConfigurationDialog.java b/src/main/java/de/embl/cba/mobie/universe/UniverseConfigurationDialog.java
deleted file mode 100644
index 87282b822..000000000
--- a/src/main/java/de/embl/cba/mobie/universe/UniverseConfigurationDialog.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package de.embl.cba.mobie.universe;
-
-import de.embl.cba.mobie.ui.SourcesDisplayManager;
-import ij.gui.NonBlockingGenericDialog;
-
-import java.util.ArrayList;
-
-public class UniverseConfigurationDialog
-{
-	private final SourcesDisplayManager sourcesDisplayManager;
-
-	public UniverseConfigurationDialog( SourcesDisplayManager sourcesDisplayManager )
-	{
-		this.sourcesDisplayManager = sourcesDisplayManager;
-	}
-
-	public void showDialog()
-	{
-		new Thread( () -> {
-			final NonBlockingGenericDialog gd = new NonBlockingGenericDialog( "3D View Preferences" );
-
-			ArrayList< String > sourceNames = new ArrayList< String >( sourcesDisplayManager.getVisibleSourceNames() );
-			gd.addMessage( "Resolution for 3D view [micrometer]" );
-			gd.addMessage( "Put 0 for an automated resolution choice." );
-
-			for ( String sourceName : sourceNames )
-			{
-				gd.addNumericField( sourceName,  sourcesDisplayManager.getSourceAndCurrentMetadata( sourceName ).metadata().resolution3dView, 2 );
-			}
-
-			gd.showDialog();
-			if ( gd.wasCanceled() ) return;
-
-			for ( String sourceName : sourceNames )
-			{
-				sourcesDisplayManager.setVoxelSpacing3DView( sourceName, gd.getNextNumber() );
-			}
-		} ).start();
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/universe/UniverseManager.java b/src/main/java/de/embl/cba/mobie/universe/UniverseManager.java
deleted file mode 100644
index fa5101c30..000000000
--- a/src/main/java/de/embl/cba/mobie/universe/UniverseManager.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package de.embl.cba.mobie.universe;
-
-public class UniverseManager
-{
-}
diff --git a/src/main/java/de/embl/cba/mobie/utils/BatchApplyAmiraTransformationsToBdvXml.java b/src/main/java/de/embl/cba/mobie/utils/BatchApplyAmiraTransformationsToBdvXml.java
deleted file mode 100644
index c5b77f8cf..000000000
--- a/src/main/java/de/embl/cba/mobie/utils/BatchApplyAmiraTransformationsToBdvXml.java
+++ /dev/null
@@ -1,161 +0,0 @@
-package de.embl.cba.mobie.utils;
-
-import de.embl.cba.tables.FileAndUrlUtils;
-import de.embl.cba.transforms.utils.TransformConversions;
-import net.imglib2.realtransform.AffineTransform3D;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import static de.embl.cba.transforms.utils.TransformConversions.changeTransformToScaledUnits;
-
-public class BatchApplyAmiraTransformationsToBdvXml
-{
-
-	final double[] rotationAxis;
-	final double rotationAngleInDegrees;
-	final double[] translationInMicrometer;
-	final String directory;
-	final String filePattern;
-	private static final Pattern sizePattern = Pattern.compile( ".*<size>(.*)</size>.*" );
-	private static final String voxelSizeStart = "<voxelSize>";
-	private static final String voxelSizeEnd = "</voxelSize>";
-	private double[] voxelSizeInMicrometer;
-	private double[] imageCentreInPixels;
-	private double[] imageCentreInMicrometer;
-
-	public BatchApplyAmiraTransformationsToBdvXml( double[] rotationAxis,
-												   double rotationAngleInDegrees,
-												   double[] translationInMicrometer,
-												   String directory,
-												   String filePattern )
-	{
-		this.rotationAxis = rotationAxis;
-		this.rotationAngleInDegrees = rotationAngleInDegrees;
-		this.translationInMicrometer = translationInMicrometer;
-		this.directory = directory;
-		this.filePattern = filePattern;
-
-	}
-
-	public void run() throws IOException
-	{
-		System.out.println( "Fetching files....");
-		final List< File > files = FileAndUrlUtils.getFileList( new File( directory ), filePattern );
-
-		for ( File file : files )
-		{
-			if ( file.getName().contains( "OldAligned.xml" ) ) continue;
-			if ( file.getName().contains( "-test.xml" ) ) continue;
-			if ( file.getName().contains( "-aligned.xml" ) ) continue;
-
-			setVoxelSizeAndDimensions( file );
-
-			final AffineTransform3D affineTransform3D = TransformConversions.getAmiraAsPixelUnitsAffineTransform3D(
-					rotationAxis,
-					rotationAngleInDegrees,
-					translationInMicrometer,
-					voxelSizeInMicrometer,
-					imageCentreInPixels );
-
-			System.out.println( "\n" + file.getName() );
-			System.out.println( "Transform in pixel units");
-			System.out.println( TransformConversions.asStringBdvStyle( affineTransform3D ) );
-
-			System.out.println( "Transform in scaled units");
-			changeTransformToScaledUnits( affineTransform3D, voxelSizeInMicrometer );
-			System.out.println( TransformConversions.asStringBdvStyle( affineTransform3D ) );
-
-			List< String > fileContent = new ArrayList<>( Files.readAllLines( file.toPath(), StandardCharsets.UTF_8));
-
-			for (int i = 0; i < fileContent.size(); i++) {
-				if ( fileContent.get(i).contains( "<affine>" ))
-				{
-					String newAffine = "       <affine>"
-							+ TransformConversions.asStringBdvStyle( affineTransform3D )
-							+ "</affine>";
-
-					fileContent.set(i, newAffine);
-					break;
-				}
-			}
-
-			// TODO: I cannot overwrite the original xml files...why?
-			final Path path = new File( directory + File.separator + file.getName() + "-aligned.xml" ).toPath();
-
-			Files.write( path, fileContent, StandardCharsets.UTF_8);
-
-			System.out.println( "Written: " + path );
-		}
-
-	}
-
-
-	private void setVoxelSizeAndDimensions( File file ) throws IOException
-	{
-		final List< String > lines = Files.readAllLines( file.toPath() );
-
-		boolean isVoxelSize = false;
-
-		for ( String line : lines )
-		{
-
-			if ( line.contains( voxelSizeStart ) )isVoxelSize = true;
-			if ( line.contains( voxelSizeEnd ) )isVoxelSize = false;
-
-			final Matcher matcher = sizePattern.matcher( line );
-
-			if ( matcher.matches()  )
-			{
-				final String[] sizes = matcher.group( 1 ).split( " " );
-
-				if ( isVoxelSize )
-				{
-					voxelSizeInMicrometer = new double[ 3 ];
-					for ( int d = 0; d < sizes.length; ++d ) voxelSizeInMicrometer[ d ] = Double.parseDouble( sizes[ d ] );
-				}
-				else
-				{
-					imageCentreInPixels = new double[ 3 ];
-					for ( int d = 0; d < sizes.length; ++d ) imageCentreInPixels[ d ] = Long.parseLong( sizes[ d ] ) / 2.0;
-				}
-			}
-		}
-
-		imageCentreInMicrometer = new double[ 3 ];
-
-		for ( int d = 0; d < imageCentreInMicrometer.length; ++d )
-		{
-			imageCentreInMicrometer[ d ] = imageCentreInPixels[ d ] * voxelSizeInMicrometer[ d ];
-		}
-
-	}
-
-	public static void main( String[] args ) throws IOException
-	{
-		final BatchApplyAmiraTransformationsToBdvXml batchApplyAmiraTransformationsToBdvXml = new BatchApplyAmiraTransformationsToBdvXml(
-				new double[]{ 0.064, 0.762, 0.643 },
-				237.0,
-				new double[]{ 147.9, 48.13, 103.0661 },
-				"/Volumes/arendt/EM_6dpf_segmentation/EM-Prospr",
-				".*parapod-fib.*.xml" );
-
-		// Old one
-//		final BatchApplyAmiraTransformationsToBdvXml batchApplyAmiraTransformationsToBdvXml = new BatchApplyAmiraTransformationsToBdvXml(
-//				new double[]{ -0.387, -0.727, -0.565 },
-//				107.596,
-//				new double[]{ 148.578, 45.701, 115.941 },
-//				"/Volumes/arendt/EM_6dpf_segmentation/EM-Prospr",
-//				".*parapod.*.xml" );
-
-		batchApplyAmiraTransformationsToBdvXml.run();
-
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/utils/CellularModelImageCreator.java b/src/main/java/de/embl/cba/mobie/utils/CellularModelImageCreator.java
deleted file mode 100644
index 7014c340a..000000000
--- a/src/main/java/de/embl/cba/mobie/utils/CellularModelImageCreator.java
+++ /dev/null
@@ -1,93 +0,0 @@
-package de.embl.cba.mobie.utils;
-
-import ij.ImageJ;
-import ij.ImagePlus;
-import net.imglib2.RandomAccess;
-import net.imglib2.RandomAccessibleInterval;
-import net.imglib2.img.array.ArrayImgs;
-import net.imglib2.img.display.imagej.ImageJFunctions;
-import net.imglib2.type.numeric.integer.IntType;
-
-import java.io.*;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class CellularModelImageCreator
-{
-	public static void main( String[] args ) throws IOException
-	{
-		final String table = "/Users/tischer/Documents/detlev-arendt-clem-registration/data/CellModels_coordinates.tsv";
-
-		Pattern coordinatePattern = Pattern.compile(".*( [0-9]{2,3}, [0-9]{2,3}, [0-9]{2,3} ).*");
-
-		final HashMap< String, List< int[] > > cmMap = new HashMap<>();
-
-		try( BufferedReader br = new BufferedReader( new FileReader(table) ) )
-		{
-
-			for(String line; (line = br.readLine()) != null; )
-			{
-				// System.out.println( line );
-
-				// Cellular model name
-
-				String[] split = line.split( "   " );
-				final String cmName = split[ 0 ];
-
-				// Coordinates
-
-				final List< int[] > coordinates = new ArrayList<>();
-				split = line.split( " ; " );
-				for ( String s : split )
-				{
-					Matcher m = coordinatePattern.matcher( s );
-					if ( m.matches() )
-					{
-						final String[] sc = m.group( 1 ).split( ", " );
-						final int[] c = new int[ 3 ];
-						for ( int d = 0; d < 3; ++d )
-						{
-							c[ d ] = Integer.parseInt( sc[ d ].trim() );
-						}
-						coordinates.add( c );
-					}
-				}
-
-				cmMap.put( cmName, coordinates );
-			}
-		}
-
-		// put into image
-
-		final RandomAccessibleInterval< IntType > image = ArrayImgs.ints( new long[]{ 320, 550, 1, 251 } );
-		final RandomAccess< IntType > access = image.randomAccess();
-
-		int i = 0;
-		for ( String cm : cmMap.keySet() )
-		{
-			final int cmIndex = i++; // TODO: which index to use? Integer.parseInt( cm.split( "_" )[ 1 ] );
-
-			final List< int[] > coordinates = cmMap.get( cm );
-
-			for ( int[] coordinate : coordinates )
-			{
-				int[] correctedDimensionOrder = new int[ 4 ];
-				correctedDimensionOrder[ 0 ] = coordinate[ 1 ];
-				correctedDimensionOrder[ 1 ] = coordinate[ 0 ];
-				correctedDimensionOrder[ 2 ] = 0; // channel
-				correctedDimensionOrder[ 3 ] = coordinate[ 2 ];
-
-				access.setPosition( correctedDimensionOrder );
-				access.get().set( cmIndex );
-			}
-		}
-
-		new ImageJ();
-		// show image
-		final ImagePlus imagePlus = ImageJFunctions.wrap( image, "cm" );
-		imagePlus.show();
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/utils/Enums.java b/src/main/java/de/embl/cba/mobie/utils/Enums.java
deleted file mode 100644
index 2738cc79f..000000000
--- a/src/main/java/de/embl/cba/mobie/utils/Enums.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.embl.cba.mobie.utils;
-
-public class Enums
-{
-	public static <T extends Enum<?>> T valueOf( Class<T> enumeration,
-												 String search) {
-		for (T each : enumeration.getEnumConstants()) {
-			if (each.name().compareToIgnoreCase(search) == 0) {
-				return each;
-			}
-		}
-		return null;
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/utils/ExportUtils.java b/src/main/java/de/embl/cba/mobie/utils/ExportUtils.java
deleted file mode 100644
index ce8337e23..000000000
--- a/src/main/java/de/embl/cba/mobie/utils/ExportUtils.java
+++ /dev/null
@@ -1,155 +0,0 @@
-package de.embl.cba.mobie.utils;
-
-import bdv.img.n5.N5ImageLoader;
-import bdv.spimdata.SpimDataMinimal;
-import de.embl.cba.mobie.n5.N5FSImageLoader;
-import de.embl.cba.mobie.n5.N5S3ImageLoader;
-import de.embl.cba.mobie.projects.projectsCreator.ProjectsCreator;
-import ij.IJ;
-import ij.ImagePlus;
-import mpicbg.spim.data.generic.sequence.BasicImgLoader;
-import mpicbg.spim.data.generic.sequence.BasicSetupImgLoader;
-import mpicbg.spim.data.sequence.FinalVoxelDimensions;
-import mpicbg.spim.data.sequence.ImgLoader;
-import net.imglib2.FinalDimensions;
-import net.imglib2.realtransform.AffineTransform3D;
-
-import java.io.File;
-
-public class ExportUtils {
-
-    public static boolean isImageSuitable(ImagePlus imp) {
-        // check the image type
-        switch (imp.getType()) {
-            case ImagePlus.GRAY8:
-            case ImagePlus.GRAY16:
-            case ImagePlus.GRAY32:
-                break;
-            default:
-                IJ.showMessage("Only 8, 16, 32-bit images are supported currently!");
-                return false;
-        }
-
-        // check the image dimensionality
-        if (imp.getNDimensions() < 2) {
-            IJ.showMessage("Image must be at least 2-dimensional!");
-            return false;
-        }
-
-        return true;
-    }
-
-    public static boolean isValidAffine ( String affine ) {
-        if ( !affine.matches("^[0-9. ]+$") ) {
-            Utils.log( "Invalid affine transform - must contain only numbers and spaces");
-            return false;
-        }
-
-        String[] splitAffine = affine.split(" ");
-        if ( splitAffine.length != 12) {
-            Utils.log( "Invalid affine transform - must be of length 12");
-            return false;
-        }
-
-        return true;
-    }
-
-    public static AffineTransform3D parseAffineString( String affine ) {
-        if ( isValidAffine( affine )) {
-            AffineTransform3D sourceTransform = new AffineTransform3D();
-            String[] splitAffineTransform = affine.split(" ");
-            double[] doubleAffineTransform = new double[splitAffineTransform.length];
-            for (int i = 0; i < splitAffineTransform.length; i++) {
-                doubleAffineTransform[i] = Double.parseDouble(splitAffineTransform[i]);
-            }
-            sourceTransform.set(doubleAffineTransform);
-            return sourceTransform;
-        } else {
-            return null;
-        }
-    }
-
-    public static String generateDefaultAffine ( ImagePlus imp ) {
-        final double pixelWidth = imp.getCalibration().pixelWidth;
-        final double pixelHeight = imp.getCalibration().pixelHeight;
-        final double pixelDepth = imp.getCalibration().pixelDepth;
-
-        String defaultAffine = pixelWidth + " 0.0 0.0 0.0 0.0 " + pixelHeight + " 0.0 0.0 0.0 0.0 " + pixelDepth + " 0.0";
-        return defaultAffine;
-    }
-
-    public static FinalVoxelDimensions getVoxelSize (ImagePlus imp ) {
-        final double pw = imp.getCalibration().pixelWidth;
-        final double ph = imp.getCalibration().pixelHeight;
-        final double pd = imp.getCalibration().pixelDepth;
-        String punit = imp.getCalibration().getUnit();
-        if ( punit == null || punit.isEmpty() )
-            punit = "px";
-        final FinalVoxelDimensions voxelSize = new FinalVoxelDimensions( punit, pw, ph, pd );
-        return voxelSize;
-    }
-
-    public static FinalDimensions getSize (ImagePlus imp ) {
-        final int w = imp.getWidth();
-        final int h = imp.getHeight();
-        final int d = imp.getNSlices();
-        final FinalDimensions size = new FinalDimensions( w, h, d );
-        return size;
-    }
-
-    public static File getSeqFileFromPath ( String seqFilename ) {
-        final File seqFile = new File( seqFilename );
-        final File parent = seqFile.getParentFile();
-        if ( parent == null || !parent.exists() || !parent.isDirectory() )
-        {
-            IJ.showMessage( "Invalid export filename " + seqFilename );
-            return null;
-        }
-        return seqFile;
-    }
-
-    public static AffineTransform3D generateSourceTransform ( FinalVoxelDimensions  voxelSize ) {
-        // create SourceTransform from the images calibration
-        final AffineTransform3D sourceTransform = new AffineTransform3D();
-        sourceTransform.set( voxelSize.dimension(0), 0, 0, 0, 0, voxelSize.dimension(1),
-                0, 0, 0, 0, voxelSize.dimension(2), 0 );
-        return sourceTransform;
-    }
-
-    public static File getN5FileFromXmlPath ( String xmlPath ) {
-        final String n5Filename = xmlPath.substring( 0, xmlPath.length() - 4 ) + ".n5";
-        return new File( n5Filename );
-    }
-
-    public static ProjectsCreator.BdvFormat getBdvFormatFromSpimDataMinimal( SpimDataMinimal spimDataMinimal ) {
-        ProjectsCreator.BdvFormat bdvFormat = null;
-        BasicImgLoader imgLoader = spimDataMinimal.getSequenceDescription().getImgLoader();
-        if ( imgLoader instanceof N5ImageLoader | imgLoader instanceof N5FSImageLoader |
-                imgLoader instanceof N5S3ImageLoader | imgLoader instanceof de.embl.cba.mobie.n5.N5ImageLoader )
-        {
-            bdvFormat = ProjectsCreator.BdvFormat.n5;
-        }
-
-        return bdvFormat;
-    }
-
-    public static File getImageLocationFromSpimDataMinimal( SpimDataMinimal spimDataMinimal, ProjectsCreator.BdvFormat bdvFormat) {
-        File imageLocation = null;
-
-        switch ( bdvFormat ) {
-            case n5:
-                // get image loader to find absolute image location
-                BasicImgLoader imgLoader = spimDataMinimal.getSequenceDescription().getImgLoader();
-                if ( imgLoader instanceof N5ImageLoader ) {
-                    N5ImageLoader n5ImageLoader = (N5ImageLoader) imgLoader;
-                    imageLocation = n5ImageLoader.getN5File();
-                } else if ( imgLoader instanceof N5FSImageLoader ) {
-                    N5FSImageLoader n5ImageLoader = (N5FSImageLoader) imgLoader;
-                    imageLocation = n5ImageLoader.getN5File();
-                }
-                break;
-        }
-
-        return imageLocation;
-    }
-}
diff --git a/src/main/java/de/embl/cba/mobie/utils/FileAndUrlUtils.java b/src/main/java/de/embl/cba/mobie/utils/FileAndUrlUtils.java
deleted file mode 100644
index a6451e831..000000000
--- a/src/main/java/de/embl/cba/mobie/utils/FileAndUrlUtils.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package de.embl.cba.mobie.utils;
-
-import java.io.File;
-import java.net.URI;
-import java.net.URISyntaxException;
-
-public class FileAndUrlUtils
-{
-	public static String getName( String path )
-	{
-		if ( path.startsWith( "http" ) )
-		{
-			final String[] split = path.split( "/" );
-			return split[ split.length - 1 ];
-		}
-		else
-		{
-			return new File( path ).getName();
-		}
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/utils/JsonUtils.java b/src/main/java/de/embl/cba/mobie/utils/JsonUtils.java
deleted file mode 100644
index bd7a0a06b..000000000
--- a/src/main/java/de/embl/cba/mobie/utils/JsonUtils.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package de.embl.cba.mobie.utils;
-
-import com.google.gson.stream.JsonReader;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.util.ArrayList;
-
-public abstract class JsonUtils
-{
-	public static ArrayList< String > readStringArray( JsonReader reader ) throws IOException
-	{
-		reader.beginArray();
-		final ArrayList< String > strings = new ArrayList< String >();
-		while ( reader.hasNext() )
-			strings.add( reader.nextString() );
-		reader.endArray();
-		reader.close();
-
-		return strings;
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/utils/SortIgnoreCase.java b/src/main/java/de/embl/cba/mobie/utils/SortIgnoreCase.java
deleted file mode 100644
index 38f97da49..000000000
--- a/src/main/java/de/embl/cba/mobie/utils/SortIgnoreCase.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package de.embl.cba.mobie.utils;
-
-import java.util.Comparator;
-
-public class SortIgnoreCase implements Comparator<Object> {
-	public int compare(Object o1, Object o2) {
-		String s1 = (String) o1;
-		String s2 = (String) o2;
-		return s1.toLowerCase().compareTo(s2.toLowerCase());
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/utils/UniverseUtils.java b/src/main/java/de/embl/cba/mobie/utils/UniverseUtils.java
deleted file mode 100644
index 7d117633c..000000000
--- a/src/main/java/de/embl/cba/mobie/utils/UniverseUtils.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package de.embl.cba.mobie.utils;
-
-import ij3d.Image3DUniverse;
-import org.scijava.java3d.Transform3D;
-
-import java.util.HashMap;
-
-public abstract class UniverseUtils
-{
-	// TODO: refactor to UniverseUtils
-	public static void setVolumeView( HashMap< String, String > transforms, Image3DUniverse universe )
-	{
-		String tmp;
-		// Set up new Content
-		if ((tmp = transforms.get("center")) != null) universe.getCenterTG().setTransform(
-				asTransform(tmp));
-		if ((tmp = transforms.get("translate")) != null) universe.getTranslateTG()
-				.setTransform( asTransform(tmp));
-		if ((tmp = transforms.get("rotate")) != null) universe.getRotationTG().setTransform(
-				asTransform(tmp));
-		if ((tmp = transforms.get("zoom")) != null) universe.getZoomTG()
-				.setTransform( asTransform(tmp));
-		if ((tmp = transforms.get("animate")) != null) universe.getAnimationTG()
-				.setTransform( asTransform(tmp));
-
-		universe.getViewPlatformTransformer().updateFrontBackClip();
-	}
-
-	// TODO: refactor to UniverseUtils
-	private static final Transform3D asTransform( final String s) {
-		final String[] sp = s.split(" ");
-		final float[] f = new float[16];
-		for (int i = 0; i < sp.length; i++)
-			f[i] = asFloat(sp[i]);
-		return new Transform3D(f);
-	}
-
-	// TODO: refactor to UniverseUtils
-	private static final float asFloat( final String s) {
-		return Float.parseFloat(s);
-	}
-
-	public static final String toString( final Transform3D t3d ) {
-		final float[] xf = new float[16];
-		t3d.get(xf);
-		String ret = "";
-		for (int i = 0; i < 16; i++)
-			ret += " " + xf[i];
-		return ret;
-	}
-
-	public static String getVolumeViewerTransform( Image3DUniverse universe )
-	{
-		if ( universe == null ) return "";
-
-		String transform = "";
-		final Transform3D t3d = new Transform3D();
-		universe.getCenterTG().getTransform(t3d);
-		transform += "center = " + toString(t3d) +"\n";
-		universe.getTranslateTG().getTransform(t3d);
-		transform += "translate = " + toString(t3d) +"\n";
-		universe.getRotationTG().getTransform(t3d);
-		transform += "rotate = " + toString(t3d) +"\n";
-		universe.getZoomTG().getTransform(t3d);
-		transform += "zoom = " + toString(t3d) +"\n";
-		universe.getAnimationTG().getTransform(t3d);
-		transform += "animate = " + toString(t3d) +"\n";
-
-		return transform;
-	}
-
-	// TODO: refactor to UniverseUtils
-	public static void setVolumeView( String view, Image3DUniverse universe )
-	{
-		UniverseUtils.setVolumeView( getTransforms( view ), universe );
-	}
-
-	// TODO: refactor to UniverseUtils
-	private static HashMap< String, String > getTransforms( String view )
-	{
-		final HashMap<String, String> transforms = new HashMap<String, String>();
-
-		final String[] lines = view.split( "\n" );
-
-		for ( String line : lines )
-		{
-			final String[] keyval = line.split("=");
-			transforms.put(keyval[0].trim(), keyval[1].trim());
-		}
-		return transforms;
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/utils/Utils.java b/src/main/java/de/embl/cba/mobie/utils/Utils.java
deleted file mode 100644
index 6964851ef..000000000
--- a/src/main/java/de/embl/cba/mobie/utils/Utils.java
+++ /dev/null
@@ -1,569 +0,0 @@
-package de.embl.cba.mobie.utils;
-
-import bdv.util.BdvHandle;
-import bdv.viewer.Source;
-import com.google.gson.GsonBuilder;
-import com.google.gson.internal.LinkedTreeMap;
-import com.google.gson.stream.JsonReader;
-import de.embl.cba.bdv.utils.BdvUtils;
-import de.embl.cba.mobie.Constants;
-import de.embl.cba.mobie.image.SourceGroupLabelSourceMetadata;
-import de.embl.cba.tables.FileUtils;
-import de.embl.cba.tables.TableColumns;
-import de.embl.cba.tables.Tables;
-import de.embl.cba.tables.imagesegment.SegmentProperty;
-import de.embl.cba.tables.imagesegment.SegmentUtils;
-import de.embl.cba.tables.tablerow.TableRowImageSegment;
-import ij.IJ;
-import mpicbg.spim.data.SpimData;
-import mpicbg.spim.data.SpimDataException;
-import mpicbg.spim.data.XmlIoSpimData;
-import net.imglib2.*;
-import net.imglib2.Cursor;
-import net.imglib2.RandomAccess;
-import net.imglib2.algorithm.neighborhood.HyperSphereShape;
-import net.imglib2.algorithm.neighborhood.Neighborhood;
-import net.imglib2.algorithm.neighborhood.Shape;
-import net.imglib2.realtransform.AffineTransform3D;
-import net.imglib2.realtransform.Scale3D;
-import net.imglib2.type.NativeType;
-import net.imglib2.type.numeric.ARGBType;
-import net.imglib2.type.numeric.RealType;
-
-import java.awt.*;
-import java.io.*;
-import java.net.URI;
-import java.util.*;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import static de.embl.cba.bdv.utils.BdvUtils.getBdvWindowCenter;
-import static de.embl.cba.bdv.utils.converters.RandomARGBConverter.goldenRatio;
-
-public class Utils
-{
-	public static double[] delimitedStringToDoubleArray( String s, String delimiter) {
-
-		String[] sA = s.split(delimiter);
-		double[] nums = new double[sA.length];
-		for (int i = 0; i < nums.length; i++) {
-			nums[i] = Double.parseDouble(sA[i].trim());
-		}
-
-		return nums;
-	}
-
-	@Deprecated // use ColorUtils instead
-	public static Color getColor(String name) {
-		try {
-			return (Color)Color.class.getField(name.toUpperCase()).get(null);
-		} catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) {
-			e.printStackTrace();
-			return null;
-		}
-	}
-
-	public static long[] asLongs( double[] doubles )
-	{
-		final long[] longs = new long[ doubles.length ];
-
-		for ( int i = 0; i < doubles.length; ++i )
-		{
-			longs[ i ] = (long) doubles[ i ];
-		}
-
-		return longs;
-	}
-
-	public static void log( String text )
-	{
-		IJ.log( text );
-	}
-
-	public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) {
-		List<Map.Entry<K, V> > list = new ArrayList<>(map.entrySet());
-		list.sort( Map.Entry.comparingByValue() );
-
-		Map<K, V> result = new LinkedHashMap<>();
-		for (Map.Entry<K, V> entry : list) {
-			result.put(entry.getKey(), entry.getValue());
-		}
-
-		return result;
-	}
-
-	public static < T extends RealType< T > &  NativeType< T > >
-	double getLocalMaximum( final RandomAccessibleInterval< T > rai, double[] position, double radius, double calibration )
-	{
-		// TODO: add out-of-bounds strategy or is this handled by the Neighborhood?
-		Shape shape = new HyperSphereShape( (int) Math.ceil( radius / calibration ) );
-		final RandomAccessible< Neighborhood< T > > nra = shape.neighborhoodsRandomAccessible( rai );
-		final RandomAccess< Neighborhood< T > > neighborhoodRandomAccess = nra.randomAccess();
-		neighborhoodRandomAccess.setPosition( getPixelPosition( position, calibration ) );
-
-		final Neighborhood< T > neighborhood = neighborhoodRandomAccess.get();
-
-		final Cursor< T > cursor = neighborhood.cursor();
-		double max = - Double.MAX_VALUE;
-		double value;
-		while( cursor.hasNext() )
-		{
-			value = cursor.next().getRealDouble();
-			if ( value > max )
-			{
-				max = value;
-			}
-		}
-
-		return max;
-	}
-
-	public static < T extends RealType< T > &  NativeType< T > >
-	double getLocalSum( final RandomAccessibleInterval< T > rai, double[] position, double radius, double calibration )
-	{
-		// TODO: add out-of-bounds strategy or is this handled by the Neighborhood?
-		Shape shape = new HyperSphereShape( (int) Math.ceil( radius / calibration ) );
-		final RandomAccessible< Neighborhood< T > > nra = shape.neighborhoodsRandomAccessible( rai );
-		final RandomAccess< Neighborhood< T > > neighborhoodRandomAccess = nra.randomAccess();
-		neighborhoodRandomAccess.setPosition( getPixelPosition( position, calibration ) );
-
-		final Neighborhood< T > neighborhood = neighborhoodRandomAccess.get();
-
-		final Cursor< T > cursor = neighborhood.cursor();
-		double sum = 0.0;
-		while( cursor.hasNext() )
-		{
-			sum += cursor.next().getRealDouble();
-		}
-
-		return sum;
-	}
-
-	public static < T extends RealType< T > &  NativeType< T > >
-	double getFractionOfNonZeroVoxels( final RandomAccessibleInterval< T > rai, double[] position, double radius, double calibration )
-	{
-		// TODO: add out-of-bounds strategy or is this handled by the Neighborhood?
-		Shape shape = new HyperSphereShape( (int) Math.ceil( radius / calibration ) );
-		final RandomAccessible< Neighborhood< T > > nra = shape.neighborhoodsRandomAccessible( rai );
-		final RandomAccess< Neighborhood< T > > neighborhoodRandomAccess = nra.randomAccess();
-		neighborhoodRandomAccess.setPosition( getPixelPosition( position, calibration ) );
-
-		final Neighborhood< T > neighborhood = neighborhoodRandomAccess.get();
-
-		final Cursor< T > neighborhoodCursor = neighborhood.cursor();
-
-		long numberOfNonZeroVoxels = 0;
-		long numberOfVoxels = 0;
-
-		while( neighborhoodCursor.hasNext() )
-		{
-			numberOfVoxels++;
-
-			final double realDouble = neighborhoodCursor.next().getRealDouble();
-
-			if ( realDouble != 0)
-			{
-				numberOfNonZeroVoxels++;
-			}
-		}
-
-		return 1.0 * numberOfNonZeroVoxels / numberOfVoxels;
-	}
-
-	public static String[] combine(String[] a, String[] b){
-		int length = a.length + b.length;
-		String[] result = new String[length];
-		System.arraycopy(a, 0, result, 0, a.length);
-		System.arraycopy(b, 0, result, a.length, b.length);
-		return result;
-	}
-
-	public static Object[] combine(Object[] a, Object[] b){
-		int length = a.length + b.length;
-		Object[] result = new Object[length];
-		System.arraycopy(a, 0, result, 0, a.length);
-		System.arraycopy(b, 0, result, a.length, b.length);
-		return result;
-	}
-
-	private static long[] getPixelPosition( double[] position, double calibration )
-	{
-		long[] pixelPosition = new long[ position.length ];
-		for ( int d = 0; d < position.length; ++d )
-		{
-			pixelPosition[ d ] = (long) ( position[ d ] / calibration );
-		}
-		return pixelPosition;
-	}
-
-	public static SpimData openSpimData( File file )
-	{
-		try
-		{
-			SpimData spimData = new XmlIoSpimData().load( file.toString() );
-			return spimData;
-		}
-		catch ( SpimDataException e )
-		{
-			System.out.println( file.toString() );
-			e.printStackTrace();
-			return null;
-		}
-	}
-
-	public static ARGBType asArgbType( Color color )
-	{
-		return new ARGBType( ARGBType.rgba( color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha() ) );
-	}
-
-	public static void logVector( String preText, double[] vector )
-	{
-		String text = preText + ": ";
-
-		for ( int i = 0; i < vector.length; ++i )
-		{
-			text += vector[ i ] + " ";
-		}
-
-		Utils.log( text );
-	}
-
-	public static void logVector( String preText, double[] vector, int numDigits )
-	{
-		String text = preText + ": ";
-
-		for ( int i = 0; i < vector.length; ++i )
-		{
-			text += String.format( "%." + numDigits + "f", vector[ i ] ) + " ";
-		}
-
-		Utils.log( text );
-	}
-
-	public static double[] getCSVasDoubles( String csv )
-	{
-		final String[] split = csv.split( "," );
-		double[] normalVector = new double[ split.length ];
-		for ( int i = 0; i < split.length; ++i )
-		{
-			normalVector[ i ] = Double.parseDouble( split[ i ] );
-		}
-		return normalVector;
-	}
-
-	public static void wait100ms()
-	{
-		try
-		{
-			Thread.sleep( 100 );
-		}
-		catch ( InterruptedException e )
-		{
-			e.printStackTrace();
-		}
-	}
-
-	public static LinkedTreeMap getLinkedTreeMap( InputStream inputStream ) throws IOException
-	{
-		final JsonReader reader = new JsonReader( new InputStreamReader( inputStream, "UTF-8" ) );
-		GsonBuilder builder = new GsonBuilder();
-		return builder.create().fromJson( reader, Object.class );
-	}
-
-	public static String padLeftSpaces(String inputString, int length) {
-		if (inputString.length() >= length) {
-			return inputString;
-		}
-		StringBuilder sb = new StringBuilder();
-		while (sb.length() < length - inputString.length()) {
-			sb.append(' ');
-		}
-		sb.append(inputString);
-
-		return sb.toString();
-	}
-
-	public static List< TableRowImageSegment > createGroupedSourcesSegmentsFromTableFile(
-			String tablePath,
-			String imageId,
-			SourceGroupLabelSourceMetadata metadata )
-	{
-		log( "Opening table: " + tablePath );
-
-		tablePath = resolveTablePath( tablePath );
-
-		Map< String, List< String > > columns =
-				TableColumns.stringColumnsFromTableFile( tablePath );
-
-		// Add anchor columns, using the metadata
-		final ArrayList< List< String > > anchorColumns = new ArrayList<>();
-		for ( int d = 0; d < 3; d++ )
-		{
-			anchorColumns.add( new ArrayList<>() );
-		}
-
-		final ArrayList< String > labelIds = new ArrayList<>();
-		final ArrayList< String > labelImageIds = new ArrayList<>();
-		final List< String > sourceNames = columns.get( Constants.SOURCE_NAME );
-
-		for ( String sourceName : sourceNames )
-		{
-			final RealInterval interval = metadata.sourceNameToInterval.get( sourceName );
-			for ( int d = 0; d < 3; d++ )
-			{
-				anchorColumns.get( d ).add( String.valueOf( ( interval.realMax( d ) + interval.realMin( d ) ) / 2 ) );
-			}
-
-			labelIds.add( String.valueOf( metadata.sourceNameToLabelIndex.get( sourceName ) ) );
-			labelImageIds.add( imageId );
-		}
-
-		columns.put( Constants.ANCHOR_X, anchorColumns.get( 0 ) );
-		columns.put( Constants.ANCHOR_Y, anchorColumns.get( 1 ) );
-		columns.put( Constants.ANCHOR_Z, anchorColumns.get( 2 ) );
-		columns.put( Constants.SEGMENT_LABEL_ID, labelIds );
-		columns.put( Constants.LABEL_IMAGE_ID, labelImageIds );
-
-		final Map< SegmentProperty, List< String > > segmentPropertyToColumn
-				= createSegmentPropertyToColumn( columns );
-
-		final List< TableRowImageSegment > segments
-				= SegmentUtils.tableRowImageSegmentsFromColumns(
-				columns, segmentPropertyToColumn, false );
-
-		return segments;
-	}
-
-	public static List< TableRowImageSegment > createAnnotatedImageSegmentsFromTableFile(
-			String tablePath,
-			String imageId )
-	{
-		log( "Opening table: " + tablePath );
-
-		tablePath = resolveTablePath( tablePath );
-
-		Map< String, List< String > > columns =
-				TableColumns.stringColumnsFromTableFile( tablePath );
-
-		TableColumns.addLabelImageIdColumn(
-				columns,
-				Constants.LABEL_IMAGE_ID,
-				imageId );
-
-		final Map< SegmentProperty, List< String > > segmentPropertyToColumn
-				= createSegmentPropertyToColumn( columns );
-
-		final List< TableRowImageSegment > segments
-				= SegmentUtils.tableRowImageSegmentsFromColumns(
-						columns, segmentPropertyToColumn, false );
-
-		return segments;
-	}
-
-	public static String resolveTablePath( String tablePath )
-	{
-		if ( tablePath.startsWith( "http" ) ) {
-			tablePath = FileUtils.resolveTableURL( URI.create(tablePath) );
-		} else {
-			tablePath = FileUtils.resolveTablePath( tablePath );
-		}
-		return tablePath;
-	}
-
-	public static boolean isRelativePath( String tablePath )
-	{
-		final BufferedReader reader = Tables.getReader( tablePath );
-		final String firstLine;
-		try
-		{
-			firstLine = reader.readLine();
-			return firstLine.startsWith( ".." );
-		}
-		catch ( IOException e )
-		{
-			e.printStackTrace();
-			return false;
-		}
-	}
-
-	public static String getRelativePath( String tablePath )
-	{
-		final BufferedReader reader = Tables.getReader( tablePath );
-		try
-		{
-			String link = reader.readLine();
-			return link;
-		}
-		catch ( IOException e )
-		{
-			e.printStackTrace();
-			return null;
-		}
-
-	}
-
-	public static Map< SegmentProperty, List< String > > createSegmentPropertyToColumn(
-			Map< String, List< String > > columns )
-	{
-		final HashMap< SegmentProperty, List< String > > segmentPropertyToColumn
-				= new HashMap<>();
-
-		segmentPropertyToColumn.put(
-				SegmentProperty.LabelImage,
-				columns.get( Constants.LABEL_IMAGE_ID ));
-
-		segmentPropertyToColumn.put(
-				SegmentProperty.ObjectLabel,
-				columns.get( Constants.SEGMENT_LABEL_ID ) );
-
-		segmentPropertyToColumn.put(
-				SegmentProperty.X,
-				columns.get( Constants.ANCHOR_X ) );
-
-		segmentPropertyToColumn.put(
-				SegmentProperty.Y,
-				columns.get( Constants.ANCHOR_Y ) );
-
-		segmentPropertyToColumn.put(
-				SegmentProperty.Z,
-				columns.get( Constants.ANCHOR_Z ) );
-
-		segmentPropertyToColumn.put(
-				SegmentProperty.T,
-				columns.get( Constants.TIMEPOINT ) );
-
-		SegmentUtils.putDefaultBoundingBoxMapping( segmentPropertyToColumn, columns );
-
-		return segmentPropertyToColumn;
-	}
-
-	/**
-	 * Converts a string to a random number between 0 and 1
-	 * @param string
-	 * @return
-	 */
-	public static double createRandom( String string )
-	{
-		double random = string.hashCode() * goldenRatio;
-		random = random - ( long ) Math.floor( random );
-		return random;
-	}
-
-	/**
-	 * TODO: Make this more generic, to also work with other things than prospr and mobie
-	 *
-	 *
-	 * @param selectionName
-	 * @param isRemoveProspr
-	 * @return
-	 */
-	public static String getSimplifiedSourceName( String selectionName, boolean isRemoveProspr )
-	{
-		if ( isRemoveProspr )
-			selectionName = selectionName.replace( "prospr-", "" );
-
-		selectionName = selectionName.replace( "6dpf-1-whole-", "" );
-		selectionName = selectionName.replace( "segmented-", "" );
-		selectionName = selectionName.replace( "mask-", "" );
-		return selectionName;
-	}
-
-	public static ArrayList< String > getSortedList( Collection< String > strings )
-	{
-		final ArrayList< String > sorted = new ArrayList<>( strings );
-		Collections.sort( sorted, new SortIgnoreCase() );
-		return sorted;
-	}
-
-	public static String createNormalisedViewerTransformString( BdvHandle bdv, double[] position )
-	{
-		final AffineTransform3D view = createNormalisedViewerTransform( bdv, position );
-		final String replace = view.toString().replace( "3d-affine: (", "" ).replace( ")", "" );
-		final String collect = Arrays.stream( replace.split( "," ) ).map( x -> "n" + x.trim() ).collect( Collectors.joining( "," ) );
-		return collect;
-	}
-
-	public static AffineTransform3D createNormalisedViewerTransform( BdvHandle bdv, double[] position )
-	{
-		final AffineTransform3D view = new AffineTransform3D();
-		bdv.getViewerPanel().state().getViewerTransform( view );
-
-		// translate position to upper left corner of the Window (0,0)
-		final AffineTransform3D translate = new AffineTransform3D();
-		translate.translate( position );
-		view.preConcatenate( translate.inverse() );
-
-		// divide by window width
-		final int bdvWindowWidth = BdvUtils.getBdvWindowWidth( bdv );
-		final Scale3D scale = new Scale3D( 1.0 / bdvWindowWidth, 1.0 / bdvWindowWidth, 1.0 / bdvWindowWidth );
-		view.preConcatenate( scale );
-
-		return view;
-	}
-
-	public static double[] getMousePosition( BdvHandle bdv )
-	{
-		final RealPoint realPoint = new RealPoint( 2 );
-		bdv.getViewerPanel().getMouseCoordinates( realPoint );
-		final double[] doubles = new double[ 3 ];
-		realPoint.localize( doubles );
-		return doubles;
-	}
-
-	public static AffineTransform3D createUnnormalizedViewerTransform( AffineTransform3D normalisedTransform, BdvHandle bdv )
-	{
-		final AffineTransform3D transform = normalisedTransform.copy();
-
-		final int bdvWindowWidth = BdvUtils.getBdvWindowWidth( bdv );
-		final Scale3D scale = new Scale3D( 1.0 / bdvWindowWidth, 1.0 / bdvWindowWidth, 1.0 / bdvWindowWidth );
-		transform.preConcatenate( scale.inverse() );
-
-		AffineTransform3D translate = new AffineTransform3D();
-		translate.translate( getBdvWindowCenter( bdv ) );
-
-		transform.preConcatenate( translate );
-
-		return transform;
-	}
-
-	public static AffineTransform3D asAffineTransform3D( double[] doubles )
-	{
-		final AffineTransform3D view = new AffineTransform3D( );
-		view.set( doubles );
-		return view;
-	}
-
-	public static FinalRealInterval estimateBounds( Source< ? > source )
-	{
-		final AffineTransform3D affineTransform3D = new AffineTransform3D();
-		source.getSourceTransform( 0, 0, affineTransform3D );
-		final FinalRealInterval bounds = affineTransform3D.estimateBounds( source.getSource( 0, 0 ) );
-		return bounds;
-	}
-
-	public static String tidyString( String string ) {
-		string = string.trim();
-		String tidyString = string.replaceAll("\\s+","_");
-
-		if ( !string.equals(tidyString) ) {
-			Utils.log( "Spaces were removed from name, and replaced by _");
-		}
-
-		// check only contains alphanumerics, or _ -
-		if ( !tidyString.matches("^[a-zA-Z0-9_-]+$") ) {
-			Utils.log( "Names must only contain letters, numbers, _ or -. Please try again " +
-					"with a different name.");
-			tidyString = null;
-		}
-
-		return tidyString;
-	}
-
-	public static int getNumTimePoints( Source< ? > source )
-	{
-		int numTimePoints = 0;
-		while ( source.isPresent( numTimePoints++ ) ){}
-		return numTimePoints - 1;
-	}
-
-}
diff --git a/src/main/java/de/embl/cba/mobie/utils/Version.java b/src/main/java/de/embl/cba/mobie/utils/Version.java
deleted file mode 100644
index 18fa83cca..000000000
--- a/src/main/java/de/embl/cba/mobie/utils/Version.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package de.embl.cba.mobie.utils;
-
-
-/**
- * https://stackoverflow.com/questions/198431/how-do-you-compare-two-version-strings-in-java
- */
-public class Version implements Comparable<Version> {
-
-	private String version;
-
-	public final String get() {
-		return this.version;
-	}
-
-	public Version(String version) {
-		if(version == null)
-			throw new IllegalArgumentException("Version can not be null");
-		if(!version.matches("[0-9]+(\\.[0-9]+)*"))
-			throw new IllegalArgumentException("Invalid version format");
-		this.version = version;
-	}
-
-	@Override public int compareTo(Version that) {
-		if(that == null)
-			return 1;
-		String[] thisParts = this.get().split("\\.");
-		String[] thatParts = that.get().split("\\.");
-		int length = Math.max(thisParts.length, thatParts.length);
-		for(int i = 0; i < length; i++) {
-			int thisPart = i < thisParts.length ?
-					Integer.parseInt(thisParts[i]) : 0;
-			int thatPart = i < thatParts.length ?
-					Integer.parseInt(thatParts[i]) : 0;
-			if(thisPart < thatPart)
-				return -1;
-			if(thisPart > thatPart)
-				return 1;
-		}
-		return 0;
-	}
-
-	@Override public boolean equals(Object that) {
-		if(this == that)
-			return true;
-		if(that == null)
-			return false;
-		if(this.getClass() != that.getClass())
-			return false;
-		return this.compareTo((Version) that) == 0;
-	}
-
-}
diff --git a/src/main/java/de/embl/cba/mobie/utils/ui/BdvTextOverlay.java b/src/main/java/de/embl/cba/mobie/utils/ui/BdvTextOverlay.java
deleted file mode 100644
index 84f14c74c..000000000
--- a/src/main/java/de/embl/cba/mobie/utils/ui/BdvTextOverlay.java
+++ /dev/null
@@ -1,101 +0,0 @@
-package de.embl.cba.mobie.utils.ui;
-
-import bdv.util.*;
-import net.imglib2.realtransform.AffineTransform2D;
-
-import java.awt.*;
-
-public class BdvTextOverlay extends BdvOverlay
-{
-
-	String text;
-	final double[] position;
-	final int textSize;
-	final Bdv bdv;
-	private final int numDimensions;
-	private final BdvOverlaySource< BdvTextOverlay > overlay;
-
-	public BdvTextOverlay( Bdv bdv, String text, double[] position )
-	{
-		super();
-		this.text = text;
-		this.position = position;
-		this.bdv = bdv;
-		this.numDimensions = position.length;
-		this.textSize = 20;
-
-		overlay = BdvFunctions.showOverlay( this, "overlay", BdvOptions.options().addTo( bdv ) );
-
-	}
-
-	public void removeFromBdv()
-	{
-		try
-		{
-			overlay.removeFromBdv();
-		}
-		catch ( Exception e )
-		{
-			e.printStackTrace();
-		}
-	}
-
-	public void setText( String text )
-	{
-		this.text = text;
-	}
-
-	@Override
-	protected void draw( final Graphics2D g )
-	{
-		final AffineTransform2D t = new AffineTransform2D();
-
-		getCurrentTransform2D( t );
-
-		final double scale = t.get( 0, 0 );
-
-		double[] center = new double[ numDimensions ];
-
-		t.apply( position, center );
-
-		g.setColor( Color.GREEN );
-
-		final FontMetrics fontMetrics = setFont( g, textSize );
-
-		int[] stringPosition = getStringPosition( text, center, fontMetrics );
-
-		g.drawString( text, stringPosition[ 0 ], stringPosition[ 1 ] );
-
-	}
-
-	private int[] getStringPosition( String name, double[] center, FontMetrics fontMetrics )
-	{
-		int[] stringSize = getStringSize( name, fontMetrics );
-
-		int[] bdvWindowCentre = new int[ 2 ];
-		bdvWindowCentre[ 0 ] = ( int ) ( bdv.getBdvHandle().getViewerPanel().getWidth() / 2.0 );
-		bdvWindowCentre[ 1 ] = ( int ) ( bdv.getBdvHandle().getViewerPanel().getHeight() / 2.0 );
-
-		int[] stringPosition = new int[ numDimensions ];
-		for ( int d = 0; d < 2; ++d )
-		{
-			stringPosition[ d ] = ( int ) ( bdvWindowCentre[ d ] - 0.5 * stringSize[ d ] );
-		}
-		return stringPosition;
-	}
-
-	private int[] getStringSize( String name, FontMetrics fontMetrics )
-	{
-		int[] graphicsSize = new int[ numDimensions ];
-		graphicsSize[ 0 ] = fontMetrics.stringWidth( name );
-		graphicsSize[ 1 ] = fontMetrics.getHeight();
-		return graphicsSize;
-	}
-
-	private FontMetrics setFont( Graphics2D g, int fontSize )
-	{
-		g.setFont( new Font("TimesRoman", Font.PLAIN, fontSize ) );
-		return g.getFontMetrics( g.getFont() );
-	}
-
-}
diff --git a/src/main/java/de/embl/cba/mobie/utils/ui/DatasetsDialog.java b/src/main/java/de/embl/cba/mobie/utils/ui/DatasetsDialog.java
deleted file mode 100644
index eee172df2..000000000
--- a/src/main/java/de/embl/cba/mobie/utils/ui/DatasetsDialog.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package de.embl.cba.mobie.utils.ui;
-
-import ij.gui.GenericDialog;
-
-import java.util.ArrayList;
-
-public class DatasetsDialog
-{
-	public String showDialog( ArrayList< String > datasets )
-	{
-		final String[] array = asArray( datasets );
-		final GenericDialog gd = new GenericDialog( "Data set" );
-		gd.addChoice( "Data Version", array, array[ array.length - 1 ] );
-		gd.showDialog();
-		if ( gd.wasCanceled() ) return null;
-		else return gd.getNextChoice();
-	}
-
-	private String[] asArray( ArrayList< String > versions )
-	{
-		final String[] versionsArray = new String[ versions.size() ];
-		for ( int i = 0; i < versionsArray.length; i++ )
-			versionsArray[ i ] = versions.get( i );
-		return versionsArray;
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/view/View.java b/src/main/java/de/embl/cba/mobie/view/View.java
new file mode 100644
index 000000000..54bbf4db5
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/view/View.java
@@ -0,0 +1,59 @@
+package de.embl.cba.mobie.view;
+
+import de.embl.cba.mobie.display.SourceDisplay;
+import de.embl.cba.mobie.transform.BdvLocationSupplier;
+import de.embl.cba.mobie.transform.SourceTransformer;
+
+import java.util.List;
+
+public class View
+{
+	private String uiSelectionGroup;
+	private List< SourceDisplay > sourceDisplays;
+	private List< SourceTransformer > sourceTransforms;
+	private BdvLocationSupplier viewerTransform;
+	private boolean isExclusive = false;
+
+	public View( String uiSelectionGroup, List< SourceDisplay > sourceDisplays,
+				 List< SourceTransformer > sourceTransforms, BdvLocationSupplier viewerTransform,
+				 boolean isExclusive ) {
+		this.uiSelectionGroup = uiSelectionGroup;
+		this.sourceDisplays = sourceDisplays;
+		this.sourceTransforms = sourceTransforms;
+		this.viewerTransform = viewerTransform;
+		this.isExclusive = isExclusive;
+	}
+
+	public View( String uiSelectionGroup, List< SourceDisplay > sourceDisplays,
+				 List< SourceTransformer > sourceTransforms, boolean isExclusive ) {
+		this.uiSelectionGroup = uiSelectionGroup;
+		this.sourceDisplays = sourceDisplays;
+		this.sourceTransforms = sourceTransforms;
+		this.isExclusive = isExclusive;
+	}
+
+	public boolean isExclusive()
+	{
+		return isExclusive;
+	}
+
+	public List< SourceTransformer > getSourceTransforms()
+	{
+		return sourceTransforms;
+	}
+
+	public List< SourceDisplay > getSourceDisplays()
+	{
+		return sourceDisplays;
+	}
+
+	public String getUiSelectionGroup()
+	{
+		return uiSelectionGroup;
+	}
+
+	public BdvLocationSupplier getViewerTransform()
+	{
+		return viewerTransform;
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/de/embl/cba/mobie/view/ViewerManager.java b/src/main/java/de/embl/cba/mobie/view/ViewerManager.java
new file mode 100644
index 000000000..c4a2f9190
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/view/ViewerManager.java
@@ -0,0 +1,381 @@
+package de.embl.cba.mobie.view;
+
+import bdv.util.BdvHandle;
+import de.embl.cba.mobie.Constants;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.Utils;
+import de.embl.cba.mobie.bdv.ImageSliceView;
+import de.embl.cba.mobie.bdv.SegmentationImageSliceView;
+import de.embl.cba.mobie.bdv.SliceViewer;
+import de.embl.cba.mobie.color.ColoringModelHelper;
+import de.embl.cba.mobie.grid.GridOverlaySourceDisplay;
+import de.embl.cba.mobie.plot.ScatterPlotViewer;
+import de.embl.cba.mobie.segment.SegmentAdapter;
+import de.embl.cba.mobie.source.SegmentationSource;
+import de.embl.cba.mobie.display.ImageSourceDisplay;
+import de.embl.cba.mobie.display.SegmentationSourceDisplay;
+import de.embl.cba.mobie.display.SourceDisplay;
+import de.embl.cba.mobie.table.TableViewer;
+import de.embl.cba.mobie.transform.*;
+import de.embl.cba.mobie.ui.UserInterface;
+import de.embl.cba.mobie.ui.WindowArrangementHelper;
+import de.embl.cba.mobie.view.additionalviews.AdditionalViewsLoader;
+import de.embl.cba.mobie.view.saving.ViewsSaver;
+import de.embl.cba.mobie.volume.SegmentsVolumeView;
+import de.embl.cba.mobie.volume.UniverseManager;
+import de.embl.cba.tables.TableColumns;
+import de.embl.cba.tables.select.DefaultSelectionModel;
+import de.embl.cba.tables.tablerow.TableRowImageSegment;
+import de.embl.cba.tables.tablerow.TableRows;
+import ij.IJ;
+import net.imglib2.realtransform.AffineTransform3D;
+import sc.fiji.bdvpg.bdv.navigate.ViewerTransformAdjuster;
+
+
+import javax.activation.UnsupportedDataTypeException;
+import javax.swing.*;
+import java.awt.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static de.embl.cba.mobie.Utils.createAnnotatedImageSegmentsFromTableFile;
+import static de.embl.cba.mobie.ui.UserInterfaceHelper.setMoBIESwingLookAndFeel;
+import static de.embl.cba.mobie.ui.UserInterfaceHelper.resetSystemSwingLookAndFeel;
+
+public class ViewerManager
+{
+	private final MoBIE moBIE2;
+	private final UserInterface userInterface;
+	private final SliceViewer sliceViewer;
+	private ArrayList< SourceDisplay > sourceDisplays;
+	private final BdvHandle bdvHandle;
+	private GridOverlaySourceDisplay gridOverlayDisplay;
+	private final UniverseManager universeManager;
+	private final AdditionalViewsLoader additionalViewsLoader;
+	private final ViewsSaver viewsSaver;
+
+	public ViewerManager(MoBIE moBIE2, UserInterface userInterface, boolean is2D, int timepoints )
+	{
+		this.moBIE2 = moBIE2;
+		this.userInterface = userInterface;
+		sourceDisplays = new ArrayList<>();
+		sliceViewer = new SliceViewer( is2D, this, timepoints );
+		universeManager = new UniverseManager();
+		bdvHandle = sliceViewer.get();
+		additionalViewsLoader = new AdditionalViewsLoader( moBIE2 );
+		viewsSaver = new ViewsSaver( moBIE2 );
+	}
+
+	public static void initScatterPlotViewer( SegmentationSourceDisplay display )
+	{
+		display.scatterPlotViewer = new ScatterPlotViewer<>( display.segments, display.selectionModel, display.coloringModel, new String[]{ Constants.ANCHOR_X, Constants.ANCHOR_Y }, new double[]{1.0, 1.0}, 0.5 );
+		display.selectionModel.listeners().add( display.scatterPlotViewer );
+		display.coloringModel.listeners().add( display.scatterPlotViewer );
+		display.sliceViewer.getBdvHandle().getViewerPanel().addTimePointListener( display.scatterPlotViewer );
+
+		if ( display.showScatterPlot() )
+			display.scatterPlotViewer.show();
+	}
+
+	public static void initTableViewer( SegmentationSourceDisplay display  )
+	{
+		display.tableViewer = new TableViewer<>( display.segments, display.selectionModel, display.coloringModel, display.getName() ).show();
+		display.selectionModel.listeners().add( display.tableViewer );
+		display.coloringModel.listeners().add( display.tableViewer );
+	}
+
+	public ArrayList< SourceDisplay > getSourceDisplays()
+	{
+		return sourceDisplays;
+	}
+
+	public SliceViewer getSliceViewer()
+	{
+		return sliceViewer;
+	}
+
+	public AdditionalViewsLoader getAdditionalViewsLoader() { return additionalViewsLoader; }
+
+	public ViewsSaver getViewsSaver() { return viewsSaver; }
+
+	public View getCurrentView( String uiSelectionGroup, boolean isExclusive, boolean includeViewerTransform ) {
+
+		List< SourceDisplay > viewSourceDisplays = new ArrayList<>();
+		List< SourceTransformer > viewSourceTransforms = new ArrayList<>();
+
+		for ( SourceDisplay sourceDisplay : sourceDisplays ) {
+			SourceDisplay currentDisplay = null;
+			if ( sourceDisplay instanceof ImageSourceDisplay ) {
+				currentDisplay = new ImageSourceDisplay( (ImageSourceDisplay) sourceDisplay );
+			} else if ( sourceDisplay instanceof  SegmentationSourceDisplay ) {
+				currentDisplay = new SegmentationSourceDisplay( (SegmentationSourceDisplay) sourceDisplay );
+			}
+
+			if ( currentDisplay != null ) {
+				viewSourceDisplays.add( currentDisplay );
+			}
+
+			// TODO - would be good to pick up any manual transforms here too. This would allow e.g. manual placement
+			// of differing sized sources into a grid
+			if ( sourceDisplay.sourceTransformers != null ) {
+				for ( SourceTransformer sourceTransformer: sourceDisplay.sourceTransformers ) {
+					if ( !viewSourceTransforms.contains( sourceTransformer ) ) {
+						viewSourceTransforms.add( sourceTransformer );
+					}
+				}
+			}
+		}
+
+		if ( includeViewerTransform ) {
+			AffineTransform3D normalisedViewTransform = Utils.createNormalisedViewerTransform(bdvHandle, Utils.getMousePosition(bdvHandle));
+			BdvLocationSupplier viewerTransform = new BdvLocationSupplier(new BdvLocation(BdvLocationType.NormalisedViewerTransform, normalisedViewTransform.getRowPackedCopy()));
+			return new View(uiSelectionGroup, viewSourceDisplays, viewSourceTransforms, viewerTransform, isExclusive);
+		} else {
+			return new View(uiSelectionGroup, viewSourceDisplays, viewSourceTransforms, isExclusive);
+		}
+	}
+
+	public synchronized void show(View view )
+	{
+		if ( view.isExclusive() )
+		{
+			removeAllSourceDisplays();
+		}
+
+		setMoBIESwingLookAndFeel();
+
+		// show the displays if there are any
+		final List< SourceDisplay > sourceDisplays = view.getSourceDisplays();
+		if ( sourceDisplays != null )
+		{
+			for ( SourceDisplay sourceDisplay : sourceDisplays )
+			{
+				// TODO: why are there transforms done here and below...
+				sourceDisplay.sourceTransformers = view.getSourceTransforms();
+				showSourceDisplay( sourceDisplay );
+			}
+		}
+
+		// ...more source transforms here, feels wrong
+		createAndShowGridView( SwingUtilities.getWindowAncestor( sliceViewer.get().getViewerPanel() ), view.getSourceTransforms() );
+
+		resetSystemSwingLookAndFeel();
+
+		// adjust the viewer transform
+		if ( view.getViewerTransform() != null )
+		{
+			BdvLocationChanger.moveToLocation( bdvHandle, view.getViewerTransform().get() );
+		}
+		else
+		{
+			if ( view.isExclusive() || this.sourceDisplays.size() == 1 )
+			{
+				// focus on the image that was added last
+				final SourceDisplay sourceDisplay = this.sourceDisplays.get( this.sourceDisplays.size() - 1 );
+				new ViewerTransformAdjuster( bdvHandle, sourceDisplay.sourceAndConverters.get( 0 ) ).run();
+			}
+		}
+	}
+
+	private void showSourceDisplay( SourceDisplay sourceDisplay )
+	{
+		if ( sourceDisplays.contains( sourceDisplay ) ) return;
+
+		sourceDisplay.sliceViewer = sliceViewer;
+
+		if ( sourceDisplay instanceof ImageSourceDisplay )
+		{
+			showImageDisplay( ( ImageSourceDisplay ) sourceDisplay );
+		}
+		else if ( sourceDisplay instanceof SegmentationSourceDisplay )
+		{
+			final SegmentationSourceDisplay segmentationDisplay = ( SegmentationSourceDisplay ) sourceDisplay;
+			showSegmentationDisplay( segmentationDisplay );
+		}
+
+		userInterface.addSourceDisplay( sourceDisplay );
+		sourceDisplays.add( sourceDisplay );
+	}
+
+	private void createAndShowGridView( Window window, List< SourceTransformer > sourceTransformers )
+	{
+		int i = 0; // TODO: can there be more than one?
+
+		if ( sourceTransformers != null )
+		{
+			for ( SourceTransformer sourceTransformer : sourceTransformers )
+			{
+				if ( sourceTransformer instanceof GridSourceTransformer )
+				{
+					final String tableDataLocation = ( ( GridSourceTransformer ) sourceTransformer ).getTableDataLocation();
+
+					if ( tableDataLocation != null )
+					{
+						gridOverlayDisplay = new GridOverlaySourceDisplay( moBIE2, bdvHandle,  "grid-" + (i++), tableDataLocation, ( GridSourceTransformer ) sourceTransformer );
+
+						userInterface.addGridView( gridOverlayDisplay );
+						sourceDisplays.add( gridOverlayDisplay );
+
+						SwingUtilities.invokeLater( () ->
+						{
+							WindowArrangementHelper.bottomAlignWindow( window, gridOverlayDisplay.getTableViewer().getWindow() );
+						} );
+					}
+				}
+			}
+		}
+	}
+
+	private void removeAllSourceDisplays()
+	{
+		// create a copy of the currently shown displays...
+		final ArrayList< SourceDisplay > currentSourceDisplays = new ArrayList<>( sourceDisplays ) ;
+
+		// ...such that we can remove the displays without
+		// modifying the list that we iterate over
+		for ( SourceDisplay sourceDisplay : currentSourceDisplays )
+		{
+			// removes display from all viewers and
+			// also from the list of currently shown sourceDisplays
+			removeSourceDisplay( sourceDisplay );
+		}
+	}
+
+	private void showImageDisplay( ImageSourceDisplay imageDisplay )
+	{
+		imageDisplay.imageSliceView = new ImageSliceView( imageDisplay, bdvHandle, ( List< String > name ) -> moBIE2.openSourceAndConverters( name ) );
+	}
+
+	// TODO: own class: SegmentationDisplayConfigurator
+	private void showSegmentationDisplay( SegmentationSourceDisplay segmentationDisplay )
+	{
+		fetchSegmentsFromTables( segmentationDisplay );
+
+		segmentationDisplay.segmentAdapter = new SegmentAdapter( segmentationDisplay.segments );
+		ColoringModelHelper.configureMoBIEColoringModel( segmentationDisplay );
+		segmentationDisplay.selectionModel = new DefaultSelectionModel<>();
+		segmentationDisplay.coloringModel.setSelectionModel(  segmentationDisplay.selectionModel );
+
+		// set selected segments
+		if ( segmentationDisplay.getSelectedSegmentIds() != null )
+		{
+			final List< TableRowImageSegment > segments = segmentationDisplay.segmentAdapter.getSegments( segmentationDisplay.getSelectedSegmentIds() );
+			segmentationDisplay.selectionModel.setSelected( segments, true );
+		}
+
+		initSliceViewer( segmentationDisplay );
+		initTableViewer( segmentationDisplay );
+		initScatterPlotViewer( segmentationDisplay );
+		initVolumeViewer( segmentationDisplay );
+
+		SwingUtilities.invokeLater( () ->
+		{
+			WindowArrangementHelper.bottomAlignWindow( segmentationDisplay.sliceViewer.getWindow(), segmentationDisplay.tableViewer.getWindow() );
+		} );
+	}
+
+	private void fetchSegmentsFromTables( SegmentationSourceDisplay segmentationDisplay )
+	{
+		segmentationDisplay.segments = new ArrayList<>();
+
+		// load default tables
+		for ( String sourceName : segmentationDisplay.getSources() )
+		{
+			final SegmentationSource source = ( SegmentationSource ) moBIE2.getSource( sourceName );
+
+			segmentationDisplay.segments.addAll( createAnnotatedImageSegmentsFromTableFile(
+					moBIE2.getDefaultTablePath( source ),
+					sourceName ) );
+		}
+
+		// load additional tables
+		// TODO: This will not work like this for the grid view with multiple sources...
+		for ( String sourceName : segmentationDisplay.getSources() )
+		{
+			final SegmentationSource source = ( SegmentationSource ) moBIE2.getSource( sourceName );
+
+			final List< String > tables = segmentationDisplay.getTables();
+			if ( tables != null )
+			{
+				for ( String table : tables )
+				{
+					final String tablePath = moBIE2.getTablePath( source.tableDataLocation, table );
+					IJ.log( "Opening table:\n" + tablePath );
+					final Map< String, List< String > > newColumns =
+							TableColumns.openAndOrderNewColumns(
+									segmentationDisplay.segments,
+									Constants.SEGMENT_LABEL_ID,
+									tablePath );
+					newColumns.remove( Constants.SEGMENT_LABEL_ID );
+					for ( String columnName : newColumns.keySet() )
+					{
+						try
+						{
+							Object[] values = TableColumns.asTypedArray( newColumns.get( columnName ) );
+							TableRows.addColumn( segmentationDisplay.segments, columnName, values );
+						} catch ( UnsupportedDataTypeException e )
+						{
+							e.printStackTrace();
+						}
+					}
+				}
+			}
+		}
+	}
+
+	private void initSliceViewer( SegmentationSourceDisplay segmentationDisplay )
+	{
+		final SegmentationImageSliceView segmentationImageSliceView = new SegmentationImageSliceView<>( segmentationDisplay, bdvHandle, ( List< String > names ) -> moBIE2.openSourceAndConverters( names ) );
+		segmentationDisplay.segmentationImageSliceView = segmentationImageSliceView;
+	}
+
+	private void initVolumeViewer( SegmentationSourceDisplay display )
+	{
+		display.segmentsVolumeViewer = new SegmentsVolumeView<>( display.selectionModel, display.coloringModel, display.sourceAndConverters, universeManager );
+		display.segmentsVolumeViewer.showSegments( display.showSelectedSegmentsIn3d() );
+		display.coloringModel.listeners().add( display.segmentsVolumeViewer );
+		display.selectionModel.listeners().add( display.segmentsVolumeViewer );
+	}
+
+	public synchronized void removeSourceDisplay( SourceDisplay sourceDisplay )
+	{
+		if ( sourceDisplay instanceof SegmentationSourceDisplay )
+		{
+			final SegmentationSourceDisplay segmentationDisplay = ( SegmentationSourceDisplay ) sourceDisplay;
+			segmentationDisplay.segmentationImageSliceView.close();
+			segmentationDisplay.tableViewer.close();
+			segmentationDisplay.scatterPlotViewer.close();
+			segmentationDisplay.segmentsVolumeViewer.close();
+		}
+		else if ( sourceDisplay instanceof ImageSourceDisplay )
+		{
+			final ImageSourceDisplay imageDisplay = ( ImageSourceDisplay ) sourceDisplay;
+			imageDisplay.imageSliceView.close();
+		}
+		else if ( sourceDisplay instanceof GridOverlaySourceDisplay )
+		{
+			final GridOverlaySourceDisplay gridOverlayDisplay = ( GridOverlaySourceDisplay ) sourceDisplay;
+			gridOverlayDisplay.close();
+		}
+
+		userInterface.removeDisplaySettingsPanel( sourceDisplay );
+		sourceDisplays.remove( sourceDisplay );
+	}
+
+	public Collection< SegmentationSourceDisplay > getSegmentationDisplays()
+	{
+		final List< SegmentationSourceDisplay > segmentationDisplays = getSourceDisplays().stream().filter( s -> s instanceof SegmentationSourceDisplay ).map( s -> ( SegmentationSourceDisplay ) s ).collect( Collectors.toList() );
+
+		return segmentationDisplays;
+	}
+
+	public void close()
+	{
+		removeAllSourceDisplays();
+		sliceViewer.getBdvHandle().close();
+		universeManager.close();
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/view/additionalviews/AdditionalViews.java b/src/main/java/de/embl/cba/mobie/view/additionalviews/AdditionalViews.java
new file mode 100644
index 000000000..0f972075d
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/view/additionalviews/AdditionalViews.java
@@ -0,0 +1,9 @@
+package de.embl.cba.mobie.view.additionalviews;
+
+import de.embl.cba.mobie.view.View;
+
+import java.util.Map;
+
+public class AdditionalViews {
+    public Map< String, View> views;
+}
diff --git a/src/main/java/de/embl/cba/mobie/view/additionalviews/AdditionalViewsLoader.java b/src/main/java/de/embl/cba/mobie/view/additionalviews/AdditionalViewsLoader.java
new file mode 100644
index 000000000..37ffdb3a2
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/view/additionalviews/AdditionalViewsLoader.java
@@ -0,0 +1,42 @@
+package de.embl.cba.mobie.view.additionalviews;
+
+import de.embl.cba.mobie.MoBIESettings;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.serialize.AdditionalViewsJsonParser;
+import de.embl.cba.mobie.ui.UserInterfaceHelper;
+import de.embl.cba.mobie.view.View;
+
+import java.io.IOException;
+import java.util.Map;
+
+import static de.embl.cba.tables.FileUtils.selectPathFromProjectOrFileSystem;
+
+public class AdditionalViewsLoader {
+
+    private MoBIE moBIE2;
+    private MoBIESettings settings;
+
+    public AdditionalViewsLoader ( MoBIE moBIE2 ) {
+        this.moBIE2 = moBIE2;
+        this.settings = moBIE2.getSettings();
+    }
+
+    public void loadAdditionalViewsDialog() {
+        try {
+            String additionalViewsDirectory = moBIE2.getDatasetPath("misc", "views" );
+            String selectedFilePath = selectPathFromProjectOrFileSystem( additionalViewsDirectory, "View" );
+            // to match to the existing view selection panels, we enable the cross platform look and feel
+            UserInterfaceHelper.resetCrossPlatformSwingLookAndFeel();
+
+            if (selectedFilePath != null) {
+                Map< String, View> views = new AdditionalViewsJsonParser().getViews( selectedFilePath ).views;
+                moBIE2.getUserInterface().addViews( views );
+            }
+
+            UserInterfaceHelper.resetSystemSwingLookAndFeel();
+
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/src/main/java/de/embl/cba/mobie/view/saving/SelectExistingViewFrame.java b/src/main/java/de/embl/cba/mobie/view/saving/SelectExistingViewFrame.java
new file mode 100644
index 000000000..ba3fce5d4
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/view/saving/SelectExistingViewFrame.java
@@ -0,0 +1,158 @@
+package de.embl.cba.mobie.view.saving;
+
+import de.embl.cba.mobie.Dataset;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.view.View;
+import de.embl.cba.mobie.view.additionalviews.AdditionalViews;
+import de.embl.cba.tables.SwingUtils;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+
+import static de.embl.cba.mobie.projectcreator.ProjectCreatorHelper.getGroupToViewsMap;
+import static de.embl.cba.mobie.ui.SwingHelper.createButton;
+import static de.embl.cba.mobie.ui.SwingHelper.getJLabel;
+import static de.embl.cba.mobie.ui.UserInterfaceHelper.resetSystemSwingLookAndFeel;
+import static de.embl.cba.mobie.ui.UserInterfaceHelper.setMoBIESwingLookAndFeel;
+import static de.embl.cba.mobie.view.saving.ViewSavingHelpers.writeAdditionalViewsJson;
+import static de.embl.cba.mobie.view.saving.ViewSavingHelpers.writeDatasetJson;
+
+public class SelectExistingViewFrame extends JFrame {
+
+    private JComboBox<String> groupsComboBox;
+    private JComboBox<String> viewsComboBox;
+    private AdditionalViews additionalViews;
+    private Dataset dataset;
+    private View view;
+    private String jsonPath;
+    private ViewsSaver.ProjectSaveLocation saveLocation;
+    private Map<String, ArrayList<String>> groupToViewsMap;
+
+    // writing to dataset json
+    public SelectExistingViewFrame( Dataset dataset, View view, String jsonPath ) {
+        setMoBIESwingLookAndFeel();
+        this.dataset = dataset;
+        this.view = view;
+        this.jsonPath = jsonPath;
+        groupToViewsMap = getGroupToViewsMap(dataset);
+        createPanels();
+    }
+
+    // write to additional views json
+    public SelectExistingViewFrame( AdditionalViews additionalViews, View view, String jsonPath ) {
+        setMoBIESwingLookAndFeel();
+        this.additionalViews = additionalViews;
+        this.view = view;
+        this.jsonPath = jsonPath;
+        groupToViewsMap = getGroupToViewsMap(additionalViews);
+        createPanels();
+    }
+
+    private void createPanels() {
+        this.setTitle( "Choose an existing view..." );
+        this.getContentPane().setLayout( new BoxLayout(this.getContentPane(), BoxLayout.Y_AXIS ) );
+        this.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
+        createComboBoxes();
+        createAcceptPanel();
+        // reset swing laf when finished
+        this.addWindowListener( new WindowAdapter()
+        {
+            @Override
+            public void windowClosing(WindowEvent e)
+            {
+                resetSystemSwingLookAndFeel();
+                e.getWindow().dispose();
+            }
+        });
+        this.pack();
+        this.setLocationRelativeTo(null);
+        this.setVisible( true );
+    }
+
+    private void createComboBoxes() {
+        groupsComboBox = new JComboBox<>( groupToViewsMap.keySet().toArray(new String[0]) );
+        String selectedGroup = (String) groupsComboBox.getSelectedItem();
+        viewsComboBox = new JComboBox<>( groupToViewsMap.get( selectedGroup ).toArray( new String[0] ) );
+        setComboBoxDimensions( groupsComboBox );
+        setComboBoxDimensions( viewsComboBox );
+        groupsComboBox.addItemListener( new SyncGroupAndViewComboBox() );
+
+        JPanel groupPanel = SwingUtils.horizontalLayoutPanel();
+        groupPanel.add(getJLabel("Ui selection group", 120, 10));
+        groupPanel.add( groupsComboBox );
+        JPanel viewPanel = SwingUtils.horizontalLayoutPanel();
+        viewPanel.add(getJLabel("View name", 120, 10));
+        viewPanel.add( viewsComboBox );
+
+        this.getContentPane().add( groupPanel );
+        this.getContentPane().add( viewPanel );
+    }
+
+    private void createAcceptPanel() {
+        JPanel acceptPanel = SwingUtils.horizontalLayoutPanel();
+        JButton selectButton = createButton("Select");
+        selectButton.addActionListener( e ->
+        {
+            new Thread( () -> {
+                this.setVisible( false );
+                String selectedView = (String) viewsComboBox.getSelectedItem();
+                if ( dataset != null ) {
+                    try {
+                        writeDatasetJson( dataset, view, selectedView, jsonPath );
+                    } catch (IOException ioException) {
+                        ioException.printStackTrace();
+                    }
+                } else if ( additionalViews != null ) {
+                    try {
+                        writeAdditionalViewsJson( additionalViews, view, selectedView, jsonPath );
+                    } catch (IOException ioException) {
+                        ioException.printStackTrace();
+                    }
+                }
+                this.dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING));
+
+            } ).start();
+        } );
+        JButton cancelButton = createButton("Cancel");
+        cancelButton.addActionListener( e ->
+        {
+            new Thread( () -> {
+                this.dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING));
+            } ).start();
+        } );
+        acceptPanel.add( selectButton );
+        acceptPanel.add( cancelButton );
+        this.getContentPane().add( acceptPanel );
+    }
+
+
+    private void setComboBoxDimensions( JComboBox< String > comboBox )
+    {
+        comboBox.setPrototypeDisplayValue( MoBIE.PROTOTYPE_DISPLAY_VALUE );
+        comboBox.setPreferredSize( new Dimension( 200, 20 ) );
+        comboBox.setMaximumSize( new Dimension( 200, 20 ) );
+    }
+
+    private class SyncGroupAndViewComboBox implements ItemListener {
+        @Override
+        public void itemStateChanged(ItemEvent event) {
+            if (event.getStateChange() == ItemEvent.SELECTED) {
+                updateViewsComboBox();
+            }
+        }
+    }
+
+    private void updateViewsComboBox() {
+        viewsComboBox.removeAllItems();
+        for ( String viewName: groupToViewsMap.get( groupsComboBox.getSelectedItem() ) ) {
+            viewsComboBox.addItem( viewName );
+        }
+    }
+}
diff --git a/src/main/java/de/embl/cba/mobie/view/saving/ViewSavingHelpers.java b/src/main/java/de/embl/cba/mobie/view/saving/ViewSavingHelpers.java
new file mode 100644
index 000000000..7e4003124
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/view/saving/ViewSavingHelpers.java
@@ -0,0 +1,39 @@
+package de.embl.cba.mobie.view.saving;
+
+import de.embl.cba.mobie.Dataset;
+import de.embl.cba.mobie.serialize.AdditionalViewsJsonParser;
+import de.embl.cba.mobie.serialize.DatasetJsonParser;
+import de.embl.cba.mobie.view.View;
+import de.embl.cba.mobie.view.additionalviews.AdditionalViews;
+import de.embl.cba.tables.github.GitHubUtils;
+
+import java.io.IOException;
+
+import static de.embl.cba.tables.FileUtils.isGithub;
+
+public class ViewSavingHelpers {
+    public static void writeDatasetJson( Dataset dataset, View view, String viewName, String datasetJsonPath ) throws IOException {
+        if ( viewName != null ) {
+            dataset.views.put(viewName, view);
+
+            if (isGithub(datasetJsonPath)) {
+                new ViewsGithubWriter(GitHubUtils.rawUrlToGitLocation(datasetJsonPath)).writeViewToDatasetJson(viewName, view);
+            } else {
+                new DatasetJsonParser().saveDataset(dataset, datasetJsonPath);
+            }
+        }
+    }
+
+    public static void writeAdditionalViewsJson( AdditionalViews additionalViews, View view, String viewName,
+                                                 String additionalViewsJsonPath ) throws IOException {
+        if ( viewName != null ) {
+            additionalViews.views.put(viewName, view);
+
+            if (isGithub( additionalViewsJsonPath )) {
+                new ViewsGithubWriter( GitHubUtils.rawUrlToGitLocation( additionalViewsJsonPath ) ).writeViewToViewsJson( viewName, view );
+            } else {
+                new AdditionalViewsJsonParser().saveViews(additionalViews, additionalViewsJsonPath );
+            }
+        }
+    }
+}
diff --git a/src/main/java/de/embl/cba/mobie/view/saving/ViewsGithubWriter.java b/src/main/java/de/embl/cba/mobie/view/saving/ViewsGithubWriter.java
new file mode 100644
index 000000000..49a8928b2
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/view/saving/ViewsGithubWriter.java
@@ -0,0 +1,170 @@
+package de.embl.cba.mobie.view.saving;
+
+import com.google.gson.GsonBuilder;
+import com.google.gson.internal.LinkedTreeMap;
+import de.embl.cba.mobie.Dataset;
+import de.embl.cba.mobie.serialize.AdditionalViewsJsonParser;
+import de.embl.cba.mobie.serialize.DatasetJsonParser;
+import de.embl.cba.mobie.view.View;
+import de.embl.cba.mobie.view.additionalviews.AdditionalViews;
+import de.embl.cba.tables.github.GitHubContentGetter;
+import de.embl.cba.tables.github.GitHubFileCommitter;
+import de.embl.cba.tables.github.GitLocation;
+import ij.IJ;
+import ij.Prefs;
+import ij.gui.GenericDialog;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+public class ViewsGithubWriter {
+    public static final String ACCESS_TOKEN = "MoBIE.GitHub access token";
+    private String accessToken;
+    private GitLocation viewJsonGitLocation;
+
+    ViewsGithubWriter( GitLocation viewJsonGitLocation ) {
+        this.viewJsonGitLocation = viewJsonGitLocation;
+
+        // Git location should be a path to a json file, so remove any trailing /
+        String path = this.viewJsonGitLocation.path;
+        if ( path.endsWith("/") ) {
+            this.viewJsonGitLocation.path = path.substring(0, path.length() - 1);
+        }
+    }
+
+    private static String writeAdditionalViewsToBase64String ( AdditionalViews additionalViews ) {
+        String jsonString = new AdditionalViewsJsonParser().viewsToJsonString( additionalViews, true );
+        jsonString += "\n";
+        byte[] jsonBytes = jsonString.getBytes( StandardCharsets.UTF_8);
+        return Base64.getEncoder().encodeToString(jsonBytes);
+    }
+
+    private static String writeDatasetToBase64String ( Dataset dataset ) {
+        String jsonString = new DatasetJsonParser().datasetToJsonString( dataset, true );
+        jsonString += "\n";
+        byte[] jsonBytes = jsonString.getBytes( StandardCharsets.UTF_8);
+        return Base64.getEncoder().encodeToString(jsonBytes);
+    }
+
+    class FilePathAndSha {
+        String filePath;
+        String sha;
+    }
+
+    private FilePathAndSha getFilePathAndSha ( String content ) {
+        GsonBuilder builder = new GsonBuilder();
+        FilePathAndSha filePathAndSha = new FilePathAndSha();
+
+        LinkedTreeMap linkedTreeMap = ( LinkedTreeMap ) builder.create().fromJson( content, Object.class );
+        filePathAndSha.filePath = ( String ) linkedTreeMap.get( "download_url" );
+        filePathAndSha.sha = (String) linkedTreeMap.get( "sha" );
+        return filePathAndSha;
+    }
+
+    private boolean showDialog()
+    {
+        final GenericDialog gd = new GenericDialog( "Save to github" );
+
+        gd.addMessage( "To save directly to your github project, you will need a personal \n access token and push rights to the repository" );
+        gd.addStringField( "GitHub access token", Prefs.get( ACCESS_TOKEN, "1234567890" ));
+        gd.showDialog();
+
+        if ( gd.wasCanceled() ) return false;
+
+        accessToken = gd.getNextString();
+
+        Prefs.set( ACCESS_TOKEN, accessToken );
+
+        return true;
+    }
+
+
+    public void overwriteExistingFile( FilePathAndSha filePathAndSha, String jsonBase64String ) {
+        final GitHubFileCommitter fileCommitter = new GitHubFileCommitter(
+                    viewJsonGitLocation.repoUrl, accessToken, viewJsonGitLocation.branch,
+                    viewJsonGitLocation.path, filePathAndSha.sha );
+        fileCommitter.commitStringAsFile("Add new views from UI", jsonBase64String );
+    }
+
+    public void writeNewFile( String jsonBase64String ) {
+        final GitHubFileCommitter fileCommitter = new GitHubFileCommitter(
+                    viewJsonGitLocation.repoUrl, accessToken, viewJsonGitLocation.branch,
+                    viewJsonGitLocation.path );
+        fileCommitter.commitStringAsFile("Add new views from UI", jsonBase64String );
+    }
+
+    public void writeViewToDatasetJson( String viewName, View view ) {
+        if ( showDialog() ) {
+            if ( viewJsonGitLocation.path.endsWith( "dataset.json") ) {
+                final GitHubContentGetter contentGetter =
+                        new GitHubContentGetter(viewJsonGitLocation.repoUrl, viewJsonGitLocation.path, viewJsonGitLocation.branch, null);
+
+                Dataset dataset;
+                try {
+                    String content = contentGetter.getContent();
+                    if (content != null) {
+                        FilePathAndSha filePathAndSha = getFilePathAndSha(content);
+                        dataset = new DatasetJsonParser().parseDataset(filePathAndSha.filePath);
+                        dataset.views.put(viewName, view);
+
+                        final String datasetBase64String = writeDatasetToBase64String(dataset);
+                        overwriteExistingFile(filePathAndSha, datasetBase64String);
+                    }
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            } else {
+                IJ.log( "Aborting saving view to github - path is not a dataset.json" );
+            }
+        }
+    }
+
+    public boolean jsonExists() {
+        final GitHubContentGetter contentGetter =
+                new GitHubContentGetter(viewJsonGitLocation.repoUrl, viewJsonGitLocation.path, viewJsonGitLocation.branch, null);
+        int responseCode = contentGetter.getContentResponseCode();
+        if ( responseCode == 404 ) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    public void writeViewToViewsJson( String viewName, View view ) {
+        if ( showDialog() ) {
+            if ( viewJsonGitLocation.path.endsWith( ".json" )) {
+                AdditionalViews additionalViews;
+                // if 404, then file doesn't exist, so make new one
+                if ( !jsonExists() ) {
+                    additionalViews = new AdditionalViews();
+                    additionalViews.views = new HashMap<>();
+                    additionalViews.views.put(viewName, view);
+
+                    final String additionalViewsBase64String = writeAdditionalViewsToBase64String(additionalViews);
+                    writeNewFile(additionalViewsBase64String);
+                    // otherwise, append to file
+                } else {
+                    try {
+                        final GitHubContentGetter contentGetter =
+                                new GitHubContentGetter(viewJsonGitLocation.repoUrl, viewJsonGitLocation.path, viewJsonGitLocation.branch, null);
+                        String content = contentGetter.getContent();
+                        if (content != null) {
+                            FilePathAndSha filePathAndSha = getFilePathAndSha(content);
+                            additionalViews = new AdditionalViewsJsonParser().getViews(filePathAndSha.filePath);
+                            additionalViews.views.put(viewName, view);
+
+                            final String additionalViewsBase64String = writeAdditionalViewsToBase64String(additionalViews);
+                            overwriteExistingFile(filePathAndSha, additionalViewsBase64String);
+                        }
+                    } catch (IOException e) {
+                        e.printStackTrace();
+                    }
+                }
+            } else {
+                IJ.log( "Aborting saving view to github - path is not a .json" );
+            }
+        }
+    }
+
+}
diff --git a/src/main/java/de/embl/cba/mobie/view/saving/ViewsSaver.java b/src/main/java/de/embl/cba/mobie/view/saving/ViewsSaver.java
new file mode 100644
index 000000000..bb23750f1
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/view/saving/ViewsSaver.java
@@ -0,0 +1,331 @@
+package de.embl.cba.mobie.view.saving;
+
+import de.embl.cba.mobie.MoBIESettings;
+import de.embl.cba.mobie.Dataset;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.serialize.AdditionalViewsJsonParser;
+import de.embl.cba.mobie.serialize.DatasetJsonParser;
+import de.embl.cba.mobie.view.View;
+import de.embl.cba.mobie.view.additionalviews.AdditionalViews;
+import de.embl.cba.tables.FileUtils;
+import de.embl.cba.tables.github.GitHubUtils;
+import ij.IJ;
+import ij.gui.GenericDialog;
+import org.apache.commons.io.FilenameUtils;
+
+import javax.swing.*;
+import javax.swing.filechooser.FileNameExtensionFilter;
+import java.io.*;
+import java.util.Arrays;
+import java.util.HashMap;
+
+import static de.embl.cba.mobie.projectcreator.ProjectCreatorHelper.makeNewUiSelectionGroup;
+import static de.embl.cba.mobie.ui.UserInterfaceHelper.tidyString;
+import static de.embl.cba.mobie.view.saving.ViewSavingHelpers.writeAdditionalViewsJson;
+import static de.embl.cba.mobie.view.saving.ViewSavingHelpers.writeDatasetJson;
+import static de.embl.cba.tables.FileUtils.*;
+
+public class ViewsSaver {
+
+    private MoBIE moBIE2;
+    private MoBIESettings settings;
+
+    enum ProjectSaveLocation {
+        datasetJson,
+        viewsJson
+    }
+
+    enum SaveMethod {
+        saveAsNewView,
+        overwriteExistingView
+    }
+
+    public ViewsSaver(MoBIE moBIE2) {
+        this.moBIE2 = moBIE2;
+        this.settings = moBIE2.getSettings();
+    }
+
+    public void saveCurrentSettingsAsViewDialog() {
+        final GenericDialog gd = new GenericDialog("Save current view");
+
+        gd.addChoice("Save to", new String[]{ FileUtils.FileLocation.Project.toString(),
+                FileUtils.FileLocation.FileSystem.toString()}, FileUtils.FileLocation.Project.toString());
+
+        String[] currentUiSelectionGroups = moBIE2.getUserInterface().getUISelectionGroupNames();
+        String[] choices = new String[currentUiSelectionGroups.length + 1];
+        choices[0] = "Make New Ui Selection Group";
+        for (int i = 0; i < currentUiSelectionGroups.length; i++) {
+            choices[i + 1] = currentUiSelectionGroups[i];
+        }
+        gd.addChoice("Ui Selection Group", choices, choices[0]);
+
+        gd.addCheckbox("exclusive", true);
+        gd.addCheckbox("Include viewer transform?", true );
+        gd.showDialog();
+
+        if (!gd.wasCanceled()) {
+            FileUtils.FileLocation fileLocation = FileUtils.FileLocation.valueOf(gd.getNextChoice());
+            String uiSelectionGroup = gd.getNextChoice();
+            boolean exclusive = gd.getNextBoolean();
+            boolean includeViewerTransform = gd.getNextBoolean();
+
+            if (uiSelectionGroup.equals("Make New Ui Selection Group")) {
+                uiSelectionGroup = makeNewUiSelectionGroup(currentUiSelectionGroups);
+            }
+
+            if (uiSelectionGroup != null) {
+                if (fileLocation == FileUtils.FileLocation.Project) {
+                    saveToProject( uiSelectionGroup, exclusive, includeViewerTransform );
+                } else {
+                    saveToFileSystem( uiSelectionGroup, exclusive, includeViewerTransform );
+                }
+            }
+        }
+    }
+
+    private void saveToFileSystem( String uiSelectionGroup, boolean exclusive, boolean includeViewerTransform ) {
+        String jsonPath = null;
+        final JFileChooser jFileChooser = new JFileChooser();
+        jFileChooser.setFileFilter(new FileNameExtensionFilter("json", "json"));
+        if (jFileChooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
+            jsonPath = jFileChooser.getSelectedFile().getAbsolutePath();
+        }
+
+        if (jsonPath != null) {
+            if (!jsonPath.endsWith(".json")) {
+                jsonPath += ".json";
+            }
+
+            View currentView = moBIE2.getViewerManager().getCurrentView(uiSelectionGroup, exclusive, includeViewerTransform);
+            try {
+                saveToAdditionalViewsJson( currentView, jsonPath );
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    private void saveToProject( String uiSelectionGroup, boolean exclusive, boolean includeViewerTransform ) {
+        if ( isS3(settings.values.getProjectLocation()) ) {
+            // TODO - support saving views to s3?
+            IJ.log("View saving aborted - saving directly to s3 is not yet supported!");
+        } else {
+            ProjectSaveLocation projectSaveLocation = chooseProjectSaveLocationDialog();
+            if (projectSaveLocation != null) {
+                View currentView = moBIE2.getViewerManager().getCurrentView(uiSelectionGroup, exclusive, includeViewerTransform);
+
+                try {
+                    if (projectSaveLocation == ProjectSaveLocation.datasetJson) {
+                        saveToDatasetJson( currentView );
+                    } else {
+                        String viewJsonPath = chooseAdditionalViewsJson();
+                        if (viewJsonPath != null) {
+                            saveToAdditionalViewsJson( currentView, viewJsonPath );
+                        }
+                    }
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    private void saveToDatasetJson( View view ) throws IOException {
+        String datasetJsonPath = moBIE2.getDatasetPath( "dataset.json");
+        Dataset dataset = new DatasetJsonParser().parseDataset( datasetJsonPath );
+
+        String viewName;
+        if ( dataset.views.keySet().size() > 0 ) {
+            SaveMethod saveMethod = chooseSaveMethodDialog();
+            if ( saveMethod != null ) {
+                switch( saveMethod ) {
+                    case saveAsNewView:
+                        viewName = chooseNewViewNameDialog();
+                        if ( viewName != null && dataset.views.containsKey( viewName ) ) {
+                            IJ.log( "View saving aborted - this view name already exists!" );
+                        } else {
+                            writeDatasetJson( dataset, view, viewName, datasetJsonPath );
+                        }
+                        break;
+                    case overwriteExistingView:
+                        new SelectExistingViewFrame( dataset, view, datasetJsonPath );
+                        break;
+                }
+            }
+        } else {
+            viewName = chooseNewViewNameDialog();
+            writeDatasetJson( dataset, view, viewName, datasetJsonPath );
+        }
+
+    }
+
+    private void saveToExistingViewsJson( View view, String jsonPath ) throws IOException {
+        AdditionalViews additionalViews = new AdditionalViewsJsonParser().getViews( jsonPath );
+        SaveMethod saveMethod = chooseSaveMethodDialog();
+        if ( saveMethod != null ) {
+            switch( saveMethod ) {
+                case saveAsNewView:
+                    String viewName = chooseNewViewNameDialog();
+                    if ( viewName != null && additionalViews.views.containsKey( viewName ) ) {
+                        IJ.log( "View saving aborted - this view name already exists!" );
+                    } else {
+                        writeAdditionalViewsJson( additionalViews, view, viewName, jsonPath );
+                    }
+                    break;
+                case overwriteExistingView:
+                    new SelectExistingViewFrame( additionalViews, view, jsonPath );
+                    break;
+            }
+        }
+    }
+
+    private void saveToAdditionalViewsJson( View view, String jsonPath ) throws IOException {
+
+        boolean jsonExists;
+        if ( isGithub( jsonPath )) {
+            jsonExists = new ViewsGithubWriter( GitHubUtils.rawUrlToGitLocation(jsonPath) ).jsonExists();
+        } else {
+            jsonExists = new File( jsonPath ).exists();
+        }
+
+        String viewName;
+        if ( jsonExists ) {
+            saveToExistingViewsJson( view, jsonPath );
+        } else {
+            viewName = chooseNewViewNameDialog();
+            AdditionalViews additionalViews = new AdditionalViews();
+            additionalViews.views = new HashMap<>();
+            writeAdditionalViewsJson( additionalViews, view, viewName, jsonPath );
+        }
+    }
+
+    private String chooseAdditionalViewsJson() {
+        String additionalViewsDirectory = moBIE2.getDatasetPath( "misc", "views");
+        String[] existingViewFiles = getFileNamesFromProject(additionalViewsDirectory);
+
+        String jsonFileName;
+        if ( existingViewFiles != null && existingViewFiles.length > 0 ) {
+            jsonFileName = chooseViewsJsonDialog( existingViewFiles );
+        } else {
+            jsonFileName = makeNewViewFile( existingViewFiles );
+        }
+
+        if ( jsonFileName != null ) {
+            return moBIE2.getDatasetPath( "misc", "views", jsonFileName);
+        } else {
+            return null;
+        }
+    }
+
+    private String chooseViewsJsonDialog(String[] viewFileNames) {
+        final GenericDialog gd = new GenericDialog("Choose views json");
+        String[] choices = new String[viewFileNames.length + 1];
+        choices[0] = "Make new views json file";
+        for (int i = 0; i < viewFileNames.length; i++) {
+            choices[i + 1] = viewFileNames[i];
+        }
+
+        gd.addChoice("Choose Views Json:", choices, choices[0]);
+        gd.showDialog();
+
+        if (!gd.wasCanceled()) {
+            String choice = gd.getNextChoice();
+            if (choice.equals("Make new views json file")) {
+                choice = makeNewViewFile( viewFileNames );
+            }
+            return choice;
+        } else {
+            return null;
+        }
+
+    }
+
+    private String makeNewViewFile( String[] existingViewFiles ) {
+        String viewFileName = chooseNewViewsFileNameDialog();
+
+        // get rid of any spaces, warn for unusual characters in basename (without the .json)
+        if ( viewFileName!= null ) {
+            viewFileName = tidyString( viewFileName);
+        }
+
+        if ( viewFileName != null ) {
+            viewFileName += ".json";
+            boolean alreadyExists = Arrays.asList( existingViewFiles ).contains( viewFileName );
+            if ( alreadyExists ) {
+                viewFileName = null;
+                IJ.log("Saving view aborted - new view file already exists");
+            }
+        }
+
+        return viewFileName;
+    }
+
+    private String chooseNewViewsFileNameDialog() {
+        final GenericDialog gd = new GenericDialog("Choose views json filename");
+        gd.addStringField("New view json filename:", "", 25 );
+        gd.showDialog();
+
+        if (!gd.wasCanceled()) {
+            String viewFileName =  gd.getNextString();
+
+            // we just want the basename, no extension
+            if ( viewFileName != null && viewFileName.endsWith(".json") ) {
+                viewFileName = FilenameUtils.removeExtension( viewFileName );
+            }
+            return viewFileName;
+        } else {
+            return null;
+        }
+
+    }
+
+    private SaveMethod chooseSaveMethodDialog() {
+        final GenericDialog gd = new GenericDialog("How to save?");
+        String[] choices = new String[]{ "Save as new view", "Overwrite existing view" };
+        gd.addChoice("Save method:", choices, choices[0] );
+        gd.showDialog();
+
+        if (!gd.wasCanceled()) {
+            String saveMethod = gd.getNextChoice();
+            if ( saveMethod.equals("Save as new view") ) {
+                return SaveMethod.saveAsNewView;
+            } else if ( saveMethod.equals( "Overwrite existing view" )) {
+                return SaveMethod.overwriteExistingView;
+            }
+        }
+
+        return null;
+    }
+
+    public String chooseNewViewNameDialog() {
+        final GenericDialog gd = new GenericDialog("Choose view name:");
+
+        gd.addStringField("View name:", "", 25 );
+        gd.showDialog();
+
+        if (!gd.wasCanceled()) {
+            return tidyString( gd.getNextString() );
+            // TODO - check if already exists
+        } else {
+            return null;
+        }
+    }
+
+    private ProjectSaveLocation chooseProjectSaveLocationDialog() {
+        final GenericDialog gd = new GenericDialog("Save location");
+        String[] choices = new String[]{"dataset.json", "views.json"};
+        gd.addChoice("Save location:", choices, choices[0]);
+        gd.showDialog();
+
+        if (!gd.wasCanceled()) {
+            String projectSaveLocation = gd.getNextChoice();
+            if (projectSaveLocation.equals("dataset.json")) {
+                return ProjectSaveLocation.datasetJson;
+            } else if (projectSaveLocation.equals("views.json")) {
+                return ProjectSaveLocation.viewsJson;
+            }
+        }
+        return null;
+    }
+
+}
diff --git a/src/main/java/de/embl/cba/mobie/volume/SegmentsVolumeView.java b/src/main/java/de/embl/cba/mobie/volume/SegmentsVolumeView.java
new file mode 100644
index 000000000..51204a6a2
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/volume/SegmentsVolumeView.java
@@ -0,0 +1,492 @@
+/*-
+ * #%L
+ * Various Java code for ImageJ
+ * %%
+ * Copyright (C) 2018 - 2021 EMBL
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+package de.embl.cba.mobie.volume;
+
+import bdv.viewer.Source;
+import bdv.viewer.SourceAndConverter;
+import customnode.CustomTriangleMesh;
+import de.embl.cba.mobie.VisibilityListener;
+import de.embl.cba.mobie.mesh.MeshCreator;
+import de.embl.cba.tables.color.ColorUtils;
+import de.embl.cba.tables.color.ColoringListener;
+import de.embl.cba.tables.color.ColoringModel;
+import de.embl.cba.tables.ij3d.AnimatedViewAdjuster;
+import de.embl.cba.tables.imagesegment.ImageSegment;
+import de.embl.cba.tables.select.SelectionListener;
+import de.embl.cba.tables.select.SelectionModel;
+import ij3d.Content;
+import ij3d.Image3DUniverse;
+import ij3d.UniverseListener;
+import net.imglib2.type.numeric.ARGBType;
+import net.imglib2.type.numeric.RealType;
+import org.scijava.java3d.View;
+import org.scijava.vecmath.Color3f;
+
+import java.awt.*;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class SegmentsVolumeView< S extends ImageSegment > implements ColoringListener, SelectionListener< S >
+{
+	private final SelectionModel< S > selectionModel;
+	private final ColoringModel< S > coloringModel;
+	private final Collection< SourceAndConverter< ? > > sourceAndConverters;
+	private final UniverseManager universeManager;
+
+	private S recentFocus;
+	private ConcurrentHashMap< S, Content > segmentToContent;
+	private ConcurrentHashMap< Content, S > contentToSegment;
+	private double transparency;
+	private int meshSmoothingIterations;
+	private int segmentFocusAnimationDurationMillis;
+	private double segmentFocusZoomLevel;
+	private double segmentFocusDxyMin;
+	private double segmentFocusDzMin;
+	private long maxNumSegmentVoxels;
+	private String objectsName;
+	private boolean showSegments = false;
+	private double voxelSpacing = 0; // 0 = auto
+	private int currentTimePoint = 0;
+	private final MeshCreator< ImageSegment > meshCreator;
+	private List< VisibilityListener > listeners = new ArrayList<>(  );
+	private Window window;
+	private Image3DUniverse universe;
+
+	public SegmentsVolumeView(
+			final SelectionModel< S > selectionModel,
+			final ColoringModel< S > coloringModel,
+			final Collection< SourceAndConverter< ? > > sourceAndConverters,
+			UniverseManager universeManager )
+	{
+		this.selectionModel = selectionModel;
+		this.coloringModel = coloringModel;
+		this.sourceAndConverters = sourceAndConverters;
+		this.universeManager = universeManager;
+
+		this.transparency = 0.0;
+		this.meshSmoothingIterations = 5;
+		this.segmentFocusAnimationDurationMillis = 750;
+		this.segmentFocusZoomLevel = 0.8;
+		this.segmentFocusDxyMin = 20.0;
+		this.segmentFocusDzMin = 20.0;
+		this.maxNumSegmentVoxels = 100 * 100 * 100;
+		this.objectsName = "";
+		this.segmentToContent = new ConcurrentHashMap<>();
+		this.contentToSegment = new ConcurrentHashMap<>();
+
+		this.meshCreator = new MeshCreator<>( meshSmoothingIterations, maxNumSegmentVoxels );
+	}
+
+	public void setObjectsName( String objectsName )
+	{
+		if ( objectsName == null )
+			throw new RuntimeException( "Cannot set objects name in Segments3dView to null." );
+
+		this.objectsName = objectsName;
+	}
+
+	public void setTransparency( double transparency )
+	{
+		this.transparency = transparency;
+	}
+
+	public void setMeshSmoothingIterations( int iterations )
+	{
+		this.meshSmoothingIterations = iterations;
+	}
+
+	public void setSegmentFocusAnimationDurationMillis( int duration )
+	{
+		this.segmentFocusAnimationDurationMillis = duration;
+	}
+
+	public void setSegmentFocusZoomLevel( double segmentFocusZoomLevel )
+	{
+		this.segmentFocusZoomLevel = segmentFocusZoomLevel;
+	}
+
+	public void setSegmentFocusDxyMin( double segmentFocusDxyMin )
+	{
+		this.segmentFocusDxyMin = segmentFocusDxyMin;
+	}
+
+	public void setSegmentFocusDzMin( double segmentFocusDzMin )
+	{
+		this.segmentFocusDzMin = segmentFocusDzMin;
+	}
+
+	public void setMaxNumSegmentVoxels( long maxNumSegmentVoxels )
+	{
+		this.maxNumSegmentVoxels = maxNumSegmentVoxels;
+	}
+
+	private void updateSegmentColors()
+	{
+		for ( S segment : segmentToContent.keySet() )
+		{
+			final Color3f color3f = getColor3f( segment );
+			final Content content = segmentToContent.get( segment );
+			content.setColor( color3f );
+		}
+	}
+
+	private synchronized void updateView( boolean recomputeMeshes )
+	{
+		// TODO: It feels that below functions should be merged...
+		updateSelectedSegments( recomputeMeshes );
+		removeUnselectedSegments();
+	}
+
+	private void removeUnselectedSegments( )
+	{
+		final Set< S > selectedSegments = selectionModel.getSelected();
+		final Set< S > currentSegments = segmentToContent.keySet();
+		final Set< S > remove = new HashSet<>();
+
+		for ( S segment : currentSegments )
+			if ( ! selectedSegments.contains( segment ) )
+				remove.add( segment );
+
+		for( S segment : remove )
+			removeSegment( segment );
+	}
+
+	private synchronized void updateSelectedSegments( boolean recomputeMeshes )
+	{
+		final Set< S > selected = selectionModel.getSelected();
+
+		for ( S segment : selected )
+		{
+			if ( segment.timePoint() == currentTimePoint )
+			{
+				if ( recomputeMeshes ) removeSegment( segment );
+
+				if ( ! segmentToContent.containsKey( segment ) )
+				{
+					final Source< ? extends RealType< ? > > source = getSource( segment );
+					final CustomTriangleMesh mesh = meshCreator.createSmoothCustomTriangleMesh( segment, voxelSpacing, recomputeMeshes, source );
+					mesh.setColor( getColor3f( segment ) );
+					addSegmentMeshToUniverse( segment, mesh );
+				}
+			}
+			else // segment is of another time point
+			{
+				removeSegment( segment );
+			}
+		}
+	}
+
+	private Source< ? extends RealType< ? > > getSource( S segment )
+	{
+		for ( SourceAndConverter< ? > sourceAndConverter : sourceAndConverters )
+		{
+			if ( sourceAndConverter.getSpimSource().getName().equals( segment.imageId() ))
+			{
+				return ( Source< ? extends RealType< ? > > ) sourceAndConverter.getSpimSource();
+			}
+		}
+
+		throw new UnsupportedOperationException( "An image segment from " + segment.imageId() + " did not have a corresponding image source."  );
+	}
+
+	private synchronized void removeSegment( S segment )
+	{
+		final Content content = segmentToContent.get( segment );
+		universe.removeContent( content.getName() );
+		segmentToContent.remove( segment );
+		contentToSegment.remove( content );
+	}
+
+	private String getSegmentIdentifier( S segment )
+	{
+		return segment.labelId() + "-" + segment.timePoint();
+	}
+
+	public synchronized void showSegments( boolean showSegments )
+	{
+		if ( showSegments && universe == null )
+		{
+			universe = universeManager.get();
+			window = universe.getWindow();
+			window.addWindowListener(
+				new WindowAdapter()
+				{
+					public void windowClosing( WindowEvent ev )
+					{
+						window = null;
+						universe = null;
+						segmentToContent.clear();
+						contentToSegment.clear();
+						setShowSegments( false );
+						universeManager.setUniverse( null );
+						for ( VisibilityListener listener : listeners )
+						{
+							listener.visibility( false );
+						}
+					}
+				} );
+		}
+
+		if ( showSegments != this.showSegments )
+		{
+			this.showSegments = showSegments;
+			if ( showSegments )
+			{
+				new Thread( () -> updateView( false ) ).start();
+			}
+			else
+			{
+				new Thread( () -> removeSegments() ).start();
+			}
+		}
+	}
+
+	public boolean getShowSegments() {
+		return showSegments;
+	}
+
+	private void setShowSegments(boolean b )
+	{
+		this.showSegments = b;
+	}
+
+	private void removeSegments()
+	{
+		final Set< S > segments = selectionModel.getSelected();
+
+		for ( S segment : segments )
+		{
+			removeSegment( segment );
+		}
+	}
+
+	private synchronized void addSegmentMeshToUniverse( S segment, CustomTriangleMesh mesh )
+	{
+		if ( mesh == null )
+			throw new RuntimeException( "Mesh of segment " + objectsName + "_" + segment.labelId() + " is null." );
+
+		if ( universe == null )
+			throw new RuntimeException( "Universe is null." );
+
+		final Content content = universe.addCustomMesh( mesh, objectsName + "_" + segment.labelId() );
+
+		content.setTransparency( ( float ) transparency );
+		content.setLocked( true );
+
+		segmentToContent.put( segment, content );
+		contentToSegment.put( content, segment );
+
+		universe.setAutoAdjustView( false );
+	}
+
+	private boolean addUniverseListener()
+	{
+		universe.addUniverseListener( new UniverseListener()
+		{
+
+			@Override
+			public void transformationStarted( View view )
+			{
+
+			}
+
+			@Override
+			public void transformationUpdated( View view )
+			{
+				// TODO: synchronize with  BDV View
+
+// 			   final Transform3D transform3D = new Transform3D();
+//			   view.getUserHeadToVworld( transform3D );
+
+//				final Transform3D transform3D = new Transform3D();
+//			    .getVworldToCamera( transform3D );
+//				System.out.println( transform3D );
+
+//				final Transform3D transform3DInverse = new Transform3D();
+//				.getVworldToCameraInverse( transform3DInverse );
+//				System.out.println( transform3DInverse );
+
+//				final TransformGroup transformGroup =
+//						.getViewingPlatform()
+//								.getMultiTransformGroup().getTransformGroup(
+//										DefaultUniverse.ZOOM_TG );
+//
+//				final Transform3D transform3D = new Transform3D();
+//				transformGroup.getTransform( transform3D );
+//
+//				System.out.println( transform3D );
+			}
+
+			@Override
+			public void transformationFinished( View view )
+			{
+
+			}
+
+			@Override
+			public void contentAdded( Content c )
+			{
+
+			}
+
+			@Override
+			public void contentRemoved( Content c )
+			{
+
+			}
+
+			@Override
+			public void contentChanged( Content c )
+			{
+
+			}
+
+			@Override
+			public void contentSelected( Content c )
+			{
+				if ( c == null ) return;
+
+				if ( ! contentToSegment.containsKey( c ) )
+					return;
+
+				final S segment = contentToSegment.get( c );
+
+				if ( selectionModel.isFocused( segment ) )
+				{
+					return;
+				}
+				else
+				{
+					recentFocus = segment; // avoids "self-focusing"
+					selectionModel.focus( segment );
+				}
+			}
+
+			@Override
+			public void canvasResized()
+			{
+
+			}
+
+			@Override
+			public void universeClosed()
+			{
+				for ( VisibilityListener listener : listeners )
+				{
+					listener.visibility( false );
+				}
+				window = null;
+				universe = null;
+			}
+		} );
+
+		return true;
+	}
+
+	private Color3f getColor3f( S imageSegment )
+	{
+		final ARGBType argbType = new ARGBType();
+		coloringModel.convert( imageSegment, argbType );
+		return new Color3f( ColorUtils.getColor( argbType ) );
+	}
+
+	public void setVoxelSpacing( double voxelSpacing )
+	{
+		this.voxelSpacing = voxelSpacing;
+	}
+
+	public double getVoxelSpacing()
+	{
+		return voxelSpacing;
+	}
+
+	public void close()
+	{
+		showSegments( false );
+	}
+
+	@Override
+	public void coloringChanged()
+	{
+		updateSegmentColors();
+	}
+
+	@Override
+	public synchronized void selectionChanged()
+	{
+		if ( ! showSegments ) return;
+
+		updateView( false );
+	}
+
+	@Override
+	public synchronized void focusEvent( S selection )
+	{
+		if ( ! showSegments ) return;
+
+		new Thread( () ->
+		{
+			if ( selection.timePoint() != currentTimePoint )
+			{
+				currentTimePoint = selection.timePoint();
+				updateView( false );
+			}
+		}).start();
+
+		if ( universe.getContents().size() == 0 ) return;
+		if ( selection == recentFocus ) return;
+		if ( ! segmentToContent.containsKey( selection ) ) return;
+
+		recentFocus = selection;
+
+		final AnimatedViewAdjuster adjuster =
+				new AnimatedViewAdjuster(
+						universe,
+						AnimatedViewAdjuster.ADJUST_BOTH );
+
+		adjuster.apply(
+				segmentToContent.get( selection ),
+				30,
+				segmentFocusAnimationDurationMillis,
+				segmentFocusZoomLevel,
+				segmentFocusDxyMin,
+				segmentFocusDzMin );
+	}
+
+	public Collection< VisibilityListener > getListeners()
+	{
+		return listeners;
+	}
+}
diff --git a/src/main/java/de/embl/cba/mobie/volume/UniverseManager.java b/src/main/java/de/embl/cba/mobie/volume/UniverseManager.java
new file mode 100644
index 000000000..7be87086c
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/volume/UniverseManager.java
@@ -0,0 +1,32 @@
+package de.embl.cba.mobie.volume;
+
+import ij3d.Image3DUniverse;
+
+public class UniverseManager
+{
+	private Image3DUniverse universe;
+
+	public Image3DUniverse get()
+	{
+		if ( universe == null )
+		{
+			universe = new Image3DUniverse();
+			universe.show();
+		}
+		return universe;
+	}
+
+	public void setUniverse( Image3DUniverse universe )
+	{
+		this.universe = universe;
+	}
+
+	public void close()
+	{
+		if ( universe != null )
+		{
+			universe.close();
+		}
+	}
+
+}


Test patch:
diff --git a/src/test/java/debug/Debug3DImageUniverse.java b/src/test/java/debug/Debug3DImageUniverse.java
new file mode 100644
index 000000000..7f8f63b08
--- /dev/null
+++ b/src/test/java/debug/Debug3DImageUniverse.java
@@ -0,0 +1,12 @@
+package debug;
+
+import ij3d.Image3DUniverse;
+
+public class Debug3DImageUniverse
+{
+	public static void main( String[] args )
+	{
+		Image3DUniverse universe = new Image3DUniverse();
+		universe.show();
+	}
+}
diff --git a/src/test/java/develop/DevelopAnnotationCorrectionPlatynereis.java b/src/test/java/develop/DevelopAnnotationCorrectionPlatynereis.java
index a0ad1b7ba..3d7a76f56 100644
--- a/src/test/java/develop/DevelopAnnotationCorrectionPlatynereis.java
+++ b/src/test/java/develop/DevelopAnnotationCorrectionPlatynereis.java
@@ -1,14 +1,14 @@
 package develop;
 
-import de.embl.cba.mobie.ui.MoBIE;
+import de.embl.cba.mobie.MoBIE;
 
 public class DevelopAnnotationCorrectionPlatynereis
 {
 	public static void main( String[] args )
 	{
-		final MoBIE viewer = new MoBIE( "https://github.com/vzinche/platybrowser-backend" );
-		viewer.getSourcesDisplayManager().show( "sbem-6dpf-1-whole-segmented-cells" );
-
-		// TODO: develop API get the views of a source: getViews( String sourceName )
+		// final MoBIE viewer = new MoBIE( "https://github.com/vzinche/platybrowser-backend" );
+		// viewer.getSourcesDisplayManager().show( "sbem-6dpf-1-whole-segmented-cells" );
+		//
+		// // TODO: develop API get the views of a source: getViews( String sourceName )
 	}
 }
diff --git a/src/test/java/develop/DevelopJsonTransformGroupParsing.java b/src/test/java/develop/DevelopJsonTransformGroupParsing.java
new file mode 100644
index 000000000..0343c5ca1
--- /dev/null
+++ b/src/test/java/develop/DevelopJsonTransformGroupParsing.java
@@ -0,0 +1,82 @@
+package develop;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import javax.xml.transform.Source;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+public class DevelopJsonTransformGroupParsing
+{
+	public interface SourceTransformer
+	{
+		List< Source > transform( List< Source > sources );
+	}
+
+	public static class AffineSourceTransformer implements SourceTransformer
+	{
+		public String[] sources;
+		public double[] transform;
+
+		@Override
+		public List< Source > transform( List< Source > sources )
+		{
+			return null;
+		}
+	}
+
+	public static class AutoGridSourceTransformer implements SourceTransformer
+	{
+		public String[][] sources;
+		public String gridType;
+
+		@Override
+		public List< Source > transform( List< Source > sources )
+		{
+			return null;}
+	}
+
+	public static class SourceTransformerSupplier
+	{
+		AffineSourceTransformer affine;
+		AutoGridSourceTransformer autoGrid;
+
+		public SourceTransformer get()
+		{
+			return null;
+		}
+	}
+
+	public static class View
+	{
+		public List< SourceTransformerSupplier > transformGroups;
+	}
+
+	public static void main( String[] args )
+	{
+		Gson gson = new Gson();
+
+		Type type = new TypeToken< View >() {}.getType();
+		View view = gson.fromJson( json(), type );
+
+		final ArrayList< SourceTransformer > sourceTransformers = new ArrayList<>();
+		for ( SourceTransformerSupplier transformerSupplier : view.transformGroups )
+		{
+			sourceTransformers.add( transformerSupplier.get() );
+		}
+	}
+
+	public static String json()
+	{
+		return "{\n" +
+				"  \"transformGroups\": [\n" +
+				"    {\"affine\": {\"transform\" : [0,0,0], \"sources\": [\"imE\", \"imF\"]}},\n" +
+				"    {\"affine\": {\"transform\" : [1,0,0], \"sources\": [\"imG\", \"imH\"]}},\n" +
+				"    {\"autoGrid\": {\"gridType\" : \"something\", \"sources\": [ [\"imA\", \"imB\"], [\"imC\", \"imD\"] ]}}\n" +
+				"  ]\n" +
+				"}";
+	}
+}
diff --git a/src/test/java/develop/DevelopNormalisedViewerTransforms.java b/src/test/java/develop/DevelopNormalisedViewerTransforms.java
index 29482ee32..efb288b92 100644
--- a/src/test/java/develop/DevelopNormalisedViewerTransforms.java
+++ b/src/test/java/develop/DevelopNormalisedViewerTransforms.java
@@ -1,10 +1,13 @@
 package develop;
 
-import de.embl.cba.mobie.ui.MoBIE;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
 import net.imagej.ImageJ;
 import net.imglib2.realtransform.AffineTransform3D;
 import net.imglib2.realtransform.Scale3D;
 
+import java.io.IOException;
+
 public class DevelopNormalisedViewerTransforms
 {
 	public static void main( String[] args )
@@ -14,7 +17,12 @@ public static void main( String[] args )
 
 		testNormalisationAndReversion();
 
-		final MoBIE moBIE = new MoBIE( "https://github.com/mobie-org/covid-em-datasets" );
+		try {
+			final MoBIE moBIE = new MoBIE("https://github.com/mobie-org/covid-em-datasets",
+					MoBIESettings.settings().imageDataStorageModality( MoBIESettings.ImageDataStorageModality.S3 ) );
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
 	}
 
 	public static void testNormalisationAndReversion()
diff --git a/src/test/java/projects/OpenLocalAutophagosomesEM.java b/src/test/java/projects/OpenLocalAutophagosomesEM.java
index 3413b3658..1c5c90b68 100644
--- a/src/test/java/projects/OpenLocalAutophagosomesEM.java
+++ b/src/test/java/projects/OpenLocalAutophagosomesEM.java
@@ -1,14 +1,21 @@
 package projects;
 
-import de.embl.cba.mobie.ui.MoBIE;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
 import net.imagej.ImageJ;
 
+import java.io.IOException;
+
 public class OpenLocalAutophagosomesEM
 {
 	public static void main( String[] args )
 	{
 		new ImageJ().ui().showUI();
-
-		final MoBIE moBIE = new MoBIE("/g/kreshuk/pape/work/my_projects/autophagosoms-clem/data");
+		try {
+			new MoBIE("/g/kreshuk/pape/work/my_projects/autophagosoms-clem/data",
+					MoBIESettings.settings().imageDataStorageModality( MoBIESettings.ImageDataStorageModality.FileSystem ) );
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
 	}
 }
diff --git a/src/test/java/projects/OpenLocalCovidEMCF.java b/src/test/java/projects/OpenLocalCovidEMCF.java
index 567eb92eb..1787974f5 100644
--- a/src/test/java/projects/OpenLocalCovidEMCF.java
+++ b/src/test/java/projects/OpenLocalCovidEMCF.java
@@ -1,11 +1,23 @@
 package projects;
 
-import de.embl.cba.mobie.ui.MoBIE;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
+import net.imagej.ImageJ;
+
+import java.io.IOException;
 
 public class OpenLocalCovidEMCF
 {
 	public static void main( String[] args )
 	{
-		new MoBIE( "/Volumes/emcf/common/5792_Sars-Cov-2/covid-em/data");
+		final ImageJ imageJ = new ImageJ();
+		imageJ.ui().showUI();
+		try {
+			new MoBIE("/Volumes/emcf/common/5792_Sars-Cov-2/covid-em/data",
+					MoBIESettings.settings().imageDataStorageModality( MoBIESettings.ImageDataStorageModality.FileSystem ) );
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+
 	}
 }
diff --git a/src/test/java/projects/OpenLocalCropTest.java b/src/test/java/projects/OpenLocalCropTest.java
new file mode 100644
index 000000000..3381a1825
--- /dev/null
+++ b/src/test/java/projects/OpenLocalCropTest.java
@@ -0,0 +1,17 @@
+package projects;
+
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
+import net.imagej.ImageJ;
+
+import java.io.IOException;
+
+public class OpenLocalCropTest
+{
+	public static void main( String[] args ) throws IOException
+	{
+		final ImageJ imageJ = new ImageJ();
+		imageJ.ui().showUI();
+		final MoBIE moBIE2 = new MoBIE("/g/emcf/pape/mobie-test-projects/mobie_crop", MoBIESettings.settings().imageDataStorageModality( MoBIESettings.ImageDataStorageModality.FileSystem ));
+	}
+}
diff --git a/src/test/java/projects/OpenLocalPlankton.java b/src/test/java/projects/OpenLocalPlankton.java
index 4fb07b975..5f7c5e985 100644
--- a/src/test/java/projects/OpenLocalPlankton.java
+++ b/src/test/java/projects/OpenLocalPlankton.java
@@ -1,14 +1,21 @@
 package projects;
 
-import de.embl.cba.mobie.ui.MoBIEOptions;
-import de.embl.cba.mobie.ui.MoBIE;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
 import net.imagej.ImageJ;
 
+import java.io.IOException;
+
 public class OpenLocalPlankton
 {
 	public static void main( String[] args )
 	{
 		new ImageJ().ui().showUI();
-		new MoBIE("/Volumes/schwab/schwab/MoBIE-plankton", MoBIEOptions.options().imageDataStorageModality( MoBIEOptions.ImageDataStorageModality.FileSystem ));
+		try {
+			new MoBIE("/Volumes/schwab/schwab/MoBIE-plankton",
+					MoBIESettings.settings().imageDataStorageModality( MoBIESettings.ImageDataStorageModality.FileSystem ) );
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
 	}
 }
diff --git a/src/test/java/projects/OpenLocalPlatynereis.java b/src/test/java/projects/OpenLocalPlatynereis.java
index 1fb88ec52..6ccde369d 100644
--- a/src/test/java/projects/OpenLocalPlatynereis.java
+++ b/src/test/java/projects/OpenLocalPlatynereis.java
@@ -1,11 +1,22 @@
 package projects;
 
-import de.embl.cba.mobie.ui.MoBIE;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
+import net.imagej.ImageJ;
+
+import java.io.IOException;
 
 public class OpenLocalPlatynereis
 {
 	public static void main( String[] args )
 	{
-		new MoBIE("/g/arendt/EM_6dpf_segmentation/platy-browser-data/data/" );
+		final ImageJ imageJ = new ImageJ();
+		imageJ.ui().showUI();
+		try {
+			new MoBIE("/g/arendt/EM_6dpf_segmentation/platy-browser-data/data/",
+					MoBIESettings.settings().imageDataStorageModality( MoBIESettings.ImageDataStorageModality.FileSystem ) );
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
 	}
 }
diff --git a/src/test/java/projects/OpenLocalTestData.java b/src/test/java/projects/OpenLocalTestData.java
new file mode 100644
index 000000000..2d1e9c99d
--- /dev/null
+++ b/src/test/java/projects/OpenLocalTestData.java
@@ -0,0 +1,15 @@
+package projects;
+
+import de.embl.cba.mobie.MoBIE;
+import net.imagej.ImageJ;
+
+import java.io.IOException;
+
+public class OpenLocalTestData
+{
+	public static void main( String[] args ) throws IOException
+	{
+		final ImageJ imageJ = new ImageJ();
+		final MoBIE moBIE2 = new MoBIE("/g/kreshuk/pape/Work/data/mobie/full-example");
+	}
+}
diff --git a/src/test/java/projects/OpenRemoteArabidopsis.java b/src/test/java/projects/OpenRemoteArabidopsis.java
index 308564be8..87ccf8429 100644
--- a/src/test/java/projects/OpenRemoteArabidopsis.java
+++ b/src/test/java/projects/OpenRemoteArabidopsis.java
@@ -1,15 +1,17 @@
 package projects;
 
-import de.embl.cba.mobie.ui.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
+import de.embl.cba.mobie.MoBIE;
 import net.imagej.ImageJ;
 
+import java.io.IOException;
+
 public class OpenRemoteArabidopsis
 {
-	public static void main( String[] args )
+	public static void main( String[] args ) throws IOException
 	{
 		final ImageJ imageJ = new ImageJ();
 		imageJ.ui().showUI();
-
-		new MoBIE("https://github.com/mobie/arabidopsis-root-lm-datasets" );
+		new MoBIE("https://github.com/mobie/arabidopsis-root-lm-datasets", MoBIESettings.settings().gitProjectBranch( "spec-v2" ).imageDataStorageModality( MoBIESettings.ImageDataStorageModality.S3 ) );
 	}
 }
diff --git a/src/test/java/projects/OpenRemoteAutophagosomesCLEM.java b/src/test/java/projects/OpenRemoteAutophagosomesCLEM.java
index d6fd5c908..3cf76853d 100644
--- a/src/test/java/projects/OpenRemoteAutophagosomesCLEM.java
+++ b/src/test/java/projects/OpenRemoteAutophagosomesCLEM.java
@@ -1,11 +1,23 @@
 package projects;
 
-import de.embl.cba.mobie.ui.MoBIE;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
+import net.imagej.ImageJ;
+
+import java.io.IOException;
 
 public class OpenRemoteAutophagosomesCLEM
 {
 	public static void main( String[] args )
 	{
-		new MoBIE( "https://github.com/mobie-org/autophagosomes-clem-datasets");
+		final ImageJ imageJ = new ImageJ();
+		imageJ.ui().showUI();
+		try {
+			new MoBIE("https://github.com/mobie-org/autophagosomes-clem-datasets",
+					MoBIESettings.settings().imageDataStorageModality( MoBIESettings.ImageDataStorageModality.S3 ) );
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+
 	}
 }
diff --git a/src/test/java/projects/OpenRemoteCovidEM.java b/src/test/java/projects/OpenRemoteCovidEM.java
index 97eb861ef..79158bd6e 100644
--- a/src/test/java/projects/OpenRemoteCovidEM.java
+++ b/src/test/java/projects/OpenRemoteCovidEM.java
@@ -1,11 +1,22 @@
 package projects;
 
-import de.embl.cba.mobie.ui.MoBIE;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
+import net.imagej.ImageJ;
+
+import java.io.IOException;
 
 public class OpenRemoteCovidEM
 {
 	public static void main( String[] args )
 	{
-		new MoBIE( "https://github.com/mobie-org/covid-em-datasets");
+		final ImageJ imageJ = new ImageJ();
+		imageJ.ui().showUI();
+		try {
+			new MoBIE("https://github.com/mobie-org/covid-em-datasets",
+					MoBIESettings.settings().imageDataStorageModality( MoBIESettings.ImageDataStorageModality.S3 ) );
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
 	}
 }
diff --git a/src/test/java/projects/OpenRemotePlatynereis.java b/src/test/java/projects/OpenRemotePlatynereis.java
index 3b8b4404c..4ed77b563 100644
--- a/src/test/java/projects/OpenRemotePlatynereis.java
+++ b/src/test/java/projects/OpenRemotePlatynereis.java
@@ -1,8 +1,11 @@
 package projects;
 
-import de.embl.cba.mobie.ui.MoBIE;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
 import net.imagej.ImageJ;
 
+import java.io.IOException;
+
 public class OpenRemotePlatynereis
 {
 	public static void main( String[] args )
@@ -10,6 +13,11 @@ public static void main( String[] args )
 		final ImageJ imageJ = new ImageJ();
 		imageJ.ui().showUI();
 
-		new MoBIE("https://github.com/platybrowser/platybrowser" );
+		try {
+			new MoBIE("https://github.com/platybrowser/platybrowser",
+					MoBIESettings.settings().imageDataStorageModality( MoBIESettings.ImageDataStorageModality.S3 ) );
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
 	}
 }
diff --git a/src/test/java/projects/OpenRemotePlatynereisBranch.java b/src/test/java/projects/OpenRemotePlatynereisBranch.java
index e7d1974fe..e39d7f166 100644
--- a/src/test/java/projects/OpenRemotePlatynereisBranch.java
+++ b/src/test/java/projects/OpenRemotePlatynereisBranch.java
@@ -1,11 +1,17 @@
 package projects;
 
-import de.embl.cba.mobie.ui.MoBIE;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
+import net.imagej.ImageJ;
+
+import java.io.IOException;
 
 public class OpenRemotePlatynereisBranch
 {
-	public static void main( String[] args )
+	public static void main( String[] args ) throws IOException
 	{
-		new MoBIE( "https://github.com/platybrowser/platybrowser" );
+		final ImageJ imageJ = new ImageJ();
+		imageJ.ui().showUI();
+		new MoBIE( "https://github.com/mobie/platybrowser-datasets", MoBIESettings.settings().gitProjectBranch( "spec-v2" ) );
 	}
 }
diff --git a/src/test/java/projects/OpenRemoteTomograms.java b/src/test/java/projects/OpenRemoteTomograms.java
index 0aaa8593b..05cdd5091 100644
--- a/src/test/java/projects/OpenRemoteTomograms.java
+++ b/src/test/java/projects/OpenRemoteTomograms.java
@@ -1,8 +1,11 @@
 package projects;
 
-import de.embl.cba.mobie.ui.MoBIE;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
 import net.imagej.ImageJ;
 
+import java.io.IOException;
+
 public class OpenRemoteTomograms
 {
 	public static void main( String[] args )
@@ -10,6 +13,11 @@ public static void main( String[] args )
 		final ImageJ imageJ = new ImageJ();
 		imageJ.ui().showUI();
 
-		new MoBIE("https://github.com/mobie/covid-tomo-datasets" );
+		try {
+			new MoBIE("https://github.com/mobie/covid-tomo-datasets",
+					MoBIESettings.settings().imageDataStorageModality( MoBIESettings.ImageDataStorageModality.S3 ) );
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
 	}
 }
diff --git a/src/test/java/projects/OpenRemoteTomogramsBranch.java b/src/test/java/projects/OpenRemoteTomogramsBranch.java
index 845640706..fcffb0006 100644
--- a/src/test/java/projects/OpenRemoteTomogramsBranch.java
+++ b/src/test/java/projects/OpenRemoteTomogramsBranch.java
@@ -1,9 +1,11 @@
 package projects;
 
-import de.embl.cba.mobie.ui.MoBIEOptions;
-import de.embl.cba.mobie.ui.MoBIE;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
 import net.imagej.ImageJ;
 
+import java.io.IOException;
+
 public class OpenRemoteTomogramsBranch
 {
 	public static void main( String[] args )
@@ -11,6 +13,11 @@ public static void main( String[] args )
 		final ImageJ imageJ = new ImageJ();
 		imageJ.ui().showUI();
 
-		new MoBIE("https://github.com/mobie/covid-tomo-datasets", MoBIEOptions.options().gitProjectBranch( "grid-test" )  );
+		try {
+			new MoBIE("https://github.com/mobie/covid-tomo-datasets",
+					MoBIESettings.settings().gitProjectBranch( "grid-test" ).imageDataStorageModality( MoBIESettings.ImageDataStorageModality.S3 ) );
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
 	}
 }
diff --git a/src/test/java/projects/OpenRemoteYeastCLEM.java b/src/test/java/projects/OpenRemoteYeastCLEM.java
new file mode 100644
index 000000000..550498824
--- /dev/null
+++ b/src/test/java/projects/OpenRemoteYeastCLEM.java
@@ -0,0 +1,18 @@
+package projects;
+
+import de.embl.cba.mobie.MoBIESettings;
+import de.embl.cba.mobie.MoBIE;
+import net.imagej.ImageJ;
+
+import java.io.IOException;
+
+public class OpenRemoteYeastCLEM
+{
+	public static void main( String[] args ) throws IOException
+	{
+		final ImageJ imageJ = new ImageJ();
+		imageJ.ui().showUI();
+
+		new MoBIE("https://github.com/mobie/yeast-clem-datasets", MoBIESettings.settings().gitProjectBranch( "spec-v2" ).imageDataStorageModality( MoBIESettings.ImageDataStorageModality.S3 ) );
+	}
+}
diff --git a/src/test/java/projects/OpenRemoteZebrafish.java b/src/test/java/projects/OpenRemoteZebrafish.java
new file mode 100644
index 000000000..c1c691a85
--- /dev/null
+++ b/src/test/java/projects/OpenRemoteZebrafish.java
@@ -0,0 +1,17 @@
+package projects;
+
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
+import net.imagej.ImageJ;
+
+import java.io.IOException;
+
+public class OpenRemoteZebrafish
+{
+	public static void main( String[] args ) throws IOException
+	{
+		final ImageJ imageJ = new ImageJ();
+		imageJ.ui().showUI();
+		new MoBIE("https://github.com/mobie/zebrafish-lm-datasets", MoBIESettings.settings().gitProjectBranch( "spec-v2" ).imageDataStorageModality( MoBIESettings.ImageDataStorageModality.S3 ) );
+	}
+}
diff --git a/src/test/java/publication/platybrowser/RegistrationVisualisationMuscles.java b/src/test/java/publication/platybrowser/RegistrationVisualisationMuscles.java
index 85e231f47..59a32b36d 100644
--- a/src/test/java/publication/platybrowser/RegistrationVisualisationMuscles.java
+++ b/src/test/java/publication/platybrowser/RegistrationVisualisationMuscles.java
@@ -1,10 +1,9 @@
 package publication.platybrowser;
 
-import de.embl.cba.mobie.bdv.BdvViewChanger;
-import de.embl.cba.mobie.bookmark.Location;
-import de.embl.cba.mobie.ui.MoBIE;
-import de.embl.cba.mobie.ui.SourcesDisplayManager;
-import de.embl.cba.mobie.utils.UniverseUtils;
+import de.embl.cba.mobie.MoBIE;
+import de.embl.cba.mobie.MoBIESettings;
+import de.embl.cba.mobie.transform.BdvLocationChanger;
+import de.embl.cba.mobie.transform.BdvLocation;
 import net.imagej.ImageJ;
 
 import java.awt.*;
@@ -15,31 +14,31 @@ public static void main( String[] args )
 	{
 		new ImageJ().ui().showUI();
 
-		final MoBIE moBIE = new MoBIE(
-				"/Volumes/arendt/EM_6dpf_segmentation/platy-browser-data/data",
-				"https://git.embl.de/tischer/platy-browser-tables/raw/dev/data" );
-
-		final SourcesDisplayManager sourcesDisplayManager = moBIE.getSourcesDisplayManager();
-
-		sourcesDisplayManager.show( "sbem-6dpf-1-whole-segmented-muscles" );
-		sourcesDisplayManager.show( "prospr-6dpf-1-whole-MHCL4-MED" );
-		sourcesDisplayManager.show( "prospr-6dpf-1-whole-ENR22-MED" );
-
-		sourcesDisplayManager.setSourceColor( "sbem-6dpf-1-whole-segmented-muscles", new Color( 255,0,255,255) );
-		sourcesDisplayManager.setSourceColor( "prospr-6dpf-1-whole-MHCL4-MED", new Color( 0,255,0,255) );
-		sourcesDisplayManager.setSourceColor( "prospr-6dpf-1-whole-ENR22-MED", new Color( 0,255,0,255) );
-
-
-		// OVERVIEW
-		BdvViewChanger.moveToLocation( moBIE.getSourcesDisplayManager().getBdv(), new Location( "View: (-0.747871017528087, -0.6676436522042726, 2.0869538546605892, 291.53347494758805, 1.4642245659643167, -1.7927905904117456, -0.04882433847545138, 337.95348110873715, 1.6300830832316395, 1.3040664665853112, 1.0013367511280067, -535.9999606550252)" ) );
-
-		// TODO: get this via keyboard shortcut
-		UniverseUtils.setVolumeView("center =  1.0 0.0 0.0 122.88 0.0 1.0 0.0 122.88 0.0 0.0 1.0 140.40001 0.0 0.0 0.0 1.0\n" +
-				"translate =  1.0 0.0 0.0 0.117842264 0.0 1.0 0.0 0.15685147 0.0 0.0 1.0 -0.52159584 0.0 0.0 0.0 1.0\n" +
-				"rotate =  -0.22049984 -0.6480566 -0.72897357 0.0 -0.22329058 0.76105946 -0.60904 0.0 0.94948465 0.028479699 -0.31251845 0.0 0.0 0.0 0.0 1.0\n" +
-				"zoom =  1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 358.10345 0.0 0.0 0.0 1.0\n" +
-				"animate =  1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0\n", moBIE.getSourcesDisplayManager().getUniverse()
-		);
+		// final MoBIE moBIE = new MoBIE(
+		// 		"/Volumes/arendt/EM_6dpf_segmentation/platy-browser-data/data",
+		// 		"https://git.embl.de/tischer/platy-browser-tables/raw/dev/data" );
+		//
+		// final SourcesDisplayManager sourcesDisplayManager = moBIE.getSourcesDisplayManager();
+		//
+		// sourcesDisplayManager.show( "sbem-6dpf-1-whole-segmented-muscles" );
+		// sourcesDisplayManager.show( "prospr-6dpf-1-whole-MHCL4-MED" );
+		// sourcesDisplayManager.show( "prospr-6dpf-1-whole-ENR22-MED" );
+		//
+		// sourcesDisplayManager.setSourceColor( "sbem-6dpf-1-whole-segmented-muscles", new Color( 255,0,255,255) );
+		// sourcesDisplayManager.setSourceColor( "prospr-6dpf-1-whole-MHCL4-MED", new Color( 0,255,0,255) );
+		// sourcesDisplayManager.setSourceColor( "prospr-6dpf-1-whole-ENR22-MED", new Color( 0,255,0,255) );
+		//
+		//
+		// // OVERVIEW
+		// BdvLocationChanger.moveToLocation( moBIE.getSourcesDisplayManager().getBdv(), new BdvLocation( "View: (-0.747871017528087, -0.6676436522042726, 2.0869538546605892, 291.53347494758805, 1.4642245659643167, -1.7927905904117456, -0.04882433847545138, 337.95348110873715, 1.6300830832316395, 1.3040664665853112, 1.0013367511280067, -535.9999606550252)" ) );
+		//
+		// // TODO: get this via keyboard shortcut
+		// UniverseUtils.setVolumeView("center =  1.0 0.0 0.0 122.88 0.0 1.0 0.0 122.88 0.0 0.0 1.0 140.40001 0.0 0.0 0.0 1.0\n" +
+		// 		"translate =  1.0 0.0 0.0 0.117842264 0.0 1.0 0.0 0.15685147 0.0 0.0 1.0 -0.52159584 0.0 0.0 0.0 1.0\n" +
+		// 		"rotate =  -0.22049984 -0.6480566 -0.72897357 0.0 -0.22329058 0.76105946 -0.60904 0.0 0.94948465 0.028479699 -0.31251845 0.0 0.0 0.0 0.0 1.0\n" +
+		// 		"zoom =  1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 358.10345 0.0 0.0 0.0 1.0\n" +
+		// 		"animate =  1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0\n", moBIE.getSourcesDisplayManager().getUniverse()
+		// );
 
 
 		// DETAIL ZOOM
diff --git a/src/test/java/publication/platybrowser/RegistrationVisualisationNeuropil.java b/src/test/java/publication/platybrowser/RegistrationVisualisationNeuropil.java
index 77ce830c3..500877fde 100644
--- a/src/test/java/publication/platybrowser/RegistrationVisualisationNeuropil.java
+++ b/src/test/java/publication/platybrowser/RegistrationVisualisationNeuropil.java
@@ -1,10 +1,7 @@
 package publication.platybrowser;
 
-import de.embl.cba.mobie.bdv.BdvViewChanger;
-import de.embl.cba.mobie.bookmark.Location;
-import de.embl.cba.mobie.ui.MoBIE;
-import de.embl.cba.mobie.ui.SourcesDisplayManager;
-import de.embl.cba.mobie.utils.UniverseUtils;
+import de.embl.cba.mobie.transform.BdvLocationChanger;
+import de.embl.cba.mobie.transform.BdvLocation;
 import net.imagej.ImageJ;
 
 import java.awt.*;
@@ -15,30 +12,30 @@ public static void main( String[] args )
 	{
 		new ImageJ().ui().showUI();
 
-		final MoBIE moBIE = new MoBIE(
-				"/Volumes/arendt/EM_6dpf_segmentation/platy-browser-data/data",
-				"https://git.embl.de/tischer/platy-browser-tables/raw/dev/data" );
-
-		final SourcesDisplayManager sourcesDisplayManager = moBIE.getSourcesDisplayManager();
-
-		sourcesDisplayManager.show( "prospr-6dpf-1-whole-segmented-aridane-Neuropil" );
-		sourcesDisplayManager.show( "prospr-6dpf-1-whole-Glt1-MED" );
-
-		sourcesDisplayManager.setSourceColor( "prospr-6dpf-1-whole-segmented-aridane-Neuropil", new Color( 255,0,255,255) );
-		sourcesDisplayManager.setSourceColor( "prospr-6dpf-1-whole-Glt1-MED", new Color( 0,255,0,255) );
-
-
-		// OVERVIEW
-
-		BdvViewChanger.moveToLocation( moBIE.getSourcesDisplayManager().getBdv(), new Location( "View: (-0.7478710175280877, -0.6676436522042725, 2.086953854660592, 291.5334749475883, 1.4642245659643176, -1.7927905904117467, -0.048824338475451055, 337.95348110873744, 1.6300830832316395, 1.3040664665853117, 1.0013367511280067, -516.0999606550258)" ) );
-
-		// TODO: get this via keyboard shortcut
-		UniverseUtils.setVolumeView("center =  1.0 0.0 0.0 122.88 0.0 1.0 0.0 122.88 0.0 0.0 1.0 140.40001 0.0 0.0 0.0 1.0\n" +
-				"translate =  1.0 0.0 0.0 0.117842264 0.0 1.0 0.0 0.15685147 0.0 0.0 1.0 -0.52159584 0.0 0.0 0.0 1.0\n" +
-				"rotate =  -0.22049984 -0.6480566 -0.72897357 0.0 -0.22329058 0.76105946 -0.60904 0.0 0.94948465 0.028479699 -0.31251845 0.0 0.0 0.0 0.0 1.0\n" +
-				"zoom =  1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 358.10345 0.0 0.0 0.0 1.0\n" +
-				"animate =  1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0\n", moBIE.getSourcesDisplayManager().getUniverse()
-		);
+		// final MoBIE moBIE = new MoBIE(
+		// 		"/Volumes/arendt/EM_6dpf_segmentation/platy-browser-data/data",
+		// 		"https://git.embl.de/tischer/platy-browser-tables/raw/dev/data" );
+		//
+		// final SourcesDisplayManager sourcesDisplayManager = moBIE.getSourcesDisplayManager();
+		//
+		// sourcesDisplayManager.show( "prospr-6dpf-1-whole-segmented-aridane-Neuropil" );
+		// sourcesDisplayManager.show( "prospr-6dpf-1-whole-Glt1-MED" );
+		//
+		// sourcesDisplayManager.setSourceColor( "prospr-6dpf-1-whole-segmented-aridane-Neuropil", new Color( 255,0,255,255) );
+		// sourcesDisplayManager.setSourceColor( "prospr-6dpf-1-whole-Glt1-MED", new Color( 0,255,0,255) );
+		//
+		//
+		// // OVERVIEW
+		//
+		// BdvLocationChanger.moveToLocation( moBIE.getSourcesDisplayManager().getBdv(), new BdvLocation( "View: (-0.7478710175280877, -0.6676436522042725, 2.086953854660592, 291.5334749475883, 1.4642245659643176, -1.7927905904117467, -0.048824338475451055, 337.95348110873744, 1.6300830832316395, 1.3040664665853117, 1.0013367511280067, -516.0999606550258)" ) );
+		//
+		// // TODO: get this via keyboard shortcut
+		// UniverseUtils.setVolumeView("center =  1.0 0.0 0.0 122.88 0.0 1.0 0.0 122.88 0.0 0.0 1.0 140.40001 0.0 0.0 0.0 1.0\n" +
+		// 		"translate =  1.0 0.0 0.0 0.117842264 0.0 1.0 0.0 0.15685147 0.0 0.0 1.0 -0.52159584 0.0 0.0 0.0 1.0\n" +
+		// 		"rotate =  -0.22049984 -0.6480566 -0.72897357 0.0 -0.22329058 0.76105946 -0.60904 0.0 0.94948465 0.028479699 -0.31251845 0.0 0.0 0.0 0.0 1.0\n" +
+		// 		"zoom =  1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 358.10345 0.0 0.0 0.0 1.0\n" +
+		// 		"animate =  1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0\n", moBIE.getSourcesDisplayManager().getUniverse()
+		// );
 
 		// DETAIL ZOOM
 		// View: (-5.457887183190528, 6.892455295688354, -0.09128822640025808, 243.5120593221493, -3.0323636942634073, -2.2962312393397, 7.926846697708436, 869.0433860416233, 6.19023779584073, 4.952190236672585, 3.8025746460164447, -2153.870206057355)
diff --git a/src/test/java/publication/platybrowser/RegistrationVisualisationNuclei.java b/src/test/java/publication/platybrowser/RegistrationVisualisationNuclei.java
index 721f9ce55..97e37bfb4 100644
--- a/src/test/java/publication/platybrowser/RegistrationVisualisationNuclei.java
+++ b/src/test/java/publication/platybrowser/RegistrationVisualisationNuclei.java
@@ -1,10 +1,7 @@
 package publication.platybrowser;
 
-import de.embl.cba.mobie.bdv.BdvViewChanger;
-import de.embl.cba.mobie.bookmark.Location;
-import de.embl.cba.mobie.ui.MoBIE;
-import de.embl.cba.mobie.ui.SourcesDisplayManager;
-import de.embl.cba.mobie.utils.UniverseUtils;
+import de.embl.cba.mobie.transform.BdvLocationChanger;
+import de.embl.cba.mobie.transform.BdvLocation;
 import net.imagej.ImageJ;
 
 import java.awt.*;
@@ -13,30 +10,30 @@ public class RegistrationVisualisationNuclei
 {
 	public static void main( String[] args )
 	{
-		new ImageJ().ui().showUI();
-
-		final MoBIE moBIE = new MoBIE(
-				"/Volumes/arendt/EM_6dpf_segmentation/platy-browser-data/data",
-				"https://git.embl.de/tischer/platy-browser-tables/raw/dev/data" );
-
-		final SourcesDisplayManager sourcesDisplayManager = moBIE.getSourcesDisplayManager();
-
-		sourcesDisplayManager.show( "sbem-6dpf-1-whole-segmented-nuclei-labels" );
-		sourcesDisplayManager.show( "prospr-6dpf-1-whole-Ref-SPM" );
-
-		sourcesDisplayManager.setSourceColor( "sbem-6dpf-1-whole-segmented-nuclei-labels", new Color( 255,0,255,255) );
-		sourcesDisplayManager.setSourceColor( "prospr-6dpf-1-whole-Ref-SPM", new Color( 0,255,0,255) );
-
-		// OVERVIEW
-
-		BdvViewChanger.moveToLocation( moBIE.getSourcesDisplayManager().getBdv(), new Location( "View: (-0.747871017528087, -0.6676436522042726, 2.0869538546605892, 291.53347494758805, 1.4642245659643167, -1.7927905904117456, -0.04882433847545138, 337.95348110873715, 1.6300830832316395, 1.3040664665853112, 1.0013367511280067, -535.9999606550252)" ) );
-
-		UniverseUtils.setVolumeView("center =  1.0 0.0 0.0 122.88 0.0 1.0 0.0 122.88 0.0 0.0 1.0 140.40001 0.0 0.0 0.0 1.0\n" +
-				"translate =  1.0 0.0 0.0 0.117842264 0.0 1.0 0.0 0.15685147 0.0 0.0 1.0 -0.52159584 0.0 0.0 0.0 1.0\n" +
-				"rotate =  -0.22049984 -0.6480566 -0.72897357 0.0 -0.22329058 0.76105946 -0.60904 0.0 0.94948465 0.028479699 -0.31251845 0.0 0.0 0.0 0.0 1.0\n" +
-				"zoom =  1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 358.10345 0.0 0.0 0.0 1.0\n" +
-				"animate =  1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0\n", moBIE.getSourcesDisplayManager().getUniverse()
-		);
+		// new ImageJ().ui().showUI();
+		//
+		// final MoBIE moBIE = new MoBIE(
+		// 		"/Volumes/arendt/EM_6dpf_segmentation/platy-browser-data/data",
+		// 		"https://git.embl.de/tischer/platy-browser-tables/raw/dev/data" );
+		//
+		// final SourcesDisplayManager sourcesDisplayManager = moBIE.getSourcesDisplayManager();
+		//
+		// sourcesDisplayManager.show( "sbem-6dpf-1-whole-segmented-nuclei-labels" );
+		// sourcesDisplayManager.show( "prospr-6dpf-1-whole-Ref-SPM" );
+		//
+		// sourcesDisplayManager.setSourceColor( "sbem-6dpf-1-whole-segmented-nuclei-labels", new Color( 255,0,255,255) );
+		// sourcesDisplayManager.setSourceColor( "prospr-6dpf-1-whole-Ref-SPM", new Color( 0,255,0,255) );
+		//
+		// // OVERVIEW
+		//
+		// BdvLocationChanger.moveToLocation( moBIE.getSourcesDisplayManager().getBdv(), new BdvLocation( "View: (-0.747871017528087, -0.6676436522042726, 2.0869538546605892, 291.53347494758805, 1.4642245659643167, -1.7927905904117456, -0.04882433847545138, 337.95348110873715, 1.6300830832316395, 1.3040664665853112, 1.0013367511280067, -535.9999606550252)" ) );
+		//
+		// UniverseUtils.setVolumeView("center =  1.0 0.0 0.0 122.88 0.0 1.0 0.0 122.88 0.0 0.0 1.0 140.40001 0.0 0.0 0.0 1.0\n" +
+		// 		"translate =  1.0 0.0 0.0 0.117842264 0.0 1.0 0.0 0.15685147 0.0 0.0 1.0 -0.52159584 0.0 0.0 0.0 1.0\n" +
+		// 		"rotate =  -0.22049984 -0.6480566 -0.72897357 0.0 -0.22329058 0.76105946 -0.60904 0.0 0.94948465 0.028479699 -0.31251845 0.0 0.0 0.0 0.0 1.0\n" +
+		// 		"zoom =  1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 358.10345 0.0 0.0 0.0 1.0\n" +
+		// 		"animate =  1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0\n", moBIE.getSourcesDisplayManager().getUniverse()
+		// );
 
 		// DETAIL ZOOM
 		// View: (-6.099350109804703, -5.3990077927603535, 18.614782509183787, -202.73480935758062, 12.078494318540145, -16.320823077240274, -0.7760115017778567, 117.84084743329208, 15.158145929086958, 10.832494343760871, 8.108585761855771, -4294.781037288093)
diff --git a/src/test/java/run/RunOpenCustomBrowserCommand.java b/src/test/java/run/RunOpenCustomBrowserCommand.java
index 4e58f884b..ddac42648 100644
--- a/src/test/java/run/RunOpenCustomBrowserCommand.java
+++ b/src/test/java/run/RunOpenCustomBrowserCommand.java
@@ -1,6 +1,6 @@
 package run;
 
-import de.embl.cba.mobie.ui.command.OpenMoBIEProjectCommand;
+import de.embl.cba.mobie.command.OpenMoBIEProjectCommand;
 import net.imagej.ImageJ;
 
 public class RunOpenCustomBrowserCommand
diff --git a/src/test/java/run/RunOpenPlatyBrowserCommand.java b/src/test/java/run/RunOpenPlatyBrowserCommand.java
index 90f681eb9..ce137ad46 100644
--- a/src/test/java/run/RunOpenPlatyBrowserCommand.java
+++ b/src/test/java/run/RunOpenPlatyBrowserCommand.java
@@ -1,6 +1,6 @@
 package run;
 
-import de.embl.cba.mobie.ui.command.OpenPlatyBrowserCommand;
+import de.embl.cba.mobie.command.OpenPlatyBrowserCommand;
 import net.imagej.ImageJ;
 
 public class RunOpenPlatyBrowserCommand
diff --git a/src/test/java/tests/TestBookmarksLoadingFromGitlab.java b/src/test/java/tests/TestBookmarksLoadingFromGitlab.java
deleted file mode 100644
index b05ef1b1c..000000000
--- a/src/test/java/tests/TestBookmarksLoadingFromGitlab.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package tests;
-
-import java.io.IOException;
-
-public class TestBookmarksLoadingFromGitlab
-{
-	//@Test
-	public static void main( String[] args ) throws IOException
-	{
-
-	}
-}
diff --git a/src/test/java/tests/TestPlatyBrowserCells3DView.java b/src/test/java/tests/TestPlatyBrowserCells3DView.java
deleted file mode 100644
index 954be4b51..000000000
--- a/src/test/java/tests/TestPlatyBrowserCells3DView.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package tests;
-
-import de.embl.cba.mobie.ui.MoBIE;
-import de.embl.cba.mobie.ui.SourcesDisplayManager;
-import de.embl.cba.tables.select.SelectionModel;
-import de.embl.cba.tables.tablerow.TableRowImageSegment;
-import de.embl.cba.tables.view.combined.SegmentsTableBdvAnd3dViews;
-import net.imagej.ImageJ;
-
-import java.util.List;
-
-public class TestPlatyBrowserCells3DView
-{
-//	@Test
-	public void viewSmallAndLargeCellIn3D( )
-	{
-		new ImageJ().ui().showUI();
-
-		final MoBIE mainFrame = new MoBIE(
-				"/Volumes/arendt/EM_6dpf_segmentation/platy-browser-data/data",
-				"https://git.embl.de/tischer/platy-browser-tables/raw/dev/data" );
-
-		final SourcesDisplayManager sourcesDisplayManager = mainFrame.getSourcesDisplayManager();
-
-		sourcesDisplayManager.show( "sbem-6dpf-1-whole-segmented-cells-labels" );
-		final SegmentsTableBdvAnd3dViews views = sourcesDisplayManager.getSourceNameToLabelViews().values().iterator().next();
-
-		final SelectionModel< TableRowImageSegment > selectionModel = views.getSelectionModel();
-		final List< TableRowImageSegment > tableRowImageSegments = views.getTableRowImageSegments();
-
-		selectionModel.setSelected( tableRowImageSegments.get( 11057 ), true ); // small cell
-		selectionModel.setSelected( tableRowImageSegments.get( 5703 ), true ); // neuropil
-	}
-
-
-	public static void main( String[] args )
-	{
-		new TestPlatyBrowserCells3DView().viewSmallAndLargeCellIn3D( );
-	}
-}
diff --git a/src/test/java/tests/TestPlatyBrowserMoveTo.java b/src/test/java/tests/TestPlatyBrowserMoveTo.java
deleted file mode 100644
index 595919cc3..000000000
--- a/src/test/java/tests/TestPlatyBrowserMoveTo.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package tests;
-
-import de.embl.cba.mobie.bdv.BdvViewChanger;
-import de.embl.cba.mobie.bookmark.Location;
-import de.embl.cba.mobie.ui.MoBIE;
-import net.imagej.ImageJ;
-
-public class TestPlatyBrowserMoveTo
-{
-//	@Test
-	public void moveToPoint( )
-	{
-		new ImageJ().ui().showUI();
-
-		final MoBIE moBIE = new MoBIE(
-				"/Volumes/arendt/EM_6dpf_segmentation/platy-browser-data/data",
-				"https://git.embl.de/tischer/platy-browser-tables/raw/dev/data" );
-
-		BdvViewChanger.moveToLocation( moBIE.getSourcesDisplayManager().getBdv(), new Location( "177.46666666666667,214.46666666666667,67.0" ) );
-	}
-
-	public static void main( String[] args )
-	{
-		new TestPlatyBrowserMoveTo().moveToPoint( );
-	}
-}
diff --git a/src/test/java/tests/TestStringToColor.java b/src/test/java/tests/TestStringToColor.java
deleted file mode 100644
index 9d83e204f..000000000
--- a/src/test/java/tests/TestStringToColor.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package tests;
-
-import de.embl.cba.mobie.utils.Utils;
-import org.janelia.saalfeldlab.n5.s3.N5AmazonS3Reader;
-import org.junit.Test;
-
-import java.awt.*;
-
-public class TestStringToColor
-{
-	@Test
-	public void stringToColor()
-	{
-		final Color green = Utils.getColor("green");
-		final Color blue = Utils.getColor("blue");
-
-		//new N5AmazonS3Reader(  )
-	}
-
-	public static void main( String[] args )
-	{
-		new TestStringToColor().stringToColor();
-	}
-}
diff --git a/src/test/java/users/sultan/TransformXRayDataUsingAmiraEulerTransform.java b/src/test/java/users/sultan/TransformXRayDataUsingAmiraEulerTransform.java
index e840a2523..383a4ecb0 100644
--- a/src/test/java/users/sultan/TransformXRayDataUsingAmiraEulerTransform.java
+++ b/src/test/java/users/sultan/TransformXRayDataUsingAmiraEulerTransform.java
@@ -1,22 +1,22 @@
 package users.sultan;
 
-import de.embl.cba.mobie.utils.BatchApplyAmiraTransformationsToBdvXml;
+// import de.embl.cba.mobie.utils.BatchApplyAmiraTransformationsToBdvXml;
 
 import java.io.IOException;
 
 public class TransformXRayDataUsingAmiraEulerTransform
 {
-	public static void main( String[] args ) throws IOException
-	{
-		final BatchApplyAmiraTransformationsToBdvXml applyAmira = new BatchApplyAmiraTransformationsToBdvXml(
-				new double[]{ -0.27, 0.81, 0.51 },
-				71.7,
-				new double[]{ -54.6, 2.59, 53.39 },
-				"/Volumes/cba/exchange/Sultan",
-				".*-transform.xml"
-		);
-
-		applyAmira.run();
-	}
+	// public static void main( String[] args ) throws IOException
+	// {
+	// 	final BatchApplyAmiraTransformationsToBdvXml applyAmira = new BatchApplyAmiraTransformationsToBdvXml(
+	// 			new double[]{ -0.27, 0.81, 0.51 },
+	// 			71.7,
+	// 			new double[]{ -54.6, 2.59, 53.39 },
+	// 			"/Volumes/cba/exchange/Sultan",
+	// 			".*-transform.xml"
+	// 	);
+	//
+	// 	applyAmira.run();
+	// }
 
 }