/**
* OpenSeadragon paperjs overlay plugin based on paper.js
* @version 0.7.6
*
* Includes additional open source libraries which are subject to copyright notices
* as indicated accompanying those segments of code.
*
* Original code:
* Copyright (c) 2022-2026, Thomas Pearce
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of osd-paperjs-annotation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
import { AnnotationLayout } from './annotationlayout.mjs';
import { FileDialog } from './filedialog.mjs';
/**
* @memberof OSDPaperjsAnnotation
* @class
* A class for creating and managing the annotation UI. Intended to be constructed only via AnnotationToolkit.addAnnotationUI().
* Toolbar and layer UI are owned by the toolkit; this class uses them via getToolbar()/getLayerUI() and places them via AnnotationLayout.
*/
class AnnotationUI {
/**
* Creates an instance of AnnotationUI.
*
* @param {Object} annotationToolkit - The annotation toolkit object.
* @param {AnnotationToolset} toolset - The toolset (owned by the toolkit).
* @param {Object} opts - The options for the AnnotationUI.
* @param {boolean} [opts.autoOpen=true] - Determines if the AnnotationUI should be automatically opened.
* @param {Array} [opts.featureCollections=[]] - An array of feature collections to load.
* @param {boolean} [opts.addButton=true] - Determines if the AnnotationUI button should be added.
* @param {boolean} [opts.addToolbar=true] - Whether toolbar is requested (toolkit already created it via getToolbar if true).
* @param {string[]} [opts.tools=null] - An array of tool names to use in the AnnotationToolbar.
* @param {boolean} [opts.addLayerUI=true] - Whether layer UI is requested (toolkit already created it via getLayerUI if true).
* @param {boolean} [opts.addFileButton=true] - Determines if the file button should be added for saving/loading annotations.
* @param {boolean} [opts.buttonTogglesToolbar=true] - Determines if the AnnotationToolbar visibility is toggled by the AnnotationUI button.
* @param {boolean} [opts.buttonTogglesLayerUI=true] - Determines if the LayerUI visibility is toggled by the AnnotationUI button.
*/
constructor(annotationToolkit, toolset, opts) {
let defaultOpts = {
autoOpen: true,
featureCollections: [],
addButton: true,
addToolbar: true,
tools: null,
addLayerUI: true,
addFileButton: true,
buttonTogglesToolbar: true,
buttonTogglesLayerUI: true,
};
opts = this.options = Object.assign(defaultOpts, opts);
this._annotationToolkit = annotationToolkit;
this._viewer = annotationToolkit.viewer;
const toolbar = annotationToolkit.getToolbar?.() ?? null;
const layerUI = annotationToolkit.getLayerUI?.() ?? null;
const toolbarRef = toolbar ? { element: toolbar.element, show: () => toolbar.show(), hide: () => toolbar.hide() } : null;
const layerUIRef = layerUI ? { element: layerUI.element, show: () => layerUI.show(), hide: () => layerUI.hide() } : null;
this._layout = new AnnotationLayout(this._viewer, {
toolbar: toolbarRef,
layerUI: layerUIRef,
addButton: opts.addButton !== false,
addViewerButton: (config) => annotationToolkit.overlay.addViewerButton(config),
buttonTogglesToolbar: opts.buttonTogglesToolbar !== false,
buttonTogglesLayerUI: opts.buttonTogglesLayerUI !== false,
initialOpen: opts.autoOpen !== false,
});
if (opts.autoOpen !== false) {
toolbar?.show();
layerUI?.show();
} else {
toolbar?.hide();
layerUI?.hide();
}
this._fileDialog = new FileDialog(annotationToolkit, { appendTo: this._viewer.element });
this._filebutton = null;
if (opts.addFileButton) {
this._filebutton = annotationToolkit.overlay.addViewerButton({
onClick: () => this._fileDialog.toggle(),
faIconClass: 'fa-save',
tooltip: 'Save/Load Annotations',
});
}
if (opts.featureCollections) {
annotationToolkit.loadGeoJSON(opts.featureCollections);
}
}
/**
* Viewer toolbar buttons managed by this UI (for host integrations such as ConfigurationWidget addSection).
* @returns {{ pencil: Object|null, file: Object|null }} OpenSeadragon button objects with `.element`, or null entries when not created.
*/
getViewerToolbarButtons() {
return {
pencil: this._layout?.getPencilViewerButton?.() ?? null,
file: this._filebutton,
};
}
/**
* Same as clicking the pencil button: toggles toolbar/layer visibility per constructor options.
*/
toggleAnnotationPanels() {
this._layout?.togglePanelsFromButton?.();
}
/**
* Whether annotation panels are open (pencil toggle state).
* @returns {boolean}
*/
areAnnotationPanelsVisible() {
return this._layout?.arePanelsVisible?.() ?? false;
}
/**
* Opens the save/load file dialog (no-op if addFileButton was false).
*/
openFileDialog() {
this._fileDialog?.show?.();
}
/**
* Destroys the AnnotationUI and cleans up its resources. Does not destroy the toolkit's toolbar or layer UI.
*/
destroy() {
this._layout?.destroy();
this._layout = null;
if (this._filebutton) {
const idx = this._viewer.buttonGroup.buttons.indexOf(this._filebutton);
if (idx > -1) this._viewer.buttonGroup.buttons.splice(idx, 1);
this._filebutton.element.remove();
this._filebutton = null;
}
}
/**
* Show the LayerUI interface
*/
showUI() {
this.ui?.show();
}
/**
* Hide the LayerUI interface
*/
hideUI() {
this.ui?.hide();
}
/**
* Show the toolbar
*/
showToolbar() {
this.toolbar?.show();
}
/**
* Hide the toolbar
*/
hideToolbar() {
this.toolbar?.hide();
}
get ui() {
return this._annotationToolkit.getLayerUI?.() ?? null;
}
get toolbar() {
return this._annotationToolkit.getToolbar?.() ?? null;
}
get element() {
return this.ui?.element ?? null;
}
}
export { AnnotationUI };