Skip to content

Commit 24f285e

Browse files
authored
Merge pull request #4015 from ProjectMirador/layer-choice-behavior-hacks
Solve vertical display order and Choice behavior, fix #3693 and #3585
2 parents 98fb916 + 817644b commit 24f285e

26 files changed

+179
-56
lines changed
Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,31 @@
11
export default {
22
catalog: [
33
{ manifestId: 'https://demos.biblissima.fr/iiif/metadata/BVMM/chateauroux/manifest.json' },
4+
{ manifestId: 'https://iiif.biblissima.fr/chateauroux/B360446201_MS0005/manifest.json' },
45
{ manifestId: 'https://prtd.app/aom/manifest.json' },
56
{ manifestId: 'https://prtd.app/fv/manifest.json' },
6-
{ manifestId: 'https://manifests.britishart.yale.edu/Osbornfa1' },
7+
{ manifestId: 'https://dvp.prtd.app/hamilton/manifest.json' },
8+
{ manifestId: 'https://iiif.io/api/cookbook/recipe/0036-composition-from-multiple-images/manifest.json' },
9+
{ manifestId: 'https://iiif.io/api/cookbook/recipe/0033-choice/manifest.json' },
10+
{ manifestId: 'https://iiif.bodleian.ox.ac.uk/iiif/manifest/1fc3f35d-bbb5-4524-8fbe-a5bcb5468be2.json' },
11+
{ manifestId: 'https://data.getty.edu/media/manifest/bayard-custom' },
12+
{ manifestId: 'https://heritage.tudelft.nl/iiif/manifests/ejection-seat-front-side/manifest.json' },
13+
{ manifestId: 'https://iiif.ub.uni-leipzig.de/exp/manifests/layers2/manifest.json' },
714
],
815
id: 'mirador',
16+
requests: {
17+
preprocessors: [
18+
(url, options) => ({
19+
...options,
20+
headers: {
21+
...options.headers,
22+
Accept: (url.includes('bodleian.ox.ac.uk') && (url.endsWith('/info.json')
23+
? 'application/ld+json;profile=http://iiif.io/api/image/3/context.json'
24+
: 'application/ld+json;profile=http://iiif.io/api/presentation/3/context.json')) || '',
25+
},
26+
}),
27+
],
28+
},
929
window: {
1030
defaultSideBarPanel: 'layers',
1131
panels: { // Configure which panels are visible in WindowSideBarButtons
@@ -14,7 +34,8 @@ export default {
1434
},
1535
sideBarOpenByDefault: true,
1636
},
17-
windows: [{
18-
manifestId: 'https://dvp.prtd.app/hamilton/manifest.json',
19-
}],
37+
windows: [
38+
{ manifestId: 'https://dvp.prtd.app/hamilton/manifest.json' },
39+
{ manifestId: 'https://iiif.io/api/cookbook/recipe/0036-composition-from-multiple-images/manifest.json' },
40+
],
2041
};

__tests__/src/components/CanvasLayers.test.js

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,22 @@ describe('CanvasLayers', () => {
3232
});
3333

3434
it('renders canvas layers in a list', () => {
35+
// TODO clean up this test once manifesto.js provides info about Choice options
36+
const res1 = new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg' }, {});
37+
res1.preferred = true;
38+
const res2 = new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png' }, {});
39+
res2.preferred = true;
3540
createWrapper({
3641
canvasId: 'https://prtd.app/hamilton/canvas/p1.json',
37-
layers: [
38-
new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg' }, {}),
39-
new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png' }, {}),
40-
],
42+
layers: [res1, res2],
4143
});
4244

4345
expect(screen.getAllByRole('listitem')[0]).toHaveTextContent('1');
4446
expect(screen.getAllByRole('listitem')[1]).toHaveTextContent('2');
4547

4648
expect(screen.getAllByRole('button', { name: 'Hide layer' }).length).toEqual(2);
47-
expect(screen.getAllByRole('button', { name: 'Move layer to top' }).length).toEqual(2);
49+
expect(screen.getAllByRole('button', { name: 'Move layer to background' }).length).toEqual(2);
50+
expect(screen.getAllByRole('button', { name: 'Move layer to front' }).length).toEqual(2);
4851
expect(screen.getAllByRole('spinbutton', { name: 'Layer opacity' }).length).toEqual(2);
4952
});
5053

@@ -87,18 +90,34 @@ describe('CanvasLayers', () => {
8790
beforeEach(() => {
8891
updateLayers = vi.fn();
8992
user = userEvent.setup();
93+
// TODO clean up this test setup once manifesto.js provides info about Choice options
94+
const res1 = new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg' }, {});
95+
const res2 = new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png' }, {});
96+
res1.preferred = true;
97+
res2.preferred = true;
98+
9099
createWrapper({
91100
canvasId: 'https://prtd.app/hamilton/canvas/p1.json',
92-
layers: [
93-
new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg' }, {}),
94-
new Resource({ id: 'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png' }, {}),
95-
],
101+
layers: [res1, res2],
96102
updateLayers,
97103
});
98104
});
99105

100-
it('has a button for moving a layer to the top', async () => {
101-
await user.click(screen.getAllByLabelText('Move layer to top')[1]);
106+
it('has a button for moving a layer to the background', async () => {
107+
await user.click(screen.getAllByLabelText('Move layer to background')[1]);
108+
109+
expect(updateLayers).toHaveBeenCalledWith('abc', 'https://prtd.app/hamilton/canvas/p1.json', {
110+
'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg': {
111+
index: 1,
112+
},
113+
'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_TS_Blue/full/862,1024/0/default.png': {
114+
index: 0,
115+
},
116+
});
117+
});
118+
119+
it('has a button for moving a layer to the front', async () => {
120+
await user.click(screen.getAllByLabelText('Move layer to front')[0]);
102121

103122
expect(updateLayers).toHaveBeenCalledWith('abc', 'https://prtd.app/hamilton/canvas/p1.json', {
104123
'https://prtd.app/image/iiif/2/hamilton%2fHL_524_1r_00_PSC/full/862,1024/0/default.jpg': {

__tests__/src/lib/CanvasWorld.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,10 @@ describe('CanvasWorld', () => {
126126

127127
describe('layerIndexOfImageResource', () => {
128128
const tileSource0 = { id: 'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44/full/full/0/default.jpg' };
129-
it('returns undefined by default', () => {
129+
it('returns actual index of the image annotation', () => {
130130
expect(
131131
new CanvasWorld(canvases).layerIndexOfImageResource(tileSource0),
132-
).toEqual(undefined);
132+
).toEqual(0);
133133
});
134134

135135
it('returns the inverse of the configured index', () => {

__tests__/src/lib/MiradorCanvas.test.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,61 @@ describe('MiradorCanvas', () => {
134134
expect(instance.v3VttContent.length).toEqual(1);
135135
});
136136
});
137+
describe('IIIF image annotations', () => {
138+
it('sets preferred=true for prezi v2 image annotations without Choices', () => {
139+
instance = new MiradorCanvas(
140+
Utils.parseManifest(fixture).getSequences()[0].getCanvases()[0],
141+
);
142+
expect(instance.imageResources[0].preferred).toBe(true);
143+
});
144+
145+
it('sets preferred=true for prezi v3 image annotations without Choices', () => {
146+
instance = new MiradorCanvas(
147+
Utils.parseManifest(fragmentFixtureV3).getSequences()[0].getCanvases()[0],
148+
);
149+
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');
150+
expect(firstImgWithoutChoice.preferred).toBe(true);
151+
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');
152+
expect(lastImgWithoutChoice.preferred).toBe(true);
153+
});
154+
155+
it('sets preferred=true for default prezi v2 Choice option', () => {
156+
instance = new MiradorCanvas(
157+
Utils.parseManifest(fragmentFixture).getSequences()[0].getCanvases()[0],
158+
);
159+
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');
160+
expect(preferredOption.preferred).toBe(true);
161+
});
162+
163+
it('sets preferred=true for first prezi v3 image Choice option', () => {
164+
instance = new MiradorCanvas(
165+
Utils.parseManifest(fragmentFixtureV3).getSequences()[0].getCanvases()[0],
166+
);
167+
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');
168+
expect(preferredOption.preferred).toBe(true);
169+
});
170+
171+
it('sets preferred=false for alternative prezi v2 Choice options', () => {
172+
instance = new MiradorCanvas(
173+
Utils.parseManifest(fragmentFixture).getSequences()[0].getCanvases()[0],
174+
);
175+
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');
176+
expect(firstAlternative.preferred).toBe(false);
177+
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');
178+
expect(lastAlternative.preferred).toBe(false);
179+
});
180+
181+
it('sets preferred=false for alternative prezi v3 Choice options', () => {
182+
instance = new MiradorCanvas(
183+
Utils.parseManifest(fragmentFixtureV3).getSequences()[0].getCanvases()[0],
184+
);
185+
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');
186+
expect(firstAlternative.preferred).toBe(false);
187+
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');
188+
expect(lastAlternative.preferred).toBe(false);
189+
});
190+
});
191+
137192
describe('textResources', () => {
138193
it('returns text', () => {
139194
instance = new MiradorCanvas(

src/components/CanvasLayers.js

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import ListItem from '@mui/material/ListItem';
88
import Slider from '@mui/material/Slider';
99
import Tooltip from '@mui/material/Tooltip';
1010
import DragHandleIcon from '@mui/icons-material/DragHandleSharp';
11-
import MoveToTopIcon from '@mui/icons-material/VerticalAlignTopSharp';
11+
import VerticalAlignTopSharp from '@mui/icons-material/VerticalAlignTopSharp';
12+
import VerticalAlignBottomSharp from '@mui/icons-material/VerticalAlignBottomSharp';
1213
import VisibilityIcon from '@mui/icons-material/VisibilitySharp';
1314
import VisibilityOffIcon from '@mui/icons-material/VisibilityOffSharp';
1415
import OpacityIcon from '@mui/icons-material/OpacitySharp';
@@ -43,14 +44,14 @@ const reorder = (list, startIndex, endIndex) => {
4344

4445
/** @private */
4546
function Layer({
46-
resource, layerMetadata = {}, index, handleOpacityChange, setLayerVisibility, moveToTop,
47+
resource, layerMetadata = {}, index, handleOpacityChange, setLayerVisibility, moveToBackground, moveToFront,
4748
}) {
4849
const { t } = useTranslation();
49-
const { width, height } = { height: undefined, width: 50 };
50+
const { width, height } = { height: undefined, width: 40 };
5051

5152
const layer = {
5253
opacity: 1,
53-
visibility: true,
54+
visibility: !!resource.preferred,
5455
...(layerMetadata || {}),
5556
};
5657

@@ -76,8 +77,13 @@ function Layer({
7677
{ layer.visibility ? <VisibilityIcon /> : <VisibilityOffIcon /> }
7778
</MiradorMenuButton>
7879
{ layer.index !== 0 && (
79-
<MiradorMenuButton aria-label={t('layer_moveToTop')} size="small" onClick={() => { moveToTop(resource.id); }}>
80-
<MoveToTopIcon />
80+
<MiradorMenuButton aria-label={t('layer_moveToBackground')} size="small" onClick={() => { moveToBackground(resource.id); }}>
81+
<VerticalAlignTopSharp />
82+
</MiradorMenuButton>
83+
)}
84+
{ layer.index !== layerMetadata && (
85+
<MiradorMenuButton aria-label={t('layer_moveToFront')} size="small" onClick={() => { moveToFront(resource.id); }}>
86+
<VerticalAlignBottomSharp />
8187
</MiradorMenuButton>
8288
)}
8389
</div>
@@ -133,7 +139,8 @@ Layer.propTypes = {
133139
opacity: PropTypes.number,
134140
visibility: PropTypes.bool,
135141
})), // eslint-disable-line react/forbid-prop-types
136-
moveToTop: PropTypes.func.isRequired,
142+
moveToBackground: PropTypes.func.isRequired,
143+
moveToFront: PropTypes.func.isRequired,
137144
resource: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
138145
setLayerVisibility: PropTypes.func.isRequired,
139146
};
@@ -235,7 +242,7 @@ export function CanvasLayers({
235242
}, [canvasId, updateLayers, windowId]);
236243

237244
/** */
238-
const moveToTop = useCallback((layerId) => {
245+
const moveToBackground = useCallback((layerId) => {
239246
const sortedLayers = reorder(layers.map(l => l.id), layers.findIndex(l => l.id === layerId), 0);
240247

241248
const payload = layers.reduce((acc, layer) => {
@@ -246,6 +253,17 @@ export function CanvasLayers({
246253
updateLayers(windowId, canvasId, payload);
247254
}, [canvasId, layers, updateLayers, windowId]);
248255

256+
const moveToFront = useCallback((layerId) => {
257+
const sortedLayers = reorder(layers.map(l => l.id), layers.findIndex(l => l.id === layerId), layers.length - 1);
258+
259+
const payload = layers.reduce((acc, layer) => {
260+
acc[layer.id] = { index: sortedLayers.indexOf(layer.id) };
261+
return acc;
262+
}, {});
263+
264+
updateLayers(windowId, canvasId, payload);
265+
}, [canvasId, layers, updateLayers, windowId]);
266+
249267
return (
250268
<>
251269
{ totalSize > 1 && (
@@ -279,7 +297,8 @@ export function CanvasLayers({
279297
layerMetadata={(layerMetadata || {})[r.id] || {}}
280298
handleOpacityChange={handleOpacityChange}
281299
setLayerVisibility={setLayerVisibility}
282-
moveToTop={moveToTop}
300+
moveToBackground={moveToBackground}
301+
moveToFront={moveToFront}
283302
/>
284303
</DraggableLayer>
285304
))

src/lib/CanvasWorld.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,6 @@ export default class CanvasWorld {
147147

148148
/** @private */
149149
getLayerMetadata(contentResource) {
150-
if (!this.layers) return undefined;
151150
const miradorCanvas = this.canvases.find(c => (
152151
c.imageResources.find(r => r.id === contentResource.id)
153152
));
@@ -156,15 +155,17 @@ export default class CanvasWorld {
156155

157156
const resourceIndex = miradorCanvas.imageResources
158157
.findIndex(r => r.id === contentResource.id);
158+
const resource = miradorCanvas.imageResources
159+
.find(r => r.id === contentResource.id);
159160

160-
const layer = this.layers[miradorCanvas.canvas.id];
161-
const imageResourceLayer = layer && layer[contentResource.id];
161+
const layer = this.layers && this.layers[miradorCanvas.canvas.id];
162+
const imageResourceLayer = (layer && layer[contentResource.id]) || {};
162163

163164
return {
164165
index: resourceIndex,
165166
opacity: 1,
166167
total: miradorCanvas.imageResources.length,
167-
visibility: true,
168+
visibility: !!resource.preferred,
168169
...imageResourceLayer,
169170
};
170171
}
@@ -183,7 +184,7 @@ export default class CanvasWorld {
183184
const layer = this.getLayerMetadata(contentResource);
184185
if (!layer) return undefined;
185186

186-
return layer.total - layer.index - 1;
187+
return layer.index;
187188
}
188189

189190
/**

src/lib/MiradorCanvas.js

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,40 @@ export default class MiradorCanvas {
6666

6767
/** */
6868
get imageResources() {
69+
// 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.
6970
const resources = flattenDeep([
7071
this.canvas.getImages().map(i => i.getResource()),
71-
this.canvas.getContent().map(i => i.getBody()),
72+
this.canvas.getContent().map(i => (i.__jsonld.body.type === 'Choice' ? i.__jsonld.body : i.getBody())),
7273
]);
7374

7475
return flatten(resources.map((resource) => {
75-
switch (resource.getProperty('type')) {
76-
case 'oa:Choice':
77-
return new Canvas({ images: flatten([resource.getProperty('default'), resource.getProperty('item')]).map(r => ({ resource: r })) }, this.canvas.options).getImages().map(i => i.getResource());
78-
default:
79-
return resource;
76+
const type = resource.type || resource.getProperty('type');
77+
switch (type) {
78+
case 'Choice': {
79+
return new Canvas({ images: resource.items.map(r => ({ resource: r })) }, this.canvas.options)
80+
.getImages().map((img, index) => {
81+
const r = img.getResource();
82+
if (r) {
83+
r.preferred = !index;
84+
}
85+
return r;
86+
});
87+
}
88+
case 'oa:Choice': {
89+
return new Canvas({ images: flattenDeep([resource.getProperty('default'), resource.getProperty('item')]).map(r => ({ resource: r })) }, this.canvas.options).getImages()
90+
.map((img, index) => {
91+
const r = img.getResource();
92+
if (r) {
93+
r.preferred = !index;
94+
}
95+
return r;
96+
});
97+
}
98+
default: {
99+
const r = resource;
100+
r.preferred = true;
101+
return r;
102+
}
80103
}
81104
}));
82105
}

src/locales/ar/translation.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@
6161
"language": "اللغة",
6262
"layer_hide": "إخفاء الطبقة",
6363
"layer_move": "تحريك الطبقة",
64-
"layer_moveToTop": "حرك الطبقة إلى الأعلى",
6564
"layer_opacity": "تعتيم الطبقة",
6665
"layer_show": "إظهار الطبقة",
6766
"layers": "طبقات",

src/locales/bg/translation.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@
7171
"language": "Език",
7272
"layer_hide": "Скриване на слой",
7373
"layer_move": "Преместване на слой",
74-
"layer_moveToTop": "Преместване на слой най-отгоре",
7574
"layer_opacity": "Прозрачност на слой",
7675
"layer_show": "Показване на слой",
7776
"layers": "Слоеве",

src/locales/de/translation.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@
7171
"language": "Sprache",
7272
"layer_hide": "Ebene verbergen",
7373
"layer_move": "Ebene verschieben",
74-
"layer_moveToTop": "Ebene ganz nach vorn bringen",
74+
"layer_moveToBackground": "Ebene in der Hintergrund verschieben",
75+
"layer_moveToFront": "Ebene ganz nach vorn bringen",
7576
"layer_opacity": "Ebenendeckkraft",
7677
"layer_show": "Ebene anzeigen",
7778
"layers": "Ebenen",

0 commit comments

Comments
 (0)