Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solve vertical display order and Choice behavior, fix #3693 and #3585 #4015

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
29 changes: 25 additions & 4 deletions __tests__/integration/mirador/mirador-configs/layers.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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' },
],
};
41 changes: 30 additions & 11 deletions __tests__/src/components/CanvasLayers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down Expand Up @@ -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': {
Expand Down
4 changes: 2 additions & 2 deletions __tests__/src/lib/CanvasWorld.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
54 changes: 54 additions & 0 deletions __tests__/src/lib/MiradorCanvas.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,58 @@ 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);
});
});
});
37 changes: 28 additions & 9 deletions src/components/CanvasLayers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 || {}),
};

Expand All @@ -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>
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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) => {
Expand All @@ -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 && (
Expand Down Expand Up @@ -279,7 +297,8 @@ export function CanvasLayers({
layerMetadata={(layerMetadata || {})[r.id] || {}}
handleOpacityChange={handleOpacityChange}
setLayerVisibility={setLayerVisibility}
moveToTop={moveToTop}
moveToBackground={moveToBackground}
moveToFront={moveToFront}
/>
</DraggableLayer>
))
Expand Down
11 changes: 6 additions & 5 deletions src/lib/CanvasWorld.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
));
Expand All @@ -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,
};
}
Expand All @@ -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;
}

/**
Expand Down
35 changes: 29 additions & 6 deletions src/lib/MiradorCanvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}));
}
Expand Down
1 change: 0 additions & 1 deletion src/locales/ar/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
"language": "اللغة",
"layer_hide": "إخفاء الطبقة",
"layer_move": "تحريك الطبقة",
"layer_moveToTop": "حرك الطبقة إلى الأعلى",
"layer_opacity": "تعتيم الطبقة",
"layer_show": "إظهار الطبقة",
"layers": "طبقات",
Expand Down
1 change: 0 additions & 1 deletion src/locales/bg/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@
"language": "Език",
"layer_hide": "Скриване на слой",
"layer_move": "Преместване на слой",
"layer_moveToTop": "Преместване на слой най-отгоре",
"layer_opacity": "Прозрачност на слой",
"layer_show": "Показване на слой",
"layers": "Слоеве",
Expand Down
3 changes: 2 additions & 1 deletion src/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading