optica-core 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "optica-core",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Shared frontend core of the Optica viewers",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -1,7 +1,9 @@
1
1
  // Minimal image identity shared by every app (stack, rotation, spectral, ...)
2
2
  export type ImageName = {
3
3
  name: string,
4
- label: string
4
+ label: string,
5
+ // sent by the backend when the thumbnail of this image exists
6
+ hasThumbnail?: boolean
5
7
  }
6
8
 
7
9
  // What an annotation keeps of its image: the name to find it back, the label and the
@@ -11,7 +11,6 @@ export type AcquisitionObject<TImage extends ProjectImage = ProjectImage> = {
11
11
  // images on their own: the stacked results, the depth maps...
12
12
  standaloneImages?: TImage[]
13
13
  size: Size
14
- thumbnails: boolean
15
14
  }
16
15
 
17
16
  // Images taken together, with the viewer state and the annotations of the acquisition.
@@ -22,7 +21,6 @@ export class Acquisition<TImage extends ProjectImage = ProjectImage, TAnnotation
22
21
  images: TImage[]
23
22
  standaloneImages: TImage[]
24
23
  size: Size
25
- thumbnails: boolean
26
24
  camera: Camera
27
25
  // shared by the images of the group and the standalone ones
28
26
  annotations: TAnnotations
@@ -37,7 +35,6 @@ export class Acquisition<TImage extends ProjectImage = ProjectImage, TAnnotation
37
35
  this.images = object.images
38
36
  this.standaloneImages = object.standaloneImages ?? []
39
37
  this.size = object.size
40
- this.thumbnails = object.thumbnails
41
38
  this.camera = createCamera()
42
39
  this.annotations = this.createAnnotations()
43
40
  this.index = 0
@@ -121,7 +118,6 @@ export class Acquisition<TImage extends ProjectImage = ProjectImage, TAnnotation
121
118
  images: this.images,
122
119
  standaloneImages: this.standaloneImages,
123
120
  size: this.size,
124
- thumbnails: this.thumbnails,
125
121
  }
126
122
  }
127
123
  }
@@ -28,11 +28,12 @@ export abstract class DataRepository<TData> implements Repository<TData> {
28
28
  return this.provider.getThumbnail(objectPath, imageName)
29
29
  }
30
30
 
31
- // Fill the image and thumbnail urls of images sent by /images
32
- protected resolveUrls<T extends ImageName>(objectPath: string, images: T[], thumbnails: boolean): (T & ProjectImage)[] {
31
+ // Fill the image and thumbnail urls of images sent by /images,
32
+ // the thumbnail url only for the images the backend marks with `hasThumbnail`
33
+ protected resolveUrls<T extends ImageName>(objectPath: string, images: T[]): (T & ProjectImage)[] {
33
34
  return images.map((image) => Object.assign(image, {
34
35
  image: this.getFullImage(objectPath, image.name),
35
- thumbnail: thumbnails ? this.getThumbnail(objectPath, image.name) : undefined,
36
+ thumbnail: image.hasThumbnail ? this.getThumbnail(objectPath, image.name) : undefined,
36
37
  }))
37
38
  }
38
39
  }
