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

Issue:
Bookmark refactoring
@K-Meech 
We should split the `BookmarksManager` into several classes.
At least we should factor out a `BookmarkViewer` and `BookmarkCreator` and `BookmarkFileWriter` and maybe see what is left to then decide what else?



Discussion / hints:
Yes - good plan! I can look at this later in the week, if you'd like.
Yes please thanks :)
The reason I want this cleaned up is that then it is easier to work on this: https://github.com/mobie/mobie/issues/20

😉 

But it should not be too much work, maybe I'll manage still today.

Fix patch:
diff --git a/pom.xml b/pom.xml
index a130639ca..8994da500 100644
--- a/pom.xml
+++ b/pom.xml
@@ -75,12 +75,16 @@
 		<scijava.app.directory>/Users/tischer/Desktop/Fiji/Fiji-MoBIE.app</scijava.app.directory>
 		<enforcer.skip>true</enforcer.skip>
 
-		<imagej-utils.version>0.5.10-SNAPSHOT</imagej-utils.version>
+		<!-- Java 8	Update Site	-->
+		<imglib2.version>5.11.1</imglib2.version>
+		<imglib2-cache.version>1.0.0-beta-16</imglib2-cache.version>
+		<bigdataviewer-core.version>10.2.0</bigdataviewer-core.version>
+		<bigdataviewer-vistools.version>1.0.0-beta-28</bigdataviewer-vistools.version>
 
-		<!--  Version 9.1.0 of BDV contains a critical fix for loading n5 data -->
-		<bigdataviewer-core.version>9.1.0</bigdataviewer-core.version>
-		<!-- https://forum.image.sc/t/error-when-visualizing-a-cachedcellimg-with-bigdataviewer/45872/5 -->
-		<bigdataviewer-vistools.version>1.0.0-beta-21</bigdataviewer-vistools.version>
+		<!-- BIOP -->
+
+		<!-- EMBL CBA -->
+		<imagej-utils.version>0.6.0</imagej-utils.version>
 
 		<!-- Version 1.6.0-scijava-3-SNAPSHOT of j3dcore addresses a minor issue
 		https://github.com/fiji/3D_Viewer/issues/26
@@ -114,6 +118,16 @@
 		</repository>
 	</repositories>
 	<dependencies>
