Source: layerui.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 { OpenSeadragon } from './osd-loader.mjs';
import { FeatureCollectionUI } from './featurecollectionui.mjs';
import { domObjectFromHTML } from './utils/domObjectFromHTML.mjs';
import { datastore } from './utils/datastore.mjs';
import { DragAndDrop } from './utils/draganddrop.mjs';
import { IconFactory } from './utils/faIcon.mjs';

/**
 * A user interface for managing layers of feature collections.
 * @class
 * @memberof OSDPaperjsAnnotation
 * @extends OpenSeadragon.EventSource
 */
class LayerUI extends OpenSeadragon.EventSource{

    /**
     * Create a new LayerUI instance.
     * @constructor
     * @property {HTMLElement} element - The HTML element associated with the LayerUI instance. refer to typedef for subproperties
     * @param {AnnotationToolkit} annotationToolkit - The paper scope object.
     */
    constructor(annotationToolkit, addFileButton){
        super();
        let _this=this;
        this._tk = annotationToolkit
        this.paperScope = this._tk.paperScope;
        this.paperScope.project.on('feature-collection-added',ev=>this._onFeatureCollectionAdded(ev));
        
        this.element = makeHTMLElement();
        this.iconFactory = new IconFactory(this.element.querySelector('.icon-factory-container'));
        this.iconFactory.convertFaIcons(this.element);
        
        this.element.querySelector('.new-feature-collection').addEventListener('click', ev => {
            ev.stopPropagation();
            ev.preventDefault();
            this._tk.addEmptyFeatureCollectionGroup();
        });

        this.element.querySelector('.annotation-ui-feature-collections').addEventListener('click', function(){
            if(this.textContent.trim().length === 0){
                _this._tk.addEmptyFeatureCollectionGroup();
            }
        });

        this.element.querySelector('.toggle-annotations').addEventListener('click',() => {
            let hidden = this.element.querySelectorAll('.annotation-ui-feature-collections .feature-collection.annotation-hidden');
            if(hidden.length > 0){
                hidden.forEach(e=>{
                    e.querySelectorAll('[data-action="show"]').forEach(a=>a.dispatchEvent(new Event('click', {bubbles:true})));
                })
            } else {
                const fcs = this.element.querySelectorAll('.annotation-ui-feature-collections .feature-collection:not(.hidden) [data-action="hide"]')
                fcs.forEach(a=>a.dispatchEvent(new Event('click',{bubbles:true})));
            }
        });

        this._dragAndDrop = new DragAndDrop({
            parent: this.element, 
            selector: '.feature-collection',
            dropTarget: this.element.querySelector('.annotation-ui-feature-collections'),
            onDrop:()=>{
                this.element.querySelectorAll('.annotation-ui-feature-collections .feature-collection').forEach(g => {
                    let fg = datastore.get(g, 'featureCollection');
                    fg.group.bringToFront();
                })
            }
        });

        
        //set up delegated events

        this.element.addEventListener('selected', function(ev){
            if(ev.target.matches('.feature')){
                ev.stopPropagation();
                this.classList.add('selected');
                this.scrollIntoView({block:'nearest'});
            }
        });
        this.element.addEventListener('deselected', function(ev){
            if(ev.target.matches('.feature')){
                ev.stopPropagation();
                this.classList.remove('selected');
            }
        });
        
        this.element.addEventListener('click', function(ev){
            if(ev.target.matches('.toggle-list')){
                this.closest('.features').classList.toggle('collapsed');
                ev.stopPropagation();
            }
            
        });
        
        this.element.addEventListener('value-changed',() => {
            this.element.querySelector('.feature.selected').dispatchEvent(new Event('selected'));
            this.element.querySelector('.feature-collection.active').dispatchEvent(new Event('selected'));
        });

        const totalOpacitySlider= this.element.querySelector('input.annotation-total-opacity');
        totalOpacitySlider.addEventListener('input',function(){
            setOpacity(this.value);
        })
        totalOpacitySlider.dispatchEvent(new Event('input'));

        const fillOpacitySlider = this.element.querySelector('input.annotation-fill-opacity');
        fillOpacitySlider.addEventListener('input',function(){
            _this.paperScope.view.fillOpacity = this.value;
        });
        fillOpacitySlider.dispatchEvent(new Event('input'));

        /**
         * Set the opacity of the feature collections.
         * @private
         * @param {number} o - The opacity value between 0 and 1.
         */
        function setOpacity(o){
            let status = Array.from(_this.element.querySelectorAll('.feature-collection')).reduce(function(ac,el){
                if( el.classList.contains('selected') ){
                    ac.selected.push(el);
                }
                else if( el.matches(':hover,.svg-hovered')){
                    ac.hover.push(el);
                }
                else{
                    ac.other.push(el);
                }
                return ac;
            },{selected:[],hover:[],other:[]});
            if(status.selected.length>0){
                status.selected.forEach(function(el){
                    let opacity=1 * o;
                    let fc=datastore.get(el, 'featureCollection');
                    fc&&fc.ui.setOpacity(opacity)
                })
                status.hover.concat(status.other).forEach(function(el){
                    let opacity=0.25 * o;
                    let fc=datastore.get(el, 'featureCollection');
                    fc&&fc.ui.setOpacity(opacity)
                })
            }
            else if(status.hover.length>0){
                status.hover.forEach(function(el){
                    let opacity=1 * o;
                    let fc=datastore.get(el, 'featureCollection');
                    fc&&fc.ui.setOpacity(opacity)
                })
                status.other.forEach(function(el){
                    let opacity=0.25 * o;
                    let fc=datastore.get(el, 'featureCollection');
                    fc&&fc.ui.setOpacity(opacity)
                })
            }
            else{
                status.other.forEach(function(el){
                    let opacity=1 * o;
                    let fc=datastore.get(el, 'featureCollection');
                    fc&&fc.ui.setOpacity(opacity)
                })
            }
        }
        
    }
    /**
     * Hide the layer UI element.
     * 
     */
    hide(){
        this.element.classList.add('hidden');
        this.raiseEvent('hide');
    }
    /**
     * Show the layer UI element.
     * 
     */
    show(){
        this.element.classList.remove('hidden');
        this.raiseEvent('show');
    }
    /**
     * Toggle the visibility of the layer UI element.
     */
    toggle(){
        this.element.matches(':visible') ? this.hide() : this.show();
    }
    /**
     * Deactivate the layer UI element.
     */
    deactivate(){
        this.element.classList.add('deactivated');
    }
    /**
     * Activate the layer UI element.
     */
    activate(){
        this.element.classList.remove('deactivated');
    }
    /**
     * Destroy the layer UI element.
     */
    destroy(){
        this.raiseEvent('destroy');
        this.element.remove();
    }
    

