Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 5-images-in-annotations
  • 61-recettage-des-outils-d-annotation
  • 69-la-video-demare-quand-on-fait-glisser-le-slider-et-le-clic-creer-un-decalage-entre-le-player
  • 75-dernieres-ameliorations-avant-workshop-du-7-02
  • autorisation_un_pannel_annotation
  • autorisation_un_pannel_edition_annotation
  • fix-error-create-annotation-pannel
  • fix-poc-mirador
  • gestion_multiple_ouverture_pannel_annotation
  • master
  • mui5-tetras-main-old-stable
  • mui5-tetras-main-stable
  • preprod
  • récupération_temps_video
  • save-shapes-and-position
  • tetras-antho-test
  • tetras-main
  • time-saving-on-annotation
  • uploads-file
  • wip-annot-video-ui
  • wip-fix-xywh
  • wip-positionement-annot
  • wip-surface-transformer
23 results

Target

Select target project
No results found
Select Git revision
  • 1-edit-annotations-on-videos
  • 5-chpk-images-in-annot
  • 5-final-images
  • 5-images-in-annotations
  • 5-old-images-in-annotations
  • 5-rebase-images-in-annot
  • 5-wip-images-in-annot
  • demo_ci_gitlab_pages
  • demo_gitlab_ci
  • devsetup
  • images_annotations
  • master
  • old_demo_ci_gitlab_pages
  • tetras-main
  • tmp
  • v0.4.0_react16
  • wip
  • wip-annot-video-ui
  • wip-annotations-on-videos
  • wip-debugging-annotations
20 results
Show changes

Commits on Source 352

252 additional commits have been omitted to prevent performance issues.
61 files
+ 13904
8707
Compare changes
  • Side-by-side
  • Inline

Files

.env.sample

