Source: papertools/annotationUITool.mjs

/**
 * OpenSeadragon paperjs overlay plugin based on paper.js
 * @version 0.4.13
 * 
 * Includes additional open source libraries which are subject to copyright notices
 * as indicated accompanying those segments of code.
 * 
 * Original code:
 * Copyright (c) 2022-2024, 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 {ToolBase} from './base.mjs';
import { OpenSeadragon } from '../osd-loader.mjs';

/**
 * Base class for annotation tools, extending the ToolBase class.
 *
 * @class
 * @extends ToolBase
 * @memberof OSDPaperjsAnnotation
 */
class AnnotationUITool extends ToolBase{
    /**
     * Create an AnnotationUITool instance.
     *
     * @constructor
     * @param {paper.PaperScope} paperScope - The Paper.js scope for the tool.
     *
     * @property {boolean} _active - Flag indicating if the tool is currently active.
     * @property {paper.Item[]} _items - Array of selected items.
     * @property {paper.Item} _item - The selected item (if only one is selected).
     * @property {paper.Item} _itemToCreate - The selected new item to be created.
     */
    constructor(paperScope){
        super(paperScope)
        
        this._active=false;
        this._items=[];
        this._item=null;

        this.tool.onMouseDown = ev => {
            this.onMouseDown(this._transformEvent(ev));
        }
        this.tool.onMouseDrag = ev => {
            this.onMouseDrag(this._transformEvent(ev));
        }
        this.tool.onMouseMove = ev => {
            this.onMouseMove(this._transformEvent(ev));
        }
        this.tool.onMouseUp = ev => {
            this.onMouseUp(this._transformEvent(ev));
        }

    }

    isActive(){
        return this._active;
    }
    
    /**
     * Activate the annotation tool, making it ready for interaction.
     * If another tool was active, it's deactivated before activating this tool.
     */
    activate(){
        if(this._active) return;//breaks possible infinite loops of tools activating/deactivating each other
        this._active=true;
        this.getSelectedItems();
        this._setTargetLayer();
        let previousTool=this.project.paperScope.getActiveTool();
        this.tool.activate();
        this.toolbarControl.activate();//console.log('toolbar control activated')
        previousTool && previousTool != this && previousTool.deactivate(true);

        this.raiseCanvasZIndex();

        this.onActivate();
        this.broadcast('activated',{target:this});
    }
    /**
     * Deactivate the annotation tool, stopping its interaction.
     * @param {boolean} finishToolAction - Whether the tool action should be completed before deactivating.
     */
    deactivate(finishToolAction){
        if(!this._active) return;//breaks possible infinite loops of tools activating/deactivating each other
        this._active=false;
        this.toolbarControl.deactivate();

        this.resetCanvasZIndex();
        
        this.onDeactivate(finishToolAction);
        this.broadcast('deactivated',{target:this}); 
    }

    /**
     * Raise the viewer canvas's z-index so that toolbars don't interfere
     */
    raiseCanvasZIndex(){
        //raise the viewer's canvas to the top of the z-stack of the container element so that the toolbars don't interfere
        const viewer = this.project.paperScope.overlay.viewer;
        const canvas = viewer.canvas;
        this._canvasPriorZIndex = window.getComputedStyle(canvas)['z-index'];
        const siblings = Array.from(viewer.canvas.parentElement.children).filter(c => c!==canvas);
        const maxZ = Math.max(...siblings.map(el => {
            const z = window.getComputedStyle(el)['z-index'];
            return z === 'auto' ? 0 : parseInt(z);
        }));
        canvas.style['z-index'] = maxZ + 1;
    }

    /**
     * Return the viewer canvas to its original z-index 
     */
    resetCanvasZIndex(){
        //reset z-index of viewer canvas
        const canvas = this.project.paperScope.overlay.viewer.canvas;
        canvas.style['z-index'] = this._canvasPriorZIndex;
    }
    /**
     * Get the associated toolbar control for the tool.
     * @returns {AnnotationUIToolbarBase} The toolbar control instance.
     */
    getToolbarControl(){
        return this.toolbarControl;
    }
    /**
     * Set the associated toolbar control for the tool.
     *
     * @param {AnnotationUIToolbarBase} toolbarControl - The toolbar control instance to set.
     * @returns {AnnotationUIToolbarBase} The provided toolbar control instance.
     */
    setToolbarControl(toolbarControl){
        this.toolbarControl = toolbarControl;
        return this.toolbarControl;
    }
    /**
     * Refresh the list of currently selected items.
     */
    refreshItems(){
        return this.getSelectedItems();
    }
    /**
     * Retrieve the list of currently selected items.
     */
    getSelectedItems(){
        this._items = this.project.paperScope.findSelectedItems();
        this._itemToCreate = this.project.paperScope.findSelectedNewItem();
    }
    /**
     * Callback function triggered when the selection of items changes.
     * This function can be overridden in subclasses to react to selection changes.
     */
    selectionChanged(){
        this.getSelectedItems();
        this._setTargetLayer();
        this.onSelectionChanged();
    }
    /**
     * Callback function triggered when the selection changes.
     * To be implemented in subclasses.
     */
    onSelectionChanged(){}
    /**
     * Get the array of currently selected items.
     *
     * @returns {paper.Item[]} An array of currently selected items.
     */
    get items(){
        return this._items;
    }
    /**
     * Get the currently selected item, if only one is selected.
     *
     * @returns {paper.Item|null} The currently selected item, or null if no item is selected.
     */
    get item(){
        return this._items.length==1 ? this._items[0] : null;
    }
    /**
     * Get the selected new item to be created.
     *
     * @returns {paper.Item|null} The selected new item, or null if no item is selected.
     */
    get itemToCreate(){
        return this._itemToCreate;
    }

