public class ConfirmSegmentController
{
	final protected ViewerPanel viewer;
	final protected SelectionController selectionController;
	final protected FragmentSegmentAssignment assignment;
	final protected SegmentAssignment completeSegments;
	final protected AbstractARGBStream colorStream;
	final protected Wheel modeWheel;

	// for keystroke actions
	private final ActionMap ksActionMap = new ActionMap();
	private final InputMap ksInputMap = new InputMap();
	private final KeyStrokeAdder ksKeyStrokeAdder;

	public ConfirmSegmentController(
			final ViewerPanel viewer,
			final SelectionController selectionController,
			final FragmentSegmentAssignment assignment,
			final SegmentAssignment completeSegments,
			final AbstractARGBStream colorStream,
			final Wheel modeWheel,
			final InputTriggerConfig config,
			final InputActionBindings inputActionBindings )
	{
		this.viewer = viewer;
		this.selectionController = selectionController;
		this.assignment = assignment;
		this.completeSegments = completeSegments;
		this.colorStream = colorStream;
		this.modeWheel = modeWheel;
		ksKeyStrokeAdder = config.keyStrokeAdder( ksInputMap, "confirm segment" );

		new ConfirmSegment( "confirm segment", "U" ).register();
		new AdvanceVisibilityMode( "advance visibility mode", "J" ).register();
		new RegressVisibilityMode( "regress visibility mode", "shift J" ).register();

		inputActionBindings.addActionMap( "confirm segment", ksActionMap );
		inputActionBindings.addInputMap( "confirm segment", ksInputMap );
	}

	private abstract class SelfRegisteringAction extends AbstractNamedAction
	{
		private final String[] defaultTriggers;

		public SelfRegisteringAction( final String name, final String ... defaultTriggers )
		{
			super( name );
			this.defaultTriggers = defaultTriggers;
		}

		public void register()
		{
			put( ksActionMap );
			ksKeyStrokeAdder.put( name(), defaultTriggers );
		}
	}

	private class ConfirmSegment extends SelfRegisteringAction
	{
		public ConfirmSegment( final String name, final String ... defaultTriggers )
		{
			super( name, defaultTriggers );
		}

		@Override
		public void actionPerformed( final ActionEvent e )
		{
			long activeSegmentId;
			synchronized ( viewer )
			{
				final long activeFragmentId = selectionController.getActiveFragmentId();
				activeSegmentId = assignment.getSegment( activeFragmentId );
				completeSegments.add( activeSegmentId );
				colorStream.clearCache();
			}
			viewer.showMessage( "completed segment " + activeSegmentId );
			viewer.requestRepaint();
		}
	}

	private class AdvanceVisibilityMode extends SelfRegisteringAction
	{
		public AdvanceVisibilityMode( final String name, final String ... defaultTriggers )
		{
			super( name, defaultTriggers );
		}

		@Override
		public void actionPerformed( final ActionEvent e )
		{
			synchronized ( viewer )
			{
				modeWheel.advance();
			}
			viewer.requestRepaint();
		}
	}

	private class RegressVisibilityMode extends SelfRegisteringAction
	{
		public RegressVisibilityMode( final String name, final String ... defaultTriggers )
		{
			super( name, defaultTriggers );
		}