0 → 100644
+14 −0
Original line number Original line Diff line number Diff line
# Use this variable to add configurations :
# docker-compose.yml : required
# ports.yml : bind the port 300 to the $PORT variable
# traefik.yml : add traefik configurations
COMPOSE_FILE=docker-compose.yml:docker/ports.yml
# Choose between "dev" and "prod"
ENV=dev
# If you use docker/ports.yml
PORT=3000
# If you use docker/traefik.yml
# A unique name for traefik router
NAME=
# A traefik host rule ex `domain.FQDN` or `domain1.FQDN`,`domain2.FQDN`
HOST=`domain.fqdn`
+5 −1
Original line number Original line Diff line number Diff line
@@ -10,6 +10,10 @@
  "parser": "@babel/eslint-parser",
  "parser": "@babel/eslint-parser",
  "plugins": ["jest"],
  "plugins": ["jest"],
  "rules": {
  "rules": {
    "import/no-extraneous-dependencies": [
    "error",
{"devDependencies": true}
],
    "import/prefer-default-export": "off",
    "import/prefer-default-export": "off",
    "no-console": "off",
    "no-console": "off",
    "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
    "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
+4 −0
Original line number Original line Diff line number Diff line
@@ -5,3 +5,7 @@
/node_modules
/node_modules
/umd
/umd
npm-debug.log*
npm-debug.log*
.idea
.package-lock.json
.env
.*.sw?

.gitmodules

0 → 100644
+3 −0
Original line number Original line Diff line number Diff line
[submodule "mirador"]
	path = mirador
	url = https://github.com/ProjectMirador/mirador/tree/mui5.git

.nvmrc

0 → 100644
+1 −0
Original line number Original line Diff line number Diff line
16.20.2
 No newline at end of file

LICENSE

0 → 100644
+674 −0

File added.

Preview size limit exceeded, changes collapsed.

+81 −14
Original line number Original line Diff line number Diff line
# mirador-annotations
ARCHIVED IN FAVOR OF https://github.com/SCENE-CE/mirador-annotation-editor


[![Travis][build-badge]][build]
# Mirador Annotation Editor - GPL edition
[![npm package][npm-badge]][npm]
[![Coveralls][coveralls-badge]][coveralls]


`mirador-annotations` is a [Mirador 3](https://github.com/projectmirador/mirador) plugin that adds annotation creation tools to the user interface. Users can` create rectangle, oval, and polygon annotations and add text descriptors. A [live demo](https://mirador-annotations.netlify.app/) that stores annotations in local storage is available for testing. See the [issue queue](https://github.com/ProjectMirador/mirador-annotations/issues) for design proposals for additional functionality.
## Presentation


![annotation creation panel](https://user-images.githubusercontent.com/5402927/86628717-23c3ae80-bf7f-11ea-8f0b-389c39eb4398.png)
### Generalities

`mirador-annotation-editor` is a [Mirador 3](https://github.com/projectmirador/mirador) plugin that adds annotation creation tools to the user interface. 

It is based on the original [mirador-annotations](https://github.com/ProjectMirador/mirador-annotations/) plugin with a lot of technical and functional modifications.

### Copyrights

#### Licence

Unlike the original [mirador-annotations](https://github.com/ProjectMirador/mirador-annotations/) plugin, this `mirador-annotation-editor` is distributed under the **GPL v3**.

Please acknoldge that any modification you make must be distributed under a compatible licence and cannot be closed source.

If you need to integrate this code base in closed source pieces of software, please contact us so we can discuss dual licencing. 

#### Property

The base of this software (up to V1) is the property of [SATT Ouest Valorisation](https://www.ouest-valorisation.fr/) that funded its development under the french public contract AO-MA2023-0004-DV5189.

#### Authors 

The authors of this software are :

- Clarisse Bardiot (concept and use cases)
- Jacob Hart (specifications)
- [Tétras Libre SARL](https://tetras-libre.fr) (development):
  - David Rouquet
  - Anthony Geourjon
  - Antoine Roy

#### Contributors (updated february 2024)

- AZOPSOFT SAS 
  - Samuel Jugnet (especially code for the Konvas part)
- Loïs Poujade (especially the original modifications to anotate videos)

### General functionatities 

- Activate a pannel with tools to create annotations on IIIF documents (manifests) containing images **and videos**
- Spatial and temporal targets for annotations
- Overlay annotations (geometric forms, free hand drawing, text and images)
- Textual/semantic annotations and tags
- Annotation metadata (based on Dublin Core)
- Annotation with anoter manifest -> network of IIIF documents

### Technical aspects 

- Update to Material UI 5 and React 18 to follow latest Mirador upgrades (React 17 release also available)
- The [paperjs](http://paperjs.org/ ) library has been replaced with [Konvas](https://konvajs.org) 
- Major refactorisation since the original `[mirador-annotations](https://github.com/ProjectMirador/mirador-annotations/) plugins`
- Works with the original [Mirador 3](https://github.com/projectmirador/mirador) if you need only image annotation
- If you need video annotation, you can use [our fork of Mirador: mirador-video](https://gitlab.tetras-libre.fr/iiif/mirador/mirador-video)


## Install (local)

This method requires `nvm`, `npm`.

```
git clone gitlab@gitlab.tetras-libre.fr:iiif/mirador/mirador-annotations.git
cd mirador-annotations
nvm use
npm install
```
NPM Install throw two errors  (https://gitlab.tetras-libre.fr/iiif/mirador/mirador-annotations/-/issues/12). To fix run : 

```
./cli post_install
```

Run mirador and the plugin :

```
npm start
```


## Persisting Annotations
## Persisting Annotations
Persisting annotations requires implementing an a IIIF annotation server. Several [examples of annotation servers](https://github.com/IIIF/awesome-iiif#annotation-servers) are available on iiif-awesome.
Persisting annotations requires implementing an a IIIF annotation server. Several [examples of annotation servers](https://github.com/IIIF/awesome-iiif#annotation-servers) are available on iiif-awesome.
@@ -17,14 +90,8 @@ Persisting annotations requires implementing an a IIIF annotation server. Severa


`mirador-annotations` requires an instance of Mirador 3. See the [Mirador wiki](https://github.com/ProjectMirador/mirador/wiki) for examples of embedding Mirador within an application. See the [live demo's index.js](https://github.com/ProjectMirador/mirador-annotations/blob/master/demo/src/index.js) for an example of importing the `mirador-annotations` plugin and configuring the adapter.
`mirador-annotations` requires an instance of Mirador 3. See the [Mirador wiki](https://github.com/ProjectMirador/mirador/wiki) for examples of embedding Mirador within an application. See the [live demo's index.js](https://github.com/ProjectMirador/mirador-annotations/blob/master/demo/src/index.js) for an example of importing the `mirador-annotations` plugin and configuring the adapter.


**You must use node v16.20.2**. You can `run nvm use` at the racine of the project to set your node version to 16.20.2.

## Contribute
## Contribute
Mirador's development, design, and maintenance is driven by community needs and ongoing feedback and discussion. Join us at our regularly scheduled community calls, on [IIIF slack #mirador](http://bit.ly/iiif-slack), or the [mirador-tech](https://groups.google.com/forum/#!forum/mirador-tech) and [iiif-discuss](https://groups.google.com/forum/#!forum/iiif-discuss) mailing lists. To suggest features, report bugs, and clarify usage, please submit a GitHub issue.
Mirador's development, design, and maintenance is driven by community needs and ongoing feedback and discussion. Join us at our regularly scheduled community calls, on [IIIF slack #mirador](http://bit.ly/iiif-slack), or the [mirador-tech](https://groups.google.com/forum/#!forum/mirador-tech) and [iiif-discuss](https://groups.google.com/forum/#!forum/iiif-discuss) mailing lists. To suggest features, report bugs, and clarify usage, please submit a GitHub issue.
[build-badge]: https://img.shields.io/travis/user/repo/master.png?style=flat-square
[build]: https://travis-ci.org/user/repo

[npm-badge]: https://img.shields.io/npm/v/mirador-annotations.png?style=flat-square
[npm]: https://www.npmjs.org/package/mirador-annotations

[coveralls-badge]: https://img.shields.io/coveralls/user/repo/master.png?style=flat-square
[coveralls]: https://coveralls.io/github/user/repo
Original line number Original line Diff line number Diff line
@@ -4,6 +4,7 @@ import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';
import AnnotationCreation from '../src/AnnotationCreation';
import AnnotationCreation from '../src/AnnotationCreation';
import AnnotationDrawing from '../src/AnnotationDrawing';
import AnnotationDrawing from '../src/AnnotationDrawing';
import TextEditor from '../src/TextEditor';
import TextEditor from '../src/TextEditor';
import ImageFormField from '../src/ImageFormField';


/** */
/** */
function createWrapper(props) {
function createWrapper(props) {
@@ -36,6 +37,10 @@ describe('AnnotationCreation', () => {
    wrapper = createWrapper();
    wrapper = createWrapper();
    expect(wrapper.dive().find(TextEditor).length).toBe(1);
    expect(wrapper.dive().find(TextEditor).length).toBe(1);
  });
  });
  it('adds the ImageFormField component', () => {
    wrapper = createWrapper();
    expect(wrapper.dive().find(ImageFormField).length).toBe(1);
  });
  it('can handle annotations without target selector', () => {
  it('can handle annotations without target selector', () => {
    wrapper = createWrapper({
    wrapper = createWrapper({
      annotation: {
      annotation: {
Original line number Original line Diff line number Diff line
@@ -3,13 +3,14 @@ import WebAnnotation from '../src/WebAnnotation';
/** */
/** */
function createSubject(args = {}) {
function createSubject(args = {}) {
  return new WebAnnotation({
  return new WebAnnotation({
    body: 'body',
    body: {
      value: 'body',
    },
    canvasId: 'canvasId',
    canvasId: 'canvasId',
    fragsel: { t: '5,10', xywh: 'xywh' },
    id: 'id',
    id: 'id',
    svg: 'svg',
    svg: 'svg',
    tags: ['tags'],
    tags: ['tags'],
    timing: [1, 3],
    xywh: 'xywh',
    ...args,
    ...args,
  });
  });
}
}
@@ -18,14 +19,19 @@ describe('WebAnnotation', () => {
  let subject = createSubject();
  let subject = createSubject();
  describe('constructor', () => {
  describe('constructor', () => {
    it('sets instance accessors', () => {
    it('sets instance accessors', () => {
      ['body', 'canvasId', 'id', 'svg', 'xywh'].forEach((prop) => {
      ['canvasId', 'id', 'svg'].forEach((prop) => {
        expect(subject[prop]).toBe(prop);
        expect(subject[prop]).toBe(prop);
      });
      });
      expect(subject.timing).toStrictEqual([1, 3]);
      expect(subject.fragsel).toStrictEqual({ t: '5,10', xywh: 'xywh' });
    });
    it('sets instance accessors for body', () => {
      ['body'].forEach((prop) => {
        expect(subject[prop].value).toBe(prop);
      });
    });
    });
  });
  });
  describe('target', () => {
  describe('target', () => {
    it('with svg, xywh and timing', () => {
    it('with svg and xywh', () => {
      expect(subject.target()).toEqual({
      expect(subject.target()).toEqual({
        selector: [
        selector: [
          {
          {
@@ -34,18 +40,14 @@ describe('WebAnnotation', () => {
          },
          },
          {
          {
            type: 'FragmentSelector',
            type: 'FragmentSelector',
            value: 'xywh=xywh',
            value: 't=5,10&xywh=xywh',
          },
          {
            type: 'FragmentSelector',
            value: 't=1,3',
          },
          },
        ],
        ],
        source: 'canvasId',
        source: 'canvasId',
      });
      });
    });
    });
    it('with svg only', () => {
    it('with svg only', () => {
      subject = createSubject({ timing: null, xywh: null });
      subject = createSubject({ fragsel: null });
      expect(subject.target()).toEqual({
      expect(subject.target()).toEqual({
        selector: {
        selector: {
          type: 'SvgSelector',
          type: 'SvgSelector',
@@ -54,8 +56,28 @@ describe('WebAnnotation', () => {
        source: 'canvasId',
        source: 'canvasId',
      });
      });
    });
    });
    it('with time interval only', () => {
      subject = createSubject({ fragsel: { t: '5,10' }, svg: null });
      expect(subject.target()).toEqual({
        selector: {
          type: 'FragmentSelector',
          value: 't=5,10',
        },
        source: 'canvasId',
      });
    });
    it('with time interval only - xywh present but null', () => {
      subject = createSubject({ fragsel: { t: '5,10', xywh: null }, svg: null });
      expect(subject.target()).toEqual({
        selector: {
          type: 'FragmentSelector',
          value: 't=5,10',
        },
        source: 'canvasId',
      });
    });
    it('with xywh only', () => {
    it('with xywh only', () => {
      subject = createSubject({ svg: null, timing: null });
      subject = createSubject({ fragsel: { xywh: 'xywh' }, svg: null });
      expect(subject.target()).toEqual({
      expect(subject.target()).toEqual({
        selector: {
        selector: {
          type: 'FragmentSelector',
          type: 'FragmentSelector',
@@ -64,18 +86,18 @@ describe('WebAnnotation', () => {
        source: 'canvasId',
        source: 'canvasId',
      });
      });
    });
    });
    it('with timing only', () => {
    it('with xywh only - time interval present but null', () => {
      subject = createSubject({ svg: null, xywh: null });
      subject = createSubject({ fragsel: { t: null, xywh: 'xywh' }, svg: null });
      expect(subject.target()).toEqual({
      expect(subject.target()).toEqual({
        selector: {
        selector: {
          type: 'FragmentSelector',
          type: 'FragmentSelector',
          value: 't=1,3',
          value: 'xywh=xywh',
        },
        },
        source: 'canvasId',
        source: 'canvasId',
      });
      });
    });
    });
    it('with no xywh, svg or timing', () => {
    it('with no xywh or svg', () => {
      subject = createSubject({ svg: null, timing: null, xywh: null });
      subject = createSubject({ fragsel: null, svg: null });
      expect(subject.target()).toBe('canvasId');
      expect(subject.target()).toBe('canvasId');
    });
    });
  });
  });
@@ -94,20 +116,40 @@ describe('WebAnnotation', () => {
      ]);
      ]);
    });
    });
    it('with text only', () => {
    it('with text only', () => {
      subject = createSubject({ tags: null });
      subject = createSubject({ image: null, tags: null });
      expect(subject.createBody()).toEqual({
      expect(subject.createBody()).toEqual({
        type: 'TextualBody',
        type: 'TextualBody',
        value: 'body',
        value: 'body',
      });
      });
    });
    });
    it('with tags only', () => {
    it('with tags only', () => {
      subject = createSubject({ body: null });
      subject = createSubject({ body: null, image: null });
      expect(subject.createBody()).toEqual({
      expect(subject.createBody()).toEqual({
        purpose: 'tagging',
        purpose: 'tagging',
        type: 'TextualBody',
        type: 'TextualBody',
        value: 'tags',
        value: 'tags',
      });
      });
    });
    });
    it('with image and text', () => {
      subject = createSubject({ body: { value: 'hello' }, image: { id: 'http://example.photo/pic.jpg' }, tags: null });
      expect(subject.createBody()).toEqual([
        {
          type: 'TextualBody',
          value: 'hello',
        },
        {
          id: 'http://example.photo/pic.jpg',
          type: 'Image',
        },
      ]);
    });
    it('with image only', () => {
      subject = createSubject({ body: null, image: { id: 'http://example.photo/pic.jpg' }, tags: null });
      expect(subject.createBody()).toEqual({
        id: 'http://example.photo/pic.jpg',
        type: 'Image',
      });
    });
  });
  });
  describe('toJson', () => {
  describe('toJson', () => {
    it('generates a WebAnnotation', () => {
    it('generates a WebAnnotation', () => {

assets/2rectangle.svg

0 → 100644
+18 −0
Original line number Original line Diff line number Diff line
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1215"
     height="684">
    <defs/>
    <g>
        <g>
            <path fill="rgb(255,0,0)" stroke="rgb(255,0,0)" paint-order="fill stroke markers"
                  d=" M 188 197.66666412353516 L 477 197.66666412353516 L 477 524.6666641235352 L 188 524.6666641235352 L 188 197.66666412353516 Z Z"
                  fill-opacity="0.5" stroke-opacity="0.5" stroke-miterlimit="10" stroke-width="12.65625"
                  stroke-dasharray=""/>
        </g>
        <g>
            <path fill="rgb(255,0,0)" stroke="rgb(255,0,0)" paint-order="fill stroke markers"
                  d=" M 625 194.66666412353513 L 916 194.66666412353513 L 916 577.6666641235352 L 625 577.6666641235352 L 625 194.66666412353513 Z Z"
                  fill-opacity="0.5" stroke-opacity="0.5" stroke-miterlimit="10" stroke-width="12.65625"
                  stroke-dasharray=""/>
        </g>
    </g>
</svg>

assets/cayenne.svg

0 → 100644
+16 −0
Original line number Original line Diff line number Diff line
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1215" height="684">
    <defs/>
    <g>
        <g>
            <path fill="rgb(255,0,0)" stroke="rgb(255,0,0)" d=" M 143 124.66666412353516 L 392 124.66666412353516 L 392 354.66666412353516 L 143 354.66666412353516 L 143 124.66666412353516 Z Z" fill-opacity="0.5" stroke-opacity="0.5" stroke-miterlimit="10" stroke-width="12.65625" stroke-dasharray=""/>
        </g>
        <g>
            <image width="1800" height="963" preserveAspectRatio="none" transform="matrix(0.3266922638266449 0 0 0.3266922638266448 478.0164251120414 122.79378743494328)" xlink:href="https://presskit.porsche.de/models/assets/images/e/P17_0701_a5_rgb-c04a4c6e.jpg"/>
        </g>
        <g>
            <g>
                <text fill="red" stroke="none" font-family="Arial" font-size="40px" font-style="normal" font-weight="normal" text-decoration="undefined" x="0" y="20" text-anchor="start" dominant-baseline="central" transform="matrix(0.6328125 0 0 0.6328125 332.99999999999994 538.6666641235352)">Mon gros Cayenne</text>
            </g>
        </g>
    </g>
</svg>

cli

0 → 100755
+41 −0
Original line number Original line Diff line number Diff line
#!/bin/bash

is_docker() {
    if [ ! -z "$(which docker-compose 2>/dev/null)" ];
    then
        echo "1"
    else
        echo "0"
    fi
}

usage() {
    echo -e "Usage $0 <command> [args]\n"
    echo -e "COMMANDS\n"
    echo "post_install"
    echo -e "\t Do post install tasks "
}

DIR="$(dirname $0)"
SCRIPTS_DIR="$DIR/public/scripts"

action=$1
shift
# Keep actions sorted
case $action in
    "post_install")
        rm -f ./mirador/node_modules/dnd-multi-backend/dist/index.js
        rm -f ./mirador/node_modules/react-dnd-multi-backend/dist/index.js
        cp ./post_install_assets/dnd-multi-backend/index.js node_modules/dnd-multi-backend/dist/index.js
        cp ./post_install_assets/react-dnd-multi-backend/index.js node_modules/react-dnd-multi-backend/dist/index.js
        ;;

    "help")
        usage
        ;;
    *)
        echo "ERROR: No command given"
        usage
        exit 1
        ;;
esac
+14 −10
Original line number Original line Diff line number Diff line
@@ -10,21 +10,25 @@ const config = {
    // adapter: (canvasId) => new AnnototAdapter(canvasId, endpointUrl),
    // adapter: (canvasId) => new AnnototAdapter(canvasId, endpointUrl),
    exportLocalStorageAnnotations: false, // display annotation JSON export button
    exportLocalStorageAnnotations: false, // display annotation JSON export button
  },
  },
  catalog: [
    { manifestId: 'https://files.tetras-libre.fr/manifests/re_walden_cut.json' },
    { manifestId: 'https://files.tetras-libre.fr/manifests/jf_peyret_re_walden.json' },
    { manifestId: 'https://files.tetras-libre.fr/manifests/test_markeas_manifest.json' },
    { manifestId: 'https://files.tetras-libre.fr/manifests/installation_fresnoy_manifest.json' },
    { manifestId: 'https://files.tetras-libre.fr/manifests/sceno_avignon_manifest.json' },
    { manifestId: 'https://files.tetras-libre.fr/manifests/walden_nouvel_manifest.json' },
    { manifestId: 'https://files.tetras-libre.fr/manifests/walden_nouvel2_manifest.json' },
    { manifestId: 'https://files.tetras-libre.fr/manifests/score_manifest.json' },
    { manifestId: 'https://files.tetras-libre.fr/manifests/program_manifest.json' },
  ],
  debugMode: true,
  id: 'demo',
  id: 'demo',
  window: {
  window: {
    defaultSideBarPanel: 'annotations',
    defaultSideBarPanel: 'annotations',
    sideBarOpenByDefault: true,
    sideBarOpenByDefault: true,
  },
  },
  catalog: [
  windows: [
    { manifestId: 'https://dzkimgs.l.u-tokyo.ac.jp/videos/iiif_in_japan_2017/manifest.json' },
  ],
    { manifestId: 'https://iiif.io/api/cookbook/recipe/0219-using-caption-file/manifest.json' },
    { manifestId: 'https://preview.iiif.io/cookbook/master/recipe/0003-mvm-video/manifest.json' },
    { manifestId: 'https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/manifest.json' },
    { manifestId: 'https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/manifest.json' },
    { manifestId: 'https://iiif.io/api/cookbook/recipe/0074-multiple-language-captions/manifest.json' },
    { manifestId: 'https://iiif.harvardartmuseums.org/manifests/object/299843' },
    { manifestId: 'https://iiif.io/api/cookbook/recipe/0002-mvm-audio/manifest.json' },
  ]
};
};


mirador.viewer(config, [...annotationPlugins]);
mirador.viewer(config, [...annotationPlugins]);

docker-compose.yml

0 → 100644
+10 −0
Original line number Original line Diff line number Diff line
version: "3"

services:
  front:
    build:
      context: "docker/"
    volumes:
      - ${PWD}:/app
    environment:
      ENV:

docker/Dockerfile

0 → 100644
+16 −0
Original line number Original line Diff line number Diff line
FROM node:16

EXPOSE 3000

VOLUME /app

WORKDIR /app

RUN npm install -g serve

COPY entrypoint.sh /

USER node

ENTRYPOINT ["/entrypoint.sh"]

docker/entrypoint.sh

0 → 100755
+20 −0
Original line number Original line Diff line number Diff line
#!/bin/bash

# Remove all node stuff to avoid arror on docker rebuild
rm -rf node_modules
rm -f package-lock.json
rm -f .babelrc
npm install
./cli post_install

if [ "$ENV" == "prod" ]; then
    npm run build
    cmd="serve -s demo/dist"
else
    cmd="npm start"
fi

if [ ! -z "$1" ];  then
    cmd=$@
fi
exec $cmd

docker/ports.yml

0 → 100644
+6 −0
Original line number Original line Diff line number Diff line
version: "3"

services:
  front:
    ports:
      - ${PORT}:3000

docker/traefik.yml

0 → 100644
+19 −0
Original line number Original line Diff line number Diff line
version: '3'

services:
  front:
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      - "traefik.http.routers.${NAME}.rule=Host(${HOST})"
      - "traefik.http.routers.${NAME}.tls.certresolver=myresolver"
      - "traefik.http.routers.${NAME}.entrypoints=web,websecure"
      - "traefik.http.routers.${NAME}.middlewares=hardening@docker"


networks:
  traefik:
    external: true
+9098 −7611

File changed.

Preview size limit exceeded, changes collapsed.

+29 −17
Original line number Original line Diff line number Diff line
@@ -14,6 +14,7 @@
    "build": "nwb build-react-component",
    "build": "nwb build-react-component",
    "clean": "nwb clean-module",
    "clean": "nwb clean-module",
    "lint": "eslint ./src ./__tests__",
    "lint": "eslint ./src ./__tests__",
    "prepare": "npm run build",
    "prepublishOnly": "npm run build",
    "prepublishOnly": "npm run build",
    "start": "nwb serve-react-demo",
    "start": "nwb serve-react-demo",
    "test": "npm run lint && jest",
    "test": "npm run lint && jest",
@@ -22,38 +23,49 @@
    "test:ci": "jest --ci --reporters=default --reporters=jest-junit --watchAll=false"
    "test:ci": "jest --ci --reporters=default --reporters=jest-junit --watchAll=false"
  },
  },
  "dependencies": {
  "dependencies": {
    "@psychobolt/react-paperjs": "< 1.0",
    "@emotion/react": "^11.11.3",
    "@emotion/styled": "^11.11.0",
    "@mui/system": "^5.15.1",
    "@psychobolt/react-paperjs": "^1.0.3",
    "@psychobolt/react-paperjs-editor": "0.0.11",
    "@psychobolt/react-paperjs-editor": "0.0.11",
    "axios": "^1.6.7",
    "draft-js": "^0.11.6",
    "draft-js": "^0.11.6",
    "draft-js-export-html": "^1.4.1",
    "draft-js-export-html": "^1.4.1",
    "draft-js-import-html": "^1.4.1",
    "draft-js-import-html": "^1.4.1",
    "material-ui-color-components": "^0.3.0",
    "paper": "^0.12.11",
    "paper": "^0.12.11",
    "react-color": "^2.18.1"
    "react-color": "^2.18.1",
    "react-konva": ">=17.0.1-3 <18.0.0",
    "react-konva-to-svg": "^1.0.2",
    "react-quill": "^2.0.0",
    "react-redux": "8.1.3",
    "react-resize-observer": "^1.1.1",
    "react-sortablejs": "^6.1.4",
    "redux": "^4.2.1",
    "sortablejs": "^1.15.2",
    "use-image": "^1.1.1"
  },
  },
  "peerDependencies": {
  "peerDependencies": {
    "@material-ui/core": "^4.9.13",
    "@material-ui/icons": "^4.9.1",
    "@material-ui/lab": "^4.0.0-alpha.52",
    "lodash": "^4.17.11",
    "lodash": "^4.17.11",
    "mirador": "git+https://gitlab.tetras-libre.fr/iiif/mirador-video-annotation#annotation-on-video",
    "mirador": "git+https://gitlab.tetras-libre.fr/iiif/mirador/mirador-video#mui5-annotation-on-video-stable",
    "prop-types": "^15.7.2",
    "prop-types": "^15.7.2",
    "react": "^16.8",
    "react": "^17.0.0",
    "react-dom": "^16.8",
    "react-dom": "^17.0.0",
    "uuid": "^8.0.0"
    "uuid": "^8.0.0"
  },
  },
  "devDependencies": {
  "devDependencies": {
    "@babel/core": "^7.10.4",
    "@babel/core": "^7.10.4",
    "@babel/eslint-parser": "^7.19.1",
    "@babel/eslint-parser": "^7.19.1",
    "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
    "@babel/preset-env": "^7.10.4",
    "@babel/preset-env": "^7.10.4",
    "@babel/preset-react": "^7.10.4",
    "@babel/preset-react": "^7.10.4",
    "@material-ui/core": "^4.11.0",
    "@mui/icons-material": "^5.11.16",
    "@material-ui/icons": "^4.9.1",
    "@mui/lab": "^5.0.0-alpha.134",
    "@material-ui/lab": "^4.0.0-alpha.56",
    "@mui/material": "^5.13.5",
    "@mui/utils": "^5.13.1",
    "@mui/x-tree-view": "^6.17.0",
    "canvas": "^2.6.1",
    "canvas": "^2.6.1",
    "enzyme": "^3.11.0",
    "enzyme": "^3.11.0",
    "enzyme-adapter-react-16": "^1.15.2",
    "eslint": "^8.56.0",
    "eslint": "^8.11.0",
    "eslint-config-airbnb": "^19.0.4",
    "eslint-config-airbnb": "^19.0.4",
    "eslint-config-react-app": "^7.0.0",
    "eslint-config-react-app": "^7.0.0",
    "eslint-plugin-flowtype": "^8.0.3",
    "eslint-plugin-flowtype": "^8.0.3",
@@ -65,11 +77,11 @@
    "jest-canvas-mock": "^2.2.0",
    "jest-canvas-mock": "^2.2.0",
    "jest-junit": "^15.0.0",
    "jest-junit": "^15.0.0",
    "jest-localstorage-mock": "^2.4.2",
    "jest-localstorage-mock": "^2.4.2",
    "mirador": "git+https://gitlab.tetras-libre.fr/iiif/mirador-video-annotation#annotation-on-video",
    "mirador": "git+https://gitlab.tetras-libre.fr/iiif/mirador/mirador-video#mui5-annotation-on-video-stable",
    "nwb": "^0.24.7",
    "nwb": "^0.24.7",
    "prop-types": "^15.7.2",
    "prop-types": "^15.7.2",
    "react": "^16.8",
    "react": "^17.0.0",
    "react-dom": "^16.8",
    "react-dom": "^17.0",
    "uuid": "^8.2.0"
    "uuid": "^8.2.0"
  },
  },
  "author": "",
  "author": "",
Original line number Original line Diff line number Diff line
var D = (r, n, e) => {
  if (!n.has(r)) throw TypeError('Cannot ' + e);
};
var t = (r, n, e) => (D(r, n, 'read from private field'), e ? e.call(r) : n.get(r)),
  o = (r, n, e) => {
    if (n.has(r)) throw TypeError('Cannot add the same private member more than once');
    n instanceof WeakSet ? n.add(r) : n.set(r, e);
  },
  p = (r, n, e, i) => (D(r, n, 'write to private field'), i ? i.call(r, e) : n.set(r, e), e);
var w,
  b = class {
    constructor() {
      o(this, w, void 0);
      this.register = n => {
        t(this, w)
          .push(n);
      };
      this.unregister = n => {
        let e;
        for (; (e = t(this, w)
          .indexOf(n)) !== -1;) {
          t(this, w)
            .splice(e, 1);
        }
      };
      this.backendChanged = n => {
        for (let e of t(this, w)) e.backendChanged(n);
      };
      p(this, w, []);
    }
  };
w = new WeakMap;
var a,
  f,
  c,
  d,
  h,
  x,
  T,
  E,
  v,
  y,
  g,
  l = class l {
    constructor(n, e, i) {
      o(this, a, void 0);
      o(this, f, void 0);
      o(this, c, void 0);
      o(this, d, void 0);
      o(this, h, void 0);
      o(this, x, (n, e, i) => {
        if (!i.backend) throw new Error(`You must specify a 'backend' property in your Backend entry: ${JSON.stringify(i)}`);
        let u = i.backend(n, e, i.options),
          s = i.id,
          k = !i.id && u && u.constructor;
        if (k && (s = u.constructor.name), s) {
          k && console.warn(`Deprecation notice: You are using a pipeline which doesn't include backends' 'id'.
        This might be unsupported in the future, please specify 'id' explicitely for every backend.`);
        } else {
          throw new Error(`You must specify an 'id' property in your Backend entry: ${JSON.stringify(i)}
        see this guide: https://github.com/louisbrunner/dnd-multi-backend/tree/master/packages/react-dnd-multi-backend#migrating-from-5xx`);
        }
        if (t(this, c)[s]) {
          throw new Error(`You must specify a unique 'id' property in your Backend entry:
        ${JSON.stringify(i)} (conflicts with: ${JSON.stringify(t(this, c)[s])})`);
        }
        return {
          id: s,
          instance: u,
          preview: i.preview ? i.preview : !1,
          transition: i.transition,
          skipDispatchOnTransition: i.skipDispatchOnTransition ? i.skipDispatchOnTransition : !1
        };
      });
      this.setup = () => {
        if (!(typeof window > 'u')) {
          if (l.isSetUp) throw new Error('Cannot have two MultiBackends at the same time.');
          l.isSetUp = !0, t(this, T)
            .call(this, window), t(this, c)[t(this, a)].instance.setup();
        }
      };
      this.teardown = () => {
        typeof window > 'u' || (l.isSetUp = !1, t(this, E)
          .call(this, window), t(this, c)[t(this, a)].instance.teardown());
      };
      this.connectDragSource = (n, e, i) => t(this, g)
        .call(this, 'connectDragSource', n, e, i);
      this.connectDragPreview = (n, e, i) => t(this, g)
        .call(this, 'connectDragPreview', n, e, i);
      this.connectDropTarget = (n, e, i) => t(this, g)
        .call(this, 'connectDropTarget', n, e, i);
      this.profile = () => t(this, c)[t(this, a)].instance.profile();
      this.previewEnabled = () => t(this, c)[t(this, a)].preview;
      this.previewsList = () => t(this, f);
      this.backendsList = () => t(this, d);
      o(this, T, n => {
        t(this, d)
          .forEach(e => {
            e.transition && n.addEventListener(e.transition.event, t(this, v));
          });
      });
      o(this, E, n => {
        t(this, d)
          .forEach(e => {
            e.transition && n.removeEventListener(e.transition.event, t(this, v));
          });
      });
      o(this, v, n => {
        let e = t(this, a);
        if (t(this, d)
          .some(i => i.id !== t(this, a) && i.transition && i.transition.check(n) ? (p(this, a, i.id), !0) : !1), t(this, a) !== e) {
          t(this, c)[e].instance.teardown(), Object.keys(t(this, h))
            .forEach(k => {
              let B = t(this, h)[k];
              B.unsubscribe(), B.unsubscribe = t(this, y)
                .call(this, B.func, ...B.args);
            }), t(this, f)
            .backendChanged(this);
          let i = t(this, c)[t(this, a)];
          if (i.instance.setup(), i.skipDispatchOnTransition) return;
          let u = n.constructor,
            s = new u(n.type, n);
          n.target && n.target.dispatchEvent(s);
        }
      });
      o(this, y, (n, e, i, u) => t(this, c)[t(this, a)].instance[n](e, i, u));
      o(this, g, (n, e, i, u) => {
        let s = `${n}_${e}`,
          k = t(this, y)
            .call(this, n, e, i, u);
        return t(this, h)[s] = {
          func: n,
          args: [e, i, u],
          unsubscribe: k
        }, () => {
          t(this, h)[s].unsubscribe(), delete t(this, h)[s];
        };
      });
      if (!i || !i.backends || i.backends.length < 1) {
        throw new Error(`You must specify at least one Backend, if you are coming from 2.x.x (or don't understand this error)
        see this guide: https://github.com/louisbrunner/dnd-multi-backend/tree/master/packages/react-dnd-multi-backend#migrating-from-2xx`);
      }
      p(this, f, new b), p(this, c, {}), p(this, d, []), i.backends.forEach(u => {
        let s = t(this, x)
          .call(this, n, e, u);
        t(this, c)[s.id] = s, t(this, d)
          .push(s);
      }), p(this, a, t(this, d)[0].id), p(this, h, {});
    }
  };
a = new WeakMap, f = new WeakMap, c = new WeakMap, d = new WeakMap, h = new WeakMap, x = new WeakMap, T = new WeakMap, E = new WeakMap, v = new WeakMap, y = new WeakMap, g = new WeakMap, l.isSetUp = !1;
var M = l;
var P = (r, n, e) => new M(r, n, e);
var m = (r, n) => ({
  event: r,
  check: n
});
var S = m('touchstart', r => {
    let n = r;
    return n.touches !== null && n.touches !== void 0;
  }),
  L = m('dragstart', r => r.type.indexOf('drag') !== -1 || r.type.indexOf('drop') !== -1),
  O = m('mousedown', r => r.type.indexOf('touch') === -1 && r.type.indexOf('mouse') !== -1),
  C = m('pointerdown', r => r.pointerType == 'mouse');
export {
  L as HTML5DragTransition,
  O as MouseTransition,
  P as MultiBackend,
  C as PointerTransition,
  S as TouchTransition,
  m as createTransition
};
Original line number Original line Diff line number Diff line
export * from 'dnd-multi-backend';
import i, { useState as l, createContext as D } from 'react';
import { DndProvider as m } from 'react-dnd';
import { MultiBackend as P } from 'dnd-multi-backend';

var u = D(null),
  g = ({
    portal: e,
    ...t
  }) => {
    let [r, o] = l(null);
    let value = e ? e : r; // TODO : remove ?? operator
    return i.createElement(u.Provider, { value: value}, i.createElement(m, { backend: P, ...t }), e ? null : i.createElement('div', { ref: o }));
  };
import w, { useContext as k } from 'react';
import { createPortal as M } from 'react-dom';
import { Preview as C, Context as b } from 'react-dnd-preview';
import { useState as v, useEffect as S, useContext as x } from 'react';
import { DndContext as f } from 'react-dnd';

var p = () => {
  let [e, t] = v(!1),
    r = x(f);
  return S(() => {
    let o = r && r.dragDropManager && r.dragDropManager.getBackend(),
    // TODO let o = r?.dragDropManager?.getBackend(),
      n = {
        backendChanged: s => {
          t(s.previewEnabled());
        }
      };
    return t(o.previewEnabled()), o.previewsList()
      .register(n), () => {
      o.previewsList()
        .unregister(n);
    };
  }, [r, r.dragDropManager]), e;
};
var E = e => {
  let t = p(),
    r = k(u);
  if (!t) return null;
  let o = w.createElement(C, { ...e });
  return r !== null ? M(o, r) : o;
};
E.Context = b;
import { useDrag as T } from 'react-dnd';
import { useContext as B } from 'react';
import { DndContext as y } from 'react-dnd';

var R = (e, t, r, o) => {
    let n = r.getBackend();
    r.receiveBackend(o);
    let s = t(e);
    return r.receiveBackend(n), s;
  },
  a = (e, t) => {
    let r = B(y),
      o = r && r.dragDropManager && r.dragDropManager.getBackend();
    if (o === void 0) throw new Error('could not find backend, make sure you are using a <DndProvider />');
    let n = t(e),
      s = {},
      d = o.backendsList();
    for (let c of d) s[c.id] = R(e, t, r.dragDropManager, c.instance);
    return [n, s];
  };
var pe = e => a(e, T);
import { useDrop as O } from 'react-dnd';

var le = e => a(e, O);
import { usePreview as h } from 'react-dnd-preview';

var Se = () => {
  let e = p(),
    t = h();
  return e ? t : { display: !1 };
};
export {
  g as DndProvider,
  E as Preview,
  b as PreviewContext,
  pe as useMultiDrag,
  le as useMultiDrop,
  Se as usePreview
};
+78 −0
Original line number Original line Diff line number Diff line
/** Extract time information from annotation target */
export function timeFromAnnoTarget(annotarget) {
  // TODO w3c media fragments: t=,10 t=5,
  const r = /t=([0-9.]+),([0-9.]+)/.exec(annotarget);
  if (!r || r.length !== 3) {
    return [0, 0];
  }
  return [Number(r[1]), Number(r[2])];
}

/** Extract xywh from annotation target */
export function geomFromAnnoTarget(annotarget) {
  const r = /xywh=((-?[0-9]+,?)+)/.exec(annotarget);
  if (!r || r.length !== 3) {
    return '';
  }
  return r[1];
}

export const OVERLAY_TOOL = {
  CURSOR: 'cursor',
  DELETE: 'delete',
  EDIT: 'edit',
  IMAGE: 'image',
  SHAPE: 'shapes',
  TEXT: 'text',
};

export const SHAPES_TOOL = {
  ARROW: 'arrow',
  ELLIPSE: 'ellipse',
  FREEHAND: 'freehand',
  POLYGON: 'polygon',
  RECTANGLE: 'rectangle',
  SHAPES: 'shapes',
};

/** Check if the active tool is a shape tool */
export function isShapesTool(activeTool) {
  // Find if active tool in the list of overlay tools. I want a boolean in return
  return Object.values(SHAPES_TOOL).find((tool) => tool === activeTool);
}

/** Save annotation in the storage adapter */
export async function saveAnnotation(canvas, storageAdapter, receiveAnnotation, annotation, isNewAnnotation) {
  if (!isNewAnnotation) {
    storageAdapter.update(annotation)
      .then((annoPage) => {
        receiveAnnotation(canvas.id, storageAdapter.annotationPageId, annoPage);
      });
  } else {
    storageAdapter.create(annotation)
      .then((annoPage) => {
        receiveAnnotation(canvas.id, storageAdapter.annotationPageId, annoPage);
      });
  }
}

/** Save annotation for each canvas */
export async function saveAnnotationInEachCanvas(canvases, config, receiveAnnotation, annotation, target, isNewAnnotation) {
  canvases.forEach(async (canvas) => {
    // Adapt target to the canvas
    // eslint-disable-next-line no-param-reassign
    annotation.target = `${canvas.id}#xywh=${target.xywh}&t=${target.t}`;
    const storageAdapter = config.annotation.adapter(canvas.id);
    saveAnnotation(canvas, storageAdapter, receiveAnnotation, annotation, isNewAnnotation);
  });
}

export const defaultToolState = {
  activeTool: OVERLAY_TOOL.EDIT,
  closedMode: 'closed',
  fillColor: 'rgba(83,162, 235, 0.5)',
  image: { id: null },
  imageEvent: null,
  strokeColor: 'rgba(20,82,168,1)',
  strokeWidth: 2,
};

src/AnnotationDrawing.js

deleted100644 → 0
+0 −189
Original line number Original line Diff line number Diff line
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { OSDReferences } from 'mirador/dist/es/src/plugins/OSDReferences';
import { VideoViewersReferences } from 'mirador/dist/es/src/plugins/VideoViewersReferences';
import { renderWithPaperScope, PaperContainer } from '@psychobolt/react-paperjs';
import
{
  EllipseTool,
  PolygonTool,
  RectangleTool,
  FreeformPathTool,
}
from '@psychobolt/react-paperjs-editor';
import { Point } from 'paper';
import flatten from 'lodash/flatten';
import EditTool from './EditTool';
import { mapChildren } from './utils';

/** Use a canvas "like a OSD viewport" (temporary) */
function viewportFromAnnotationOverlayVideo(annotationOverlayVideo) {
  const { canvas } = annotationOverlayVideo;
  return {
    getCenter: () => ({ x: canvas.getWidth() / 2, y: canvas.getHeight() / 2 }),
    getFlip: () => false,
    getRotation: () => false,
    getZoom: () => 1,
  };
}

/** */
class AnnotationDrawing extends Component {
  /** */
  constructor(props) {
    super(props);

    this.addPath = this.addPath.bind(this);
  }

  /** */
  addPath(path) {
    const { closed, strokeWidth, updateGeometry } = this.props;
    // TODO: Compute xywh of bounding container of layers
    const { bounds } = path;
    const {
      x, y, width, height,
    } = bounds;
    path.closed = closed; // eslint-disable-line no-param-reassign
    // Reset strokeWidth for persistence
    path.strokeWidth = strokeWidth; // eslint-disable-line no-param-reassign
    path.data.state = null; // eslint-disable-line no-param-reassign
    const svgExports = flatten(path.project.layers.map((layer) => (
      flatten(mapChildren(layer)).map((aPath) => aPath.exportSVG({ asString: true }))
    )));
    svgExports.unshift("<svg xmlns='http://www.w3.org/2000/svg'>");
    svgExports.push('</svg>');
    updateGeometry({
      svg: svgExports.join(''),
      xywh: [
        Math.floor(x),
        Math.floor(y),
        Math.floor(width),
        Math.floor(height),
      ].join(','),
    });
  }

  /** */
  paperThing() {
    const { windowId } = this.props;
    let viewport = null;
    let img = null;
    if (OSDReferences.get(windowId)) {
      console.debug('[annotation-plugin] OSD reference: ', OSDReferences.get(windowId));
      viewport = OSDReferences.get(windowId).current.viewport;
      img = OSDReferences.get(windowId).current.world.getItemAt(0);
    } else if (VideoViewersReferences.get(windowId)) {
      console.debug('[annotation-plugin] VideoViewers reference: ', VideoViewersReferences.get(windowId));
      viewport = viewportFromAnnotationOverlayVideo(VideoViewersReferences.get(windowId).props);
    }
    const {
      activeTool, fillColor, strokeColor, strokeWidth, svg,
    } = this.props;
    if (!activeTool || activeTool === 'cursor') return null;
    // Setup Paper View to have the same center and zoom as the OSD Viewport/video canvas
    const center = img
      ? img.viewportToImageCoordinates(viewport.getCenter(true))
      : viewport.getCenter();
    const flipped = viewport.getFlip();

    const viewProps = {
      center: new Point(center.x, center.y),
      rotation: viewport.getRotation(),
      scaling: new Point(flipped ? -1 : 1, 1),
      zoom: img ? img.viewportToImageZoom(viewport.getZoom()) : viewport.getZoom(),
    };

    let ActiveTool = RectangleTool;
    switch (activeTool) {
      case 'rectangle':
        ActiveTool = RectangleTool;
        break;
      case 'ellipse':
        ActiveTool = EllipseTool;
        break;
      case 'polygon':
        ActiveTool = PolygonTool;
        break;
      case 'freehand':
        ActiveTool = FreeformPathTool;
        break;
      case 'edit':
        ActiveTool = EditTool;
        break;
      default:
        break;
    }

    return (
      <div
        className="foo"
        style={{
          height: '100%', left: 0, position: 'absolute', top: 0, width: '100%',
        }}
      >
        <PaperContainer
          canvasProps={{ style: { height: '100%', width: '100%' } }}
          viewProps={viewProps}
        >
          {renderWithPaperScope((paper) => {
            const paths = flatten(paper.project.layers.map((layer) => (
              flatten(mapChildren(layer)).map((aPath) => aPath)
            )));
            if (svg && paths.length === 0) {
              paper.project.importSVG(svg);
            }
            paper.settings.handleSize = 10; // eslint-disable-line no-param-reassign
            paper.settings.hitTolerance = 10; // eslint-disable-line no-param-reassign
            return (
              <ActiveTool
                onPathAdd={this.addPath}
                pathProps={{
                  fillColor,
                  strokeColor,
                  strokeWidth: strokeWidth / paper.view.zoom,
                }}
                paper={paper}
              />
            );
          })}
        </PaperContainer>
      </div>
    );
  }

  /** */
  render() {
    const { windowId } = this.props;
    const container = OSDReferences.get(windowId)
      ? OSDReferences.get(windowId).current.element
      : VideoViewersReferences.get(windowId).apiRef.current;

    return (
      ReactDOM.createPortal(this.paperThing(), container)
    );
  }
}

AnnotationDrawing.propTypes = {
  activeTool: PropTypes.string,
  closed: PropTypes.bool,
  fillColor: PropTypes.string,
  strokeColor: PropTypes.string,
  strokeWidth: PropTypes.number,
  svg: PropTypes.string,
  updateGeometry: PropTypes.func.isRequired,
  windowId: PropTypes.string.isRequired,
};

AnnotationDrawing.defaultProps = {
  activeTool: null,
  closed: true,
  fillColor: null,
  strokeColor: '#00BFFF',
  strokeWidth: 1,
  svg: null,
};

export default AnnotationDrawing;
Original line number Original line Diff line number Diff line
import React, { Component } from 'react';
import React, { Component } from 'react';
import Dialog from '@material-ui/core/Dialog';
import Dialog from '@mui/material/Dialog';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogTitle from '@mui/material/DialogTitle';
import GetAppIcon from '@material-ui/icons/GetApp';
import GetAppIcon from '@mui/icons-material/GetApp';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemText from '@mui/material/ListItemText';
import MenuList from '@material-ui/core/MenuList';
import MenuList from '@mui/material/MenuList';
import MenuItem from '@material-ui/core/MenuItem';
import MenuItem from '@mui/material/MenuItem';
import Typography from '@material-ui/core/Typography';
import Typography from '@mui/material/Typography';
import PropTypes, { bool } from 'prop-types';
import PropTypes, { bool } from 'prop-types';
import { withStyles } from '@material-ui/core';
import { styled } from '@mui/system';


/** */
/** */
const styles = (theme) => ({
const styles = (theme) => ({
@@ -137,4 +137,4 @@ AnnotationExportDialog.defaultProps = {
  classes: {},
  classes: {},
};
};


export default withStyles(styles)(AnnotationExportDialog);
export default styled(styles)(AnnotationExportDialog);
Original line number Original line Diff line number Diff line
import React, { Component } from 'react';
import React, {
  useState, useContext, forwardRef, useEffect,
} from 'react';
import PropTypes from 'prop-types';
import PropTypes from 'prop-types';
import DeleteIcon from '@material-ui/icons/DeleteForever';
import DeleteIcon from '@mui/icons-material/DeleteForever';
import EditIcon from '@material-ui/icons/Edit';
import EditIcon from '@mui/icons-material/Edit';
import ToggleButton from '@material-ui/lab/ToggleButton';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import flatten from 'lodash/flatten';
import flatten from 'lodash/flatten';
import AnnotationActionsContext from './AnnotationActionsContext';
import AnnotationActionsContext from './AnnotationActionsContext';


/** */
const CanvasListItem = forwardRef((props, ref) => {
class CanvasListItem extends Component {
  const [isHovering, setIsHovering] = useState(false);
  /** */
  const context = useContext(AnnotationActionsContext);
  constructor(props) {
    super(props);


    this.state = {
  const handleMouseHover = () => {
      isHovering: false,
    setIsHovering(!isHovering);
  };
  };


    this.handleMouseHover = this.handleMouseHover.bind(this);
  const handleDelete = () => {
    this.handleDelete = this.handleDelete.bind(this);
    const { canvases, receiveAnnotation, storageAdapter } = context;
    this.handleEdit = this.handleEdit.bind(this);
    const { annotationid } = props;
  }

  /** */
  handleDelete() {
    const { canvases, receiveAnnotation, storageAdapter } = this.context;
    const { annotationid } = this.props;
    canvases.forEach((canvas) => {
    canvases.forEach((canvas) => {
      const adapter = storageAdapter(canvas.id);
      const adapter = storageAdapter(canvas.id);
      adapter.delete(annotationid).then((annoPage) => {
      adapter.delete(annotationid).then((annoPage) => {
        receiveAnnotation(canvas.id, adapter.annotationPageId, annoPage);
        receiveAnnotation(canvas.id, adapter.annotationPageId, annoPage);
      });
      });
    });
    });
  }
  };


  /** */
  const handleEdit = () => {
  handleEdit() {
    const {
    const {
      addCompanionWindow, canvases, annotationsOnCanvases,
      addCompanionWindow, canvases, annotationsOnCanvases,
    } = this.context;
    } = context;
    const { annotationid } = this.props;
    const { annotationid } = props;
    let annotation;
    let annotation;
    canvases.some((canvas) => {
    canvases.some((canvas) => {
      if (annotationsOnCanvases[canvas.id]) {
      if (annotationsOnCanvases[canvas.id]) {
        Object.entries(annotationsOnCanvases[canvas.id]).forEach(([key, value], i) => {
        Object.entries(annotationsOnCanvases[canvas.id]).forEach(([key, value]) => {
          if (value.json && value.json.items) {
          if (value.json && value.json.items) {
            annotation = value.json.items.find((anno) => anno.id === annotationid);
            annotation = value.json.items.find((anno) => anno.id === annotationid);
          }
          }
@@ -55,22 +48,14 @@ class CanvasListItem extends Component {
      annotationid,
      annotationid,
      position: 'right',
      position: 'right',
    });
    });
  }
  };

  /** */
  handleMouseHover() {
    this.setState((prevState) => ({
      isHovering: !prevState.isHovering,
    }));
  }


  /** */
  const editable = () => {
  editable() {
    const { annotationsOnCanvases, canvases } = context;
    const { annotationsOnCanvases, canvases } = this.context;
    const { annotationid } = props;
    const { annotationid } = this.props;
    const annoIds = canvases.map((canvas) => {
    const annoIds = canvases.map((canvas) => {
      if (annotationsOnCanvases[canvas.id]) {
      if (annotationsOnCanvases[canvas.id]) {
        return flatten(Object.entries(annotationsOnCanvases[canvas.id]).map(([key, value], i) => {
        return flatten(Object.entries(annotationsOnCanvases[canvas.id]).map(([key, value]) => {
          if (value.json && value.json.items) {
          if (value.json && value.json.items) {
            return value.json.items.map((item) => item.id);
            return value.json.items.map((item) => item.id);
          }
          }
@@ -80,19 +65,16 @@ class CanvasListItem extends Component {
      return [];
      return [];
    });
    });
    return flatten(annoIds).includes(annotationid);
    return flatten(annoIds).includes(annotationid);
  }
  };


  /** */
  render() {
    const { children } = this.props;
    const { isHovering } = this.state;
    const { windowViewType, toggleSingleCanvasDialogOpen } = this.context;
  return (
  return (
    <div
    <div
        onMouseEnter={this.handleMouseHover}
      onMouseEnter={handleMouseHover}
        onMouseLeave={this.handleMouseHover}
      onMouseLeave={handleMouseHover}
      className="mirador-annotation-list-item"
      ref={ref}
    >
    >
        {isHovering && this.editable() && (
      {isHovering && editable() && (
        <div
        <div
          style={{
          style={{
            position: 'relative',
            position: 'relative',
@@ -103,35 +85,35 @@ class CanvasListItem extends Component {
          <ToggleButtonGroup
          <ToggleButtonGroup
            aria-label="annotation tools"
            aria-label="annotation tools"
            size="small"
            size="small"
              style={{
            style={{ position: 'absolute', right: 0 }}
                position: 'absolute',
            disabled={!context.annotationEditCompanionWindowIsOpened}
                right: 0,
              }}
          >
          >
            <ToggleButton
            <ToggleButton
              aria-label="Edit"
              aria-label="Edit"
                onClick={windowViewType === 'single' ? this.handleEdit : toggleSingleCanvasDialogOpen}
              onClick={context.windowViewType === 'single' ? handleEdit : context.toggleSingleCanvasDialogOpen}
              value="edit"
              value="edit"
            >
            >
              <EditIcon />
              <EditIcon />
            </ToggleButton>
            </ToggleButton>
              <ToggleButton aria-label="Delete" onClick={this.handleDelete} value="delete">
            <ToggleButton
              aria-label="Delete"
              onClick={handleDelete}
              value="delete"
            >
              <DeleteIcon />
              <DeleteIcon />
            </ToggleButton>
            </ToggleButton>
          </ToggleButtonGroup>
          </ToggleButtonGroup>
        </div>
        </div>
      )}
      )}
        <li
      <li {...props}>
          {...this.props} // eslint-disable-line react/jsx-props-no-spreading
        {props.children}
        >
          {children}
      </li>
      </li>
    </div>
    </div>
  );
  );
  }
});
}


CanvasListItem.propTypes = {
CanvasListItem.propTypes = {
  annotationEditCompanionWindowIsOpened: PropTypes.bool.isRequired,
  annotationid: PropTypes.string.isRequired,
  annotationid: PropTypes.string.isRequired,
  children: PropTypes.oneOfType([
  children: PropTypes.oneOfType([
    PropTypes.func,
    PropTypes.func,
@@ -139,6 +121,4 @@ CanvasListItem.propTypes = {
  ]).isRequired,
  ]).isRequired,
};
};


CanvasListItem.contextType = AnnotationActionsContext;

export default CanvasListItem;
export default CanvasListItem;

src/HMSInput.js

0 → 100644
+84 −0
Original line number Original line Diff line number Diff line
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Input, styled } from '@mui/material';
import { secondsToHMSarray } from './utils';

const StyledInput = styled(Input)(({ theme }) => ({
  height: 'fit-content',
}));

const StyledHMSLabel = styled('span')({
  color: 'grey',
});

const StyledRoot = styled('div')({
  alignItems: 'center',
  display: 'flex',
});

/** Hours minutes seconds form inputs */
function HMSInput({ seconds, onChange }) {
  const [hms, setHms] = useState(secondsToHMSarray(seconds));

  useEffect(() => {
    if (seconds != null) {
      setHms(secondsToHMSarray(Number(seconds)));
    }
  }, [seconds]);

  /** Handle change on one form */
  const someChange = (ev) => {
    if (!ev.target.value) {
      return;
    }
    hms[ev.target.name] = Number(ev.target.value);
    setHms(hms);
    onChange(hms.hours * 3600 + hms.minutes * 60 + hms.seconds);
  };

  return (
    <StyledRoot>
      <StyledInput
        variant="filled"
        type="number"
        min="0"
        name="hours"
        value={hms.hours}
        onChange={someChange}
        dir="rtl"
        inputProps={{ style: { width: '35px' } }}

      />
      <StyledHMSLabel style={{ margin: '2px' }}>h</StyledHMSLabel>
      <StyledInput
        type="number"
        min="0"
        max="59"
        name="minutes"
        value={hms.minutes}
        onChange={someChange}
        dir="rtl"
        inputProps={{ style: { width: '35px' } }}
      />
      <StyledHMSLabel style={{ margin: '2px' }}>m</StyledHMSLabel>
      <StyledInput
        type="number"
        min="0"
        max="59"
        name="seconds"
        value={hms.seconds}
        onChange={someChange}
        dir="rtl"
        inputProps={{ style: { width: '35px' } }}
      />
      <StyledHMSLabel style={{ margin: '2px' }}>s</StyledHMSLabel>
    </StyledRoot>
  );
}

HMSInput.propTypes = {
  onChange: PropTypes.func.isRequired,
  seconds: PropTypes.number.isRequired,
};

export default HMSInput;
Original line number Original line Diff line number Diff line
@@ -15,6 +15,7 @@ export default class LocalStorageAdapter {
    const annotationPage = await this.all() || emptyAnnoPage;
    const annotationPage = await this.all() || emptyAnnoPage;
    annotationPage.items.push(annotation);
    annotationPage.items.push(annotation);
    localStorage.setItem(this.annotationPageId, JSON.stringify(annotationPage));
    localStorage.setItem(this.annotationPageId, JSON.stringify(annotationPage));
    console.log('CREATE ANNOTATION', annotationPage)
    return annotationPage;
    return annotationPage;
  }
  }


@@ -25,6 +26,7 @@ export default class LocalStorageAdapter {
      const currentIndex = annotationPage.items.findIndex((item) => item.id === annotation.id);
      const currentIndex = annotationPage.items.findIndex((item) => item.id === annotation.id);
      annotationPage.items.splice(currentIndex, 1, annotation);
      annotationPage.items.splice(currentIndex, 1, annotation);
      localStorage.setItem(this.annotationPageId, JSON.stringify(annotationPage));
      localStorage.setItem(this.annotationPageId, JSON.stringify(annotationPage));
      console.log('UPDATE ANNOTATION', annotationPage)
      return annotationPage;
      return annotationPage;
    }
    }
    return null;
    return null;
Original line number Original line Diff line number Diff line
import React, { Component } from 'react';
import React, { Component } from 'react';
import Button from '@material-ui/core/Button';
import Button from '@mui/material/Button';
import Dialog from '@material-ui/core/Dialog';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogTitle from '@mui/material/DialogTitle';
import Typography from '@material-ui/core/Typography';
import Typography from '@mui/material/Typography';
import PropTypes from 'prop-types';
import PropTypes from 'prop-types';


/**
/**
+26 −115
Original line number Original line Diff line number Diff line
import React, { Component } from 'react';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import PropTypes from 'prop-types';
import { Editor, EditorState, RichUtils } from 'draft-js';
import ReactQuill from 'react-quill';
import ToggleButton from '@material-ui/lab/ToggleButton';
import 'react-quill/dist/quill.snow.css';
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';
import { styled } from '@mui/material/styles';
import BoldIcon from '@material-ui/icons/FormatBold';
import ItalicIcon from '@material-ui/icons/FormatItalic';
import { withStyles } from '@material-ui/core/styles';
import { stateToHTML } from 'draft-js-export-html';
import { stateFromHTML } from 'draft-js-import-html';


/** */
const StyledReactQuill = styled(ReactQuill)(({ theme }) => ({
class TextEditor extends Component {
  '.ql-editor': {
  /** */
    minHeight: '150px',
  constructor(props) {
  },
    super(props);
}));
    this.state = {
      editorState: EditorState.createWithContent(stateFromHTML(props.annoHtml)),
    };
    this.onChange = this.onChange.bind(this);
    this.handleKeyCommand = this.handleKeyCommand.bind(this);
    this.handleFormating = this.handleFormating.bind(this);
    this.handleFocus = this.handleFocus.bind(this);
    this.editorRef = React.createRef();
  }

  /**
   * This is a kinda silly hack (but apparently recommended approach) to
   * making sure the whole visible editor area is clickable, not just the first line.
   */
  handleFocus() {
    if (this.editorRef.current) this.editorRef.current.focus();
  }

  /** */
  handleFormating(e, newFormat) {
    const { editorState } = this.state;
    this.onChange(RichUtils.toggleInlineStyle(editorState, newFormat));
  }


  /** */
/** Rich text editor for annotation body */
  handleKeyCommand(command, editorState) {
function TextEditor({ annoHtml, updateAnnotationBody }) {
    const newState = RichUtils.handleKeyCommand(editorState, command);
  const [editorHtml, setEditorHtml] = useState(annoHtml);
    if (newState) {
      this.onChange(newState);
      return 'handled';
    }
    return 'not-handled';
  }


  /** */
  const handleChange = (html) => {
  onChange(editorState) {
    setEditorHtml(html);
    const { updateAnnotationBody } = this.props;
    this.setState({ editorState });
    if (updateAnnotationBody) {
    if (updateAnnotationBody) {
      const options = {
      updateAnnotationBody(html);
        inlineStyles: {
          BOLD: { element: 'b' },
          ITALIC: { element: 'i' },
        },
      };
      updateAnnotationBody(stateToHTML(editorState.getCurrentContent(), options).toString());
    }
    }
    }

  };
  /** */
  render() {
    const { classes } = this.props;
    const { editorState } = this.state;
    const currentStyle = editorState.getCurrentInlineStyle();


  return (
  return (
    <div>
    <div>
        <ToggleButtonGroup
      <StyledReactQuill
          size="small"
        value={editorHtml}
          value={currentStyle.toArray()}
        onChange={handleChange}
        >
          <ToggleButton
            onClick={this.handleFormating}
            value="BOLD"
          >
            <BoldIcon />
          </ToggleButton>
          <ToggleButton
            onClick={this.handleFormating}
            value="ITALIC"
          >
            <ItalicIcon />
          </ToggleButton>
        </ToggleButtonGroup>

        <div className={classes.editorRoot} onClick={this.handleFocus}>
          <Editor
            editorState={editorState}
            handleKeyCommand={this.handleKeyCommand}
            onChange={this.onChange}
            ref={this.editorRef}
      />
      />
    </div>
    </div>
      </div>
  );
  );
}
}
}

/** */
const styles = (theme) => ({
  editorRoot: {
    borderColor: theme.palette.type === 'light' ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255, 255, 255, 0.23)',
    borderRadius: theme.shape.borderRadius,
    borderStyle: 'solid',
    borderWidth: 1,
    fontFamily: theme.typography.fontFamily,
    marginBottom: theme.spacing(1),
    marginTop: theme.spacing(1),
    minHeight: theme.typography.fontSize * 6,
    padding: theme.spacing(1),
  },
});


TextEditor.propTypes = {
TextEditor.propTypes = {
  annoHtml: PropTypes.string,
  annoHtml: PropTypes.string,
  classes: PropTypes.shape({
    editorRoot: PropTypes.string,
  }).isRequired,
  updateAnnotationBody: PropTypes.func,
  updateAnnotationBody: PropTypes.func,
};
};


@@ -130,4 +41,4 @@ TextEditor.defaultProps = {
  updateAnnotationBody: () => {},
  updateAnnotationBody: () => {},
};
};


export default withStyles(styles)(TextEditor);
export default TextEditor;
Original line number Original line Diff line number Diff line
@@ -2,77 +2,93 @@
export default class WebAnnotation {
export default class WebAnnotation {
  /** */
  /** */
  constructor({
  constructor({
    canvasId, id, xywh, timing, body, tags, svg, manifestId,
    id, body, drawingStateSerialized, motivation, target,
  }) {
  }) {
    this.id = id;
    this.id = id;
    this.canvasId = canvasId;
    this.type = 'Annotation';
    this.xywh = xywh;
    this.motivation = motivation;
    this.timing = timing;
    this.body = body;
    this.body = body;
    this.tags = tags;
    this.drawingState = drawingStateSerialized;
    this.svg = svg;
    this.target = target;
    this.manifestId = manifestId;
  }
  }


  /** */
  /** */
  toJson() {
  toJson() {
    return {
    // const result = {
      body: this.createBody(),
    //   body: this.createBody(),
      id: this.id,
    //   drawingState: this.drawingState,
      motivation: 'commenting',
    //   id: this.id,
      target: this.target(),
    //   motivation: 'commenting',
      type: 'Annotation',
    //   target: this.target(),
    };
    //   type: 'Annotation',
    // };

    const result = this;

    return result;
  }
  }


  /** */
  /** */
  createBody() {
  createBody() {
    let bodies = [];
    let bodies = [];
    if (this.body) {
    if (this.body && this.body.value !== '') {
      bodies.push({
      const textBody = {
        type: 'TextualBody',
        type: 'TextualBody',
        value: this.body,
        value: this.body.value,
      });
      };
      bodies.push(textBody);
    }
    }
    if (this.tags) {

      bodies = bodies.concat(this.tags.map((tag) => ({
    if (this.image) {
        purpose: 'tagging',
      // TODO dumb image { this.image.id}
        type: 'TextualBody',
      const imgBody = {
        value: tag,
        id: 'https://tetras-libre.fr/themes/tetras/img/logo.svg',
      })));
        type: 'Image',
        format: 'image/svg+xml',
      };
      //bodies.push(imgBody);
      const testImageBody = {
            "id": "https://files.tetras-libre.fr/dev/Hakanai/media/10_HKN-Garges_A2B4243.JPG",
            "type": "Image",
            "format": "image/jpg"
          };
      bodies.push(testImageBody);
    }
    }

    // if (this.tags) {
    //   bodies = bodies.concat(this.tags.map((tag) => ({
    //     purpose: 'tagging',
    //     type: 'TextualBody',
    //     value: tag,
    //   })));
    // }
    if (bodies.length === 1) {
    if (bodies.length === 1) {
      return bodies[0];
      return bodies[0];
    }
    }
    return bodies;
    return bodies;
  }
  }


  /** */
  /** Fill target object with selectors (if any), else returns target url */
  target() {
  target() {
    if (!this.svg && !this.xywh && !this.timing) {
    if (!this.svg
      && (!this.fragsel || !Object.values(this.fragsel).find((e) => e !== null))) {
      return this.canvasId;
      return this.canvasId;
    }
    }
    const target = { source: this.source() };
    const selectors = [];
    const selectors = [];
    const target = {
      source: this.source(),
    };
    if (this.svg) {
    if (this.svg) {
      selectors.push({
      selectors.push({
        type: 'SvgSelector',
        type: 'SvgSelector',
        value: this.svg,
        value: this.svg,
      });
      });
    }
    }
    if (this.xywh) {
    if (this.fragsel) {
      selectors.push({
        type: 'FragmentSelector',
        value: `xywh=${this.xywh}`,
      });
    }
    if (this.timing) {
      const [start, end] = this.timing;
      selectors.push({
      selectors.push({
        type: 'FragmentSelector',
        type: 'FragmentSelector',
        value: `t=${start},${end}`,
        value: Object.entries(this.fragsel)
          .filter((kv) => kv[1])
          .map((kv) => `${kv[0]}=${kv[1]}`)
          .join('&'),
      });
      });
    }
    }
    target.selector = selectors.length === 1 ? selectors[0] : selectors;
    target.selector = selectors.length === 1 ? selectors[0] : selectors;
+117 −0
Original line number Original line Diff line number Diff line
/** */
export default class WebAnnotation {
  /** */
  constructor({
    canvasId, id, fragsel, image, body, tags, svg, manifestId, drawingStateSerialized
  }) {
    this.id = id;
    this.canvasId = canvasId;
    this.fragsel = fragsel;
    this.body = body;
    this.tags = tags;
    this.svg = svg;
    //this.image = image;
    this.image = image;
    this.manifestId = manifestId;
    this.drawingState = drawingStateSerialized;

    console.log('WebAnnotation constructor', this);
  }

  /** */
  toJson() {
    const result =  {
      body: this.createBody(),
      id: this.id,
      motivation: 'commenting',
      target: this.target(),
      type: 'Annotation',
      drawingState: this.drawingState,
    };
    console.log('WebAnnotation toJson', result);
    return result;
  }

  /** */
  createBody() {
    let bodies = [];
    if (this.body && this.body.value !== '') {
      const textBody = {
        type: 'TextualBody',
        value: this.body.value,
      };
      bodies.push(textBody);
    }

    if (this.image) {
      // TODO dumb image { this.image.id}
      const imgBody = {
        id: 'https://tetras-libre.fr/themes/tetras/img/logo.svg',
        type: 'Image',
        format: 'image/svg+xml',
      };
      //bodies.push(imgBody);
      const testImageBody = {
            "id": "https://files.tetras-libre.fr/dev/Hakanai/media/10_HKN-Garges_A2B4243.JPG",
            "type": "Image",
            "format": "image/jpg"
          };
      bodies.push(testImageBody);
    }

    // if (this.tags) {
    //   bodies = bodies.concat(this.tags.map((tag) => ({
    //     purpose: 'tagging',
    //     type: 'TextualBody',
    //     value: tag,
    //   })));
    // }
    if (bodies.length === 1) {
      return bodies[0];
    }
    return bodies;
  }

  /** Fill target object with selectors (if any), else returns target url */
  target() {
    if (!this.svg
      && (!this.fragsel || !Object.values(this.fragsel).find((e) => e !== null))) {
      return this.canvasId;
    }
    const target = { source: this.source() };
    const selectors = [];
    if (this.svg) {
      selectors.push({
        type: 'SvgSelector',
        value: this.svg,
      });
    }
    if (this.fragsel) {
      selectors.push({
        type: 'FragmentSelector',
        value: Object.entries(this.fragsel)
          .filter((kv) => kv[1])
          .map((kv) => `${kv[0]}=${kv[1]}`)
          .join('&'),
      });
    }
    target.selector = selectors.length === 1 ? selectors[0] : selectors;
    return target;
  }

  /** */
  source() {
    let source = this.canvasId;
    if (this.manifest) {
      source = {
        id: this.canvasId,
        partOf: {
          id: this.manifest.id,
          type: 'Manifest',
        },
        type: 'Canvas',
      };
    }
    return source;
  }
}
+515 −0
Original line number Original line Diff line number Diff line
/* eslint-disable require-jsdoc */
import React, {
  useEffect, useState, useLayoutEffect,
} from 'react';
import ReactDOM from 'react-dom';
import PropTypes, { object } from 'prop-types';
import { Stage } from 'react-konva';
import { v4 as uuidv4 } from 'uuid';
// eslint-disable-next-line import/no-extraneous-dependencies
import { OSDReferences } from 'mirador/dist/es/src/plugins/OSDReferences';
// eslint-disable-next-line import/no-extraneous-dependencies
import { VideosReferences } from 'mirador/dist/es/src/plugins/VideosReferences';
import ParentComponent from './AnnotationFormOverlay/KonvaDrawing/shapes/ParentComponent';
import { OVERLAY_TOOL, SHAPES_TOOL } from '../AnnotationCreationUtils';
/** All the stuff to draw on the canvas */
function AnnotationDrawing({ drawingState, setDrawingState, height, width, ...props }) {


  useEffect(() => {
    const overlay = props.mediaVideo ? props.mediaVideo.ref.current : null;
    if (overlay) {
      props.updateScale(overlay.containerWidth / overlay.canvasWidth);
    }
  }, [{ height, width }]);

  useEffect(() => {
    // TODO clean
    if (!props.imageEvent) return;
    if (!props.imageEvent.id) return;
    const shape = {
      id: uuidv4(),
      rotation: 0,
      scaleX: 1,
      scaleY: 1,
      type: 'image',
      url: props.imageEvent.id,
      x: 0,
      y: 0,
    };

    setDrawingState({
      ...drawingState,
      currentShape: shape,
      shapes: [...drawingState.shapes, shape],
    });
  }, [props.imageEvent]);

  const { fillColor, strokeColor, strokeWidth } = props;

  useEffect(() => {
    // Perform an action when fillColor, strokeColor, or strokeWidth change
    // update current shape
    if (drawingState.currentShape) {
      drawingState.currentShape.fill = fillColor;
      drawingState.currentShape.stroke = strokeColor;
      drawingState.currentShape.strokeWidth = strokeWidth;
      updateCurrentShapeInShapes(drawingState.currentShape);
    }
  }, [fillColor, strokeColor, strokeWidth]);

  // TODO Can be removed ? --> move currentSHape and shapes in the same state
  useLayoutEffect(() => {
    if (drawingState.shapes.find((s) => s.id === drawingState.currentShape?.id)) {
      window.addEventListener('keydown', handleKeyPress);

      // Set here all the properties of the current shape for the tool options
      props.setColorToolFromCurrentShape(
        {
          fillColor: drawingState.currentShape.fill,
          strokeColor: drawingState.currentShape.stroke,
          strokeWidth: drawingState.currentShape.strokeWidth,
          text: drawingState.currentShape.text,
        },
      );

      return () => {
        window.removeEventListener('keydown', handleKeyPress);
      };
    }
  }, [drawingState.currentShape]);

  /** */
  const onShapeClick = async (shp) => {
    const shape = drawingState.shapes.find((s) => s.id === shp.id);
    if (props.activeTool === 'delete') {
      const newShapes = drawingState.shapes.filter((s) => s.id !== shape.id);
      setDrawingState({
        ...drawingState,
        shapes: newShapes,
      });
      return;
    }

    setDrawingState({
      ...drawingState,
      currentShape: shape,
    });

    // props.setShapeProperties(shape); // TODO Check that code ?
    props.setColorToolFromCurrentShape(
      {
        fillColor: shape.fill,
        strokeColor: shape.stroke,
        strokeWidth: shape.strokeWidth,
      },
    );
  };

  const onTransform = (evt) => {
    const modifiedshape = evt.target.attrs;

    const shape = drawingState.shapes.find((s) => s.id === modifiedshape.id);

    Object.assign(shape, modifiedshape);
    drawingState.currentShape = shape;
    updateCurrentShapeInShapes(drawingState.currentShape);
  };

  const handleDragEnd = (evt) => {
    const modifiedshape = evt.currentTarget.attrs;
    const shape = drawingState.shapes.find((s) => s.id === modifiedshape.id);
    shape.x = modifiedshape.x;
    shape.y = modifiedshape.y;

    updateCurrentShapeInShapes(shape);
  };

  const handleDragStart = (evt) => {
    const modifiedshape = evt.currentTarget.attrs;

    setDrawingState({
      ...drawingState,
      currentShape: drawingState.shapes.find((s) => s.id === modifiedshape.id),
    });
  };

  /** */
  const handleKeyPress = (e) => {
    e.stopPropagation();
    const unnalowedKeys = ['Shift', 'Control', 'Alt', 'Meta', 'Enter', 'Escape', 'Tab', 'AltGraph', 'CapsLock', 'NumLock', 'ScrollLock', 'Pause', 'Insert', 'Home', 'PageUp', 'PageDown', 'End', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ContextMenu', 'PrintScreen', 'Help', 'Clear', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'OS'];

    if (!drawingState.currentShape) {
      return;
    }

    if (e.key === 'Delete') {
      const shapesWithoutTheDeleted = drawingState.shapes.filter((shape) => shape.id !== drawingState.currentShape.id);
      setDrawingState({
        ...drawingState,
        shapes: shapesWithoutTheDeleted,
      });
      return;
    }

    // TODO This comportment must be handle by the text component
    if (drawingState.currentShape.type === 'text') {
      let newText = drawingState.currentShape.text;
      if (e.key === 'Backspace') {
        newText = newText.slice(0, -1);
      } else {
        if (unnalowedKeys.includes(e.key)) {
          return;
        }
        newText += e.key;
      }

      // Potentially bug during the update
      const newCurrentShape = { ...drawingState.currentShape, text: newText };

      setDrawingState({
        ...drawingState,
        shapes: drawingState.shapes.map((shape) => (shape.id === drawingState.currentShape.id ? newCurrentShape : shape)),
        currentShape: newCurrentShape,
      });
    }
  };

  /** */
  const updateCurrentShapeInShapes = (currentShape) => {
    const index = drawingState.shapes.findIndex((s) => s.id === currentShape.id);

    if (index !== -1) {
      drawingState.shapes[index] = currentShape;
      setDrawingState({
        ...drawingState,
        currentShape,
      });
    } else {
      setDrawingState({
        ...drawingState,
        shapes: [...drawingState.shapes, currentShape],
        currentShape,
      });
    }
  };

  /** */
  const handleMouseDown = (e) => {
    try {
      const pos = e.target.getStage().getRelativePointerPosition();
      pos.x /= props.scale;
      pos.y /= props.scale;
      let shape = null;
      switch (props.activeTool) {
        case SHAPES_TOOL.RECTANGLE:
          shape = {
            fill: props.fillColor,
            height: 1,
            id: uuidv4(),
            rotation: 0,
            scaleX: 1,
            scaleY: 1,
            stroke: props.strokeColor,
            strokeWidth: props.strokeWidth,
            type: props.activeTool,
            width: 1,
            x: pos.x,
            y: pos.y,
          };
          setDrawingState({
            currentShape: shape,
            isDrawing: true,
            shapes: [...drawingState.shapes, shape],
          });
          break;
        case SHAPES_TOOL.ELLIPSE:
          shape = {
            fill: props.fillColor,
            height: 1,
            id: uuidv4(),
            radiusX: 1,
            radiusY: 1,
            rotation: 0,
            scaleX: 1,
            scaleY: 1,
            stroke: props.strokeColor,
            strokeWidth: props.strokeWidth,
            type: props.activeTool,
            width: 1,
            x: pos.x,
            y: pos.y,
          };
          setDrawingState({
            currentShape: shape,
            isDrawing: true,
            shapes: [...drawingState.shapes, shape],
          });
          break;
        case 'text':
          shape = {
            fill: props.fillColor,
            fontSize: 20,
            id: uuidv4(),
            rotation: 0,
            scaleX: 1,
            scaleY: 1,
            text: 'text',
            type: OVERLAY_TOOL.TEXT,
            x: pos.x,
            y: pos.y,
          };

          setDrawingState({
            ...drawingState,
            currentShape: shape,
            shapes: [...drawingState.shapes, shape],
          });
          break;
        case SHAPES_TOOL.FREEHAND:
          shape = {
            fill: props.fillColor,
            id: uuidv4(),
            lines: [
              {
                points: [pos.x, pos.y, pos.x, pos.y],
                stroke: props.strokeColor,
                strokeWidth: props.strokeWidth,
                x: 0,
                y: 0,
              },
            ],
            rotation: 0,
            scaleX: 1,
            scaleY: 1,
            stroke: props.strokeColor,
            strokeWidth: props.strokeWidth,
            type: SHAPES_TOOL.FREEHAND,
            x: 0,
            y: 0,
          };
          setDrawingState({
            currentShape: shape,
            isDrawing: true,
            shapes: [...drawingState.shapes, shape],
          });
          break;
        case SHAPES_TOOL.POLYGON:
          shape = {
            fill: props.fillColor,
            id: uuidv4(),
            points: [pos.x, pos.y],
            rotation: 0,
            scaleX: 1,
            scaleY: 1,
            stroke: props.strokeColor,
            strokeWidth: props.strokeWidth,
            type: SHAPES_TOOL.POLYGON,
            x: 0,
            y: 0,
          };
          setDrawingState({
            currentShape: shape,
            isDrawing: true,
            shapes: [...drawingState.shapes, shape],
          });
          break;
        case SHAPES_TOOL.ARROW:
          shape = {
            fill: props.fillColor,
            id: uuidv4(),
            pointerLength: 20,
            pointerWidth: 20,
            points: [pos.x, pos.y, pos.x, pos.y],
            rotation: 0,
            scaleX: 1,
            scaleY: 1,
            stroke: props.strokeColor,
            strokeWidth: props.strokeWidth,
            type: SHAPES_TOOL.ARROW,
          };
          setDrawingState({
            currentShape: shape,
            isDrawing: true,
            shapes: [...drawingState.shapes, shape],
          });
          break;
        default:
          // Handle other cases if any
          break;
      }
    } catch (error) {
      console.error('error', error);
    }
  };

  /** */
  const handleMouseMove = (e) => {
    try {
      if (!drawingState.isDrawing) {
        return;
      }
      if (!drawingState.currentShape) {
        return;
      }
      const pos = e.target.getStage().getRelativePointerPosition();
      pos.x /= props.scale;
      pos.y /= props.scale;

      switch (props.activeTool) {
        case SHAPES_TOOL.RECTANGLE:
          updateCurrentShapeInShapes({
            ...drawingState.currentShape,
            height: pos.y - drawingState.currentShape.y,
            width: pos.x - drawingState.currentShape.x,
          });
          break;
        case SHAPES_TOOL.ELLIPSE:
          // prevent negative radius for ellipse
          if (pos.x < drawingState.currentShape.x) {
            pos.x = drawingState.currentShape.x;
          }
          if (pos.y < drawingState.currentShape.y) {
            pos.y = drawingState.currentShape.y;
          }

          updateCurrentShapeInShapes({
            ...drawingState.currentShape,
            height: pos.y - drawingState.currentShape.y,
            radiusX: (pos.x - drawingState.currentShape.x) / 2,
            radiusY: (pos.y - drawingState.currentShape.y) / 2,
            width: pos.x - drawingState.currentShape.x,
          });

          break;
        case SHAPES_TOOL.FREEHAND:
          const freehandShape = drawingState.currentShape; // TODO Check if not nuse { ...drawingState.currentShape };
          freehandShape.lines.push({
            points: [pos.x, pos.y, pos.x, pos.y],
            stroke: props.strokeColor,
            strokeWidth: props.strokeWidth,
          });
          updateCurrentShapeInShapes(freehandShape);
          break;
        case SHAPES_TOOL.POLYGON:
          const polygonShape = drawingState.currentShape;
          polygonShape.points[2] = pos.x;
          polygonShape.points[3] = pos.y;
          updateCurrentShapeInShapes(polygonShape);
          break;
        case SHAPES_TOOL.ARROW:
          // TODO improve
          const arrowShape = {};
          // update points
          arrowShape.points = [drawingState.currentShape.points[0], drawingState.currentShape.points[1], pos.x, pos.y];
          arrowShape.id = drawingState.currentShape.id;
          arrowShape.type = drawingState.currentShape.type;
          arrowShape.pointerLength = drawingState.currentShape.pointerLength;
          arrowShape.pointerWidth = drawingState.currentShape.pointerWidth;
          arrowShape.x = drawingState.currentShape.x;
          arrowShape.y = drawingState.currentShape.y;
          arrowShape.fill = props.fillColor;
          arrowShape.stroke = props.strokeColor;
          arrowShape.strokeWidth = props.strokeWidth;

          updateCurrentShapeInShapes(arrowShape);
          break;
        default:
          break;
      }
    } catch (error) {
      console.log('error', error);
    }
  };

  /** Stop drawing */
  const handleMouseUp = (e) => {
    const pos = e.target.getStage().getRelativePointerPosition();
    pos.x /= props.scale;
    pos.y /= props.scale;
    try {
      if (!drawingState.currentShape) {
        return;
      }
      // For these cases, the action is similar: stop drawing and add the shape
      setDrawingState({
        ...drawingState,
        isDrawing: false,
      });
    } catch (error) {
      console.error('error', error);
    }
  };

  /** */
  const drawKonvas = () => (
    <Stage
      width={width}
      height={height}
      style={{
        height: 'auto',
        left: 0,
        objectFit: 'contain',
        overflow: 'clip',
        overflowClipMargin: 'content-box',
        position: 'absolute',
        top: 0,
        width: '100%',
      }}
      onMouseDown={handleMouseDown}
      onMouseUp={handleMouseUp}
      onMouseMove={handleMouseMove}
      id={props.windowId}
    >
      <ParentComponent
        shapes={drawingState.shapes}
        onShapeClick={onShapeClick}
        activeTool={props.activeTool}
        selectedShapeId={drawingState.currentShape?.id}
        style={{
          height: 'auto',
          left: 0,
          objectFit: 'contain',
          overflow: 'clip',
          overflowClipMargin: 'content-box',
          position: 'absolute',
          top: 0,
          width: '100%',
        }}
        scale={props.scale}
        width={props.originalWidth}
        height={props.originalHeight}
        onTransform={onTransform}
        handleDragEnd={handleDragEnd}
        handleDragStart={handleDragStart}
        isMouseOverSave={props.isMouseOverSave}
      />
    </Stage>
  );
  const osdref = OSDReferences.get(props.windowId);
  const videoref = VideosReferences.get(props.windowId);
  if (!osdref && !videoref) {
    throw new Error("Unknown or missing data player, didn't found OpenSeadragon (image viewer) nor the video player");
  }
  if (osdref && videoref) {
    throw new Error('Unhandled case: both OpenSeadragon (image viewer) and video player on the same canvas');
  }
  const container = osdref ? osdref.current.container : videoref.ref.current.parentElement;

  return ReactDOM.createPortal(drawKonvas(), container);
}

AnnotationDrawing.propTypes = {
  activeTool: PropTypes.string.isRequired,
  closed: PropTypes.bool.isRequired,
  drawingState: PropTypes.object.isRequired,
  fillColor: PropTypes.string.isRequired,
  selectedShapeId: PropTypes.string.isRequired,
  strokeColor: PropTypes.string.isRequired,
  strokeWidth: PropTypes.number.isRequired,
  svg: PropTypes.func.isRequired,
  updateGeometry: PropTypes.func.isRequired,
  windowId: PropTypes.string.isRequired,
};

export default AnnotationDrawing;
Original line number Original line Diff line number Diff line
import React from 'react';
import { Grid, Paper, Typography } from '@mui/material';
import PropTypes from 'prop-types';
import TextEditor from '../TextEditor';

/** Form part for edit annotation content and body */
function AnnotationFormContent({ textBody, updateTextBody, textEditorStateBustingKey }) {
  return (
    <Paper style={{ padding: '5px' }}>
      <Typography variant="overline">
        Infos
      </Typography>
      <Grid>
        <TextEditor
          key={textEditorStateBustingKey}
          annoHtml={textBody}
          updateAnnotationBody={updateTextBody}
        />
      </Grid>
    </Paper>
  );
}

AnnotationFormContent.propTypes = {
  textBody: PropTypes.string.isRequired,
  textEditorStateBustingKey: PropTypes.string.isRequired,
  updateTextBody: PropTypes.func.isRequired,
};

export default AnnotationFormContent;
Original line number Original line Diff line number Diff line
import { Button } from '@mui/material';
import PropTypes from 'prop-types';
import { styled } from '@mui/material/styles';
import React from 'react';
import { v4 as uuid } from 'uuid';
import {
  saveAnnotationInEachCanvas,
} from '../AnnotationCreationUtils';
import { removeHTMLTags, secondsToHMS } from '../utils';
import {
  getKonvaAsDataURL,
} from './AnnotationFormOverlay/KonvaDrawing/KonvaUtils';

const StyledButtonDivSaveOrCancel = styled('div')(({ theme }) => ({
  display: 'flex',
  justifyContent: 'flex-end',
}));

/** Annotation form footer, save or cancel the edition/creation of an annotation */
function AnnotationFormFooter({
  annotation,
  canvases,
  closeFormCompanionWindow,
  config,
  drawingState,
  receiveAnnotation,
  resetStateAfterSave,
  state,
  windowId,
}) {
  /**
   * Validate form and save annotation
   */
  const submitAnnotationForm = async (e) => {
    e.preventDefault();
    // TODO Possibly problem of syncing
    // TODO Improve this code
    // If we are in edit mode, we have the transformer on the stage saved in the annotation
    /* if (viewTool === OVERLAY_VIEW && state.activeTool === 'edit') {
      setState((prevState) => ({
        ...prevState,
        activeTool: 'cursor',
      }));
      return;
    } */

    const {
      textBody,
      xywh,
      tstart,
      tend,
      manifestNetwork,
    } = state;

    // Temporal target of the annotation
    const target = {
      t: (tstart && tend) ? `${tstart},${tend}` : null,
      xywh, // TODO retrouver calcul de xywh
    };

    let annotationText;
    if (textBody.length == 0 || removeHTMLTags(textBody).length == 0) {
      if (target.t) {
        annotationText = `${new Date().toLocaleString()} - ${secondsToHMS(tstart)} -> ${secondsToHMS(tend)}`;
      } else {
        annotationText = new Date().toLocaleString();
      }
    } else {
      annotationText = textBody;
    }

    let id = annotation?.id ? annotation.id : `https://${uuid()}`;
    id = id.split('#')[0];
    if (manifestNetwork) {
      id = `${id}#${manifestNetwork}`;
    }

    const annotationToSaved = {
      body: {
        id: null, // Will be updated after
        type: 'Image',
        format: 'image/svg+xml',
        value: annotationText,
      },
      drawingState: JSON.stringify(drawingState),
      id,
      manifestNetwork,
      motivation: 'commenting',
      target: null,
      type: 'Annotation', // Will be updated in saveAnnotationInEachCanvas
    };

    const isNewAnnotation = !annotation;

    // Save jpg image of the drawing in a data url
    getKonvaAsDataURL(windowId).then((dataURL) => {
      console.log('dataURL:', dataURL);
      const annotation = { ...annotationToSaved };
      annotation.body.id = dataURL;
      saveAnnotationInEachCanvas(canvases, config, receiveAnnotation, annotation, target, isNewAnnotation);
      closeFormCompanionWindow();
      resetStateAfterSave();
    });
  };

  return (
    <StyledButtonDivSaveOrCancel>
      <Button onClick={closeFormCompanionWindow}>
        Cancel
      </Button>
      <Button
        variant="contained"
        color="primary"
        type="submit"
        onClick={submitAnnotationForm}
      >
        Save
      </Button>
    </StyledButtonDivSaveOrCancel>
  );
}

AnnotationFormFooter.propTypes = {
  annotation: PropTypes.object, // eslint-disable-line react/forbid-prop-types
  canvases: PropTypes.arrayOf(PropTypes.object).isRequired,
  closeFormCompanionWindow: PropTypes.func.isRequired,
  config: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
  drawingState: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
  receiveAnnotation: PropTypes.func.isRequired,
  resetStateAfterSave: PropTypes.func.isRequired,
  state: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
  windowId: PropTypes.string.isRequired,
};

export default AnnotationFormFooter;
Original line number Original line Diff line number Diff line
import React from 'react';
import {
  Grid, Paper, TextField, Typography, Button, Link,
} from '@mui/material';
import PropTypes from 'prop-types';
import { isValidUrl } from '../utils';

/** Form part for edit annotation content and body */
function AnnotationFormNetwork({ manifestNetwork, updateManifestNetwork }) {
  return (
    <Paper style={{ padding: '5px' }}>
      <Typography variant="overline">
        Network
      </Typography>
      <Grid>
        <TextField
          value={manifestNetwork}
          onChange={(event) => updateManifestNetwork(event.target.value.trim())}
          label="Manifest URL"
          type="url"
        />
        {
          isValidUrl(manifestNetwork) ? (
            <Link
              href={manifestNetwork}
              target="_blank"
            >
              {manifestNetwork}
            </Link>
          ) : (
            <Typography variant="caption">
              Not a valid URL
            </Typography>
          )
        }
      </Grid>
    </Paper>
  );
}

AnnotationFormNetwork.propTypes = {
  manifestNetwork: PropTypes.string.isRequired,
  updateManifestNetwork: PropTypes.func.isRequired,
};

export default AnnotationFormNetwork;
Original line number Original line Diff line number Diff line
import {
  Accordion, AccordionDetails, AccordionSummary, Paper,
} from '@mui/material';
import React from 'react';
import Typography from '@mui/material/Typography';
import PropTypes from 'prop-types';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import Button from '@mui/material/Button';
import DeleteIcon from '@mui/icons-material/DeleteForever';

function AccordionShapes({ shapes, deleteShape, currentShapeId }) {
  return (
    <Paper>
      {shapes.map((shape) => (
        <Accordion style={shape.id === currentShapeId ? { fontWeight: 'bold' } : {}}>
          <AccordionSummary
            expandIcon={<ExpandMoreIcon />}
            aria-controls="panel1-content"
            id="panel1-header"
          >
            {shape.id}
          </AccordionSummary>
          <AccordionDetails>
            <ul>
              {Object.keys(shape).sort().map((key) => (
                <>
                  { key !== 'lines' && key !== 'image' && (
                  <li key={key}>
                    {key}
                    :
                    {shape[key]}
                  </li>
                  )}
                </>
              ))}
            </ul>
            <Button
              onClick={() => deleteShape(shape.id)}
            >
              <DeleteIcon />
            </Button>
          </AccordionDetails>
        </Accordion>

      ))}
    </Paper>
  );
}

AccordionShapes.propTypes = {
  shapes: PropTypes.array.isRequired,
  deleteShape: PropTypes.func.isRequired,
  currentShapeId: PropTypes.string.isRequired,
};

export default AccordionShapes;
Original line number Original line Diff line number Diff line
import {
  Button, Grid, Paper,
} from '@mui/material';
import Typography from '@mui/material/Typography';
import ToggleButton from '@mui/material/ToggleButton';
import TitleIcon from '@mui/icons-material/Title';
import ImageIcon from '@mui/icons-material/Image';
import DeleteIcon from '@mui/icons-material/Delete';
import React, { useEffect } from 'react';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import PropTypes from 'prop-types';
import { styled } from '@mui/material/styles';
import CategoryIcon from '@mui/icons-material/Category';
import CursorIcon from '../../icons/Cursor';
import AnnotationFormOverlayTool from './AnnotationFormOverlayTool';
import { OVERLAY_TOOL } from '../../AnnotationCreationUtils';

const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }) => ({
  '&:first-of-type': {
    borderRadius: theme.shape.borderRadius,
  },
  '&:not(:first-of-type)': {
    borderRadius: theme.shape.borderRadius,
  },
  border: 'none',
  margin: theme.spacing(0.5),
}));


const StyledPaper = styled(Paper)(({ theme }) => ({
  padding: '5px',
}));

/** All the stuff to manage to choose the drawing tool */
function AnnotationFormOverlay({
  updateToolState, toolState, deleteShape, currentShape, shapes
}) {
  useEffect(() => {

  }, [toolState.fillColor, toolState.strokeColor, toolState.strokeWidth]);

  const changeTool = (e, tool) => {
    updateToolState({
      ...toolState,
      activeTool: tool,
      currentShape: null,
    });
  };



  const {
    activeTool,
  } = toolState;

  return (
    <StyledPaper>
      <div>
        <Grid container>
          <Grid item xs={12}>
            <Typography variant="overline">
              Overlay
            </Typography>
          </Grid>
          <Grid item xs={12}>
            <StyledToggleButtonGroup
              value={activeTool} // State or props ?
              exclusive
              onChange={changeTool}
              aria-label="tool selection"
              size="small"
            >
              <ToggleButton value={OVERLAY_TOOL.EDIT} aria-label="select cursor">
                <CursorIcon />
              </ToggleButton>
              <ToggleButton value={OVERLAY_TOOL.SHAPE} aria-label="select cursor">
                <CategoryIcon />
              </ToggleButton>
              <ToggleButton value={OVERLAY_TOOL.IMAGE} aria-label="select cursor">
                <ImageIcon />
              </ToggleButton>
              <ToggleButton value={OVERLAY_TOOL.TEXT} aria-label="select text">
                <TitleIcon />
              </ToggleButton>
              <ToggleButton value={OVERLAY_TOOL.DELETE} aria-label="select cursor">
                <DeleteIcon />
              </ToggleButton>
            </StyledToggleButtonGroup>
            <AnnotationFormOverlayTool
              toolState={toolState}
              updateToolState={updateToolState}
              currentShape={currentShape}
              shapes={shapes}
              deleteShape={deleteShape}
            />
          </Grid>
        </Grid>
      </div>
    </StyledPaper>
  );
}

AnnotationFormOverlay.propTypes = {
  currentShape: PropTypes.object.isRequired,
  deleteShape: PropTypes.func.isRequired,
  shapes: PropTypes.array.isRequired,
  toolState: PropTypes.shape({
    activeTool: PropTypes.string.isRequired,
    closedMode: PropTypes.bool.isRequired,
    fillColor: PropTypes.string.isRequired,
    image: PropTypes.shape({
      id: PropTypes.string,
    }).isRequired,
    strokeColor: PropTypes.string.isRequired,
    strokeWidth: PropTypes.number.isRequired,
    updateColor: PropTypes.func.isRequired,
  }).isRequired,
  updateToolState: PropTypes.func.isRequired,
};

export default AnnotationFormOverlay;
Original line number Original line Diff line number Diff line
import ToggleButton from '@mui/material/ToggleButton';
import RectangleIcon from '@mui/icons-material/CheckBoxOutlineBlank';
import CircleIcon from '@mui/icons-material/RadioButtonUnchecked';
import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward';
import PolygonIcon from '@mui/icons-material/Timeline';
import GestureIcon from '@mui/icons-material/Gesture';
import PropTypes from 'prop-types';
import React from 'react';
import { styled } from '@mui/material/styles';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import { Button, Paper } from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import Typography from '@mui/material/Typography';
import AnnotationFormOverlayToolOptions from './AnnotationFormOverlayToolOptions';
import { isShapesTool, OVERLAY_TOOL, SHAPES_TOOL } from '../../AnnotationCreationUtils';
import AccordionShapes from './Accordion';

const StyledLi = styled('li')(({ theme }) => ({
  display: 'flex',
  wordBreak: 'break-word',
}));

const StyledUl = styled('ul')(({ theme }) => ({
  display: 'flex',
  flexDirection: 'column',
  gap: '5px',
  listStyle: 'none',
  paddingLeft: '0',
}));

// TODO WIP code duplicated
const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }) => ({
  '&:first-of-type': {
    borderRadius: theme.shape.borderRadius,
  },
  '&:not(:first-of-type)': {
    borderRadius: theme.shape.borderRadius,
  },
  border: 'none',
  margin: theme.spacing(0.5),
}));

/** All the form part for the overlay view */
function AnnotationFormOverlayTool({
  toolState, updateToolState, currentShape, shapes, deleteShape,
}) {
  /** Change the active overlay tool */
  const changeTool = (e, tool) => {
    updateToolState({
      ...toolState,
      activeTool: tool,
    });
  };

  /** Stay in edit mode when a shape is selected */
  const customUpdateToolState = (newState) => {
    updateToolState({
      ...newState,
      activeTool: OVERLAY_TOOL.EDIT,
    });
  };

  return (
    <>
      {
          toolState.activeTool === OVERLAY_TOOL.EDIT && (
          <>
            {
            currentShape && (
            <Paper>
              <Typography variant="overline">
                Selected object
              </Typography>
              {/* <ul> // useful for debug */}
              {/*   { */}
              {/*     Object.keys(currentShape).sort().map((key) => ( */}
              {/*       <> */}
              {/*         { key !== 'lines' && key !== 'image' && ( */}
              {/*           <li key={key}> */}
              {/*             {key} */}
              {/*             : */}
              {/*             {currentShape[key]} */}
              {/*           </li> */}
              {/*         )} */}
              {/*       </> */}
              {/*     )) */}
              {/*   } */}
              {/* </ul> */}
              <AnnotationFormOverlayToolOptions
                toolState={{
                  ...toolState,
                  activeTool: currentShape.type,
                  closedMode: currentShape.closedMode,
                  fillColor: currentShape.fill,
                  image: { id: currentShape.url },
                  strokeColor: currentShape.stroke,
                  strokeWidth: currentShape.strokeWidth,
                  text: currentShape.text,
                }}
                updateToolState={customUpdateToolState}

              />
            </Paper>
            )
            }
            {
              shapes.length > 0 && (
                <>
                  <Typography variant="overline">
                    Object lists
                  </Typography>
                  <AccordionShapes
                    currentShapeId={currentShape?.id}
                    shapes={shapes}
                    deleteShape={deleteShape}
                  />
                </>
              )
            }
          </>

          )
      }
      {
        isShapesTool(toolState.activeTool) && (
        <>
          <Typography variant="overline">
            Drawing tool
          </Typography>
          <StyledToggleButtonGroup
            value={toolState.activeTool} // State or props ?
            exclusive
            onChange={changeTool}
            aria-label="tool selection"
            size="small"
          >
            <ToggleButton value={SHAPES_TOOL.RECTANGLE} aria-label="add a rectangle">
              <RectangleIcon />
            </ToggleButton>
            <ToggleButton value={SHAPES_TOOL.ELLIPSE} aria-label="add a circle">
              <CircleIcon />
            </ToggleButton>
            <ToggleButton value={SHAPES_TOOL.ARROW} aria-label="add an arrow">
              <ArrowOutwardIcon />
            </ToggleButton>
            <ToggleButton value={SHAPES_TOOL.POLYGON} aria-label="add a polygon" style={{ display: 'none' }}>
              <PolygonIcon />
            </ToggleButton>
            <ToggleButton value={SHAPES_TOOL.FREEHAND} aria-label="free hand polygon">
              <GestureIcon />
            </ToggleButton>
          </StyledToggleButtonGroup>
        </>
        )
      }
      {
        toolState.activeTool === OVERLAY_TOOL.DELETE && (
        <>
          <Typography variant="overline">
            Delete
          </Typography>
          <p>
            Click on object to remove it.
          </p>
          <Button
            onClick={() => deleteShape()}
          >
            <span>Delete all</span>
            <DeleteIcon color="red" />
          </Button>
        </>
        )
      }
      <AnnotationFormOverlayToolOptions
        toolState={toolState}
        updateToolState={updateToolState}
      />
    </>
  );
}

AnnotationFormOverlayTool.propTypes = {
  currentShape: PropTypes.object.isRequired,
  deleteShape: PropTypes.func.isRequired,
  shapes: PropTypes.array.isRequired,
  toolState: PropTypes.shape({
    activeTool: PropTypes.string.isRequired,
    closedMode: PropTypes.bool.isRequired,
    fillColor: PropTypes.string.isRequired,
    image: PropTypes.shape({
      id: PropTypes.string,
    }).isRequired,
    strokeColor: PropTypes.string.isRequired,
    strokeWidth: PropTypes.number.isRequired,
    updateColor: PropTypes.func.isRequired,
  }).isRequired,
  updateToolState: PropTypes.func.isRequired,

};

export default AnnotationFormOverlayTool;
Original line number Original line Diff line number Diff line
import {
  Button,
  ClickAwayListener, Divider, Grid, MenuItem, MenuList, Paper, Popover, TextField,
} from '@mui/material';
import Typography from '@mui/material/Typography';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import ToggleButton from '@mui/material/ToggleButton';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import StrokeColorIcon from '@mui/icons-material/BorderColor';
import LineWeightIcon from '@mui/icons-material/LineWeight';
import FormatColorFillIcon from '@mui/icons-material/FormatColorFill';
import React, { useEffect, useState } from 'react';
import ClosedPolygonIcon from '@mui/icons-material/ChangeHistory';
import OpenPolygonIcon from '@mui/icons-material/ShowChart';
import PropTypes from 'prop-types';
import { styled } from '@mui/material/styles';
import { SketchPicker } from 'react-color';
import AddPhotoAlternateIcon from '@mui/icons-material/AddPhotoAlternate';
import { v4 as uuidv4 } from 'uuid';
import ImageFormField from './ImageFormField';
import { isShapesTool, OVERLAY_TOOL } from '../../AnnotationCreationUtils';
import { defaultLineWeightChoices } from './KonvaDrawing/KonvaUtils';

const StyledDivider = styled(Divider)(({ theme }) => ({
  margin: theme.spacing(1, 0.5),
}));

const StyledDivButtonImage = styled('div')(({ theme }) => ({
  display: 'flex',
  justifyContent: 'flex-end',
  marginTop: '5px',
}));

/** Utils functions to convert string to object */
const rgbaToObj = (rgba = 'rgba(255,255,255,0.5)') => {
  const rgbaArray = rgba.split(',');
  return {
    // eslint-disable-next-line sort-keys
    r: Number(rgbaArray[0].split('(')[1]),
    // eslint-disable-next-line sort-keys
    g: Number(rgbaArray[1]),
    // eslint-disable-next-line sort-keys
    b: Number(rgbaArray[2]),
    // eslint-disable-next-line sort-keys
    a: Number(rgbaArray[3].split(')')[0]),
  };
};

/** Convert color object to rgba string */
const objToRgba = (obj = {
  // eslint-disable-next-line sort-keys
  r: 255, g: 255, b: 255, a: 0.5,
}) => `rgba(${obj.r},${obj.g},${obj.b},${obj.a})`;

/** All the tools options for the overlay options */
function AnnotationFormOverlayToolOptions({ updateToolState, toolState }) {
  // set toolOptionsValue
  const [toolOptions, setToolOptions] = useState({
    colorPopoverOpen: false,
    currentColorType: null,
    lineWeightPopoverOpen: false,
    popoverAnchorEl: null,
    popoverLineWeightAnchorEl: null,
  });

  useEffect(() => {
    // TODO: This useEffect fix the bug on konva to svg but may be useless
  }, []);
  // Set unused default color to avoid error on render
  const currentColor = toolOptions.currentColorType ? rgbaToObj(toolState[toolOptions.currentColorType]) : 'rgba(255, 0, 0, 0.5)';

  // Fonction to manage option displaying
  /** */
  const openChooseLineWeight = (e) => {
    setToolOptions({
      ...toolOptions,
      lineWeightPopoverOpen: true,
      popoverLineWeightAnchorEl: e.currentTarget,
    });
  };

  /** */
  const handleLineWeightSelect = (e) => {
    setToolOptions({
      ...toolOptions,
      lineWeightPopoverOpen: false,
      popoverLineWeightAnchorEl: null,
    });
    updateToolState({
      ...toolState,
      strokeWidth: e.currentTarget.value,
    });
  };

  /** Close color popover window */
  const closeChooseColor = (e) => {
    setToolOptions({
      ...toolOptions,
      colorPopoverOpen: false,
      currentColorType: null,
      popoverAnchorEl: null,
    });
  };

  /** */
  const openChooseColor = (e) => {
    console.log('openChooseColor', e.currentTarget.value);
    setToolOptions({
      ...toolOptions,
      colorPopoverOpen: true,
      currentColorType: e.currentTarget.value,
      popoverAnchorEl: e.currentTarget,
    });
  };

  /** */
  const handleCloseLineWeight = (e) => {
    setToolOptions({
      ...toolOptions,
      lineWeightPopoverOpen: false,
      popoverLineWeightAnchorEl: null,
    });
  };

  /**  closed mode change */
  const changeClosedMode = (e) => {
    updateToolState({
      ...toolState,
      closedMode: e.currentTarget.value,
    });
  };

  /** Update color : fillColor or strokeColor */
  const updateColor = (color) => {
    updateToolState({
      ...toolState,
      [toolOptions.currentColorType]: objToRgba(color.rgb),
    });
  };

  const addImage = () => {
    const data = {
      id: toolState?.image?.id,
      uuid: uuidv4(),
    };

    updateToolState({
      ...toolState,
      image: { id: null },
      imageEvent: data,
    });
  };

  /** TODO Code duplicate ?? */
  const handleImgChange = (newUrl, imgRef) => {
    updateToolState({
      ...toolState,
      image: { ...toolState.image, id: newUrl },
    });
  };

  return (
    <div>
      {
        isShapesTool(toolState.activeTool) && (
          <Grid container>
            <Grid item xs={12}>
              <Typography variant="overline">
                Style
              </Typography>
            </Grid>
            <Grid item xs={12}>
              <ToggleButtonGroup
                aria-label="style selection"
                size="small"
              >
                <ToggleButton
                  value="strokeColor"
                  aria-label="select color"
                  onClick={openChooseColor}
                >
                  <StrokeColorIcon style={{ fill: toolState.strokeColor }} />
                  <ArrowDropDownIcon />
                </ToggleButton>
                <ToggleButton
                  value="strokeColor"
                  aria-label="select line weight"
                  onClick={openChooseLineWeight}
                >
                  <LineWeightIcon />
                  <ArrowDropDownIcon />
                </ToggleButton>
                <ToggleButton
                  value="fillColor"
                  aria-label="select color"
                  onClick={openChooseColor}
                >
                  <FormatColorFillIcon style={{ fill: toolState.fillColor }} />
                  <ArrowDropDownIcon />
                </ToggleButton>
              </ToggleButtonGroup>

              <StyledDivider flexItem orientation="vertical" />
              { /* close / open polygon mode only for freehand drawing mode. */
              false
                && (
                  <ToggleButtonGroup
                    size="small"
                    value={toolState.closedMode}
                    onChange={changeClosedMode}
                  >
                    <ToggleButton value="closed">
                      <ClosedPolygonIcon />
                    </ToggleButton>
                    <ToggleButton value="open">
                      <OpenPolygonIcon />
                    </ToggleButton>
                  </ToggleButtonGroup>
                )
            }
            </Grid>
            <Popover
              open={toolOptions.lineWeightPopoverOpen}
              anchorEl={toolOptions.popoverLineWeightAnchorEl}
            >
              <Paper>
                <ClickAwayListener onClickAway={handleCloseLineWeight}>
                  <MenuList autoFocus role="listbox">
                    {defaultLineWeightChoices.map((option, index) => (
                      <MenuItem
                        key={option}
                        onClick={handleLineWeightSelect}
                        value={option}
                        selected={option === toolState.strokeWidth}
                        role="option"
                        aria-selected={option === toolState.strokeWidth}
                      >
                        {option}
                      </MenuItem>
                    ))}
                  </MenuList>
                </ClickAwayListener>
              </Paper>
            </Popover>
            <Popover
              open={toolOptions.colorPopoverOpen}
              anchorEl={toolOptions.popoverAnchorEl}
              onClose={closeChooseColor}
            >
              <SketchPicker
                disableAlpha={false}
                color={currentColor}
                onChangeComplete={updateColor}
              />
            </Popover>
          </Grid>
        )
      }
      {
          toolState.activeTool === OVERLAY_TOOL.IMAGE && (
          <>
            <Typography variant="overline">
              Add image from URL
            </Typography>
            <Grid container>
              <ImageFormField xs={8} value={toolState.image} onChange={handleImgChange} />
            </Grid>
            <StyledDivButtonImage>
              <Button variant="contained" onClick={addImage}>
                <AddPhotoAlternateIcon />
              </Button>
            </StyledDivButtonImage>
          </>
          )
      }
      {
          toolState.activeTool === 'text' && (
          <>
            <Typography variant="overline">
              Text
            </Typography>
            { toolState.text ? (
              <TextField
                value={toolState.text}
                fullWidth
              />
            ) : (
              <p>Click on canva to add text</p>
            )}
          </>
          )
      }
    </div>
  );
}

AnnotationFormOverlayToolOptions.propTypes = {
  toolState: PropTypes.shape({
    activeTool: PropTypes.string.isRequired,
    closedMode: PropTypes.bool.isRequired,
    fillColor: PropTypes.string.isRequired,
    image: PropTypes.shape({
      id: PropTypes.string,
    }).isRequired,
    strokeColor: PropTypes.string.isRequired,
    strokeWidth: PropTypes.number.isRequired,
    textBody: PropTypes.string,
    updateColor: PropTypes.func.isRequired,
  }).isRequired,
  updateToolState: PropTypes.func.isRequired,
};

export default AnnotationFormOverlayToolOptions;
Original line number Original line Diff line number Diff line
import React, { useRef, useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { TextField, styled } from '@mui/material';

const StyledRoot = styled('div')(({ theme }) => ({
  alignItems: 'center',
  display: 'flex',
  flexDirection: 'column',
  justifyContent: 'center',
}));

const StyledTextField = styled(TextField)(({ theme }) => ({
  marginBottom: '0',
  marginTop: '0',
}));

/** Image input field for the annotation form */
function ImageFormField({ value: image, onChange }) {
  const inputRef = useRef(null);
  const [imgIsValid, setImgIsValid] = useState(false);

  useEffect(() => {
    if (inputRef.current) {
      setImgIsValid(image.id && inputRef.current.checkValidity());
    } else {
      setImgIsValid(!!image.id);
    }
  }, [image]);

  const imgUrl = image.id === null ? '' : image.id;

  return (
    <StyledRoot>
      <StyledTextField
        value={imgUrl}
        onChange={(ev) => onChange(ev.target.value)}
        error={imgUrl !== '' && !imgIsValid}
        margin="dense"
        label="Image URL"
        type="url"
        fullWidth
        inputRef={inputRef}
      />
      {imgIsValid && <img src={image.id} width="100%" height="auto" alt="loading failed" />}
    </StyledRoot>
  );
}

ImageFormField.propTypes = {
  onChange: PropTypes.func.isRequired,
  value: PropTypes.shape({
    id: PropTypes.string,
  }).isRequired,
};

export default ImageFormField;
Original line number Original line Diff line number Diff line
import { exportStageSVG } from 'react-konva-to-svg';

/**
 * Get SVG picture containing all the stuff draw in the stage (Konva Stage).
 * This image will be put in overlay of the iiif media
 */
export async function getSvg(windowId) {
  const stage = window.Konva.stages.find((s) => s.attrs.id === windowId);
  const svg = await exportStageSVG(stage, false); // TODO clean
  console.log('SVG:', svg);
  return svg;
}

/** Export the stage as a JPG image in a data url */
export async function getKonvaAsDataURL(windowId) {
  const stage = window.Konva.stages.find((s) => s.attrs.id === windowId);
  const dataURL = stage.toDataURL({
    mimeType: 'image/png',
    quality: 1,
  });
  console.log('dataURL:', dataURL);
  return dataURL;
}

export const defaultLineWeightChoices = [0, 2, 5, 10, 20, 50];
Original line number Original line Diff line number Diff line
import React, { useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Arrow, Transformer } from 'react-konva';

/**  */
function ArrowNode({
  onShapeClick, shape, activeTool, isSelected, onTransform, handleDragEnd,
}) {
  const shapeRef = useRef();
  const trRef = useRef();

  useEffect(() => {
    if (trRef.current) {
      trRef.current.nodes([shapeRef.current]);
      trRef.current.getLayer().batchDraw();
    }
  }, [isSelected]);

  /** handle click on the arrow */
  const handleClick = () => {
    onShapeClick(shape);
  };

  return (
    <>
      <Arrow
        ref={shapeRef}
        fill={shape.fill}
        scaleX={shape.scaleX}
        scaleY={shape.scaleY}
        rotation={shape.rotation}
        x={shape.x}
        y={shape.y}
        stroke={shape.stroke}
        strokeWidth={shape.strokeWidth}
        points={shape.points}
        id={shape.id}
        draggable={activeTool === 'cursor' || activeTool === 'edit'}
        onClick={handleClick}
        pointerLength={shape.pointerLength}
        pointerWidth={shape.pointerWidth}
        onTransform={onTransform}
        onDragEnd={handleDragEnd}
        onDragStart={handleDragEnd}
      />
      <Transformer
        ref={trRef}
        visible={activeTool === 'edit' && isSelected}
      />
    </>
  );
}

ArrowNode.propTypes = {
  activeTool: PropTypes.string,
  handleDragEnd: PropTypes.func.isRequired,
  handleDragStart: PropTypes.func.isRequired,
  isSelected: PropTypes.bool.isRequired,
  onShapeClick: PropTypes.func.isRequired,
  onTransform: PropTypes.func.isRequired,
  shape: PropTypes.object.isRequired,
};


export default ArrowNode;
Original line number Original line Diff line number Diff line
import React from 'react';
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
import SvgIcon from '@mui/material/SvgIcon';


/**
/**
 * CursorIcon ~
 * CursorIcon ~
+6 −2

File changed.

Preview size limit exceeded, changes collapsed.

+36 −0

File changed.

Preview size limit exceeded, changes collapsed.