Source: papertools/select.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 {AnnotationUITool, AnnotationUIToolbarBase} from './annotationUITool.mjs';
import { paper } from '../paperjs.mjs';
import { makeFaIcon } from '../utils/faIcon.mjs';

/**
 * Represents the SelectTool class that extends the AnnotationUITool.
 * This tool allows users to select and manipulate GeoJSON feature items on the Paper.js project.
 * @class
 */
class SelectTool extends AnnotationUITool{
    /**
     * Creates an instance of SelectTool.
     * @constructor
     * @param {Object} paperScope - The Paper.js paper scope object.
     * @property {Object} ps - Reference to the Paper.js project scope.
     * @property {SelectToolbar} toolbarControl - Sets the toolbar control for the SelectTool.
     * @property {paper.Path.Rectangle} selectionRectangle - The selection rectangle used for area-based selection.
     * @property {paper.Path.Rectangle} sr2 - A second selection rectangle with a dashed border.
     * @description This tool provides the ability to select and manipulate GeoJSON feature items on the canvas. Users can select items by clicking
     * on them or by performing area-based selection through click-and-drag. It also emits selection-related events for interaction and provides
     * functions to retrieve selected items and check for the existence of GeoJSON feature items.
     */
    constructor(paperScope){
        super(paperScope);
        let self=this;
        this.ps = this.project.paperScope;
        this.setToolbarControl(new SelectToolbar(this));

        let selectionRectangle = new paper.Path.Rectangle({strokeWidth:1,rescale:{strokeWidth:1},strokeColor:'black'});
        let sr2 = new paper.Path.Rectangle({strokeWidth:1,dashArray:[10,10],rescale:{strokeWidth:1,dashArray:[10,10]},strokeColor:'white'});
        this.project.toolLayer.addChild(selectionRectangle);
        this.project.toolLayer.addChild(sr2);
        selectionRectangle.applyRescale();
        sr2.applyRescale();
        selectionRectangle.visible=false;
        sr2.visible=false;
        
        this.extensions.onActivate=function(){ 
            self.tool.onMouseMove = (ev)=>self.onMouseMove(ev);
        }    
        this.extensions.onDeactivate=function(shouldFinish){
            self.project.overlay.removeClass('selectable-layer');
            self.tool.onMouseMove = null;
        }
        this.tool.extensions.onKeyUp=function(ev){
            if(ev.key=='escape'){
                self.project.paperScope.findSelectedItems().forEach(item=>item.deselect());
            }
        }
       
        /**
         * Event handler for mouse up events.
         * @private
         * @param {Event} ev - The mouse up event.
         * @property {boolean} visible - Hide the selection rectangle.
         * @property {HitResult} hitResult - The result of the hit test to find the item under the mouse pointer.
         * @property {boolean} toggleSelection - Indicates whether the 'Control' or 'Meta' key was pressed during the event.
         * @property {HitResult[]} hitResults - An array of hit test results containing items found within the area.
         * @property {boolean} keepExistingSelection - Indicates whether the 'Control' or 'Meta' key was pressed during the event.
         * @property {Item[]} selectedItems - An array of selected items to be deselected.
         */        
        this.tool.onMouseUp=function(ev){
            selectionRectangle.visible=false;
            sr2.visible=false;
            if(ev.downPoint.subtract(ev.point).length==0){
                //not a click-and-drag, do element selection
                let hitResult = self.hitTestPoint(ev);
                hitResult && self._isItemSelectable(hitResult.item) && hitResult.item.toggle((ev.modifiers.control || ev.modifiers.meta));
                
            } else{
                //click and drag, do area-based selection
                let hitResults = self.hitTestArea(ev);
                let keepExistingSelection = (ev.modifiers.control || ev.modifiers.meta);
                if(!keepExistingSelection){
                    self.project.paperScope.findSelectedItems().forEach(item=>item.deselect());
                }
                hitResults.forEach(item=>item.select(true))
                //limit results to a single layer
                // hitResults.filter(item=>item.layer === hitResults[0].layer).forEach(item=>item.select(true))
            }
        }
        /**
         * Event handler for mouse drag events.
         * @private
         * @param {Event} ev - The mouse drag event.
         * @property {boolean} visible - Show the selection rectangle.
         * @property {Rectangle} r - The bounding rectangle of the selection area.
         */
        this.tool.onMouseDrag = function(ev){
            selectionRectangle.visible=true;
            sr2.visible=true;
            let r=new paper.Rectangle(ev.downPoint,ev.point);
            selectionRectangle.set({segments:[r.topLeft, r.topRight, r.bottomRight, r.bottomLeft]});
            sr2.set({segments:[r.topLeft, r.topRight, r.bottomRight, r.bottomLeft]});
            // console.log(selectionRectangle.visible, selectionRectangle.segments)
        }
    }
//   /**
//    * Gets the selected items that are GeoJSON features.
//    * This method retrieves all the items in the Paper.js project that are considered as GeoJSON features and are currently selected.
//    * @returns {Array<Object>} An array of selected items that are GeoJSON features.
//    */
//     getSelectedItems(){
//         return this.ps.project.selectedItems.filter(i=>i.isGeoJSONFeature);
//     }
  /**
   * Checks if there are any GeoJSON feature items in the project.
   * This method searches through all the items in the Paper.js project and determines if there are any GeoJSON feature items.
   * @returns {boolean} Returns true if there are GeoJSON feature items, false otherwise.
   */
    doAnnotationItemsExist(){
        return this.ps.project.getItems({match:i=>i.isGeoJSONFeature}).length>0; 
    }

