ProjectMirador / mirador

An open-source, web-based 'multi-up' viewer that supports zoom-pan-rotate functionality, ability to display/compare simple images, and images with annotations.
https://projectmirador.org
Apache License 2.0
558 stars 255 forks source link

Mirador API Requirements (Developer UX Specification) #1693

Closed aeschylus closed 3 years ago

aeschylus commented 5 years ago

This ticket is intended to be a gathering place for many other issues, but I want to ensure we have properly captured all the desired API functionality in detail. In practice, these areas all need to be realised as integration tests, and imply we get our distribution channels handled. My hope is that this becomes the seed of our comprehensive API documentation, and the One True Thread for debate on the external API surface area. I will edit this comment as discussion continues below.

Prior Art

These are some previous attempts to organise feedback on the shape of the Mirador API surface area and capabilities. [wiki page on embedding/import contexts]() [plugin types]() Known Consuming Contexts Known Mirador Plugins

Deployment Contexts

Mirador is a javascript library to be included in several different kinds of software contexts. These slides from the Mirador Community Update in Washington D.C. last year sums up the various contexts where Mirador needs to run as a library. image

image

These contexts and their attendant use cases will determine the shape of its API surface area, which may not be identical for each context. Our job is to make it a pleasure to use (as a developer) from all four of these different perspectives.

Packaging/Publication

In general, we want to publish the main package with NPM

NPM

Full Bundle

Developer Experience

The developer types npm install mirador from their project folder.
In their project Code, they can now type

import Mirador from `mirador`;

const viewer = Mirador({id:'container'});

Where the DOM node with an id container could come from a jade template, for example.

How should this work in rails? How should this work within a react application?

import Mirador from `mirador`;

Tree-shaken sub-bundles

CDN

It should be possible to grab the total Mirador bundle, or any of its sub-bundles, from a CDN for creating and sharing examples in sandbox websites, static pages, presentations, or local use. Example

<script crossorigin src="https://unpkg.com/mirador/umd/mirador.min.js"></script>

Configuration

Configuration should be human-readable and easily writable. An average webmaster should be able to easily tweak the configuration after reading a few examples.

Theming

More comprehensive theming usecases are gathered in #1649.

Javascript Runtime API

There should be a runtime API provided by the viewer instance that provides access to the redux store and actions, as well as a mechanism for dispatching and listening for events that occur in the viewer without having to learn redux.

Redux Store and Actions Access

Event Pub/Sub Mechanism