@@ -34,8 +34,8 @@ export class ProjectRepository<TImage extends ProjectImage = ProjectImage, TAcqu
34
34
  async getImages(objectPath: string): Promise<Project<TImage, TAcquisition>> {
35
35
  const res = await this.provider.getImages(objectPath)
36
36
  const acquisitions = this.parse(res.data).map((object) => {
37
- object.images = this.resolveUrls(objectPath, object.images, object.thumbnails)
38
- object.standaloneImages = this.resolveUrls(objectPath, object.standaloneImages ?? [], object.thumbnails)
37
+ object.images = this.resolveUrls(objectPath, object.images)
38
+ object.standaloneImages = this.resolveUrls(objectPath, object.standaloneImages ?? [])
39
39
  return this.createAcquisition(object)
40
40
  })
41
41
  return new Project<TImage, TAcquisition>(objectPath, acquisitions)
@@ -9,7 +9,7 @@ import {
9
9
  const size = { width: 100, height: 100 }
10
10
  const image = (name: string): ProjectImage => ({ name, label: name, image: "" })
11
11
  const landmark = (id: string, img = image("i0")) => new Landmark(id, id, { marker: { x: 1, y: 2 }, image: img }, { x: 10, y: 20 }, Color("#ff0000"))
12
- const acquisition = (id: string, names: string[] = ["i0"]) => new Acquisition({ id, images: names.map(image), size, thumbnails: false })
12
+ const acquisition = (id: string, names: string[] = ["i0"]) => new Acquisition({ id, images: names.map(image), size })
13
13
 
14
14
  describe("Annotations", () => {
15
15
  it("creates a selected distance and returns the reactive copy through a store", () => {
@@ -88,7 +88,7 @@ class TaggedAcquisition extends Acquisition<ProjectImage> {
88
88
 
89
89
  describe("Project annotations", () => {
90
90
  it("lets an app use its own Annotations", () => {
91
- const a = new TaggedAcquisition({ id: "a", images: [image("i0")], size, thumbnails: false })
91
+ const a = new TaggedAcquisition({ id: "a", images: [image("i0")], size })
92
92
  expect(a.annotations.tagged).toBe(true)
93
93
  a.annotations.restore({ distances: [{ label: "d", color: "#00ff00", landmarks: [], value: 0 }] })
94
94
  expect(a.annotations.distances[0]).toBeInstanceOf(Distance)
@@ -6,7 +6,7 @@ const size = { width: 2000, height: 1000 }
6
6
  const img = (name: string, extra = {}): ProjectImage => ({ name, label: name, image: "", ...extra })
7
7
 
8
8
  function acquisition(images: string[], standalone: string[] = []): Acquisition {
9
- return new Acquisition({ id: "a", images: images.map((n) => img(n)), standaloneImages: standalone.map((n) => img(n)), size, thumbnails: false })
9
+ return new Acquisition({ id: "a", images: images.map((n) => img(n)), standaloneImages: standalone.map((n) => img(n)), size })
10
10
  }
11
11
 
12
12
  describe("Acquisition", () => {
@@ -42,7 +42,7 @@ describe("Acquisition", () => {
42
42
  })
43
43
 
44
44
  it("uses the size of the image when it has one", () => {
45
- const a = new Acquisition({ id: "a", images: [], standaloneImages: [img("d0", { size: { width: 5, height: 5 } }), img("d1")], size, thumbnails: false })
45
+ const a = new Acquisition({ id: "a", images: [], standaloneImages: [img("d0", { size: { width: 5, height: 5 } }), img("d1")], size })
46
46
  expect(a.sizeOf()).toEqual({ width: 5, height: 5 })
47
47
  expect(a.sizeOf(a.getImage("d1"))).toEqual(size)
48
48
  })
@@ -50,8 +50,8 @@ describe("Acquisition", () => {
50
50
 
51
51
  describe("Project", () => {
52
52
  it("selects acquisitions and finds images", () => {
53
- const vis = new Acquisition({ id: "VIS", images: [img("vis0")], size, thumbnails: false })
54
- const uv = new Acquisition({ id: "UV", images: [img("uv0")], standaloneImages: [img("uvRGB")], size, thumbnails: false })
53
+ const vis = new Acquisition({ id: "VIS", images: [img("vis0")], size })
54
+ const uv = new Acquisition({ id: "UV", images: [img("uv0")], standaloneImages: [img("uvRGB")], size })
55
55
  const p = new Project("A1", [vis, uv])
56
56
  expect(p.selectedImage?.name).toBe("vis0")
57
57
  p.selectAcquisition("UV")
@@ -75,21 +75,32 @@ function provider(data: unknown): DataProvider {
75
75
 
76
76
  describe("ProjectRepository", () => {
77
77
  it("reads the standard payload and resolves the urls", async () => {
78
- const payload = { acquisitions: [{ id: "stack", images: [{ name: "s0", label: "s0" }], standaloneImages: [{ name: "r", label: "Result" }], size, thumbnails: true }] }
78
+ const payload = { acquisitions: [{ id: "stack", images: [{ name: "s0", label: "s0", hasThumbnail: true }], standaloneImages: [{ name: "r", label: "Result" }], size }] }
79
79
  const project = await new ProjectRepository(provider(payload)).getImages("A1")
80
80
  const a = project.acquisition!
81
81
  expect(a.images[0]).toMatchObject({ image: "S/A1/s0/full-image", thumbnail: "S/A1/s0/thumbnail" })
82
82
  expect(a.standaloneImages[0]).toMatchObject({ label: "Result", image: "S/A1/r/full-image" })
83
83
  })
84
84
 
85
+ it("resolves the thumbnail of the images marked with hasThumbnail only", async () => {
86
+ const payload = { acquisitions: [{
87
+ id: "stack", size,
88
+ images: [{ name: "s0", label: "s0", hasThumbnail: true }, { name: "s1", label: "s1", hasThumbnail: false }, { name: "s2", label: "s2" }],
89
+ standaloneImages: [{ name: "r", label: "Result", hasThumbnail: true }],
90
+ }] }
91
+ const a = (await new ProjectRepository(provider(payload)).getImages("A1")).acquisition!
92
+ expect(a.images.map((image) => image.thumbnail)).toEqual(["S/A1/s0/thumbnail", undefined, undefined])
93
+ expect(a.standaloneImages[0].thumbnail).toBe("S/A1/r/thumbnail")
94
+ })
95
+
85
96
  it("adapts a legacy payload with a parser (spectral: one acquisition per group)", async () => {
86
97
  type SpectralImage = ProjectImage & { wavelength: { value: number } }
87
98
  const legacy = {
88
- VIS: { spectralImages: [{ name: "v450", label: "v450", wavelength: { value: 450 } }], individualImages: { RGB: { name: "vRGB", label: "RGB" } }, size, thumbnails: false },
89
- UV: { spectralImages: [], individualImages: { RGB: { name: "uRGB", label: "RGB" } }, size, thumbnails: false },
99
+ VIS: { spectralImages: [{ name: "v450", label: "v450", wavelength: { value: 450 } }], individualImages: { RGB: { name: "vRGB", label: "RGB" } }, size },
100
+ UV: { spectralImages: [], individualImages: { RGB: { name: "uRGB", label: "RGB" } }, size },
90
101
  }
91
102
  const parse = (data: any): AcquisitionObject<SpectralImage>[] => Object.entries<any>(data).map(([id, group]) => ({
92
- id, images: group.spectralImages, standaloneImages: Object.values(group.individualImages), size: group.size, thumbnails: group.thumbnails,
103
+ id, images: group.spectralImages, standaloneImages: Object.values(group.individualImages), size: group.size,
93
104
  }))
94
105
  const project = await new ProjectRepository<SpectralImage>(provider(legacy), parse).getImages("A1")
95
106
  expect(project.acquisitions.map((a) => a.id)).toEqual(["VIS", "UV"])
@@ -99,9 +110,9 @@ describe("ProjectRepository", () => {
99
110
  })
100
111
 
101
112
  it("adapts depth: no group, standalone images with their own size", async () => {
102
- const legacy = { images: [{ id: "d0", label: "D0", size: { width: 10, height: 20 } }], thumbnails: false }
113
+ const legacy = { images: [{ id: "d0", label: "D0", size: { width: 10, height: 20 } }] }
103
114
  const parse = (data: any): AcquisitionObject[] => [{
104
- id: "depth", images: [], size: data.images[0].size, thumbnails: data.thumbnails,
115
+ id: "depth", images: [], size: data.images[0].size,
105
116
  standaloneImages: data.images.map((i: any) => ({ name: i.id, label: i.label, size: i.size })),
106
117
  }]
107
118
  const project = await new ProjectRepository(provider(legacy), parse).getImages("A1")