├── .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 | ![Demo video](https://raw.githubusercontent.com/recogito/recogito-client-plugins/main/plugins/annotorious-better-polygon/screencast.gif) 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 | 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 | ![Example screenshot](https://raw.githubusercontent.com/recogito/recogito-client-plugins/main/plugins/annotorious-shape-labels/screenshot.jpg) 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 | ![Animated screenshot](https://raw.githubusercontent.com/recogito/recogito-client-plugins/main/plugins/annotorious-tensorflow-tag-suggestions/screenshot.gif) 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 | ![Example screen capture](screencap.gif) 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 | [![Join the chat at https://gitter.im/recogito/annotorious](https://badges.gitter.im/recogito/annotorious.svg)](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 | ![Example screen capture](screencap.gif) 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 | [![Join the chat at https://gitter.im/recogito/annotorious](https://badges.gitter.im/recogito/annotorious.svg)](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 |
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 | 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'; --------------------------------------------------------------------------------