+		<dependency>
+			<groupId>net.imglib2</groupId>
+			<artifactId>imglib2</artifactId>
+			<version>${imglib2.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>net.imglib2</groupId>
+			<artifactId>imglib2-cache</artifactId>
+			<version>${imglib2-cache.version}</version>
+		</dependency>
 		<dependency>
 			<groupId>sc.fiji</groupId>
 			<artifactId>bigdataviewer-core</artifactId>
diff --git a/src/main/java/de/embl/cba/mobie/bookmark/Bookmark.java b/src/main/java/de/embl/cba/mobie/bookmark/Bookmark.java
index 179b3805e..619a3ce18 100644
--- a/src/main/java/de/embl/cba/mobie/bookmark/Bookmark.java
+++ b/src/main/java/de/embl/cba/mobie/bookmark/Bookmark.java
@@ -11,4 +11,5 @@ public class Bookmark
 	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
new file mode 100644
index 000000000..b12f03337
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/bookmark/BookmarkManager.java
@@ -0,0 +1,260 @@
+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.ui.viewer.SourcesPanel;
+import de.embl.cba.mobie.bdv.BdvViewChanger;
+import de.embl.cba.mobie.utils.Utils;
+import de.embl.cba.tables.FileUtils.FileLocation;
+import de.embl.cba.tables.image.SourceAndMetadata;
+import net.imglib2.FinalRealInterval;
+import net.imglib2.realtransform.AffineTransform3D;
+
+import javax.swing.*;
+import java.io.IOException;
+import java.util.*;
+
+public class BookmarkManager
+{
+	private final SourcesPanel sourcesPanel;
+	private Map< String, Bookmark > nameToBookmark;
+	private final BookmarkReader bookmarkReader;
+	private JComboBox<String> bookmarkDropDown;
+	private final String datasetLocation;
+
+	public BookmarkManager( SourcesPanel sourcesPanel, Map< String, Bookmark > nameToBookmark,
+							BookmarkReader bookmarkReader )
+	{
+		this.sourcesPanel = sourcesPanel;
+		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 )
+		{
+			sourcesPanel.removeAllSourcesFromPanelAndViewers();
+			addSourcesToPanelAndViewer( 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 addSourcesToPanelAndViewer( Bookmark bookmark )
+	{
+		final HashMap< String, SourceAndMetadata > sourceNameToSourceAndMetadata = createSourcesAndMetadata( bookmark );
+
+		if ( sourceNameToSourceAndMetadata.size() == 0 ) return;
+
+		if ( bookmark.layouts != null  )
+		{
+			for ( String layoutName : bookmark.layouts.keySet() )
+			{
+				final Layout layout = bookmark.layouts.get( layoutName );
+				adjustSourceTransforms( sourceNameToSourceAndMetadata, layout );
+			}
+		}
+
+		for ( SourceAndMetadata< ? > sam : sourceNameToSourceAndMetadata.values() )
+		{
+			sourcesPanel.addSourceToPanelAndViewer( sam );
+		}
+	}
+
+	protected void adjustSourceTransforms( HashMap< String, SourceAndMetadata > sourceNameToSourceAndMetadata, Layout layout )
+	{
+		if ( layout.layoutType.equals( LayoutType.AutoGrid ) )
+		{
+			final int numSources = layout.layers.size();
+			final int numColumns = ( int ) Math.ceil( Math.sqrt( numSources ) );
+			FinalRealInterval bounds = estimateBounds( 0, layout, sourceNameToSourceAndMetadata );
+			final double spacingFactor = 0.1;
+			double border = spacingFactor * ( bounds.realMax( 0 ) - bounds.realMin( 0 ) );
+			double offsetX = bounds.realMax( 0 ) + border;
+			double offsetY = 0;
+			for ( int sourceIndex = 1, columnIndex = 1; 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;
+				}
+			}
+		}
+	}
+
+	protected FinalRealInterval estimateBounds( int sourceIndex, Layout layout, HashMap< String, SourceAndMetadata > sourceNameToSourceAndMetadata )
+	{
+		final String sourceName = layout.layers.get( sourceIndex );
+		final Source< ? > source = sourceNameToSourceAndMetadata.get( sourceName ).source();
+		final AffineTransform3D affineTransform3D = new AffineTransform3D();
+		source.getSourceTransform( 0, 0, affineTransform3D );
+		final FinalRealInterval bounds = affineTransform3D.estimateBounds( source.getSource( 0, 0 ) );
+		return bounds;
+	}
+
+	private HashMap< String, SourceAndMetadata > createSourcesAndMetadata( Bookmark bookmark )
+	{
+		final HashMap< String, SourceAndMetadata > sourceNameToSourceAndMetadata = new HashMap<>();
+		for ( String sourceName : bookmark.layers.keySet() )
+		{
+			if ( sourcesPanel.getVisibleSourceNames().contains( sourceName ) )
+				continue;
+
+			final Source< ? > source
+					= sourcesPanel.getSourceAndDefaultMetadata( sourceName ).source();
+
+			final Metadata metadata
+					= sourcesPanel.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( sourcesPanel.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 = sourcesPanel.getVisibleSourceNames();
+
+		for (String sourceName : visibleSourceNames) {
+			sourcesPanel.updateCurrentMetadata( sourceName );
+			MutableImageProperties imageProperties = getMutableImagePropertiesFromCurrentMetadata(sourceName);
+			layers.put(sourceName, imageProperties);
+		}
+
+		BdvHandle bdv = sourcesPanel.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 = sourcesPanel.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
new file mode 100644
index 000000000..6ba3e850c
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/bookmark/BookmarkReader.java
@@ -0,0 +1,93 @@
+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/BookmarksJsonParser.java b/src/main/java/de/embl/cba/mobie/bookmark/BookmarksJsonParser.java
deleted file mode 100644
index 77d8b13e8..000000000
--- a/src/main/java/de/embl/cba/mobie/bookmark/BookmarksJsonParser.java
+++ /dev/null
@@ -1,230 +0,0 @@
-package de.embl.cba.mobie.bookmark;
-
-import com.google.gson.ExclusionStrategy;
-import com.google.gson.FieldAttributes;
-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.tables.FileAndUrlUtils;
-import de.embl.cba.tables.FileUtils.FileLocation;
-import de.embl.cba.tables.github.GitHubUtils;
-import de.embl.cba.tables.github.GitLocation;
-
-import javax.swing.*;
-import javax.swing.filechooser.FileNameExtensionFilter;
-import java.io.*;
-import java.lang.reflect.Type;
-import java.nio.charset.StandardCharsets;
-import java.util.*;
-
-import static de.embl.cba.tables.FileUtils.selectPathFromProjectOrFileSystem;
-
-public class BookmarksJsonParser {
-	private final String datasetLocation;
-
-	public BookmarksJsonParser(String datasetLocation) {
-		this.datasetLocation = datasetLocation;
-	}
-
-	public String getDatasetLocation() {
-		return datasetLocation;
-	}
-
-	public Map<String, Bookmark> getDefaultBookmarks() {
-		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> selectAndLoadBookmarks() {
-		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;
-	}
-
-	public void saveBookmarksToGithub(ArrayList<Bookmark> bookmarks) {
-		final GitLocation gitLocation = GitHubUtils.rawUrlToGitLocation( datasetLocation );
-		gitLocation.path += "misc/bookmarks";
-
-		BookmarkGithubWriter bookmarkWriter = new BookmarkGithubWriter(gitLocation, this);
-		bookmarkWriter.writeBookmarksToGithub(bookmarks);
-	}
-
-	private 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;
-	}
-
-	private String  chooseJsonLocation( FileLocation fileLocation ) {
-		String jsonPath = null;
-		final JFileChooser jFileChooser;
-		if (fileLocation.equals(FileLocation.File_system)) {
-			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 void saveBookmarksToFile(ArrayList<Bookmark> bookmarks, FileLocation fileLocation) throws IOException {
-		HashMap<String, Bookmark> namesToBookmarks = new HashMap<>();
-		for (Bookmark bookmark : bookmarks) {
-			namesToBookmarks.put(bookmark.name, bookmark);
-		}
-
-		String jsonPath = chooseJsonLocation( fileLocation );
-
-		if ( jsonPath != null ) {
-			File jsonFile = new File( jsonPath );
-
-			saveBookmarksToFile( namesToBookmarks, jsonFile );
-		}
-
-	}
-
-	public void saveBookmarksToFile( Map<String, Bookmark> bookmarks, File jsonFile ) throws IOException {
-		Gson gson = createGsonBuilder(false);
-		Type type = new TypeToken<Map<String, Bookmark>>() {
-		}.getType();
-
-		writeBookmarksToFile(gson, type, jsonFile, bookmarks);
-	}
-
-	public 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;
-		}
-	}
-
-	private 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;
-		}
-	}
-
-	private 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(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 String writeBookmarksToBase64String (Map<String, Bookmark> bookmarks) {
-		Gson gson = 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?
-	}
-}
diff --git a/src/main/java/de/embl/cba/mobie/bookmark/BookmarksManager.java b/src/main/java/de/embl/cba/mobie/bookmark/BookmarksManager.java
deleted file mode 100644
index c09c432cb..000000000
--- a/src/main/java/de/embl/cba/mobie/bookmark/BookmarksManager.java
+++ /dev/null
@@ -1,201 +0,0 @@
-package de.embl.cba.mobie.bookmark;
-
-import bdv.util.BdvHandle;
-import de.embl.cba.bdv.utils.BdvUtils;
-import de.embl.cba.bdv.utils.sources.Metadata;
-import de.embl.cba.mobie.image.ImagePropertiesToMetadataAdapter;
-import de.embl.cba.mobie.image.MutableImageProperties;
-import de.embl.cba.mobie.ui.viewer.SourcesPanel;
-import de.embl.cba.mobie.bdv.BdvViewChanger;
-import de.embl.cba.mobie.utils.Utils;
-import de.embl.cba.tables.FileUtils.FileLocation;
-import de.embl.cba.tables.image.SourceAndMetadata;
-import ij.gui.GenericDialog;
-
-import javax.swing.*;
-import java.io.IOException;
-import java.util.*;
-
-public class BookmarksManager
-{
-	private final SourcesPanel sourcesPanel;
-	private Map< String, Bookmark > nameToBookmark;
-	private BookmarksJsonParser bookmarksJsonParser;
-	private JComboBox<String> bookmarkDropDown;
-
-	public BookmarksManager( SourcesPanel sourcesPanel, Map< String, Bookmark > nameToBookmark,
-							 BookmarksJsonParser bookmarksJsonParser )
-	{
-		this.sourcesPanel = sourcesPanel;
-		this.nameToBookmark = nameToBookmark;
-		this.bookmarksJsonParser = bookmarksJsonParser;
-	}
-
-	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 )
-		{
-			sourcesPanel.removeAllSourcesFromPanelAndViewers();
-			addSourcesToPanelAndViewer( 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 addSourcesToPanelAndViewer( Bookmark bookmark )
-	{
-		for ( Map.Entry< String, MutableImageProperties> entry : bookmark.layers.entrySet() )
-		{
-			final String sourceName = entry.getKey();
-			if ( ! sourcesPanel.getVisibleSourceNames().contains( sourceName ) )
-			{
-				final SourceAndMetadata< ? > samDefault = sourcesPanel.getSourceAndDefaultMetadata( sourceName );
-				final SourceAndMetadata< ? > samBookmark = new SourceAndMetadata(samDefault.source(), samDefault.metadata().copy());
-				updateSourceMetadata(entry, samBookmark.metadata());
-
-				sourcesPanel.addSourceToPanelAndViewer( samBookmark );
-			}
-		}
-	}
-
-	public void updateSourceMetadata( Map.Entry< String, MutableImageProperties > entry, Metadata sourceMetadata )
-	{
-		final ImagePropertiesToMetadataAdapter adapter = new ImagePropertiesToMetadataAdapter();
-		adapter.setMetadataFromMutableImageProperties( sourceMetadata, entry.getValue() );
-	}
-
-	public void adaptViewerTransform( Bookmark bookmark )
-	{
-		final Location location = getLocationFromBookmark( bookmark );
-
-		if ( location != null )
-		{
-			BdvViewChanger.moveToLocation( sourcesPanel.getBdv(), location );
-		}
-	}
-
-	public void loadAdditionalBookmarks() {
-			Map<String, Bookmark> additionalBookmarks = bookmarksJsonParser.selectAndLoadBookmarks();
-			if (additionalBookmarks != null) {
-				nameToBookmark.putAll(additionalBookmarks);
-				bookmarkDropDown.removeAllItems();
-				for (String bookmarkName : nameToBookmark.keySet()) {
-					bookmarkDropDown.addItem(bookmarkName);
-				}
-			}
-	}
-
-	public void saveCurrentSettingsAsBookmark () {
-		NameAndLocation bookmarkNameAndLocation = bookmarkSaveDialog();
-		Bookmark currentBookmark = createBookmarkFromCurrentSettings(bookmarkNameAndLocation.name);
-		ArrayList<Bookmark> bookmarks = new ArrayList<>();
-		bookmarks.add(currentBookmark);
-
-		if ( bookmarkNameAndLocation.location.equals( FileLocation.Project ) &&
-				bookmarksJsonParser.getDatasetLocation().contains( "raw.githubusercontent" )) {
-			bookmarksJsonParser.saveBookmarksToGithub(bookmarks);
-		} else {
-			try {
-				bookmarksJsonParser.saveBookmarksToFile(bookmarks, bookmarkNameAndLocation.location);
-			} catch (IOException e) {
-				e.printStackTrace();
-			}
-		}
-	}
-
-	class NameAndLocation {
-		String name;
-		FileLocation location;
-	}
-
-	private NameAndLocation bookmarkSaveDialog () {
-		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[]{ FileLocation.Project.toString(),
-				FileLocation.File_system.toString() }, FileLocation.Project.toString() );
-		gd.showDialog();
-
-		if ( gd.wasCanceled() ) return null;
-		bookmarkName = gd.getNextString();
-		fileLocation = FileLocation.valueOf( gd.getNextChoice() );
-
-		NameAndLocation bookmarkNameAndLocation = new NameAndLocation();
-		bookmarkNameAndLocation.name = bookmarkName;
-		bookmarkNameAndLocation.location = fileLocation;
-
-		return bookmarkNameAndLocation;
-	}
-
-	public Bookmark createBookmarkFromCurrentSettings(String bookmarkName) {
-		HashMap< String, MutableImageProperties > layers = new HashMap<>();
-		Set<String> visibleSourceNames = sourcesPanel.getVisibleSourceNames();
-
-		for (String sourceName : visibleSourceNames) {
-			sourcesPanel.updateCurrentMetadata( sourceName );
-			MutableImageProperties imageProperties = getMutableImagePropertiesFromCurrentMetadata(sourceName);
-			layers.put(sourceName, imageProperties);
-		}
-
-		BdvHandle bdv = sourcesPanel.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 = sourcesPanel.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/Layout.java b/src/main/java/de/embl/cba/mobie/bookmark/Layout.java
new file mode 100644
index 000000000..f973d843b
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/bookmark/Layout.java
@@ -0,0 +1,9 @@
+package de.embl.cba.mobie.bookmark;
+
+import java.util.ArrayList;
+
+public class Layout
+{
+	public ArrayList< String > layers;
+	public LayoutType layoutType;
+}
diff --git a/src/main/java/de/embl/cba/mobie/bookmark/LayoutType.java b/src/main/java/de/embl/cba/mobie/bookmark/LayoutType.java
new file mode 100644
index 000000000..1b40c5973
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/bookmark/LayoutType.java
@@ -0,0 +1,6 @@
+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
new file mode 100644
index 000000000..d9e977ffa
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/bookmark/write/BookmarkFileWriter.java
@@ -0,0 +1,104 @@
+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/BookmarkGithubWriter.java b/src/main/java/de/embl/cba/mobie/bookmark/write/BookmarkGithubWriter.java
similarity index 82%
rename from src/main/java/de/embl/cba/mobie/bookmark/BookmarkGithubWriter.java
rename to src/main/java/de/embl/cba/mobie/bookmark/write/BookmarkGithubWriter.java
index a15a04f64..ee69c7e10 100644
--- a/src/main/java/de/embl/cba/mobie/bookmark/BookmarkGithubWriter.java
+++ b/src/main/java/de/embl/cba/mobie/bookmark/write/BookmarkGithubWriter.java
@@ -1,7 +1,11 @@
-package de.embl.cba.mobie.bookmark;
+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;
@@ -10,10 +14,9 @@
 import org.apache.commons.compress.utils.FileNameUtils;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
 
 public class BookmarkGithubWriter {
 
@@ -21,11 +24,21 @@ public class BookmarkGithubWriter {
     private String accessToken;
     private String bookmarkFileName;
     private GitLocation bookmarkGitLocation;
-    private BookmarksJsonParser bookmarksJsonParser;
+    private BookmarkReader bookmarkReader;
 
-    BookmarkGithubWriter(GitLocation bookmarkGitLocation, BookmarksJsonParser bookmarksJsonParser) {
+    BookmarkGithubWriter(GitLocation bookmarkGitLocation, BookmarkReader bookmarkReader ) {
         this.bookmarkGitLocation = bookmarkGitLocation;
-        this.bookmarksJsonParser = bookmarksJsonParser;
+        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()
@@ -90,7 +103,7 @@ public void writeBookmarksToGithub(ArrayList<Bookmark> bookmarks) {
 
                 boolean appendToFile = false;
                 if (matchingFilePathAndSha != null) {
-                    appendToFile = bookmarksJsonParser.appendToFileDialog();
+                    appendToFile = BookmarkFileWriter.appendToFileDialog();
                 }
 
                 // don't continue if matching file was found, but user does not want to append to it
@@ -101,12 +114,12 @@ public void writeBookmarksToGithub(ArrayList<Bookmark> bookmarks) {
                     if (appendToFile) {
                         ArrayList<String> matchingFilePathsFromGithub = new ArrayList<>();
                         matchingFilePathsFromGithub.add(matchingFilePathAndSha.filePath);
-                        Map<String, Bookmark> existingBookmarks = bookmarksJsonParser.parseBookmarks(matchingFilePathsFromGithub);
+                        Map<String, Bookmark> existingBookmarks = bookmarkReader.parseBookmarks(matchingFilePathsFromGithub);
                         bookmarksInFile.putAll(existingBookmarks);
                     }
                     bookmarksInFile.putAll(namesToBookmarks);
 
-                    final String bookmarkJsonBase64String = bookmarksJsonParser.writeBookmarksToBase64String(bookmarksInFile);
+                    final String bookmarkJsonBase64String = writeBookmarksToBase64String(bookmarksInFile);
 
                     final GitHubFileCommitter fileCommitter;
                     if (appendToFile) {
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
new file mode 100644
index 000000000..74608fcdf
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/bookmark/write/BookmarkGsonBuilderCreator.java
@@ -0,0 +1,36 @@
+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
new file mode 100644
index 000000000..d2d6f6721
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/bookmark/write/BookmarkWriter.java
@@ -0,0 +1,41 @@
+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
new file mode 100644
index 000000000..ef5051f59
--- /dev/null
+++ b/src/main/java/de/embl/cba/mobie/bookmark/write/NameAndFileLocation.java
@@ -0,0 +1,9 @@
+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/image/SourcesModel.java b/src/main/java/de/embl/cba/mobie/image/SourcesModel.java
index 9c8cfb2bb..560988584 100644
--- a/src/main/java/de/embl/cba/mobie/image/SourcesModel.java
+++ b/src/main/java/de/embl/cba/mobie/image/SourcesModel.java
@@ -20,35 +20,25 @@
 
 public class SourcesModel implements ImageSourcesModel
 {
-	public static final String SEGMENTATION_ID = "-segmented" ;
-	public static final String BDV_XML_SUFFIX = ".xml";
-	public static final String EM_RAW_FILE_ID = "-raw";
 	public static final String EM_ID = "em-";
 	public static final String XRAY_ID = "xray-";
-	public static final String MASK_FILE_ID = "mask-";
-	private final String imageDataLocation;
 	private final MoBIEOptions.ImageDataStorageModality imageDataStorageModality;
 
 	private Map< String, SourceAndMetadata< ? > > nameToSourceAndDefaultMetadata;
 	private final String tableDataLocation;
-	private GlasbeyARGBLut glasbeyARGBLut;
 	private String storageModality;
 	private String imageRootLocation;
 
 	public SourcesModel( String imageDataLocation, MoBIEOptions.ImageDataStorageModality imageDataStorageModality, String tableDataLocation )
 	{
-		this.imageDataLocation = imageDataLocation;
 		this.imageDataStorageModality = imageDataStorageModality;
 		this.tableDataLocation = tableDataLocation;
 
 		nameToSourceAndDefaultMetadata = new HashMap<>();
-		glasbeyARGBLut = new GlasbeyARGBLut();
 
 		fetchSources( imageDataLocation );
 	}
 
-
-
 	@Override
 	public Map< String, SourceAndMetadata< ? > > sources()
 	{
diff --git a/src/main/java/de/embl/cba/mobie/projects/ProjectsCreator.java b/src/main/java/de/embl/cba/mobie/projects/ProjectsCreator.java
index 288d6768a..69b3290dd 100644
--- a/src/main/java/de/embl/cba/mobie/projects/ProjectsCreator.java
+++ b/src/main/java/de/embl/cba/mobie/projects/ProjectsCreator.java
@@ -6,7 +6,8 @@
 import bdv.spimdata.XmlIoSpimDataMinimal;
 import de.embl.cba.bdv.utils.sources.LazySpimSource;
 import de.embl.cba.mobie.bookmark.Bookmark;
-import de.embl.cba.mobie.bookmark.BookmarksJsonParser;
+import de.embl.cba.mobie.bookmark.write.BookmarkFileWriter;
+import de.embl.cba.mobie.bookmark.BookmarkReader;
 import de.embl.cba.mobie.dataset.Datasets;
 import de.embl.cba.mobie.dataset.DatasetsParser;
 import de.embl.cba.mobie.image.*;
@@ -392,7 +393,7 @@ public void updateCurrentImageProperties(String datasetName ) {
     public void updateCurrentDefaultBookmarks( String datasetName ) {
         File defaultBookmarkJson = new File ( getDefaultBookmarkJsonPath( datasetName ) );
         if ( defaultBookmarkJson.exists() ) {
-            currentDefaultBookmarks = new BookmarksJsonParser( getDatasetPath( datasetName ) ).getDefaultBookmarks();
+            currentDefaultBookmarks = new BookmarkReader( getDatasetPath( datasetName ) ).readDefaultBookmarks();
         } else {
             currentDefaultBookmarks = new HashMap<>();
         }
@@ -512,7 +513,7 @@ public void removeImageFromDefaultBookmark( String imageName, String datasetName
 
     public void writeDefaultBookmarksJson ( String datasetName ) {
         try {
-            new BookmarksJsonParser( getDatasetPath( datasetName) ).saveBookmarksToFile( currentDefaultBookmarks, new File (getDefaultBookmarkJsonPath( datasetName )) );
+            BookmarkFileWriter.saveBookmarksToFile( currentDefaultBookmarks, new File (getDefaultBookmarkJsonPath( datasetName )) );
         } catch (IOException e) {
             e.printStackTrace();
         }
diff --git a/src/main/java/de/embl/cba/mobie/ui/viewer/ActionPanel.java b/src/main/java/de/embl/cba/mobie/ui/viewer/ActionPanel.java
index 4d357181c..6add8c1b0 100644
--- a/src/main/java/de/embl/cba/mobie/ui/viewer/ActionPanel.java
+++ b/src/main/java/de/embl/cba/mobie/ui/viewer/ActionPanel.java
@@ -9,7 +9,7 @@
 import de.embl.cba.mobie.bookmark.LocationType;
 import de.embl.cba.mobie.platybrowser.GeneSearch;
 import de.embl.cba.mobie.platybrowser.GeneSearchResults;
-import de.embl.cba.mobie.bookmark.BookmarksManager;
+import de.embl.cba.mobie.bookmark.BookmarkManager;
 import de.embl.cba.mobie.bdv.BdvViewChanger;
 import de.embl.cba.mobie.ui.UniverseConfigurationDialog;
 import de.embl.cba.mobie.utils.Utils;
@@ -43,7 +43,7 @@ public class ActionPanel extends JPanel
 	private BdvHandle bdv;
 	private final MoBIEViewer moBIEViewer;
 	private final ArrayList< String > datasets;
-	private final BookmarksManager bookmarksManager;
+	private final BookmarkManager bookmarkManager;
 	private Behaviours behaviours;
 
 	private double[] levelingVector;
@@ -58,7 +58,7 @@ public ActionPanel( MoBIEViewer moBIEViewer )
 		this.moBIEViewer = moBIEViewer;
 		this.sourcesPanel = moBIEViewer.getSourcesPanel();
 		this.datasets = moBIEViewer.getDatasets();
-		this.bookmarksManager = moBIEViewer.getBookmarksManager();
+		this.bookmarkManager = moBIEViewer.getBookmarkManager();
 		this.levelingVector = moBIEViewer.getLevelingVector();
 		this.projectLocation = moBIEViewer.getProjectLocation();
 
@@ -68,7 +68,7 @@ public ActionPanel( MoBIEViewer moBIEViewer )
 		this.add( new JSeparator( SwingConstants.HORIZONTAL ) );
 		addSourceSelectionUI( this );
 		this.add( new JSeparator( SwingConstants.HORIZONTAL ) );
-		bookmarksManager.setBookmarkDropDown( addBookmarksUI( this  ) );
+		bookmarkManager.setBookmarkDropDown( addBookmarksUI( this  ) );
 		addMoveToLocationUI( this );
 		addLevelingUI( this );
 		configPanel();
@@ -106,14 +106,14 @@ private void installBdvBehavioursAndPopupMenu()
 		BdvPopupMenus.addAction(bdv, "Load Additional Bookmarks",
 				() -> {
 					new Thread( () -> {
-						SwingUtilities.invokeLater( () -> bookmarksManager.loadAdditionalBookmarks() );
+						SwingUtilities.invokeLater( () -> bookmarkManager.loadAdditionalBookmarks() );
 					} ).start();
 				});
 
 		BdvPopupMenus.addAction(bdv, "Save Current Settings As Bookmark",
 				() -> {
 					new Thread( () -> {
-						SwingUtilities.invokeLater( () -> bookmarksManager.saveCurrentSettingsAsBookmark() );
+						SwingUtilities.invokeLater( () -> bookmarkManager.saveCurrentSettingsAsBookmark() );
 						} ).start();
 				});
 
@@ -262,7 +262,7 @@ public void searchGenes( double[] micrometerPosition, double micrometerRadius )
 		GeneSearch geneSearch = new GeneSearch(
 				micrometerRadius,
 				micrometerPosition,
-				sourcesPanel.getImageSourcesModel() );
+				sourcesPanel.getSourcesModel() );
 
 		final Map< String, Double > geneExpressionLevels = geneSearch.runSearchAndGetLocalExpression();
 
@@ -504,7 +504,7 @@ private JComboBox<String> addBookmarksUI( JPanel panel )
 
 		final JComboBox< String > comboBox = new JComboBox<>( bookmarkNames );
 		setComboBoxDimensions( comboBox );
-		button.addActionListener( e -> bookmarksManager.setView( ( String ) comboBox.getSelectedItem() ) );
+		button.addActionListener( e -> bookmarkManager.setView( ( String ) comboBox.getSelectedItem() ) );
 
 		horizontalLayoutPanel.add( getJLabel( "bookmark" ) );
 		horizontalLayoutPanel.add( comboBox );
@@ -599,7 +599,7 @@ private void switchDataset( String dataset )
 	// TODO simplify code below
 	private String[] getBookmarkNames()
 	{
-		final Set< String > viewNames = bookmarksManager.getBookmarkNames();
+		final Set< String > viewNames = bookmarkManager.getBookmarkNames();
 		final String[] positionsAndViews = new String[ viewNames.size() ];
 //		positionsAndViews[ 0 ] = "...type here...";
 		final Iterator< String > iterator = viewNames.iterator();
diff --git a/src/main/java/de/embl/cba/mobie/ui/viewer/MoBIEViewer.java b/src/main/java/de/embl/cba/mobie/ui/viewer/MoBIEViewer.java
index aff3b8dd6..cd2c5ceed 100644
--- a/src/main/java/de/embl/cba/mobie/ui/viewer/MoBIEViewer.java
+++ b/src/main/java/de/embl/cba/mobie/ui/viewer/MoBIEViewer.java
@@ -8,12 +8,11 @@
 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.BookmarksJsonParser;
-import de.embl.cba.mobie.bookmark.BookmarksManager;
+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 ij.WindowManager;
-import ij.gui.NonBlockingGenericDialog;
 import net.imglib2.realtransform.AffineTransform3D;
 
 import javax.swing.*;
@@ -40,7 +39,7 @@ public class MoBIEViewer
 	private String tablesLocation;
 
 	private int frameWidth;
-	private BookmarksManager bookmarksManager;
+	private BookmarkManager bookmarkManager;
 	private Datasets datasets;
 	private final double[] levelingVector;
 	private final JFrame jFrame;
@@ -75,7 +74,7 @@ public MoBIEViewer(
 		sourcesModel = new SourcesModel( imagesLocation, options.values.getImageDataStorageModality(), tablesLocation );
 		sourcesPanel = new SourcesPanel( sourcesModel, projectName );
 
-		bookmarksManager = fetchBookmarks( projectLocation );
+		bookmarkManager = fetchBookmarks( projectLocation );
 		levelingVector = fetchLeveling( imagesLocation );
 
 		actionPanel = new ActionPanel( this );
@@ -83,7 +82,7 @@ public MoBIEViewer(
 		jFrame = new JFrame( "MoBIE: " + projectName + "-" + dataset );
 
 		// open bdv and show default bookmark (this will also initialise the bdv in sourcesPanel)
-		bookmarksManager.setView( "default" );
+		bookmarkManager.setView( "default" );
 		actionPanel.setBdvAndInstallBehavioursAndPopupMenu( sourcesPanel.getBdv() );
 		defaultNormalisedViewerTransform = Utils.createNormalisedViewerTransform( sourcesPanel.getBdv(), BdvUtils.getBdvWindowCenter( sourcesPanel.getBdv() ) );
 
@@ -130,9 +129,9 @@ public ArrayList< String > getDatasets()
 		return datasets.datasets;
 	}
 
-	public BookmarksManager getBookmarksManager()
+	public BookmarkManager getBookmarkManager()
 	{
-		return bookmarksManager;
+		return bookmarkManager;
 	}
 
 	private double[] fetchLeveling( String dataLocation )
@@ -199,12 +198,12 @@ public String adaptUrl( String url, String projectBranch )
 		return url;
 	}
 
-	public BookmarksManager fetchBookmarks( String location )
+	public BookmarkManager fetchBookmarks( String location )
 	{
-		BookmarksJsonParser bookmarkParser = new BookmarksJsonParser(location);
-		Map< String, Bookmark > nameToBookmark = bookmarkParser.getDefaultBookmarks();
+		BookmarkReader bookmarkParser = new BookmarkReader(location);
+		Map< String, Bookmark > nameToBookmark = bookmarkParser.readDefaultBookmarks();
 
-		return new BookmarksManager( sourcesPanel, nameToBookmark, bookmarkParser);
+		return new BookmarkManager( sourcesPanel, nameToBookmark, bookmarkParser);
 	}
 
 	public void setLogWindowPositionAndSize( JFrame jFrame )
diff --git a/src/main/java/de/embl/cba/mobie/ui/viewer/SourcesDisplayUI.java b/src/main/java/de/embl/cba/mobie/ui/viewer/SourcesDisplayUI.java
index 2b1d05de5..ea2f94bcf 100644
--- a/src/main/java/de/embl/cba/mobie/ui/viewer/SourcesDisplayUI.java
+++ b/src/main/java/de/embl/cba/mobie/ui/viewer/SourcesDisplayUI.java
@@ -8,9 +8,7 @@
 import java.awt.*;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
-import java.util.ArrayList;
-
-import static de.embl.cba.bdv.utils.BdvUtils.getConverterSetups;
+import java.util.List;
 
 public class SourcesDisplayUI
 {
@@ -76,8 +74,7 @@ public static JButton createBrightnessButton( int[] buttonDimensions,
 
 		button.addActionListener( e ->
 		{
-			final ArrayList< ConverterSetup > converterSetups
-					= getConverterSetups( sam.metadata().bdvStackSource );
+			final List< ConverterSetup > converterSetups = sam.metadata().bdvStackSource.getConverterSetups();
 
 			BdvDialogs.showBrightnessDialog(
 					sam.metadata().displayName,
diff --git a/src/main/java/de/embl/cba/mobie/ui/viewer/SourcesPanel.java b/src/main/java/de/embl/cba/mobie/ui/viewer/SourcesPanel.java
index d30e4e010..1a4feac40 100644
--- a/src/main/java/de/embl/cba/mobie/ui/viewer/SourcesPanel.java
+++ b/src/main/java/de/embl/cba/mobie/ui/viewer/SourcesPanel.java
@@ -36,13 +36,10 @@
 
 import javax.swing.*;
 import java.awt.*;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
 import java.net.URI;
-import java.util.*;
 import java.util.List;
+import java.util.*;
 
-import static de.embl.cba.bdv.utils.BdvUtils.getConverterSetups;
 import static de.embl.cba.mobie.utils.Utils.createAnnotatedImageSegmentsFromTableFile;
 import static de.embl.cba.mobie.utils.Utils.createRandom;
 
@@ -52,15 +49,14 @@ public class SourcesPanel extends JPanel
     private Map< String, JPanel > sourceNameToPanel;
     private Map< String, SourceAndMetadata< ? > > sourceNameToSourceAndCurrentMetadata;
     private BdvHandle bdv;
-    private final SourcesModel imageSourcesModel;
+    private final SourcesModel sourcesModel;
     private final String projectName;
     private Image3DUniverse universe;
-    private int meshSmoothingIterations = 5;
-    private int numRenderingThreads;
+    private int meshSmoothingIterations = 5; // TODO: Why is this here?
 
-    public SourcesPanel( SourcesModel imageSourcesModel, String projectName )
+    public SourcesPanel( SourcesModel sourcesModel, String projectName )
     {
-        this.imageSourcesModel = imageSourcesModel;
+        this.sourcesModel = sourcesModel;
         this.projectName = projectName;
         sourceNameToPanel = new LinkedHashMap<>();
         sourceNameToLabelViews = new LinkedHashMap<>();
@@ -98,11 +94,9 @@ public static void updateSegments3dView( SourceAndMetadata< ? > sam, SourcesPane
 
     private JButton createColorButton( JPanel panel, int[] buttonDimensions, SourceAndMetadata< ? > sam )
     {
-        JButton colorButton;
-        colorButton = new JButton( "C" );
+        JButton colorButton = new JButton( "C" );
 
-        colorButton.setPreferredSize(
-                new Dimension( buttonDimensions[ 0 ], buttonDimensions[ 1 ] ) );
+        colorButton.setPreferredSize( new Dimension( buttonDimensions[ 0 ], buttonDimensions[ 1 ] ) );
 
         colorButton.addActionListener( e -> {
             Color color = JColorChooser.showDialog( null, "", null );
@@ -110,7 +104,6 @@ private JButton createColorButton( JPanel panel, int[] buttonDimensions, SourceA
             if ( color == null ) return;
 
             setSourceColor( sam, color, panel );
-
         } );
 
         return colorButton;
@@ -239,8 +232,8 @@ public void updateCurrentMetadata( String sourceName )
         }
 
         double[] currentContrastLimits = new double[2];
-        currentContrastLimits[0] = getConverterSetups( bdvStackSource ).get(0).getDisplayRangeMin();
-        currentContrastLimits[1] = getConverterSetups( bdvStackSource ).get(0).getDisplayRangeMax();
+        currentContrastLimits[0] = bdvStackSource.getConverterSetups().get(0).getDisplayRangeMin();
+        currentContrastLimits[1] = bdvStackSource.getConverterSetups().get(0).getDisplayRangeMax();
         metadata.contrastLimits = currentContrastLimits;
     }
 
@@ -397,7 +390,7 @@ public void addSourceToPanelAndViewer( String sourceName )
 
     public SourceAndMetadata< ? > getSourceAndDefaultMetadata( String sourceName )
     {
-        return imageSourcesModel.sources().get( sourceName );
+        return sourcesModel.sources().get( sourceName );
     }
 
     // necessary as loading from bookmark creates a new sourceAndMetadata that is separate from the default
@@ -408,7 +401,7 @@ public void addSourceToPanelAndViewer( String sourceName )
 
     public ArrayList< String > getSourceNames()
     {
-        return new ArrayList<>( imageSourcesModel.sources().keySet() );
+        return new ArrayList<>( sourcesModel.sources().keySet() );
     }
 
     public Set< String > getVisibleSourceNames()
@@ -416,9 +409,9 @@ public Set< String > getVisibleSourceNames()
         return Collections.unmodifiableSet( new HashSet<>( sourceNameToPanel.keySet() ) );
     }
 
-    public SourcesModel getImageSourcesModel()
+    public SourcesModel getSourcesModel()
     {
-        return imageSourcesModel;
+        return sourcesModel;
     }
 
     private void configPanel()
@@ -442,11 +435,13 @@ public void addSourceToPanelAndViewer( SourceAndMetadata< ? > sam )
             sourceNameToSourceAndCurrentMetadata.put( sourceName, sam );
 
             addSourceToViewer( sam );
+            //bdv.getViewerPanel().state().addSourcesToGroup(  )
+
             SwingUtilities.invokeLater( () -> addSourceToPanel( sam ) );
         }
     }
 
-    private void addSourceToViewer( SourceAndMetadata< ? > sam )
+    public void addSourceToViewer( SourceAndMetadata< ? > sam )
     {
         Prefs.showScaleBar( true );
         Prefs.showMultibox( false );
@@ -808,18 +803,9 @@ private JButton createRemoveButton(
             int[] buttonDimensions )
     {
         JButton removeButton = new JButton( "X" );
-        removeButton.setPreferredSize(
-                new Dimension( buttonDimensions[ 0 ], buttonDimensions[ 1 ] ) );
+        removeButton.setPreferredSize( new Dimension( buttonDimensions[ 0 ], buttonDimensions[ 1 ] ) );
 
-        removeButton.addActionListener(
-                new ActionListener()
-                {
-                    @Override
-                    public void actionPerformed( ActionEvent e )
-                    {
-                        removeSourceFromPanelAndViewers( sam );
-                    }
-                } );
+        removeButton.addActionListener( e -> removeSourceFromPanelAndViewers( sam ) );
 
         return removeButton;
     }


Test patch:
diff --git a/src/test/java/projects/OpenRemoteTomograms.java b/src/test/java/projects/OpenRemoteTomograms.java
index 21c96dac7..fa2c9f108 100644
--- a/src/test/java/projects/OpenRemoteTomograms.java
+++ b/src/test/java/projects/OpenRemoteTomograms.java
@@ -1,5 +1,6 @@
 package projects;
 
+import de.embl.cba.mobie.ui.viewer.MoBIEOptions;
 import de.embl.cba.mobie.ui.viewer.MoBIEViewer;
 import net.imagej.ImageJ;
 
diff --git a/src/test/java/projects/OpenRemoteTomogramsBranch.java b/src/test/java/projects/OpenRemoteTomogramsBranch.java
new file mode 100644
index 000000000..bb0254a0f
--- /dev/null
+++ b/src/test/java/projects/OpenRemoteTomogramsBranch.java
@@ -0,0 +1,16 @@
+package projects;
+
+import de.embl.cba.mobie.ui.viewer.MoBIEOptions;
+import de.embl.cba.mobie.ui.viewer.MoBIEViewer;
+import net.imagej.ImageJ;
+
+public class OpenRemoteTomogramsBranch
+{
+	public static void main( String[] args )
+	{
+		final ImageJ imageJ = new ImageJ();
+		imageJ.ui().showUI();
+
+		new MoBIEViewer("https://github.com/mobie/covid-tomo-datasets", MoBIEOptions.options().gitProjectBranch( "grid-test" )  );
+	}
+}