Source: annotationui.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.
 * 
 */

//styles in annotationui.css

import { addCSS } from './utils/addcss.mjs';
import { AnnotationToolbar } from './annotationtoolbar.mjs';
import { LayerUI } from './layerui.mjs';
import { FileDialog } from './filedialog.mjs';

addCSS('annotationui.css', 'annotationui');
addCSS('editablecontent.css', 'editablecontent');

/**
 * @memberof OSDPaperjsAnnotation
 * @class
 * A class for creating and managing the annotation UI
 */
class AnnotationUI {

  /**
   * Creates an instance of AnnotationUI.
   *
   * @param {Object} annotationToolkit - The annotation toolkit object.
   * @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] - Determines if the AnnotationToolbar should be added.
   * @param {string[]} [opts.tools=null] - An array of tool names to use in the AnnotationToolbar. If not provided, all available tools will be used.
   * @param {boolean} [opts.addLayerUI=true] - Determines if the LayerUI dialog should be added.
   * @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, 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);
    let _viewer = this._viewer = annotationToolkit.viewer; // shorter alias
    this._isOpen = !!opts.autoOpen;

  
    /**
     * _toolbar: AnnotationToolbar UI for interactive tools
     * @private
     */
    this._toolbar = new AnnotationToolbar(annotationToolkit.overlay.paperScope, opts.tools);

    /**
     * _fileDialog: FileDialog UI for loading/saving data
     * @private
     */
    this._fileDialog = new FileDialog(annotationToolkit, { appendTo: _viewer.element });
    this._filebutton = null;
    if (opts.addFileButton) {
      //Handles the click event of the file button.
      this._filebutton = annotationToolkit.overlay.addViewerButton({
        onClick: () => {
          this._fileDialog.toggle();
        },
        faIconClass: 'fa-save',
        tooltip: 'Save/Load Annotations',
      });
    }

    /**
     * _layerUI: LayerUI: graphical user interface for this annotation layer
     * @private
     */
    this._layerUI = new LayerUI(annotationToolkit, opts.addFileButton);
    if (opts.addLayerUI) {
      this._addToViewer();
    }

    if(opts.autoOpen){
      this._layerUI.show();
      this._toolbar.show();
    } else {
      this._layerUI.hide();
      this._toolbar.hide();
    }


    /**
     * _button: Button for toggling LayerUI and/or AnnotationToolbar
     * @private
     */
    this._button = null;
    if (opts.addButton) {
      this._button = annotationToolkit.overlay.addViewerButton({
        onClick: () => {
          this._isOpen = !this._isOpen;
          if (this._isOpen) {
            this.options.buttonTogglesToolbar && this._toolbar.show();
            this.options.buttonTogglesLayerUI && this._layerUI.show();
          } else {
            this.options.buttonTogglesToolbar && this._toolbar.hide();
            this.options.buttonTogglesLayerUI && this._layerUI.hide();
          }
        },
        faIconClass: 'fa-pencil',
        tooltip: 'Annotation Interface',
      });
    }

    if (opts.featureCollections) {
      annotationToolkit.loadGeoJSON(opts.featureCollections);
    }
  }

  /**
   * Destroys the AnnotationUI and cleans up its resources.
   */
  destroy() {
    this._layerUI.destroy();
    this._toolbar.destroy();
    if (this._button) {
      let idx = this._viewer.buttonGroup.buttons.indexOf(this._button);
      if (idx > -1) {
        this._viewer.buttonGroup.buttons.splice(idx, 1);
      }
      this._button.element.remove();
    }
    if (this._filebutton) {
      let idx = this._viewer.buttonGroup.buttons.indexOf(this._filebutton);
      if (idx > -1) {
        this._viewer.buttonGroup.buttons.splice(idx, 1);
      }
      this._filebutton.element.remove();
    }
  }

  /**
   * 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._layerUI;
  }

  get toolbar() {
    return this._toolbar;
  }

  get element(){
    return this._layerUI.element;
  }

  /**
   * Set up the grid that adds the UI to the viewer
   */
  _addToViewer(){
    
    const container = document.createElement('div');
    
    this._viewer.element.appendChild(container);
    
    const top = document.createElement('div');
    const bottom = document.createElement('div');
    const center = document.createElement('div');
    const left = document.createElement('div');
    const right = document.createElement('div');
    const resizeRight = document.createElement('div');
    const classes={
      'annotation-ui-grid':container,
      'top':top,
      'bottom':bottom,
      'center':center,
      'left':left,
      'right':right,
      'resize-right':resizeRight
    }

    Object.entries(classes).forEach(([name, node])=>node.classList.add(name));
    [center, right, left, top, bottom].forEach(div => container.appendChild(div));

    center.appendChild(this._viewer.container);
    right.appendChild(resizeRight);
    right.appendChild(this.element);
    top.appendChild(this._toolbar.element);

    // keep a reference to the UI element
    const element = this.element;

    // add event handlers to do the resizing.
    const body = document.querySelector('body');
    let offset;

    resizeRight.addEventListener('mousedown',function(ev){
      this.classList.add('resizing');
      body.classList.add('.annotation-ui-noselect');

      document.addEventListener('mousemove', moveHandler);
      document.addEventListener('mouseleave', finishResize);
      document.addEventListener('mouseup', finishResize);

      offset = element.getBoundingClientRect().left - ev.x;

    });

    function moveHandler(ev){
      if(resizeRight.classList.contains('resizing')){
        if(ev.movementX){
          const bounds = element.getBoundingClientRect();
          element.style.width = bounds.right - ev.x - offset + 'px';
        }
        ev.preventDefault();
      }
    }
    function finishResize(ev){
      document.removeEventListener('mousemove', moveHandler);
      document.removeEventListener('mouseleave', finishResize);
      document.removeEventListener('mouseup', finishResize);
      body.classList.remove('.annotation-ui-noselect');
      resizeRight.classList.remove('resizing');
    }
    
  }

}

export { AnnotationUI };