Skip to content

Conversation

tpietzsch
Copy link
Member

The JSON "link" includes viewer transform, color and intensity, visibility, and display mode settings.

Additionally, the link also includes ResourceSpecs for re-creating Sources (or matching existing sources) if they are registered with the new ResourceManager. (This is already implemented for SpimData sources in this PR).

When copying and pasting links, it is not clear what the user wants:

  • Copy&paste everything?
  • Copy&paste only the current transformation, but leave the color settings untouched?
  • etc...

The approach to this is to copy as much information as possible, and allow the user to configure what to use when pasting.
This can be configured in the Preferences dialog and is saved to a settings YAML file:
Screenshot 2025-03-25 at 13 30 57

In case the user often wants to change the pasting behaviour, this configuration page can also be added to the side panel.
Screenshot 2025-03-25 at 13 31 35

Default shortcuts for copying and pasting are command/ctrl-C and command/ctrl-V, obviously...
Screenshot 2025-03-25 at 13 32 02

@imagesc-bot
Copy link

This pull request has been mentioned on Image.sc Forum. There might be relevant details there:

https://forum.image.sc/t/fiji-friends-weekly-dev-update-thread/103718/80

@StephanPreibisch
Copy link
Member

StephanPreibisch commented Apr 5, 2025

Hi @tpietzsch, sorry, I get two errors when using this pull request in bigstitcher, and it does not seem to work (I assumed copy&paste is somehow obvious, I couldn't find really any hints of it in the GUI)

1) it's of course not there, not sure if it has any consequences:

java.io.FileNotFoundException: /Users/preibischs/Library/Application Support/sc.fiji.bigdataviewer/linksettings.yaml (No such file or directory)
	at java.io.FileInputStream.open0(Native Method)
	at java.io.FileInputStream.open(FileInputStream.java:195)
	at java.io.FileInputStream.<init>(FileInputStream.java:138)
	at java.io.FileInputStream.<init>(FileInputStream.java:93)
	at java.io.FileReader.<init>(FileReader.java:58)
	at bdv.ui.links.LinkSettingsIO.load(LinkSettingsIO.java:64)
	at bdv.ui.links.LinkSettingsManager.load(LinkSettingsManager.java:79)
	at bdv.ui.links.LinkSettingsManager.load(LinkSettingsManager.java:72)
	at bdv.ui.links.LinkSettingsManager.<init>(LinkSettingsManager.java:62)
	at bdv.BigDataViewer.<init>(BigDataViewer.java:435)
	at bdv.BigDataViewer.<init>(BigDataViewer.java:393)
	at bdv.BigDataViewer.open(BigDataViewer.java:629)
	at net.preibisch.mvrecon.fiji.spimdata.explorer.popup.BDVPopup.createBDV(BDVPopup.java:336)
	at net.preibisch.mvrecon.fiji.spimdata.explorer.ViewSetupExplorerPanel.<init>(ViewSetupExplorerPanel.java:173)
	at net.preibisch.mvrecon.fiji.spimdata.explorer.ViewSetupExplorer.<init>(ViewSetupExplorer.java:47)
	at net.preibisch.mvrecon.fiji.plugin.Data_Explorer.run(Data_Explorer.java:77)
	at net.preibisch.mvrecon.fiji.plugin.Data_Explorer.main(Data_Explorer.java:117)

2) this one goes into an infinite loop (when pressing Apple+c):