		@Override
		public void actionPerformed( final ActionEvent e )
		{
			synchronized ( viewer )
			{
				modeWheel.regress();
			}
			viewer.requestRepaint();
		}
	}
} public class SelectionController
{
	final protected ViewerPanel viewer;
	final protected IdPicker idPicker;
	final protected AbstractSaturatedARGBStream colorStream;
	final protected IdService idService;

	final protected SelectionOverlay selectionOverlay;

	protected long activeFragmentId = 0;
	protected long hoverFragmentId = 0;
	protected RealPoint lastClick = new RealPoint(3);

	// for behavioUrs
	private final BehaviourMap behaviourMap = new BehaviourMap();
	private final InputTriggerMap inputTriggerMap = new InputTriggerMap();
	private final InputTriggerAdder inputAdder;

	// for keystroke actions
	private final ActionMap ksActionMap = new ActionMap();
	private final InputMap ksInputMap = new InputMap();
	private final KeyStrokeAdder ksKeyStrokeAdder;

	public SelectionController(
			final ViewerPanel viewer,
			final IdPicker idPicker,
			final AbstractSaturatedARGBStream colorStream,
			final IdService idService,
			final FragmentSegmentAssignment assignment,
			final InputTriggerConfig config,
			final InputActionBindings inputActionBindings,
			final KeyStrokeAdder.Factory keyProperties )
	{
		this.viewer = viewer;
		this.idPicker = idPicker;
		this.colorStream = colorStream;
		this.idService = idService;

		selectionOverlay = new SelectionOverlay( viewer, this, assignment, colorStream );

		inputAdder = config.inputTriggerAdder( inputTriggerMap, "select" );
		ksKeyStrokeAdder = keyProperties.keyStrokeAdder( ksInputMap, "select" );

		new SelectFragment( "select fragment", "button1" ).register();

		/* no fancy behavior for simple hovering yet */
		viewer.getDisplay().addMouseMotionListener( new HoverFragment() );

		new NewActiveFragmentId( "new fragment", "N" ).register();
		new IncColorSeed( "increase color seed", "C" ).register();
		new DecColorSeed( "decrease color seed", "shift C" ).register();

		inputActionBindings.addActionMap( "select", ksActionMap );
		inputActionBindings.addInputMap( "select", ksInputMap );
	}

	public long getActiveFragmentId()
	{
		return activeFragmentId;
	}

	public long getHoverFragmentId()
	{
		return hoverFragmentId;
	}

	public void setActiveFragmentId( final long id )
	{
		activeFragmentId = id;
		colorStream.setActive( id );
		System.out.println( "activeID = " + activeFragmentId );
	}

	public void setHoverFragmentId( final long id )
	{
		hoverFragmentId = id;
	}

	public SelectionOverlay getSelectionOverlay()
	{
		return selectionOverlay;
	}

	////////////////
	// behavioUrs //
	////////////////

	public BehaviourMap getBehaviourMap()
	{
		return behaviourMap;
	}

	public InputTriggerMap getInputTriggerMap()
	{
		return inputTriggerMap;
	}

	private abstract class SelfRegisteringBehaviour implements Behaviour
	{
		private final String name;

		private final String[] defaultTriggers;

		public SelfRegisteringBehaviour( final String name, final String... defaultTriggers )
		{
			this.name = name;
			this.defaultTriggers = defaultTriggers;
		}

		public void register()
		{
			behaviourMap.put( name, this );
			inputAdder.put( name, defaultTriggers );
		}
	}

	private abstract class SelfRegisteringAction extends AbstractNamedAction
	{
		private final String[] defaultTriggers;

		public SelfRegisteringAction( final String name, final String ... defaultTriggers )
		{
			super( name );
			this.defaultTriggers = defaultTriggers;
		}

		public void register()
		{
			put( ksActionMap );
			ksKeyStrokeAdder.put( name(), defaultTriggers );
		}
	}

	private class SelectFragment extends SelfRegisteringBehaviour implements ClickBehaviour
	{
		public SelectFragment( final String name, final String ... defaultTriggers )
		{
			super( name, defaultTriggers );
		}

		@Override
		public void click( final int x, final int y )
		{
			final long id = idPicker.getIdAtDisplayCoordinate( x, y );
			viewer.displayToGlobalCoordinates(x, y, lastClick);
			setActiveFragmentId( id );
			viewer.requestRepaint();
		}
	}

	private class HoverFragment implements MouseMotionListener
	{
		@Override
		public void mouseDragged( final MouseEvent e ) {}

		@Override
		public void mouseMoved( final MouseEvent e )
		{
			final int x = e.getX();
			final int y = e.getY();

			final long id = idPicker.getIdAtDisplayCoordinate( x, y );
			setHoverFragmentId( id );
			viewer.getDisplay().repaint();
		}
	}

	private class NewActiveFragmentId extends SelfRegisteringAction
	{
		public NewActiveFragmentId( final String name, final String ... defaultTriggers )
		{
			super( name, defaultTriggers );
		}

		@Override
		public void actionPerformed( final ActionEvent e )
		{
			synchronized ( viewer )
			{
				setActiveFragmentId( idService.next() );
			}
			viewer.requestRepaint();
		}
	}

	private class IncColorSeed extends SelfRegisteringAction
	{
		public IncColorSeed( final String name, final String ... defaultTriggers )
		{
			super( name, defaultTriggers );
		}

		@Override
		public void actionPerformed( final ActionEvent e )
		{
			colorStream.incSeed();
			viewer.requestRepaint();
		}
	}

	private class DecColorSeed extends SelfRegisteringAction
	{
		public DecColorSeed( final String name, final String ... defaultTriggers )
		{
			super( name, defaultTriggers );
		}

		@Override
		public void actionPerformed( final ActionEvent e )
		{
			colorStream.decSeed();
			viewer.requestRepaint();
		}
	}
} public class TranslateZController
{
	final protected ViewerPanel viewer;

	// for behavioUrs
	private final BehaviourMap behaviourMap = new BehaviourMap();
	private final InputTriggerMap inputTriggerMap = new InputTriggerMap();
	private final InputTriggerAdder inputAdder;

	public TranslateZController(
			final ViewerPanel viewer,
			final double[] resolution,
			final InputTriggerConfig config)
	{
		this.viewer = viewer;
		inputAdder = config.inputTriggerAdder( inputTriggerMap, "translate_z" );

		final double min = Math.min(Math.min(resolution[0], resolution[1]), resolution[2]);
		final double max = Math.max(Math.max(resolution[0], resolution[1]), resolution[2]);
		final double f = max/min;

//		// fast scrolling is the default, normal and slow with modifiers
//		new FixDistanceTranslateZ( max, "scroll browse z fast", "scroll" ).register();
//		new FixDistanceTranslateZ( min, "scroll browse z", "shift scroll" ).register();
//		new FixDistanceTranslateZ( min/f, "scroll browse z slow", "ctrl scroll" ).register();

		/*
		 * The above doesn't work, "scroll" still triggers "scroll browse z",
		 * might be related to bdv mapping with same name...
		 *
		 * So we have to do things with the wrong names...
		 */
		new FixDistanceTranslateZ( 10 * min, "scroll browse z fast", "shift scroll" ).register();
		new FixDistanceTranslateZ( max, "scroll browse z", "scroll" ).register();
		new FixDistanceTranslateZ( min / 10, "scroll browse z slow", "ctrl scroll" ).register();
	}



	public BehaviourMap getBehaviourMap()
	{
		return behaviourMap;
	}

	////////////////
	// behavioUrs //
	////////////////

	private abstract class SelfRegisteringBehaviour implements Behaviour
	{
		private final String name;

		private final String[] defaultTriggers;

		public SelfRegisteringBehaviour( final String name, final String ... defaultTriggers )
		{
			this.name = name;
			this.defaultTriggers = defaultTriggers;
		}

		public void register()
		{
			behaviourMap.put( name, this );
			inputAdder.put( name, defaultTriggers );
		}
	}

	private class FixDistanceTranslateZ extends SelfRegisteringBehaviour implements ScrollBehaviour
	{
		final private double speed;

		final private AffineTransform3D affine = new AffineTransform3D();

		public FixDistanceTranslateZ( final double speed, final String name, final String ... defaultTriggers )
		{
			super( name, defaultTriggers );
			this.speed = speed;
		}

		@Override
		public void scroll( final double wheelRotation, final boolean isHorizontal, final int x, final int y )
		{
			final double direction = (wheelRotation < 0 ? 1 : -1);

			synchronized ( viewer )
			{
				viewer.getState().getViewerTransform( affine );
				final double dZ = speed * direction * Affine3DHelpers.extractScale( affine, 0 );
				affine.set( affine.get( 2, 3 ) - dZ, 2, 3 );
				viewer.setCurrentViewerTransform( affine );
			}
		}
	}
} public class FragmentSegmentAssignment
{
	/**
	 * Serializes {@link FragmentSegmentAssignment} into JSON of the form
	 * <pre>
	 * {
	 *   "lut" : {
	 *     "<fragment_id1>" : <segment_id1>,
	 *     "<fragment_id2>" : <segment_id2>,
	 *     ...
	 *   }
	 * }
	 * </pre>
	 */
	static public class FragmentSegmentSerializer implements JsonSerializer< FragmentSegmentAssignment >
	{
		@Override
		public JsonElement serialize( final FragmentSegmentAssignment src, final Type typeOfSrc, final JsonSerializationContext context )
		{
			final JsonObject jsonLut = new JsonObject();
			final TLongLongIterator lutIterator = src.lut.iterator();
			while ( lutIterator.hasNext() )
			{
				lutIterator.advance();
				jsonLut.addProperty( Long.toString( lutIterator.key() ), lutIterator.value() );
			}

			final JsonObject jsonObject = new JsonObject();
			jsonObject.add( "lut", jsonLut );

			return jsonObject;
		}
	}

	/**
	 * Serializes {@link FragmentSegmentAssignment} into JSON of the form
	 * <pre>
	 * {
	 *   "fragments" : [<segment_id1>, <segment_id2>, ...],
	 *   "segments" : [<object_id1>, <object_id2>, ...]
	 *   }
	 * }
	 * </pre>
	 *
	 * TODO number of elements and bodies is limited by implementation to
	 * 2<sup>31</sup> and most likely significantly less due to memory
	 * consumption on {@link JsonPrimitive} creation per each number.
	 */
	static public class SegmentBodyListSerializer implements JsonSerializer< FragmentSegmentAssignment >
	{
		@Override
		public JsonElement serialize( final FragmentSegmentAssignment src, final Type typeOfSrc, final JsonSerializationContext context )
		{
			final JsonArray fragments = new JsonArray();
			final JsonArray segments = new JsonArray();
			final TLongLongIterator lutIterator = src.lut.iterator();
			while ( lutIterator.hasNext() )
			{
				lutIterator.advance();
				fragments.add( new JsonPrimitive( lutIterator.key() ) );
				segments.add( new JsonPrimitive( lutIterator.value() ) );
			}

			final JsonObject jsonObject = new JsonObject();
			jsonObject.add( "fragments", fragments );
			jsonObject.add( "segments", segments );

			return jsonObject;
		}
	}

	/**
	 * Serializes {@link FragmentSegmentAssignment} into JSON of the form
	 * <pre>
	 * {
	 *   "ilut" : {
	 *     "<segment_id1>" : [<fragment_id1>, <fragment_id2>, ...],
	 *     "<segment_id2>" : [<fragment_id3>, <fragment_id4>, ...],
	 *     ...
	 *   }
	 * }
	 * </pre>
	 */
	static public class BodySegmentsSerializer implements JsonSerializer< FragmentSegmentAssignment >
	{
		@Override
		public JsonElement serialize( final FragmentSegmentAssignment src, final Type typeOfSrc, final JsonSerializationContext context )
		{
			final Gson gson = new Gson();

			final JsonObject jsonILut = new JsonObject();
			final TLongObjectIterator< long[] > ilutIterator = src.ilut.iterator();
			while ( ilutIterator.hasNext() )
			{
				ilutIterator.advance();
				jsonILut.add(
						Long.toString( ilutIterator.key() ),
						gson.toJsonTree( ilutIterator.value() ) );
			}

			final JsonObject jsonObject = new JsonObject();
			jsonObject.add( "ilut", jsonILut );

			return jsonObject;
		}
	}

	/**
	 * Deserializes the various serializations of FragmentSegmentAssignment.
	 *
	 * TODO number of fragments and segmentsis limited by implementation to
	 * 2<sup>31</sup> due to collecting them in a long[].
	 *
	 */
	static public class GSONDeserializer implements JsonDeserializer< FragmentSegmentAssignment >
	{
		@Override
		public FragmentSegmentAssignment deserialize( final JsonElement json, final Type typeOfT, final JsonDeserializationContext context ) throws JsonParseException
		{
			final JsonObject jsonObject = json.getAsJsonObject();

			boolean notYetDone = true;
			long[] fragments = null;
			long[] segments = null;
			String message = "Could not find either lut, ilut, or segments + bodies properties.";
			if ( jsonObject.has( "lut" ) )
			{
				final TLongArrayList fragmentsList = new TLongArrayList();
				final TLongArrayList segmentsList = new TLongArrayList();

				final JsonElement lutJsonElement = jsonObject.get( "lut" );
				if ( lutJsonElement.isJsonObject() )
				{
					final Set< Entry< String, JsonElement > > lutJsonEntrySet = lutJsonElement.getAsJsonObject().entrySet();
					notYetDone = false;
					for ( final Entry< String, JsonElement > entry : lutJsonEntrySet )
					{
						try
						{
							final long fragmentId = Long.parseLong( entry.getKey() );
							final JsonElement value = entry.getValue();
							if ( value.isJsonPrimitive() )
							{
								final long segmentId = value.getAsLong();
								fragmentsList.add( fragmentId );
								segmentsList.add( segmentId );
							}
							else
							{
								notYetDone = true;
								break;
							}
						}
						catch ( final Exception e )
						{
							notYetDone = true;
							message = e.getMessage();
						}
					}
					if ( !notYetDone )
					{
						fragments = fragmentsList.toArray();
						segments = segmentsList.toArray();
					}
				}
			}
			if ( notYetDone && jsonObject.has( "ilut" ) )
			{
				final TLongArrayList fragmentsList = new TLongArrayList();
				final TLongArrayList segmentsList = new TLongArrayList();

				final JsonElement ilutJsonElement = jsonObject.get( "lut" );
				if ( ilutJsonElement.isJsonObject() )
				{
					final Set< Entry< String, JsonElement > > ilutJsonEntrySet = ilutJsonElement.getAsJsonObject().entrySet();
					notYetDone = false;
A:					for ( final Entry< String, JsonElement > entry : ilutJsonEntrySet )
					{
						try
						{
							final long segmentId = Long.parseLong( entry.getKey() );
							final JsonElement value = entry.getValue();
							if ( value.isJsonArray() )
							{
								for ( final JsonElement fragment : value.getAsJsonArray() )
								{
									if ( fragment.isJsonPrimitive() )
									{
										final long fragmentId = fragment.getAsLong();
										fragmentsList.add( fragmentId );
										segmentsList.add( segmentId );
									}
									else
									{
										notYetDone = true;
										break A;
									}
								}
							}
							else
							{
								notYetDone = true;
								break;
							}
						}
						catch ( final Exception e )
						{
							notYetDone = true;
							message = e.getMessage();
						}
					}
				}
			}
			if ( notYetDone && jsonObject.has( "fragments" ) && jsonObject.has( "segments" ) )
			{
				final Gson gson = new Gson();
				notYetDone = false;
				try
				{
					fragments = gson.fromJson( jsonObject.get( "fragments" ), long[].class );
					segments = gson.fromJson( jsonObject.get( "segments" ), long[].class );
				}
				catch ( final Exception e )
				{
					notYetDone = true;
					message = e.getMessage();
				}
			}
			if ( notYetDone )
				throw new JsonParseException( message );
			else
			{
				final LocalIdService idService = new LocalIdService();
				final long maxId = IdService.max( IdService.max( fragments ), IdService.max( segments ) );
				idService.setNext( maxId + 1 );
				return new FragmentSegmentAssignment( fragments, segments, idService );
			}
		}
	}

	final protected TLongLongHashMap lut = new TLongLongHashMap(Constants.DEFAULT_CAPACITY, Constants.DEFAULT_LOAD_FACTOR, Label.TRANSPARENT, Label.TRANSPARENT);
	final protected TLongObjectHashMap< long[] > ilut = new TLongObjectHashMap< long[] >(Constants.DEFAULT_CAPACITY, Constants.DEFAULT_LOAD_FACTOR, Label.TRANSPARENT);

	protected IdService idService;

	public FragmentSegmentAssignment( final IdService idService )
	{
		this.idService = idService;
	}

	public FragmentSegmentAssignment( final long[] fragments, final long[] segments, final IdService idService )
	{
		assert fragments.length == segments.length : "segments and bodies must be of same length";

		for ( int i = 0; i < fragments.length; ++i )
			lut.put( fragments[ i ], segments[ i ] );

		this.idService = idService;

		syncILut();
	}

	public TLongLongHashMap getLut()
	{
		return lut;
	}

	public void setIdService( final IdService idService )
	{
		this.idService = idService;
	}

	public void initLut( final TLongLongHashMap lut )
	{
		this.lut.clear();
		this.ilut.clear();
		this.lut.putAll( lut );
		syncILut();

		System.out.println( "Done" );
	}

	/**
	 * Synchronize the inverse Lookup (segment > [fragments]) with the current
	 * forward lookup (fragment > segment)).  The current state of the inverse
	 * lookup will be cleared.
	 */
	protected void syncILut()
	{
		ilut.clear();
		final TLongLongIterator lutIterator =  lut.iterator();
		while ( lutIterator.hasNext() )
		{
			lutIterator.advance();
			final long fragmentId = lutIterator.key();
			final long segmentId = lutIterator.value();
			long[] fragments = ilut.get( segmentId );
			if ( fragments == null )
				fragments = new long[]{ fragmentId };
			else
				fragments = ArrayUtils.add( fragments, fragmentId );
			ilut.put( segmentId, fragments);
		}
	}

	/**
	 * Get the body that is assigned to a fragment id.
	 *
	 * @param id
	 */
	public long getSegment( final long fragmentId )
	{
		final long id;
		synchronized ( this )
		{
			final long segmentId = lut.get( fragmentId );
			if ( segmentId == lut.getNoEntryValue() ) {
				id = fragmentId;
				lut.put( fragmentId, id );
				ilut.put( id, new long[]{ fragmentId } );
			}
			else
				id = segmentId;
		}
		return id;
	}

	/**
	 * Get the segments assigned to a body.
	 *
	 * @param id
	 */
	public long[] getFragments( final long segmentId )
	{
		final long[] fragments;
		synchronized ( this )
		{
			fragments = ilut.get( segmentId );
		}
		return fragments;
	}

	/**
	 * Assign all fragments of segmentId1 to segmentId2.
	 *
	 * @param segmentId1
	 * @param segmentId2
	 */
	public void assignFragments( final long segmentId1, final long segmentId2 )
	{
		if ( segmentId1 == segmentId2 )
			return;

		synchronized ( this )
		{
			final long[] fragments1 = ilut.get( segmentId1 );
			final long[] fragments2 = ilut.get( segmentId2 );
			for ( final long fragmentId : fragments1 )
				lut.put( fragmentId, segmentId2 );
			ilut.put( segmentId2, ArrayUtils.addAll( fragments1, fragments2 ) );
			ilut.remove( segmentId1 );
		}
	}

	/**
	 * Merge two segments.
	 *
	 * @param segmentId1
	 * @param segmentId2
	 */
	public void mergeSegments( final long segmentId1, final long segmentId2 )
	{
		if ( segmentId1 == segmentId2 )
			return;

		final long mergedSegmentId = idService.next();
		synchronized ( this )
		{
			final long[] fragments1 = ilut.get( segmentId1 );
			final long[] fragments2 = ilut.get( segmentId2 );
			final long[] fragments = ArrayUtils.addAll( fragments1, fragments2 );
			for ( final long fragmentId : fragments )
				lut.put( fragmentId, mergedSegmentId );
			ilut.put( mergedSegmentId, fragments );
			ilut.remove( segmentId1 );
			ilut.remove( segmentId2 );
		}
	}

	/**
	 * Merge two segments assigned to two fragment ids.
	 *
	 * @param fragmentId1
	 * @param fragmentId2
	 */
	public void mergeFragmentSegments( final long fragmentId1, final long fragmentId2 )
	{
		final long segmentId1, segmentId2;
		segmentId1 = getSegment( fragmentId1 );
		segmentId2 = getSegment( fragmentId2 );
		mergeSegments( segmentId1, segmentId2 );
	}

	/**
	 * Detach a segment from the body that it has been associated with
	 *
	 * @param fragmentId
	 */
	public void detachFragment( final long fragmentId )
	{
		synchronized ( this )
		{
			final long segmentId = lut.get( fragmentId );
			final long[] fragments = ilut.get( segmentId );
			if ( fragments != null && fragments.length > 1 )
			{
				final long[] newFragments = ArrayUtils.removeElement( fragments, fragmentId );
				ilut.put( segmentId, newFragments );

				final long newSegmentId = fragmentId;
				lut.put( fragmentId, newSegmentId );
				ilut.put( newSegmentId, new long[]{ fragmentId } );
			}
		}
	}
} public class SegmentAssignment
{
	final private TLongHashSet segments = new TLongHashSet();

	public TLongHashSet getAssignedSegments()
	{
		return segments;
	}

	public void add( final long segmentId )
	{
		segments.add( segmentId );
	}

	public boolean remove( final long segmentId )
	{
		return segments.remove( segmentId );
	}

	public boolean contains( final long segmentId )
	{
		return segments.contains( segmentId );
	}
} public class ARGBConvertedLabelsSource extends AbstractARGBConvertedLabelsSource
{
	final private AbstractViewerSetupImgLoader< LabelMultisetType, VolatileLabelMultisetType > multisetImageLoader;

	public ARGBConvertedLabelsSource(
			final int setupId,
			final AbstractViewerSetupImgLoader< LabelMultisetType, VolatileLabelMultisetType > multisetImageLoader,
			final ARGBStream argStream )
	{
		super( setupId, argStream );
		this.multisetImageLoader = multisetImageLoader;
	}

	final public AbstractViewerSetupImgLoader< LabelMultisetType, VolatileLabelMultisetType > getLoader()
	{
		return multisetImageLoader;
	}

	@Override
	public RandomAccessibleInterval< VolatileARGBType > getSource( final int t, final int level )
	{
		return Converters.convert(
				multisetImageLoader.getVolatileImage( t, level ),
				new VolatileLabelMultisetARGBConverter( argbStream ),
				new VolatileARGBType() );
	}

	@Override
	public void getSourceTransform( final int t, final int level, final AffineTransform3D transform )
	{
		transform.set( multisetImageLoader.getMipmapTransforms()[ level ] );
	}

	/**
	 * TODO Store this in a field
	 */
	@Override
	public int getNumMipmapLevels()
	{
		return multisetImageLoader.getMipmapResolutions().length;
	}
} public class ModalGoldenAngleSaturatedARGBStream extends GoldenAngleSaturatedARGBStream implements Wheel
{
	final static public int NORMAL = 0;
	final static public int HIDE_COMPLETE = 1;
	final static public int SELECTED_ONLY = 2;

	protected int mode = 0;

	final protected SegmentAssignment completeSegments;

	public ModalGoldenAngleSaturatedARGBStream(
			final FragmentSegmentAssignment assignment,
			final SegmentAssignment completeSegments )
	{
		super( assignment );
		this.completeSegments = completeSegments;
		seed = 1;
	}

	@Override
	public synchronized void advance()
	{
		++mode;
		if ( mode == 3 )
			mode = 0;
		clearCache();
	}

	@Override
	public synchronized void regress()
	{
		--mode;
		if ( mode == -1 )
			mode = 2;
		clearCache();
	}

	@Override
	public void setMode( final int value )
	{
		mode = value % 3;
		clearCache();
	}

	@Override
	public int getMode()
	{
		return mode;
	}

	@Override
	public int argb( final long fragmentId )
	{
		int argb = fragmentARGBCache.get( fragmentId );

		if ( argb == 0x00000000 )
		{
			final long segmentId = assignment.getSegment( fragmentId );

			argb = segmentARGBCache.get( segmentId );

			if ( argb == 0x00000000 )
			{
				double x = getDouble( seed + segmentId );
				x *= 6.0;
				final int k = ( int )x;
				final int l = k + 1;
				final double u = x - k;
				final double v = 1.0 - u;

				final int r = interpolate( rs, k, l, u, v );
				final int g = interpolate( gs, k, l, u, v );
				final int b = interpolate( bs, k, l, u, v );

				argb = argb( r, g, b, alpha );

				if ( ( mode == SELECTED_ONLY && segmentId != activeSegment ) || ( mode == HIDE_COMPLETE && completeSegments.contains( segmentId ) ) )
					argb = argb & 0x00ffffff;
				else if ( activeSegment == segmentId )
					argb = argb & 0x00ffffff | activeSegmentAlpha;

				synchronized ( segmentARGBCache )
				{
					segmentARGBCache.put( segmentId, argb );
				}
			}
			if ( activeFragment == fragmentId && ( argb & 0xff000000 ) != 0 )
				argb = argb & 0x00ffffff | activeFragmentAlpha;

			synchronized ( fragmentARGBCache )
			{
				fragmentARGBCache.put( fragmentId, argb );
			}
		}

		return argb;
	}
} public class H5LabelMultisetSetupImageLoader
		extends AbstractCachedViewerSetupImgLoader< LabelMultisetType, VolatileLabelMultisetType, VolatileLabelMultisetArray >
		implements ViewerImgLoader, SetCache
{
	static private CacheArrayLoader< VolatileLabelMultisetArray > typedLoader(
			final IHDF5Reader reader,
			final IHDF5Reader scaleReader,
			final String dataset )
	{
		final HDF5DataSetInformation typeInfo = reader.object().getDataSetInformation( dataset );
		final Class< ? > cls = typeInfo.getTypeInformation().tryGetJavaType();
//		System.out.println( typeInfo.getTypeInformation().tryGetJavaType().toString() );
		if ( float.class == cls )
			return new H5FloatLabelMultisetArrayLoader( reader, scaleReader, dataset );
		else if ( short.class == cls )
			return new H5ShortLabelMultisetArrayLoader( reader, scaleReader, dataset );
		else if ( int.class == cls )
			return new H5IntLabelMultisetArrayLoader( reader, scaleReader, dataset );
		else if ( long.class == cls )
			return new H5LongLabelMultisetArrayLoader( reader, scaleReader, dataset );
		else
			return null;
	}

	static private long[][] readDimensions(
			final IHDF5Reader reader,
			final IHDF5Reader scaleReader,
			final String dataset )
	{
		final long[] h5dim = reader.object().getDimensions( dataset );
		if ( scaleReader == null )
			return new long[][] { { h5dim[ 2 ], h5dim[ 1 ], h5dim[ 0 ] } };
		else
		{
			final int numMipmapLevels = scaleReader.uint32().read( "levels" );
			final long[][] dimensions = new long[ numMipmapLevels ][];

			dimensions[ 0 ] = new long[] { h5dim[ 2 ], h5dim[ 1 ], h5dim[ 0 ] };

			for ( int level = 1; level < numMipmapLevels; ++level )
			{
				final String dimensionsPath = String.format( "l%02d/dimensions", level );
				dimensions[ level ] = scaleReader.uint64().readArray( dimensionsPath );
			}
			return dimensions;
		}
	}

	static private int[][] readCellDimensions(
			final IHDF5Reader reader,
			final IHDF5Reader scaleReader,
			final String dataset,
			final int[] cellDimension )
	{
		if ( scaleReader == null )
			return new int[][] { cellDimension };
		else
		{
			final int numMipmapLevels = scaleReader.uint32().read( "levels" );
			final int[][] cellDimensions = new int[ numMipmapLevels ][];

			cellDimensions[ 0 ] = cellDimension;

			for ( int level = 1; level < numMipmapLevels; ++level )
			{
				final String blocksizePath = String.format( "l%02d/blocksize", level );
				cellDimensions[ level ] = Util.long2int( scaleReader.uint64().readArray( blocksizePath ) );
			}
			return cellDimensions;
		}
	}

	static private double[][] readResolutions(
			final IHDF5Reader reader,
			final IHDF5Reader scaleReader,
			final String dataset,
			final double[] resolution )
	{
		if ( scaleReader == null )
		{
			if ( reader.object().hasAttribute( dataset, "resolution" ) )
			{
				final double[] h5res = reader.float64().getArrayAttr( dataset, "resolution" );
				return new double[][] { { h5res[ 2 ], h5res[ 1 ], h5res[ 0 ] } };
			}
			else
				return new double[][] { { 1, 1, 1 } };
		}
		else
		{
			final int numMipmapLevels = scaleReader.uint32().read( "levels" );
			final double[][] resolutions = new double[ numMipmapLevels ][];

			resolutions[ 0 ] = resolution;

			for ( int level = 1; level < numMipmapLevels; ++level )
			{
				final String factorsPath = String.format( "l%02d/factors", level );

				final long[] factors = scaleReader.uint64().readArray( factorsPath );
				resolutions[ level ] = new double[ 3 ];
				for ( int d = 0; d < 3; ++d )
					resolutions[ level ][ d ] = resolution[ d ] * factors[ d ];
			}
			return resolutions;
		}
	}

	final static protected double[] readResolution( final IHDF5Reader reader, final String dataset )
	{
		final double[] h5res = reader.float64().getArrayAttr( dataset, "resolution" );
		return new double[] { h5res[ 2 ], h5res[ 1 ], h5res[ 0 ] };
	}

	final static protected double[] readOffset( final IHDF5Reader reader, final String dataset )
	{
		final double[] h5res = reader.float64().getArrayAttr( dataset, "offset" );
		return new double[] { h5res[ 2 ], h5res[ 1 ], h5res[ 0 ] };
	}

	private final double[] offset;

	public H5LabelMultisetSetupImageLoader(
			final IHDF5Reader reader,
			final IHDF5Reader scaleReader,
			final String dataset,
			final int setupId,
			final int[] cellDimension,
			final double[] resolution,
			final double[] offset,
			final VolatileGlobalCellCache cache ) throws IOException
	{

		super( setupId,
				readDimensions( reader, scaleReader, dataset ),
				readCellDimensions( reader, scaleReader, dataset, cellDimension ),
				readResolutions( reader, scaleReader, dataset, resolution ),
				new LabelMultisetType(),
				new VolatileLabelMultisetType(),
				typedLoader( reader, scaleReader, dataset ),
				cache );
		this.offset = offset;
		/* offset mipmap transforms */
		for ( int i = 0; i < resolutions.length; ++i )
		{
			final AffineTransform3D mipmapTransform = mipmapTransforms[ i ];
			final double[] scaledResolution = resolutions[ i ];
			mipmapTransform.set(offset[0] / scaledResolution[0] * resolution[0], 0, 3);
			mipmapTransform.set(offset[1] / scaledResolution[1] * resolution[1], 1, 3);
			mipmapTransform.set(offset[2] / scaledResolution[2] * resolution[2], 2, 3);
		}
	}

	public H5LabelMultisetSetupImageLoader(
			final IHDF5Reader reader,
			final IHDF5Reader scaleReader,
			final String dataset,
			final int setupId,
			final int[] cellDimension,
			final VolatileGlobalCellCache cache ) throws IOException
	{
		this( reader, scaleReader, dataset, setupId, cellDimension, readResolution( reader, dataset ), readOffset( reader, dataset ), cache );
	}

	@Override
	public void setCache( final VolatileGlobalCellCache cache )
	{
		this.cache = cache;
	}

	@Override
	public ViewerSetupImgLoader< ?, ? > getSetupImgLoader( final int setupId )
	{
		return this;
	}

	@Override
	public CacheControl getCacheControl()
	{
		return cache;
	}

	public double[] getOffset()
	{
		return this.offset;
	}
} public class H5Utils
{
	static public void cropCellDimensions(
			final Dimensions sourceDimensions,
			final long[] offset,
			final int[] cellDimensions,
			final long[] croppedCellDimensions )
	{
		final int n = sourceDimensions.numDimensions();
		for ( int d = 0; d < n; ++d )
			croppedCellDimensions[ d ] = Math.min( cellDimensions[ d ], sourceDimensions.dimension( d ) - offset[ d ] );
	}

	static public void cropCellDimensions(
			final long[] max,
			final long[] offset,
			final int[] cellDimensions,
			final long[] croppedCellDimensions )
	{
		for ( int d = 0; d < max.length; ++d )
			croppedCellDimensions[ d ] = Math.min( cellDimensions[ d ], max[ d ] - offset[ d ] + 1 );
	}

	/**
	 * Load an HDF5 float32 dataset into a {@link CellImg} of {@link FloatType}.
	 *
	 * @param reader
	 * @param dataset
	 * @param cellDimensions
	 */
	static public CellImg< FloatType, ? > loadFloat(
			final IHDF5Reader reader,
			final String dataset,
			final int[] cellDimensions )
	{
		final IHDF5FloatReader float32Reader = reader.float32();

		final long[] dimensions = reorder( reader.object().getDimensions( dataset ) );
		final int n = dimensions.length;

		final CellImg< FloatType, ? > target = new CellImgFactory< FloatType >( cellDimensions ).create( dimensions, new FloatType() );

		final long[] offset = new long[ n ];
		final long[] targetCellDimensions = new long[ n ];
		for ( int d = 0; d < n; )
		{
			cropCellDimensions( target, offset, cellDimensions, targetCellDimensions );
			final RandomAccessibleInterval< FloatType > targetBlock = Views.offsetInterval( target, offset, targetCellDimensions );
			final MDFloatArray targetCell = float32Reader.readMDArrayBlockWithOffset(
					dataset,
					Util.long2int( reorder( targetCellDimensions ) ),
					reorder( offset ) );

			int i = 0;
			for ( final FloatType t : Views.flatIterable( targetBlock ) )
				t.set( targetCell.get( i++ ) );

			for ( d = 0; d < n; ++d )
			{
				offset[ d ] += cellDimensions[ d ];
				if ( offset[ d ] < dimensions[ d ] )
					break;
				else
					offset[ d ] = 0;
			}
		}

		return target;
	}

	/**
	 * Load an HDF5 float32 dataset into a {@link CellImg} of {@link FloatType}.
	 *
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public CellImg< FloatType, ? > loadFloat(
			final File file,
			final String dataset,
			final int[] cellDimensions )
	{
		final IHDF5Reader reader = HDF5Factory.openForReading( file );
		final CellImg< FloatType, ? > target = loadFloat( reader, dataset, cellDimensions );
		reader.close();
		return target;
	}

	/**
	 * Load an HDF5 float32 dataset into a {@link CellImg} of {@link FloatType}.
	 *
	 * @param filePath
	 * @param dataset
	 * @param cellDimensions
	 */
	static public CellImg< FloatType, ? > loadFloat(
			final String filePath,
			final String dataset,
			final int[] cellDimensions )
	{
		return loadFloat( new File( filePath ), dataset, cellDimensions );
	}

	/**
	 * Load an HDF5 float64 dataset into a {@link CellImg} of
	 * {@link DoubleType}.
	 *
	 * @param reader
	 * @param dataset
	 * @param cellDimensions
	 */
	static public CellImg< DoubleType, ? > loadDouble(
			final IHDF5Reader reader,
			final String dataset,
			final int[] cellDimensions )
	{
		final IHDF5DoubleReader float64Reader = reader.float64();

		final long[] dimensions = reorder( reader.object().getDimensions( dataset ) );
		final int n = dimensions.length;

		final CellImg< DoubleType, ? > target = new CellImgFactory< DoubleType >( cellDimensions ).create( dimensions, new DoubleType() );

		final long[] offset = new long[ n ];
		final long[] targetCellDimensions = new long[ n ];
		for ( int d = 0; d < n; )
		{
			cropCellDimensions( target, offset, cellDimensions, targetCellDimensions );
			final RandomAccessibleInterval< DoubleType > targetBlock = Views.offsetInterval( target, offset, targetCellDimensions );
			final MDDoubleArray targetCell = float64Reader.readMDArrayBlockWithOffset(
					dataset,
					Util.long2int( reorder( targetCellDimensions ) ),
					reorder( offset ) );

			int i = 0;
			for ( final DoubleType t : Views.flatIterable( targetBlock ) )
				t.set( targetCell.get( i++ ) );

			for ( d = 0; d < n; ++d )
			{
				offset[ d ] += cellDimensions[ d ];
				if ( offset[ d ] < dimensions[ d ] )
					break;
				else
					offset[ d ] = 0;
			}
		}

		return target;
	}

	/**
	 * Load an HDF5 float64 dataset into a {@link CellImg} of
	 * {@link DoubleType}.
	 *
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public CellImg< DoubleType, ? > loadDouble(
			final File file,
			final String dataset,
			final int[] cellDimensions )
	{
		final IHDF5Reader reader = HDF5Factory.openForReading( file );
		final CellImg< DoubleType, ? > target = loadDouble( reader, dataset, cellDimensions );
		reader.close();
		return target;
	}

	/**
	 * Load an HDF5 float64 dataset into a {@link CellImg} of
	 * {@link DoubleType}.
	 *
	 * @param filePath
	 * @param dataset
	 * @param cellDimensions
	 */
	static public CellImg< DoubleType, ? > loadDouble(
			final String filePath,
			final String dataset,
			final int[] cellDimensions )
	{
		return loadDouble( new File( filePath ), dataset, cellDimensions );
	}

	/**
	 * Load an HDF5 uint64 dataset into a {@link CellImg} of {@link LongType}.
	 *
	 * @param reader
	 * @param dataset
	 * @param cellDimensions
	 */
	static public CellImg< LongType, ? > loadUnsignedLong(
			final IHDF5Reader reader,
			final String dataset,
			final int[] cellDimensions )
	{
		final IHDF5LongReader uint64Reader = reader.uint64();

		final long[] dimensions = reorder( reader.object().getDimensions( dataset ) );
		final int n = dimensions.length;

		final CellImg< LongType, ? > target = new CellImgFactory< LongType >( cellDimensions ).create( dimensions, new LongType() );

		final long[] offset = new long[ n ];
		final long[] targetCellDimensions = new long[ n ];
		for ( int d = 0; d < n; )
		{
			cropCellDimensions( target, offset, cellDimensions, targetCellDimensions );
			final RandomAccessibleInterval< LongType > targetBlock = Views.offsetInterval( target, offset, targetCellDimensions );
			final MDLongArray targetCell = uint64Reader.readMDArrayBlockWithOffset(
					dataset,
					Util.long2int( reorder( targetCellDimensions ) ),
					reorder( offset ) );

			int i = 0;
			for ( final LongType t : Views.flatIterable( targetBlock ) )
				t.set( targetCell.get( i++ ) );

			for ( d = 0; d < n; ++d )
			{
				offset[ d ] += cellDimensions[ d ];
				if ( offset[ d ] < dimensions[ d ] )
					break;
				else
					offset[ d ] = 0;
			}
		}

		return target;
	}

	/**
	 * Load an HDF5 uint64 dataset into a {@link CellImg} of {@link LongType}.
	 *
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public CellImg< LongType, ? > loadUnsignedLong(
			final File file,
			final String dataset,
			final int[] cellDimensions )
	{
		final IHDF5Reader reader = HDF5Factory.openForReading( file );
		final CellImg< LongType, ? > target = loadUnsignedLong( reader, dataset, cellDimensions );
		reader.close();
		return target;
	}

	/**
	 * Load an HDF5 uint64 dataset into a {@link CellImg} of {@link LongType}.
	 *
	 * @param filePath
	 * @param dataset
	 * @param cellDimensions
	 */
	static public CellImg< LongType, ? > loadUnsignedLong(
			final String filePath,
			final String dataset,
			final int[] cellDimensions )
	{
		return loadUnsignedLong( new File( filePath ), dataset, cellDimensions );
	}

	/**
	 * Load an HDF5 uint16 dataset into a {@link CellImg} of
	 * {@link UnsignedShortType}.
	 *
	 * @param reader
	 * @param dataset
	 * @param cellDimensions
	 */
	static public CellImg< UnsignedShortType, ? > loadUnsignedShort(
			final IHDF5Reader reader,
			final String dataset,
			final int[] cellDimensions )
	{
		final IHDF5ShortReader uint16Reader = reader.uint16();

		final long[] dimensions = reorder( reader.object().getDimensions( dataset ) );
		final int n = dimensions.length;

		final CellImg< UnsignedShortType, ? > target = new CellImgFactory< UnsignedShortType >( cellDimensions ).create( dimensions, new UnsignedShortType() );

		final long[] offset = new long[ n ];
		final long[] targetCellDimensions = new long[ n ];
		for ( int d = 0; d < n; )
		{
			cropCellDimensions( target, offset, cellDimensions, targetCellDimensions );
			final RandomAccessibleInterval< UnsignedShortType > targetBlock = Views.offsetInterval( target, offset, targetCellDimensions );
			final MDShortArray targetCell = uint16Reader.readMDArrayBlockWithOffset(
					dataset,
					Util.long2int( reorder( targetCellDimensions ) ),
					reorder( offset ) );

			int i = 0;
			for ( final UnsignedShortType t : Views.flatIterable( targetBlock ) )
				t.set( targetCell.get( i++ ) );

			for ( d = 0; d < n; ++d )
			{
				offset[ d ] += cellDimensions[ d ];
				if ( offset[ d ] < dimensions[ d ] )
					break;
				else
					offset[ d ] = 0;
			}
		}

		return target;
	}

	/**
	 * Load an HDF5 uint16 dataset into a {@link CellImg} of
	 * {@link UnsignedShortType}.
	 *
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public CellImg< UnsignedShortType, ? > loadUnsignedShort(
			final File file,
			final String dataset,
			final int[] cellDimensions )
	{
		final IHDF5Reader reader = HDF5Factory.openForReading( file );
		final CellImg< UnsignedShortType, ? > target = loadUnsignedShort( reader, dataset, cellDimensions );
		reader.close();
		return target;
	}

	/**
	 * Load an HDF5 uint16 dataset into a {@link CellImg} of
	 * {@link UnsignedShortType}.
	 *
	 * @param filePath
	 * @param dataset
	 * @param cellDimensions
	 */
	static public CellImg< UnsignedShortType, ? > loadUnsignedShort(
			final String filePath,
			final String dataset,
			final int[] cellDimensions )
	{
		return loadUnsignedShort( new File( filePath ), dataset, cellDimensions );
	}

	/**
	 * Load an HDF5 uint8 dataset into a {@link CellImg} of
	 * {@link UnsignedByteType}.
	 *
	 * @param reader
	 * @param dataset
	 * @param cellDimensions
	 */
	static public CellImg< UnsignedByteType, ? > loadUnsignedByte(
			final IHDF5Reader reader,
			final String dataset,
			final int[] cellDimensions )
	{
		final IHDF5ByteReader uint8Reader = reader.uint8();

		final long[] dimensions = reorder( reader.object().getDimensions( dataset ) );
		final int n = dimensions.length;

		final CellImg< UnsignedByteType, ? > target = new CellImgFactory< UnsignedByteType >( cellDimensions ).create( dimensions, new UnsignedByteType() );

		final long[] offset = new long[ n ];
		final long[] targetCellDimensions = new long[ n ];
		for ( int d = 0; d < n; )
		{
			cropCellDimensions( target, offset, cellDimensions, targetCellDimensions );
			final RandomAccessibleInterval< UnsignedByteType > targetBlock = Views.offsetInterval( target, offset, targetCellDimensions );
			final MDByteArray targetCell = uint8Reader.readMDArrayBlockWithOffset(
					dataset,
					Util.long2int( reorder( targetCellDimensions ) ),
					reorder( offset ) );

			int i = 0;
			for ( final UnsignedByteType t : Views.flatIterable( targetBlock ) )
				t.set( targetCell.get( i++ ) );

			for ( d = 0; d < n; ++d )
			{
				offset[ d ] += cellDimensions[ d ];
				if ( offset[ d ] < dimensions[ d ] )
					break;
				else
					offset[ d ] = 0;
			}
		}

		return target;
	}

	/**
	 * Load an HDF5 uint8 dataset into a {@link CellImg} of
	 * {@link UnsignedByteType}.
	 *
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public CellImg< UnsignedByteType, ? > loadUnsignedByte(
			final File file,
			final String dataset,
			final int[] cellDimensions )
	{
		final IHDF5Reader reader = HDF5Factory.openForReading( file );
		final CellImg< UnsignedByteType, ? > target = loadUnsignedByte( reader, dataset, cellDimensions );
		reader.close();
		return target;
	}

	/**
	 * Load an HDF5 uint8 dataset into a {@link CellImg} of
	 * {@link UnsignedByteType}.
	 *
	 * @param filePath
	 * @param dataset
	 * @param cellDimensions
	 */
	static public CellImg< UnsignedByteType, ? > loadUnsignedByte(
			final String filePath,
			final String dataset,
			final int[] cellDimensions )
	{
		return loadUnsignedByte( new File( filePath ), dataset, cellDimensions );
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link LongType} into an HDF5
	 * uint8 dataset.
	 *
	 * @param source
	 *            source
	 * @param dimensions
	 *            dimensions of the dataset if created new
	 * @param writer
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void createUnsignedByte(
			final IHDF5Writer writer,
			final String dataset,
			final Dimensions datasetDimensions,
			final int[] cellDimensions )
	{
		final IHDF5ByteWriter uint8Writer = writer.uint8();

		if ( writer.exists( dataset ) )
			writer.delete( dataset );

		uint8Writer.createMDArray(
				dataset,
				reorder( Intervals.dimensionsAsLongArray( datasetDimensions ) ),
				reorder( cellDimensions ),
				HDF5IntStorageFeatures.INT_AUTO_SCALING_DEFLATE );

	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link UnsignedByteType} into
	 * an HDF5 uint8 dataset.
	 *
	 * @param source
	 * @param writer
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveUnsignedByte(
			final RandomAccessibleInterval< UnsignedByteType > source,
			final IHDF5Writer writer,
			final String dataset,
			final int[] cellDimensions )
	{
		if ( !writer.exists( dataset ) )
			createUnsignedByte( writer, dataset, source, cellDimensions );

		final long[] dimensions = reorder( writer.object().getDimensions( dataset ) );
		final int n = source.numDimensions();

		final IHDF5ByteWriter uint8Writer = writer.uint8();

		/* min is >= 0, max is < dimensions */
		final long[] min = Intervals.minAsLongArray( source );
		final long[] max = Intervals.maxAsLongArray( source );
		for ( int d = 0; d < min.length; ++d )
		{
			min[ d ] = Math.max( 0, min[ d ] );
			max[ d ] = Math.min( dimensions[ d ] - 1, max[ d ] );
		}

		final long[] offset = min.clone();
		final long[] sourceCellDimensions = new long[ n ];
		for ( int d = 0; d < n; )
		{
			cropCellDimensions( max, offset, cellDimensions, sourceCellDimensions );
			final RandomAccessibleInterval< UnsignedByteType > sourceBlock = Views.offsetInterval( source, offset, sourceCellDimensions );
			final MDByteArray targetCell = new MDByteArray( reorder( sourceCellDimensions ) );
			int i = 0;
			for ( final UnsignedByteType t : Views.flatIterable( sourceBlock ) )
				targetCell.set( UnsignedByteType.getCodedSignedByte( t.get() ), i++ );

			uint8Writer.writeMDArrayBlockWithOffset( dataset, targetCell, reorder( offset ) );

			for ( d = 0; d < n; ++d )
			{
				offset[ d ] += cellDimensions[ d ];
				if ( offset[ d ] <= max[ d ] )
					break;
				else
					offset[ d ] = min[ d ];
			}
		}
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link UnsignedByteType} into
	 * an HDF5 uint8 dataset.
	 *
	 * @param source
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveUnsignedByte(
			final RandomAccessibleInterval< UnsignedByteType > source,
			final File file,
			final String dataset,
			final int[] cellDimensions )
	{
		final IHDF5Writer writer = HDF5Factory.open( file );
		saveUnsignedByte( source, writer, dataset, cellDimensions );
		writer.close();
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link UnsignedByteType} into
	 * an HDF5 uint8 dataset.
	 *
	 * @param source
	 * @param filePAth
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveUnsignedByte(
			final RandomAccessibleInterval< UnsignedByteType > source,
			final String filePath,
			final String dataset,
			final int[] cellDimensions )
	{
		saveUnsignedByte( source, new File( filePath ), dataset, cellDimensions );
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link FloatType} into an HDF5
	 * float32 dataset.
	 *
	 * @param source
	 * @param writer
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveFloat(
			final RandomAccessibleInterval< FloatType > source,
			final IHDF5Writer writer,
			final String dataset,
			final int[] cellDimensions )
	{
		final int n = source.numDimensions();
		final long[] dimensions = Intervals.dimensionsAsLongArray( source );
		final IHDF5FloatWriter float32Writer = writer.float32();
		if ( !writer.exists( dataset ) )
			float32Writer.createMDArray(
					dataset,
					reorder( dimensions ),
					reorder( cellDimensions ) );

		final long[] offset = new long[ n ];
		final long[] sourceCellDimensions = new long[ n ];
		for ( int d = 0; d < n; )
		{
			cropCellDimensions( source, offset, cellDimensions, sourceCellDimensions );
			final RandomAccessibleInterval< FloatType > sourceBlock = Views.offsetInterval( source, offset, sourceCellDimensions );
			final MDFloatArray targetCell = new MDFloatArray( reorder( sourceCellDimensions ) );
			int i = 0;
			for ( final FloatType t : Views.flatIterable( sourceBlock ) )
				targetCell.set( t.get(), i++ );

			float32Writer.writeMDArrayBlockWithOffset( dataset, targetCell, reorder( offset ) );

			for ( d = 0; d < n; ++d )
			{
				offset[ d ] += cellDimensions[ d ];
				if ( offset[ d ] < source.dimension( d ) )
					break;
				else
					offset[ d ] = 0;
			}
		}
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link FloatType} into an HDF5
	 * float32 dataset.
	 *
	 * @param source
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveFloat(
			final RandomAccessibleInterval< FloatType > source,
			final File file,
			final String dataset,
			final int[] cellDimensions )
	{
		final IHDF5Writer writer = HDF5Factory.open( file );
		saveFloat( source, writer, dataset, cellDimensions );
		writer.close();
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link FloatType} into an HDF5
	 * float32 dataset.
	 *
	 * @param source
	 * @param filePAth
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveFloat(
			final RandomAccessibleInterval< FloatType > source,
			final String filePath,
			final String dataset,
			final int[] cellDimensions )
	{
		saveFloat( source, new File( filePath ), dataset, cellDimensions );
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link DoubleType} into an
	 * HDF5 float64 dataset.
	 *
	 * @param source
	 * @param writer
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveDouble(
			final RandomAccessibleInterval< DoubleType > source,
			final IHDF5Writer writer,
			final String dataset,
			final int[] cellDimensions )
	{
		final int n = source.numDimensions();
		final long[] dimensions = Intervals.dimensionsAsLongArray( source );
		final IHDF5DoubleWriter float64Writer = writer.float64();
		if ( !writer.exists( dataset ) )
			float64Writer.createMDArray(
					dataset,
					reorder( dimensions ),
					reorder( cellDimensions ) );

		final long[] offset = new long[ n ];
		final long[] sourceCellDimensions = new long[ n ];
		for ( int d = 0; d < n; )
		{
			cropCellDimensions( source, offset, cellDimensions, sourceCellDimensions );
			final RandomAccessibleInterval< DoubleType > sourceBlock = Views.offsetInterval( source, offset, sourceCellDimensions );
			final MDDoubleArray targetCell = new MDDoubleArray( reorder( sourceCellDimensions ) );
			int i = 0;
			for ( final DoubleType t : Views.flatIterable( sourceBlock ) )
				targetCell.set( t.get(), i++ );

			float64Writer.writeMDArrayBlockWithOffset( dataset, targetCell, reorder( offset ) );

			for ( d = 0; d < n; ++d )
			{
				offset[ d ] += cellDimensions[ d ];
				if ( offset[ d ] < source.dimension( d ) )
					break;
				else
					offset[ d ] = 0;
			}
		}
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link DoubleType} into an
	 * HDF5 float64 dataset.
	 *
	 * @param source
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveDouble(
			final RandomAccessibleInterval< DoubleType > source,
			final File file,
			final String dataset,
			final int[] cellDimensions )
	{
		final IHDF5Writer writer = HDF5Factory.open( file );
		saveDouble( source, writer, dataset, cellDimensions );
		writer.close();
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link DoubleType} into an
	 * HDF5 float64 dataset.
	 *
	 * @param source
	 * @param filePAth
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveDouble(
			final RandomAccessibleInterval< DoubleType > source,
			final String filePath,
			final String dataset,
			final int[] cellDimensions )
	{
		saveDouble( source, new File( filePath ), dataset, cellDimensions );
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link ShortType} into an HDF5
	 * uint16 dataset.
	 *
	 * @param source
	 * @param writer
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveUnsignedShort(
			final RandomAccessibleInterval< ShortType > source,
			final IHDF5Writer writer,
			final String dataset,
			final int[] cellDimensions )
	{
		final int n = source.numDimensions();
		final long[] dimensions = Intervals.dimensionsAsLongArray( source );
		final IHDF5ShortWriter uint16Writer = writer.uint16();
		if ( !writer.exists( dataset ) )
			uint16Writer.createMDArray(
					dataset,
					reorder( dimensions ),
					reorder( cellDimensions ),
					HDF5IntStorageFeatures.INT_AUTO_SCALING_DEFLATE );

		final long[] offset = new long[ n ];
		final long[] sourceCellDimensions = new long[ n ];
		for ( int d = 0; d < n; )
		{
			cropCellDimensions( source, offset, cellDimensions, sourceCellDimensions );
			final RandomAccessibleInterval< ShortType > sourceBlock = Views.offsetInterval( source, offset, sourceCellDimensions );
			final MDShortArray targetCell = new MDShortArray( reorder( sourceCellDimensions ) );
			int i = 0;
			for ( final ShortType t : Views.flatIterable( sourceBlock ) )
				targetCell.set( t.get(), i++ );

			uint16Writer.writeMDArrayBlockWithOffset( dataset, targetCell, reorder( offset ) );

			for ( d = 0; d < n; ++d )
			{
				offset[ d ] += cellDimensions[ d ];
				if ( offset[ d ] < source.dimension( d ) )
					break;
				else
					offset[ d ] = 0;
			}
		}
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link ShortType} into an HDF5
	 * uint16 dataset.
	 *
	 * @param source
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveUnsignedShort(
			final RandomAccessibleInterval< ShortType > source,
			final File file,
			final String dataset,
			final int[] cellDimensions )
	{
		final IHDF5Writer writer = HDF5Factory.open( file );
		saveUnsignedShort( source, writer, dataset, cellDimensions );
		writer.close();
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link ShortType} into an HDF5
	 * uint16 dataset.
	 *
	 * @param source
	 * @param filePAth
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveUnsignedShort(
			final RandomAccessibleInterval< ShortType > source,
			final String filePath,
			final String dataset,
			final int[] cellDimensions )
	{
		saveUnsignedShort( source, new File( filePath ), dataset, cellDimensions );
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link LongType} into an HDF5
	 * uint64 dataset.
	 *
	 * @param source
	 *            source
	 * @param dimensions
	 *            dimensions of the dataset if created new
	 * @param writer
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void createUnsignedLong(
			final IHDF5Writer writer,
			final String dataset,
			final Dimensions datasetDimensions,
			final int[] cellDimensions )
	{
		final IHDF5LongWriter uint64Writer = writer.uint64();

		if ( writer.exists( dataset ) )
			writer.delete( dataset );

		uint64Writer.createMDArray(
				dataset,
				reorder( Intervals.dimensionsAsLongArray( datasetDimensions ) ),
				reorder( cellDimensions ),
				HDF5IntStorageFeatures.INT_AUTO_SCALING_DEFLATE );
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link LongType} into an HDF5
	 * uint64 dataset.
	 *
	 * @param source
	 *            source
	 * @param writer
	 * @param dataset
	 * @param cellDimensions
	 */
	static public <T extends IntegerType<T>> void saveUnsignedLong(
			final RandomAccessibleInterval<T> source,
			final IHDF5Writer writer,
			final String dataset,
			final int[] cellDimensions )
	{
		if ( !writer.exists( dataset ) )
			createUnsignedLong( writer, dataset, source, cellDimensions );

		final long[] dimensions = reorder( writer.object().getDimensions( dataset ) );
		final int n = source.numDimensions();

		final IHDF5LongWriter uint64Writer = writer.uint64();

		/* min is >= 0, max is < dimensions */
		final long[] min = Intervals.minAsLongArray( source );
		final long[] max = Intervals.maxAsLongArray( source );
		for ( int d = 0; d < min.length; ++d )
		{
			min[ d ] = Math.max( 0, min[ d ] );
			max[ d ] = Math.min( dimensions[ d ] - 1, max[ d ] );
		}

		final long[] offset = min.clone();
		final long[] sourceCellDimensions = new long[ n ];
		for ( int d = 0; d < n; )
		{
			cropCellDimensions( max, offset, cellDimensions, sourceCellDimensions );
			final RandomAccessibleInterval<T> sourceBlock = Views.offsetInterval( source, offset, sourceCellDimensions );
			final MDLongArray targetCell = new MDLongArray( reorder( sourceCellDimensions ) );
			int i = 0;
			for ( final T t : Views.flatIterable( sourceBlock ) )
				targetCell.set( t.getIntegerLong(), i++ );

			uint64Writer.writeMDArrayBlockWithOffset( dataset, targetCell, reorder( offset ) );

			for ( d = 0; d < n; ++d )
			{
				offset[ d ] += cellDimensions[ d ];
				if ( offset[ d ] <= max[ d ] )
					break;
				else
					offset[ d ] = min[ d ];
			}
		}
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link LongType} into an HDF5
	 * uint64 dataset.
	 *
	 * @param source
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public <T extends IntegerType<T>> void saveUnsignedLong(
			final RandomAccessibleInterval<T> source,
			final File file,
			final String dataset,
			final int[] cellDimensions )
	{
		final IHDF5Writer writer = HDF5Factory.open( file );
		saveUnsignedLong( source, writer, dataset, cellDimensions );
		writer.close();
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link LongType} into an HDF5
	 * uint64 dataset.
	 *
	 * @param source
	 * @param filePAth
	 * @param dataset
	 * @param cellDimensions
	 */
	static public <T extends IntegerType<T>> void saveUnsignedLong(
			final RandomAccessibleInterval<T> source,
			final String filePath,
			final String dataset,
			final int[] cellDimensions )
	{
		saveUnsignedLong( source, new File( filePath ), dataset, cellDimensions );
	}

	/**
	 * Create anHDF5 int64 dataset.
	 *
	 * @param source
	 *            source
	 * @param dimensions
	 *            dimensions of the dataset if created new
	 * @param writer
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void createLong(
			final IHDF5Writer writer,
			final String dataset,
			final Dimensions datasetDimensions,
			final int[] cellDimensions )
	{
		final IHDF5LongWriter int64Writer = writer.int64();

		if ( writer.exists( dataset ) )
			writer.delete( dataset );

		int64Writer.createMDArray(
				dataset,
				reorder( Intervals.dimensionsAsLongArray( datasetDimensions ) ),
				reorder( cellDimensions ),
				HDF5IntStorageFeatures.INT_AUTO_SCALING_DEFLATE );
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link LongType} into an HDF5
	 * int64 dataset.
	 *
	 * @param source
	 *            source
	 * @param writer
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveLong(
			final RandomAccessibleInterval< LongType > source,
			final IHDF5Writer writer,
			final String dataset,
			final int[] cellDimensions )
	{
		if ( !writer.exists( dataset ) )
			createLong( writer, dataset, source, cellDimensions );

		final long[] dimensions = reorder( writer.object().getDimensions( dataset ) );
		final int n = source.numDimensions();

		final IHDF5LongWriter int64Writer = writer.int64();

		/* min is >= 0, max is < dimensions */
		final long[] min = Intervals.minAsLongArray( source );
		final long[] max = Intervals.maxAsLongArray( source );
		for ( int d = 0; d < min.length; ++d )
		{
			min[ d ] = Math.max( 0, min[ d ] );
			max[ d ] = Math.min( dimensions[ d ] - 1, max[ d ] );
		}

		final long[] offset = min.clone();
		final long[] sourceCellDimensions = new long[ n ];
		for ( int d = 0; d < n; )
		{
			cropCellDimensions( max, offset, cellDimensions, sourceCellDimensions );
			final RandomAccessibleInterval< LongType > sourceBlock = Views.offsetInterval( source, offset, sourceCellDimensions );
			final MDLongArray targetCell = new MDLongArray( reorder( sourceCellDimensions ) );
			int i = 0;
			for ( final LongType t : Views.flatIterable( sourceBlock ) )
				targetCell.set( t.get(), i++ );

			int64Writer.writeMDArrayBlockWithOffset( dataset, targetCell, reorder( offset ) );

			for ( d = 0; d < n; ++d )
			{
				offset[ d ] += cellDimensions[ d ];
				if ( offset[ d ] <= max[ d ] )
					break;
				else
					offset[ d ] = min[ d ];
			}
		}
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link LongType} into an HDF5
	 * int64 dataset.
	 *
	 * @param source
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveLong(
			final RandomAccessibleInterval< LongType > source,
			final File file,
			final String dataset,
			final int[] cellDimensions )
	{
		final IHDF5Writer writer = HDF5Factory.open( file );
		saveLong( source, writer, dataset, cellDimensions );
		writer.close();
	}

	/**
	 * Save a {@link RandomAccessibleInterval} of {@link LongType} into an HDF5
	 * int64 dataset.
	 *
	 * @param source
	 * @param filePAth
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveLong(
			final RandomAccessibleInterval< LongType > source,
			final String filePath,
			final String dataset,
			final int[] cellDimensions )
	{
		saveLong( source, new File( filePath ), dataset, cellDimensions );
	}

	/**
	 * Save the combination of a single element {@link LabelMultiset} source and
	 * a {@link LongType} overlay with transparent pixels into an HDF5 uint64
	 * dataset.
	 *
	 * @param labelMultisetSource
	 *            the background
	 * @param labelSource
	 *            the overlay
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveSingleElementLabelMultisetLongPair(
			final RandomAccessible< LabelMultisetType > labelMultisetSource,
			final RandomAccessible< LongType > labelSource,
			final Interval interval,
			final File file,
			final String dataset,
			final int[] cellDimensions )
	{
		assert labelMultisetSource.numDimensions() == labelSource.numDimensions() &&
				labelSource.numDimensions() == interval.numDimensions(): "input dimensions do not match";

		final RandomAccessiblePair< LabelMultisetType, LongType > pair = new RandomAccessiblePair<>( labelMultisetSource, labelSource );
		final RandomAccessibleInterval< Pair< LabelMultisetType, LongType > > pairInterval = Views.offsetInterval( pair, interval );
		final Converter< Pair< LabelMultisetType, LongType >, LongType > converter =
				( input, output ) -> {
					final long inputB = input.getB().get();
					if ( inputB == Label.TRANSPARENT )
						output.set( input.getA().entrySet().iterator().next().getElement().id() );
					else
						output.set( inputB );
				};

		final RandomAccessibleInterval< LongType > source =
				Converters.convert(
						pairInterval,
						converter,
						new LongType() );

		saveUnsignedLong( source, file, dataset, cellDimensions );
	}

	/**
	 * Save the combination of a single element {@link LabelMultiset} source and
	 * a fragment to segment assignment table and a {@link LongType} overlay
	 * with transparent pixels into an HDF5 uint64 dataset.
	 *
	 * @param labelMultisetSource
	 *            the background
	 * @param labelSource
	 *            the overlay
	 * @param interval
	 *            the interval to be saved
	 * @param assignment
	 *            fragment to segment assignment
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveAssignedSingleElementLabelMultisetLongPair(
			final RandomAccessible< LabelMultisetType > labelMultisetSource,
			final RandomAccessible< LongType > labelSource,
			final Interval interval,
			final FragmentSegmentAssignment assignment,
			final File file,
			final String dataset,
			final int[] cellDimensions )
	{
		assert labelMultisetSource.numDimensions() == labelSource.numDimensions() &&
				labelSource.numDimensions() == interval.numDimensions(): "input dimensions do not match";

		final RandomAccessiblePair< LabelMultisetType, LongType > pair = new RandomAccessiblePair<>( labelMultisetSource, labelSource );
		final RandomAccessibleInterval< Pair< LabelMultisetType, LongType > > pairInterval = Views.offsetInterval( pair, interval );
		final Converter< Pair< LabelMultisetType, LongType >, LongType > converter =
				( input, output ) -> {
					final long inputB = input.getB().get();
					if ( inputB == Label.TRANSPARENT )
						output.set( assignment.getSegment( input.getA().entrySet().iterator().next().getElement().id() ) );
					else
						output.set( assignment.getSegment( inputB ) );
				};

		final RandomAccessibleInterval< LongType > source =
				Converters.convert(
						pairInterval,
						converter,
						new LongType() );

		saveUnsignedLong( source, file, dataset, cellDimensions );
	}

	/**
	 * Load a long to long lookup table from an HDF5 dataset
	 *
	 * @param reader
	 * @param dataset
	 * @param blockSize
	 */
	static public TLongLongHashMap loadLongLongLut(
			final IHDF5Reader reader,
			final String dataset,
			final int blockSize )
	{
		final IHDF5LongReader uint64Reader = reader.uint64();

		if ( !reader.exists( dataset ) )
			return null;

		final long[] dimensions = reader.object().getDimensions( dataset );
		if ( !( dimensions.length == 2 && dimensions[ 0 ] == 2 ) )
		{
			System.err.println( "LUT is not a lookup table, dimensions = " + Arrays.toString( dimensions ) );
			return null;
		}

		final long size = dimensions[ 1 ];

		final TLongLongHashMap lut = new TLongLongHashMap(
				Constants.DEFAULT_CAPACITY,
				Constants.DEFAULT_LOAD_FACTOR,
				Label.TRANSPARENT,
				Label.TRANSPARENT );

		for ( int offset = 0; offset < size; offset += blockSize )
		{
			final MDLongArray block = uint64Reader.readMDArrayBlockWithOffset(
					dataset,
					new int[] { 2, ( int ) Math.min( blockSize, size - offset ) },
					new long[] { 0, offset } );

			for ( int i = 0; i < block.size( 1 ); ++i )
				lut.put( block.get( 0, i ), block.get( 1, i ) );

		}

		return lut;
	}

	/**
	 * Load a long to long lookup table from an HDF5 dataset.
	 *
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public TLongLongHashMap loadLongLongLut(
			final File file,
			final String dataset,
			final int blockSize )
	{
		final IHDF5Reader reader = HDF5Factory.openForReading( file );
		final TLongLongHashMap lut = loadLongLongLut( reader, dataset, blockSize );
		reader.close();
		return lut;
	}

	/**
	 * Load a long to long lookup table from an HDF5 dataset.
	 *
	 * @param filePath
	 * @param dataset
	 * @param cellDimensions
	 */
	static public TLongLongHashMap loadLongLongLut(
			final String filePath,
			final String dataset,
			final int blockSize )
	{
		final IHDF5Reader reader = HDF5Factory.openForReading( filePath );
		final TLongLongHashMap lut = loadLongLongLut( reader, dataset, blockSize );
		reader.close();
		return lut;
	}

	/**
	 * Save a long to long lookup table into an HDF5 uint64 dataset.
	 *
	 * @param lut
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveLongLongLut(
			final TLongLongHashMap lut,
			final IHDF5Writer writer,
			final String dataset,
			final int blockSize )
	{
		final IHDF5LongWriter uint64Writer = writer.uint64();
		if ( !writer.exists( dataset ) )
			uint64Writer.createMDArray(
					dataset,
					new long[] { 2, lut.size() },
					new int[] { 2, blockSize },
					HDF5IntStorageFeatures.INT_AUTO_SCALING_DEFLATE );

		final long[] keys = lut.keys();
		for ( int offset = 0, i = 0; offset < lut.size(); offset += blockSize )
		{
			final int size = Math.min( blockSize, lut.size() - offset );
			final MDLongArray targetCell = new MDLongArray( new int[] { 2, size } );
			for ( int j = 0; j < size; ++j, ++i )
			{
				targetCell.set( keys[ i ], 0, j );
				targetCell.set( lut.get( keys[ i ] ), 1, j );
			}

			uint64Writer.writeMDArrayBlockWithOffset( dataset, targetCell, new long[] { 0, offset } );
		}
	}

	/**
	 * Save a long to long lookup table into an HDF5 uint64 dataset.
	 *
	 * @param lut
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveLongLongLut(
			final TLongLongHashMap lut,
			final File file,
			final String dataset,
			final int blockSize )
	{
		final IHDF5Writer writer = HDF5Factory.open( file );
		saveLongLongLut( lut, writer, dataset, blockSize );
		writer.close();
	}

	/**
	 * Save a long to long lookup table into an HDF5 uint64 dataset.
	 *
	 * @param lut
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveLongLongLut(
			final TLongLongHashMap lut,
			final String filePath,
			final String dataset,
			final int blockSize )
	{
		final IHDF5Writer writer = HDF5Factory.open( filePath );
		saveLongLongLut( lut, writer, dataset, blockSize );
		writer.close();
	}

	/**
	 * Load a long collection from an HDF5 dataset
	 *
	 * @param collection
	 * @param reader
	 * @param dataset
	 * @param blockSize
	 */
	static public boolean loadLongCollection(
			final TLongCollection collection,
			final IHDF5Reader reader,
			final String dataset,
			final int blockSize )
	{
		final IHDF5LongReader uint64Reader = reader.uint64();

		if ( !reader.exists( dataset ) )
			return false;

		final long[] dimensions = reader.object().getDimensions( dataset );
		if ( dimensions.length != 1 )
		{
			System.err.println( "Dataset is not a collection, dimensions = " + Arrays.toString( dimensions ) );
			return false;
		}

		final long size = dimensions[ 0 ];

		for ( int offset = 0; offset < size; offset += blockSize )
		{
			final MDLongArray block = uint64Reader.readMDArrayBlockWithOffset(
					dataset,
					new int[] { ( int ) Math.min( blockSize, size - offset ) },
					new long[] { offset } );

			for ( int i = 0; i < block.size( 0 ); ++i )
				collection.add( block.get( i ) );
		}

		return true;
	}

	/**
	 * Load a long collection from an HDF5 dataset
	 *
	 * @param collection
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public boolean loadLongCollection(
			final TLongCollection collection,
			final File file,
			final String dataset,
			final int blockSize )
	{
		final IHDF5Reader reader = HDF5Factory.openForReading( file );
		final boolean success = loadLongCollection( collection, reader, dataset, blockSize );
		reader.close();
		return success;
	}

	/**
	 * Load a long collection from an HDF5 dataset
	 *
	 * @param collection
	 * @param filePath
	 * @param dataset
	 * @param cellDimensions
	 */
	static public boolean loadLongCollection(
			final TLongCollection collection,
			final String filePath,
			final String dataset,
			final int blockSize )
	{
		final IHDF5Reader reader = HDF5Factory.openForReading( filePath );
		final boolean success = loadLongCollection( collection, reader, dataset, blockSize );
		reader.close();
		return success;
	}

	/**
	 * Save a long collection into an HDF5 uint64 dataset.
	 *
	 * @param lut
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveLongCollection(
			final TLongCollection longs,
			final IHDF5Writer writer,
			final String dataset,
			final int blockSize )
	{
		final IHDF5LongWriter uint64Writer = writer.uint64();
		if ( !writer.exists( dataset ) )
			uint64Writer.createMDArray(
					dataset,
					new long[] { longs.size() },
					new int[] { blockSize },
					HDF5IntStorageFeatures.INT_AUTO_SCALING_DEFLATE );

		final TLongIterator iterator = longs.iterator();

		for ( int offset = 0; offset < longs.size(); offset += blockSize )
		{
			final int size = Math.min( blockSize, longs.size() - offset );
			final MDLongArray targetCell = new MDLongArray( new int[] { size } );
			for ( int i = 0; i < size; ++i )
				targetCell.set( iterator.next(), i );

			uint64Writer.writeMDArrayBlockWithOffset( dataset, targetCell, new long[] { offset } );
		}
	}

	/**
	 * Save a long collection into an HDF5 uint64 dataset.
	 *
	 * @param longs
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveLongCollection(
			final TLongCollection longs,
			final File file,
			final String dataset,
			final int blockSize )
	{
		final IHDF5Writer writer = HDF5Factory.open( file );
		saveLongCollection( longs, writer, dataset, blockSize );
		writer.close();
	}

	/**
	 * Save a collection of longs into an HDF5 uint64 dataset.
	 *
	 * @param longs
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public void saveLongCollection(
			final TLongCollection longs,
			final String filePath,
			final String dataset,
			final int blockSize )
	{
		final IHDF5Writer writer = HDF5Factory.open( filePath );
		saveLongCollection( longs, writer, dataset, blockSize );
		writer.close();
	}

	/**
	 * Load an attribute from of an HDF5 object.
	 *
	 * @param lut
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	@SuppressWarnings( "unchecked" )
	static public < T > T loadAttribute(
			final IHDF5Reader reader,
			final String object,
			final String attribute )
	{
		if ( !reader.exists( object ) )
			return null;

		if ( !reader.object().hasAttribute( object, attribute ) )
			return null;

		final HDF5DataTypeInformation attributeInfo = reader.object().getAttributeInformation( object, attribute );
		final Class< ? > type = attributeInfo.tryGetJavaType();
		if ( type.isAssignableFrom( long[].class ) )
			if ( attributeInfo.isSigned() )
				return ( T ) reader.int64().getArrayAttr( object, attribute );
			else
				return ( T ) reader.uint64().getArrayAttr( object, attribute );
		if ( type.isAssignableFrom( int[].class ) )
			if ( attributeInfo.isSigned() )
				return ( T ) reader.int32().getArrayAttr( object, attribute );
			else
				return ( T ) reader.uint32().getArrayAttr( object, attribute );
		if ( type.isAssignableFrom( short[].class ) )
			if ( attributeInfo.isSigned() )
				return ( T ) reader.int16().getArrayAttr( object, attribute );
			else
				return ( T ) reader.uint16().getArrayAttr( object, attribute );
		if ( type.isAssignableFrom( byte[].class ) )
		{
			if ( attributeInfo.isSigned() )
				return ( T ) reader.int8().getArrayAttr( object, attribute );
			else
				return ( T ) reader.uint8().getArrayAttr( object, attribute );
		}
		else if ( type.isAssignableFrom( double[].class ) )
			return ( T ) reader.float64().getArrayAttr( object, attribute );
		else if ( type.isAssignableFrom( float[].class ) )
			return ( T ) reader.float32().getArrayAttr( object, attribute );
		else if ( type.isAssignableFrom( String[].class ) )
			return ( T ) reader.string().getArrayAttr( object, attribute );
		if ( type.isAssignableFrom( long.class ) )
		{
			if ( attributeInfo.isSigned() )
				return ( T ) new Long( reader.int64().getAttr( object, attribute ) );
			else
				return ( T ) new Long( reader.uint64().getAttr( object, attribute ) );
		}
		else if ( type.isAssignableFrom( int.class ) )
		{
			if ( attributeInfo.isSigned() )
				return ( T ) new Integer( reader.int32().getAttr( object, attribute ) );
			else
				return ( T ) new Integer( reader.uint32().getAttr( object, attribute ) );
		}
		else if ( type.isAssignableFrom( short.class ) )
		{
			if ( attributeInfo.isSigned() )
				return ( T ) new Short( reader.int16().getAttr( object, attribute ) );
			else
				return ( T ) new Short( reader.uint16().getAttr( object, attribute ) );
		}
		else if ( type.isAssignableFrom( byte.class ) )
		{
			if ( attributeInfo.isSigned() )
				return ( T ) new Byte( reader.int8().getAttr( object, attribute ) );
			else
				return ( T ) new Byte( reader.uint8().getAttr( object, attribute ) );
		}
		else if ( type.isAssignableFrom( double.class ) )
			return ( T ) new Double( reader.float64().getAttr( object, attribute ) );
		else if ( type.isAssignableFrom( float.class ) )
			return ( T ) new Double( reader.float32().getAttr( object, attribute ) );
		else if ( type.isAssignableFrom( String.class ) )
			return ( T ) new String( reader.string().getAttr( object, attribute ) );

		System.err.println( "Reading attributes of type " + attributeInfo + " not yet implemented." );
		return null;
	}

	/**
	 * Load an attribute from of an HDF5 object.
	 *
	 * @param lut
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public < T > T loadAttribute(
			final File file,
			final String object,
			final String attribute )
	{
		final IHDF5Reader reader = HDF5Factory.openForReading( file );
		final T t = loadAttribute( reader, object, attribute );
		reader.close();
		return t;
	}

	static public void getAllDatasetPaths(
			final IHDF5Reader reader,
			final String object,
			final List< String > paths )
	{
		List< String > allPaths = null;

		if ( reader.object().isDataSet( object ) )
		{
			paths.add( object );
		}
		else
		{
			allPaths = reader.getGroupMembers( object );
			for ( int j = 0; j < allPaths.size(); j++ )
			{
				getAllDatasetPaths( reader, object.concat( allPaths.get( j ) ).concat( "/" ), paths );
			}
		}
	}

	/**
	 * Load an attribute from of an HDF5 object.
	 *
	 * @param lut
	 * @param file
	 * @param dataset
	 * @param cellDimensions
	 */
	static public < T > T loadAttribute(
			final String filePath,
			final String object,
			final String attribute )
	{
		final IHDF5Reader reader = HDF5Factory.openForReading( filePath );
		final T t = loadAttribute( reader, object, attribute );
		reader.close();
		return t;
	}

	/**
	 * Save a double array as a float64[] attribute of an HDF5 object.
	 *
	 * @param values
	 * @param writer
	 * @param object
	 * @param attribute
	 */
	static public void saveAttribute(
			final double[] values,
			final IHDF5Writer writer,
			final String object,
			final String attribute )
	{
		if ( !writer.exists( object ) )
			writer.object().createGroup( object );

		writer.float64().setArrayAttr( object, attribute, values );
	}

	/**
	 * Save a double array as a float64[] attribute of an HDF5 object.
	 *
	 * @param values
	 * @param file
	 * @param object
	 * @param attribute
	 */
	static public void saveAttribute(
			final double[] values,
			final File file,
			final String object,
			final String attribute )
	{
		final IHDF5Writer writer = HDF5Factory.open( file );
		saveAttribute( values, writer, object, attribute );
		writer.close();
	}

	/**
	 * Save a double array as a float64[] attribute of an HDF5 object.
	 *
	 * @param values
	 * @param filePath
	 * @param object
	 * @param attribute
	 */
	static public void saveAttribute(
			final double[] values,
			final String filePath,
			final String object,
			final String attribute )
	{
		final IHDF5Writer writer = HDF5Factory.open( filePath );
		saveAttribute( values, writer, object, attribute );
		writer.close();
	}

	/**
	 * Save a long value as a uint64 attribute of an HDF5 object.
	 *
	 * @param value
	 * @param writer
	 * @param object
	 * @param attribute
	 */
	static public void saveUint64Attribute(
			final long value,
			final IHDF5Writer writer,
			final String object,
			final String attribute )
	{
		if ( !writer.exists( object ) )
			writer.object().createGroup( object );

		// TODO Bug in JHDF5, does not save the value most of the time when
		// using the non-deprecated method
		writer.uint64().setAttr( object, attribute, value );
		// The deprecated method no longer exists
//		writer.setLongAttribute( object, attribute, value );
//		writer.file().flush();
	}

	/**
	 * Save a long value as a uint64 attribute of an HDF5 object.
	 *
	 * @param value
	 * @param file
	 * @param object
	 * @param attribute
	 */
	static public void saveUint64Attribute(
			final long value,
			final File file,
			final String object,
			final String attribute )
	{
		final IHDF5Writer writer = HDF5Factory.open( file );
		saveUint64Attribute( value, writer, object, attribute );
		writer.close();
	}

	/**
	 * Save a long value as a uint64 attribute of an HDF5 object.
	 *
	 * @param value
	 * @param filePath
	 * @param object
	 * @param attribute
	 */
	static public void saveUint64Attribute(
			final long value,
			final String filePath,
			final String object,
			final String attribute )
	{
		final IHDF5Writer writer = HDF5Factory.open( filePath );
		saveUint64Attribute( value, writer, object, attribute );
		writer.close();
	}

	static public void saveFloatArrayAttribute(
			final float[] value,
			final File file,
			final String dataset,
			final String attribute )
	{
		final IHDF5Writer writer = HDF5Factory.open( file );
		writer.float32().setArrayAttr( dataset, attribute, value );
		writer.close();
	}

	static public void saveDoubleArrayAttribute(
			final double[] value,
			final File file,
			final String dataset,
			final String attribute )
	{
		final IHDF5Writer writer = HDF5Factory.open( file );
		writer.float64().setArrayAttr( dataset, attribute, value );
		writer.close();
	}

	/**
	 * Create a {@link CellLoader} for use in a lazy {@link CachedCellImg}.
	 *
	 * @param reader
	 * @param dataset
	 * @param type
	 * @param signed
	 * @return
	 */
	public static < T extends NativeType< T > > CellLoader< T > createCellLoader(
			final IHDF5Reader reader,
			final String dataset,
			final Class< ? > type,
			final boolean signed )
	{
		if ( type.isAssignableFrom( byte.class ) )
			return signed ? ( img ) -> {
				final byte[] data = reader.int8().readMDArrayBlockWithOffset(
						dataset,
						reorder( Intervals.dimensionsAsIntArray( img ) ),
						reorder( Intervals.minAsLongArray( img ) ) ).getAsFlatArray();

				@SuppressWarnings( "unchecked" )
				final Cursor< ? extends GenericByteType< ? > > c = ( Cursor< ? extends GenericByteType< ? > > ) img.cursor();
				for ( int i = 0; i < data.length; ++i )
					c.next().setByte( data[ i ] );
			} : ( img ) -> {
				final byte[] data = reader.uint8().readMDArrayBlockWithOffset(
						dataset,
						reorder( Intervals.dimensionsAsIntArray( img ) ),
						reorder( Intervals.minAsLongArray( img ) ) ).getAsFlatArray();

				@SuppressWarnings( "unchecked" )
				final Cursor< ? extends GenericByteType< ? > > c = ( Cursor< ? extends GenericByteType< ? > > ) img.cursor();
				for ( int i = 0; i < data.length; ++i )
					c.next().setByte( data[ i ] );
			};
		else if ( type.isAssignableFrom( short.class ) )
			return signed ? ( img ) -> {
				final short[] data = reader.int16().readMDArrayBlockWithOffset(
						dataset,
						reorder( Intervals.dimensionsAsIntArray( img ) ),
						reorder( Intervals.minAsLongArray( img ) ) ).getAsFlatArray();

				@SuppressWarnings( "unchecked" )
				final Cursor< ? extends GenericShortType< ? > > c = ( Cursor< ? extends GenericShortType< ? > > ) img.cursor();
				for ( int i = 0; i < data.length; ++i )
					c.next().setShort( data[ i ] );
			} : ( img ) -> {
				final short[] data = reader.uint16().readMDArrayBlockWithOffset(
						dataset,
						reorder( Intervals.dimensionsAsIntArray( img ) ),
						reorder( Intervals.minAsLongArray( img ) ) ).getAsFlatArray();

				@SuppressWarnings( "unchecked" )
				final Cursor< ? extends GenericShortType< ? > > c = ( Cursor< ? extends GenericShortType< ? > > ) img.cursor();
				for ( int i = 0; i < data.length; ++i )
					c.next().setShort( data[ i ] );
			};
		else if ( type.isAssignableFrom( int.class ) )
			return signed ? ( img ) -> {
				final int[] data = reader.int32().readMDArrayBlockWithOffset(
						dataset,
						reorder( Intervals.dimensionsAsIntArray( img ) ),
						reorder( Intervals.minAsLongArray( img ) ) ).getAsFlatArray();

				@SuppressWarnings( "unchecked" )
				final Cursor< ? extends GenericIntType< ? > > c = ( Cursor< ? extends GenericIntType< ? > > ) img.cursor();
				for ( int i = 0; i < data.length; ++i )
					c.next().setInt( data[ i ] );
			} : ( img ) -> {
				final int[] data = reader.uint32().readMDArrayBlockWithOffset(
						dataset,
						reorder( Intervals.dimensionsAsIntArray( img ) ),
						reorder( Intervals.minAsLongArray( img ) ) ).getAsFlatArray();

				@SuppressWarnings( "unchecked" )
				final Cursor< ? extends GenericIntType< ? > > c = ( Cursor< ? extends GenericIntType< ? > > ) img.cursor();
				for ( int i = 0; i < data.length; ++i )
					c.next().setInt( data[ i ] );
			};
		else if ( type.isAssignableFrom( long.class ) )
			return signed ? ( img ) -> {
				final long[] data = reader.int64().readMDArrayBlockWithOffset(
						dataset,
						reorder( Intervals.dimensionsAsIntArray( img ) ),
						reorder( Intervals.minAsLongArray( img ) ) ).getAsFlatArray();

				@SuppressWarnings( "unchecked" )
				final Cursor< ? extends GenericLongType< ? > > c = ( Cursor< ? extends GenericLongType< ? > > ) img.cursor();
				for ( int i = 0; i < data.length; ++i )
					c.next().setLong( data[ i ] );
			} : ( img ) -> {
				final long[] data = reader.uint64().readMDArrayBlockWithOffset(
						dataset,
						reorder( Intervals.dimensionsAsIntArray( img ) ),
						reorder( Intervals.minAsLongArray( img ) ) ).getAsFlatArray();

				@SuppressWarnings( "unchecked" )
				final Cursor< ? extends GenericLongType< ? > > c = ( Cursor< ? extends GenericLongType< ? > > ) img.cursor();
				for ( int i = 0; i < data.length; ++i )
					c.next().setLong( data[ i ] );
			};
		else if ( type.isAssignableFrom( float.class ) )
			return ( img ) -> {
				final float[] data = reader.float32().readMDArrayBlockWithOffset(
						dataset,
						reorder( Intervals.dimensionsAsIntArray( img ) ),
						reorder( Intervals.minAsLongArray( img ) ) ).getAsFlatArray();

				@SuppressWarnings( "unchecked" )
				final Cursor< ? extends FloatType > c = ( Cursor< ? extends FloatType > ) img.cursor();
				for ( int i = 0; i < data.length; ++i )
					c.next().set( data[ i ] );
			};
		else if ( type.isAssignableFrom( double.class ) )
			return ( img ) -> {
				final double[] data = reader.float64().readMDArrayBlockWithOffset(
						dataset,
						reorder( Intervals.dimensionsAsIntArray( img ) ),
						reorder( Intervals.minAsLongArray( img ) ) ).getAsFlatArray();

				@SuppressWarnings( "unchecked" )
				final Cursor< ? extends DoubleType > c = ( Cursor< ? extends DoubleType > ) img.cursor();
				for ( int i = 0; i < data.length; ++i )
					c.next().set( data[ i ] );
			};
		else
			return null;
	}

	/**
	 * Open an HDF5 dataset as a memory cached {@link LazyCellImg}, using
	 * {@link VolatileAccess} to enable wrapping as a volatile.
	 *
	 * @param n5
	 * @param dataset
	 * @return
	 * @throws IOException
	 */
	@SuppressWarnings( { "unchecked", "rawtypes" } )
	public static final < T extends NativeType< T > > RandomAccessibleInterval< T > open(
			final IHDF5Reader reader,
			final String dataset,
			final int[] blockSize ) throws IOException
	{
		final Class< ? > dataType = reader.getDataSetInformation( dataset ).getTypeInformation().tryGetJavaType();
		final boolean signed = reader.getDataSetInformation( dataset ).getTypeInformation().isSigned();

		final CellLoader< T > loader = createCellLoader( reader, dataset, dataType, signed );

		final long[] dimensions = reorder( reader.object().getDataSetInformation( dataset ).getDimensions() );

		final CellGrid grid = new CellGrid( dimensions, blockSize );

		final CachedCellImg< T, ? > img;
		final T type;
		final Cache< Long, Cell< ? > > cache;

		if ( dataType.isAssignableFrom( byte.class ) )
		{
			if ( signed )
			{
				type = ( T ) new ByteType();
				cache = ( Cache ) new SoftRefLoaderCache< Long, Cell< ByteArray > >()
						.withLoader( LoadedCellCacheLoader.get( grid, loader, type, VOLATILE ) );
				img = new CachedCellImg( grid, type, cache, ArrayDataAccessFactory.get( BYTE, VOLATILE ) );
			}
			else
			{
				type = ( T ) new UnsignedByteType();
				cache = ( Cache ) new SoftRefLoaderCache< Long, Cell< ByteArray > >()
						.withLoader( LoadedCellCacheLoader.get( grid, loader, type, VOLATILE ) );
				img = new CachedCellImg( grid, type, cache, ArrayDataAccessFactory.get( BYTE, VOLATILE ) );
			}
		}
		else if ( dataType.isAssignableFrom( short.class ) )
		{
			if ( signed )
			{
				type = ( T ) new ShortType();
				cache = ( Cache ) new SoftRefLoaderCache< Long, Cell< ShortArray > >()
						.withLoader( LoadedCellCacheLoader.get( grid, loader, type, VOLATILE ) );
				img = new CachedCellImg( grid, type, cache, ArrayDataAccessFactory.get( SHORT, VOLATILE ) );
			}
			else
			{
				type = ( T ) new UnsignedShortType();
				cache = ( Cache ) new SoftRefLoaderCache< Long, Cell< ShortArray > >()
						.withLoader( LoadedCellCacheLoader.get( grid, loader, type, VOLATILE ) );
				img = new CachedCellImg( grid, type, cache, ArrayDataAccessFactory.get( SHORT, VOLATILE ) );
			}
		}
		else if ( dataType.isAssignableFrom( int.class ) )
		{
			if ( signed )
			{
				type = ( T ) new IntType();
				cache = ( Cache ) new SoftRefLoaderCache< Long, Cell< IntArray > >()
						.withLoader( LoadedCellCacheLoader.get( grid, loader, type, VOLATILE ) );
				img = new CachedCellImg( grid, type, cache, ArrayDataAccessFactory.get( INT, VOLATILE ) );
			}
			else
			{
				type = ( T ) new UnsignedIntType();
				cache = ( Cache ) new SoftRefLoaderCache< Long, Cell< IntArray > >()
						.withLoader( LoadedCellCacheLoader.get( grid, loader, type, VOLATILE ) );
				img = new CachedCellImg( grid, type, cache, ArrayDataAccessFactory.get( INT, VOLATILE ) );
			}
		}
		else if ( dataType.isAssignableFrom( long.class ) )
		{
			if ( signed )
			{
				type = ( T ) new LongType();
				cache = ( Cache ) new SoftRefLoaderCache< Long, Cell< LongArray > >()
						.withLoader( LoadedCellCacheLoader.get( grid, loader, type, VOLATILE ) );
				img = new CachedCellImg( grid, type, cache, ArrayDataAccessFactory.get( LONG, VOLATILE ) );
			}
			else
			{
				type = ( T ) new UnsignedLongType();
				cache = ( Cache ) new SoftRefLoaderCache< Long, Cell< LongArray > >()
						.withLoader( LoadedCellCacheLoader.get( grid, loader, type, VOLATILE ) );
				img = new CachedCellImg( grid, type, cache, ArrayDataAccessFactory.get( LONG, VOLATILE ) );
			}
		}
		else if ( dataType.isAssignableFrom( float.class ) )
		{
			type = ( T ) new FloatType();
			cache = ( Cache ) new SoftRefLoaderCache< Long, Cell< FloatArray > >()
					.withLoader( LoadedCellCacheLoader.get( grid, loader, type, VOLATILE ) );
			img = new CachedCellImg( grid, type, cache, ArrayDataAccessFactory.get( FLOAT, VOLATILE ) );
		}
		else if ( dataType.isAssignableFrom( double.class ) )
		{
			type = ( T ) new DoubleType();
			cache = ( Cache ) new SoftRefLoaderCache< Long, Cell< DoubleArray > >()
					.withLoader( LoadedCellCacheLoader.get( grid, loader, type, VOLATILE ) );
			img = new CachedCellImg( grid, type, cache, ArrayDataAccessFactory.get( DOUBLE, VOLATILE ) );
		}
		else
			img = null;

		return img;
	}

	/**
	 * Open an HDF5 dataset as a memory cached {@link LazyCellImg}, using
	 * {@link VolatileAccess} to enable wrapping as a volatile.
	 *
	 * @param n5
	 * @param dataset
	 * @return
	 * @throws IOException
	 */
	public static final < T extends NativeType< T > > RandomAccessibleInterval< T > open(
			final IHDF5Reader reader,
			final String dataset ) throws IOException
	{
		final HDF5DataSetInformation datasetInfo = reader.object().getDataSetInformation( dataset );
		final int[] blockSize;
		if ( datasetInfo.getStorageLayout() == HDF5StorageLayout.CHUNKED )
			blockSize = reorder( datasetInfo.tryGetChunkSizes() );
		else
		{
			blockSize = new int[ datasetInfo.getRank() ];
			Arrays.fill( blockSize, 32 );
		}

		return open( reader, dataset, blockSize );
	}
} public class LabelMultisetType extends AbstractNativeType< LabelMultisetType > implements Multiset< Label >
{
	public static final LabelMultisetType type = new LabelMultisetType();

	private final NativeImg< ?, VolatileLabelMultisetArray > img;

	private VolatileLabelMultisetArray access;

	private final LabelMultisetEntryList entries;

	private final Set< Entry< Label > > entrySet;

	// this is the constructor if you want it to read from an array
	public LabelMultisetType( final NativeImg< ?, VolatileLabelMultisetArray > img )
	{
		this( img, null );
	}

	// this is the constructor if you want to specify the dataAccess
	public LabelMultisetType( final VolatileLabelMultisetArray access )
	{
		this( null, access );
	}

	// this is the constructor if you want it to be a variable
	public LabelMultisetType()
	{
		this( null, new VolatileLabelMultisetArray( 1, true ) );
	}

	private LabelMultisetType( final NativeImg< ?, VolatileLabelMultisetArray > img, final VolatileLabelMultisetArray access )
	{
		this.entries = new LabelMultisetEntryList();
		this.img = img;
		this.access = access;
		this.entrySet = new AbstractSet< Entry< Label > >()
		{
			private final RefIterator< Entry< Label > > iterator = new RefIterator< Entry< Label > >()
			{
				private final RefIterator< LabelMultisetEntry > it = entries.iterator();

				@Override
				public boolean hasNext()
				{
					return it.hasNext();
				}

				@Override
				public LabelMultisetEntry next()
				{
					return it.next();
				}

				@Override
				public void release()
				{
					it.release();
				}

				@Override
				public void reset()
				{
					it.reset();
				}
			};

			@Override
			public RefIterator< Entry< Label > > iterator()
			{
				iterator.reset();
				return iterator;
			}

			@Override
			public int size()
			{
				return entries.size();
			}
		};
	}

	@Override
	public Fraction getEntitiesPerPixel()
	{
		return new Fraction();
	}

	@Override
	public void updateContainer( final Object c )
	{
		access = img.update( c );
	}

	@Override
	public LabelMultisetType createVariable()
	{
		return new LabelMultisetType();
	}

	@Override
	public LabelMultisetType copy()
	{
		return new LabelMultisetType( img, access );
	}

	@Override
	public void set( final LabelMultisetType c )
	{
		throw new UnsupportedOperationException();
	}

	@Override
	public NativeImg< LabelMultisetType, ? > createSuitableNativeImg( final NativeImgFactory< LabelMultisetType > storageFactory, final long[] dim )
	{
		throw new UnsupportedOperationException();
	}

	@Override
	public LabelMultisetType duplicateTypeOnSameNativeImg()
	{
		return new LabelMultisetType( img );
	}

	// ==== Multiset< Label > =====

	@Override
	public int size()
	{
		access.getValue( i, entries );
		return entries.multisetSize();
	}

	@Override
	public boolean isEmpty()
	{
		access.getValue( i, entries );
		return entries.isEmpty();
	}

	@Override
	public boolean contains( final Object o )
	{
		access.getValue( i, entries );
		return ( ( o instanceof Label ) && entries.binarySearch( ( ( Label ) o ).id() ) >= 0 );
	}

	public boolean contains( final long id )
	{
		access.getValue( i, entries );
		return entries.binarySearch( id ) >= 0;
	}

	public boolean containsAll( final long[] ids )
	{
		access.getValue( i, entries );
		for ( final long id : ids )
			if ( entries.binarySearch( id ) < 0 )
				return false;
		return true;
	}

	@Override
	public boolean containsAll( final Collection< ? > c )
	{
		access.getValue( i, entries );
		for ( final Object o : c )
			if ( ! ( ( o instanceof Label ) && entries.binarySearch( ( ( Label ) o ).id() ) >= 0 ) )
				return false;
		return true;
	}

	@Override
	public int count( final Object o )
	{
		access.getValue( i, entries );
		if ( ! ( o instanceof Label ) )
			return 0;

		final int pos = entries.binarySearch( ( ( Label ) o ).id() );
		if ( pos < 0 )
			return 0;

		return entries.get( pos ).getCount();
	}

	@Override
	public Set< Entry< Label > > entrySet()
	{
		access.getValue( i, entries );
		return entrySet;
	}

	@Override
	public String toString()
	{
		access.getValue( i, entries );
		return entries.toString();
	}

	// for volatile type
	boolean isValid()
	{
		return access.isValid();
	}

	@Override public Iterator< Label > iterator() { throw new UnsupportedOperationException(); }
	@Override public Object[] toArray() { throw new UnsupportedOperationException(); }
	@Override public < T > T[] toArray( final T[] a ) { throw new UnsupportedOperationException(); }
	@Override public boolean add( final Label e ) { throw new UnsupportedOperationException(); }
	@Override public boolean remove( final Object o ) { throw new UnsupportedOperationException(); }
	@Override public boolean addAll( final Collection< ? extends Label > c ) { throw new UnsupportedOperationException(); }
	@Override public boolean removeAll( final Collection< ? > c ) { throw new UnsupportedOperationException(); }
	@Override public boolean retainAll( final Collection< ? > c ) { throw new UnsupportedOperationException(); }
	@Override public void clear() { throw new UnsupportedOperationException(); }

	@Override
	public boolean valueEquals( LabelMultisetType other )
	{
		if ( entries.size() != other.entries.size() )
			return false;

		final RefIterator< LabelMultisetEntry > ai = entries.iterator();
		final RefIterator< LabelMultisetEntry > bi = other.entries.iterator();

		while ( ai.hasNext() )
		{
			final LabelMultisetEntry a = ai.next();
			final LabelMultisetEntry b = bi.next();
			if ( !( a.getId() == b.getId() && a.getCount() == b.getCount() ) )
				return false;
		}
		return true;
	}
} public interface IdService
{
	/**
	 * Invalidate an ID.  Sets the next ID of this service if the passed ID
	 * is greater than the current next ID.
	 *
	 * @param id
	 */
	public void invalidate( final long id );

	/**
	 * Get the next ID.
	 *
	 * @return
	 */
	public long next();

	/**
	 * Get the n next IDs.
	 *
	 * @param n
	 * @return
	 */
	public long[] next( final int n );

	/**
	 * Greater than comparison for two uint64 passed as long.
	 *
	 * @param a
	 * @param b
	 * @return
	 */
	static public boolean greaterThan( final long a, final long b )
	{
		return a + Long.MIN_VALUE > b + Long.MIN_VALUE;
	}

	/**
	 * Max of two uint64 passed as long.
	 *
	 * @param a
	 * @param b
	 * @return
	 */
	static public long max( final long a, final long b )
	{
		return a + Long.MIN_VALUE > b + Long.MIN_VALUE ? a : b;
	}

	/**
	 * Max of a stream of uint64 passed as long.
	 *
	 * @param ids
	 * @return
	 */
	static public long max( final LongStream ids )
	{
		return ids.reduce( 0, ( a, b ) -> max( a, b ) );
	}

	static public long max( final long[] ids )
	{
		return max( LongStream.of( ids ) );
	}
}
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Callable;
import org.scijava.ui.behaviour.io.InputTriggerConfig;
import org.scijava.ui.behaviour.io.InputTriggerDescription;
import org.scijava.ui.behaviour.io.yaml.YamlConfigIO;
import org.scijava.ui.behaviour.util.TriggerBehaviourBindings;
import bdv.BigDataViewer;
import bdv.bigcat.composite.ARGBCompositeAlphaYCbCr;
import bdv.bigcat.composite.Composite;
import bdv.bigcat.composite.CompositeCopy;
import bdv.bigcat.control.ConfirmSegmentController;
import bdv.bigcat.control.MergeController;
import bdv.bigcat.control.SelectionController;
import bdv.bigcat.control.TranslateZController;
import bdv.bigcat.label.FragmentSegmentAssignment;
import bdv.bigcat.label.LabelMultiSetIdPicker;
import bdv.bigcat.label.SegmentAssignment;
import bdv.bigcat.ui.ARGBConvertedLabelsSource;
import bdv.bigcat.ui.AbstractARGBConvertedLabelsSource;
import bdv.bigcat.ui.ModalGoldenAngleSaturatedARGBStream;
import bdv.bigcat.ui.Util;
import bdv.img.SetCache;
import bdv.img.cache.VolatileGlobalCellCache;
import bdv.img.h5.H5LabelMultisetSetupImageLoader;
import bdv.img.h5.H5UnsignedByteSetupImageLoader;
import bdv.img.h5.H5Utils;
import bdv.labels.labelset.Label;
import bdv.labels.labelset.LabelMultisetType;
import bdv.labels.labelset.Multiset;
import bdv.util.IdService;
import bdv.util.LocalIdService;
import ch.systemsx.cisd.hdf5.HDF5Factory;
import ch.systemsx.cisd.hdf5.IHDF5Reader;
import gnu.trove.map.hash.TLongLongHashMap;
import gnu.trove.set.hash.TLongHashSet;
import net.imglib2.interpolation.randomaccess.NearestNeighborInterpolatorFactory;
import net.imglib2.realtransform.RealViews;
import net.imglib2.type.numeric.ARGBType;
import net.imglib2.type.numeric.integer.LongType;
import net.imglib2.view.Views;
import picocli.CommandLine;
import picocli.CommandLine.Option;
package bdv.bigcat;




