├── .gitignore
├── LICENSE
├── README.md
├── assets
├── 1280px-Hallstatt.jpg
├── 640px-Hallstatt.jpg
├── Carta_Marina.jpeg
├── openseadragon-bin-2.4.2
│ ├── LICENSE.txt
│ ├── changelog.txt
│ ├── images
│ │ ├── button_grouphover.png
│ │ ├── button_hover.png
│ │ ├── button_pressed.png
│ │ ├── button_rest.png
│ │ ├── flip_grouphover.png
│ │ ├── flip_hover.png
│ │ ├── flip_pressed.png
│ │ ├── flip_rest.png
│ │ ├── fullpage_grouphover.png
│ │ ├── fullpage_hover.png
│ │ ├── fullpage_pressed.png
│ │ ├── fullpage_rest.png
│ │ ├── home_grouphover.png
│ │ ├── home_hover.png
│ │ ├── home_pressed.png
│ │ ├── home_rest.png
│ │ ├── next_grouphover.png
│ │ ├── next_hover.png
│ │ ├── next_pressed.png
│ │ ├── next_rest.png
│ │ ├── previous_grouphover.png
│ │ ├── previous_hover.png
│ │ ├── previous_pressed.png
│ │ ├── previous_rest.png
│ │ ├── rotateleft_grouphover.png
│ │ ├── rotateleft_hover.png
│ │ ├── rotateleft_pressed.png
│ │ ├── rotateleft_rest.png
│ │ ├── rotateright_grouphover.png
│ │ ├── rotateright_hover.png
│ │ ├── rotateright_pressed.png
│ │ ├── rotateright_rest.png
│ │ ├── zoomin_grouphover.png
│ │ ├── zoomin_hover.png
│ │ ├── zoomin_pressed.png
│ │ ├── zoomin_rest.png
│ │ ├── zoomout_grouphover.png
│ │ ├── zoomout_hover.png
│ │ ├── zoomout_pressed.png
│ │ └── zoomout_rest.png
│ ├── openseadragon.js
│ ├── openseadragon.js.map
│ ├── openseadragon.min.js
│ └── openseadragon.min.js.map
├── openseadragon-bin-3.0.0
│ └── openseadragon.3.0.0.min.js
└── vasques_07.jpg
└── plugins
├── annotorious-better-polygon
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── index.html
│ └── osd.html
├── screencast.gif
├── src
│ ├── ImEditablePolygon.js
│ ├── ImRubberbandPolygon.js
│ ├── ImRubberbandPolygonTool.js
│ ├── index.css
│ └── index.js
└── webpack.config.js
├── annotorious-find-contours
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── index.html
│ └── opencv.js
├── src
│ └── index.js
└── webpack.config.js
├── annotorious-hover-tooltip
├── .gitignore
├── package-lock.json
├── package.json
├── public
│ └── index.html
├── src
│ ├── index.css
│ └── index.js
└── webpack.config.js
├── annotorious-map-annotation
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── index.html
│ └── sample-annotations.json
├── src
│ ├── crosswalk
│ │ ├── fragment
│ │ │ └── index.js
│ │ ├── index.js
│ │ └── svg
│ │ │ ├── ellipse.js
│ │ │ ├── index.js
│ │ │ ├── path.js
│ │ │ └── polygon.js
│ └── index.js
└── webpack.config.js
├── annotorious-osd-snap-polygon
├── .gitignore
├── package-lock.json
├── package.json
├── public
│ ├── annotations.json
│ ├── default.jpg
│ ├── dev
│ │ ├── annotorious-openseadragon.umd.js.map
│ │ ├── annotorious.min.css
│ │ └── openseadragon-annotorious.min.js
│ └── index.html
├── src
│ ├── SnapCursor.js
│ ├── SnapEditablePath.js
│ ├── SnapEditablePolygon.js
│ ├── SnapPolygonTool.js
│ ├── SnapRubberbandPolygon.js
│ ├── index.css
│ ├── index.js
│ └── storeUtils.js
└── webpack.config.js
├── annotorious-segment-outline
├── README.md
├── public
│ └── index.html
├── src
│ └── index.js
└── webpack.config.js
├── annotorious-sequence-mode
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── index.html
│ └── wadirum.jpg
├── src
│ └── index.js
└── webpack.config.js
├── annotorious-shape-labels
├── README.md
├── dist
│ ├── annotorious-shape-labels.min.js
│ └── index.html
├── package-lock.json
├── package.json
├── public
│ ├── index.html
│ └── osd.html
├── screenshot.jpg
├── src
│ ├── index.css
│ └── index.js
└── webpack.config.js
├── annotorious-tensorflow-tag-suggestions
├── .gitignore
├── README.md
├── babel.config.js
├── dist
│ └── index.html
├── package-lock.json
├── package.json
├── rollup.config.js
├── screenshot.gif
└── src
│ └── index.js
├── annotorious-tilted-box
├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── public
│ └── index.html
├── screencap.gif
├── src
│ ├── EditableTiltedBox.js
│ ├── Geom2D.js
│ ├── RubberbandTiltedBox.js
│ ├── TiltedBox.js
│ ├── TiltedBoxTool.js
│ ├── TiltedBoxTool.scss
│ └── index.js
└── webpack.config.js
├── annotorious-toolbar
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
│ └── index.html
├── screencap.gif
├── src
│ ├── icons
│ │ ├── Circle.js
│ │ ├── Ellipse.js
│ │ ├── Freehand.js
│ │ ├── Line.js
│ │ ├── Mouse.js
│ │ ├── Point.js
│ │ ├── Polygon.js
│ │ ├── Rectangle.js
│ │ └── TiltedBox.js
│ ├── index.css
│ └── index.js
└── webpack.config.js
├── storage-firebase
├── .gitignore
├── README.md
├── babel.config.js
├── dist
│ └── index.html
├── package-lock.json
├── package.json
├── src
│ └── index.js
└── webpack.config.js
├── storage-legacy-platform
├── .gitignore
├── README.md
├── babel.config.js
├── package-lock.json
├── package.json
├── public
│ └── index.html
├── src
│ ├── FragmentSelector.js
│ ├── SvgSelector.js
│ └── index.js
└── webpack.config.js
├── widget-react-helloworld
├── README.md
├── dist
│ ├── index.html
│ └── recogito-helloworld-widget.js
├── package-lock.json
├── package.json
├── src
│ ├── index.css
│ └── index.js
└── webpack.config.js
└── widget-tag-validation
├── package-lock.json
├── package.json
└── src
├── Icons.js
├── TagAutocomplete.jsx
├── TagInput.jsx
├── TagValidatorWidget.jsx
├── TagValidatorWidget.scss
├── ValidatableTag.jsx
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | **/node_modules/*
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2020, Pelagios | Recogito
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Recogito Client Plugins
2 |
3 | Plugins for the [RecogitoJS](https://github.com/recogito/recogito-js),
4 | [Annotorious](https://github.com/recogito/annotorious) and
5 | [Annotorious OpenSeadragon](https://github.com/recogito/annotorious-openseadragon) JavaScript
6 | annotation libraries.
7 |
8 | More info:
9 |
--------------------------------------------------------------------------------
/assets/1280px-Hallstatt.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/1280px-Hallstatt.jpg
--------------------------------------------------------------------------------
/assets/640px-Hallstatt.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/640px-Hallstatt.jpg
--------------------------------------------------------------------------------
/assets/Carta_Marina.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/Carta_Marina.jpeg
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (C) 2009 CodePlex Foundation
2 | Copyright (C) 2010-2013 OpenSeadragon contributors
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | - Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 |
10 | - Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | - Neither the name of CodePlex Foundation nor the names of its contributors
15 | may be used to endorse or promote products derived from this software
16 | without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
22 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28 | POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/button_grouphover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/button_grouphover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/button_hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/button_hover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/button_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/button_pressed.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/button_rest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/button_rest.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/flip_grouphover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/flip_grouphover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/flip_hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/flip_hover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/flip_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/flip_pressed.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/flip_rest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/flip_rest.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/fullpage_grouphover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/fullpage_grouphover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/fullpage_hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/fullpage_hover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/fullpage_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/fullpage_pressed.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/fullpage_rest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/fullpage_rest.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/home_grouphover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/home_grouphover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/home_hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/home_hover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/home_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/home_pressed.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/home_rest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/home_rest.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/next_grouphover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/next_grouphover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/next_hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/next_hover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/next_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/next_pressed.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/next_rest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/next_rest.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/previous_grouphover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/previous_grouphover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/previous_hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/previous_hover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/previous_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/previous_pressed.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/previous_rest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/previous_rest.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/rotateleft_grouphover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/rotateleft_grouphover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/rotateleft_hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/rotateleft_hover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/rotateleft_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/rotateleft_pressed.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/rotateleft_rest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/rotateleft_rest.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/rotateright_grouphover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/rotateright_grouphover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/rotateright_hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/rotateright_hover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/rotateright_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/rotateright_pressed.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/rotateright_rest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/rotateright_rest.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/zoomin_grouphover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/zoomin_grouphover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/zoomin_hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/zoomin_hover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/zoomin_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/zoomin_pressed.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/zoomin_rest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/zoomin_rest.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/zoomout_grouphover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/zoomout_grouphover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/zoomout_hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/zoomout_hover.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/zoomout_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/zoomout_pressed.png
--------------------------------------------------------------------------------
/assets/openseadragon-bin-2.4.2/images/zoomout_rest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/openseadragon-bin-2.4.2/images/zoomout_rest.png
--------------------------------------------------------------------------------
/assets/vasques_07.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/assets/vasques_07.jpg
--------------------------------------------------------------------------------
/plugins/annotorious-better-polygon/.gitignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/plugins/annotorious-better-polygon/README.md:
--------------------------------------------------------------------------------
1 | # Annotorious Better Polygon
2 |
3 | A better polygon selection tool for [Annotorious](https://annotorious.com). Compatible with both
4 | the Annotorious standard version and Annotorious for OpenSeadragon.
5 |
6 | 
7 |
8 | ## Features
9 |
10 | - Close the polygon either by double clicking (or long tap), or re-selecting the first point
11 | - When approaching the first point, the mouse will snap to it to make selecting easier
12 | - Add points by clicking and dragging the line midpoint handles
13 | - Remove points by selecting them with a click and pressing the DEL key
14 | - Optionally start drawing by drag or single click
15 |
16 | ## Installation
17 |
18 | __Better Polygon__ requires Annotorious version 2.5.9 or higher.
19 |
20 | ```html
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
44 |
45 |
46 | ```
47 |
48 | __Or via npm:__
49 |
50 | ```
51 | $ npm i @recogito/annotorious-better-polygon
52 | ```
53 |
54 | __Import and initialize:__
55 |
56 | ```js
57 | import BetterPolygon from '@recogito/annotorious-better-polygon';
58 |
59 | //...
60 |
61 | BetterPolygon(anno);
62 | ```
63 |
64 |
--------------------------------------------------------------------------------
/plugins/annotorious-better-polygon/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@recogito/annotorious-better-polygon",
3 | "version": "0.2.1",
4 | "description": "An improved polygon editing tool with more features and better usability",
5 | "main": "dist/annotorious-better-polygon.js",
6 | "scripts": {
7 | "start": "webpack serve --open --mode=development",
8 | "build": "webpack --mode=production"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/recogito/recogito-client-plugins.git"
13 | },
14 | "author": "Rainer Simon",
15 | "license": "BSD-3-Clause",
16 | "bugs": {
17 | "url": "https://github.com/recogito/recogito-client-plugins/issues"
18 | },
19 | "homepage": "https://github.com/recogito/recogito-client-plugins/tree/main/plugins/annotorious-better-polygon",
20 | "devDependencies": {
21 | "@babel/core": "^7.15.8",
22 | "@babel/plugin-proposal-class-properties": "^7.14.5",
23 | "@babel/preset-env": "^7.15.8",
24 | "babel-loader": "^8.2.3",
25 | "css-loader": "^6.5.0",
26 | "html-webpack-plugin": "^5.5.0",
27 | "style-loader": "^3.3.1",
28 | "webpack": "^5.60.0",
29 | "webpack-cli": "^4.9.1",
30 | "webpack-dev-server": "^4.3.1"
31 | },
32 | "dependencies": {
33 | "@recogito/annotorious": "^2.5.9"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/plugins/annotorious-better-polygon/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Annotorious | Better Polygon
5 |
6 |
7 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
47 |
48 |
--------------------------------------------------------------------------------
/plugins/annotorious-better-polygon/public/osd.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Annotorious | Better Polygon
5 |
6 |
7 |
8 |
9 |
15 |
16 |
17 |
18 |
19 |
44 |
45 |
--------------------------------------------------------------------------------
/plugins/annotorious-better-polygon/screencast.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/plugins/annotorious-better-polygon/screencast.gif
--------------------------------------------------------------------------------
/plugins/annotorious-better-polygon/src/ImRubberbandPolygon.js:
--------------------------------------------------------------------------------
1 | import { SVG_NAMESPACE } from '@recogito/annotorious/src/util/SVG';
2 | import { Selection, ToolLike } from '@recogito/annotorious/src/tools/Tool';
3 | import Mask from '@recogito/annotorious/src/tools/polygon/PolygonMask';
4 |
5 | import { toSVGTarget } from './ImRubberbandPolygonTool';
6 |
7 | export default class ImRubberbandPolygon extends ToolLike {
8 |
9 | constructor(anchor, g, config, env) {
10 | super(g, config, env);
11 |
12 | // Needed later to construct the Selection
13 | this.env = env;
14 |
15 | // UI scale
16 | this.scale = 1;
17 |
18 | // Polygon state
19 | this.points = [ anchor ];
20 |
21 | // Mouse state
22 | this.mousepos = anchor;
23 |
24 | // SVG geometry
25 | this.container = document.createElementNS(SVG_NAMESPACE, 'g');
26 |
27 | // The selection consisting of an (inner and outer) path, and
28 | // the 'rubberband' polygon
29 | this.selection = document.createElementNS(SVG_NAMESPACE, 'g');
30 | this.selection.setAttribute('class', 'a9s-selection improved-polygon');
31 |
32 | this.outerPath = document.createElementNS(SVG_NAMESPACE, 'path');
33 | this.outerPath.setAttribute('class', 'a9s-outer');
34 |
35 | this.innerPath = document.createElementNS(SVG_NAMESPACE, 'path');
36 | this.innerPath.setAttribute('class', 'a9s-inner');
37 |
38 | this.rubberband = document.createElementNS(SVG_NAMESPACE, 'polygon');
39 | this.rubberband.setAttribute('class', 'a9s-rubberband');
40 |
41 | this.closeHandle = this.drawHandle(anchor[0], anchor[1]);
42 | this.closeHandle.style.display = 'none';
43 |
44 | this.setPoints(this.points);
45 |
46 | this.selection.appendChild(this.rubberband)
47 | this.selection.appendChild(this.outerPath);
48 | this.selection.appendChild(this.innerPath);
49 | this.selection.appendChild(this.closeHandle);
50 |
51 | this.mask = new Mask(env.image, this.rubberband);
52 |
53 | // Hide until user actually moves the mouse
54 | this.container.style.display = 'none';
55 |
56 | this.container.appendChild(this.mask.element);
57 | this.container.appendChild(this.selection);
58 |
59 | g.appendChild(this.container);
60 | }
61 |
62 | addPoint = () => {
63 | if (this.isClosable()) {
64 | // Close, don't add
65 | this.close();
66 | } else {
67 | // Don't add a new point if distance < 2 pixels
68 | const [x, y] = this.mousepos;
69 | const lastCorner = this.points[this.points.length - 1];
70 | const dist = Math.pow(x - lastCorner[0], 2) + Math.pow(y - lastCorner[1], 2);
71 |
72 | if (dist > 4) {
73 | this.points = [...this.points, this.mousepos];
74 | this.setPoints(this.points);
75 | this.mask.redraw();
76 | }
77 | }
78 | }
79 |
80 | close = () => {
81 | const selection = new Selection(toSVGTarget(this.points, this.env.image));
82 | this.emit('close', { shape: this.selection, selection });
83 | }
84 |
85 | destroy = () => {
86 | this.container.parentNode.removeChild(this.container);
87 | }
88 |
89 | dragTo = xy => {
90 | // Make visible
91 | this.container.style.display = null;
92 |
93 | this.mousepos = xy;
94 |
95 | const d = this.getDistanceToStart();
96 |
97 | // Display close handle if distance < 40px
98 | if (d < 40) {
99 | this.closeHandle.style.display = null;
100 | } else {
101 | this.closeHandle.style.display = 'none';
102 | }
103 |
104 | // Snap if nearby
105 | if (d < 20) {
106 | this.mousepos = this.points[0];
107 | }
108 |
109 | // Shape is points + (snapped) mousepos
110 | this.setPoints([ ...this.points, this.mousepos ]);
111 | this.mask.redraw();
112 | }
113 |
114 | get element() {
115 | return this.selection;
116 | }
117 |
118 | getBoundingClientRect = () =>
119 | this.rubberband.getBoundingClientRect();
120 |
121 | getDistanceToStart = () => {
122 | if (this.points.length < 3)
123 | return Infinity; // Just return if not at least 3 points
124 |
125 | const dx = Math.abs(this.mousepos[0] - this.points[0][0]);
126 | const dy = Math.abs(this.mousepos[1] - this.points[0][1]);
127 |
128 | return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) / this.scale;
129 | }
130 |
131 | /**
132 | * Tests if the mouse is over the first point, meaning that
133 | * the polygon would be closed on click
134 | */
135 | isClosable = () => {
136 | const d = this.getDistanceToStart();
137 | return d < 6 * this.scale;
138 | }
139 |
140 | onScaleChanged = scale => {
141 | this.scale = scale;
142 |
143 | const inner = this.closeHandle.querySelector('.a9s-handle-inner');
144 | const outer = this.closeHandle.querySelector('.a9s-handle-outer');
145 |
146 | const radius = scale * (this.config.handleRadius || 6);
147 |
148 | inner.setAttribute('r', radius);
149 | outer.setAttribute('r', radius);
150 | }
151 |
152 | /** Removes last corner **/
153 | pop = () => {
154 | this.points.pop();
155 | this.setPoints(this.points);
156 | this.mask.redraw();
157 | }
158 |
159 | setPoints = arr => {
160 | const [head, ...tail]= arr;
161 |
162 | const path =
163 | `M ${head[0]} ${head[1]} ` +
164 | tail.map(([x,y]) => `L ${x} ${y}`).join(' ');
165 |
166 | this.outerPath.setAttribute('d', path);
167 | this.innerPath.setAttribute('d', path);
168 |
169 | const points = arr.map(t => `${t[0]},${t[1]}`).join(' ');
170 | this.rubberband.setAttribute('points', points);
171 | }
172 |
173 | }
--------------------------------------------------------------------------------
/plugins/annotorious-better-polygon/src/ImRubberbandPolygonTool.js:
--------------------------------------------------------------------------------
1 | import Tool from '@recogito/annotorious/src/tools/Tool';
2 | import { isTouchDevice } from '@recogito/annotorious/src/util/Touch';
3 |
4 | import ImEditablePolygon from './ImEditablePolygon';
5 | import ImRubberbandPolygon from './ImRubberbandPolygon';
6 |
7 | const isTouch = isTouchDevice();
8 |
9 | export const toSVGTarget = (points, image) => ({
10 | source: image?.src,
11 | selector: {
12 | type: "SvgSelector",
13 | value: ` `
14 | }
15 | });
16 |
17 | export default class ImRubberbandPolygonTool extends Tool {
18 |
19 | constructor(g, config, env) {
20 | super(g, config, env);
21 |
22 | this._isDrawing = false;
23 | this._startOnSingleClick = false;
24 | }
25 |
26 | get isDrawing() {
27 | return this._isDrawing;
28 | }
29 |
30 | startDrawing = (x, y, startOnSingleClick) => {
31 | this._isDrawing = true;
32 | this._startOnSingleClick = startOnSingleClick;
33 |
34 | this.attachListeners({
35 | mouseMove: this.onMouseMove,
36 | mouseUp: this.onMouseUp,
37 | dblClick: this.onDblClick
38 | });
39 |
40 | this.rubberband =
41 | new ImRubberbandPolygon([x, y], this.g, this.config, this.env);
42 |
43 | this.rubberband.on('close', ({ shape, selection }) => {
44 | shape.annotation = selection;
45 | this.emit('complete', shape);
46 | this.stop();
47 | });
48 | }
49 |
50 | stop = () => {
51 | this.detachListeners();
52 |
53 | this._isDrawing = false;
54 |
55 | if (this.rubberband) {
56 | this.rubberband.destroy();
57 | this.rubberband = null;
58 | }
59 | }
60 |
61 | onDblClick = () => {
62 | if (this.rubberband?.points.length > 2) {
63 | this.rubberband.close();
64 | this.stop();
65 | }
66 | }
67 |
68 | onMouseMove = (x, y) =>
69 | this.rubberband.dragTo([x, y]);
70 |
71 | onMouseUp = () => {
72 | const { width, height } = this.rubberband.getBoundingClientRect();
73 |
74 | const minWidth = this.config.minSelectionWidth || 4;
75 | const minHeight = this.config.minSelectionHeight || 4;
76 |
77 | if (width >= minWidth || height >= minHeight) {
78 | this.rubberband.addPoint();
79 | } else if (!this._startOnSingleClick) {
80 | this.emit('cancel');
81 | this.stop();
82 | }
83 | }
84 |
85 | onScaleChanged = scale => {
86 | if (this.rubberband)
87 | this.rubberband.onScaleChanged(scale);
88 | }
89 |
90 | createEditableShape = annotation =>
91 | new ImEditablePolygon(annotation, this.g, this.config, this.env);
92 |
93 | }
94 |
95 | ImRubberbandPolygonTool.identifier = 'polygon';
96 |
97 | ImRubberbandPolygonTool.supports = annotation => {
98 | const selector = annotation.selector('SvgSelector');
99 | if (selector)
100 | return selector.value?.match(/^ {
6 |
7 | anno.addDrawingTool(ImRubberbandPolygonTool);
8 |
9 | }
10 |
11 | export default ImprovedPolygonPlugin;
--------------------------------------------------------------------------------
/plugins/annotorious-better-polygon/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 |
6 | const APP_DIR = fs.realpathSync(process.cwd());
7 |
8 | const resolveAppPath = relativePath => path.resolve(APP_DIR, relativePath);
9 |
10 | module.exports = {
11 | entry: resolveAppPath('src'),
12 | output: {
13 | filename: 'annotorious-better-polygon.js',
14 | library: ['Annotorious', 'BetterPolygon'],
15 | libraryTarget: 'umd',
16 | libraryExport: 'default'
17 | },
18 | performance: {
19 | hints: false
20 | },
21 | optimization: {
22 | minimize: true
23 | },
24 | resolve: {
25 | extensions: ['.js']
26 | },
27 | module: {
28 | rules: [
29 | {
30 | test: /\.js$/,
31 | use: {
32 | loader: 'babel-loader' ,
33 | options: {
34 | "presets": [
35 | "@babel/preset-env",
36 | ],
37 | "plugins": [
38 | [
39 | "@babel/plugin-proposal-class-properties"
40 | ]
41 | ]
42 | }
43 | }
44 | },
45 | { test: /\.css$/, use: [ 'style-loader', 'css-loader'] },
46 | ]
47 | },
48 | devServer: {
49 | compress: true,
50 | hot: true,
51 | host: process.env.HOST || 'localhost',
52 | port: 3000,
53 | static: [{
54 | directory: resolveAppPath('public'),
55 | publicPath: '/'
56 | },{
57 | directory: resolveAppPath('../../assets'),
58 | publicPath: '/'
59 | }]
60 | },
61 | plugins: [
62 | new HtmlWebpackPlugin ({
63 | template: resolveAppPath('public/index.html')
64 | })
65 | ]
66 | }
--------------------------------------------------------------------------------
/plugins/annotorious-find-contours/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/plugins/annotorious-find-contours/README.md
--------------------------------------------------------------------------------
/plugins/annotorious-find-contours/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@recogito/annotorious-find-contours",
3 | "version": "0.1.0",
4 | "description": "A plugin that uses OpenCV to simplify selection of polygons in Annotorious and AnnotoriousOSD",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "start": "webpack serve --open --mode=development",
8 | "build": "webpack --mode=production"
9 | },
10 | "author": "Rainer Simon",
11 | "license": "BSD-3-Clause",
12 | "devDependencies": {
13 | "@babel/core": "^7.6.2",
14 | "@babel/plugin-proposal-class-properties": "^7.5.5",
15 | "@babel/preset-env": "^7.6.2",
16 | "babel-loader": "^8.0.6",
17 | "css-loader": "^5.2.5",
18 | "html-webpack-plugin": "^5.5.0",
19 | "style-loader": "^2.0.0",
20 | "webpack": "^5.60.0",
21 | "webpack-cli": "^4.9.1",
22 | "webpack-dev-server": "^4.3.1"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/plugins/annotorious-find-contours/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Annotorious | Find Contours
5 |
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
19 |
28 |
29 |
--------------------------------------------------------------------------------
/plugins/annotorious-find-contours/src/index.js:
--------------------------------------------------------------------------------
1 | /*************************************************************************
2 | *
3 | * Basic concept for this is from the official OpenCV docs:
4 | * https://docs.opencv.org/3.4/dc/dcf/tutorial_js_contour_features.html
5 | *
6 | *************************************************************************/
7 |
8 | /**
9 | * Helper: chunks an array (i.e array to array of arrays)
10 | */
11 | const chunk = (array, size) => {
12 | const chunked_arr = [];
13 |
14 | let index = 0;
15 | while (index < array.length) {
16 | chunked_arr.push(array.slice(index, size + index));
17 | index += size;
18 | }
19 |
20 | return chunked_arr;
21 | }
22 |
23 | /**
24 | * Renders intermediate OpenCV results into a 'debug DIV'
25 | */
26 | const renderDebugImages = (canvasInput, src, polygons, hierarchy, div) => {
27 | const dst = cv.Mat.zeros(src.rows, src.cols, cv.CV_8UC3);
28 |
29 | const color = new cv.Scalar(
30 | Math.round(Math.random() * 255),
31 | Math.round(Math.random() * 255),
32 | Math.round(Math.random() * 255));
33 |
34 | cv.drawContours(dst, polygons, -1, color, 1, 8, hierarchy, 0);
35 |
36 | const mask = document.createElement('CANVAS');
37 | mask.width = canvasInput.width;
38 | mask.height = canvasInput.height;
39 | cv.imshow(mask, src);
40 |
41 | div.appendChild(mask);
42 |
43 | const output = document.createElement('CANVAS');
44 | output.width = canvasInput.width;
45 | output.height = canvasInput.height;
46 | cv.imshow(output, dst);
47 |
48 | div.appendChild(output);
49 |
50 | dst.delete();
51 | }
52 |
53 | /**
54 | * Computer vision magic happens here
55 | */
56 | const findContourPolygon = (canvasInput, debugDiv) => {
57 | const src = cv.imread(canvasInput);
58 |
59 | // Convert to grayscale & threshold
60 | cv.cvtColor(src, src, cv.COLOR_RGB2GRAY, 0);
61 | cv.threshold(src, src, 120, 200, cv.THRESH_BINARY + cv.THRESH_OTSU);
62 |
63 | // Find contours
64 | const contours = new cv.MatVector();
65 | const hierarchy = new cv.Mat();
66 |
67 | cv.findContours(src, contours, hierarchy, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE);
68 |
69 | let poly = new cv.MatVector();
70 |
71 | for (let i = 0; i < contours.size(); ++i) {
72 | let tmp = new cv.Mat();
73 | let cnt = contours.get(i);
74 | // You can try more different parameters
75 | cv.convexHull(cnt, tmp, false, true);
76 | poly.push_back(tmp);
77 | cnt.delete(); tmp.delete();
78 | }
79 |
80 | // draw contours with random Scalar
81 | const dst = cv.Mat.zeros(src.rows, src.cols, cv.CV_8UC3);
82 | for (let i = 0; i < contours.size(); ++i) {
83 | let color = new cv.Scalar(Math.round(Math.random() * 255), Math.round(Math.random() * 255),
84 | Math.round(Math.random() * 255));
85 | cv.drawContours(dst, poly, i, color, 1, 8, hierarchy, 0);
86 | }
87 |
88 | const output = document.createElement('CANVAS');
89 | output.width = canvasInput.width;
90 | output.height = canvasInput.height;
91 | cv.imshow(output, dst);
92 | debugDiv.appendChild(output);
93 |
94 | src.delete(); dst.delete(); hierarchy.delete(); contours.delete(); poly.delete();
95 |
96 | /*
97 | const dst = cv.Mat.zeros(src.rows, src.cols, cv.CV_8UC3);
98 | for (let i = 0; i < contours.size(); ++i) {
99 | let color = new cv.Scalar(Math.round(Math.random() * 255), Math.round(Math.random() * 255),
100 | Math.round(Math.random() * 255));
101 | cv.drawContours(dst, contours, i, color, 1, cv.LINE_8, hierarchy, 100);
102 | }
103 |
104 |
105 | */
106 |
107 | // Approximate closed polygons, keep only the largest
108 | // let largestAreaPolygon = { area: 0 };
109 |
110 | /*
111 | const polygons = new cv.MatVector();
112 |
113 | for (let i = 0; i < contours.size(); ++i) {
114 | const polygon = new cv.Mat();
115 | const contour = contours.get(i);
116 |
117 | cv.approxPolyDP(contour, polygon, 3, true);
118 |
119 | // polygons.push_back(polygon);
120 |
121 | // Compute contour areas
122 | const area = cv.contourArea(polygon);
123 |
124 | // if (area > largestAreaPolygon.area)
125 | largestAreaPolygon = { area, polygon };
126 |
127 | // contour.delete();
128 | // polygon.delete();
129 | }
130 |
131 | polygons.push_back(largestAreaPolygon.polygon);
132 |
133 | // if (debugDiv)
134 | // renderDebugImages(canvasInput, src, polygons, hierarchy,debugDiv);
135 |
136 | src.delete();
137 |
138 | hierarchy.delete();
139 | contours.delete();
140 | polygons.delete();
141 | */
142 |
143 | // return chunk(largestAreaPolygon.polygon.data32S, 2);
144 | }
145 |
146 | const FindContours = (anno, debugDiv) => {
147 |
148 | anno.on('createSelection', async function() {
149 | const { snippet, transform } = anno.getSelectedImageSnippet();
150 |
151 | const localCoords = findContourPolygon(snippet, debugDiv);
152 | const coords = localCoords.map(xy => transform(xy));
153 |
154 | const annotation = {
155 | "@context": "http://www.w3.org/ns/anno.jsonld",
156 | "id": "#a88b22d0-6106-4872-9435-c78b5e89fede",
157 | "type": "Annotation",
158 | "body": [],
159 | "target": {
160 | "selector": [{
161 | "type": "SvgSelector",
162 | "value": ` `
163 | }]
164 | }
165 | }
166 |
167 | setTimeout(function() {
168 | anno.setAnnotations([ annotation ]);
169 | anno.selectAnnotation(annotation);
170 | }, 10);
171 | });
172 |
173 | }
174 |
175 | export default FindContours;
--------------------------------------------------------------------------------
/plugins/annotorious-find-contours/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 |
6 | const APP_DIR = fs.realpathSync(process.cwd());
7 |
8 | const resolveAppPath = relativePath => path.resolve(APP_DIR, relativePath);
9 |
10 | module.exports = {
11 | entry: resolveAppPath('src'),
12 | output: {
13 | filename: 'annotorious-find-contours.min.js',
14 | library: ['Annotorious', 'FindContours'],
15 | libraryTarget: 'umd',
16 | libraryExport: 'default'
17 | },
18 | performance: {
19 | hints: false
20 | },
21 | optimization: {
22 | minimize: true
23 | },
24 | resolve: {
25 | extensions: ['.js']
26 | },
27 | module: {
28 | rules: [
29 | {
30 | test: /\.js$/,
31 | use: {
32 | loader: 'babel-loader' ,
33 | options: {
34 | "presets": [
35 | "@babel/preset-env",
36 | ],
37 | "plugins": [
38 | [
39 | "@babel/plugin-proposal-class-properties"
40 | ]
41 | ]
42 | }
43 | }
44 | },
45 | { test: /\.css$/, use: [ 'style-loader', 'css-loader'] },
46 | ]
47 | },
48 | devServer: {
49 | compress: true,
50 | hot: true,
51 | host: process.env.HOST || 'localhost',
52 | port: 3000,
53 | static: [{
54 | directory: resolveAppPath('public'),
55 | publicPath: '/'
56 | },{
57 | directory: resolveAppPath('../../assets'),
58 | publicPath: '/'
59 | }]
60 | },
61 | plugins: [
62 | new HtmlWebpackPlugin ({
63 | template: resolveAppPath('public/index.html')
64 | })
65 | ]
66 | }
--------------------------------------------------------------------------------
/plugins/annotorious-hover-tooltip/.gitignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/plugins/annotorious-hover-tooltip/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@recogito/annotorious-hover-tooltip",
3 | "version": "0.1.2",
4 | "description": "A hover tooltip plugin for Annotorious and AnnotoriousOSD",
5 | "main": "dist/annotorious-hover-tooltip.js",
6 | "scripts": {
7 | "start": "webpack serve --open --mode=development",
8 | "build": "webpack --mode=production"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/recogito/recogito-client-plugins.git"
13 | },
14 | "author": "Rainer Simon",
15 | "license": "BSD-3-Clause",
16 | "bugs": {
17 | "url": "https://github.com/recogito/recogito-client-plugins/issues"
18 | },
19 | "devDependencies": {
20 | "@babel/core": "^7.16.12",
21 | "@babel/plugin-proposal-class-properties": "^7.16.7",
22 | "@babel/preset-env": "^7.16.11",
23 | "babel-loader": "^8.2.3",
24 | "css-loader": "^6.5.1",
25 | "html-webpack-plugin": "^5.5.0",
26 | "style-loader": "^3.3.1",
27 | "webpack": "^5.67.0",
28 | "webpack-cli": "^4.9.1",
29 | "webpack-dev-server": "^4.7.3"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/plugins/annotorious-hover-tooltip/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Annotorious | Hover Tooltip
5 |
6 |
7 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
32 |
33 |
--------------------------------------------------------------------------------
/plugins/annotorious-hover-tooltip/src/index.css:
--------------------------------------------------------------------------------
1 | .a9s-hover-tooltip {
2 | position:absolute;
3 | background-color:#fff;
4 | padding:8px 12px;
5 | pointer-events:none;
6 | border-radius:3px;
7 | box-shadow:0 0 17px rgba(0, 0, 0, 0.4);
8 | }
--------------------------------------------------------------------------------
/plugins/annotorious-hover-tooltip/src/index.js:
--------------------------------------------------------------------------------
1 | import './index.css';
2 |
3 | const getFirstTextBody = annotation => {
4 | if (!annotation.body)
5 | return;
6 |
7 | const bodies = Array.isArray(annotation.body) ?
8 | annotation.body : [ annotation.body ];
9 |
10 | return bodies.find(b => b.type === 'TextualBody');
11 | }
12 |
13 | export default anno => {
14 |
15 | let tooltip = null;
16 |
17 | const onMouseMove = evt => {
18 | tooltip.style.top = `${evt.offsetY}px`;
19 | tooltip.style.left = `${evt.offsetX}px`;
20 | }
21 |
22 | const showTooltip = (annotation, shape) => {
23 | const body = getFirstTextBody(annotation);
24 |
25 | if (body) {
26 | // Create tooltip element
27 | tooltip = document.createElement('div');
28 | tooltip.setAttribute('class', 'a9s-hover-tooltip');
29 |
30 | // TODO get first TextualBody
31 | tooltip.innerHTML = body.value;
32 |
33 | anno._element.appendChild(tooltip);
34 |
35 | shape.addEventListener('mousemove', onMouseMove);
36 | }
37 | }
38 |
39 | const hideTooltip = shape => {
40 | shape.removeEventListener('mousemove', onMouseMove);
41 |
42 | if (tooltip) {
43 | anno._element.removeChild(tooltip);
44 | tooltip = null;
45 | }
46 | }
47 |
48 | anno.on('mouseEnterAnnotation', (annotation, shape) =>
49 | showTooltip(annotation, shape));
50 |
51 | anno.on('mouseLeaveAnnotation', (_, shape) =>
52 | hideTooltip(shape));
53 |
54 | }
--------------------------------------------------------------------------------
/plugins/annotorious-hover-tooltip/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 |
6 | const APP_DIR = fs.realpathSync(process.cwd());
7 |
8 | const resolveAppPath = relativePath => path.resolve(APP_DIR, relativePath);
9 |
10 | module.exports = {
11 | entry: resolveAppPath('src'),
12 | output: {
13 | filename: 'annotorious-hover-tooltip.js',
14 | library: ['Annotorious', 'HoverTooltip'],
15 | libraryTarget: 'umd',
16 | libraryExport: 'default'
17 | },
18 | performance: {
19 | hints: false
20 | },
21 | optimization: {
22 | minimize: true
23 | },
24 | resolve: {
25 | extensions: ['.js']
26 | },
27 | module: {
28 | rules: [
29 | {
30 | test: /\.js$/,
31 | use: {
32 | loader: 'babel-loader' ,
33 | options: {
34 | "presets": [
35 | "@babel/preset-env",
36 | ],
37 | "plugins": [
38 | [
39 | "@babel/plugin-proposal-class-properties"
40 | ]
41 | ]
42 | }
43 | }
44 | },
45 | { test: /\.css$/, use: [ 'style-loader', 'css-loader'] },
46 | ]
47 | },
48 | devServer: {
49 | compress: true,
50 | hot: true,
51 | host: process.env.HOST || 'localhost',
52 | port: 3000,
53 | static: [{
54 | directory: resolveAppPath('public'),
55 | publicPath: '/'
56 | },{
57 | directory: resolveAppPath('../../assets'),
58 | publicPath: '/'
59 | }]
60 | },
61 | plugins: [
62 | new HtmlWebpackPlugin ({
63 | template: resolveAppPath('public/index.html')
64 | })
65 | ]
66 | }
--------------------------------------------------------------------------------
/plugins/annotorious-map-annotation/.gitignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/plugins/annotorious-map-annotation/README.md:
--------------------------------------------------------------------------------
1 | # Annotorious Map Annotation
2 |
3 | A helper plugin for using Annotorious with maps and geo-referenced images. After the
4 | plugin is installed, annotation shapes will use geographic coordinates (latitude, longitude)
5 | rather than image pixel coordinates.
6 |
7 | This plugin currently works with [Annotorious OpenSeadragon](https://github.com/recogito/annotorious-openseadragon)
8 | and WMTS maps __only__.
9 |
10 | The plugin depends on:
11 | - [OpenSeadragon](https://openseadragon.github.io/)
12 | - [Annotorious OpenSeadragon](https://github.com/recogito/annotorious-openseadragon)
13 | - [OpenSeadragon WTMS plugin](https://github.com/recogito/openseadragon-wmts)
14 |
15 | ## Installing
16 | `npm install @recogito/annotorious-map-annotation`
17 |
18 | or
19 |
20 | ```html
21 |
22 | ```
23 |
24 | ## Using
25 |
26 | ```js
27 | // OpenSeadragon viewer
28 | var viewer = OpenSeadragon({
29 | id: "openseadragon",
30 | prefixUrl: "openseadragon/images/"
31 | });
32 |
33 | // OpenSeadragon WMTS plugin
34 | var map = await OpenSeadragon.WMTS(viewer, {
35 | url: 'http://maps.wien.gv.at/wmts/1.0.0/WMTSCapabilities.xml'
36 | });
37 |
38 | // Initialize Annotorious
39 | var anno = OpenSeadragon.Annotorious(viewer);
40 |
41 | // Add the map annotation plugin
42 | Annotorious.MapAnnotation(anno, map);
43 | ```
--------------------------------------------------------------------------------
/plugins/annotorious-map-annotation/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@recogito/annotorious-map-annotation",
3 | "version": "0.3.3",
4 | "description": "An Annotorious plugin for annotating maps",
5 | "main": "dist/annotorious-map-annotation.js",
6 | "scripts": {
7 | "start": "webpack serve --open --mode=development",
8 | "build": "webpack --mode=production",
9 | "prepare": "webpack --mode=production"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/recogito/annotorious-map-annotation.git"
14 | },
15 | "keywords": [
16 | "Annotation",
17 | "Maps",
18 | "WMTS",
19 | "OpenSeadragon",
20 | "Map Annotation"
21 | ],
22 | "author": "Rainer Simon",
23 | "license": "BSD-3-Clause",
24 | "bugs": {
25 | "url": "https://github.com/recogito/annotorious-map-annotation/issues"
26 | },
27 | "homepage": "https://github.com/recogito/annotorious-map-annotation#readme",
28 | "devDependencies": {
29 | "@babel/plugin-proposal-class-properties": "^7.14.5",
30 | "@babel/preset-env": "^7.15.8",
31 | "babel-loader": "^8.2.3",
32 | "html-webpack-plugin": "^5.4.0",
33 | "webpack": "^5.59.1",
34 | "webpack-cli": "^4.9.1",
35 | "webpack-dev-server": "^4.3.1"
36 | },
37 | "dependencies": {
38 | "tiny-emitter": "^2.1.0"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/plugins/annotorious-map-annotation/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Annotorious Map Annotation
5 |
6 |
7 |
8 |
9 |
45 |
46 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/plugins/annotorious-map-annotation/public/sample-annotations.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "Annotation",
4 | "body": [
5 | {
6 | "type": "TextualBody",
7 | "value": "York",
8 | "purpose": "commenting"
9 | }
10 | ],
11 | "target": {
12 | "selector": {
13 | "type": "SvgSelector",
14 | "value": " "
15 | }
16 | },
17 | "@context": "http://www.w3.org/ns/anno.jsonld",
18 | "id": "#4caa7cfa-fd21-4ec7-9213-be2f0ce57584"
19 | },
20 | {
21 | "type": "Annotation",
22 | "body": [
23 | {
24 | "type": "TextualBody",
25 | "value": "Rectangle",
26 | "purpose": "commenting"
27 | }
28 | ],
29 | "target": {
30 | "selector": {
31 | "type": "FragmentSelector",
32 | "conformsTo": "http://www.w3.org/TR/media-frags/",
33 | "value": "xywh=-1.168770118761931,53.95607151237134,0.07807796835071201,0.028219233454784387"
34 | }
35 | },
36 | "@context": "http://www.w3.org/ns/anno.jsonld",
37 | "id": "#f1e56eb9-de56-4134-b92b-ed28d8c6a175"
38 | }
39 | ]
--------------------------------------------------------------------------------
/plugins/annotorious-map-annotation/src/crosswalk/fragment/index.js:
--------------------------------------------------------------------------------
1 | const parseRectFragment = selector => {
2 | const { value } = selector;
3 | const coords = value.includes(':') ? value.substring(value.indexOf(':') + 1) : value.substring(value.indexOf('=') + 1);
4 |
5 | const [ x, y, w, h ] = coords.split(',').map(parseFloat);
6 |
7 | return { x, y, w, h };
8 | }
9 |
10 | const transform = (selector, fn) => {
11 | const { x, y, w, h } = parseRectFragment(selector);
12 |
13 | const mapTopLeft = [x, y];
14 | const mapBottomRight = [x + w, y + h];
15 |
16 | const llTopLeft = fn(mapTopLeft);
17 | const llBottomRight = fn(mapBottomRight);
18 |
19 | const width = llBottomRight[0] - llTopLeft[0];
20 | const height = llTopLeft[1] - llBottomRight[1];
21 |
22 | return {
23 | type: "FragmentSelector",
24 | conformsTo: "http://www.w3.org/TR/media-frags/",
25 | value: `xywh=${llTopLeft[0]},${llTopLeft[1]},${width},${height}`
26 | };
27 | }
28 |
29 | export const fragmentForward = (selector, map) =>
30 | transform(selector, map.lonLatToImage);
31 |
32 | /**
33 | * Converts the fragment from native map coordinates to lon/lat.
34 | */
35 | export const fragmentReverse = (selector, map) =>
36 | transform(selector, map.imageToLonLat);
37 |
--------------------------------------------------------------------------------
/plugins/annotorious-map-annotation/src/crosswalk/index.js:
--------------------------------------------------------------------------------
1 | import { fragmentForward, fragmentReverse } from './fragment';
2 | import { svgForward, svgReverse } from './svg';
3 |
4 | // Shorthand
5 | const merge = (annotation, selector) => ({
6 | ...annotation,
7 | target: {
8 | ...annotation.target,
9 | selector
10 | }
11 | })
12 |
13 | /** Crosswalks an annotation from lat/lon to map projection **/
14 | export const forward = map => annotation => {
15 |
16 | // Spec allows array as well as object
17 | const selector = Array.isArray(annotation.target.selector) ?
18 | annotation.target.selector[0] : annotation.target.selector;
19 |
20 | if (selector.type === 'FragmentSelector') {
21 | return merge(annotation, fragmentForward(selector, map));
22 | } else if (selector.type === 'SvgSelector') {
23 | return merge(annotation, svgForward(selector, map));
24 | } else {
25 | throw 'Unsupported selector type: ' + selector.type;
26 | }
27 |
28 | }
29 |
30 | /** Crosswalks an annotation from map projection to lat/lon **/
31 | export const reverse = map => annotation => {
32 |
33 | const { selector } = annotation.target;
34 |
35 | if (selector.type === 'FragmentSelector') {
36 | return merge(annotation, fragmentReverse(selector, map));
37 | } else if (selector.type === 'SvgSelector') {
38 | return merge(annotation, svgReverse(selector, map));
39 | } else {
40 | throw 'Unsupported selector type: ' + selector.type;
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/plugins/annotorious-map-annotation/src/crosswalk/svg/ellipse.js:
--------------------------------------------------------------------------------
1 | const transform = (shape, fn) => {
2 | // Shorthand
3 | const attr = name =>
4 | parseFloat(shape.getAttribute(name));
5 |
6 | const cx = attr('cx');
7 | const cy = attr('cy');
8 | const rx = attr('rx');
9 | const ry = attr('ry');
10 |
11 | const center = fn([cx, cy]);
12 | const top = fn([cx, cy - ry]);
13 | const left = fn([cx - rx, cy]);
14 |
15 | shape.setAttribute('cx', center[0]);
16 | shape.setAttribute('cy', center[1]);
17 | shape.setAttribute('rx', center[0] - left[0]);
18 | shape.setAttribute('ry', top[1] - center[1]);
19 | return shape;
20 | }
21 |
22 | export const ellipseForward = (shape, map) =>
23 | transform(shape, map.lonLatToImage);
24 |
25 | export const ellipseReverse = (shape, map) =>
26 | transform(shape, map.imageToLonLat);
--------------------------------------------------------------------------------
/plugins/annotorious-map-annotation/src/crosswalk/svg/index.js:
--------------------------------------------------------------------------------
1 | import { ellipseForward, ellipseReverse } from './ellipse';
2 | import { pathForward, pathReverse } from './path';
3 | import { polygonForward, polygonReverse } from './polygon';
4 |
5 | export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
6 |
7 | const parseSVGFragment = selector => {
8 | const parser = new DOMParser();
9 |
10 | // Parse the XML document, assuming SVG
11 | const { value } = selector;
12 | const doc = parser.parseFromString(value, 'image/svg+xml');
13 |
14 | return doc.documentElement;
15 | }
16 |
17 | const serialize = doc =>
18 | new XMLSerializer().serializeToString(doc);
19 |
20 | export const svgForward = (selector, map) => {
21 | const doc = parseSVGFragment(selector);
22 | const shape = doc.firstChild;
23 | const nodeName = shape.nodeName.toLowerCase();
24 |
25 | let crosswalked = null;
26 |
27 | if (nodeName === 'polygon') {
28 | crosswalked = polygonForward(shape, map);
29 | } else if (nodeName === 'ellipse') {
30 | crosswalked = ellipseForward(shape, map);
31 | } else if (nodeName === 'path') {
32 | crosswalked = pathForward(shape, map);
33 | } else {
34 | throw 'Forward crosswalk: unsupported shape type ' + nodeName;
35 | }
36 |
37 | doc.replaceChild(shape, crosswalked);
38 |
39 | return {
40 | type: 'SvgSelector',
41 | value: serialize(doc)
42 | };
43 | }
44 |
45 | export const svgReverse = (selector, map) => {
46 | const doc = parseSVGFragment(selector);
47 | const shape = doc.firstChild;
48 | const nodeName = shape.nodeName.toLowerCase();
49 |
50 | let crosswalked = null;
51 |
52 | if (nodeName === 'polygon') {
53 | crosswalked = polygonReverse(shape, map);
54 | } else if (nodeName === 'ellipse') {
55 | crosswalked = ellipseReverse(shape, map);
56 | } else if (nodeName === 'path') {
57 | crosswalked = pathReverse(shape, map);
58 | } else {
59 | throw 'Reverse crosswalk: unsupported SVG shape type ' + nodeName;
60 | }
61 |
62 | doc.replaceChild(shape, crosswalked);
63 |
64 | return {
65 | type: 'SvgSelector',
66 | value: serialize(doc)
67 | };
68 | }
--------------------------------------------------------------------------------
/plugins/annotorious-map-annotation/src/crosswalk/svg/path.js:
--------------------------------------------------------------------------------
1 | const transform = (shape, fn) => {
2 | const commands = shape.getAttribute('d')
3 | .split(/(?=M|m|L|l|H|h|V|v|Z|z)/g)
4 | .map(str => str.trim());
5 |
6 | const transformed = commands.map(cmd => {
7 | const op = cmd.substring(0, 1);
8 |
9 | if (op.toLowerCase() === 'z') {
10 | return op;
11 | } else {
12 | const xy = cmd.substring(1).trim().split(' ')
13 | .map(str => parseFloat(str.trim()));
14 |
15 | const [tx, ty] = fn(xy);
16 | return op + ' ' + tx + ' ' + ty;
17 | }
18 | }).join(' ');
19 |
20 | shape.setAttribute('d', transformed);
21 | return shape;
22 | }
23 |
24 | export const pathForward = (shape, map) =>
25 | transform(shape, map.lonLatToImage);
26 |
27 | export const pathReverse = (shape, map) =>
28 | transform(shape, map.imageToLonLat);
--------------------------------------------------------------------------------
/plugins/annotorious-map-annotation/src/crosswalk/svg/polygon.js:
--------------------------------------------------------------------------------
1 | const transform = (shape, fn) => {
2 | const points = shape.getAttribute('points')
3 | .split(' ')
4 | .map(t => t.split(',')
5 | .map(c => parseFloat(c.trim())));
6 |
7 | const transformed = points.map(orig => {
8 | const [ x, y ] = fn(orig);
9 | return x + ',' + y;
10 | }).join(' ');
11 |
12 | shape.setAttribute('points', transformed);
13 | return shape;
14 | }
15 |
16 | export const polygonForward = (shape, map) =>
17 | transform(shape, map.lonLatToImage);
18 |
19 | export const polygonReverse = (shape, map) =>
20 | transform(shape, map.imageToLonLat);
--------------------------------------------------------------------------------
/plugins/annotorious-map-annotation/src/index.js:
--------------------------------------------------------------------------------
1 | import { forward, reverse } from './crosswalk';
2 |
3 | const MapAnnotationPlugin = (anno, map) => {
4 |
5 | // Keep a list of handlers and their wrapped counterparts,
6 | // so we can remove them using .off
7 | const handlers = [];
8 |
9 | // Forward and reverse crosswalks
10 | const fwd = forward(map);
11 | const rvs = reverse(map);
12 |
13 | // Monkey-patch API methods for getting and setting annotations
14 | const _addAnnotation = anno.addAnnotation;
15 | const _setAnnotations = anno.setAnnotations;
16 | const _getAnnotations = anno.getAnnotations;
17 | const _getAnnotationById = anno.getAnnotationById;
18 |
19 | anno.addAnnotation = (annotation, readOnly) => {
20 | const crosswalked = fwd(annotation);
21 | _addAnnotation(crosswalked, readOnly);
22 | }
23 |
24 | anno.setAnnotations = annotations => {
25 | const crosswalked = annotations.map(a => fwd(a));
26 | _setAnnotations(crosswalked);
27 | }
28 |
29 | anno.getAnnotations = () =>
30 | _getAnnotations().map(a => rvs(a));
31 |
32 | anno.getAnnotationById = (id, skipCrosswalk) => {
33 | if (skipCrosswalk)
34 | return _getAnnotationById(id);
35 |
36 | const a = _getAnnotationById(id);
37 | return a ? rvs(a) : null;
38 | }
39 |
40 | // Monkey patch .on and .once
41 | const STANDARD_EVENTS = new Set([
42 | 'cancelSelected',
43 | 'clickAnnotation',
44 | 'createSelection',
45 | 'deleteAnnotation',
46 | 'mouseEnterAnnotation',
47 | 'mouseLeaveAnnotation',
48 | 'selectAnnotation'
49 | ]);
50 |
51 | const wrapHandler = origHandler => (event, handler) => {
52 | let wrapped;
53 |
54 | if (STANDARD_EVENTS.has(event)) {
55 | wrapped = (a, arg) => handler(rvs(a), arg);
56 | } else if (event === 'createAnnotation') {
57 | wrapped = (a, overrideId) => handler(rvs(a), overrideId);
58 | } else if (event === 'updateAnnotation') {
59 | // updateAnnotation has two annotations as argument
60 | wrapped = (a, p) => handler(rvs(a), rvs(p));
61 | } else if (event === 'changeSelectionTarget') {
62 | wrapped = target => {
63 | // Crosswalks expect an object with a 'target'
64 | const wrapper = { target };
65 | handler(rvs(wrapper));
66 | };
67 | } else if (event === 'startSelection') {
68 | // TODO startSelection(point)
69 | throw 'startSelection event is not yet supported by the map annotation plugin';
70 | }
71 |
72 | if (wrapped) {
73 | // Keep a reference, so we can remove later
74 | handlers.push({ handler, wrapped });
75 |
76 | origHandler(event, wrapped);
77 | }
78 | }
79 |
80 | anno.on = wrapHandler(anno.on);
81 | anno.once = wrapHandler(anno.once);
82 |
83 | // Monkey-patch .off
84 | const _off = anno.off;
85 |
86 | anno.off = (event, optCallback) => {
87 | if (optCallback) {
88 | const t = handlers.find(({ handler }) => handler === optCallback);
89 | if (t)
90 | _off(event, t.wrapped);
91 | } else {
92 | _off(event);
93 | }
94 | }
95 |
96 | }
97 |
98 | export default MapAnnotationPlugin;
99 |
--------------------------------------------------------------------------------
/plugins/annotorious-map-annotation/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | const APP_DIR = fs.realpathSync(process.cwd());
5 |
6 | const resolveAppPath = relativePath => path.resolve(APP_DIR, relativePath);
7 |
8 | const HtmlWebpackPlugin = require('html-webpack-plugin');
9 |
10 | module.exports = {
11 | entry: resolveAppPath('src'),
12 | output: {
13 | filename: 'annotorious-map-annotation.js',
14 | library: ['Annotorious', 'MapAnnotation'],
15 | libraryTarget: 'umd',
16 | libraryExport: 'default'
17 | },
18 | performance: {
19 | hints: false
20 | },
21 | devtool: 'source-map',
22 | optimization: {
23 | minimize: true
24 | },
25 | resolve: {
26 | extensions: ['.js' ]
27 | },
28 | module: {
29 | rules: [
30 | {
31 | test: /\.js$/,
32 | use: {
33 | loader: 'babel-loader' ,
34 | options: {
35 | "presets": [
36 | "@babel/preset-env"
37 | ],
38 | "plugins": [
39 | [
40 | "@babel/plugin-proposal-class-properties"
41 | ]
42 | ]
43 | }
44 | }
45 | }
46 | ]
47 | },
48 | devServer: {
49 | compress: true,
50 | hot: true,
51 | host: process.env.HOST || 'localhost',
52 | port: 3000,
53 | static: [{
54 | directory: resolveAppPath('public'),
55 | publicPath: '/'
56 | },{
57 | directory: resolveAppPath('../../assets'),
58 | publicPath: '/'
59 | }]
60 | },
61 | plugins: [
62 | new HtmlWebpackPlugin ({
63 | template: resolveAppPath('public/index.html')
64 | })
65 | ]
66 | }
--------------------------------------------------------------------------------
/plugins/annotorious-osd-snap-polygon/.gitignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/plugins/annotorious-osd-snap-polygon/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@recogito/annotorious-osd-snap-polygon",
3 | "version": "0.2.4",
4 | "description": "A plugin for Annotorious OSD which implements a polygon tool with mouse cursor snapping behavior",
5 | "main": "dist/annotorious-osd-snap.js",
6 | "scripts": {
7 | "start": "webpack serve --open --mode=development",
8 | "build": "webpack --mode=production"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/recogito/recogito-client-plugins.git"
13 | },
14 | "author": "Rainer Simon",
15 | "license": "BSD-3-Clause",
16 | "bugs": {
17 | "url": "https://github.com/recogito/recogito-client-plugins/issues"
18 | },
19 | "homepage": "https://github.com/recogito/recogito-client-plugins/tree/main/plugins/annotorious-better-polygon",
20 | "devDependencies": {
21 | "@babel/core": "^7.15.8",
22 | "@babel/plugin-proposal-class-properties": "^7.14.5",
23 | "@babel/preset-env": "^7.15.8",
24 | "babel-loader": "^8.2.3",
25 | "css-loader": "^6.5.0",
26 | "html-webpack-plugin": "^5.5.0",
27 | "style-loader": "^3.3.1",
28 | "webpack": "^5.60.0",
29 | "webpack-cli": "^4.9.1",
30 | "webpack-dev-server": "^4.3.1"
31 | },
32 | "dependencies": {
33 | "@recogito/annotorious": "^2.7.10"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/plugins/annotorious-osd-snap-polygon/public/annotations.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "@context": "http://www.w3.org/ns/anno.jsonld",
4 | "type": "Annotation",
5 | "body": [
6 | {
7 | "type": "TextualBody",
8 | "value": "Annotation 01",
9 | "purpose": "commenting"
10 | }
11 | ],
12 | "target": {
13 | "source": "http://localhost:3000/Carta_Marina.jpeg",
14 | "selector": {
15 | "type": "SvgSelector",
16 | "value": " "
17 | }
18 | },
19 | "id": "#875aeab4-6e00-47ed-afaf-8483f1d6762b"
20 | },
21 | {
22 | "@context": "http://www.w3.org/ns/anno.jsonld",
23 | "type": "Annotation",
24 | "body": [
25 | {
26 | "type": "TextualBody",
27 | "value": "Annotation 02",
28 | "purpose": "commenting"
29 | }
30 | ],
31 | "target": {
32 | "source": "http://localhost:3000/Carta_Marina.jpeg",
33 | "selector": {
34 | "type": "SvgSelector",
35 | "value": " "
36 | }
37 | },
38 | "id": "#22d40c73-b9cb-4bb8-a935-a65df56442b9"
39 | },
40 | {
41 | "@context": "http://www.w3.org/ns/anno.jsonld",
42 | "type": "Annotation",
43 | "body": [
44 | {
45 | "type": "TextualBody",
46 | "value": "Annotation 3",
47 | "purpose": "commenting"
48 | }
49 | ],
50 | "target": {
51 | "source": "http://localhost:3000/Carta_Marina.jpeg",
52 | "selector": {
53 | "type": "FragmentSelector",
54 | "conformsTo": "http://www.w3.org/TR/media-frags/",
55 | "value": "xywh=pixel:1921.1610107421875,862.6381225585938,642.4395751953125,392.00738525390625"
56 | }
57 | },
58 | "id": "#42b4d395-68be-4e04-a99f-67a68115f64a"
59 | },
60 | {
61 | "@context": "http://www.w3.org/ns/anno.jsonld",
62 | "type": "Annotation",
63 | "body": [
64 | {
65 | "type": "TextualBody",
66 | "value": "Annotation 04",
67 | "purpose": "commenting"
68 | }
69 | ],
70 | "target": {
71 | "source": "http://localhost:3000/Carta_Marina.jpeg",
72 | "selector": {
73 | "type": "FragmentSelector",
74 | "conformsTo": "http://www.w3.org/TR/media-frags/",
75 | "value": "xywh=pixel:1536,2955,0,0"
76 | },
77 | "renderedVia": {
78 | "name": "point"
79 | }
80 | },
81 | "id": "#01d9c207-82f0-4a13-b24d-cd02fe080a78"
82 | }
83 | ]
--------------------------------------------------------------------------------
/plugins/annotorious-osd-snap-polygon/public/default.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/plugins/annotorious-osd-snap-polygon/public/default.jpg
--------------------------------------------------------------------------------
/plugins/annotorious-osd-snap-polygon/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | AnnotoriousOSD | Snap
5 |
6 |
7 |
8 |
27 |
28 |
29 | DRAW
30 |
31 |
32 |
33 |
73 |
74 |
--------------------------------------------------------------------------------
/plugins/annotorious-osd-snap-polygon/src/SnapCursor.js:
--------------------------------------------------------------------------------
1 | import { SVG_NAMESPACE } from '@recogito/annotorious/src/util/SVG';
2 |
3 | const CIRCLE_RADIUS = 6;
4 |
5 | const INDICATOR_RADIUS = 12;
6 |
7 | export const drawCursor = (xy, scale = 1) => {
8 | const [x, y] = xy;
9 |
10 | const containerGroup = document.createElementNS(SVG_NAMESPACE, 'g');
11 | containerGroup.setAttribute('class', 'a9s-snap-cursor');
12 |
13 | const drawCircle = r => {
14 | const c = document.createElementNS(SVG_NAMESPACE, 'circle');
15 | c.setAttribute('cx', x);
16 | c.setAttribute('cy', y);
17 | c.setAttribute('r', r * scale);
18 | c.setAttribute('transform-origin', `${x} ${y}`);
19 | return c;
20 | }
21 |
22 | const inner = drawCircle(CIRCLE_RADIUS);
23 | inner.setAttribute('class', 'a9s-snap-cursor-inner')
24 |
25 | const outer = drawCircle(INDICATOR_RADIUS);
26 | outer.setAttribute('class', 'a9s-snap-cursor-outer')
27 |
28 | containerGroup.appendChild(outer);
29 | containerGroup.appendChild(inner);
30 | return containerGroup;
31 | }
32 |
33 | export const setCursorXY = (cursor, xy) => {
34 | const [x, y] = xy;
35 |
36 | const inner = cursor.querySelector('.a9s-snap-cursor-inner');
37 | inner.setAttribute('cx', x);
38 | inner.setAttribute('cy', y);
39 | inner.setAttribute('transform-origin', `${x} ${y}`);
40 |
41 | const outer = cursor.querySelector('.a9s-snap-cursor-outer');
42 | outer.setAttribute('cx', x);
43 | outer.setAttribute('cy', y);
44 | outer.setAttribute('transform-origin', `${x} ${y}`);
45 | }
46 |
47 | export const setSnapEnabled = (cursor, enabled) => {
48 | if (enabled)
49 | cursor.setAttribute('class', 'a9s-snap-cursor active');
50 | else
51 | cursor.setAttribute('class', 'a9s-snap-cursor');
52 | }
53 |
54 | export const scaleCursor = (cursor, scale) => {
55 | const inner = cursor.querySelector('.a9s-snap-cursor-inner');
56 | const outer = cursor.querySelector('.a9s-snap-cursor-outer');
57 |
58 | inner.setAttribute('r', scale * CIRCLE_RADIUS);
59 | outer.setAttribute('r', scale * INDICATOR_RADIUS);
60 | }
--------------------------------------------------------------------------------
/plugins/annotorious-osd-snap-polygon/src/SnapRubberbandPolygon.js:
--------------------------------------------------------------------------------
1 | import { SVG_NAMESPACE } from '@recogito/annotorious/src/util/SVG';
2 | import { Selection, ToolLike } from '@recogito/annotorious/src/tools/Tool';
3 | import Mask from '@recogito/annotorious/src/tools/polygon/PolygonMask';
4 | import { toSVGTarget } from './SnapPolygonTool';
5 |
6 | export default class SnapRubberbandPolygon extends ToolLike {
7 |
8 | constructor(anchor, g, config, env) {
9 | super(g, config, env);
10 |
11 | // Needed later to construct the Selection
12 | this.env = env;
13 |
14 | // UI scale
15 | this.scale = 1;
16 |
17 | // Polygon state
18 | this.points = [ anchor ];
19 |
20 | // Mouse state
21 | this.mousepos = anchor;
22 |
23 | // SVG geometry
24 | this.container = document.createElementNS(SVG_NAMESPACE, 'g');
25 |
26 | // The selection consisting of an (inner and outer) path, and
27 | // the 'rubberband' polygon
28 | this.selection = document.createElementNS(SVG_NAMESPACE, 'g');
29 | this.selection.setAttribute('class', 'a9s-selection snap-polygon');
30 |
31 | this.outerPath = document.createElementNS(SVG_NAMESPACE, 'path');
32 | this.outerPath.setAttribute('class', 'a9s-outer');
33 |
34 | this.innerPath = document.createElementNS(SVG_NAMESPACE, 'path');
35 | this.innerPath.setAttribute('class', 'a9s-inner');
36 |
37 | this.rubberband = document.createElementNS(SVG_NAMESPACE, 'polygon');
38 | this.rubberband.setAttribute('class', 'a9s-rubberband');
39 |
40 | this.closeHandle = this.drawHandle(anchor[0], anchor[1]);
41 | this.closeHandle.style.display = 'none';
42 |
43 | this.setPoints(this.points);
44 |
45 | this.selection.appendChild(this.rubberband)
46 | this.selection.appendChild(this.outerPath);
47 | this.selection.appendChild(this.innerPath);
48 | this.selection.appendChild(this.closeHandle);
49 |
50 | this.mask = new Mask(env.image, this.rubberband);
51 |
52 | // Hide until user actually moves the mouse
53 | this.container.style.display = 'none';
54 |
55 | this.container.appendChild(this.mask.element);
56 | this.container.appendChild(this.selection);
57 |
58 | g.appendChild(this.container);
59 | }
60 |
61 | addPoint = () => {
62 | if (this.isClosable()) {
63 | // Close, don't add
64 | this.done(true);
65 | } else {
66 | // Don't add a new point if distance < 2 pixels
67 | const [x, y] = this.mousepos;
68 | const lastCorner = this.points[this.points.length - 1];
69 | const dist = Math.pow(x - lastCorner[0], 2) + Math.pow(y - lastCorner[1], 2);
70 |
71 | if (dist > 4) {
72 | this.points = [...this.points, this.mousepos];
73 | this.setPoints(this.points);
74 | this.mask.redraw();
75 | }
76 | }
77 | }
78 |
79 | done = (close) => {
80 | const selection = new Selection(toSVGTarget(this.points, this.env.image, close));
81 | this.emit('done', { shape: this.selection, selection });
82 | }
83 |
84 | destroy = () => {
85 | this.container.parentNode.removeChild(this.container);
86 | }
87 |
88 | dragTo = xy => {
89 | // Make visible
90 | this.container.style.display = null;
91 |
92 | this.mousepos = xy;
93 |
94 | // Distance to start
95 | const d = this.getDistanceToStart();
96 |
97 | // Display close handle if distance < 40px
98 | if (d < 40) {
99 | this.closeHandle.style.display = null;
100 | } else {
101 | this.closeHandle.style.display = 'none';
102 | }
103 |
104 | if (d < 20)
105 | this.mousepos = this.points[0];
106 |
107 | // Shape is points + (snapped) mousepos
108 | this.setPoints([ ...this.points, this.mousepos ]);
109 | this.mask.redraw();
110 | }
111 |
112 | get element() {
113 | return this.selection;
114 | }
115 |
116 | getBoundingClientRect = () =>
117 | this.rubberband.getBoundingClientRect();
118 |
119 | getDistanceToStart = () => {
120 | if (this.points.length < 3)
121 | return Infinity; // Just return if not at least 3 points
122 |
123 | const dx = Math.abs(this.mousepos[0] - this.points[0][0]);
124 | const dy = Math.abs(this.mousepos[1] - this.points[0][1]);
125 |
126 | return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) / this.scale;
127 | }
128 |
129 | /**
130 | * Tests if the mouse is over the first point, meaning that
131 | * the polygon would be closed on click
132 | */
133 | isClosable = () => {
134 | if (this.points.length > 2) {
135 | const d = this.getDistanceToStart();
136 | return d < 6 * this.scale;
137 | } else {
138 | return false;
139 | }
140 | }
141 |
142 | onScaleChanged = scale => {
143 | this.scale = scale;
144 |
145 | const inner = this.closeHandle.querySelector('.a9s-handle-inner');
146 | const outer = this.closeHandle.querySelector('.a9s-handle-outer');
147 |
148 | const radius = scale * (this.config.handleRadius || 6);
149 |
150 | inner.setAttribute('r', radius);
151 | outer.setAttribute('r', radius);
152 | }
153 |
154 | /** Removes last corner **/
155 | pop = () => {
156 | this.points.pop();
157 | this.setPoints(this.points);
158 | this.mask.redraw();
159 | }
160 |
161 | setPoints = arr => {
162 | const [head, ...tail]= arr;
163 |
164 | const path =
165 | `M${head[0]} ${head[1]} ` +
166 | tail.map(([x,y]) => `L${x} ${y}`).join(' ');
167 |
168 | this.outerPath.setAttribute('d', path);
169 | this.innerPath.setAttribute('d', path);
170 |
171 | const points = arr.map(t => `${t[0]},${t[1]}`).join(' ');
172 | this.rubberband.setAttribute('points', points);
173 | }
174 |
175 | }
--------------------------------------------------------------------------------
/plugins/annotorious-osd-snap-polygon/src/index.css:
--------------------------------------------------------------------------------
1 | /** Cursor **/
2 | .a9s-snap-cursor .a9s-snap-cursor-inner {
3 | stroke-width: 1px !important;
4 | stroke: #000 !important;
5 | fill: none !important;
6 | vector-effect: non-scaling-stroke;
7 | }
8 |
9 | .a9s-snap-cursor .a9s-snap-cursor-outer {
10 | opacity: 0;
11 | stroke-width: 6px !important;
12 | stroke: rgba(0, 0, 0, 0.35) !important;
13 | fill: none !important;
14 | stroke-dasharray: 12.6, 6.1 !important;
15 | transform: rotateZ(11.25deg);
16 | vector-effect: non-scaling-stroke;
17 | }
18 |
19 | .a9s-snap-cursor.active .a9s-snap-cursor-outer {
20 | opacity: 1;
21 | }
22 |
23 | /** Rubberband **/
24 | .a9s-selection.snap-polygon .a9s-rubberband {
25 | fill:rgba(255,255,255,0.15);
26 | stroke-width:1px;
27 | stroke:rgba(0, 0, 0, 0.3);
28 | }
29 |
30 | /** Polygon **/
31 | .a9s-selection.snap-polygon .a9s-inner,
32 | .a9s-annotation.snap-polygon.editable .a9s-inner {
33 | stroke-width:2px;
34 | stroke:#fff;
35 | stroke-dasharray:5 3;
36 | }
37 |
38 | .a9s-annotation.snap-polygon.editable .a9s-inner:hover {
39 | fill:transparent;
40 | }
41 |
42 | .a9s-selection.snap-polygon .a9s-outer,
43 | .a9s-annotation.snap-polygon.editable .a9s-outer {
44 | stroke-width:4px;
45 | stroke:rgba(0, 0, 0, 0.35);
46 | }
47 |
48 | /** Corner handles **/
49 | .a9s-selection.snap-polygon .a9s-handle .a9s-handle-outer,
50 | .a9s-annotation.snap-polygon.editable .a9s-handle .a9s-handle-outer {
51 | opacity: 0;
52 | stroke-width: 6px !important;
53 | stroke: rgba(0, 0, 0, 0.35) !important;
54 | fill: none;
55 | stroke-dasharray: 12.6, 6.1;
56 | transform: rotateZ(11.25deg);
57 | vector-effect: non-scaling-stroke;
58 | }
59 |
60 | .a9s-selection.snap-polygon.active .a9s-handle .a9s-handle-outer,
61 | .a9s-annotation.snap-polygon.active.editable .a9s-handle .a9s-handle-outer {
62 | opacity: 1;
63 | }
64 |
65 | .a9s-selection.snap-polygon .a9s-handle .a9s-handle-inner,
66 | .a9s-annotation.snap-polygon.editable .a9s-handle .a9s-handle-inner {
67 | stroke-width:2;
68 | stroke:#fff;
69 | fill:#000;
70 | }
71 |
72 | .a9s-selection.snap-polygon .a9s-handle .a9s-handle-inner:hover,
73 | .a9s-annotation.snap-polygon.editable .a9s-handle.selected .a9s-handle-inner,
74 | .a9s-annotation.snap-polygon.editable .a9s-handle .a9s-handle-inner:hover {
75 | fill:#fff;
76 | }
77 |
78 | /** Midpoints **/
79 | .a9s-annotation.snap-polygon.editable .a9s-midpoint {
80 | display:none;
81 | fill:rgba(255,255,255,0.65);
82 | stroke-width:1;
83 | stroke:rgba(0,0,0,0.65);
84 | }
85 |
86 | .a9s-annotation.snap-polygon.editable:hover .a9s-midpoint {
87 | display:block;
88 | }
89 |
90 | .a9s-annotation.snap-polygon.editable .a9s-midpoint:hover {
91 | fill:#fff;
92 | }
--------------------------------------------------------------------------------
/plugins/annotorious-osd-snap-polygon/src/index.js:
--------------------------------------------------------------------------------
1 | import SnapPolygonTool from './SnapPolygonTool';
2 |
3 | import './index.css';
4 |
5 | const SnappablePolygonPlugin = anno => {
6 |
7 | anno.addDrawingTool(SnapPolygonTool);
8 |
9 | }
10 |
11 | export default SnappablePolygonPlugin;
--------------------------------------------------------------------------------
/plugins/annotorious-osd-snap-polygon/src/storeUtils.js:
--------------------------------------------------------------------------------
1 | import {
2 | parseRectFragment,
3 | svgFragmentToShape,
4 | } from '@recogito/annotorious/src/selectors';
5 |
6 | const dist = (a, b) => {
7 | const dx = b[0] - a[0];
8 | const dy = b[1] - a[1];
9 |
10 | return Math.sqrt(dx * dx + dy * dy);
11 | }
12 |
13 | export const getNearestSnappablePoint = (env, currentScale, xy, threshold = 20) => {
14 | // Distance buffer for querying the store
15 | const buffer = threshold * currentScale;
16 |
17 | // Query bounds
18 | const vicinity = {
19 | minX: xy[0] - buffer,
20 | minY: xy[1] - buffer,
21 | maxX: xy[0] + buffer,
22 | maxY: xy[1] + buffer
23 | };
24 |
25 | // All annotations intersecting the vicinity
26 | const nearby = env.store.getAnnotationsIntersecting(vicinity);
27 |
28 | // Parse annotations and extract corner points
29 | const points = nearby.reduce((all, annotation) => {
30 | const { selector } = annotation.target;
31 |
32 | if (selector.type === 'SvgSelector') {
33 | const shape = svgFragmentToShape(annotation);
34 | const nodeName = shape.nodeName.toLowerCase();
35 |
36 | if (nodeName === 'polygon') {
37 | const points = shape.getAttribute('points')
38 | .split(' ')
39 | .map(xy => xy.split(',').map(d => parseFloat(d.trim())));
40 |
41 | return [...all, ...points];
42 | } else if (nodeName === 'path') {
43 | const d = shape.getAttribute('d');
44 |
45 | const points = d.split(/[ML]/)
46 | .map(str => str.trim())
47 | .filter(str => str)
48 | .map(str => str.split(' ').map(parseFloat));
49 |
50 | return [...all, ...points];
51 | } else {
52 | return all;
53 | }
54 | } else if (selector.type === 'FragmentSelector') {
55 | const { x, y, w, h } = parseRectFragment(annotation);
56 |
57 | const points = (w + h) > 0 ? [
58 | [ x, y ],
59 | [ x + w, y ],
60 | [ x + w, y + h ],
61 | [ x, y + h ]
62 | ] : [
63 | [ x, y]
64 | ];
65 |
66 | return [...all, ...points ];
67 | } else {
68 | console.warn('Unsupported selector type: ' + selector.type);
69 | return all;
70 | }
71 | }, []);
72 |
73 | // Remove all points further away than 'threshold'
74 | const nearbyPoints = points.filter(pt => dist(xy, pt) <= buffer);
75 |
76 | // Sort by distance
77 | nearbyPoints.sort((a, b) => dist(xy, a) - dist(xy, b));
78 |
79 | return nearbyPoints.length === 0 ? null : nearbyPoints[0];
80 | }
81 |
--------------------------------------------------------------------------------
/plugins/annotorious-osd-snap-polygon/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 |
6 | const APP_DIR = fs.realpathSync(process.cwd());
7 |
8 | const resolveAppPath = relativePath => path.resolve(APP_DIR, relativePath);
9 |
10 | module.exports = {
11 | entry: resolveAppPath('src'),
12 | output: {
13 | filename: 'annotorious-osd-snap.js',
14 | library: ['Annotorious', 'SnapPolygon'],
15 | libraryTarget: 'umd',
16 | libraryExport: 'default'
17 | },
18 | performance: {
19 | hints: false
20 | },
21 | optimization: {
22 | minimize: true
23 | },
24 | resolve: {
25 | extensions: ['.js']
26 | },
27 | module: {
28 | rules: [
29 | {
30 | test: /\.js$/,
31 | use: {
32 | loader: 'babel-loader' ,
33 | options: {
34 | "presets": [
35 | "@babel/preset-env",
36 | ],
37 | "plugins": [
38 | [
39 | "@babel/plugin-proposal-class-properties"
40 | ]
41 | ]
42 | }
43 | }
44 | },
45 | { test: /\.css$/, use: [ 'style-loader', 'css-loader'] },
46 | ]
47 | },
48 | devServer: {
49 | compress: true,
50 | hot: true,
51 | host: process.env.HOST || 'localhost',
52 | port: 3000,
53 | static: [{
54 | directory: resolveAppPath('public'),
55 | publicPath: '/'
56 | },{
57 | directory: resolveAppPath('../../assets'),
58 | publicPath: '/'
59 | }]
60 | },
61 | plugins: [
62 | new HtmlWebpackPlugin ({
63 | template: resolveAppPath('public/index.html')
64 | })
65 | ]
66 | }
--------------------------------------------------------------------------------
/plugins/annotorious-segment-outline/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/plugins/annotorious-segment-outline/README.md
--------------------------------------------------------------------------------
/plugins/annotorious-segment-outline/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Annotorious | TFSegment
5 |
6 |
7 |
13 |
14 |
15 |
16 |
17 |
18 |
27 |
28 |
--------------------------------------------------------------------------------
/plugins/annotorious-segment-outline/src/index.js:
--------------------------------------------------------------------------------
1 | import * as deeplab from '@tensorflow-models/deeplab';
2 |
3 | const segment = async (canvas, model) => {
4 | console.time("segmenting");
5 | const segments = await model.segment(canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height));
6 | console.timeEnd("segmenting");
7 |
8 | const mask = new ImageData(segments.segmentationMap, segments.width, segments.height);
9 | console.log(segments);
10 |
11 | const canvas2 = document.createElement('canvas');
12 | canvas2.width = segments.width;
13 | canvas2.height = segments.height
14 | const ctx = canvas2.getContext("2d");
15 | ctx.putImageData(mask, 0, 0);
16 |
17 | document.getElementById('app').appendChild(canvas2);
18 | }
19 |
20 | const TFSegment = anno => {
21 |
22 | // Set to your preferred model: 'pascal', 'cityscapes' or 'ade20k'
23 | console.time("loading segmentation model");
24 | deeplab.load({ base: "pascal", quantizationBytes: 4 }).then(model => {
25 | console.timeEnd("loading segmentation model");
26 |
27 | const image = document.getElementById('image');
28 | const anno = new Annotorious({ image });
29 |
30 | let canvas;
31 |
32 | anno.on('createSelection', selection => {
33 | // Parse fragment selector
34 | const [ x, y, w, h ] = selection.target.selector.value
35 | .split(':')[1]
36 | .split(',')
37 | .map(str => parseFloat(str));
38 |
39 | // Read selected image data
40 | requestAnimationFrame(() => {
41 | canvas = document.createElement('canvas');
42 | const ctx = canvas.getContext('2d');
43 | ctx.canvas.width = w;
44 | ctx.canvas.height = h;
45 | ctx.drawImage(image, x, y, w, h, 0, 0, w, h);
46 |
47 | document.getElementById('app').appendChild(canvas);
48 | });
49 |
50 | requestAnimationFrame(() => segment(canvas, model));
51 | });
52 | });
53 |
54 | }
55 |
56 | export default TFSegment;
--------------------------------------------------------------------------------
/plugins/annotorious-segment-outline/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const TerserPlugin = require('terser-webpack-plugin');
6 |
7 | const APP_DIR = fs.realpathSync(process.cwd());
8 |
9 | const resolveAppPath = relativePath => path.resolve(APP_DIR, relativePath);
10 |
11 | module.exports = {
12 | entry: resolveAppPath('src'),
13 | output: {
14 | filename: 'annotorious-tf-segment.min.js',
15 | library: ['Annotorious', 'TFSegment'],
16 | libraryTarget: 'umd',
17 | libraryExport: 'default'
18 | },
19 | performance: {
20 | hints: false
21 | },
22 | optimization: {
23 | minimize: true,
24 | minimizer: [new TerserPlugin()],
25 | },
26 | resolve: {
27 | extensions: ['.js']
28 | },
29 | module: {
30 | rules: [
31 | {
32 | test: /\.js$/,
33 | use: {
34 | loader: 'babel-loader' ,
35 | options: {
36 | "presets": [
37 | "@babel/preset-env",
38 | ],
39 | "plugins": [
40 | [
41 | "@babel/plugin-proposal-class-properties"
42 | ]
43 | ]
44 | }
45 | }
46 | },
47 | { test: /\.css$/, use: [ 'style-loader', 'css-loader'] },
48 | ]
49 | },
50 | devServer: {
51 | contentBase: [ resolveAppPath('public'), resolveAppPath('../../public') ],
52 | compress: true,
53 | hot: true,
54 | host: process.env.HOST || 'localhost',
55 | port: 3000,
56 | publicPath: '/'
57 | },
58 | plugins: [
59 | new HtmlWebpackPlugin ({
60 | template: resolveAppPath('public/index.html')
61 | })
62 | ]
63 | }
--------------------------------------------------------------------------------
/plugins/annotorious-sequence-mode/.gitignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/plugins/annotorious-sequence-mode/README.md:
--------------------------------------------------------------------------------
1 | # Annotorious Sequence Mode Plugin
2 |
3 | A helper to simplify the use of Annotorious with OpenSeadragon Sequence Mode.
4 |
5 | Contributed by [Umesh Timalsina](https://github.com/umesh-timalsina).
6 |
7 | ## Run in development mode
8 |
9 | ```sh
10 | $ npm install
11 | $ npm start
12 | ```
13 |
14 | ## Build from source
15 |
16 | ```sh
17 | $ npm run build
18 | ```
--------------------------------------------------------------------------------
/plugins/annotorious-sequence-mode/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@recogito/annotorious-sequence-mode",
3 | "version": "0.1.0",
4 | "description": "Adds support for annotation pagination in OpenSeadragon sequence mode.",
5 | "main": "src/index.js",
6 | "files": [
7 | "dist"
8 | ],
9 | "scripts": {
10 | "start": "webpack serve --open --mode=development",
11 | "build": "webpack --mode=production"
12 | },
13 | "license": "BSD-3-Clause",
14 | "devDependencies": {
15 | "@babel/core": "^7.12.10",
16 | "@babel/plugin-proposal-class-properties": "^7.13.0",
17 | "@babel/preset-env": "^7.12.11",
18 | "babel-loader": "^8.2.2",
19 | "css-loader": "^3.2.0",
20 | "html-webpack-plugin": "^5.5.0",
21 | "sass": "^1.43.3",
22 | "sass-loader": "^8.0.0",
23 | "style-loader": "^2.0.0",
24 | "webpack": "^5.60.0",
25 | "webpack-cli": "^4.9.1",
26 | "webpack-dev-server": "^4.3.1"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/plugins/annotorious-sequence-mode/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Annotorious | Sequence Mode Example
6 |
7 |
8 |
48 |
132 |
133 |
134 |
135 |
Annotorious-OpenSeadragon | Sequence Mode Plugin
136 |
137 | When this plugin is used with OpenSeadragon and the osd viewer is in sequence mode,
138 | the annotation layer is cleared and all the annotations are removed, this plugin implements
139 | a simple pagination. The function getAllAnnotations
is also added to the
140 | annotator which can be used to get all the annotations pages.
141 |
142 |
143 |
144 |
145 |
146 |
147 |
--------------------------------------------------------------------------------
/plugins/annotorious-sequence-mode/public/wadirum.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/plugins/annotorious-sequence-mode/public/wadirum.jpg
--------------------------------------------------------------------------------
/plugins/annotorious-sequence-mode/src/index.js:
--------------------------------------------------------------------------------
1 | const SequenceModePlugin = (anno, viewer, opts={}) => {
2 |
3 | const pagedAnnotations = [];
4 | const {initialAnnotations} = opts;
5 |
6 | const addOrUpdateAnnotationPages = (annotation) => {
7 | const currentPage = viewer.currentPage();
8 |
9 | if(!pagedAnnotations[currentPage]) {
10 | pagedAnnotations[currentPage] = {};
11 | }
12 |
13 | pagedAnnotations[currentPage][annotation.id] = annotation;
14 | };
15 |
16 | const removePagedAnnotation = (annotation) => {
17 | const currentPage = viewer.currentPage();
18 | delete pagedAnnotations[currentPage]?.[annotation.id];
19 | };
20 |
21 | anno.on('createAnnotation', addOrUpdateAnnotationPages);
22 | anno.on('updateAnnotation', addOrUpdateAnnotationPages);
23 | anno.on('deleteAnnotation', removePagedAnnotation);
24 |
25 | viewer.addHandler('open', () => {
26 | const tileSourceURL = viewer.world.getItemAt(0).source.url;
27 | const currentPage = viewer.currentPage();
28 | if(!pagedAnnotations[currentPage] && initialAnnotations?.[tileSourceURL]) {
29 | pagedAnnotations[currentPage] = {};
30 | initialAnnotations[tileSourceURL].forEach(annotation => {
31 | pagedAnnotations[currentPage][annotation.id] = annotation;
32 | });
33 | }
34 | anno.setAnnotations(Object.values(pagedAnnotations[currentPage]||{}));
35 | });
36 |
37 |
38 | viewer.addHandler('page', () => {
39 | anno.cancelSelected();
40 | anno.clearAnnotations();
41 | });
42 |
43 | anno.getAllAnnotations = () => {
44 | return pagedAnnotations
45 | .filter(val => !!val)
46 | .map(Object.values)
47 | .flat();
48 | };
49 | };
50 |
51 | export default SequenceModePlugin;
--------------------------------------------------------------------------------
/plugins/annotorious-sequence-mode/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | const APP_DIR = fs.realpathSync(process.cwd());
5 |
6 | const resolveAppPath = relativePath => path.resolve(APP_DIR, relativePath);
7 |
8 | const HtmlWebpackPlugin = require('html-webpack-plugin');
9 |
10 | module.exports = {
11 | entry: resolveAppPath('src'),
12 | output: {
13 | filename: 'annotorious-sequence-mode.min.js',
14 | library: ['Annotorious', 'SequenceMode'],
15 | libraryTarget: 'umd',
16 | libraryExport: 'default',
17 | pathinfo: true
18 | },
19 | performance: {
20 | hints: false
21 | },
22 | devtool: 'source-map',
23 | optimization: {
24 | minimize: true
25 | },
26 | resolve: {
27 | extensions: ['.js' ]
28 | },
29 | module: {
30 | rules: [
31 | {
32 | test: /\.js$/,
33 | use: {
34 | loader: 'babel-loader' ,
35 | options: {
36 | "presets": [
37 | "@babel/preset-env"
38 | ],
39 | "plugins": [
40 | [
41 | "@babel/plugin-proposal-class-properties"
42 | ]
43 | ]
44 | }
45 | }
46 | },
47 | { test: /\.scss$/, use: [ 'style-loader', 'css-loader', 'sass-loader' ] }
48 | ]
49 | },
50 | devServer: {
51 | compress: true,
52 | hot: true,
53 | host: process.env.HOST || 'localhost',
54 | port: 3000,
55 | static: [{
56 | directory: resolveAppPath('public'),
57 | publicPath: '/'
58 | }, {
59 | directory: resolveAppPath('../../assets'),
60 | publicPath: '/'
61 | }]
62 | },
63 | plugins: [
64 | new HtmlWebpackPlugin ({
65 | template: resolveAppPath('public/index.html')
66 | })
67 | ]
68 | }
--------------------------------------------------------------------------------
/plugins/annotorious-shape-labels/README.md:
--------------------------------------------------------------------------------
1 | # Annotorious Shape Labels
2 |
3 | A plugin for Annotorious and Annotorious OpenSeadragon that adds a the first tag as a label
4 | to the annotation shape.
5 |
6 | 
7 |
8 | ## Install
9 |
10 | Download the [latest minified release](https://github.com/recogito/recogito-client-plugins/blob/main/plugins/annotorious-shape-labels/dist/annotorious-shape-labels.min.js) or include directly via CDN.
11 |
12 | ```html
13 |
14 | ```
15 |
16 | Import via npm:
17 |
18 | ```sh
19 | npm install @recogito/annotorious-shape-labels
20 | ```
21 |
22 | ## Use
23 |
24 | ```html
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
44 |
45 |
46 | ```
47 |
48 | ## CSS Styling
49 |
50 | The label is inserted into the SVG annotation group as a `foreignObject` element. In addition,
51 | the plugin adds the first tag as an extra CSS class to the annotation shape.
52 |
53 | To apply your own CSS styles, follow this structure:
54 |
55 | ```svg
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | FirstTagValue
64 |
65 |
66 |
67 |
68 | ```
--------------------------------------------------------------------------------
/plugins/annotorious-shape-labels/dist/annotorious-shape-labels.min.js:
--------------------------------------------------------------------------------
1 | !function(e,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?exports.Annotorious=n():(e.Annotorious=e.Annotorious||{},e.Annotorious.ShapeLabelsFormatter=n())}(self,(function(){return(()=>{"use strict";var e={922:e=>{e.exports=function(e){var n=[];return n.toString=function(){return this.map((function(n){var t=e(n);return n[2]?"@media ".concat(n[2]," {").concat(t,"}"):t})).join("")},n.i=function(e,t,r){"string"==typeof e&&(e=[[null,e,""]]);var o={};if(r)for(var a=0;a{t.d(n,{Z:()=>a});var r=t(922),o=t.n(r)()((function(e){return e[1]}));o.push([e.id,".a9s-annotationlayer .a9s-formatter-el,\n.a9s-annotationlayer .a9s-formatter-el foreignObject {\n overflow:visible;\n pointer-events:none;\n}\n\n.a9s-annotationlayer .a9s-formatter-el foreignObject .a9s-shape-label-wrapper {\n position:relative;\n transform:translateY(-100%);\n padding-bottom:4px;\n}\n\n.a9s-annotationlayer .a9s-formatter-el foreignObject .a9s-shape-label-wrapper .a9s-shape-label {\n display:table;\n padding:3px 5px;\n white-space:nowrap;\n background-color:rgba(255, 255, 255, 0.85);\n border-radius:3px;\n font-size:14px;\n}",""]);const a=o},379:(e,n,t)=>{var r,o=function(){var e={};return function(n){if(void 0===e[n]){var t=document.querySelector(n);if(window.HTMLIFrameElement&&t instanceof window.HTMLIFrameElement)try{t=t.contentDocument.head}catch(e){t=null}e[n]=t}return e[n]}}(),a=[];function i(e){for(var n=-1,t=0;t{var n=e&&e.__esModule?()=>e.default:()=>e;return t.d(n,{a:n}),n},t.d=(e,n)=>{for(var r in n)t.o(n,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:n[r]})},t.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n);var r={};return(()=>{t.d(r,{default:()=>a});var e=t(379),n=t.n(e),o=t(424);n()(o.Z,{insert:"head",singleton:!1}),o.Z.locals;const a=function(e){return function(e){var n=(Array.isArray(e.body)?e.body:[e.body]).find((function(e){return"tagging"==e.purpose}));if(n){var t=document.createElementNS("http://www.w3.org/2000/svg","foreignObject");return t.setAttribute("width","1px"),t.setAttribute("height","1px"),t.innerHTML='\n \n
\n '.concat(n.value,"\n
\n
"),{element:t,className:n.value}}}}})(),r.default})()}));
--------------------------------------------------------------------------------
/plugins/annotorious-shape-labels/dist/index.html:
--------------------------------------------------------------------------------
1 | Shape Labels Plugin
--------------------------------------------------------------------------------
/plugins/annotorious-shape-labels/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@recogito/annotorious-shape-labels",
3 | "version": "0.2.4",
4 | "description": "Shape label plugin for Annotorious and Annotorious OpenSeadragon",
5 | "main": "dist/annotorious-shape-labels.min.js",
6 | "scripts": {
7 | "start": "webpack serve --open --mode=development",
8 | "build": "webpack --mode=production"
9 | },
10 | "author": "Rainer Simon",
11 | "license": "BSD-3-Clause",
12 | "devDependencies": {
13 | "@babel/core": "^7.6.2",
14 | "@babel/plugin-proposal-class-properties": "^7.5.5",
15 | "@babel/preset-env": "^7.6.2",
16 | "babel-loader": "^8.0.6",
17 | "css-loader": "^5.2.5",
18 | "html-webpack-plugin": "^5.5.0",
19 | "style-loader": "^2.0.0",
20 | "webpack": "^5.60.0",
21 | "webpack-cli": "^4.9.1",
22 | "webpack-dev-server": "^4.3.1"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/plugins/annotorious-shape-labels/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Shape Labels Plugin
5 |
6 |
7 |
8 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
58 |
59 |
--------------------------------------------------------------------------------
/plugins/annotorious-shape-labels/public/osd.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Shape Labels Plugin
5 |
6 |
7 |
8 |
9 |
15 |
16 |
17 |
18 |
19 |
36 |
37 |
--------------------------------------------------------------------------------
/plugins/annotorious-shape-labels/screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/plugins/annotorious-shape-labels/screenshot.jpg
--------------------------------------------------------------------------------
/plugins/annotorious-shape-labels/src/index.css:
--------------------------------------------------------------------------------
1 | .a9s-annotationlayer .a9s-formatter-el,
2 | .a9s-annotationlayer .a9s-formatter-el foreignObject {
3 | overflow:visible;
4 | pointer-events:none;
5 | }
6 |
7 | .a9s-annotationlayer .a9s-formatter-el foreignObject .a9s-shape-label-wrapper {
8 | position:relative;
9 | transform:translateY(-100%);
10 | padding-bottom:4px;
11 | }
12 |
13 | .a9s-annotationlayer .a9s-formatter-el foreignObject .a9s-shape-label-wrapper .a9s-shape-label {
14 | display:table;
15 | padding:3px 5px;
16 | white-space:nowrap;
17 | background-color:rgba(255, 255, 255, 0.85);
18 | border-radius:3px;
19 | font-size:14px;
20 | }
--------------------------------------------------------------------------------
/plugins/annotorious-shape-labels/src/index.js:
--------------------------------------------------------------------------------
1 | import './index.css';
2 |
3 | const ShapeLabelsFormatter = config => annotation => {
4 |
5 | const bodies = Array.isArray(annotation.body) ?
6 | annotation.body : [ annotation.body ];
7 |
8 | const firstTag = bodies.find(b => b.purpose == 'tagging');
9 |
10 | if (firstTag) {
11 | const foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
12 |
13 | // Overflow is set to visible, but the foreignObject needs >0 zero size,
14 | // otherwise FF doesn't render...
15 | foreignObject.setAttribute('width', '1px');
16 | foreignObject.setAttribute('height', '1px');
17 |
18 | foreignObject.innerHTML = `
19 |
20 |
21 | ${firstTag.value}
22 |
23 |
`;
24 |
25 | return {
26 | element: foreignObject,
27 | className: firstTag.value
28 | };
29 | }
30 | }
31 |
32 | export default ShapeLabelsFormatter;
--------------------------------------------------------------------------------
/plugins/annotorious-shape-labels/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 |
6 | const APP_DIR = fs.realpathSync(process.cwd());
7 |
8 | const resolveAppPath = relativePath => path.resolve(APP_DIR, relativePath);
9 |
10 | module.exports = {
11 | entry: resolveAppPath('src'),
12 | output: {
13 | filename: 'annotorious-shape-labels.min.js',
14 | library: ['Annotorious', 'ShapeLabelsFormatter'],
15 | libraryTarget: 'umd',
16 | libraryExport: 'default'
17 | },
18 | performance: {
19 | hints: false
20 | },
21 | optimization: {
22 | minimize: true
23 | },
24 | resolve: {
25 | extensions: ['.js']
26 | },
27 | module: {
28 | rules: [
29 | {
30 | test: /\.js$/,
31 | use: {
32 | loader: 'babel-loader' ,
33 | options: {
34 | "presets": [
35 | "@babel/preset-env"
36 | ],
37 | "plugins": [
38 | [
39 | "@babel/plugin-proposal-class-properties"
40 | ]
41 | ]
42 | }
43 | }
44 | },
45 | { test: /\.css$/, use: [ 'style-loader', 'css-loader'] },
46 | ]
47 | },
48 | devServer: {
49 | compress: true,
50 | hot: true,
51 | host: process.env.HOST || 'localhost',
52 | port: 3000,
53 | static: [{
54 | directory: resolveAppPath('public'),
55 | publicPath: '/'
56 | },{
57 | directory: resolveAppPath('../../assets'),
58 | publicPath: '/'
59 | }]
60 | },
61 | plugins: [
62 | new HtmlWebpackPlugin ({
63 | template: resolveAppPath('public/index.html')
64 | })
65 | ]
66 | }
--------------------------------------------------------------------------------
/plugins/annotorious-tensorflow-tag-suggestions/.gitignore:
--------------------------------------------------------------------------------
1 | dist/*.js
--------------------------------------------------------------------------------
/plugins/annotorious-tensorflow-tag-suggestions/README.md:
--------------------------------------------------------------------------------
1 | # Annotorious Tensorflow Tag Suggestions
2 |
3 | A plugin that adds AI-powered automatic tag suggestions to Annotorious and Annotorious OpenSeadragon. Tag image regions manually first.
4 | After learning from at least two examples, the plugin provides tag suggestions automatically. Uses Transfer Learning in
5 | [Tensorflow.js](https://www.tensorflow.org/js) on top of the MobileNet image classifier. Based on this example:
6 |
7 | https://codelabs.developers.google.com/codelabs/tensorflowjs-teachablemachine-codelab/index.html#6
8 |
9 | 
10 |
11 | ## Installation
12 |
13 | Install via npm
14 |
15 | ```sh
16 | npm install @recogito/annotorious-tensorflow-tag-suggestions
17 | ```
18 |
19 | or include in the page
20 |
21 | ```html
22 |
23 | ```
24 |
25 | ## Example
26 |
27 | ```html
28 |
29 |
30 |
31 | Annotorious Smart Tagging Demo
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
60 |
61 |
62 | ```
63 |
--------------------------------------------------------------------------------
/plugins/annotorious-tensorflow-tag-suggestions/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | sourceMaps: true,
3 | presets: [
4 | [ "@babel/preset-env", {
5 | "forceAllTransforms": true,
6 | "useBuiltIns": "usage",
7 | "corejs": 3
8 | }]
9 | ]
10 | };
--------------------------------------------------------------------------------
/plugins/annotorious-tensorflow-tag-suggestions/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Annotorious Smart Tagging Demo
5 |
6 |
7 |
8 |
9 |
47 |
48 |
49 |
50 |
51 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/plugins/annotorious-tensorflow-tag-suggestions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@recogito/annotorious-tensorflow-tag-suggestions",
3 | "version": "0.1.2",
4 | "description": "An automated tagging plugin for Annotorious and AnnotoriousOSD using MobileNet and TensorflowJS",
5 | "main": "dist/annotorious-tf-tag-suggestions.min.js",
6 | "scripts": {
7 | "build": "rollup --config"
8 | },
9 | "author": "Rainer Simon",
10 | "license": "BSD-3-Clause",
11 | "devDependencies": {
12 | "@babel/core": "^7.12.10",
13 | "@babel/preset-env": "^7.12.11",
14 | "@rollup/plugin-babel": "^5.2.2",
15 | "@rollup/plugin-commonjs": "^17.0.0",
16 | "@rollup/plugin-node-resolve": "^11.0.1",
17 | "rollup": "^2.35.1",
18 | "rollup-plugin-node-polyfills": "^0.2.1",
19 | "rollup-plugin-serve": "^1.1.0",
20 | "rollup-plugin-uglify": "^6.0.4"
21 | },
22 | "dependencies": {
23 | "@tensorflow-models/knn-classifier": "^1.2.2",
24 | "@tensorflow-models/mobilenet": "^2.0.4",
25 | "@tensorflow/tfjs": "^2.7.0"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/plugins/annotorious-tensorflow-tag-suggestions/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { uglify } from 'rollup-plugin-uglify';
2 | import babel from '@rollup/plugin-babel';
3 | import { nodeResolve } from '@rollup/plugin-node-resolve';
4 | import commonjs from '@rollup/plugin-commonjs';
5 | import nodePolyfills from 'rollup-plugin-node-polyfills';
6 | import serve from 'rollup-plugin-serve';
7 |
8 | const config = {
9 | input: 'src/index.js',
10 | output: {
11 | file: 'dist/annotorious-tf-tag-suggestions.min.js',
12 | format: 'umd',
13 | name: 'recogito.AnnotoriousTFSuggestions',
14 | compact: true,
15 | },
16 | plugins: [,
17 | nodePolyfills(),
18 | nodeResolve(),
19 | commonjs(),
20 | babel({ babelHelpers: 'bundled' }),
21 | uglify(),
22 | serve({
23 | open: true,
24 | contentBase: ['dist', '../../public']
25 | })
26 | ]
27 | };
28 |
29 | export default config;
--------------------------------------------------------------------------------
/plugins/annotorious-tensorflow-tag-suggestions/screenshot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/plugins/annotorious-tensorflow-tag-suggestions/screenshot.gif
--------------------------------------------------------------------------------
/plugins/annotorious-tensorflow-tag-suggestions/src/index.js:
--------------------------------------------------------------------------------
1 | import * as MobileNet from '@tensorflow-models/mobilenet';
2 | import * as KNNClassifier from '@tensorflow-models/knn-classifier';
3 |
4 | import '@tensorflow/tfjs';
5 |
6 | const AnnotoriousSmartTagging = async (anno, onLoad) => {
7 | console.log('Loading MobileNet');
8 | console.time('MobileNet loaded');
9 | const mnet = await MobileNet.load();
10 | console.timeEnd('MobileNet loaded');
11 |
12 | const classifier = KNNClassifier.create();
13 |
14 | // When the user creates a new selection, we'll classify the snippet
15 | anno.on('createSelection', async function(selection) {
16 | if (classifier.getNumClasses() > 1) {
17 | const { snippet } = anno.getSelectedImageSnippet();
18 |
19 | const activation = mnet.infer(snippet, 'conv_preds');
20 | const result = await classifier.predictClass(activation);
21 |
22 | if (result) {
23 | // Inject into the current annotation
24 | selection.body = [{
25 | type: 'TextualBody',
26 | purpose: 'tagging',
27 | value: result.label
28 | }];
29 |
30 | anno.updateSelected(selection);
31 | }
32 | }
33 | });
34 |
35 | // When the user hits 'Ok', we'll store the snippet as a new example
36 | anno.on('createAnnotation', function(annotation) {
37 | const { snippet } = anno.getSelectedImageSnippet();
38 | const tag = annotation.body.find(b => b.purpose === 'tagging').value;
39 |
40 | // See https://codelabs.developers.google.com/codelabs/tensorflowjs-teachablemachine-codelab/index.html#6
41 | const activation = mnet.infer(snippet, true);
42 | classifier.addExample(activation, tag);
43 | });
44 |
45 | if (onLoad)
46 | onLoad();
47 | }
48 |
49 | export default AnnotoriousSmartTagging;
--------------------------------------------------------------------------------
/plugins/annotorious-tilted-box/.gitignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/plugins/annotorious-tilted-box/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2020, Pelagios Network
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/plugins/annotorious-tilted-box/README.md:
--------------------------------------------------------------------------------
1 | # Annotorious Tilted Box Plugin
2 |
3 | This plugin for [Annotorious](https://github.com/recogito/annotorious) adds a new
4 | selection tool: the __Tilted Box__.
5 |
6 | 
7 |
8 | The Tilted Box allows you to quickly draw a rectangle in an arbitrary rotation.
9 | This can be especially useful for selecting text that appears in photographs or
10 | digitized map images.
11 |
12 | __!!! BETA !!!__ the current version support __creating__ annotations only. You
13 | can not (yet) edit existing shapes.
14 |
15 | ## Install
16 |
17 | Include the plugin directly via the CDN:
18 |
19 | ```html
20 |
21 |
22 |
23 |
24 |
25 |
26 | ```
27 |
28 | Or if you are using npm:
29 |
30 | ```
31 | $ npm install @recogito/annotorious-tilted-box
32 | ```
33 |
34 | Instantiate Annotorious the normal way, and then register the plugin:
35 |
36 | ```js
37 | var anno = Annotorious.init({
38 | image: 'my-image'
39 | });
40 |
41 | // Init the plugin
42 | Annotorious.TiltedBox(anno);
43 |
44 | // Annotorious now has an additional drawing
45 | // tool - set it as the active tool
46 | anno.setDrawingTool('annotorious-tilted-box');
47 | ```
48 |
49 | Questions? Feedack? Feature requests? Join the [Annotorious chat on Gitter](https://gitter.im/recogito/annotorious).
50 |
51 | [](https://gitter.im/recogito/annotorious?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
52 |
53 | ## License
54 |
55 | [BSD-3 Clause](https://github.com/recogito/recogito-client-plugins/blob/main/packages/annotorious-tilted-box/LICENSE) (= feel
56 | free to use this code in whatever way you wish. But keep the attribution/license file,
57 | and if this code breaks something, don't complain to us :-)
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/plugins/annotorious-tilted-box/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@recogito/annotorious-tilted-box",
3 | "version": "0.3.2",
4 | "description": "The Tilted Box selection tool from Recogito - now for Annotorious",
5 | "main": "dist/annotorious-tilted-box.js",
6 | "scripts": {
7 | "start": "webpack serve --open --mode=development",
8 | "build": "webpack --mode=production"
9 | },
10 | "author": "Rainer Simon",
11 | "license": "BSD-3-Clause",
12 | "devDependencies": {
13 | "@babel/plugin-proposal-class-properties": "^7.14.5",
14 | "@babel/preset-env": "^7.14.7",
15 | "babel-loader": "^8.2.2",
16 | "css-loader": "^5.2.6",
17 | "html-webpack-plugin": "^5.4.0",
18 | "sass": "^1.35.2",
19 | "sass-loader": "^10.2.0",
20 | "style-loader": "^2.0.0",
21 | "webpack": "^5.59.1",
22 | "webpack-cli": "^4.9.1",
23 | "webpack-dev-server": "^4.3.1"
24 | },
25 | "dependencies": {
26 | "@recogito/annotorious": "^2.6.0"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/plugins/annotorious-tilted-box/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Annotorious | Tilted Box
5 |
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
42 |
43 |
--------------------------------------------------------------------------------
/plugins/annotorious-tilted-box/screencap.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/plugins/annotorious-tilted-box/screencap.gif
--------------------------------------------------------------------------------
/plugins/annotorious-tilted-box/src/Geom2D.js:
--------------------------------------------------------------------------------
1 | /** Computes the length of a vector **/
2 | export const len = (xy1, xy2) =>
3 | xy2 ?
4 | // Treat input as two vectors and compute length of result vector
5 | Math.sqrt(Math.pow(xy2[0] - xy1[0], 2) + Math.pow(xy2[1] - xy1[1], 2)) :
6 |
7 | // Treat input as a vector
8 | Math.sqrt(Math.pow(xy1[0], 2) + Math.pow(xy1[1], 2));
9 |
10 | /** Returns the vector between two points **/
11 | export const vec = (terminal, starting) =>
12 | [ terminal[0] - starting[0], terminal[1] - starting [1] ];
13 |
14 | /** Rounds the vector coordinates to the given decimal placs **/
15 | export const round = (vec, decimalPlaces) => {
16 | const factor = Math.pow(10, decimalPlaces || 0);
17 | return [
18 | Math.round(vec[0] * factor) / factor,
19 | Math.round(vec[1] * factor) / factor
20 | ];
21 | }
22 | /** Returns true if the vectors are equal **/
23 | export const eq = (xy1, xy2) =>
24 | xy1[0] === xy2[0] && xy1[1] === xy2[1];
25 |
26 | /** Normalizes a vector to length = 1 **/
27 | export const normalize = xy => {
28 | var l = Math.max(len(xy), 0.00001); // Prevent div by zero
29 | return [ xy[0] / l, xy[1] / l ];
30 | }
31 |
32 | /** Computes the angle between two vectors **/
33 | export const angleBetween = (a, b) => {
34 | const dotProduct = a[0] * b[0] + a[1] * b[1];
35 | return Math.acos(dotProduct);
36 | }
37 |
38 | export const angleOf = xy =>
39 | Math.atan2(xy[1], xy[0]);
40 |
41 | /** Adds two vectors */
42 | export const add = (a, b) =>
43 | [ a[0] + b[0], a[1] + b[1] ];
44 |
45 | export const mult = (vec, factor) =>
46 | [ factor * vec[0], factor * vec[1] ];
47 |
--------------------------------------------------------------------------------
/plugins/annotorious-tilted-box/src/RubberbandTiltedBox.js:
--------------------------------------------------------------------------------
1 | import { Selection, ToolLike } from '@recogito/annotorious/src/tools/Tool';
2 | import { SVG_NAMESPACE } from '@recogito/annotorious/src/util/SVG';
3 |
4 | import * as Geom2D from './Geom2D';
5 | import {
6 | createBaseline,
7 | createBox,
8 | setBaseline,
9 | setBoxPoints
10 | } from './TiltedBox';
11 |
12 | const polygonBounds = points => {
13 | return { w: 100, h: 100 };
14 | }
15 |
16 | export default class TiltedBox extends ToolLike {
17 |
18 | constructor(points, g, config, env) {
19 | super(g, config, env);
20 |
21 | this.points = points;
22 |
23 | const [ a, b, ..._ ] = points;
24 |
25 | this.group = document.createElementNS(SVG_NAMESPACE, 'g');
26 |
27 | this.element = document.createElementNS(SVG_NAMESPACE, 'g');
28 | this.element.setAttribute('class', 'a9s-selection tilted-box');
29 |
30 | this.baseline = createBaseline();
31 | this.tiltedbox = createBox();
32 |
33 | this.pivot = this.drawHandle(0, 0);
34 |
35 | this.setPoints(points);
36 |
37 | // We make the selection transparent to
38 | // pointer events because it would interfere with the
39 | // rendered annotations' mouseleave/enter events
40 | this.element.style.pointerEvents = 'none';
41 |
42 | this.element.appendChild(this.tiltedbox);
43 | this.element.appendChild(this.baseline);
44 | this.element.appendChild(this.pivot);
45 |
46 | this.group.appendChild(this.element);
47 |
48 | g.appendChild(this.group);
49 | }
50 |
51 | scalePivot = scale => {
52 | const inner = this.pivot.querySelector('.a9s-handle-inner');
53 | const outer = this.pivot.querySelector('.a9s-handle-outer');
54 |
55 | const radius = scale * (this.config.handleRadius || 6);
56 |
57 | inner.setAttribute('r', radius);
58 | outer.setAttribute('r', radius);
59 | }
60 |
61 | get isCollapsed() {
62 | const { w, h } = polygonBounds(this.points);
63 | return w * h < 9;
64 | }
65 |
66 | setPoints = points => {
67 | this.points = points;
68 |
69 | const [ a, b, ..._ ] = points;
70 |
71 | setBaseline(this.baseline, a, b);
72 | setBoxPoints(this.tiltedbox, points);
73 |
74 | this.setHandleXY(this.pivot, a[0], a[1]);
75 | }
76 |
77 | setBaseEnd = (x, y) => {
78 | const a = this.points[0];
79 | const b = [ x, y ];
80 | this.setPoints([ a, b, b, a ]);
81 | }
82 |
83 | extrude = (pointerX, pointerY) => {
84 | const [ a, b, ..._] = this.points;
85 |
86 | // Baseline normal (len = 1)
87 | const baseline = Geom2D.vec(b, a);
88 | const normal = Geom2D.normalize([ - baseline[1], baseline[0] ]);
89 |
90 | // Vector baseline end -> mouse
91 | const toMouse = Geom2D.vec([ pointerX, pointerY ], b);
92 |
93 | // Projection of toMouse onto normal
94 | const f = [
95 | normal[0] * Geom2D.len(toMouse) * Math.cos(Geom2D.angleBetween(normal, Geom2D.normalize(toMouse))),
96 | normal[1] * Geom2D.len(toMouse) * Math.cos(Geom2D.angleBetween(normal, Geom2D.normalize(toMouse)))
97 | ];
98 |
99 | const c = Geom2D.add(b, f);
100 | const d = Geom2D.add(a, f);
101 |
102 | this.setPoints([ a, b, c, d ]);
103 | }
104 |
105 | toSelection = source => {
106 | const points = this.tiltedbox.querySelector('.a9s-inner').getAttribute('points');
107 |
108 | return new Selection({
109 | source,
110 | selector: {
111 | type: 'SvgSelector',
112 | value: ` `,
113 | },
114 | renderedVia: {
115 | name: 'annotorious-tilted-box'
116 | }
117 | });
118 | }
119 |
120 | destroy = () => {
121 | this.group.parentNode.removeChild(this.group);
122 |
123 | this.element = null;
124 | this.group = null;
125 | }
126 |
127 | }
--------------------------------------------------------------------------------
/plugins/annotorious-tilted-box/src/TiltedBox.js:
--------------------------------------------------------------------------------
1 | import { SVG_NAMESPACE } from '@recogito/annotorious/src/util/SVG';
2 |
3 | /** Common to all SVG shapes: draw group with inner + outer **/
4 | const createElement = elem => {
5 | const g = document.createElementNS(SVG_NAMESPACE, 'g');
6 |
7 | const outer = document.createElementNS(SVG_NAMESPACE, elem);
8 | outer.setAttribute('class', 'a9s-outer');
9 |
10 | const inner = document.createElementNS(SVG_NAMESPACE, elem);
11 | inner.setAttribute('class', 'a9s-inner');
12 |
13 | g.appendChild(outer);
14 | g.appendChild(inner);
15 |
16 | return g;
17 | }
18 |
19 | export const createBaseline = (optA, optB) => {
20 | const g = createElement('line');
21 | g.setAttribute('class', 'a9s-tilted-box-baseline');
22 |
23 | if (optA && optB)
24 | setBaseline(g, optA, optB);
25 |
26 | return g;
27 | }
28 |
29 | export const createBox = optPoints => {
30 | const g = createElement('polygon');
31 |
32 | if (optPoints)
33 | setBoxPoints(g, optPoints);
34 |
35 | return g;
36 | }
37 |
38 | export const setBaseline = (g, from, to) => {
39 |
40 | const setCoords = line => {
41 | line.setAttribute('x1', from[0]);
42 | line.setAttribute('y1', from[1]);
43 | line.setAttribute('x2', to[0]);
44 | line.setAttribute('y2', to[1]);
45 | }
46 |
47 | const inner = g.querySelector('.a9s-inner');
48 | setCoords(inner);
49 |
50 | const outer = g.querySelector('.a9s-outer');
51 | setCoords(outer);
52 | }
53 |
54 | export const setBoxPoints = (g, points) => {
55 | const attr = points.map(xy => xy.join(',')).join(' ');
56 |
57 | const inner = g.querySelector('.a9s-inner');
58 | inner.setAttribute('points', attr);
59 |
60 | const outer = g.querySelector('.a9s-outer');
61 | outer.setAttribute('points', attr);
62 | }
63 |
64 | export const getBoxPoints = g =>
65 | g.querySelector('.a9s-inner').getAttribute('points')
66 | .split(' ')
67 | .map(t => t.split(',').map(num => parseFloat(num)));
68 |
69 | export const createMinorHandle = (xy, handleRadius) => {
70 | // Make this 80% smaller than the configured handle radius
71 | const radius = Math.round((handleRadius || 6) * 0.8);
72 |
73 | const c = document.createElementNS(SVG_NAMESPACE, 'circle');
74 | c.setAttribute('class', 'a9s-minor-handle');
75 | c.setAttribute('cx', xy[0]);
76 | c.setAttribute('cy', xy[1]);
77 | c.setAttribute('r', radius);
78 | return c;
79 | }
--------------------------------------------------------------------------------
/plugins/annotorious-tilted-box/src/TiltedBoxTool.js:
--------------------------------------------------------------------------------
1 | import Tool from '@recogito/annotorious/src/tools/Tool';
2 | import EditableTiltedBox from './EditableTiltedBox';
3 | import RubberbandTiltedBox from './RubberbandTiltedBox';
4 |
5 | import './TiltedBoxTool.scss';
6 |
7 | export default class TiltedBoxTool extends Tool {
8 |
9 | constructor(g, config, env) {
10 | super(g, config, env);
11 |
12 | this.drawingState = null;
13 |
14 | this.rubberbandShape = null;
15 | }
16 |
17 | onScaleChanged = scale =>
18 | this.rubberbandShape?.scalePivot(scale);
19 |
20 | startDrawing = (x, y, _) => {
21 | this.attachListeners({
22 | mouseMove: this.onMouseMove,
23 | mouseUp: this.onMouseUp
24 | });
25 |
26 | this.drawingState = 'BASELINE';
27 |
28 | this.rubberbandShape = new RubberbandTiltedBox([
29 | [ x, y ],
30 | [ x, y ],
31 | [ x, y ],
32 | [ x, y ]
33 | ], this.g, this.config, this.env);
34 | }
35 |
36 | onMouseMove = (x, y) => {
37 | if (this.drawingState === 'BASELINE')
38 | this.rubberbandShape.setBaseEnd(x, y);
39 | else if (this.drawingState === 'EXTRUDE')
40 | this.rubberbandShape.extrude(x, y);
41 | }
42 |
43 | onMouseUp = () => {
44 | if (this.drawingState === 'BASELINE') {
45 | if (this.rubberbandShape.isCollapsed) {
46 | this.emit('cancel');
47 | this.stop();
48 | } else {
49 | this.drawingState = 'EXTRUDE'
50 | }
51 | } else if (this.drawingState === 'EXTRUDE') {
52 | const shape = this.rubberbandShape.element;
53 | shape.annotation = this.rubberbandShape.toSelection(this.env.image.src);
54 | this.emit('complete', shape);
55 | this.stop();
56 | }
57 | }
58 |
59 | stop = () => {
60 | this.detachListeners();
61 | this.drawingState = null;
62 |
63 | if (this.rubberbandShape) {
64 | this.rubberbandShape.destroy();
65 | this.rubberbandShape = null;
66 | }
67 | }
68 |
69 | get isDrawing() {
70 | return this.drawingState != null;
71 | }
72 |
73 | createEditableShape = annotation =>
74 | new EditableTiltedBox(annotation, this.g, this.config, this.env);
75 |
76 | }
77 |
78 | TiltedBoxTool.identifier = 'annotorious-tilted-box';
79 |
80 | TiltedBoxTool.supports = annotation => false;
81 |
--------------------------------------------------------------------------------
/plugins/annotorious-tilted-box/src/TiltedBoxTool.scss:
--------------------------------------------------------------------------------
1 | g.a9s-annotation.tilted-box,
2 | g.a9s-selection.tilted-box {
3 |
4 | .a9s-tilted-box-baseline {
5 |
6 | * {
7 | vector-effect:non-scaling-stroke;
8 | }
9 |
10 | .a9s-inner {
11 | stroke-width:3px;
12 | stroke:#fff;
13 | }
14 |
15 | .a9s-outer {
16 | stroke-width:5px;
17 | }
18 |
19 | }
20 |
21 | .a9s-minor-handle {
22 | stroke:#fff000;
23 | fill:#000;
24 | }
25 |
26 | .a9s-minor-handle:hover {
27 | stroke:#000;
28 | fill:#fff000;
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/plugins/annotorious-tilted-box/src/index.js:
--------------------------------------------------------------------------------
1 | import TiltedBoxTool from './TiltedBoxTool';
2 |
3 | const TiltedBoxPlugin = anno => {
4 |
5 | anno.addDrawingTool(TiltedBoxTool);
6 |
7 | }
8 |
9 | export default TiltedBoxPlugin;
--------------------------------------------------------------------------------
/plugins/annotorious-tilted-box/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | const APP_DIR = fs.realpathSync(process.cwd());
5 |
6 | const resolveAppPath = relativePath => path.resolve(APP_DIR, relativePath);
7 |
8 | const HtmlWebpackPlugin = require('html-webpack-plugin');
9 |
10 | module.exports = {
11 | entry: resolveAppPath('src'),
12 | output: {
13 | filename: 'annotorious-tilted-box.js',
14 | library: ['Annotorious', 'TiltedBox'],
15 | libraryTarget: 'umd',
16 | libraryExport: 'default',
17 | pathinfo: true
18 | },
19 | performance: {
20 | hints: false
21 | },
22 | devtool: 'source-map',
23 | optimization: {
24 | minimize: true
25 | },
26 | resolve: {
27 | extensions: ['.js' ]
28 | },
29 | module: {
30 | rules: [
31 | {
32 | test: /\.js$/,
33 | use: {
34 | loader: 'babel-loader' ,
35 | options: {
36 | "presets": [
37 | "@babel/preset-env"
38 | ],
39 | "plugins": [
40 | [
41 | "@babel/plugin-proposal-class-properties"
42 | ]
43 | ]
44 | }
45 | }
46 | },
47 | { test: /\.scss$/, use: [ 'style-loader', 'css-loader', 'sass-loader' ] }
48 | ]
49 | },
50 | devServer: {
51 | compress: true,
52 | hot: true,
53 | host: process.env.HOST || 'localhost',
54 | port: 3000,
55 | static: [{
56 | directory: resolveAppPath('public'),
57 | publicPath: '/'
58 | }, {
59 | directory: resolveAppPath('../../assets'),
60 | publicPath: '/'
61 | }]
62 | },
63 | plugins: [
64 | new HtmlWebpackPlugin ({
65 | template: resolveAppPath('public/index.html')
66 | })
67 | ]
68 | }
--------------------------------------------------------------------------------
/plugins/annotorious-toolbar/.gitignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/plugins/annotorious-toolbar/README.md:
--------------------------------------------------------------------------------
1 | # Annotorious Toolbar
2 |
3 | A simple toolbar to switch between drawing tools.
4 |
5 | 
6 |
7 | ## Install
8 |
9 | Include the plugin directly via the CDN:
10 |
11 | ```html
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | ```
30 |
31 | Or if you are using npm:
32 |
33 | ```
34 | $ npm install @recogito/annotorious-toolbar
35 | ```
36 |
37 | Instantiate Annotorious the normal way, and then register the plugin:
38 |
39 | ```js
40 | var anno = Annotorious.init({
41 | image: 'my-image'
42 | });
43 |
44 | // Init the plugin
45 | Annotorious.Toolbar(anno, document.getElementById('my-toolbar-container'));
46 | ```
47 |
48 | ## Optional Settings
49 |
50 | All settings are added as a third element that are key, value pairs.
51 |
52 | ```
53 | Annotorious.Toolbar(anno, document.getElementById('my-toolbar-container'), {'key': 'value', 'key2': 'value2'});
54 | ```
55 | 1. `withMouse`: adds Mouse icon to the toolbar for dragging around the image. Also changes the toolbar settings so that an annotation creation with a shape is always enabled until switched to the mouse icon.
56 |
57 | ```
58 | Annotorious.Toolbar(anno, document.getElementById('my-toolbar-container'), {'withMouse': true});
59 | ```
60 |
61 | 2. `drawingTools`: a list of tools you want in the toolbar. The options are: [ 'annotorious-tilted-box', 'rect', 'polygon', 'circle', 'ellipse', 'freehand', 'point', 'line']. This allows you to order your toolbar in the order/items you want to show up. You have to have the specific tool installed in order for it to show up in your toolbar.
62 |
63 | ```
64 | Annotorious.Toolbar(anno, document.getElementById('my-toolbar-container'), {'drawingTools': ['polygon', 'rect', 'circle']});
65 | ```
66 |
67 | 3. `infoElement`: a HTML element where hints about the drawing tools show up. Currently the only hint is the hint on how to stop the polygon tool.
68 |
69 | ```
70 | Annotorious.Toolbar(anno, document.getElementById('my-toolbar-container'), {'infoElement': document.getElementById('info-element')});
71 | ```
72 |
73 | 4. `withLabel`: A setting that enables a text label for the drawing tool to show up next to the shape. For example, when enabled, there will the text 'Rectangle' next to the 'Rectangle' icon.
74 |
75 | ```
76 | Annotorious.Toolbar(anno, document.getElementById('my-toolbar-container'), {'withLabel': true});
77 | ```
78 |
79 | 5. `withTooltip`: A setting that enables a tooltip(hover effect) over each button specifying the name of the button.
80 |
81 | ```
82 | Annotorious.Toolbar(anno, document.getElementById('my-toolbar-container'), {'withTooltip': true});
83 | ```
84 |
85 | Questions? Feedack? Feature requests? Join the [Annotorious chat on Gitter](https://gitter.im/recogito/annotorious).
86 |
87 | [](https://gitter.im/recogito/annotorious?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
88 |
89 | ## License
90 |
91 | [BSD-3 Clause](https://github.com/recogito/recogito-client-plugins/blob/main/packages/annotorious-tilted-box/LICENSE) (= feel
92 | free to use this code in whatever way you wish. But keep the attribution/license file,
93 | and if this code breaks something, don't complain to us :-)
94 |
--------------------------------------------------------------------------------
/plugins/annotorious-toolbar/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@recogito/annotorious-toolbar",
3 | "version": "1.2.1",
4 | "description": "A simple toolbar for switching Annotorious drawing tools",
5 | "main": "dist/annotorious-toolbar.min.js",
6 | "scripts": {
7 | "start": "webpack serve --open --mode=development",
8 | "build": "webpack --mode=production"
9 | },
10 | "author": "Rainer Simon",
11 | "license": "BSD-3-Clause",
12 | "devDependencies": {
13 | "@babel/core": "^7.6.2",
14 | "@babel/plugin-proposal-class-properties": "^7.5.5",
15 | "@babel/preset-env": "^7.6.2",
16 | "babel-loader": "^8.0.6",
17 | "css-loader": "^3.2.0",
18 | "html-webpack-plugin": "^5.5.0",
19 | "style-loader": "^2.0.0",
20 | "webpack": "^5.60.0",
21 | "webpack-cli": "^4.9.1",
22 | "webpack-dev-server": "^4.3.1"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/plugins/annotorious-toolbar/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Annotorious | Toolbar
5 |
6 |
7 |
8 |
9 |
17 |
18 |
19 |
20 |
21 |
41 |
42 |
--------------------------------------------------------------------------------
/plugins/annotorious-toolbar/screencap.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/annotorious/annotorious-v2-plugins/6dc05bf19fa34ad4c092054f8ce2a6b1ee7eefaa/plugins/annotorious-toolbar/screencap.gif
--------------------------------------------------------------------------------
/plugins/annotorious-toolbar/src/icons/Circle.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
3 |
4 | svg.setAttribute('viewBox', '0 0 70 50');
5 |
6 | svg.innerHTML = `
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | `;
17 |
18 | return svg;
19 | }
--------------------------------------------------------------------------------
/plugins/annotorious-toolbar/src/icons/Ellipse.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
3 |
4 | svg.setAttribute('viewBox', '0 0 70 50');
5 |
6 | svg.innerHTML = `
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | `;
17 |
18 | return svg;
19 | }
--------------------------------------------------------------------------------
/plugins/annotorious-toolbar/src/icons/Freehand.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
3 |
4 | svg.setAttribute('viewBox', '0 0 70 50');
5 |
6 | svg.innerHTML = `
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | `;
15 |
16 | return svg;
17 | }
--------------------------------------------------------------------------------
/plugins/annotorious-toolbar/src/icons/Line.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
3 |
4 | svg.setAttribute('viewBox', '0 0 70 50');
5 |
6 | svg.innerHTML = `
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | `;
15 |
16 | return svg;
17 | }
--------------------------------------------------------------------------------
/plugins/annotorious-toolbar/src/icons/Mouse.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
3 |
4 | svg.setAttribute('viewBox', '0 0 320 540');
5 | svg.setAttribute('style', 'width: 29px; height: 20px;');
6 |
7 | svg.innerHTML =
8 | `
9 |
10 | `;
11 |
12 | return svg;
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/plugins/annotorious-toolbar/src/icons/Point.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
3 |
4 | svg.setAttribute('viewBox', '0 0 70 50');
5 |
6 | svg.innerHTML = `
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | `;
16 |
17 | return svg;
18 | }
--------------------------------------------------------------------------------
/plugins/annotorious-toolbar/src/icons/Polygon.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
3 |
4 | svg.setAttribute('viewBox', '0 0 70 50');
5 |
6 | svg.innerHTML = `
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | `;
17 |
18 | return svg;
19 | }
--------------------------------------------------------------------------------
/plugins/annotorious-toolbar/src/icons/Rectangle.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
3 |
4 | svg.setAttribute('viewBox', '0 0 70 50');
5 |
6 | svg.innerHTML = `
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | `;
17 |
18 | return svg;
19 | }
--------------------------------------------------------------------------------
/plugins/annotorious-toolbar/src/icons/TiltedBox.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
3 |
4 | svg.setAttribute('viewBox', '0 0 70 50');
5 |
6 | svg.innerHTML = `
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | `;
17 |
18 | return svg;
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/plugins/annotorious-toolbar/src/index.css:
--------------------------------------------------------------------------------
1 | .a9s-toolbar-btn {
2 | margin:4px 4px 4px 0;
3 | outline:0;
4 | border:none;
5 | cursor:pointer;
6 | background-color:transparent;
7 | border-radius:4px;
8 | padding:8px;
9 | width:45px;
10 | height:45px;
11 | }
12 |
13 | .a9s-toolbar-btn:hover {
14 | background-color:rgba(0,0,0,0.05);
15 | }
16 |
17 | .a9s-toolbar-btn-inner {
18 | display:flex;
19 | align-items: center;
20 | }
21 |
22 | .a9s-toolbar-btn svg {
23 | overflow:visible;
24 | width:100%;
25 | height:100%;
26 | }
27 |
28 | .a9s-toolbar-btn svg * {
29 | stroke-width:5px;
30 | fill:none;
31 | stroke:rgba(0,0,0,0.5);
32 | }
33 |
34 | .a9s-toolbar-btn g.handles circle {
35 | stroke-width:4px;
36 | fill:#fff;
37 | stroke:#000;
38 | }
39 |
40 | .a9s-toolbar-btn.mouse path{
41 | fill:#000;
42 | }
43 |
44 | .a9s-toolbar-btn.active {
45 | background-color:rgba(0,0,0,0.3);
46 | }
47 |
48 | .a9s-toolbar-btn.active svg * {
49 | stroke:rgba(255,255,255,0.6);
50 | }
51 |
52 | .a9s-toolbar-btn.active.mouse path{
53 | fill: rgba(255,255,255,0.6);
54 | }
55 |
56 | .a9s-toolbar-btn.active g.handles circle {
57 | stroke:#fff;
58 | fill:rgba(0,0,0,0.2);
59 | }
60 |
--------------------------------------------------------------------------------
/plugins/annotorious-toolbar/src/index.js:
--------------------------------------------------------------------------------
1 | import createRectangle from './icons/Rectangle';
2 | import createPolygon from './icons/Polygon';
3 | import createCircle from './icons/Circle';
4 | import createEllipse from './icons/Ellipse';
5 | import createFreehand from './icons/Freehand';
6 | import createPoint from './icons/Point';
7 | import createTiltedBox from './icons/TiltedBox';
8 | import createLine from './icons/Line';
9 | import createMouse from './icons/Mouse';
10 |
11 | import './index.css';
12 |
13 | const ICONS = {
14 | 'mouse': createMouse(),
15 | 'rect': createRectangle(),
16 | 'polygon': createPolygon(),
17 | 'circle': createCircle(),
18 | 'ellipse': createEllipse(),
19 | 'freehand': createFreehand(),
20 | 'point': createPoint(),
21 | 'annotorious-tilted-box': createTiltedBox(),
22 | 'line': createLine()
23 | }
24 |
25 | const ICONLABEL = {
26 | 'mouse': '',
27 | 'rect': 'Rectangle',
28 | 'polygon': 'Polygon',
29 | 'circle': 'Circle',
30 | 'ellipse': 'Ellipse',
31 | 'freehand': 'Freehand',
32 | 'point': 'Point',
33 | 'annotorious-tilted-box': 'Angled Box',
34 | 'line': 'Line'
35 | }
36 |
37 | // IE11 doesn't support adding/removing classes to SVG elements except
38 | // via .setAttribute
39 | const addClass = (el, className) => {
40 | const classNames = new Set(el.getAttribute('class').split(' '));
41 | classNames.add(className);
42 | el.setAttribute('class', Array.from(classNames).join(' '));
43 | }
44 |
45 | const removeClass = (el, className) => {
46 | const classNames = el.getAttribute('class').split(' ').filter(c => c !== className);
47 | el.setAttribute('class', classNames.join(' '));
48 | }
49 |
50 | const Toolbar = (anno, container, settings={}) => {
51 | // Bit of a hack...
52 | const isOSDPlugin = !!anno.fitBounds;
53 |
54 | const toolbar = document.createElement('div');
55 | toolbar.className = 'a9s-toolbar';
56 |
57 | const clearActive = () => {
58 | const currentActive = toolbar.querySelector('.a9s-toolbar-btn.active');
59 | if (currentActive)
60 | removeClass(currentActive, 'active');
61 | }
62 |
63 | const setActive = button => {
64 | clearActive();
65 | addClass(button, 'active');
66 | }
67 |
68 | const enableDrawing = (toolId, anno) => {
69 | toolId = toolId ? toolId : toolbar.querySelector('.a9s-toolbar-btn.active').classList[1];
70 | if (isOSDPlugin && toolId != 'mouse'){
71 | anno.setDrawingEnabled(true);
72 | } else if (isOSDPlugin && toolId == 'mouse') {
73 | anno.setDrawingEnabled(false);
74 | }
75 | }
76 | // Helper to create one tool button
77 | const createButton = (toolId, isActive) => {
78 | const icon = ICONS[toolId];
79 |
80 | if (icon) {
81 | const button = document.createElement('button');
82 | button.type = "button";
83 |
84 | if (isActive)
85 | button.className = `a9s-toolbar-btn ${toolId} active`;
86 | else
87 | button.className = `a9s-toolbar-btn ${toolId}`;
88 |
89 | const ariaLabel = ICONLABEL[toolId] ? `Create a ${ICONLABEL[toolId]} annotation` : toolId == 'mouse' ? 'Disable annotation creation, move around the image' : `Create a ${toolId} annotation`;
90 | button.setAttribute('aria-label', ariaLabel);
91 |
92 | const inner = document.createElement('span');
93 | inner.className = 'a9s-toolbar-btn-inner';
94 | inner.appendChild(icon);
95 |
96 | if (settings['withLabel'] && ICONLABEL[toolId]) {
97 | inner.innerHTML += `${ICONLABEL[toolId]} `;
98 | }
99 |
100 | button.addEventListener('click', () => {
101 | setActive(button);
102 | if (toolId != 'mouse'){
103 | anno.setDrawingTool(toolId);
104 | }
105 |
106 | if (settings['infoElement']) {
107 | if (toolId == 'polygon'){
108 | settings['infoElement'].innerHTML = 'To stop Polygon annotation selection double click.';
109 | } else {
110 | settings['infoElement'].innerHTML = '';
111 | }
112 | }
113 | enableDrawing(toolId, anno)
114 | });
115 |
116 |
117 |
118 | button.appendChild(inner);
119 | toolbar.appendChild(button);
120 | if (settings['withMouse']){
121 | anno.on('cancelSelected', function() {
122 | enableDrawing('', anno);
123 | });
124 | anno.on('createAnnotation', function(annotation) {
125 | enableDrawing('', anno);
126 | });
127 |
128 | anno.on('updateAnnotation', function(annotation) {
129 | enableDrawing('', anno);
130 | });
131 |
132 | anno.on('deleteAnnotation', function(annotation) {
133 | enableDrawing('', anno);
134 | });
135 | }
136 | // Tooltip Feature
137 | if (settings['withTooltip'] && ICONLABEL[toolId]){
138 | button.title=ICONLABEL[toolId];
139 | }
140 | }
141 | }
142 | if (settings['withMouse']){
143 | createButton('mouse', true);
144 | }
145 | const drawingTools = settings['drawingTools'] ? settings['drawingTools'].filter(elem => anno.listDrawingTools().indexOf(elem) != -1) : anno.listDrawingTools();
146 | drawingTools.forEach((toolId, idx) => {
147 | // In standard version, activate first button
148 | const activateFirst = !isOSDPlugin && idx === 0;
149 | createButton(toolId, activateFirst);
150 | });
151 |
152 |
153 | if (isOSDPlugin && !settings['withMouse']){
154 | anno.on('createSelection', clearActive);
155 | }
156 | container.appendChild(toolbar);
157 | }
158 |
159 | export default Toolbar;
160 |
--------------------------------------------------------------------------------
/plugins/annotorious-toolbar/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 |
6 | const APP_DIR = fs.realpathSync(process.cwd());
7 |
8 | const resolveAppPath = relativePath => path.resolve(APP_DIR, relativePath);
9 |
10 | module.exports = {
11 | entry: resolveAppPath('src'),
12 | output: {
13 | filename: 'annotorious-toolbar.min.js',
14 | library: ['Annotorious', 'Toolbar'],
15 | libraryTarget: 'umd',
16 | libraryExport: 'default'
17 | },
18 | performance: {
19 | hints: false
20 | },
21 | optimization: {
22 | minimize: true
23 | },
24 | resolve: {
25 | extensions: ['.js']
26 | },
27 | module: {
28 | rules: [
29 | {
30 | test: /\.js$/,
31 | use: {
32 | loader: 'babel-loader' ,
33 | options: {
34 | "presets": [
35 | "@babel/preset-env",
36 | ],
37 | "plugins": [
38 | [
39 | "@babel/plugin-proposal-class-properties"
40 | ]
41 | ]
42 | }
43 | }
44 | },
45 | { test: /\.css$/, use: [ 'style-loader', 'css-loader'] },
46 | ]
47 | },
48 | devServer: {
49 | compress: true,
50 | hot: true,
51 | host: process.env.HOST || 'localhost',
52 | port: 3000,
53 | static: [{
54 | directory: resolveAppPath('public'),
55 | publicPath: '/'
56 | },{
57 | directory: resolveAppPath('../../assets'),
58 | publicPath: '/'
59 | }]
60 | },
61 | plugins: [
62 | new HtmlWebpackPlugin ({
63 | template: resolveAppPath('public/index.html')
64 | })
65 | ]
66 | }
--------------------------------------------------------------------------------
/plugins/storage-firebase/.gitignore:
--------------------------------------------------------------------------------
1 | dist/*.js
--------------------------------------------------------------------------------
/plugins/storage-firebase/README.md:
--------------------------------------------------------------------------------
1 | # Firebase Storage
2 |
3 | A storage plugin for RecogitoJS and Annotorious/AnnotoriousOSD that uses
4 | Google Firebase as annotation store.
5 |
6 | ## Installation
7 |
8 | Install via npm
9 |
10 | ```sh
11 | npm install @recogito/firebase-storage
12 | ```
13 |
14 | or include in the page
15 |
16 | ```html
17 |
18 | ```
19 |
20 | ## Example
21 |
22 | ```html
23 |
24 |
25 |
26 | RecogitoJS/Annotorious Firebase Storage Adapter
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
61 |
62 |
63 | ```
64 |
65 |
--------------------------------------------------------------------------------
/plugins/storage-firebase/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ["@babel/preset-env"]
3 | };
--------------------------------------------------------------------------------
/plugins/storage-firebase/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | RecogitoJS/Annotorious Firebase Storage Adapter
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
38 |
39 |
--------------------------------------------------------------------------------
/plugins/storage-firebase/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@recogito/firebase-storage",
3 | "version": "0.2.1",
4 | "description": "Google Firebase storage adapter for RecogitoJS, Annotorious and AnnotoriousOSD",
5 | "main": "dist/recogito-firebase-storage.js",
6 | "scripts": {
7 | "build": "webpack --mode=production"
8 | },
9 | "author": "Rainer Simon",
10 | "license": "BSD-3-Clause",
11 | "homepage": "https://github.com/recogito/recogito-plugins-common/tree/main/packages/storage-firebase",
12 | "devDependencies": {
13 | "@babel/core": "^7.12.10",
14 | "@babel/plugin-proposal-class-properties": "^7.5.5",
15 | "@babel/preset-env": "^7.12.11",
16 | "babel-loader": "^8.2.3",
17 | "webpack": "^5.60.0",
18 | "webpack-cli": "^4.9.1"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/plugins/storage-firebase/src/index.js:
--------------------------------------------------------------------------------
1 | const FirebaseStorage = (client, firebaseConfig, settings) => {
2 |
3 | const collectionName = settings.collectionName || 'annotations';
4 | const { annotationTarget } = settings;
5 |
6 | firebase.initializeApp(firebaseConfig);
7 |
8 | const db = firebase.firestore();
9 |
10 | // Helper to find a Firebase doc by annotation ID
11 | const findById = id => {
12 | const query = db.collection(collectionName).where('id', '==', id);
13 | return query.get().then(querySnapshot => {
14 | return querySnapshot.docs[0]
15 | });
16 | }
17 |
18 | // Load annotations for this image
19 | db.collection(collectionName).where('target.source', '==', annotationTarget)
20 | .get().then(querySnapshot => {
21 | const annotations = querySnapshot.docs.map(function(doc) {
22 | return doc.data();
23 | });
24 |
25 | client.setAnnotations(annotations);
26 | });
27 |
28 | // Lifecycle event handlers
29 | client.on('createAnnotation', a => {
30 | db.collection(collectionName)
31 | .add(a).catch(error =>
32 | console.error('Error storing annotation', error, a))
33 | });
34 |
35 | client.on('updateAnnotation', (annotation, previous) => {
36 | findById(previous.id)
37 | .then(doc => doc.ref.update(annotation))
38 | .catch(error => console.log('Error updating annotation', error, previous, annotation))
39 | });
40 |
41 | client.on('deleteAnnotation', annotation => {
42 | findById(annotation.id)
43 | .then(doc => doc.ref.delete())
44 | .catch(error => console.log('Error deleting annotation', error, annotation));
45 | });
46 |
47 | }
48 |
49 | export default FirebaseStorage;
50 |
--------------------------------------------------------------------------------
/plugins/storage-firebase/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | const APP_DIR = fs.realpathSync(process.cwd());
5 |
6 | const resolveAppPath = relativePath => path.resolve(APP_DIR, relativePath);
7 |
8 | module.exports = {
9 | entry: resolveAppPath('src'),
10 | output: {
11 | filename: 'recogito-firebase-storage.js',
12 | library: ['recogito', 'FirebaseStorage'],
13 | libraryTarget: 'umd',
14 | libraryExport: 'default'
15 | },
16 | performance: {
17 | hints: false
18 | },
19 | optimization: {
20 | minimize: true
21 | },
22 | resolve: {
23 | extensions: ['.js']
24 | },
25 | module: {
26 | rules: [
27 | {
28 | test: /\.js$/,
29 | use: {
30 | loader: 'babel-loader' ,
31 | options: {
32 | "presets": [
33 | "@babel/preset-env",
34 | ],
35 | "plugins": [
36 | [
37 | "@babel/plugin-proposal-class-properties"
38 | ]
39 | ]
40 | }
41 | }
42 | },
43 | { test: /\.css$/, use: [ 'style-loader', 'css-loader'] },
44 | ]
45 | }
46 | }
--------------------------------------------------------------------------------
/plugins/storage-legacy-platform/.gitignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/plugins/storage-legacy-platform/README.md:
--------------------------------------------------------------------------------
1 | # Recogito Legacy Platform Storage
2 |
3 | A storage interop plugin that makes RecogitoJS and Annotorious/AnnotoriousOSD work in
4 | the [legacy Recogito platform](https://github.com/pelagios/recogito2).
--------------------------------------------------------------------------------
/plugins/storage-legacy-platform/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ["@babel/preset-env"]
3 | };
--------------------------------------------------------------------------------
/plugins/storage-legacy-platform/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@recogito/recogito-legacy-storage",
3 | "version": "0.7.5",
4 | "description": "A storage adapator that works with the legacy Recogito v.3 annotation platform",
5 | "main": "dist/recogito-legacy-storage.js",
6 | "scripts": {
7 | "start": "webpack serve --open --mode=development",
8 | "build": "webpack --mode=production"
9 | },
10 | "author": "Rainer Simon",
11 | "license": "BSD-3-Clause",
12 | "devDependencies": {
13 | "@babel/core": "^7.12.10",
14 | "@babel/plugin-proposal-class-properties": "^7.5.5",
15 | "@babel/preset-env": "^7.12.11",
16 | "babel-loader": "^8.0.6",
17 | "html-webpack-plugin": "^5.4.0",
18 | "webpack": "^5.59.1",
19 | "webpack-cli": "^4.9.1",
20 | "webpack-dev-server": "^4.3.1"
21 | },
22 | "dependencies": {
23 | "axios": "^0.21.1"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/plugins/storage-legacy-platform/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Annotorious | Legacy Storage
5 |
6 |
7 |
8 |
9 |
17 |
18 |
19 |
20 |
21 |
54 |
55 |
--------------------------------------------------------------------------------
/plugins/storage-legacy-platform/src/FragmentSelector.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Convert media fragment syntax (xywh=pixel:292,69,137,125) to
3 | * proprietary Recogito rect syntax (rect:x=292,y=69,w=137,h=125)
4 | */
5 | export const rectFragmentToLegacy = selector => {
6 | const [ _, coords ] = selector.value.includes(':') ?
7 | selector.value.split(':') : selector.value.split('=');
8 |
9 | const [ x, y, w, h] = coords.split(',').map(parseFloat);
10 |
11 | return w > 0 && h > 0 ?
12 | `rect:x=${x},y=${y},w=${w},h=${h}` :
13 | `point:${x},${y}`;
14 | }
15 |
16 | /**
17 | * Convert proprietary Recogito rect syntax to media fragment
18 | */
19 | export const legacyRectToSelector = anchor => {
20 | const [ _, tuples ] = anchor.split(':');
21 | const [ x, y, w, h ] = tuples.split(',').map(t => parseFloat(t.split('=')[1]))
22 |
23 | return {
24 | type: 'FragmentSelector',
25 | conformsTo: 'http://www.w3.org/TR/media-frags/',
26 | value: `xywh=pixel:${x},${y},${w},${h}`
27 | }
28 | }
29 |
30 | export const legacyPointToSelector = anchor => {
31 | const [ _, xy ] = anchor.split(':');
32 | const [ x, y ] = xy.split(',').map(t => parseFloat(t));
33 |
34 | return {
35 | type: 'FragmentSelector',
36 | conformsTo: 'http://www.w3.org/TR/media-frags/',
37 | value: `xywh=pixel:${x},${y},0,0`
38 | }}
--------------------------------------------------------------------------------
/plugins/storage-legacy-platform/src/SvgSelector.js:
--------------------------------------------------------------------------------
1 | export const svgPolygonToLegacy = selector =>
2 | `svg.polygon:${selector.value}`;
3 |
4 | export const tiltedBoxToLegacy = selector =>
5 | `svg.tbox:${selector.value}`;
6 |
7 | export const legacyPolygonToSelector = anchor => ({
8 | type: 'SvgSelector',
9 | value: anchor.split(':')[1]
10 | });
--------------------------------------------------------------------------------
/plugins/storage-legacy-platform/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 |
6 | const APP_DIR = fs.realpathSync(process.cwd());
7 |
8 | const resolveAppPath = relativePath => path.resolve(APP_DIR, relativePath);
9 |
10 | module.exports = {
11 | entry: resolveAppPath('src'),
12 | output: {
13 | filename: 'recogito-legacy-storage.js',
14 | library: ['recogito', 'LegacyStorage'],
15 | libraryTarget: 'umd',
16 | libraryExport: 'default'
17 | },
18 | performance: {
19 | hints: false
20 | },
21 | optimization: {
22 | minimize: true
23 | },
24 | resolve: {
25 | extensions: ['.js']
26 | },
27 | module: {
28 | rules: [
29 | {
30 | test: /\.js$/,
31 | use: {
32 | loader: 'babel-loader' ,
33 | options: {
34 | "presets": [
35 | "@babel/preset-env",
36 | ],
37 | "plugins": [
38 | [
39 | "@babel/plugin-proposal-class-properties"
40 | ]
41 | ]
42 | }
43 | }
44 | }
45 | ]
46 | },
47 | devServer: {
48 | compress: true,
49 | hot: true,
50 | host: process.env.HOST || 'localhost',
51 | port: 3000,
52 | static: [{
53 | directory: resolveAppPath('public'),
54 | publicPath: '/'
55 | },{
56 | directory: resolveAppPath('../../assets'),
57 | publicPath: '/'
58 | }],
59 | proxy: {
60 | '/api': {
61 | target: 'http://localhost:9000',
62 | secure: false,
63 | changeOrigin: true
64 | }
65 | }
66 | },
67 | plugins: [
68 | new HtmlWebpackPlugin ({
69 | template: resolveAppPath('public/index.html')
70 | })
71 | ]
72 | }
--------------------------------------------------------------------------------
/plugins/widget-react-helloworld/README.md:
--------------------------------------------------------------------------------
1 | # An Example React Editor Widget
2 |
3 | The example React widget from the [guide](https://recogito.github.io/guides/editor-widgets/).
4 |
5 | ## Build
6 |
7 | A built, minified version is in the [dist folder](https://github.com/recogito/recogito-client-plugins/tree/main/packages/widget-react-helloworld/dist).
8 |
9 | To build from source, run
10 |
11 | ```sh
12 | $ npm install
13 | $ npm run build
14 | ```
15 |
16 | ## Usage
17 |
18 | ```html
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
47 |
48 |
49 | ```
--------------------------------------------------------------------------------
/plugins/widget-react-helloworld/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | React Hello World Plugin
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
24 |
25 |
--------------------------------------------------------------------------------
/plugins/widget-react-helloworld/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@recogito/react-helloworld-widget",
3 | "version": "0.1.0",
4 | "description": "A minimal code example showing how to build your own editor widgets with React",
5 | "main": "dist/recogito-helloworld-widget.js",
6 | "module": "dist/recogito-helloworld-widget.js",
7 | "scripts": {
8 | "build": "webpack --mode=production",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "author": "Rainer Simon",
12 | "license": "BSD-3-Clause",
13 | "devDependencies": {
14 | "@babel/core": "^7.6.2",
15 | "@babel/plugin-proposal-class-properties": "^7.5.5",
16 | "@babel/preset-env": "^7.6.2",
17 | "@babel/preset-react": "^7.0.0",
18 | "babel-loader": "^8.0.6",
19 | "css-loader": "^5.2.5",
20 | "style-loader": "^2.0.0",
21 | "terser-webpack-plugin": "^3.1.0",
22 | "webpack": "^4.41.0",
23 | "webpack-cli": "^3.3.9",
24 | "webpack-dev-server": "^3.11.0"
25 | },
26 | "dependencies": {
27 | "preact": "^10.4.1"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/plugins/widget-react-helloworld/src/index.css:
--------------------------------------------------------------------------------
1 | .helloworld-widget {
2 | padding:5px;
3 | border-bottom:1px solid #e5e5e5;
4 | }
5 |
6 | .helloworld-widget button {
7 | outline:none;
8 | border:none;
9 | display:inline-block;
10 | width:20px;
11 | height:20px;
12 | border-radius:50%;
13 | cursor:pointer;
14 | opacity:0.5;
15 | margin:4px;
16 | }
17 |
18 | .helloworld-widget button.selected,
19 | .helloworld-widget button:hover {
20 | opacity:1;
21 | }
--------------------------------------------------------------------------------
/plugins/widget-react-helloworld/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'preact/compat';
2 |
3 | import './index.css';
4 |
5 | const HelloWorldWidget = props => {
6 | // We'll be using 'highlighting' as body purpose for
7 | // this type of widget
8 | const currentHighlight = props.annotation ?
9 | props.annotation.bodies.find(b => b.purpose === 'highlighting') : null;
10 |
11 | // This function handles body updates as the user presses buttons
12 | const setHighlightBody = value => () => {
13 | props.onUpsertBody(currentHighlight, { value, purpose: 'highlighting' });
14 | }
15 |
16 | return (
17 |
18 | { [ 'red', 'green', 'blue' ].map(color =>
19 |
24 | )}
25 |
26 | )
27 |
28 | }
29 |
30 | export default HelloWorldWidget;
--------------------------------------------------------------------------------
/plugins/widget-react-helloworld/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | const TerserPlugin = require('terser-webpack-plugin');
5 |
6 | const APP_DIR = fs.realpathSync(process.cwd());
7 |
8 | const resolveAppPath = relativePath => path.resolve(APP_DIR, relativePath);
9 |
10 | module.exports = {
11 | entry: resolveAppPath('src'),
12 | output: {
13 | filename: 'recogito-helloworld-widget.js',
14 | library: ['recogito', 'HelloWorldWidget'],
15 | libraryTarget: 'umd',
16 | libraryExport: 'default'
17 | },
18 | performance: {
19 | hints: false
20 | },
21 | optimization: {
22 | minimize: true,
23 | minimizer: [new TerserPlugin()],
24 | },
25 | resolve: {
26 | extensions: ['.js', '.jsx'],
27 | alias: {
28 | "react": "preact/compat",
29 | "react-dom": "preact/compat"
30 | }
31 | },
32 | module: {
33 | rules: [
34 | {
35 | test: /\.(js|jsx)$/,
36 | use: {
37 | loader: 'babel-loader' ,
38 | options: {
39 | "presets": [
40 | "@babel/preset-env",
41 | "@babel/preset-react"
42 | ],
43 | "plugins": [
44 | [
45 | "@babel/plugin-proposal-class-properties"
46 | ]
47 | ]
48 | }
49 | }
50 | },
51 | { test: /\.css$/, use: [ 'style-loader', 'css-loader'] },
52 | ]
53 | }
54 | }
--------------------------------------------------------------------------------
/plugins/widget-tag-validation/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@recogito/tag-validation-widget",
3 | "version": "0.1.0",
4 | "description": "A tag validation editor widget for RecogitoJS, Annotorious and AnnotoriousOSD",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "Rainer Simon",
10 | "license": "BSD-3-Clause",
11 | "devDependencies": {
12 | "@babel/core": "^7.6.2",
13 | "@babel/plugin-proposal-class-properties": "^7.5.5",
14 | "@babel/preset-env": "^7.6.2",
15 | "@babel/preset-react": "^7.0.0",
16 | "babel-loader": "^8.0.6",
17 | "css-loader": "^3.2.0",
18 | "html-webpack-plugin": "^3.2.0",
19 | "mini-css-extract-plugin": "^0.11.1",
20 | "node-forge": ">=0.10.0",
21 | "node-sass": "^4.14.1",
22 | "sass-loader": "^8.0.0",
23 | "serialize-javascript": "^3.1.0",
24 | "uglifyjs-webpack-plugin": "^2.2.0",
25 | "webpack": "^4.41.0",
26 | "webpack-cli": "^3.3.9",
27 | "webpack-dev-server": "^3.11.0"
28 | },
29 | "dependencies": {
30 | "downshift": "^5.4.6",
31 | "preact": "^10.4.1",
32 | "react-transition-group": "^4.3.0"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/plugins/widget-tag-validation/src/Icons.js:
--------------------------------------------------------------------------------
1 | import React from 'preact/compat';
2 |
3 | export const Checkmark = props => {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
11 | export const Cross = props => {
12 | return (
13 |
14 |
15 |
16 | )
17 | }
--------------------------------------------------------------------------------
/plugins/widget-tag-validation/src/TagAutocomplete.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react'
2 | import { useCombobox } from 'downshift'
3 |
4 | const TagAutocomplete = props => {
5 |
6 | const element = useRef();
7 |
8 | const [ inputItems, setInputItems ] = useState(props.vocabulary);
9 |
10 | useEffect(() =>
11 | element.current?.querySelector('input').focus(), []);
12 |
13 | const onInputValueChange = ({ inputValue }) => {
14 | props.onChange(inputValue);
15 | setInputItems(
16 | props.vocabulary.filter(item =>
17 | item.toLowerCase().startsWith(inputValue.toLowerCase())))
18 | }
19 |
20 | const {
21 | isOpen,
22 | getMenuProps,
23 | getInputProps,
24 | getComboboxProps,
25 | highlightedIndex,
26 | getItemProps,
27 | } = useCombobox({ items: inputItems, onInputValueChange });
28 |
29 | const onKeyDown = evt => {
30 | evt.stopPropagation();
31 |
32 | if (evt.which === 13) { // Enter is the only relevant key
33 | // Only forward key events if the dropdown is closed, or no option selected)
34 | if (!isOpen || highlightedIndex == -1) {
35 | props.onKeyDown(evt);
36 | } else if (highlightedIndex > -1) {
37 | props.onSelectSuggestion(inputItems[highlightedIndex]);
38 | }
39 | }
40 | }
41 |
42 | return (
43 |
44 |
45 |
49 |
50 |
51 | {isOpen && inputItems.map((item, index) => (
52 |
59 | {item}
60 |
61 | ))}
62 |
63 |
64 | )
65 |
66 | }
67 |
68 | export default TagAutocomplete;
--------------------------------------------------------------------------------
/plugins/widget-tag-validation/src/TagInput.jsx:
--------------------------------------------------------------------------------
1 | import React from 'preact/compat';
2 | import TagAutocomplete from './TagAutocomplete';
3 |
4 | // We can later extend this for URI-based tags
5 | const renderSuggestion = suggestion =>
6 | { suggestion }
7 |
8 | const TagInput = props => {
9 |
10 | const onKeyDown = evt => {
11 | if (evt.which === 13) { // Enter
12 | if (evt.ctrlKey)
13 | props.onCtrlEnter();
14 | else
15 | props.onEnter();
16 | }
17 | }
18 |
19 | return (
20 |
21 | { props.visible &&
22 |
29 | }
30 |
31 | )
32 |
33 | }
34 |
35 | export default TagInput;
--------------------------------------------------------------------------------
/plugins/widget-tag-validation/src/TagValidatorWidget.jsx:
--------------------------------------------------------------------------------
1 | import React from 'preact/compat';
2 | import { useState, useEffect } from 'preact/hooks';
3 | import { CSSTransition, TransitionGroup } from 'react-transition-group';
4 | import ValidatableTag from './ValidatableTag';
5 | import TagInput from './TagInput';
6 |
7 | import './TagValidatorWidget.scss';
8 |
9 | /**
10 | * Get existing draft tag or create a new, empty one
11 | */
12 | const getDraftTag = existing => {
13 | return existing ? existing : {
14 | type: 'TextualBody', value: '', purpose: 'tagging', confirmed: true, draft: true
15 | };
16 | };
17 |
18 | const TagValidatorWidget = props => {
19 |
20 | // All tags (draft and non-draft)
21 | const all = props.annotation ?
22 | props.annotation.bodies.filter(b => b.purpose === 'tagging') : [];
23 |
24 | // Non-draft tags
25 | const tags = all.filter(b => !b.draft);
26 |
27 | // Draft tag (if any)
28 | const draftTag = getDraftTag(all.find(b => b.draft));
29 |
30 | const [ showTagInput, setShowTagInput ] = useState(false);
31 |
32 | // Open the tag input field in case there are 0 tags
33 | useEffect(() => {
34 | setShowTagInput(tags.length == 0);
35 | }, [ tags.length ]);
36 |
37 | useEffect(() => {
38 | // 'Enter' on the popup is handled as OK
39 | const listener = evt => {
40 | if (evt.which === 13)
41 | onCtrlEnter();
42 | }
43 |
44 | document.addEventListener('keydown', listener);
45 | return () => document.removeEventListener('keydown', listener);
46 | });
47 |
48 | const onAddTag = () => {
49 | if (draftTag.value) {
50 | setShowTagInput(false);
51 |
52 | const { draft, ...tag } = draftTag;
53 |
54 | props.onUpdateBody(draftTag, tag);
55 | }
56 | }
57 |
58 | const onEditDraft = value => {
59 | const prev = draftTag.value.trim();
60 | const updated = value.trim();
61 |
62 | if (prev.length === 0 && updated.length > 0) {
63 | props.onAppendBody({ ...draftTag, value: updated });
64 | } else if (prev.length > 0 && updated.length === 0) {
65 | props.onRemoveBody(draftTag);
66 | } else {
67 | props.onUpdateBody(draftTag, { ...draftTag, value: updated });
68 | }
69 | }
70 |
71 | const onCtrlEnter = tag => {
72 | setShowTagInput(false);
73 | props.onSaveAndClose();
74 | }
75 |
76 | const onSelectSuggestion = value => {
77 | const { draft, ...tag } = draftTag;
78 | const updated = { ...tag, value };
79 |
80 | if (draftTag.value) {
81 | props.onUpdateBody(draftTag, updated);
82 | } else {
83 | props.onAppendBody(updated);
84 | }
85 |
86 | setShowTagInput(false);
87 | }
88 |
89 | return (
90 |
91 |
92 | { tags.map(tag =>
93 |
94 |
98 |
99 | )}
100 |
101 |
109 |
110 |
111 | setShowTagInput(!showTagInput)}>
112 | Add Tag
113 |
114 |
115 | )
116 |
117 | }
118 |
119 | export default TagValidatorWidget;
--------------------------------------------------------------------------------
/plugins/widget-tag-validation/src/TagValidatorWidget.scss:
--------------------------------------------------------------------------------
1 | @mixin rounded-corners($radius) {
2 | -webkit-border-radius:$radius;
3 | -khtml-border-radius:$radius;
4 | -moz-border-radius:$radius;
5 | border-radius:$radius;
6 | }
7 |
8 | .r6o-tag-validator {
9 | padding:5px 3px;
10 | text-align:right;
11 |
12 | .tag-list {
13 | display:inline;
14 | }
15 |
16 | .validatable-tag {
17 | font-family:'Baloo 2';
18 | font-size:17px;
19 | position:relative;
20 | display:inline-block;
21 | margin:0 2px;
22 | min-width:100px;
23 | text-align:center;
24 | color:#2b2b2b;
25 | cursor:pointer;
26 | opacity:1;
27 | transition:opacity 200ms;
28 |
29 | .label {
30 | display:inline-block;
31 | width:100%;
32 | height:100%;
33 | border:1px solid #aeaeae;
34 | padding:2px 12px;
35 | box-sizing:border-box;
36 | background-color:#d2d2d2;
37 | @include rounded-corners(3px);
38 | }
39 |
40 | }
41 |
42 | .validatable-tag.exit {
43 | opacity:0;
44 | }
45 |
46 | .validatable-tag.confirmed .label {
47 | color:#20660e;
48 | background-color:#d0f0c6;
49 | border-color:#9bc391;
50 | }
51 |
52 | .validation-buttons {
53 | position:absolute;
54 | top:0;
55 | left:0;
56 | width:100%;
57 | height:100%;
58 | display:flex;
59 | flex-direction:row;
60 | align-items:stretch;
61 | overflow:hidden;
62 | opacity:0;
63 | transition:opacity 0.3s;
64 | @include rounded-corners(3px);
65 |
66 | .validation-button {
67 | display:inline-block;
68 | flex:1;
69 | color:transparent; // Hide label
70 | border:1px solid #c2c2c2;
71 | background-color:#d2d2d2;
72 | color:#fff;
73 | }
74 |
75 | .validation-button::before {
76 | display:inline-block;
77 | width:100%;
78 | text-align:center;
79 | font-size:30px;
80 | font-family:Arial, Helvetica, sans-serif;
81 | line-height:32px;
82 | vertical-align:top;
83 | }
84 |
85 | .confirm {
86 | border-width:1px 0 1px 1px;
87 | padding-top:3px;
88 | }
89 |
90 | .confirm:hover {
91 | color:#2d8d14;
92 | background-color:#d0f0c6;
93 | border-color:#9bc391;
94 | }
95 |
96 | .reject {
97 | padding-top:4px;
98 | border-width:1px 1px 1px 0;
99 | }
100 |
101 | .reject:hover {
102 | color: #9e0a0a;
103 | background-color: #eccfcf;
104 | border-color: #c9a1a1;
105 | }
106 |
107 | }
108 |
109 | .add-tag {
110 | display:inline-block;
111 | margin:0 2px;
112 | background-color:#f5f7f7;
113 | border:1px solid #e5e5e5;;
114 | outline:none;
115 | height:33px;
116 | width:33px;
117 | vertical-align:top;
118 | line-height:32px;
119 | cursor:pointer;
120 | @include rounded-corners(3px);
121 |
122 | span {
123 | display:none;
124 | }
125 |
126 | }
127 |
128 | .add-tag:hover {
129 | background-color:#daedf3;
130 | border:1px solid #98c0cd;
131 | }
132 |
133 | .add-tag::after {
134 | content:'\FF0B';
135 | text-align:center;
136 | font-weight:bold;
137 | font-size:16px;
138 | color:#cdcdcd;
139 | }
140 |
141 | .add-tag:hover::after {
142 | color:#6898a8;
143 | }
144 |
145 | .tag-input {
146 | display:inline;
147 |
148 | div {
149 | display:inline;
150 | position:relative;
151 | }
152 |
153 | input {
154 | margin:0 2px;
155 | background-color:#daedf3;
156 | border:1px solid #98c0cd;
157 | font-family: 'Baloo 2';
158 | font-size:17px;
159 | padding:2px 12px;
160 | width:100px;
161 | outline:none;
162 | color:#2b2b2b;
163 | @include rounded-corners(3px);
164 | }
165 |
166 | ul {
167 | position:absolute;
168 | background:#fff;
169 | margin:0;
170 | padding:0;
171 | list-style-type:none;
172 | min-width:100%;
173 | border-radius:3px;
174 | border:1px solid #d6d7d9;
175 | box-sizing:border-box;
176 | box-shadow:0 0 20px rgba(0,0,0,0.25);
177 | }
178 |
179 | ul:empty {
180 | display:none;
181 | }
182 |
183 | li {
184 | box-sizing:border-box;
185 | padding:2px 12px;
186 | width:100%;
187 | text-align:left;
188 | cursor:pointer;
189 | }
190 |
191 | li.react-autosuggest__suggestion--highlighted {
192 | background-color:#ecf0f1;
193 | }
194 |
195 | }
196 |
197 | }
--------------------------------------------------------------------------------
/plugins/widget-tag-validation/src/ValidatableTag.jsx:
--------------------------------------------------------------------------------
1 | import React from 'preact/compat';
2 | import { useState, useEffect } from 'preact/hooks';
3 | import { Checkmark, Cross } from './Icons';
4 |
5 | const ValidatableTag = props => {
6 |
7 | const [ isConfirmed, setIsConfirmed ] = useState(props.body.confirmed);
8 |
9 | const [ buttonsVisible, setButtonsVisible ] = useState(false);
10 |
11 | useEffect(_ => {
12 | const updatedBody = { ...props.body };
13 |
14 | if (isConfirmed)
15 | updatedBody.confirmed = true;
16 | else
17 | delete updatedBody.confirmed;
18 |
19 | props.onUpdateConfirmation(props.body, updatedBody);
20 | }, [ isConfirmed ]);
21 |
22 | const toggleConfirmed = () => {
23 | setIsConfirmed(!isConfirmed);
24 | setButtonsVisible(false);
25 | }
26 |
27 | const reject = () =>
28 | props.onReject(props.body);
29 |
30 | return (
31 | setButtonsVisible(true)}
34 | onMouseLeave={() => setButtonsVisible(false)}>
35 |
36 |
{ props.body.value }
37 |
38 |
39 |
42 |
43 |
44 |
45 |
48 |
49 |
50 |
51 |
52 | )
53 |
54 | }
55 |
56 | export default ValidatableTag;
--------------------------------------------------------------------------------
/plugins/widget-tag-validation/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as TagValidatorWidget } from './TagValidatorWidget';
--------------------------------------------------------------------------------