Exception in thread "AWT-EventQueue-0" java.lang.StackOverflowError
	at sun.reflect.UnsafeFieldAccessorImpl.ensureObj(UnsafeFieldAccessorImpl.java:57)
	at sun.reflect.UnsafeDoubleFieldAccessorImpl.getDouble(UnsafeDoubleFieldAccessorImpl.java:68)
	at sun.reflect.UnsafeDoubleFieldAccessorImpl.get(UnsafeDoubleFieldAccessorImpl.java:36)
	at java.lang.reflect.Field.get(Field.java:393)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.write(ReflectiveTypeAdapterFactory.java:187)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:368)
	at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:70)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.write(ReflectiveTypeAdapterFactory.java:196)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:368)
	at com.google.gson.Gson$FutureTypeAdapter.write(Gson.java:1371)
	at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:70)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.write(ReflectiveTypeAdapterFactory.java:196)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:368)
	at com.google.gson.Gson$FutureTypeAdapter.write(Gson.java:1371)
	at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:70)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.write(ReflectiveTypeAdapterFactory.java:196)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:368)
	at com.google.gson.Gson$FutureTypeAdapter.write(Gson.java:1371)
	at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:70)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.write(ReflectiveTypeAdapterFactory.java:196)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:368)
	at com.google.gson.Gson$FutureTypeAdapter.write(Gson.java:1371)
	at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:70)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.write(ReflectiveTypeAdapterFactory.java:196)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:368)
	at com.google.gson.Gson$FutureTypeAdapter.write(Gson.java:1371)

@tpietzsch
Copy link
Member Author

tpietzsch commented Apr 7, 2025

@StephanPreibisch The first exception is expected and not a problem.

The second one, I have not seen, but @bogovicj has the same.
The JsonAdapters are annotated and discovered using scijava annotations. Maybe something is going wrong there? Could you check when you build bigdataviewer-core whether you have an annotation index file that looks like like this:

pietzsch@mycroft:~/workspace/bigdataviewer/bigdataviewer-core (link*)$ cat target/classes/META-INF/json/bdv.tools.JsonUtils\$JsonIo
{"class":"bdv.img.remote.AffineTransform3DJsonSerializer","values":{"jsonType":"AffineTransform3D.Anchor","type":"net.imglib2.realtransform.AffineTransform3D"}}
{"class":"bdv.tools.JsonUtils$Typed$JsonAdapter","values":{"jsonType":"JsonUtils.Typed","type":"bdv.tools.JsonUtils$Typed"}}
{"class":"bdv.tools.JsonUtils$TypedList$JsonAdapter","values":{"jsonType":"TypedList","type":"bdv.tools.JsonUtils$TypedList"}}
{"class":"bdv.tools.links.JsonAdapters$AnchorAdapter","values":{"jsonType":"BdvPropertiesV0.Anchor","type":"bdv.tools.links.BdvPropertiesV0$Anchor"}}
{"class":"bdv.tools.links.JsonAdapters$SourceConverterConfigAdapter","values":{"jsonType":"BdvProperiesV0.SourceConverterConfig","type":"bdv.tools.links.BdvPropertiesV0$SourceConverterConfig"}}
{"class":"bdv.tools.links.JsonAdapters$DisplayModeAdapter","values":{"jsonType":"DisplayMode","type":"bdv.viewer.DisplayMode"}}
{"class":"bdv.tools.links.JsonAdapters$InterpolationAdapter","values":{"jsonType":"Interpolation","type":"bdv.viewer.Interpolation"}}
{"class":"bdv.tools.links.Links$VersionAndProperties$Adapter","values":{"jsonType":"VersionAndProperties","type":"bdv.tools.links.Links$VersionAndProperties"}}
{"class":"bdv.tools.links.resource.SpimDataMinimalFileResource$SpecAdapter","values":{"jsonType":"SpimDataMinimalFileResource.Spec","type":"bdv.tools.links.resource.SpimDataMinimalFileResource$Spec"}}
{"class":"bdv.tools.links.resource.SpimDataMinimalFileResource$ConfigAdapter","values":{"jsonType":"SpimDataMinimalFileResource.Config","type":"bdv.tools.links.resource.SpimDataMinimalFileResource$Config"}}
{"class":"bdv.tools.links.resource.SpimDataSetupSourceResource$JsonAdapter","values":{"jsonType":"SpimDataSetupSourceResource.Spec","type":"bdv.tools.links.resource.SpimDataSetupSourceResource$Spec"}}
{"class":"bdv.tools.links.resource.SpimDataSetupSourceResource$ConfigAdapter","values":{"jsonType":"SpimDataSetupSourceResource.Config","type":"bdv.tools.links.resource.SpimDataSetupSourceResource$Config"}}
{"class":"bdv.tools.links.resource.TransformedSourceResource$SpecAdapter","values":{"jsonType":"TransformedSourceResource.Spec","type":"bdv.tools.links.resource.TransformedSourceResource$Spec"}}
{"class":"bdv.tools.links.resource.TransformedSourceResource$ConfigAdapter","values":{"jsonType":"TransformedSourceResource.Config","type":"bdv.tools.links.resource.TransformedSourceResource$Config"}}
{"class":"bdv.tools.links.resource.UnknownResource$SpecAdapter","values":{"jsonType":"UnknownResource.Spec","type":"bdv.tools.links.resource.UnknownResource$Spec"}}
{"class":"bdv.tools.links.resource.UnknownResource$ConfigAdapter","values":{"jsonType":"UnknownResource.Config","type":"bdv.tools.links.resource.UnknownResource$Config"}}%