public class BigCatViewer< P extends BigCatViewer.Parameters >
{
	static public class Parameters implements Callable<Optional<Void>>
	{
		@Option( names = { "--infile", "-i" }, required = true, description = "Input file path" )
		public String inFile = "";

		@Option( names = { "--infilelabels", "-j" }, description = "Input file path for labels (if different from input file path for raw data)" )
		public String inFileLabels = null;

		@Option( names = { "--raw", "-r" }, description = "raw pixel datasets" )
		public List< String > raws = new ArrayList<>();

		@Option( names = { "--label", "-l" }, description = "label datasets" )
		public List< String > labels = new ArrayList<>();

		@Option( names = { "--assignment", "-a" }, description = "fragment segment assignment table" )
		public String assignment = "/fragment_segment_lut";

		@Option( names = { "--complete", "-f" }, description = "complete segments" )
		public String completeSegments = "/complete_segments";

		@Override
		public Optional<Void> call()
		{
			if ( inFileLabels == null )
				inFileLabels = inFile;

			if ( inFile == null )
				inFile = inFileLabels;

			return Optional.empty();
		}
	}

	/** default cell dimensions */
	final static protected int[] cellDimensions = new int[]{ 64, 64, 8 };

	/** global ID generator */
	protected IdService idService;

	/** raw pixels (image data) */
	final protected ArrayList< H5UnsignedByteSetupImageLoader > raws = new ArrayList<>();

	/** fragment to segment assignment */
	protected FragmentSegmentAssignment assignment;