Repository: ProjectMirador/mirador (js)

Issue:
Choice in Mirador 3 does not display as expected and breaks from previous Mirador versions
Where a manifest uses Choice, M3 currently defaults to displaying all images in a choice stack at once (all are "on") and they are displayed in reverse order from what is expected. For a user, this means that in order to see a specific image in the stack, they must first turn off all of the layers to see the desired image. As well, the first image that appears in the manifest is at the top of the stack, meaning when this image is displayed and a user chooses to display another image, the original/first image must be turned off. The user interaction here should be additive, not subtractive.

The desired behavior would be to revert to how M2 handled choice: 
- Only the first painting annotation in a choice stack is enabled on load (this is inline with the spec)
- The presentation of images is upwards in a z-index from the first painting annotation encountered (this is necessary so that as new images are selected, they are visible without having to deselect the first image.

The current behavior breaks the functionality of choice manifests created following the IIIF Presentation spec (2 or 3) and used earlier with M2. Also, the layering of images from the top down mean that selected images are not shown.

You can see this using the cookbook recipe: https://projectmirador.org/embed/?iiif-content=https://iiif.io/api/cookbook/recipe/0033-choice/manifest.json

My particular use case is manuscripts with MSI. Each manuscript folio has 12-25 multi-spectral images in a given choice stack, so a user having to uncheck each layer and reordering them to be able to view a specific image is not a good user-experience.

This relates to another issue (https://github.com/ProjectMirador/mirador/issues/3585) that points out that the current M3 method results in many unnecessary image requests.

This is a major blocker for migrating from M2 to M3 since existing manifests do not work as expected.

Some examples:
- Sinai Palimpsests Project: https://sinai.library.ucla.edu | https://sinai-images.library.ucla.edu/iiif/ark:%2F21198%2Fz1kd1z25/manifest (this requires a login to view the images)
- https://prtd.app/#72f604db-6869-4c08-91ce-7c79502a7f35 | https://dvp.prtd.app/hamilton/manifest.json

This current M3 behavior also affects viewing when there are multiple resources layered on a canvas (without choice). Take for example the reconstruction of a manuscript folio: https://iiif.io/api/cookbook/recipe/0036-composition-from-multiple-images/. In this case, it does make sense to load all resources when the manifest is loaded into the viewer, but the order in which the resources are displayed is top to bottom, instead of bottom to top, meaning the subsequent resources are buried under the first resource and the user cannot view the "scene" as intended.


Pull request:
Solve vertical display order and Choice behavior, fix #3693 and #3585
This PR demonstrates a way how #3693 and #3585 could be addressed. At the current state, it is more of a hack than an actual solution. The tests are not adjusted yet as I don't expect the behavior to stay like it is.

This addresses two problems:
- The less preferred options of a Choice are unnecessarily loaded and displayed from the start (#3585, #3693).
- The vertical image order is opposite to the order intended by the Image API authors (##3693).

At first I explored the possibility to solve the first problem via changes to the layers redux state, e.g. by setting all less preferred Choice option images invisible after adding a manifest to a window or after changing the current Canvas, but this would lead to extensive and error prone rework. The current "solution" does not require any changes to the state; instead it changes the default behavior. It is, however, not the cleanest way to do this. One problem is that currently, to my knowledge, manifesto does not indicate in any way if an image annotation is a Choice option, and if so, if it is the preferred one. There is a comment in the code that acknowledges that:

https://github.com/IIIF-Commons/manifesto/blob/63a230dfa0584ed71eeed956e1d0f134ec9b4bed/src/Annotation.ts#L25

We could try to make a suggestion for manifesto, but I'm not sure this will be ready before we want to release M4.

Another **issue for discussion** is the behavior of the Layer sidebar panel. Should images be ordered like they are in the annotations of the Canvas (current behavior, and also the behavior of Mirador 2), or would it be more comprehensible if they were ordered from top to bottom in the same way that they are ordered on a z-axis on the Canvas (behavior in the PR branch)? The current "Move layer to top" button would also make more sense for the latter behavior. It would have to be reshaped if the order in the panel stays as it is.

I'd appreciate any thoughts both on the layer panel order and on possible technical solutions with or without manifesto changes. I'd also be happy about more examples of v2 and v3 manifests with multiple images, Choices or both.

Patch:
diff --git a/src/components/CanvasLayers.js b/src/components/CanvasLayers.js
index b5826643..849ff4fa 100644
--- a/src/components/CanvasLayers.js
+++ b/src/components/CanvasLayers.js
@@ -8,7 +8,8 @@ import ListItem from '@mui/material/ListItem';
 import Slider from '@mui/material/Slider';
 import Tooltip from '@mui/material/Tooltip';
 import DragHandleIcon from '@mui/icons-material/DragHandleSharp';
-import MoveToTopIcon from '@mui/icons-material/VerticalAlignTopSharp';
+import VerticalAlignTopSharp from '@mui/icons-material/VerticalAlignTopSharp';
+import VerticalAlignBottomSharp from '@mui/icons-material/VerticalAlignBottomSharp';
 import VisibilityIcon from '@mui/icons-material/VisibilitySharp';
 import VisibilityOffIcon from '@mui/icons-material/VisibilityOffSharp';
 import OpacityIcon from '@mui/icons-material/OpacitySharp';
@@ -43,14 +44,14 @@ const reorder = (list, startIndex, endIndex) => {
 
 /** @private */
 function Layer({
-  resource, layerMetadata = {}, index, handleOpacityChange, setLayerVisibility, moveToTop,
+  resource, layerMetadata = {}, index, handleOpacityChange, setLayerVisibility, moveToBackground, moveToFront,
 }) {
   const { t } = useTranslation();
-  const { width, height } = { height: undefined, width: 50 };
+  const { width, height } = { height: undefined, width: 40 };
 
   const layer = {
     opacity: 1,
-    visibility: true,
+    visibility: !!resource.preferred,
     ...(layerMetadata || {}),
   };
 
@@ -76,8 +77,13 @@ function Layer({
               { layer.visibility ? <VisibilityIcon /> : <VisibilityOffIcon /> }
             </MiradorMenuButton>
             { layer.index !== 0 && (
-              <MiradorMenuButton aria-label={t('layer_moveToTop')} size="small" onClick={() => { moveToTop(resource.id); }}>
-                <MoveToTopIcon />
+              <MiradorMenuButton aria-label={t('layer_moveToBackground')} size="small" onClick={() => { moveToBackground(resource.id); }}>
+                <VerticalAlignTopSharp />
+              </MiradorMenuButton>
+            )}
+            { layer.index !== layerMetadata && (
+              <MiradorMenuButton aria-label={t('layer_moveToFront')} size="small" onClick={() => { moveToFront(resource.id); }}>
+                <VerticalAlignBottomSharp />
               </MiradorMenuButton>
             )}
           </div>
@@ -133,7 +139,8 @@ Layer.propTypes = {
     opacity: PropTypes.number,
     visibility: PropTypes.bool,
   })), // eslint-disable-line react/forbid-prop-types
-  moveToTop: PropTypes.func.isRequired,
+  moveToBackground: PropTypes.func.isRequired,
+  moveToFront: PropTypes.func.isRequired,
   resource: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
   setLayerVisibility: PropTypes.func.isRequired,
 };
@@ -235,7 +242,7 @@ export function CanvasLayers({
   }, [canvasId, updateLayers, windowId]);
 
   /** */
-  const moveToTop = useCallback((layerId) => {
+  const moveToBackground = useCallback((layerId) => {
     const sortedLayers = reorder(layers.map(l => l.id), layers.findIndex(l => l.id === layerId), 0);
 
     const payload = layers.reduce((acc, layer) => {
@@ -246,6 +253,17 @@ export function CanvasLayers({
     updateLayers(windowId, canvasId, payload);
   }, [canvasId, layers, updateLayers, windowId]);
 
+  const moveToFront = useCallback((layerId) => {
+    const sortedLayers = reorder(layers.map(l => l.id), layers.findIndex(l => l.id === layerId), layers.length - 1);
+
+    const payload = layers.reduce((acc, layer) => {
+      acc[layer.id] = { index: sortedLayers.indexOf(layer.id) };
+      return acc;
+    }, {});
+
+    updateLayers(windowId, canvasId, payload);
+  }, [canvasId, layers, updateLayers, windowId]);
+
   return (
     <>
       { totalSize > 1 && (
@@ -279,7 +297,8 @@ export function CanvasLayers({
                       layerMetadata={(layerMetadata || {})[r.id] || {}}
                       handleOpacityChange={handleOpacityChange}
                       setLayerVisibility={setLayerVisibility}
-                      moveToTop={moveToTop}
+                      moveToBackground={moveToBackground}
+                      moveToFront={moveToFront}
                     />
                   </DraggableLayer>
                 ))
diff --git a/src/lib/CanvasWorld.js b/src/lib/CanvasWorld.js
index b80ab87b..b7caa6d6 100644
--- a/src/lib/CanvasWorld.js
+++ b/src/lib/CanvasWorld.js
@@ -147,7 +147,6 @@ export default class CanvasWorld {
 
   /** @private */
   getLayerMetadata(contentResource) {
-    if (!this.layers) return undefined;
     const miradorCanvas = this.canvases.find(c => (
       c.imageResources.find(r => r.id === contentResource.id)
     ));
@@ -156,15 +155,17 @@ export default class CanvasWorld {
 
     const resourceIndex = miradorCanvas.imageResources
       .findIndex(r => r.id === contentResource.id);
+    const resource = miradorCanvas.imageResources
+      .find(r => r.id === contentResource.id);
 
-    const layer = this.layers[miradorCanvas.canvas.id];
-    const imageResourceLayer = layer && layer[contentResource.id];
+    const layer = this.layers && this.layers[miradorCanvas.canvas.id];
+    const imageResourceLayer = (layer && layer[contentResource.id]) || {};
 
     return {
       index: resourceIndex,
       opacity: 1,
       total: miradorCanvas.imageResources.length,
-      visibility: true,
+      visibility: !!resource.preferred,
       ...imageResourceLayer,
     };
   }
@@ -183,7 +184,7 @@ export default class CanvasWorld {
     const layer = this.getLayerMetadata(contentResource);
     if (!layer) return undefined;
 
-    return layer.total - layer.index - 1;
+    return layer.index;
   }
 
   /**
diff --git a/src/lib/MiradorCanvas.js b/src/lib/MiradorCanvas.js
index a0184fef..6d367cf9 100644
--- a/src/lib/MiradorCanvas.js
+++ b/src/lib/MiradorCanvas.js
@@ -66,17 +66,40 @@ export default class MiradorCanvas {
 
   /** */
   get imageResources() {
+    // TODO Clean up the following hack as soon as manifesto.js provides any information if an annotation body is a Choice option, and if so, whether it is the preferred one.
     const resources = flattenDeep([
       this.canvas.getImages().map(i => i.getResource()),
-      this.canvas.getContent().map(i => i.getBody()),
+      this.canvas.getContent().map(i => (i.__jsonld.body.type === 'Choice' ? i.__jsonld.body : i.getBody())),
     ]);
 
     return flatten(resources.map((resource) => {
-      switch (resource.getProperty('type')) {
-        case 'oa:Choice':
-          return new Canvas({ images: flatten([resource.getProperty('default'), resource.getProperty('item')]).map(r => ({ resource: r })) }, this.canvas.options).getImages().map(i => i.getResource());
-        default:
-          return resource;
+      const type = resource.type || resource.getProperty('type');
+      switch (type) {
+        case 'Choice': {
+          return new Canvas({ images: resource.items.map(r => ({ resource: r })) }, this.canvas.options)
+            .getImages().map((img, index) => {
+              const r = img.getResource();
+              if (r) {
+                r.preferred = !index;
+              }
+              return r;
+            });
+        }
+        case 'oa:Choice': {
+          return new Canvas({ images: flattenDeep([resource.getProperty('default'), resource.getProperty('item')]).map(r => ({ resource: r })) }, this.canvas.options).getImages()
+            .map((img, index) => {
+              const r = img.getResource();
+              if (r) {
+                r.preferred = !index;
+              }
+              return r;
+            });
+        }
+        default: {
+          const r = resource;
+          r.preferred = true;
+          return r;
+        }
       }
     }));
   }
diff --git a/src/locales/ar/translation.json b/src/locales/ar/translation.json
index 848e01f6..2f9e0c6e 100644
--- a/src/locales/ar/translation.json
+++ b/src/locales/ar/translation.json
@@ -61,7 +61,6 @@
     "language": "اللغة",
     "layer_hide": "إخفاء الطبقة",
     "layer_move": "تحريك الطبقة",
-    "layer_moveToTop": "حرك الطبقة إلى الأعلى",
     "layer_opacity": "تعتيم الطبقة",
     "layer_show": "إظهار الطبقة",
     "layers": "طبقات",
diff --git a/src/locales/bg/translation.json b/src/locales/bg/translation.json
index 609b925d..84703f37 100644
--- a/src/locales/bg/translation.json
+++ b/src/locales/bg/translation.json
@@ -71,7 +71,6 @@
     "language": "Език",
     "layer_hide": "Скриване на слой",
     "layer_move": "Преместване на слой",
-    "layer_moveToTop": "Преместване на слой най-отгоре",
     "layer_opacity": "Прозрачност на слой",
     "layer_show": "Показване на слой",
     "layers": "Слоеве",
diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json
index ad23350c..20823396 100644
--- a/src/locales/de/translation.json
+++ b/src/locales/de/translation.json
@@ -71,7 +71,8 @@
     "language": "Sprache",
     "layer_hide": "Ebene verbergen",
     "layer_move": "Ebene verschieben",
-    "layer_moveToTop": "Ebene ganz nach vorn bringen",
+    "layer_moveToBackground": "Ebene in der Hintergrund verschieben",
+    "layer_moveToFront": "Ebene ganz nach vorn bringen",
     "layer_opacity": "Ebenendeckkraft",
     "layer_show": "Ebene anzeigen",
     "layers": "Ebenen",
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 13d45b91..0db241d9 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -71,7 +71,8 @@
     "language": "Language",
     "layer_hide": "Hide layer",
     "layer_move": "Move layer",
-    "layer_moveToTop": "Move layer to top",
+    "layer_moveToBackground": "Move layer to background",
+    "layer_moveToFront": "Move layer to front",
     "layer_opacity": "Layer opacity",
     "layer_show": "Show layer",
     "layers": "Layers",
diff --git a/src/locales/et/translation.json b/src/locales/et/translation.json
index 68cc843f..d8d921dd 100755
--- a/src/locales/et/translation.json
+++ b/src/locales/et/translation.json
@@ -71,7 +71,6 @@
     "language": "Keel",
     "layer_hide": "Peida kiht",
     "layer_move": "Liiguta kihti",
-    "layer_moveToTop": "Tõsta kiht esile",
     "layer_opacity": "Kihi läbipaistvus",
     "layer_show": "Näita kihti",
     "layers": "Kihid",
diff --git a/src/locales/fa/translation.json b/src/locales/fa/translation.json
index b277f001..f3b7b478 100644
--- a/src/locales/fa/translation.json
+++ b/src/locales/fa/translation.json
@@ -71,7 +71,6 @@
     "language": "زبان",
     "layer_hide": "لایه را پنهان کنید",
     "layer_move": "لایه را حرکت دهید",
-    "layer_moveToTop": "لایه را به بالا منتقل کنید",
     "layer_opacity": "لایه کدورت",
     "layer_show": "نمایش لایه",
     "layers": "لایه ها",
diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json
index 0686b170..a0f16eea 100644
--- a/src/locales/fr/translation.json
+++ b/src/locales/fr/translation.json
@@ -68,7 +68,6 @@
     "language": "Langue",
     "layer_hide": "Masquer le calque",
     "layer_move": "Déplacer le calque",
-    "layer_moveToTop": "Déplacer le calque en haut",
     "layer_opacity": "Opacité du calque",
     "layer_show": "Afficher le calque",
     "layers": "Calques",
diff --git a/src/locales/hr/translation.json b/src/locales/hr/translation.json
index 5f26f3da..baf4f4e8 100644
--- a/src/locales/hr/translation.json
+++ b/src/locales/hr/translation.json
@@ -71,7 +71,6 @@
     "language": "Jezik",
     "layer_hide": "Sakrij sloj",
     "layer_move": "Premjesti sloj",
-    "layer_moveToTop": "Premjesti sloj na vrh",
     "layer_opacity": "Prozirnost sloja",
     "layer_show": "Prikaži sloj",
     "layers": "Slojevi",
diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json
index 8c90bac9..5749b041 100644
--- a/src/locales/it/translation.json
+++ b/src/locales/it/translation.json
@@ -70,7 +70,6 @@
     "language": "Lingua",
     "layer_hide": "Nascondi livello",
     "layer_move": "Sposta livello",
-    "layer_moveToTop": "Sposta il livello in alto",
     "layer_opacity": "Opacità del livello",
     "layer_show": "Visualizza livello",
     "layers": "Livelli",
diff --git a/src/locales/ja/translation.json b/src/locales/ja/translation.json
index c5fc2630..dd540b71 100644
--- a/src/locales/ja/translation.json
+++ b/src/locales/ja/translation.json
@@ -70,7 +70,6 @@
     "language": "言語",
     "layer_hide": "レイヤーを隠す",
     "layer_move": "レイヤーを動かす",
-    "layer_moveToTop": "レイヤーをトップへ",
     "layer_opacity": "レイヤーの透過度",
     "layer_show": "レイヤーを表示",
     "layers": "レイヤー",
diff --git a/src/locales/kr/translation.json b/src/locales/kr/translation.json
index 9a0df953..ae57986d 100644
--- a/src/locales/kr/translation.json
+++ b/src/locales/kr/translation.json
@@ -70,7 +70,6 @@
     "language": "언어",
     "layer_hide": "레이어 숨기기",
     "layer_move": "레이어 움직이기",
-    "layer_moveToTop": "레이어를 위로 옮기기",
     "layer_opacity": "레이어 불투명도",
     "layer_show": "레이어 보이기",
     "layers": "레이어",
diff --git a/src/locales/lt/translation.json b/src/locales/lt/translation.json
index 356cdef6..1ab9535d 100644
--- a/src/locales/lt/translation.json
+++ b/src/locales/lt/translation.json
@@ -68,7 +68,6 @@
     "language": "Kalba",
     "layer_hide": "Slėpti sluoksnį",
     "layer_move": "Perkelti sluoksnį",
-    "layer_moveToTop": "Perkelti sluoksnį į viršų",
     "layer_opacity": "Sluoksnio nepermatomumas",
     "layer_show": "Rodyti sluoksnį",
     "layers": "Sluoksniai",
diff --git a/src/locales/nbNo/translation.json b/src/locales/nbNo/translation.json
index c64f1db9..023adf6d 100644
--- a/src/locales/nbNo/translation.json
+++ b/src/locales/nbNo/translation.json
@@ -70,7 +70,6 @@
     "language": "Språk",
     "layer_hide": "Skjul lag",
     "layer_move": "Flytt lag",
-    "layer_moveToTop": "Flytt laget øverst",
     "layer_opacity": "Lag-gjennomsiktighet",
     "layer_show": "Vis lag",
     "layers": "Lag",
diff --git a/src/locales/pl/translation.json b/src/locales/pl/translation.json
index d12e23e7..ea13a879 100644
--- a/src/locales/pl/translation.json
+++ b/src/locales/pl/translation.json
@@ -70,7 +70,6 @@
     "language": "Język",
     "layer_hide": "Ukryj warstwę",
     "layer_move": "Przenieś warstwę",
-    "layer_moveToTop": "Przenieś warstwę na górę",
     "layer_opacity": "Krycie warstwy",
     "layer_show": "Pokaż warstwę",
     "layers": "Warstwy",
diff --git a/src/locales/sr/translation.json b/src/locales/sr/translation.json
index c020ac4c..132a1f29 100644
--- a/src/locales/sr/translation.json
+++ b/src/locales/sr/translation.json
@@ -70,7 +70,6 @@
     "language": "Језик приказа",
     "layer_hide": "Сакријте слој",
     "layer_move": "Померите слој",
-    "layer_moveToTop": "Померите слој на врх",
     "layer_opacity": "Видљивост слоја",
     "layer_show": "Приказ слоја",
     "layers": "Слојеви",
diff --git a/src/locales/sv/translation.json b/src/locales/sv/translation.json
index 2f1baf5e..8e884fb2 100644
--- a/src/locales/sv/translation.json
+++ b/src/locales/sv/translation.json
@@ -70,7 +70,6 @@
     "language": "Språk",
     "layer_hide": "Dölj lager",
     "layer_move": "Flytta lager",
-    "layer_moveToTop": "Flytta lager till toppen",
     "layer_opacity": "Lageropacitet",
     "layer_show": "Visa lager",
     "layers": "Lager",
diff --git a/src/locales/vi/translation.json b/src/locales/vi/translation.json
index 0557d8b9..02824fbe 100644
--- a/src/locales/vi/translation.json
+++ b/src/locales/vi/translation.json
@@ -70,7 +70,6 @@
     "language": "Ngôn ngữ",
     "layer_hide": "Giấu tầng",
     "layer_move": "Chuyển tầng",
-    "layer_moveToTop": "Chuyển tầng lên đỉnh",
     "layer_opacity": "Làm mờ tầng",
     "layer_show": "Hiện tầng",
     "layers": "Tầng",
diff --git a/src/locales/zhCn/translation.json b/src/locales/zhCn/translation.json
index 18710c04..d6010b1c 100644
--- a/src/locales/zhCn/translation.json
+++ b/src/locales/zhCn/translation.json
@@ -70,7 +70,6 @@
     "language": "语言",
     "layer_hide": "隐藏图层",
     "layer_move": "移动图层",
-    "layer_moveToTop": "将图层移到顶部",
     "layer_opacity": "图层不透明度",
     "layer_show": "显示图层",
     "layers": "图层",
diff --git a/src/locales/zhTw/translation.json b/src/locales/zhTw/translation.json
index b9434cf2..785563ef 100644
--- a/src/locales/zhTw/translation.json
+++ b/src/locales/zhTw/translation.json
@@ -70,7 +70,6 @@
     "language": "語言",
     "layer_hide": "隱藏圖層",
     "layer_move": "移動圖層",
-    "layer_moveToTop": "將圖層移到頂部",
     "layer_opacity": "圖層不透明度",
     "layer_show": "顯示圖層",
     "layers": "圖層",


Test patch:
diff --git a/__tests__/integration/mirador/mirador-configs/layers.js b/__tests__/integration/mirador/mirador-configs/layers.js
index d882a48d..05d61985 100644
--- a/__tests__/integration/mirador/mirador-configs/layers.js
+++ b/__tests__/integration/mirador/mirador-configs/layers.js
@@ -1,11 +1,31 @@
 export default {
   catalog: [
     { manifestId: 'https://demos.biblissima.fr/iiif/metadata/BVMM/chateauroux/manifest.json' },
+    { manifestId: 'https://iiif.biblissima.fr/chateauroux/B360446201_MS0005/manifest.json' },
     { manifestId: 'https://prtd.app/aom/manifest.json' },
     { manifestId: 'https://prtd.app/fv/manifest.json' },
-    { manifestId: 'https://manifests.britishart.yale.edu/Osbornfa1' },
+    { manifestId: 'https://dvp.prtd.app/hamilton/manifest.json' },
+    { manifestId: 'https://iiif.io/api/cookbook/recipe/0036-composition-from-multiple-images/manifest.json' },
+    { manifestId: 'https://iiif.io/api/cookbook/recipe/0033-choice/manifest.json' },
+    { manifestId: 'https://iiif.bodleian.ox.ac.uk/iiif/manifest/1fc3f35d-bbb5-4524-8fbe-a5bcb5468be2.json' },
+    { manifestId: 'https://data.getty.edu/media/manifest/bayard-custom' },
+    { manifestId: 'https://heritage.tudelft.nl/iiif/manifests/ejection-seat-front-side/manifest.json' },
+    { manifestId: 'https://iiif.ub.uni-leipzig.de/exp/manifests/layers2/manifest.json' },
   ],
   id: 'mirador',
+  requests: {
+    preprocessors: [
+      (url, options) => ({
+        ...options,
+        headers: {
+          ...options.headers,
+          Accept: (url.includes('bodleian.ox.ac.uk') && (url.endsWith('/info.json')
+            ? 'application/ld+json;profile=http://iiif.io/api/image/3/context.json'
+            : 'application/ld+json;profile=http://iiif.io/api/presentation/3/context.json')) || '',
+        },
+      }),
+    ],
+  },
   window: {
     defaultSideBarPanel: 'layers',
     panels: { // Configure which panels are visible in WindowSideBarButtons
@@ -14,7 +34,8 @@ export default {
     },
     sideBarOpenByDefault: true,
   },
-  windows: [{
-    manifestId: 'https://dvp.prtd.app/hamilton/manifest.json',
-  }],
+  windows: [
+    { manifestId: 'https://dvp.prtd.app/hamilton/manifest.json' },
+    { manifestId: 'https://iiif.io/api/cookbook/recipe/0036-composition-from-multiple-images/manifest.json' },
+  ],
 };
diff --git a/__tests__/src/components/CanvasLayers.test.js b/__tests__/src/components/CanvasLayers.test.js
index 803ae7f8..b679700a 100644
--- a/__tests__/src/components/CanvasLayers.test.js
+++ b/__tests__/src/components/CanvasLayers.test.js
@@ -32,19 +32,22 @@ describe('CanvasLayers', () => {
   });
 
   it('renders canvas layers in a list', () => {
+    // TODO clean up this test once manifesto.js provides info about Choice options
+    const res1 = new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg' }, {});
+    res1.preferred = true;
+    const res2 = new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png' }, {});
+    res2.preferred = true;
     createWrapper({
       canvasId: 'https://prtd.app/hamilton/canvas/p1.json',
-      layers: [
-        new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg' }, {}),
-        new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png' }, {}),
-      ],
+      layers: [res1, res2],
     });
 
     expect(screen.getAllByRole('listitem')[0]).toHaveTextContent('1');
     expect(screen.getAllByRole('listitem')[1]).toHaveTextContent('2');
 
     expect(screen.getAllByRole('button', { name: 'Hide layer' }).length).toEqual(2);
-    expect(screen.getAllByRole('button', { name: 'Move layer to top' }).length).toEqual(2);
+    expect(screen.getAllByRole('button', { name: 'Move layer to background' }).length).toEqual(2);
+    expect(screen.getAllByRole('button', { name: 'Move layer to front' }).length).toEqual(2);
     expect(screen.getAllByRole('spinbutton', { name: 'Layer opacity' }).length).toEqual(2);
   });
 
@@ -87,18 +90,34 @@ describe('CanvasLayers', () => {
     beforeEach(() => {
       updateLayers = vi.fn();
       user = userEvent.setup();
+      // TODO clean up this test setup once manifesto.js provides info about Choice options
+      const res1 = new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg' }, {});
+      const res2 = new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png' }, {});
+      res1.preferred = true;
+      res2.preferred = true;
+
       createWrapper({
         canvasId: 'https://prtd.app/hamilton/canvas/p1.json',
-        layers: [
-          new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg' }, {}),
-          new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png' }, {}),
-        ],
+        layers: [res1, res2],
         updateLayers,
       });
     });
 
-    it('has a button for moving a layer to the top', async () => {
-      await user.click(screen.getAllByLabelText('Move layer to top')[1]);
+    it('has a button for moving a layer to the background', async () => {
+      await user.click(screen.getAllByLabelText('Move layer to background')[1]);
+
+      expect(updateLayers).toHaveBeenCalledWith('abc', 'https://prtd.app/hamilton/canvas/p1.json', {
+        'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg': {
+          index: 1,
+        },
+        'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png': {
+          index: 0,
+        },
+      });
+    });
+
+    it('has a button for moving a layer to the front', async () => {
+      await user.click(screen.getAllByLabelText('Move layer to front')[0]);
 
       expect(updateLayers).toHaveBeenCalledWith('abc', 'https://prtd.app/hamilton/canvas/p1.json', {
         'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg': {
diff --git a/__tests__/src/lib/CanvasWorld.test.js b/__tests__/src/lib/CanvasWorld.test.js
index 41d702f1..34ed1a12 100644
--- a/__tests__/src/lib/CanvasWorld.test.js
+++ b/__tests__/src/lib/CanvasWorld.test.js
@@ -126,10 +126,10 @@ describe('CanvasWorld', () => {
 
   describe('layerIndexOfImageResource', () => {
     const tileSource0 = { id: 'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44/full/full/0/default.jpg' };
-    it('returns undefined by default', () => {
+    it('returns actual index of the image annotation', () => {
       expect(
         new CanvasWorld(canvases).layerIndexOfImageResource(tileSource0),
-      ).toEqual(undefined);
+      ).toEqual(0);
     });
 
     it('returns the inverse of the configured index', () => {
diff --git a/__tests__/src/lib/MiradorCanvas.test.js b/__tests__/src/lib/MiradorCanvas.test.js
index e5d19e3b..de4dc137 100644
--- a/__tests__/src/lib/MiradorCanvas.test.js
+++ b/__tests__/src/lib/MiradorCanvas.test.js
@@ -134,6 +134,61 @@ describe('MiradorCanvas', () => {
       expect(instance.v3VttContent.length).toEqual(1);
     });
   });
+  describe('IIIF image annotations', () => {
+    it('sets preferred=true for prezi v2 image annotations without Choices', () => {
+      instance = new MiradorCanvas(
+        Utils.parseManifest(fixture).getSequences()[0].getCanvases()[0],
+      );
+      expect(instance.imageResources[0].preferred).toBe(true);
+    });
+
+    it('sets preferred=true for prezi v3 image annotations without Choices', () => {
+      instance = new MiradorCanvas(
+        Utils.parseManifest(fragmentFixtureV3).getSequences()[0].getCanvases()[0],
+      );
+      const firstImgWithoutChoice = instance.imageResources.find((resource) => resource.id === 'https://images.prtd.app/iiif/2/hamilton%2fHL_524_1r_00_PC17/full/739,521/0/default.jpg');
+      expect(firstImgWithoutChoice.preferred).toBe(true);
+      const lastImgWithoutChoice = instance.imageResources.find((resource) => resource.id === 'https://images.prtd.app/iiif/2/hamilton%2fHL_524_1r_00_PCA_RGB-1-3-5_gradi/full/739,521/0/default.jpg');
+      expect(lastImgWithoutChoice.preferred).toBe(true);
+    });
+
+    it('sets preferred=true for default prezi v2 Choice option', () => {
+      instance = new MiradorCanvas(
+        Utils.parseManifest(fragmentFixture).getSequences()[0].getCanvases()[0],
+      );
+      const preferredOption = instance.imageResources.find((resource) => resource.id === 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg');
+      expect(preferredOption.preferred).toBe(true);
+    });
+
+    it('sets preferred=true for first prezi v3 image Choice option', () => {
+      instance = new MiradorCanvas(
+        Utils.parseManifest(fragmentFixtureV3).getSequences()[0].getCanvases()[0],
+      );
+      const preferredOption = instance.imageResources.find((resource) => resource.id === 'https://images.prtd.app/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/,800/0/default.jpg');
+      expect(preferredOption.preferred).toBe(true);
+    });
+
+    it('sets preferred=false for alternative prezi v2 Choice options', () => {
+      instance = new MiradorCanvas(
+        Utils.parseManifest(fragmentFixture).getSequences()[0].getCanvases()[0],
+      );
+      const firstAlternative = instance.imageResources.find((img) => img.id === 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png');
+      expect(firstAlternative.preferred).toBe(false);
+      const lastAlternative = instance.imageResources.find((img) => img.id === 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_017_F/full/862,1024/0/default.jpg');
+      expect(lastAlternative.preferred).toBe(false);
+    });
+
+    it('sets preferred=false for alternative prezi v3 Choice options', () => {
+      instance = new MiradorCanvas(
+        Utils.parseManifest(fragmentFixtureV3).getSequences()[0].getCanvases()[0],
+      );
+      const firstAlternative = instance.imageResources.find((img) => img.id === 'https://images.prtd.app/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png');
+      expect(firstAlternative.preferred).toBe(false);
+      const lastAlternative = instance.imageResources.find((img) => img.id === 'https://images.prtd.app/iiif/2/hamilton%2fHL_524_1r_00_017_F/full/,800/0/default.jpg');
+      expect(lastAlternative.preferred).toBe(false);
+    });
+  });
+
   describe('textResources', () => {
     it('returns text', () => {
       instance = new MiradorCanvas(