    /**
     * Handle the feature collection added event.
     * @param {object} ev - The event object.
     * @private
     * 
     */
    _onFeatureCollectionAdded(ev){
        let grp = ev.group;
        
        let fc=new FeatureCollectionUI(grp, {
            iconFactory: this.iconFactory
        });
        this.element.querySelector('.annotation-ui-feature-collections').appendChild(fc.element);
        this._dragAndDrop.refresh();
        fc.element.dispatchEvent(new Event('element-added'));
        setTimeout(function(){fc.element.classList.add('inserted'); }, 30);//this allows opacity fade-in to be triggered

    }
    
    

}
export{LayerUI};
/**
 *  Create an HTML element for the layer UI.
 * @private
 * @returns {jQuery} The jQuery object of the HTML element.
 */
function makeHTMLElement(){
    let html = `
        <div class="annotation-ui-mainwindow" title="Annotations">
            <div><span class='fa-save'></span> <span class="annotation-ui-title">Annotation Interface</span></div>
            <div class='annotation-ui-toolbar annotation-visibility-controls'>                
                <div class="visibility-buttons btn-group btn-group-sm disable-when-deactivated" role="group">
                    <button class="btn btn-default toggle-annotations" type="button" title="Toggle annotations">
                        <span class="glyphicon glyphicon-eye-open fa fa-eye"></span><span class="glyphicon glyphicon-eye-close fa fa-eye-slash"></span>
                    </button>
                </div>
                <span class="annotation-opacity-container disable-when-annotations-hidden" title="Change total opacity">
                    <input class="annotation-total-opacity" type="range" min="0" max="1" step="0.01" value="1">
                </span>
                <span class="annotation-opacity-container disable-when-annotations-hidden" title="Change fill opacity">
                    <input class="annotation-fill-opacity" type="range" min="0" max="1" step="0.01" value="0.25">
                </span>
            </div>
            <div class='annotation-ui-feature-collections disable-when-annotations-hidden disable-when-deactivated'></div>
            <div class='new-feature-collection disable-when-deactivated'><span class='glyphicon glyphicon-plus fa fa-plus'></span>Add Feature Collection</div>
            <div class='icon-factory-container'></div>
        </div>`;
    let element = domObjectFromHTML(html);
    let guid= 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,function(c) {
        let r = Math.random() * 16|0;
        let v = c == 'x' ? r : (r&0x3|0x8);
        return v.toString(16);
    });
    // element.attr('data-ui-id',guid);
    element.dataset.uiId = guid;
    return element;
}