If you don't have it, could you try to mvn clean install and see whether it is produced?
Which OS are you on? Maybe the nested JsonUtils.JsonIo annotation interface and resulting filename
bdv.tools.JsonUtils\$JsonIo is a problem?

@tischi
Copy link
Contributor

tischi commented May 2, 2025

Hi @tpietzsch,

I also tried and I am getting some other error:

Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
	at bdv.tools.links.BdvPropertiesV0.getSourceConverterConfigs(BdvPropertiesV0.java:120)
	at bdv.tools.links.BdvPropertiesV0.<init>(BdvPropertiesV0.java:94)
	at bdv.tools.links.Links.copyV0(Links.java:89)
	at bdv.tools.links.Links.copyJson(Links.java:49)
	at bdv.tools.links.LinkActions.copyViewerState(LinkActions.java:84)
	at bdv.tools.links.LinkActions.lambda$install$0(LinkActions.java:132)
	at org.scijava.ui.behaviour.util.RunnableAction.actionPerformed(RunnableAction.java:47)
	at javax.swing.SwingUtilities.notifyAction(SwingUtilities.java:1668)
	at javax.swing.JComponent.processKeyBinding(JComponent.java:2876)
	at javax.swing.JComponent.processKeyBindings(JComponent.java:2937)
	at javax.swing.JComponent.processKeyEvent(JComponent.java:2839)
	at java.awt.Component.processEvent(Component.java:6316)
	at java.awt.Container.processEvent(Container.java:2239)
	at java.awt.Component.dispatchEventImpl(Component.java:4889)
	at java.awt.Container.dispatchEventImpl(Container.java:2297)
	at java.awt.Component.dispatchEvent(Component.java:4711)
	at java.awt.KeyboardFocusManager.redispatchEvent(KeyboardFocusManager.java:1954)
	at java.awt.DefaultKeyboardFocusManager.dispatchKeyEvent(DefaultKeyboardFocusManager.java:835)
	at java.awt.DefaultKeyboardFocusManager.preDispatchKeyEvent(DefaultKeyboardFocusManager.java:1103)
	at java.awt.DefaultKeyboardFocusManager.typeAheadAssertions(DefaultKeyboardFocusManager.java:974)
	at java.awt.DefaultKeyboardFocusManager.dispatchEvent(DefaultKeyboardFocusManager.java:800)
	at java.awt.Component.dispatchEventImpl(Component.java:4760)
	at java.awt.Container.dispatchEventImpl(Container.java:2297)
	at java.awt.Window.dispatchEventImpl(Window.java:2746)
	at java.awt.Component.dispatchEvent(Component.java:4711)
	at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:760)
	at java.awt.EventQueue.access$500(EventQueue.java:97)
	at java.awt.EventQueue$3.run(EventQueue.java:709)
	at java.awt.EventQueue$3.run(EventQueue.java:703)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:74)
	at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:84)
	at java.awt.EventQueue$4.run(EventQueue.java:733)
	at java.awt.EventQueue$4.run(EventQueue.java:731)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:74)
	at java.awt.EventQueue.dispatchEvent(EventQueue.java:730)
	at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:205)
	at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116)
	at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105)
	at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
	at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93)
	at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)