  /**
   * Handles mouse movement events and emits selection-related events for items under the cursor.
   * When the mouse moves within the Paper.js project area, this method detects if it is over any item and triggers related selection events.
   * It updates the currently hovered item and layer, and applies a CSS class to the project's overlay for highlighting selectable layers.
   * @param {Object} ev - The mouse move event object containing information about the cursor position.
   */
    onMouseMove(ev){
        if(ev.item && this._isItemSelectable(ev.item)){
            if(this.currentItem != ev.item) (ev.item.emit('selection:mouseenter')||true) 
            if(this.currentLayer != ev.item.layer) ev.item.layer.emit('selection:mouseenter');
            this.currentItem = ev.item;
            this.currentLayer = this.currentItem.layer;
            this.project.overlay.addClass('selectable-layer')
        }
        else{
            this.currenItem && (this.currentItem.emit('selection:mouseleave',ev)||true) 
            this.currentLayer && this.currentLayer.emit('selection:mouseleave',ev);
            this.project.overlay.removeClass('selectable-layer')
            this.currentItem = null;
            this.currentLayer = null;
        }   
    }
    /**
     * Performs a hit test on a specific point and returns hit results for GeoJSON feature items.
     * This method performs a hit test on the provided point and filters the results to include only GeoJSON feature items.
     * It also adjusts the hit result if the initial hit is not on the GeoJSON feature itself, but on a child item.
     * @param {Object} ev - The mouse event object containing the point to perform the hit test on.
     * @returns {HitResult} The hit result object containing information about the hit test.
     */
    hitTestPoint(ev){
        let hitResult = this.ps.project.hitTest(ev.point,{
            fill:true,
            stroke:true,
            segments:true,
            tolerance:this.getTolerance(5),
            match:i=>i.item.isGeoJSONFeature || i.item.parent.isGeoJSONFeature,
        })
        if(hitResult && !hitResult.item.isGeoJSONFeature){
            hitResult.item = hitResult.item.parent;
        }
        return hitResult;
    }
    /**
     * Performs a hit test within an area and returns hit results for GeoJSON feature items.
     * This method performs a hit test within the provided area and returns hit results that include only GeoJSON feature items.
     * It supports options for testing against fully contained or overlapping items.
     * @param {Object} ev - The mouse event object containing the area for hit testing.
     * @param {boolean} [onlyFullyContained=false] - Flag to indicate if hit test should be performed only on fully contained items.
     * @returns {HitResult[]} An array of hit results containing GeoJSON feature items within the specified area.
     */
    hitTestArea(ev,onlyFullyContained){
        let options = {
            match:item=>item.isGeoJSONFeature,
        }
        let testRectangle=new paper.Rectangle(ev.point,ev.downPoint);
        if(onlyFullyContained){
            options.inside=testRectangle;
        }
        else{
            options.overlapping=testRectangle;
        }
        let hitResult = this.ps.project.getItems(options);
        return hitResult;
    }

    _isItemSelectable(item){
        return true;
        return (this.items.length==0) || (item.layer == this.targetLayer);
    }
}
export{SelectTool};


class SelectToolbar extends AnnotationUIToolbarBase{
    constructor(tool){
        super(tool);
        this.dropdown.classList.add('select-dropdown');
        
        const i = makeFaIcon('fa-arrow-pointer');
        this.button.configure(i,'Selection Tool');
        
        const s = document.createElement('div');
        s.setAttribute('data-active', 'select');
        this.dropdown.appendChild(s);
        const span = document.createElement('span');
        span.innerHTML = '(Ctrl)click to select items.';
        s.append(span);        
    }
    
    isEnabledForMode(mode){
        let itemsExist = this.tool.doAnnotationItemsExist();
        return itemsExist && [
            'default',
            'select',
            'multiselection',
            'Polygon',
            'MultiPolygon',
            'Point:Rectangle',
            'Point:Ellipse',
            'Point',
            'LineString',
            'MultiLineString',
            'GeometryColletion:Raster',
        ].includes(mode);
    }
    
}