    get targetLayer(){
        return this._targetLayer;
    }

    get targetMatrix(){
        return this.targetLayer ? this.targetLayer.matrix : this._identityMatrix;
    }

    // private
    _setTargetLayer(){
        if(this.item){
            this._targetLayer = this.item.layer;
        } else if(this.itemToCreate){
            this._targetLayer = this.itemToCreate.layer;
        } else if(this.items){
            let layerSet = new Set(this.items.map(item=>item.layer));
            if(layerSet.size === 1){
                this._targetLayer = layerSet.values().next().value;
            } else {
                this._targetLayer = this.project.overlay.viewer.viewport.paperLayer;
            }
        } else {
            this._targetLayer = this.project.paperScope.project.activeLayer;
        }
    }
    // private
    _transformEvent(ev){
        let matrix = this.targetMatrix;
        let transformed = {
            point: matrix.inverseTransform(ev.point),
            downPoint: matrix.inverseTransform(ev.downPoint),
            lastPoint: matrix.inverseTransform(ev.lastPoint),
            middlePoint: matrix.inverseTransform(ev.middlePoint),
        };
        let deltaStart = ev.point.subtract(ev.delta);
        transformed.delta = transformed.point.subtract(matrix.inverseTransform(deltaStart));

        ev.original = {
            point: ev.point,
            downPoint: ev.downPoint,
            lastPoint: ev.lastPoint,
            middlePoint: ev.middlePoint,
            delta: ev.delta
        };

        Object.assign(ev, transformed);

        return ev;
    }
        
}
export{AnnotationUITool};

/**
 * Base class for annotation toolbar controls.
 *
 * @class
 * @memberof OSDPaperjsAnnotation.AnnotationUITool
 */
class AnnotationUIToolbarBase{
    /**
     * Create a new instance of AnnotationUIToolbarBase associated with an annotation tool.
     *
     * @constructor
     * @param {AnnotationUITool} tool - The annotation tool linked to the toolbar control.
     */
    constructor(tool){
        // let self=this;
        this._active=false;
        let button=document.createElement('button');
        button.classList.add('btn','invisible');
        button.textContent = 'Generic Tool';

        this.button=new OpenSeadragon.Button({
            tooltip:'Generic Tool',
            element:button,
            onClick:function(ev){if(!ev.eventSource.element.disabled) tool._active?tool.deactivate(true):tool.activate()},
        });
        this.button.configure=function(node,tooltip){
            this.element.title = tooltip;
            this.element.replaceChildren(node);
            this.element.classList.remove('invisible');
            this.tooltip=tooltip;
        }
        this.dropdown=document.createElement('div');
        this.dropdown.classList.add('dropdown'); 
        this.tool = tool;
    }
    /**
     * Check whether the toolbar control is enabled for a specific mode.
     *
     * @param {string} mode - The mode to check for enabling.
     * @returns {boolean} True if the toolbar control is enabled for the mode, otherwise false.
     */
    isEnabledForMode(mode){
        return false;
    }
    
    /**
     * Activate the toolbar control, making it visually active.
     */
    activate(){
        if(this._active) return;
        this._active=true;
        //this.tool.activate();
        this.button.element.classList.add('active');
        this.dropdown.classList.add('active');
    }
    /**
     * Deactivate the toolbar control, making it visually inactive.
     *
     * @param {boolean} shouldFinish - Whether the action associated with the control should be completed.
     */
    deactivate(shouldFinish){
        if(!this._active) return;
        this._active=false;
        //this.tool.deactivate(shouldFinish);
        this.button.element.classList.remove('active');
        this.dropdown.classList.remove('active');
    }
}
export{AnnotationUIToolbarBase};