I checked and it is here:

final int color = setup.getColor().get();

within getSourceConverterConfigs.

For the SAC where it crashed it shows:

image

Maybe you could change the code such that it does not try to get the color if it does not support colors?

Note that this SAC is one that MoBIE builds for a label image, where I don't use the normal ColorConverter.

Typed<T> instances of any T with a @JsonIo-annotated adapter are
serialized as json objects with a "type" attribute with the value of the
JsonIo.type() annotation, and an "obj" attribute, which is serialized
using the annotated adapter. For deserialization, the correct adapter is
looked up via the "type" attribute.
@mzouink
Copy link

mzouink commented Sep 9, 2025

Does this support N5 viewer ?

I tested with this data:
https://s3.janelia.org/opendata/flyefish/NP08_R2_5_5_SS89446_Lk_Eh_546_NPF_Trissin_647_1x_ROL.zarr/0
No error but it didn't work.
this is the json i got by ctrl+C

{"bdv":{"version":0,"properties":{"transform":[0.8382628726031017,0.0,0.014631932868201325,60.190337241966006,0.0,0.8383905635467658,0.0,112.28515624999997,-0.014631932868201325,0.0,0.8382628726031017,-116.85546297429316],"timepoint":0,"displaymode":"fused-source","interpolation":"nearest","sourceSpecs":[{"type":"UnknownResource.Spec","data":{}},{"type":"UnknownResource.Spec","data":{}},{"type":"UnknownResource.Spec","data":{}},{"type":"UnknownResource.Spec","data":{}}],"sourceConfigs":[{"type":"UnknownResource.Config","data":{}},{"type":"UnknownResource.Config","data":{}},{"type":"UnknownResource.Config","data":{}},{"type":"UnknownResource.Config","data":{}}],"converterConfigs":[{"color":"ffff6699","min":0.0,"max":1000.0,"minBound":0.0,"maxBound":65535.0},{"color":"ffffff00","min":0.0,"max":1000.0,"minBound":0.0,"maxBound":65535.0},{"color":"ff66ff99","min":0.0,"max":1000.0,"minBound":0.0,"maxBound":65535.0},{"color":"ff0000ff","min":0.0,"max":1000.0,"minBound":0.0,"maxBound":65535.0}],"currentSourceIndex":0,"activeSourceIndices":[0,1,2,3],"panelsize":[500,600],"mousepos":[404,244],"anchor":"center"}}}
@tpietzsch @bogovicj @krokicki

@tpietzsch
Copy link
Member Author

Does this support N5 viewer ?

Not yet.

Here is an explanation that I send to @bogovicj a while ago on zulip. I thought I also put that somewhere public, but I couldn't find it anymore...:

(...) how the specification of sources in the JSON works, and how that could be integrated into n5-viewer.
Here is a rough description:

Here is the part of the JSON that describes the first BDV Source:

{
  "type": "TransformedSourceResource.Spec",
  "data": {
    "delegate": {
      "type": "SpimDataSetupSourceResource.Spec",
      "data": {
        "spimData": {
          "type": "SpimDataMinimalFileResource.Spec",
          "data": {
            "uri": "file:/Users/pietzsch/workspace/data/111010_weber_resave.xml"
          }
        },
        "setupId": 0,
        "name": "a 0"
      }
    }
  }
}