As a developer, I want to be able to call functions that initiate certain events in the viewer. This is necessary for creating embedded experiences in IMS LTI environments like Canvas and Blackboard, HarvardX, etc., or DH projects that provide a "guided tour"-like presentation of content. The verbosity of viewer.store.dispatch(viewer.store.actions.nextCanvas({windowId: '1234', canvasId: "my-really-long-canvas-id")) is annoying; I've never heard of redux, I don't know what stores are, or why I'm calling a function inside a dispatch thingy. More relatable would be viewer.nextCanvas('my-probably-long-window-id'). I need to easily:

I also need my containing page to receive notice when things in the viewer change. For example, a multi-spectral imaging program or museum conservation system may wish to display a histogram of the colours sampled at a given mouse position. I will therefore need the IIIF canvas coordinate of the current mouse pointer to retrieve accurate data from my backend. I want to listen specifically to:

Required Examples

Documentation is deeply improved by using examples. We should have hosted, tweakable, forkable, shareable examples of all the major setup and common customisation questions. Platforms like Observable and codePen help developers get a concrete grasp on and easy starting point for their problems and questions. We should endeavour to provide a platform for example-making as a community norm by publishing clear and well-structured examples for the following use-cases:

rsinghal commented 5 years ago

Event pub/subs that are needed by Harvard's annotation tool: #1865

charbugs commented 5 years ago

I think the idea of event types can help us to overcome the problem of event notification that came up in the recent comunity call.

Here is the problem (please correct me if I got it wrong): Adopters to Mirador should be able to listen to actions/changes in the store by providing custom reducers through the plugin API. Doing so, they can react on certain action types. But there is no easy way to find out what exactly has changed.

For example, a adopter wants to know if a window with a certain manifest has been opened. She can intercept a OPEN_WINDOW action in her custom reducer. But the manifest URL may not be provided in this type of action. So she has to digg in the state and that implies a knowledge of the internal structure of the state.

With event types we can provide the adopters with data that we consider as usefull for the event. In case of the open window example we could pass the manifest url and canvas index:

// in an event library
export function newWindowOpened(manifestUrl, canvasIndex) {
  return { type: 'EVENT_NEW_WINDOW_OPENED', manifestUrl, canvasIndex }
}

// in an action creator (a thunk here)
export function openNewWindow(manifestId) {
  return function (dispatch, getState) {
    const windowId = dispatch(actions.createWindow({ manifestId })).id
    const { manifestUrl } = getState().manifests[manifestId]
    const { canvasIndex } = getState().windows[windowId]
    // fire the event
    dispatch(events.newWindowOpened(manifestUrl, canvasIndex))
  }
}

Event types were only for information and hold some data, but they wouldn't have any effect on the state.

This way, we could precisely determine at which point we consider an event finalized and ready to fire. We also would not have to care about whether our action type cascades match the needs of the adopters. We would have an isolated event API.

aeschylus commented 5 years ago

@charbugs points out a needed revision of the plugin system to also allow plugins to be passed in on instantiation of the viewer (both in react and in the API-based functional initiation) https://github.com/ProjectMirador/mirador/issues/2082#issuecomment-470619573

I've added this as a requirement to the documentation ticket at the top of this thread, and created a more general ticket for figuring that out here: https://github.com/ProjectMirador/mirador/issues/2082#issuecomment-470619573

jeffreycwitt commented 5 years ago

I wanted to chime here just to add my own outside impression of how important the developer API experience is. (These notes are also intended to be a summary of the discussion @aeschylus @sdellis and I @jeffreycwitt had on Friday April 12 as we experimented with Mirador Imports and Plugin Creations.

As Mirador3 progress and we now have a bundle we can import, people like me and others outside the core development team are able to start consuming the Mirador library.

One benefit of treating Mirador3 as react component is how easy it is (or should be) to bring Mirador into other react applications.

I've followed @mejackreed instructions for doing this, and it looks like @gigamorph has does this as well here: https://github.com/ProjectMirador/mirador/wiki/M3-Embedding-in-Another-Environment

A couple notes about this:

First, the general import seems to work well, but I think it should be noted that the import pattern is a little different than most react third party libraries. The "react" intuition would, I think, be to import "Mirador" from "mirador" and then be able to drop the Mirador component into the desired place. The need to reconstruct the "provider" and "store" seems like an un-intuitive step that will be a hang-up for a lot of people downstream. In a sense, to integrate Mirador into a react app, it seems like we have to rebuild part of the Mirador itself, basically repeat some of the code steps found in https://github.com/ProjectMirador/mirador/blob/master/src/lib/MiradorViewer.js#L28-L32. Maybe this is necessary, but I also feels kind of redundant.

Second, while the above is more an aesthetic or comfort problem, the current embed instructions and Plugin system seems to make it difficult or even impossible to embed Mirador into a wrapping react app with accompanying plugins.

If one is building a react application, in which they want to embed Mirador as a third part react app, but also want to instantiate that Mirador instance with a certain set of plugins (or even dynamically construct Mirador with a certain said of plugins), there doesn't seem to be a way to do this.

I would imagine doing something like:

import "Mirador" from "mirador"
import "PluginOne" from "plugin-one"
import "PluginTwo" from "plugin-two"

render(){
   return (
<div>
<Mirador plugins={[Plugin1, Plugin2]}>
)
}

However at present it seem like the only way to add plugins is through the functional API that calls MiradorViewer.

The only way then that I can see to add several plugins is to build a new Mirador Bundle with accompanying plugins and then import my custom Mirador Fork into my larger react app wrapper.

Perhaps that is possible, but it is inconvenient. Now I can no longer import mirador and the plugins and I want directly from their maintained repos. I have to build an intermediate Mirador bundle and maintain this as well as the app I'm trying to build. It just seems like this is creating some friction that will make for a slightly frustrating developer experience down the road.

(As I think about it now, maybe it would be possible to simply import the PluginProvider and then wrap just as the provider was imported. If that would work, then my second concern would mostly go away, and would be left only with the aesthetic or ease problem described in number 1 above, namely, that it takes several imports and detailed component arrangements to actually embed the mirador react component.)

charbugs commented 5 years ago

@jeffreycwitt There is another way to embedd Mirador in a react application that is not covered in the wiki page. Not sure, but I think it solves the issue with plugins you described. Here's an example: https://codesandbox.io/s/4x4vn4o4m4

In general, I agree that there is work to do to make the embedding of Mirador more convenient. Please note that we want to support different usage contexts like embedding via script tag in HTML, using npm, react and non-react apps etc.