Skip to main content

Handling Choice

A Canvas have have multiple images. Sometimes, they are all a part of the scene to be rendered and the developer doesn't have to do anything extra - Canvas Panel will just render the scene.

But sometimes, the multiple images form a set of choices, for the user to pick from. Multispectral images are an example of this.

For further details on this scenario, see the IIIF Cookbook Choice example. (TODO - replace with published link)

You don't necessarily need to know in advance that the Canvas has resources with Choice for something to render. In simple scenarios, you might not need to know, because there's nothing you can do about it anyway: let canvas panel make a sensible default choice, because anything more means you need to provide more user interface to deal with the user making one or more choices. At the other end, your app might be sophisticated and allow blending of layers, e.g., to view multispectral captures of paintings.

For the more sophisticated UI, you need to react to the presence of one or more Choices on the canvas. You need to know what choices are available - their labels, ids and their "stacking" order.

You can then render UI for offering the choices to the user - and react to that choice by telling Canvas Panel to update the scene to reflect changes. Your UI component might be bound to Canvas Panel.

You might also support blending: "Show the choice with id xxx at 80% opacity AND show the choice with id yyy at 50% opacity, all the others are at 0%".

Usually, if Choice is present at all, it's only one set of choices, for the whole canvas. But it's possible for a scene to be made of multiple content resources each of which have their own set of choices.

Simple scenario - known choice

<canvas-panel iiif-content="http://example.org/canvas-1.json" choice-id="http://example.org/choice-1" />
<canvas-panel 
    manifest-id="https://preview.iiif.io/cookbook/3333-choice/recipe/0033-choice/manifest.json"
    canvas-id="https://preview.iiif.io/cookbook/3333-choice/recipe/0033-choice/canvas/p1"
    choice-id="https://iiif.io/api/image/3.0/example/reference/421e65be2ce95439b3ad6ef1f2ab87a9-dee-xray/full/max/0/default.jpg" 
/>
<!-- Try changing the value of choice-id to this:
https://iiif.io/api/image/3.0/example/reference/421e65be2ce95439b3ad6ef1f2ab87a9-dee-natural/full/max/0/default.jpg
-->

Here the value of choice-id is the id of the content resource within a set of choices. You don't need to specify which set of choices, in the rare event that there is more than one - although you can specify more than one value:

<canvas-panel iiif-content="http://example.org/canvas-1.json" 
choice-id="http://example.org/choice-set-a/3, http://example.org/choice-set-b/7" />

Before loading

Before you put the <canvas-panel /> web component on a page, you can first load the manifest into the vault, find out if there is a choice and render a UI.

In this example, the "choice" event is fired when Canvas Panel detects that a Choice is present on the rendered Canvas.

tip

The choice event may be fired multiple times as Canvas Panel loads

import '@digirati/canvas-panel-web-components';
import { getValue } from '@iiif/vault-helpers';
import './styles.css';

async function load() {
  const cp = document.getElementById("cp");
  await cp.vault.loadManifest("https://preview.iiif.io/cookbook/3333-choice/recipe/0033-choice/manifest.json");
  cp.addEventListener("choice", (e) => {
    let msg = "  Choices: ";
    for (const choice of e.detail.choice.items) {
      msg += "  Choice: " + getValue(choice.label) + "\n";
      msg += "   - Id: " + choice.id + "\n";
    }
    document.getElementById("pseudoUI").innerText = msg;
  });
  cp.setCanvas("https://preview.iiif.io/cookbook/3333-choice/recipe/0033-choice/canvas/p1");
}

load();

Canvas panel also has the additional helpers for scenarios where you don't want to react to the choice event:

cp.setDefaultChoiceIds(ids);
cp.makeChoice(id, options)

And you can render a choice directly, with opacity, via attributes (e.g., if generating the markup on the server):

<canvas-panel iiif-content="http://example.org/canvas-1.json" choice-id="http://example.org/choice-1#opacity=0.5" />

Choice React Example

This is a more realistic use of Canvas Panel's choice-handling capability:

import { useLayoutEffect, useRef, useState } from "react";
import "./styles.css";
import "@digirati/canvas-panel-web-components";

export default function App() {
  const viewer = useRef();
  const [choice, setChoice] = useState();
  const disabledChoice = choice
    ? choice.items.filter((i) => i.selected).length === 1
    : false;

  useLayoutEffect(() => {
    viewer.current.addEventListener("choice", (e) => {
      setChoice(e.detail.choice);
    });
  }, []);

  return (
    <div className="App">
      {choice
        ? choice.items.map((item, idx) => {
            return (
              <div key={item.id}>
                <input
                  type="checkbox"
                  disabled={disabledChoice && item.selected}
                  onChange={(e) => {
                    if (idx !== 0 && disabledChoice) {
                      // Select the first (default) choice when another choice is made.
                      viewer.current.makeChoice(choice.items[0].id, {
                        deselectOthers: false,
                      });
                    }
                    viewer.current.makeChoice(item.id, {
                      deselect: item.selected,
                      deselectOthers: false
                    });
                  }}
                  checked={item.selected}
                />
                <strong>{item.label.en.join("")}</strong>
                <input
                  type="range"
                  min={0}
                  max={100}
                  defaultValue={100}
                  onChange={(e) => {
                    viewer.current.applyStyles(item.id, {
                      opacity: e.target.value / 100
                    });
                  }}
                />
              </div>
            );
          })
        : null}

      <canvas-panel
        ref={viewer}
        // choice-id={`https://iiif.io/api/image/3.0/example/reference/421e65be2ce95439b3ad6ef1f2ab87a9-dee-xray/full/max/0/default.jpg#opacity=0.5,https://iiif.io/api/image/3.0/example/reference/421e65be2ce95439b3ad6ef1f2ab87a9-dee-natural/full/max/0/default.jpg#opacity=0.25`}
        iiif-content="JTdCJTIyaWQlMjIlM0ElMjJodHRwcyUzQSUyRiUyRnByZXZpZXcuaWlpZi5pbyUyRmNvb2tib29rJTJGMzMzMy1jaG9pY2UlMkZyZWNpcGUlMkYwMDMzLWNob2ljZSUyRmNhbnZhcyUyRnAxJTIzeHl3aCUzRDgzNyUyQzkxMyUyQzc3MSUyQzMzNCUyMiUyQyUyMnR5cGUlMjIlM0ElMjJDYW52YXMlMjIlMkMlMjJwYXJ0T2YlMjIlM0ElNUIlN0IlMjJpZCUyMiUzQSUyMmh0dHBzJTNBJTJGJTJGcHJldmlldy5paWlmLmlvJTJGY29va2Jvb2slMkYzMzMzLWNob2ljZSUyRnJlY2lwZSUyRjAwMzMtY2hvaWNlJTJGbWFuaWZlc3QuanNvbiUyMiUyQyUyMnR5cGUlMjIlM0ElMjJNYW5pZmVzdCUyMiU3RCU1RCU3RA"
      />
    </div>
  );
}

Additional choice helper API

There is an additional helper that can be used to extract choices.

import { createPaintingAnnotationsHelper } from '@iiif/vault-helpers';

const helper = createPaintingAnnotationsHelper(element.vault);
const choice = helper.extractChoices("http://example.org/manifest/canvas-1.json");
// Choice looks like this:
// {
// type: 'single-choice';
// label?: InternationalString;
// items: Array<{
// id: string;
// label?: InternationalString;
// selected?: true;
// }>
// }

You might want to analyse the canvas even earlier, to decide what UI to render. You need to ensure that the Manifest is loaded before you extract the choices for your canvas.

Using this helper gives complete flexibility over choices at the data level, and can happen before anything is rendered to the user.

Since the choice-id attribute also drives the users choice, I could do the following to manually set the choice ID.

element.setAttribute('choice-id', 'http://example.org/choice-1')

// or even

element.setAttribute('choice-id', 'http://example.org/choice-1#opacity=20')
Discuss on GitHub