De-serialized, this corresponds to nested ResourceSpec objects.
The outer-most is a TransformedSourceResource.Spec (

public interface TransformedSourceResource
{
class Spec implements ResourceSpec< SourceAndConverter< ? > >
{
private final ResourceSpec< SourceAndConverter< ? > > delegateSpec;
public Spec( final ResourceSpec< SourceAndConverter< ? > > delegateSpec )
{
this.delegateSpec = delegateSpec;
}
@Override
public SourceAndConverter< ? > create( final ResourceManager resources ) throws ResourceCreationException
{
final SourceAndConverter< ? > delegate = resources.getOrCreateResource( delegateSpec );
return BigDataViewer.wrapWithTransformedSource( delegate, resources );
}
@Override
public ResourceConfig getConfig( final ResourceManager resources )
{
final ResourceConfig delegateConfig = delegateSpec.getConfig( resources );
final SourceAndConverter< ? > soc = resources.getResource( this );
final TransformedSource< ? > ts = ( TransformedSource< ? > ) soc.getSpimSource();
final AffineTransform3D transform = new AffineTransform3D();
ts.getFixedTransform( transform );
return new Config( delegateConfig, transform );
}
@Override
public String toString()
{
return "TransformedSourceResource.Spec{" +
"delegateSpec=" + delegateSpec +
'}';
}
@Override
public boolean equals( final Object o )
{
if ( !( o instanceof Spec ) )
return false;
final Spec that = ( Spec ) o;
return Objects.equals( delegateSpec, that.delegateSpec );
}
@Override
public int hashCode()
{
return Objects.hashCode( delegateSpec );
}
}
).
That wraps a SpimDataSetupSourceResource.Spec, and that wraps a SpimDataMinimalFileResource.Spec.

ResourceSpec have a create method (

/**
* Creates the specified resource. Resources for nested specs are
* retrieved from a {@code Resources} map, or created and put into the map.
*
* @throws ResourceCreationException
* if the resource could not be created
*/
T create( ResourceManager resources ) throws ResourceCreationException;
)
to create the described resource.
They can ask the ResourceManager to provide resources corresponding to nested specs.
(The create method should also register the created resource with the ResourceManager).

To make the Links mechanism aware of how to serialize ResourceSpec subclasses, a JsonAdapter has to be annotated with @JsonIo, for example:

@JsonUtils.JsonIo( jsonType = "TransformedSourceResource.Spec", type = TransformedSourceResource.Spec.class )
class SpecAdapter implements JsonDeserializer< Spec >, JsonSerializer< Spec >
{
@Override
public Spec deserialize(
final JsonElement json,
final Type typeOfT,
final JsonDeserializationContext context )
{
final JsonObject obj = json.getAsJsonObject();
final Typed< ResourceSpec< SourceAndConverter< ? > > > delegateSpec = context.deserialize( obj.get( "delegate" ), Typed.class );
return new Spec( delegateSpec.get() );
}
@Override
public JsonElement serialize(
final Spec src,
final Type typeOfSrc,
final JsonSerializationContext context )
{
final JsonObject obj = new JsonObject();
obj.add( "delegate", context.serialize( typed( src.delegateSpec ) ) );
return obj;
}
}

On top of that, there is also ResourceConfig that has "dynamic" configuration of a Resource which are not part of the ResourceSpec.
(This would be the manual transform for a TransformedSource for example. If you would want to add ResourceSpec for BigWarp sources, the warp parameters would probably go to the ResourceConfig.)
For the N5 sources, there is probably no dynamic configuration, so we can ignore that part for now.

@tpietzsch
Copy link
Member Author

What is still missing from that explanation is some concrete advice of how to modify n5-viewer so that the resources are picked up:

Probably you will need a type of resource for referencing a N5Reader (just containing URI probably):
Something like

And you will need something to tie SourceAndConverter that you add to BDV to that N5Reader.
Something like

Then when you open something in n5-viewer (for example, here: https://github.com/saalfeldlab/n5-viewer/blob/0a2aace2654aa05a972f1ba3c389744472184348/src/main/java/org/janelia/saalfeldlab/n5/bdv/N5Viewer.java#L307-L366) you need to register it with the ResourceManager so that it can be found later when copying the state.
For BDV this happens here:

final ResourceManager resources = options.values.getResourceManager();
resources.put( spimData, new SpimDataMinimalFileResource.Spec( xmlFilename ) );

and
final SourceAndConverter< T > soc = new SourceAndConverter<>( s, createConverterToARGB( type ), vsoc );
if (resources != null)
{
final SpimDataSetupSourceResource.Spec spec = new SpimDataSetupSourceResource.Spec(
resources.getResourceSpec( spimData ),
setupId, name );
resources.put( soc, spec );
resources.keepAlive( soc, spimData );
}

for example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants