/**
* 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 { AnnotationUIToolbarBase } from './annotationUITool.mjs';
import {PolygonTool} from './polygon.mjs';
import { paper } from '../paperjs.mjs';
import { makeFaIcon } from '../utils/faIcon.mjs';
/**
* The LinestringTool class extends the PolygonTool and provides functionality for creating and modifying linestrings.
* @extends PolygonTool
* @class
* @memberof OSDPaperjsAnnotation
*/
class LinestringTool extends PolygonTool{
/**
* The constructor initializes the LinestringTool by calling the base class (PolygonTool) constructor and sets up the necessary toolbar control (LinestringToolbar).
* @memberof OSDPaperjsAnnotation.LinestringTool
* @constructor
* @param {paper.PaperScope} paperScope - The Paper.js scope for the tool.
* @property {paper.Shape.Circle} cursor - The cursor representing the pen for drawing linestrings. It is a Paper.js Circle shape.
* @property {number} radius - The brush radius for drawing linestrings. This property controls the width of the linestring paths.
* @property {paper.Path} draggingSegment - The segment that is being dragged during the mouse drag event. It is a Paper.js Path representing the segment.
*/
constructor(paperScope){
super(paperScope);
let self = this;
let tool = this.tool;
this.setToolbarControl(new LinestringToolbar(this));
let lastClickTime=0;
let drawColor = new paper.Color('green');
let eraseColor= new paper.Color('red');
this.radius = 0;
this.cursor=new paper.Shape.Circle(new paper.Point(0,0),this.radius);
this.cursor.set({
strokeWidth:1,
strokeColor:'black',
fillColor:drawColor,
opacity:1,
visible:false,
});
self.project.toolLayer.addChild(this.cursor);
this.clickAction = 'startPath';
this.extensions.onActivate= ()=>{
this.cursor.radius = this.radius/this.project.getZoom();
this.cursor.strokeWidth=1/this.project.getZoom();
this.refreshCursorVisibility();
tool.minDistance=4/self.project.getZoom();
tool.maxDistance=10/self.project.getZoom();
}
this.extensions.onDeactivate = finished => {
this.cursor.visible=false;
if(finished){
this.finish();
}
}
tool.onMouseWheel = ev => {
ev.preventDefault();
ev.stopPropagation();
if(ev.deltaY==0) return;//ignore lateral "scrolls"
this.toolbarControl.updateBrushRadius({larger:ev.deltaY < 0});
}
}
/**
* Set the brush radius for the linestring tool.
* This function updates the brush radius used for drawing linestrings.
* The new radius is adjusted according to the current zoom level.
* @param {number} r - The new brush radius value to set.
*
*/
setRadius(r){
this.radius = r;
this.cursor.radius= r / this.project.getZoom();
}
onMouseDown(ev){
this.draggingSegment=null;
if(this.itemToCreate){
this.itemToCreate.initializeGeoJSONFeature('MultiLineString');
this.refreshItems();
this.startNewPath(ev);
return;
}
let hitResult = this.item?.hitTest(ev.point,{fill:false,stroke:false,segments:true,tolerance:this.getTolerance(5)})
if(hitResult){
//if erasing and hitResult is a segment, hitResult.segment.remove()
if(hitResult.type=='segment' && this.eraseMode){
hitResult.segment.remove();
}
//if hitResult is a segment and NOT erasing, save reference to hitResult.segment for dragging it
else if(hitResult.type=='segment'){
this.draggingSegment = hitResult.segment;
}
//if hitResult is a stroke, add a point (unless in erase mode):
else if(hitResult.type=='stroke' && !this.eraseMode){
let insertIndex = hitResult.location.index +1;
hitResult.item.insert(insertIndex, ev.point);
}
}
else{ //not drawing yet, but start now!
if(!this.eraseMode) this.startNewPath(ev);
}
}
onMouseMove(ev){
this.cursor.position=ev.original.point;
let hitResult = this.item?.hitTest(ev.point,{fill:false,stroke:false,segments:true,tolerance:this.getTolerance(5)})
if(hitResult){
let action = hitResult.type + (this.eraseMode ? '-erase' : '');
this.project.overlay.addClass('tool-action').setAttribute('data-tool-action',action);
this.clickAction = action;
}
else{
this.project.overlay.removeClass('tool-action').setAttribute('data-tool-action','');
this.clickAction = 'startPath';
}
this.refreshCursorVisibility();
}
onMouseDrag(ev){
this.cursor.position=ev.original.point;
PolygonTool.prototype.onMouseDrag.call(this, ev);
let dr = this.drawing();
dr && (dr.path.segments = this.simplifier.simplify(dr.path.segments.map(s=>s.point)));
}
onMouseUp(ev){
this.finishCurrentPath();
}
/**
* Start a new linestring path when the user clicks the mouse.
* This function initializes the creation of a new linestring path, sets up a drawing group to hold the path, and listens for user mouse events to add new points to the path.
* @function startNewPath
* @memberof OSDPaperjsAnnotation.LinestringTool#
* @param {paper.MouseEvent} ev - The mouse event containing the click information.
*/
startNewPath(ev){
this.finishCurrentPath();
this.drawingGroup.removeChildren();
this.drawingGroup.addChild(new paper.Path([ev.point]));
// this.drawing = {path:this.drawingGroup.lastChild, index: 1};
this.drawingGroup.visible=true;
this.drawingGroup.selected=true;
this.drawingGroup.selectedColor= this.eraseMode ? 'red' : null;
let path = this.drawing().path;
path.set({
strokeWidth:this.radius * 2 / this.targetLayer.scaling.x / this.project.getZoom(),
strokeColor:this.item.strokeColor,
strokeJoin: 'round',
strokeCap: 'round',
});
}
//override finishCurrentPath so it doesn't close the path
/**
* Finish the current linestring path when the user releases the mouse.
* This function finalizes the current linestring path by adding it to the main item and clears the drawing group.
* @function finishCurrentPath
* @memberof OSDPaperjsAnnotation.LinestringTool#
*/
finishCurrentPath(){
if(!this.drawing() || !this.item) return;
let newPath = this.drawing().path;
if(newPath.segments.length>1){
this.item.addChild(this.drawing().path);
}
this.drawingGroup.removeChildren();
}
refreshCursorVisibility(){
this.cursor.visible = !this.eraseMode && this.clickAction==='startPath';
}
}
export{LinestringTool};
/**
* The LinestringToolbar class extends the AnnotationUIToolbarBase and provides the toolbar controls for the LinestringTool.
* The constructor initializes the LinestringToolbar by calling the base class (AnnotationUIToolbarBase) constructor and sets up the necessary toolbar controls.
* @extends AnnotationUIToolbarBase
* @class
* @memberof OSDPaperjsAnnotation.LinestringTool
* @param {OSDPaperjsAnnotation.LinestringTool} linestringTool - The LinestringTool instance associated with the toolbar.
* @property {jQuery} rangeInput - The range input element for adjusting the brush radius in the toolbar.
* @property {jQuery} eraseButton - The erase button element in the toolbar for toggling erase mode.
*
*/
class LinestringToolbar extends AnnotationUIToolbarBase{
/**
* Create a new LinestringToolbar instance.
* The constructor initializes the LinestringToolbar by calling the base class (AnnotationUIToolbarBase) constructor and sets up the necessary toolbar controls.
* @constructor
* @param {OSDPaperjsAnnotation.LinestringTool} linestringTool - The LinestringTool instance associated with the toolbar.
*/
constructor(linestringTool){
super(linestringTool);
this.linestringTool = linestringTool;
const i = makeFaIcon('fa-pen-nib');
this.button.configure(i,'Linestring Tool');
const fdd = document.createElement('div');
fdd.classList.add('dropdown','linestring-toolbar');
fdd.setAttribute('data-tool','linestring');
this.dropdown.appendChild(fdd);
const label = document.createElement('label');
label.innerHTML = 'Set pen width:';
fdd.appendChild(label);
let defaultRadius=4;
this.rangeInput = document.createElement('input');
fdd.appendChild(this.rangeInput);
Object.assign(this.rangeInput, {type:'range', min:0.2, max:12, step:0.1, value:defaultRadius});
this.rangeInput.addEventListener('change', function(){
linestringTool.setRadius(this.value);
});
this.eraseButton = document.createElement('button');
fdd.appendChild(this.eraseButton);
this.eraseButton.innerHTML = 'Eraser';
this.eraseButton.setAttribute('data-action','erase');
this.eraseButton.addEventListener('click',function(){
let erasing = this.classList.toggle('active');
linestringTool.setEraseMode(erasing);
});
setTimeout(()=>linestringTool.setRadius(defaultRadius));
}
/**
* Update the brush radius based on the mouse wheel scroll direction.
* The updateBrushRadius function is called when the user scrolls the mouse wheel in the LinestringToolbar.
* It updates the brush radius value based on the direction of the mouse wheel scroll.
* If the larger property of the update object is true, it increases the brush radius.
* If the larger property of the update object is false, it decreases the brush radius.
* @function
* @param {Object} update - An object containing the update information.
* @param {boolean} update.larger - A boolean value indicating whether the brush radius should be increased or decreased.
*/
updateBrushRadius(update){
if(update.larger){
this.rangeInput.value = parseFloat(this.rangeInput.value)+parseFloat(this.rangeInput.step);
this.rangeInput.dispatchEvent(new Event('change'));
}
else{
this.rangeInput.value = parseFloat(this.rangeInput.value)-parseFloat(this.rangeInput.step);
this.rangeInput.dispatchEvent(new Event('change'));
}
}
/**
* Check if the LinestringTool should be enabled for the current mode.
* The isEnabledForMode function is called to determine if the LinestringTool should be enabled for the current mode.
* It returns true if the mode is 'new', 'LineString', or 'MultiLineString', and false otherwise.
* @function
* @param {string} mode - The current mode of the tool.
* @returns {boolean} - A boolean value indicating whether the LinestringTool should be enabled for the current mode.
*/
isEnabledForMode(mode){
return ['new','LineString','MultiLineString'].includes(mode);
}
/**
* Set the erase mode for the LinestringTool.
* The setEraseMode function is called when the user clicks the erase button in the LinestringToolbar.
* It sets the erase mode of the associated LinestringTool based on the value of the erasing parameter.
* If erasing is true, it enables the erase mode in the LinestringTool by adding the 'active' class to the erase button.
* If erasing is false, it disables the erase mode by removing the 'active' class from the erase button.
* @function
* @param {boolean} erasing - A boolean value indicating whether the erase mode should be enabled or disabled.
*/
setEraseMode(erasing){
erasing ? this.eraseButton.classList.add('active') : this.eraseButton.classList.remove('active');
this.linestringTool.refreshCursorVisibility();
}
}