From 9d03f984be27d981b915ea6446235c33928fb6d7 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Thu, 24 Sep 2026 14:46:47 +0200 Subject: [PATCH] OME 0.6: write coordinate system references as {name, path} objects OME-NGFF 0.6 requires the input/output of coordinate transformations to be JSON objects {"name": ..., "path": ...}. The v0.6 model stored them as strings, squashing objects into "path#name" on read and writing strings back, so reading a conformant file and saving it corrupted it. Scale levels created via createScaleLevel also lacked the mandatory dataset input/output. - Add CoordinateSystemRef (name/path, NON_NULL) and use it for input/output on all v0.6 transformations; factory methods take CoordinateSystemRef and gain ref-less scale/translation and a sequence factory. - Legacy pre-release string refs are still read: a bare string is a name, except for the input of a dataset transformation, which is the dataset path; "#" strings from earlier zarr-java are split. Writing always emits the object form. - createScaleLevel writes input {"path": } and output {"name": } (from existing datasets, else first coordinate system; ZarrException if neither). Multiple transformations are wrapped in a sequence whose nested transformations carry no refs; getMultiscaleNode flattens such sequences back to [scale, translation]. - Scene graph resolution works on CoordinateSystemRef. Co-Authored-By: Claude Opus 5.5 --- USERGUIDE-OME-ZARR.md | 19 ++ .../ome/v0_6/MultiscaleImage.java | 75 ++++- .../ome/v0_6/SceneReferenceResolver.java | 31 +- .../ome/v0_6/metadata/Dataset.java | 37 +++ .../AffineCoordinateTransformation.java | 5 +- .../BaseCoordinateTransformation.java | 12 +- .../BijectionCoordinateTransformation.java | 5 +- .../ByDimensionCoordinateTransformation.java | 5 +- .../transform/CoordinateSystemRef.java | 153 ++++++++++ .../transform/CoordinateSystemRefSerde.java | 41 --- .../transform/CoordinateTransformation.java | 36 ++- .../CoordinatesCoordinateTransformation.java | 5 +- ...DisplacementsCoordinateTransformation.java | 5 +- .../GenericCoordinateTransformation.java | 5 +- .../IdentityCoordinateTransformation.java | 13 +- .../MapAxisCoordinateTransformation.java | 5 +- .../RotationCoordinateTransformation.java | 5 +- .../ScaleCoordinateTransformation.java | 13 +- .../SequenceCoordinateTransformation.java | 5 +- .../TranslationCoordinateTransformation.java | 13 +- .../ome/OmeObjectMappersTest.java | 11 +- .../experimental/ome/OmeZarrSceneV06Test.java | 23 +- .../OmeZarrV06CoordinateSystemRefTest.java | 288 ++++++++++++++++++ .../experimental/ome/OmeZarrV06Test.java | 9 +- 24 files changed, 680 insertions(+), 139 deletions(-) create mode 100644 src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateSystemRef.java delete mode 100644 src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateSystemRefSerde.java create mode 100644 src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV06CoordinateSystemRefTest.java diff --git a/USERGUIDE-OME-ZARR.md b/USERGUIDE-OME-ZARR.md index 313945af..7259f3dd 100644 --- a/USERGUIDE-OME-ZARR.md +++ b/USERGUIDE-OME-ZARR.md @@ -74,6 +74,25 @@ Scene roots (groups with `ome.scene`) are supported via `dev.zarr.zarrjava.exper Notes: - Parsing is permissive and explicit (no strict full-spec validation). - Scene-level references (`input`/`output`) are resolved against scene-root coordinate systems and child image coordinate systems for graph inspection. + +### v0.6 coordinate system references + +In v0.6, the `input`/`output` of a coordinate transformation is a `CoordinateSystemRef` (`{"name": ..., "path": ...}`, both optional): + +```java +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateSystemRef; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation; + +// multiscales > datasets: input references the dataset path, output the intrinsic coordinate system +CoordinateTransformation.scale(Arrays.asList(0.5, 0.5), + CoordinateSystemRef.ofPath("s0"), CoordinateSystemRef.ofName("physical")); +// scene > coordinateTransformations: name + path of the image group +CoordinateTransformation.identity( + CoordinateSystemRef.of("physical", "imageA"), CoordinateSystemRef.of("physical", "imageB")); +``` + +`createScaleLevel(...)` on a v0.6 image fills these in automatically (input `{"path": }`, output `{"name": }`, where the intrinsic name is taken from existing datasets or the first entry of `coordinateSystems`); passing several transformations wraps them in a `sequence`. +Pre-release string references (e.g. `"input": "s0"`, `"output": "physical"`) are still read: a bare string is a coordinate system name, except the input of a dataset transformation, which is the dataset path. They are always written back in object form. - Path-based transform assets can be normalized with `Scene.normalizeCoordinateTransformPath(...)` and grouped under `coordinateTransformations/` via `createCoordinateTransformationsGroup()`. ## Read example diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/MultiscaleImage.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/MultiscaleImage.java index 327f7187..80544217 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/MultiscaleImage.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/MultiscaleImage.java @@ -122,9 +122,21 @@ public void createScaleLevel( if (!(arrayMetadata instanceof dev.zarr.zarrjava.v3.ArrayMetadata)) { throw new ZarrException("Expected v3.ArrayMetadata for OME-Zarr v0.6, got " + arrayMetadata.getClass()); } + String intrinsicName = intrinsicCoordinateSystemName(omeMetadata.multiscales.get(0)); Array.create(storeHandle.resolve(path), (dev.zarr.zarrjava.v3.ArrayMetadata) arrayMetadata); // Convert ome.metadata.CoordinateTransformation to v0.6 CoordinateTransformation + // Per the 0.6 spec, a dataset carries a single transformation (scale, identity, or a sequence of + // scale + translation) whose input is {"path": } and whose output is + // {"name": }. Multiple transformations are wrapped in a sequence; + // nested transformations carry no input/output. + boolean wrapInSequence = coordinateTransformations.size() > 1; + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateSystemRef datasetInput = + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateSystemRef.ofPath(path); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateSystemRef datasetOutput = + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateSystemRef.ofName(intrinsicName); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateSystemRef input = wrapInSequence ? null : datasetInput; + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateSystemRef output = wrapInSequence ? null : datasetOutput; List v06Transforms = new ArrayList<>(); for (CoordinateTransformation ct : coordinateTransformations) { String type = ct.type; @@ -156,63 +168,68 @@ public void createScaleLevel( if ("sequence".equals(type)) { v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.SequenceCoordinateTransformation( - null, null, null, castV06TransformList(raw.get("transformations")))); + input, output, null, castV06TransformList(raw.get("transformations")))); continue; } if ("mapAxis".equals(type)) { v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.MapAxisCoordinateTransformation( - null, null, null, castIntList(raw.get("mapAxis")), castV06Transform(raw.get("transformation")))); + input, output, null, castIntList(raw.get("mapAxis")), castV06Transform(raw.get("transformation")))); continue; } if ("affine".equals(type)) { v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation( - null, null, null, castMatrix(raw.get("affine")), rawPath)); + input, output, null, castMatrix(raw.get("affine")), rawPath)); continue; } if ("rotation".equals(type)) { v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.RotationCoordinateTransformation( - null, null, null, castMatrix(raw.get("rotation")), rawPath)); + input, output, null, castMatrix(raw.get("rotation")), rawPath)); continue; } if ("displacements".equals(type)) { v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.DisplacementsCoordinateTransformation( - null, null, null, rawPath)); + input, output, null, rawPath)); continue; } if ("coordinates".equals(type)) { v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinatesCoordinateTransformation( - null, null, null, rawPath)); + input, output, null, rawPath)); continue; } if ("bijection".equals(type)) { v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.BijectionCoordinateTransformation( - null, null, null, castV06Transform(raw.get("forward")), castV06Transform(raw.get("inverse")))); + input, output, null, castV06Transform(raw.get("forward")), castV06Transform(raw.get("inverse")))); continue; } if ("byDimension".equals(type)) { v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ByDimensionCoordinateTransformation( - null, null, null, castByDimensionTransformList(raw.get("transformations")))); + input, output, null, castByDimensionTransformList(raw.get("transformations")))); continue; } } if ("scale".equals(type)) { v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ScaleCoordinateTransformation( - null, null, null, scale, rawPath)); + input, output, null, scale, rawPath)); } else if ("translation".equals(type)) { v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.TranslationCoordinateTransformation( - null, null, null, translation, rawPath)); + input, output, null, translation, rawPath)); } else if ("identity".equals(type)) { v06Transforms.add(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.IdentityCoordinateTransformation( - null, null, null, rawPath)); + input, output, null, rawPath)); } else { dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.GenericCoordinateTransformation generic = - new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.GenericCoordinateTransformation(type, null, null, null); + new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.GenericCoordinateTransformation(type, input, output, null); if (scale != null) generic.raw.put("scale", scale); if (translation != null) generic.raw.put("translation", translation); if (rawPath != null) generic.raw.put("path", rawPath); v06Transforms.add(generic); } } + if (wrapInSequence) { + v06Transforms = Collections.singletonList( + new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.SequenceCoordinateTransformation( + datasetInput, datasetOutput, null, v06Transforms)); + } MultiscalesEntry current = omeMetadata.multiscales.get(0); MultiscalesEntry updated = current.withDataset(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.Dataset(path, v06Transforms)); @@ -229,6 +246,32 @@ public void createScaleLevel( setAttributes(omeAttributes(omeMetadata)); } + /** + * Name of the "intrinsic" coordinate system of a multiscale: the output of the existing dataset + * transformations if any, otherwise the first entry of {@code coordinateSystems}. + */ + private static String intrinsicCoordinateSystemName(MultiscalesEntry entry) throws ZarrException { + if (entry.datasets != null) { + for (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.Dataset ds : entry.datasets) { + if (ds.coordinateTransformations == null) { + continue; + } + for (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation ct : ds.coordinateTransformations) { + if (ct != null && ct.getOutput() != null && ct.getOutput().name != null) { + return ct.getOutput().name; + } + } + } + } + if (entry.coordinateSystems != null && !entry.coordinateSystems.isEmpty() + && entry.coordinateSystems.get(0).name != null) { + return entry.coordinateSystems.get(0).name; + } + throw new ZarrException( + "Cannot determine the intrinsic coordinate system for a new OME-Zarr v0.6 scale level: " + + "the multiscales entry has neither dataset transformations with an output nor coordinateSystems"); + } + @Override public dev.zarr.zarrjava.experimental.ome.metadata.MultiscalesEntry getMultiscaleNode(int i) throws ZarrException { MultiscalesEntry entry = getMultiscalesEntry(i); @@ -236,6 +279,14 @@ public dev.zarr.zarrjava.experimental.ome.metadata.MultiscalesEntry getMultiscal for (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.Dataset ds : entry.datasets) { List mapped = new ArrayList<>(); for (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation ct : ds.coordinateTransformations) { + if (ct instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.SequenceCoordinateTransformation + && ((dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.SequenceCoordinateTransformation) ct).transformations != null) { + // 0.6 datasets wrap scale + translation in a sequence; the version-independent view + // uses the flat [scale, translation] list of earlier versions. + mapped.addAll(mapTransformList( + ((dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.SequenceCoordinateTransformation) ct).transformations)); + continue; + } mapped.add(mapTransform(ct)); } mappedDatasets.add(new dev.zarr.zarrjava.experimental.ome.metadata.Dataset(ds.path, mapped)); diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/SceneReferenceResolver.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/SceneReferenceResolver.java index 88ea7497..bec94fc8 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/SceneReferenceResolver.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/SceneReferenceResolver.java @@ -3,6 +3,7 @@ import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.CoordinateSystem; import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry; import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.SceneMetadata; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateSystemRef; import javax.annotation.Nullable; import java.util.ArrayList; @@ -40,15 +41,16 @@ final class SceneReferenceResolver { } } + /** + * Resolves a scene-level reference. A {@code null}/empty path refers to the scene's own + * {@code coordinateSystems}; otherwise the path names an image subgroup of the scene. + */ @Nullable - ResolvedCoordinateSystem resolve(@Nullable String reference) { - if (reference == null || reference.isEmpty()) { + ResolvedCoordinateSystem resolve(@Nullable CoordinateSystemRef reference) { + if (reference == null || reference.name == null) { return null; } - if (!reference.contains("#")) { - return null; - } - return index.get(reference); + return index.get(nodeId(reference.path, reference.name)); } List list() { @@ -59,11 +61,24 @@ private void addCoordinateSystem(String groupPath, CoordinateSystem coordinateSy if (coordinateSystem == null || coordinateSystem.name == null) { return; } - String canonicalPath = groupPath == null || groupPath.isEmpty() ? "." : groupPath; - String id = canonicalPath + "#" + coordinateSystem.name; + String canonicalPath = canonicalGroupPath(groupPath); + String id = nodeId(canonicalPath, coordinateSystem.name); index.put(id, new ResolvedCoordinateSystem(id, canonicalPath, coordinateSystem)); } + private static String canonicalGroupPath(@Nullable String groupPath) { + String normalized = Scene.normalizeCoordinateTransformPath(groupPath); + while (normalized != null && normalized.endsWith("/")) { + normalized = normalized.substring(0, normalized.length() - 1); + } + return normalized == null || normalized.isEmpty() || ".".equals(normalized) ? "." : normalized; + } + + /** Graph node id: {@code "#"}, with {@code "."} as the scene root's group path. */ + private static String nodeId(@Nullable String groupPath, String name) { + return canonicalGroupPath(groupPath) + "#" + name; + } + static final class ResolvedCoordinateSystem { final String id; final String groupPath; diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/Dataset.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/Dataset.java index f08e39db..ebfb2ea2 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/Dataset.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/Dataset.java @@ -3,8 +3,16 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.node.ObjectNode; import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation; +import java.io.IOException; import java.util.List; @JsonInclude(JsonInclude.Include.NON_NULL) @@ -18,9 +26,38 @@ public final class Dataset { public Dataset( @JsonProperty(value = "path", required = true) String path, @JsonProperty(value = "coordinateTransformations", required = true) + @JsonDeserialize(using = TransformationsDeserializer.class) List coordinateTransformations ) { this.path = path; this.coordinateTransformations = coordinateTransformations; } + + /** + * Reads dataset-level transformations, mapping the legacy (pre-release 0.6) bare-string + * {@code "input": ""} to {@code {"path": ""}}. Everywhere else a bare + * string reference is read as a coordinate system name (see + * {@link dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateSystemRef}). + */ + static final class TransformationsDeserializer extends JsonDeserializer> { + @Override + public List deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode tree = p.readValueAsTree(); + if (tree != null && tree.isArray()) { + for (JsonNode item : tree) { + if (item instanceof ObjectNode) { + ObjectNode obj = (ObjectNode) item; + JsonNode input = obj.get("input"); + if (input != null && input.isTextual()) { + ObjectNode ref = obj.objectNode(); + ref.put("path", input.asText()); + obj.set("input", ref); + } + } + } + } + JavaType type = ctxt.getTypeFactory().constructCollectionType(List.class, CoordinateTransformation.class); + return ctxt.readTreeAsValue(tree, type); + } + } } diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/AffineCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/AffineCoordinateTransformation.java index ae5a5834..34d44162 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/AffineCoordinateTransformation.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/AffineCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; import java.util.List; @@ -13,8 +12,8 @@ public final class AffineCoordinateTransformation extends BaseCoordinateTransfor @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public AffineCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("affine") List> affine, @Nullable @JsonProperty("path") String path diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/BaseCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/BaseCoordinateTransformation.java index 87b8e205..c6f2f823 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/BaseCoordinateTransformation.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/BaseCoordinateTransformation.java @@ -4,14 +4,14 @@ abstract class BaseCoordinateTransformation implements CoordinateTransformation { public final String type; - @Nullable public final String input; - @Nullable public final String output; + @Nullable public final CoordinateSystemRef input; + @Nullable public final CoordinateSystemRef output; @Nullable public final String name; protected BaseCoordinateTransformation( String type, - @Nullable String input, - @Nullable String output, + @Nullable CoordinateSystemRef input, + @Nullable CoordinateSystemRef output, @Nullable String name ) { this.type = type; @@ -26,12 +26,12 @@ public String getType() { } @Override - public String getInput() { + public CoordinateSystemRef getInput() { return input; } @Override - public String getOutput() { + public CoordinateSystemRef getOutput() { return output; } diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/BijectionCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/BijectionCoordinateTransformation.java index 4e36ba39..12a597be 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/BijectionCoordinateTransformation.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/BijectionCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; @@ -12,8 +11,8 @@ public final class BijectionCoordinateTransformation extends BaseCoordinateTrans @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public BijectionCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("forward") CoordinateTransformation forward, @Nullable @JsonProperty("inverse") CoordinateTransformation inverse diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ByDimensionCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ByDimensionCoordinateTransformation.java index 03e44183..79d5892a 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ByDimensionCoordinateTransformation.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ByDimensionCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; import java.util.List; @@ -12,8 +11,8 @@ public final class ByDimensionCoordinateTransformation extends BaseCoordinateTra @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public ByDimensionCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("transformations") List transformations ) { diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateSystemRef.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateSystemRef.java new file mode 100644 index 00000000..f425f554 --- /dev/null +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateSystemRef.java @@ -0,0 +1,153 @@ +package dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.Objects; + +/** + * Reference to a coordinate system, used as the {@code input} / {@code output} of an OME-Zarr v0.6 + * coordinate transformation. + * + *

Serialized as a JSON object {@code {"name": ..., "path": ...}}; both fields are optional and + * omitted when {@code null}. Which fields are required depends on the context + * (see the OME-Zarr 0.6 "coordinateTransformations" constraints): + *

    + *
  • {@code multiscales > datasets}: input {@code {"path": ""}}, + * output {@code {"name": ""}}
  • + *
  • {@code multiscales > coordinateTransformations}: {@code {"name": ...}}, optionally with a + * {@code path} to a child labels group
  • + *
  • {@code scene > coordinateTransformations}: {@code {"name": ..., "path": ""}}
  • + *
+ * A {@code null} or omitted {@code path} refers to a coordinate system in the same {@code zarr.json}. + * + *

Legacy string form. Pre-release 0.6 drafts stored references as plain strings. For + * backwards compatibility a bare JSON string is still accepted on read and interpreted as a + * coordinate system {@code name}, except for the {@code input} of a top-level + * {@code multiscales > datasets} transformation, where it is interpreted as the dataset + * {@code path} (see {@link dev.zarr.zarrjava.experimental.ome.v0_6.metadata.Dataset}). + * Strings of the form {@code "#"} (as written by earlier zarr-java versions) are split + * into path and name, with {@code "."} meaning "no path". Writing always emits the object form. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonDeserialize(using = CoordinateSystemRef.Deserializer.class) +public final class CoordinateSystemRef { + + @Nullable + @JsonProperty("name") + public final String name; + @Nullable + @JsonProperty("path") + public final String path; + + public CoordinateSystemRef(@Nullable String name, @Nullable String path) { + this.name = name; + this.path = path; + } + + /** Reference to a named coordinate system in the same {@code zarr.json}. */ + public static CoordinateSystemRef ofName(String name) { + return new CoordinateSystemRef(name, null); + } + + /** Reference by path only (e.g. the input of a dataset transformation: the array at {@code path}). */ + public static CoordinateSystemRef ofPath(String path) { + return new CoordinateSystemRef(null, path); + } + + /** Reference to the coordinate system {@code name} defined in the Zarr group at {@code path}. */ + public static CoordinateSystemRef of(@Nullable String name, @Nullable String path) { + return new CoordinateSystemRef(name, path); + } + + /** + * Parses the legacy (pre-release 0.6) string form of a reference: a bare string is a name; + * {@code "#"} is split into path and name ({@code "."} or empty path means no path). + */ + public static CoordinateSystemRef fromLegacyString(String value) { + int hash = value.indexOf('#'); + if (hash < 0) { + return ofName(value); + } + String p = value.substring(0, hash); + String n = value.substring(hash + 1); + if (p.isEmpty() || ".".equals(p)) { + p = null; + } + return new CoordinateSystemRef(n.isEmpty() ? null : n, p); + } + + @Nullable + public String getName() { + return name; + } + + @Nullable + public String getPath() { + return path; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CoordinateSystemRef)) { + return false; + } + CoordinateSystemRef that = (CoordinateSystemRef) o; + return Objects.equals(name, that.name) && Objects.equals(path, that.path); + } + + @Override + public int hashCode() { + return Objects.hash(name, path); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("{"); + if (name != null) { + sb.append("name=").append(name); + } + if (path != null) { + if (name != null) { + sb.append(", "); + } + sb.append("path=").append(path); + } + return sb.append('}').toString(); + } + + static final class Deserializer extends StdDeserializer { + Deserializer() { + super(CoordinateSystemRef.class); + } + + @Override + public CoordinateSystemRef deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonToken token = p.currentToken(); + if (token == JsonToken.VALUE_STRING) { + return fromLegacyString(p.getValueAsString()); + } + if (token == JsonToken.START_OBJECT) { + JsonNode node = p.readValueAsTree(); + return new CoordinateSystemRef(textOrNull(node.get("name")), textOrNull(node.get("path"))); + } + return (CoordinateSystemRef) ctxt.handleUnexpectedToken(CoordinateSystemRef.class, p); + } + + @Nullable + private static String textOrNull(@Nullable JsonNode node) { + return node != null && node.isTextual() ? node.asText() : null; + } + } +} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateSystemRefSerde.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateSystemRefSerde.java deleted file mode 100644 index f0ac2dcc..00000000 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateSystemRefSerde.java +++ /dev/null @@ -1,41 +0,0 @@ -package dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; - -import java.io.IOException; - -final class CoordinateSystemRefSerde { - private CoordinateSystemRefSerde() { - } - - static final class Deserializer extends StdDeserializer { - Deserializer() { - super(String.class); - } - - @Override - public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - JsonToken token = p.currentToken(); - if (token == JsonToken.VALUE_STRING) { - return p.getValueAsString(); - } - if (token == JsonToken.START_OBJECT) { - JsonNode node = p.readValueAsTree(); - JsonNode pathNode = node.get("path"); - JsonNode nameNode = node.get("name"); - String path = pathNode != null && pathNode.isTextual() ? pathNode.asText() : null; - String name = nameNode != null && nameNode.isTextual() ? nameNode.asText() : null; - if (name == null) { - return path; - } - String canonicalPath = (path == null || path.isEmpty()) ? "." : path; - return canonicalPath + "#" + name; - } - return (String) ctxt.handleUnexpectedToken(String.class, p); - } - } -} diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateTransformation.java index 4463d185..8b2d1815 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateTransformation.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinateTransformation.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import javax.annotation.Nullable; import java.util.List; @JsonTypeInfo( @@ -25,19 +27,43 @@ }) public interface CoordinateTransformation { String getType(); - String getInput(); - String getOutput(); + @Nullable CoordinateSystemRef getInput(); + @Nullable CoordinateSystemRef getOutput(); String getName(); - static CoordinateTransformation scale(List scale, String input, String output) { + /** + * Scale transformation from {@code input} to {@code output}. For a {@code multiscales > datasets} + * entry use {@code CoordinateSystemRef.ofPath(datasetPath)} as input and + * {@code CoordinateSystemRef.ofName(intrinsicName)} as output. + */ + static CoordinateTransformation scale( + List scale, @Nullable CoordinateSystemRef input, @Nullable CoordinateSystemRef output) { return new ScaleCoordinateTransformation(input, output, null, scale, null); } - static CoordinateTransformation translation(List translation, String input, String output) { + /** Scale transformation without input/output, e.g. for use inside a {@code sequence}. */ + static CoordinateTransformation scale(List scale) { + return scale(scale, null, null); + } + + static CoordinateTransformation translation( + List translation, @Nullable CoordinateSystemRef input, @Nullable CoordinateSystemRef output) { return new TranslationCoordinateTransformation(input, output, null, translation, null); } - static CoordinateTransformation identity(String input, String output) { + /** Translation transformation without input/output, e.g. for use inside a {@code sequence}. */ + static CoordinateTransformation translation(List translation) { + return translation(translation, null, null); + } + + static CoordinateTransformation identity(@Nullable CoordinateSystemRef input, @Nullable CoordinateSystemRef output) { return new IdentityCoordinateTransformation(input, output, null, null); } + + static CoordinateTransformation sequence( + List transformations, + @Nullable CoordinateSystemRef input, + @Nullable CoordinateSystemRef output) { + return new SequenceCoordinateTransformation(input, output, null, transformations); + } } diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinatesCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinatesCoordinateTransformation.java index 00e1e254..3498ae7e 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinatesCoordinateTransformation.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/CoordinatesCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; @@ -11,8 +10,8 @@ public final class CoordinatesCoordinateTransformation extends BaseCoordinateTra @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public CoordinatesCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("path") String path ) { diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/DisplacementsCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/DisplacementsCoordinateTransformation.java index 953ea841..052ed3e4 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/DisplacementsCoordinateTransformation.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/DisplacementsCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; @@ -11,8 +10,8 @@ public final class DisplacementsCoordinateTransformation extends BaseCoordinateT @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public DisplacementsCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("path") String path ) { diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/GenericCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/GenericCoordinateTransformation.java index fafa177c..f85286e2 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/GenericCoordinateTransformation.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/GenericCoordinateTransformation.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; import java.util.LinkedHashMap; @@ -21,8 +20,8 @@ public final class GenericCoordinateTransformation extends BaseCoordinateTransfo @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public GenericCoordinateTransformation( @JsonProperty(value = "type", required = true) String type, - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name ) { super(type, input, output, name); diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/IdentityCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/IdentityCoordinateTransformation.java index 8511d34c..2eb71879 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/IdentityCoordinateTransformation.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/IdentityCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; @@ -10,14 +9,14 @@ public final class IdentityCoordinateTransformation extends dev.zarr.zarrjava.experimental.ome.metadata.transform.IdentityCoordinateTransformation implements CoordinateTransformation { - @Nullable public final String input; - @Nullable public final String output; + @Nullable public final CoordinateSystemRef input; + @Nullable public final CoordinateSystemRef output; @Nullable public final String name; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public IdentityCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("path") String path ) { @@ -33,12 +32,12 @@ public String getType() { } @Override - public String getInput() { + public CoordinateSystemRef getInput() { return input; } @Override - public String getOutput() { + public CoordinateSystemRef getOutput() { return output; } diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/MapAxisCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/MapAxisCoordinateTransformation.java index ad4480d0..3209d0c7 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/MapAxisCoordinateTransformation.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/MapAxisCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; import java.util.List; @@ -13,8 +12,8 @@ public final class MapAxisCoordinateTransformation extends BaseCoordinateTransfo @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public MapAxisCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("mapAxis") List mapAxis, @Nullable @JsonProperty("transformation") CoordinateTransformation transformation diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/RotationCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/RotationCoordinateTransformation.java index 6e0a0c2b..8f0853f7 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/RotationCoordinateTransformation.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/RotationCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; import java.util.List; @@ -13,8 +12,8 @@ public final class RotationCoordinateTransformation extends BaseCoordinateTransf @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public RotationCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("rotation") List> rotation, @Nullable @JsonProperty("path") String path diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ScaleCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ScaleCoordinateTransformation.java index 9fe75880..c9fc5b7f 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ScaleCoordinateTransformation.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/ScaleCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; import java.util.List; @@ -11,14 +10,14 @@ public final class ScaleCoordinateTransformation extends dev.zarr.zarrjava.experimental.ome.metadata.transform.ScaleCoordinateTransformation implements CoordinateTransformation { - @Nullable public final String input; - @Nullable public final String output; + @Nullable public final CoordinateSystemRef input; + @Nullable public final CoordinateSystemRef output; @Nullable public final String name; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public ScaleCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("scale") List scale, @Nullable @JsonProperty("path") String path @@ -35,12 +34,12 @@ public String getType() { } @Override - public String getInput() { + public CoordinateSystemRef getInput() { return input; } @Override - public String getOutput() { + public CoordinateSystemRef getOutput() { return output; } diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/SequenceCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/SequenceCoordinateTransformation.java index 660fb180..bc15f84c 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/SequenceCoordinateTransformation.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/SequenceCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; import java.util.List; @@ -12,8 +11,8 @@ public final class SequenceCoordinateTransformation extends BaseCoordinateTransf @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public SequenceCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("transformations") List transformations ) { diff --git a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/TranslationCoordinateTransformation.java b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/TranslationCoordinateTransformation.java index ff307540..8eb4cb3a 100644 --- a/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/TranslationCoordinateTransformation.java +++ b/src/main/java/dev/zarr/zarrjava/experimental/ome/v0_6/metadata/transform/TranslationCoordinateTransformation.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import javax.annotation.Nullable; import java.util.List; @@ -11,14 +10,14 @@ public final class TranslationCoordinateTransformation extends dev.zarr.zarrjava.experimental.ome.metadata.transform.TranslationCoordinateTransformation implements CoordinateTransformation { - @Nullable public final String input; - @Nullable public final String output; + @Nullable public final CoordinateSystemRef input; + @Nullable public final CoordinateSystemRef output; @Nullable public final String name; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public TranslationCoordinateTransformation( - @Nullable @JsonProperty("input") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String input, - @Nullable @JsonProperty("output") @JsonDeserialize(using = CoordinateSystemRefSerde.Deserializer.class) String output, + @Nullable @JsonProperty("input") CoordinateSystemRef input, + @Nullable @JsonProperty("output") CoordinateSystemRef output, @Nullable @JsonProperty("name") String name, @Nullable @JsonProperty("translation") List translation, @Nullable @JsonProperty("path") String path @@ -35,12 +34,12 @@ public String getType() { } @Override - public String getInput() { + public CoordinateSystemRef getInput() { return input; } @Override - public String getOutput() { + public CoordinateSystemRef getOutput() { return output; } diff --git a/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeObjectMappersTest.java b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeObjectMappersTest.java index 23f35f9f..a530611e 100644 --- a/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeObjectMappersTest.java +++ b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeObjectMappersTest.java @@ -197,8 +197,9 @@ void v2MapperParsesUnknownV06TransformAsGenericAndPreservesRawFields() { (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.GenericCoordinateTransformation) entry.datasets.get(0).coordinateTransformations.get(0); assertEquals("customNonLinear", generic.type); - assertEquals("s0", generic.input); - assertEquals("physical", generic.output); + // legacy bare-string refs: dataset input -> path, output -> name + assertEquals(dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateSystemRef.ofPath("s0"), generic.input); + assertEquals(dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateSystemRef.ofName("physical"), generic.output); assertEquals("custom-stage", generic.name); assertEquals(Arrays.asList(1, 4, 9), generic.raw.get("lut")); assertEquals(extension, generic.raw.get("extension")); @@ -336,8 +337,10 @@ void v3MapperParsesV06SceneMetadataRefsAndNestedTransforms() { dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation parsedTranslation = seq.transformations.get(0); assertEquals("translation", parsedTranslation.getType()); - assertEquals("imgA#physical", parsedTranslation.getInput()); - assertEquals(".#world", parsedTranslation.getOutput()); + assertEquals(dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateSystemRef.of("physical", "imgA"), + parsedTranslation.getInput()); + assertEquals(dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateSystemRef.ofName("world"), + parsedTranslation.getOutput()); dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation parsedByDim = seq.transformations.get(1); diff --git a/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrSceneV06Test.java b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrSceneV06Test.java index 5a28c5c1..dcdc7478 100644 --- a/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrSceneV06Test.java +++ b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrSceneV06Test.java @@ -1,6 +1,7 @@ package dev.zarr.zarrjava.experimental.ome; import dev.zarr.zarrjava.store.FilesystemStore; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateSystemRef; import dev.zarr.zarrjava.store.StoreHandle; import org.junit.jupiter.api.Test; @@ -57,8 +58,8 @@ void openSceneAndNavigateImages() throws Exception { dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation top = scene.getSceneMetadata().coordinateTransformations.get(0); assertEquals("bijection", top.getType()); - assertEquals("JRC2018F#physical", top.getInput()); - assertEquals("FCWB#physical", top.getOutput()); + assertEquals(CoordinateSystemRef.of("physical", "JRC2018F"), top.getInput()); + assertEquals(CoordinateSystemRef.of("physical", "FCWB"), top.getOutput()); assertTrue(scene.listImageNodes().contains("FCWB")); assertTrue(scene.listImageNodes().contains("JRC2018F")); @@ -91,7 +92,7 @@ void createAndReopenScene() throws Exception { dev.zarr.zarrjava.experimental.ome.v0_6.metadata.SceneMetadata sceneMetadata = new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.SceneMetadata( Collections.singletonList(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.TranslationCoordinateTransformation( - "imageA#physical", ".#world", "imageA to world", Arrays.asList(1.0, 2.0), null)), + CoordinateSystemRef.of("physical", "imageA"), CoordinateSystemRef.ofName("world"), "imageA to world", Arrays.asList(1.0, 2.0), null)), Collections.singletonList(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.CoordinateSystem( "world", Arrays.asList(y, x))) ); @@ -105,7 +106,7 @@ void createAndReopenScene() throws Exception { "s0", Collections.singletonList( dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation.scale( - Arrays.asList(1.0, 1.0), "s0", "physical")))), + Arrays.asList(1.0, 1.0), CoordinateSystemRef.ofPath("s0"), CoordinateSystemRef.ofName("physical"))))), null, Collections.singletonList(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.CoordinateSystem( "physical", Arrays.asList(y, x))), @@ -125,7 +126,7 @@ void createAndReopenScene() throws Exception { dev.zarr.zarrjava.experimental.ome.v0_6.Scene reopened = dev.zarr.zarrjava.experimental.ome.v0_6.Scene.openScene(root); assertEquals(Collections.singletonList("imageA"), reopened.listImageNodes()); assertEquals("world", reopened.getSceneMetadata().coordinateSystems.get(0).name); - assertEquals("imageA#physical", reopened.getSceneMetadata().coordinateTransformations.get(0).getInput()); + assertEquals(CoordinateSystemRef.of("physical", "imageA"), reopened.getSceneMetadata().coordinateTransformations.get(0).getInput()); dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage imageA = reopened.openImageNode("imageA"); assertEquals(1, imageA.getScaleLevelCount()); @@ -149,8 +150,8 @@ void openSceneExample1AffinePathBetweenInstruments() throws Exception { dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation ct = scene.getSceneMetadata().coordinateTransformations.get(0); assertEquals("affine", ct.getType()); - assertEquals("sampleA_instrument2#physical_instrument2", ct.getInput()); - assertEquals("sampleA_instrument1#physical_instrument1", ct.getOutput()); + assertEquals(CoordinateSystemRef.of("physical_instrument2", "sampleA_instrument2"), ct.getInput()); + assertEquals(CoordinateSystemRef.of("physical_instrument1", "sampleA_instrument1"), ct.getOutput()); assertTrue(ct instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation); assertEquals("coordinateTransformations/sampleA_instrument2-to-instrument1", ((dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation) ct).path); @@ -179,10 +180,10 @@ void openSceneExample2TwoAffineLinksViaInstrument2() throws Exception { scene.getSceneMetadata().coordinateTransformations.get(1); assertEquals("affine", t0.getType()); assertEquals("affine", t1.getType()); - assertEquals("instrument1#physical", t0.getInput()); - assertEquals("instrument2#physical", t0.getOutput()); - assertEquals("instrument3#physical", t1.getInput()); - assertEquals("instrument2#physical", t1.getOutput()); + assertEquals(CoordinateSystemRef.of("physical", "instrument1"), t0.getInput()); + assertEquals(CoordinateSystemRef.of("physical", "instrument2"), t0.getOutput()); + assertEquals(CoordinateSystemRef.of("physical", "instrument3"), t1.getInput()); + assertEquals(CoordinateSystemRef.of("physical", "instrument2"), t1.getOutput()); assertTrue(t0 instanceof dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation); assertTrue(((dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.AffineCoordinateTransformation) t0).affine.size() > 0); diff --git a/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV06CoordinateSystemRefTest.java b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV06CoordinateSystemRefTest.java new file mode 100644 index 00000000..8670eb77 --- /dev/null +++ b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV06CoordinateSystemRefTest.java @@ -0,0 +1,288 @@ +package dev.zarr.zarrjava.experimental.ome; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.zarr.zarrjava.ZarrException; +import dev.zarr.zarrjava.ZarrTest; +import dev.zarr.zarrjava.experimental.ome.metadata.Axis; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.CoordinateSystem; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.OmeMetadata; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateSystemRef; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.SequenceCoordinateTransformation; +import dev.zarr.zarrjava.store.FilesystemStore; +import dev.zarr.zarrjava.store.StoreHandle; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** OME-Zarr 0.6 coordinate system references ({@code input}/{@code output} as {@code {name, path}} objects). */ +public class OmeZarrV06CoordinateSystemRefTest extends ZarrTest { + + private static final String SPEC_MULTISCALES_JSON = "{" + + "\"version\": \"0.6\"," + + "\"multiscales\": [{" + + " \"name\": \"example\"," + + " \"coordinateSystems\": [" + + " {\"name\": \"physical\", \"axes\": [" + + " {\"name\": \"y\", \"type\": \"space\", \"unit\": \"micrometer\"}," + + " {\"name\": \"x\", \"type\": \"space\", \"unit\": \"micrometer\"}]}," + + " {\"name\": \"output\", \"axes\": [" + + " {\"name\": \"y\", \"type\": \"space\", \"unit\": \"micrometer\"}," + + " {\"name\": \"x\", \"type\": \"space\", \"unit\": \"micrometer\"}]}" + + " ]," + + " \"datasets\": [" + + " {\"path\": \"s0\", \"coordinateTransformations\": [{" + + " \"type\": \"scale\", \"scale\": [0.5, 0.5]," + + " \"input\": {\"path\": \"s0\"}, \"output\": {\"name\": \"physical\"}}]}," + + " {\"path\": \"s1\", \"coordinateTransformations\": [{" + + " \"type\": \"sequence\"," + + " \"input\": {\"path\": \"s1\"}, \"output\": {\"name\": \"physical\"}," + + " \"transformations\": [" + + " {\"type\": \"scale\", \"scale\": [1.0, 1.0]}," + + " {\"type\": \"translation\", \"translation\": [0.25, 0.25]}]}]}" + + " ]," + + " \"coordinateTransformations\": [" + + " {\"type\": \"identity\", \"input\": {\"name\": \"physical\"}, \"output\": {\"name\": \"output\"}}," + + " {\"type\": \"scale\", \"scale\": [2.0, 2.0]," + + " \"input\": {\"name\": \"physical\"}, \"output\": {\"name\": \"physical\", \"path\": \"labels/cells\"}}" + + " ]" + + "}]}"; + + private static final String SPEC_SCENE_JSON = "{" + + "\"version\": \"0.6\"," + + "\"scene\": {" + + " \"coordinateSystems\": [{\"name\": \"world\", \"axes\": [" + + " {\"name\": \"y\", \"type\": \"space\", \"unit\": \"micrometer\"}," + + " {\"name\": \"x\", \"type\": \"space\", \"unit\": \"micrometer\"}]}]," + + " \"coordinateTransformations\": [" + + " {\"type\": \"translation\", \"name\": \"A to B\", \"translation\": [1.0, 2.0]," + + " \"input\": {\"name\": \"physical\", \"path\": \"imageA\"}," + + " \"output\": {\"name\": \"physical\", \"path\": \"imageB\"}}," + + " {\"type\": \"identity\"," + + " \"input\": {\"name\": \"physical\", \"path\": \"imageB\"}," + + " \"output\": {\"name\": \"world\"}}" + + " ]" + + "}}"; + + private static final ObjectMapper JSON = new ObjectMapper(); + + private static JsonNode roundTrip(String json) throws Exception { + OmeMetadata parsed = OmeObjectMappers.makeV3Mapper().readValue(json, OmeMetadata.class); + return dev.zarr.zarrjava.v3.Node.makeObjectMapper().valueToTree(parsed); + } + + private static JsonNode readOmeAttributes(Path groupPath) throws Exception { + return JSON.readTree(Files.readAllBytes(groupPath.resolve("zarr.json"))).get("attributes").get("ome"); + } + + @Test + void specMultiscalesJsonRoundTrips() throws Exception { + assertEquals(JSON.readTree(SPEC_MULTISCALES_JSON), roundTrip(SPEC_MULTISCALES_JSON)); + } + + @Test + void specSceneJsonRoundTrips() throws Exception { + assertEquals(JSON.readTree(SPEC_SCENE_JSON), roundTrip(SPEC_SCENE_JSON)); + } + + @Test + void specRefsAreParsedIntoNameAndPath() throws Exception { + OmeMetadata parsed = OmeObjectMappers.makeV3Mapper().readValue(SPEC_SCENE_JSON, OmeMetadata.class); + CoordinateTransformation t = parsed.scene.coordinateTransformations.get(0); + assertEquals(CoordinateSystemRef.of("physical", "imageA"), t.getInput()); + assertEquals(CoordinateSystemRef.of("physical", "imageB"), t.getOutput()); + assertEquals(CoordinateSystemRef.ofName("world"), parsed.scene.coordinateTransformations.get(1).getOutput()); + + OmeMetadata ms = OmeObjectMappers.makeV3Mapper().readValue(SPEC_MULTISCALES_JSON, OmeMetadata.class); + CoordinateTransformation ds = ms.multiscales.get(0).datasets.get(0).coordinateTransformations.get(0); + assertEquals(CoordinateSystemRef.ofPath("s0"), ds.getInput()); + assertEquals(CoordinateSystemRef.ofName("physical"), ds.getOutput()); + } + + @Test + void sceneFileWrittenAndReopenedKeepsObjectRefsAndResolvesGraph() throws Exception { + Path out = TESTOUTPUT.resolve("ome_v06_scene_object_refs"); + StoreHandle root = new FilesystemStore(out).resolve(); + OmeMetadata parsed = OmeObjectMappers.makeV3Mapper().readValue(SPEC_SCENE_JSON, OmeMetadata.class); + dev.zarr.zarrjava.experimental.ome.v0_6.Scene.createScene(root, parsed.scene); + for (String image : Arrays.asList("imageA", "imageB")) { + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage.create(root.resolve(image), physicalEntry()); + } + + assertEquals(JSON.readTree(SPEC_SCENE_JSON), readOmeAttributes(out)); + + dev.zarr.zarrjava.experimental.ome.v0_6.Scene scene = dev.zarr.zarrjava.experimental.ome.v0_6.Scene.openScene(root); + dev.zarr.zarrjava.experimental.ome.v0_6.SceneTransformationGraph graph = scene.getCoordinateTransformationGraph(); + assertTrue(graph.warnings.isEmpty(), graph.warnings.toString()); + assertEquals(3, graph.nodes.size()); + assertEquals(2, graph.edges.size()); + assertEquals("imageA#physical", graph.edges.get(0).inputNodeId); + assertEquals("imageB#physical", graph.edges.get(0).outputNodeId); + assertEquals(".#world", graph.edges.get(1).outputNodeId); + } + + @Test + void createScaleLevelWritesDatasetInputPathAndIntrinsicOutputName() throws Exception { + Path out = TESTOUTPUT.resolve("ome_v06_create_scale_level_refs"); + StoreHandle handle = new FilesystemStore(out).resolve(); + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage image = + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage.create(handle, physicalEntry()); + image.createScaleLevel("s0", arrayMetadata(16), + Collections.singletonList(dev.zarr.zarrjava.experimental.ome.metadata.transform.CoordinateTransformation.scale( + Arrays.asList(1.0, 1.0)))); + image.createScaleLevel("s1", arrayMetadata(8), + Arrays.asList( + dev.zarr.zarrjava.experimental.ome.metadata.transform.CoordinateTransformation.scale( + Arrays.asList(2.0, 2.0)), + dev.zarr.zarrjava.experimental.ome.metadata.transform.CoordinateTransformation.translation( + Arrays.asList(0.5, 0.5)))); + + JsonNode datasets = readOmeAttributes(out).get("multiscales").get(0).get("datasets"); + JsonNode s0 = datasets.get(0).get("coordinateTransformations"); + assertEquals(1, s0.size()); + assertEquals("scale", s0.get(0).get("type").asText()); + assertEquals(JSON.readTree("{\"path\": \"s0\"}"), s0.get(0).get("input")); + assertEquals(JSON.readTree("{\"name\": \"physical\"}"), s0.get(0).get("output")); + + // multiple transformations are wrapped in a sequence; nested ones carry no input/output + JsonNode s1 = datasets.get(1).get("coordinateTransformations"); + assertEquals(1, s1.size()); + assertEquals("sequence", s1.get(0).get("type").asText()); + assertEquals(JSON.readTree("{\"path\": \"s1\"}"), s1.get(0).get("input")); + assertEquals(JSON.readTree("{\"name\": \"physical\"}"), s1.get(0).get("output")); + JsonNode nested = s1.get(0).get("transformations"); + assertEquals(2, nested.size()); + for (JsonNode n : nested) { + assertFalse(n.has("input")); + assertFalse(n.has("output")); + } + + // version-independent view keeps the flat [scale, translation] list + MultiscaleImage reopened = MultiscaleImage.open(handle); + List generic = + reopened.getMultiscaleNode(0).datasets.get(1).coordinateTransformations; + assertEquals(2, generic.size()); + assertEquals("scale", generic.get(0).type); + assertEquals("translation", generic.get(1).type); + } + + @Test + void createScaleLevelUsesExistingDatasetOutputName() throws Exception { + Path out = TESTOUTPUT.resolve("ome_v06_create_scale_level_existing_output"); + StoreHandle handle = new FilesystemStore(out).resolve(); + List axes = yx(); + dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry entry = + new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry( + Collections.singletonList(new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.Dataset("s0", + Collections.singletonList(CoordinateTransformation.scale(Arrays.asList(1.0, 1.0), + CoordinateSystemRef.ofPath("s0"), CoordinateSystemRef.ofName("intrinsic"))))), + Arrays.asList(new CoordinateSystem("other", axes), new CoordinateSystem("intrinsic", axes)), + "multiscales"); + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage image = + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage.create(handle, entry); + image.createScaleLevel("s1", arrayMetadata(8), + Collections.singletonList(dev.zarr.zarrjava.experimental.ome.metadata.transform.CoordinateTransformation.scale( + Arrays.asList(2.0, 2.0)))); + + CoordinateTransformation written = image.getMultiscalesEntry(0).datasets.get(1).coordinateTransformations.get(0); + assertEquals(CoordinateSystemRef.ofPath("s1"), written.getInput()); + assertEquals(CoordinateSystemRef.ofName("intrinsic"), written.getOutput()); + } + + @Test + void createScaleLevelWithoutCoordinateSystemsFails() throws Exception { + StoreHandle handle = new FilesystemStore(TESTOUTPUT.resolve("ome_v06_create_scale_level_no_cs")).resolve(); + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage image = + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage.create(handle, + new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry( + Collections.emptyList(), + null, "multiscales")); + ZarrException ex = assertThrows(ZarrException.class, () -> image.createScaleLevel("s0", arrayMetadata(8), + Collections.singletonList(dev.zarr.zarrjava.experimental.ome.metadata.transform.CoordinateTransformation.scale( + Arrays.asList(1.0, 1.0))))); + assertTrue(ex.getMessage().contains("intrinsic coordinate system")); + } + + @Test + void legacyStringFixtureOpensAndIsWrittenBackAsObjects() throws Exception { + // the rfc5 examples still use the pre-release string form ("input": "s0", "output": "physical") + Path fixture = TESTDATA.resolve("ome/v0.6/examples/2d/basic/scale_multiscale.zarr"); + JsonNode raw = readOmeAttributes(fixture).get("multiscales").get(0).get("datasets").get(0) + .get("coordinateTransformations").get(0); + assertTrue(raw.get("input").isTextual()); + + dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage image = + (dev.zarr.zarrjava.experimental.ome.v0_6.MultiscaleImage) MultiscaleImage.open( + new FilesystemStore(fixture).resolve()); + CoordinateTransformation ct = image.getMultiscalesEntry(0).datasets.get(0).coordinateTransformations.get(0); + assertEquals(CoordinateSystemRef.ofPath("s0"), ct.getInput()); + assertEquals(CoordinateSystemRef.ofName("physical"), ct.getOutput()); + + OmeMetadata reparsed = OmeObjectMappers.makeV3Mapper().convertValue( + readOmeAttributes(fixture), OmeMetadata.class); + JsonNode rewritten = dev.zarr.zarrjava.v3.Node.makeObjectMapper().valueToTree(reparsed) + .get("multiscales").get(0).get("datasets").get(0).get("coordinateTransformations").get(0); + assertEquals(JSON.readTree("{\"path\": \"s0\"}"), rewritten.get("input")); + assertEquals(JSON.readTree("{\"name\": \"physical\"}"), rewritten.get("output")); + } + + @Test + void legacyStringRefsOutsideDatasetsAreNames() throws Exception { + String json = "{\"version\": \"0.6\", \"multiscales\": [{" + + "\"coordinateSystems\": []," + + "\"datasets\": []," + + "\"coordinateTransformations\": [{\"type\": \"identity\", \"input\": \"physical\", \"output\": \"array\"}]" + + "}], \"scene\": {\"coordinateTransformations\": [" + + "{\"type\": \"identity\", \"input\": \"imgA#physical\", \"output\": \".#world\"}]}}"; + OmeMetadata parsed = OmeObjectMappers.makeV3Mapper().readValue(json, OmeMetadata.class); + CoordinateTransformation ms = parsed.multiscales.get(0).coordinateTransformations.get(0); + assertEquals(CoordinateSystemRef.ofName("physical"), ms.getInput()); + assertEquals(CoordinateSystemRef.ofName("array"), ms.getOutput()); + // "#" strings written by earlier zarr-java versions + CoordinateTransformation scene = parsed.scene.coordinateTransformations.get(0); + assertEquals(CoordinateSystemRef.of("physical", "imgA"), scene.getInput()); + assertEquals(CoordinateSystemRef.ofName("world"), scene.getOutput()); + } + + @Test + void sequenceFactoryAndNestedTransformsHaveNoRefs() { + CoordinateTransformation seq = CoordinateTransformation.sequence( + Arrays.asList(CoordinateTransformation.scale(Arrays.asList(2.0, 2.0)), + CoordinateTransformation.translation(Arrays.asList(0.5, 0.5))), + CoordinateSystemRef.ofPath("s1"), CoordinateSystemRef.ofName("physical")); + JsonNode tree = dev.zarr.zarrjava.v3.Node.makeObjectMapper().valueToTree(seq); + assertEquals(JSON.createObjectNode().put("path", "s1"), tree.get("input")); + assertEquals(JSON.createObjectNode().put("name", "physical"), tree.get("output")); + for (JsonNode n : tree.get("transformations")) { + assertFalse(n.has("input")); + assertFalse(n.has("output")); + } + assertEquals(2, ((SequenceCoordinateTransformation) seq).transformations.size()); + } + + private static List yx() { + return Arrays.asList(new Axis("y", "space", "micrometer"), new Axis("x", "space", "micrometer")); + } + + private static dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry physicalEntry() { + return new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry( + Collections.emptyList(), + Collections.singletonList(new CoordinateSystem("physical", yx())), + "multiscales"); + } + + private static dev.zarr.zarrjava.v3.ArrayMetadata arrayMetadata(int size) throws ZarrException { + return dev.zarr.zarrjava.v3.Array.metadataBuilder() + .withShape(size, size) + .withChunkShape(8, 8) + .withDataType(dev.zarr.zarrjava.v3.DataType.UINT16) + .build(); + } +} diff --git a/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV06Test.java b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV06Test.java index 779d0d4e..6b3dcd97 100644 --- a/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV06Test.java +++ b/src/test/java/dev/zarr/zarrjava/experimental/ome/OmeZarrV06Test.java @@ -10,6 +10,7 @@ import dev.zarr.zarrjava.experimental.ome.metadata.WellMetadata; import dev.zarr.zarrjava.experimental.ome.metadata.WellRef; import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.CoordinateSystem; +import dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateSystemRef; import dev.zarr.zarrjava.store.StoreHandle; import org.junit.jupiter.api.Test; @@ -87,8 +88,8 @@ void datasetsPathsAndTransformations() throws Exception { dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation ct = entry.datasets.get(0).coordinateTransformations.get(0); assertEquals("scale", ct.getType()); - assertEquals("s0", ct.getInput()); - assertEquals("physical", ct.getOutput()); + assertEquals(CoordinateSystemRef.ofPath("s0"), ct.getInput()); + assertEquals(CoordinateSystemRef.ofName("physical"), ct.getOutput()); assertInstanceOf(dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ScaleCoordinateTransformation.class, ct); dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ScaleCoordinateTransformation scaleCt = (dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.ScaleCoordinateTransformation) ct; @@ -174,7 +175,7 @@ void readV06WithOmeroMetadata() throws Exception { "s0", java.util.Collections.singletonList( dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation.scale( - Arrays.asList(1.0, 1.0), "s0", "physical")))); + Arrays.asList(1.0, 1.0), CoordinateSystemRef.ofPath("s0"), CoordinateSystemRef.ofName("physical"))))); dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry ms = new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry( null, @@ -245,7 +246,7 @@ void readV06WithBioformats2rawLayout() throws Exception { "s0", java.util.Collections.singletonList( dev.zarr.zarrjava.experimental.ome.v0_6.metadata.transform.CoordinateTransformation.scale( - Arrays.asList(1.0, 1.0), "s0", "physical")))); + Arrays.asList(1.0, 1.0), CoordinateSystemRef.ofPath("s0"), CoordinateSystemRef.ofName("physical"))))); dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry ms = new dev.zarr.zarrjava.experimental.ome.v0_6.metadata.MultiscalesEntry( null,