Source: annotationtoolkit.mjs

/**
 * OpenSeadragon annotation 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-2023, 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 this project 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 { paper } from './paperjs.mjs';
import { AnnotationUI } from './annotationui.mjs';
import { PaperOverlay } from './paper-overlay.mjs';
import { AnnotationItemFactory } from './paperitems/annotationitem.mjs';
import { MultiPolygon } from './paperitems/multipolygon.mjs';
import { Placeholder } from './paperitems/placeholder.mjs';
import { Linestring } from './paperitems/linestring.mjs';
import { MultiLinestring } from './paperitems/multilinestring.mjs';
import { Raster } from './paperitems/raster.mjs';
import { Point } from './paperitems/point.mjs';
import { PointText } from './paperitems/pointtext.mjs';
import { Rectangle } from './paperitems/rectangle.mjs';
import { Ellipse } from './paperitems/ellipse.mjs';
import { cyrb53 } from './utils/hash.mjs';

//extend paper prototypes to add functionality
//property definitions

Object.defineProperty(paper.Item.prototype, 'displayName', displayNamePropertyDef());
Object.defineProperty(paper.Item.prototype, 'featureCollection', featureCollectionPropertyDef());
Object.defineProperty(paper.TextItem.prototype, 'content', textItemContentPropertyDef());
Object.defineProperty(paper.Project.prototype, 'descendants', descendantsDefProject());

//extend remove function to emit events for GeoJSON type annotation objects
let origRemove=paper.Item.prototype.remove;
paper.Item.prototype.remove=function(){
    const childrenToFireRemove = this.getItems({match: item=>item.isGeoJSONFeatureCollection});
    (this.isGeoJSONFeature || this.isGeoJSONFeatureCollection) && this.project.emit('item-removed',{item: this});
    childrenToFireRemove.forEach(fc => this.project.emit('item-removed', {item: fc}));
    origRemove.call(this);
    (this.isGeoJSONFeature || this.isGeoJSONFeatureCollection) && this.emit('removed',{item: this});
    childrenToFireRemove.forEach(fc => fc.emit('removed', {item: fc}));
}
//function definitions
paper.Group.prototype.insertChildren=getInsertChildrenDef();
paper.Color.prototype.toJSON = paper.Color.prototype.toCSS;//for saving/restoring colors as JSON
paper.Style.prototype.toJSON = styleToJSON;
paper.View.prototype.getImageData = paperViewGetImageData;
paper.PathItem.prototype.toCompoundPath = toCompoundPath;
paper.PathItem.prototype.applyBounds = applyBounds;
paper.Item.prototype.select = paperItemSelect;
paper.Item.prototype.deselect = paperItemDeselect;
paper.Item.prototype.toggle = paperItemToggle;
//to do: should these all be installed on project instead of scope?
paper.PaperScope.prototype.findSelectedNewItem = findSelectedNewItem;
paper.PaperScope.prototype.findSelectedItems = findSelectedItems;
paper.PaperScope.prototype.findSelectedItem = findSelectedItem;
paper.PaperScope.prototype.scaleByCurrentZoom = function (v) { return v / this.view.getZoom(); };
paper.PaperScope.prototype.getActiveTool = function(){ return this.tool ? this.tool._toolObject : null; }        


/**
 * A class for creating and managing annotation tools on an OpenSeadragon viewer.
 * @class 
 * @memberof OSDPaperjsAnnotation
 * @extends OpenSeadragon.EventSource
 */
class AnnotationToolkit extends OpenSeadragon.EventSource{
    /**
     * Create a new AnnotationToolkit instance.
     * @constructor
     * @param {OpenSeadragon.Viewer} openSeadragonViewer - The OpenSeadragon viewer object.
     * @param {object} [opts]
     * @param {object} [opts.addUI] a configuration object for the UI, if desired
     * @param {object} [opts.overlay] a PaperOverlay object to use
     * @param {object} [opts.destroyOnViewerClose] whether to destroy the toolkit and its overlay when the viewer closes
     * @param {object} [opts.cacheAnnotations] whether to keep annotations in memory for images which aren't currently open
     */
    constructor(openSeadragonViewer, opts = {}) {
        super();

        
        if(!opts){
            opts = {};
        }

        this._defaultOptions = {
            addUI: false,
            overlay: null,
            destroyOnViewerClose: false,
            cacheAnnotations: false,
        }
        this.options = Object.assign({}, this._defaultOptions, opts);
        
        this._defaultStyle = {
            fillColor: new paper.Color('white'),
            strokeColor: new paper.Color('black'),
            fillOpacity:1,
            strokeOpacity:1,
            strokeWidth: 1,
            rescale: {
                strokeWidth: 1
            }
        };
        this.viewer = openSeadragonViewer;
        
        // set up overlay. If one is passed in, use it. Otherwise, create one.
        if(this.options.overlay){
            if(this.options.overlay instanceof PaperOverlay){
                this.overlay = this.options.overlay;
            }
        } else {
            this.overlay = new PaperOverlay(this.viewer, {type: 'image'});
        }
        this.paperScope.project.defaultStyle = new paper.Style();
        this.paperScope.project.defaultStyle.set(this.defaultStyle);

        // set the overlay to auto rescale items
        this.overlay.autoRescaleItems(true);

        // optionally destroy the annotation toolkit when the viewer closes
        if(this.options.destroyOnViewerClose){
            this.viewer.addOnceHandler('close', ()=>this.destroy());
        }

        //bind a reference to this to the viewer and the paperScope, for convenient access
        this.viewer.annotationToolkit = this;
        this.paperScope.annotationToolkit = this;

        this.viewer.world.addHandler('add-item',ev=>{
            if(this.options.cacheAnnotations){
                this._loadCachedAnnotations(ev.item);
            }
        })
        this.viewer.world.addHandler('remove-item',ev=>{
            if(this.options.cacheAnnotations){
                this._cacheAnnotations(ev.item);
            }
        }, false, 1);


        //register item constructors
        AnnotationItemFactory.register(MultiPolygon);
        AnnotationItemFactory.register(Placeholder);
        AnnotationItemFactory.register(Linestring);
        AnnotationItemFactory.register(MultiLinestring);
        AnnotationItemFactory.register(Raster);
        AnnotationItemFactory.register(Point);
        AnnotationItemFactory.register(PointText);
        AnnotationItemFactory.register(Rectangle);
        AnnotationItemFactory.register(Ellipse);

        paper.Item.fromGeoJSON = AnnotationItemFactory.itemFromGeoJSON;
        paper.Item.fromAnnotationItem = AnnotationItemFactory.itemFromAnnotationItem;

        this._cached = {};

        if(this.options.addUI){
            let uiOpts = {}
            if(typeof opts.addUI === 'object'){
                uiOpts = this.options.addUI;
            }
            this.addAnnotationUI(uiOpts)
        }

    }

    /**
     * Get the default style for the annotation items.
     * 
     * @returns {object} The default style object.
     */
    get defaultStyle(){
        return this._defaultStyle;
    }
    
    /**
     * Get the default style for the annotation items.
     * 
     * @returns {object} The default style object.
     */
    get annotationUI(){
        return this._annotationUI;
    }


    /**
     * Get the paperScope associated with this toolkit
     * 
     * @returns {object} The paperScope object for this toolkit's PaperOverlay.
     */
    get paperScope(){
        return this.overlay.paperScope;
    }

    /**
     * Empty any cached annotations
     */
    clearCache(){
        this._cached = {};
    }

    /**
     * save the current feature collections to the cache
     * @param {TiledImage} tiledImage 
     * @private
     */
    _cacheAnnotations(tiledImage){
        try{
            const key = cyrb53(JSON.stringify(tiledImage.source));
            const featureCollections = tiledImage.paperLayer.getItems({match: item=>item.isGeoJSONFeatureCollection});
            this._cached[key] = featureCollections;
        } catch(e){
            console.error('Error with caching', e);
        }
    }

    _loadCachedAnnotations(tiledImage){
        try{
            const key = cyrb53(JSON.stringify(tiledImage.source));
            const featureCollections = this._cached[key] || [];
            for(const fcGroup of featureCollections){
                this._addFeatureCollectionGroupToLayer(fcGroup, tiledImage.paperLayer);
            }
        } catch(e){
            console.error('Error with fetching from cache', e);
        }
    }

    /**
     * Add an annotation UI to the toolkit.
     * 
     * @param {object} [opts={}] - The options for the annotation UI.
     * @returns {AnnotationUI} The annotation UI object.
     */
    addAnnotationUI(opts = {}){
        if (!this._annotationUI) this._annotationUI = new AnnotationUI(this, opts);
        return this._annotationUI;
    }
    /**
     * Destroy the toolkit and its components.
     */
    destroy() {
        this.raiseEvent('before-destroy');
        let tool=this.paperScope && this.paperScope.getActiveTool();
        if(tool) tool.deactivate(true);

        this.viewer.annotationToolkit = null;
        this._annotationUI && this._annotationUI.destroy();
        this.overlay.destroy();
        this.raiseEvent('destroy');
    }
    /**
     * Close the toolkit and remove its feature collections.
     */
    close() {
        this.raiseEvent('before-close');
        let tool=this.paperScope && this.paperScope.getActiveTool();
        if(tool) tool.deactivate(true);

        this.addFeatureCollections([],true);
    }
    /**
     * Set the global visibility of the toolkit.
     * @param {boolean} [show=false] - Whether to show or hide the toolkit.
     */
    setGlobalVisibility(show = false){
        this.paperScope.view._element.setAttribute('style', 'visibility:' + (show ? 'visible;' : 'hidden;'));
    }
    /**
     * Add feature collections to the toolkit from GeoJSON objects.
     * @param {object[]} featureCollections - The array of GeoJSON objects representing feature collections.
     * @param {boolean} replaceCurrent - Whether to replace the current feature collections or not.
     * @param {OpenSeadragon.TiledImage | OpenSeadragon.Viewport | false} [parentImage] - which image to add the feature collections to
     */
    addFeatureCollections(featureCollections,replaceCurrent, parentImage){
        this.loadGeoJSON(featureCollections,replaceCurrent, parentImage);
        this.overlay.rescaleItems();
        this.paperScope.project.emit('items-changed');
    }
    /**
     * Get the feature collection groups that the toolkit is managing.
     * @param {paper.Layer} [parentLayer]  The layer to find feature collections within. If not specified, finds across all layers.
     * @returns {paper.Group[]} The array of paper groups representing feature collections.
     */
    getFeatureCollectionGroups(parentLayer){
        // return this.overlay.paperScope.project.layers.filter(l=>l.isGeoJSONFeatureCollection);
        return this.paperScope.project.getItems({match: item=>item.isGeoJSONFeatureCollection && (parentLayer ? item.layer === parentLayer : true)});
    }
    /**
     * Get the features in the toolkit.
     * @returns {paper.Item[]} The array of paper item objects representing features.
     */
    getFeatures(){
        return this.paperScope.project.getItems({match:i=>i.isGeoJSONFeature});
    }
     /**
     * Register an item as a GeoJSONFeature that the toolkit should track
     * @param {paper.Item} item - The item to track as a geoJSONFeature
     */
    static registerFeature(item){
        item.isGeoJSONFeature = true;
    }
     /**
     * Register a group as a GeoJSONFeatureCollection that the toolkit should track
     * @param {paper.Group} group - The group to track as a geoJSONFeatureCollection
     */
    static registerFeatureCollection(group){
        group.isGeoJSONFeatureCollection = true;
    }

    /**
     * Convert the feature collections in the toolkit to GeoJSON objects.
     * @param {Object} [options] 
     * @param {Layer} [options.layer] The specific layer to use
     * @returns {Object[]} The array of GeoJSON objects representing feature collections.
     */
    toGeoJSON(options){
        const defaults = {
            layer:null,
        }
        options = Object.assign(defaults, options);

        const parent = options.layer || this.paperScope.project;
        //find all featureCollection items and convert to GeoJSON compatible structures
        return parent.getItems({match:i=>i.isGeoJSONFeatureCollection}).map(grp=>{
            
            let geoJSON = {
                type:'FeatureCollection',
                features: grp.descendants.filter(d=>d.annotationItem).map(d=>d.annotationItem.toGeoJSONFeature()),
                properties:{
                    defaultStyle: grp.defaultStyle.toJSON(),
                    userdata: grp.data.userdata,
                },
                label:grp.displayName,
            }
            
            return geoJSON;
        })
    }
    
    /**
     * Convert the feature collections in the project to a JSON string.
     * @param {function} [replacer] - The replacer function for JSON.stringify().
     * @param {number|string} [space] - The space argument for JSON.stringify().
     * @returns {string} The JSON string representing the feature collections.
     */
    toGeoJSONString(replacer,space){
        return JSON.stringify(this.toGeoJSON(),replacer,space);
    }
    /**
     * Load feature collections from GeoJSON objects and add them to the project.
     * @param {object[]} geoJSON - The array of GeoJSON objects representing feature collections.
     * @param {boolean} replaceCurrent - Whether to replace the current feature collections or not.
     * @param {OpenSeadragon.TiledImage | OpenSeadragon.Viewport | false} [parentImage] - Which image (or viewport) to add the object to
     * @param {boolean} [pixelCoordinates]
     */
    loadGeoJSON(geoJSON, replaceCurrent, parentImage){
        let parentLayer = parentImage ? parentImage.paperLayer : false;
        if(replaceCurrent){
            this.getFeatureCollectionGroups(parentImage).forEach(grp=>grp.remove());
        }
        if(!Array.isArray(geoJSON)){
            geoJSON = [geoJSON];
        }
        
        geoJSON.forEach(obj=>{
            if(obj.type=='FeatureCollection'){
                let group = this._createFeatureCollectionGroup({label: obj.label, parent: parentLayer});
                let props = (obj.properties || {});
                group.data.userdata = Object.assign({},props.userdata);
                group.defaultStyle.set(props.defaultStyle);
                obj.features.forEach(feature=>{
                    let item = paper.Item.fromGeoJSON(feature);
                    group.addChild(item);
                })
            }
            else{
                console.warn('GeoJSON object not loaded: wrong type. Only FeatureCollection objects are currently supported');
            }
        })
    }

    /**
     * Add a new, empty FeatureCollection with default label and parent
     * @returns {paper.Group} The paper group object representing the feature collection.
     */
    addEmptyFeatureCollectionGroup(){
        return this._createFeatureCollectionGroup();
    }
    
    /**
     * Create a new feature collection group in the project scope.
     * @private
     * @param {Object} [opts] - Object with fields label and parent
     * @returns {paper.Group} The paper group object representing the feature collection.
     */
    _createFeatureCollectionGroup(opts = {}) {
        let defaultOpts = {
            label:null,
            parent:null
        }
        opts = Object.assign({}, defaultOpts, opts);

        let displayLabel = opts.label;
        
        let parent = opts.parent;
        if(!parent){
            let numItems = this.viewer.world.getItemCount();
            if( numItems == 1){
                parent = this.viewer.world.getItemAt(0).paperLayer;
            } else if (numItems == 0){
                parent = this.viewer.viewport.paperLayer;
            } else {
                //TODO: Update the UI and associated APIs to allow selecting specific tiled images for multi-image use
                console.warn('Use of AnnotationToolkit with multi-image is not yet fully supported. All annotations will be added to the top-level tiled image.');
                parent = this.viewer.world.getItemAt(numItems - 1).paperLayer;
            }
        }
        if(!parent){
            console.error('Failed to create feature collection group: no parent could be found');
            return;
        }

        let grp = new paper.Group();
        this._addFeatureCollectionGroupToLayer(grp, parent);
        let grpNum = this.getFeatureCollectionGroups().length;
        grp.name = grp.displayName = displayLabel!==null ? displayLabel : `Annotation Group ${grpNum}`;
        grp.defaultStyle = new paper.Style(this.paperScope.project.defaultStyle);
        return grp;
    }

    _addFeatureCollectionGroupToLayer(fcGroup, layer){
        layer.addChild(fcGroup);
        AnnotationToolkit.registerFeatureCollection(fcGroup);
        this.paperScope.project.emit('feature-collection-added',{group:fcGroup});
        // re-insert children to trigger events
        if(fcGroup.children){
            fcGroup.insertChildren(0, fcGroup.children);
        }
    }

    /**
     * Make a placeholder annotation item
     * @param {Object} style - options (e.g strokeColor) to pass to the paper item
     */
    makePlaceholderItem(style){
        return new Placeholder(style);
    }
    
};

export {AnnotationToolkit as AnnotationToolkit};



// private functions

/**
 * Create a compound path from a path item.
 * @private
 * @returns {paper.CompoundPath} The compound path object.
 */
function toCompoundPath() {
    if (this.constructor !== paper.CompoundPath) {
        let np = new paper.CompoundPath({ children: [this], fillRule: 'evenodd' });
        np.selected = this.selected;
        this.selected = false;
        return np;
    }
    return this;
}
/**
 * Apply bounds to a path item.
 * @private
 * @param {paper.Item[]} boundingItems - The array of paper items to use as bounds.
 */
function applyBounds(boundingItems) {
    if (boundingItems.length == 0)
        return;
    let intersection;
    if (boundingItems.length == 1) {
        let bounds = boundingItems[0];
        intersection = bounds.intersect(this, { insert: false });
    }
    else if (boundingItems.length > 1) {
        let bounds = new paper.CompoundPath(boundingItems.map(b => b.clone().children).flat());
        intersection = bounds.intersect(this, { insert: false });
        bounds.remove();
    }
    if (this.children) {
        //compound path
        this.removeChildren();
        this.addChildren(intersection.children ? intersection.children : [intersection]);
    }
    else {
        //simple path
        this.segments = intersection.segments ? intersection.segments : intersection.firstChild.segments;
    }

}
/**
 * Select a paper item and emit events.
 * @private
 * @param {boolean} [keepOtherSelectedItems=false] - Whether to keep other selected items or not.
 */
function paperItemSelect(keepOtherSelectedItems) {
    if(!keepOtherSelectedItems){
        this.project._scope.findSelectedItems().forEach(item => item.deselect());
    }
    this.selected = true;
    this.emit('selected');
    this.project.emit('item-selected', { item: this });
}
/**
 * Deselect a paper item and emit events.
 * @private
 * @param {boolean} [keepOtherSelectedItems=false] - Whether to keep other selected items or not.
 */
function paperItemDeselect(keepOtherSelectedItems) {
    if(!keepOtherSelectedItems){
        this.project._scope.findSelectedItems().forEach(item => item.deselect(true));
        return;
    }
    this.selected = false;
    this.emit('deselected');
    this.project.emit('item-deselected', { item: this });
}
/**
 * Toggle the selection of a paper item and emit events.
 * @private
 * @param {boolean} [keepOtherSelectedItems=false] - Whether to keep other selected items or not.
 */
function paperItemToggle(keepOtherSelectedItems) {
    this.selected ? this.deselect(keepOtherSelectedItems) : this.select(keepOtherSelectedItems);
}

/**
 * Find the selected new item in the project scope.
 * @private
 * @returns {paper.Item} The selected new item, or null if none exists.
 */
function findSelectedNewItem() {
    //to do: change this to use type=='Feature' and geometry==null to match GeoJSON spec and AnnotationItemPlaceholder definition
    return this.project.getItems({ selected:true, match: function (i) { return i.isGeoJSONFeature && i.initializeGeoJSONFeature; } })[0];
}
/**
 * Find the selected items in the project scope.
 * @private
 * @returns {paper.Item[]} The array of selected items, or an empty array if none exists.
 */
function findSelectedItems() {
    return this.project.getItems({ selected: true, match: function (i) { return i.isGeoJSONFeature; } });
}
/**
 * Find the first selected item in the project scope.
 * @private
 * @returns {paper.Item} The first selected item, or null if none exists.
 */
function findSelectedItem() {
    return this.findSelectedItems()[0];
}



/**
 * Define the display name property for a paper item object.
 * The display name property defines the name used to identify a paper item object.
 * @private
 * @returns {object} The property descriptor object.
 * @property {function} set - The setter function for the display name property.
 *   @param {string} input - The display name value.
 * @property {function} get - The getter function for the display name property.
 *   @returns {string} The display name value.
 */
function displayNamePropertyDef(){
    return {
        set: function displayName(input){
            if(Array.isArray(input)){
                this._displayName = new String(input[0]);
                this._displayName.source=input[1];
            }
            else{
                this._displayName = input;
            }
            this.name = this._displayName;
            this.emit('display-name-changed',{displayName:this._displayName});
        },
        get: function displayName(){
            return this._displayName;
        }
    }
}

/**
 * Define the featureCollection property for a paper item object.
 * @private
 */
function featureCollectionPropertyDef(){
    return {
        get: function fc(){
            return this.hierarchy.filter(i=>i.isGeoJSONFeatureCollection)[0];
        }
    }
}


/**
 * Define the descendants property for a paper project object.
 * The descendants property represents all the descendants (layers and their children) of a paper project object.
 * @private
 * @returns {object} The property descriptor object.
 * @property {function} get - The getter function for the descendants property.
 *   @returns {paper.Item[]} The array of paper item objects representing the descendants.
 */
function descendantsDefProject(){
    return {
        get: function descendants(){
            // return this.layers ? this.layers.filter(layer=>layer.isGeoJSONFeatureCollection).map(child=>child.descendants).flat() : [this];
            return this.layers ? this.getItems({match: item=>item.isGeoJSONFeatureCollection}).map(child=>child.descendants).flat() : [this];
        }
    }
}

/**
 * Convert a paper style object to a JSON object.
 * @private
 * @returns {object} The JSON object representing the style.
 */
function styleToJSON(){
    let output={};
    Object.keys(this._values).forEach(key=>{
        output[key] = this[key];//invoke getter
    })
    return output;
}
/**
 * Get the image data of a paper view element.
 * @private
 * @returns {ImageData} The image data object of the view element.
 */
function paperViewGetImageData(){
    return this.element.getContext('2d').getImageData(0,0,this.element.width, this.element.height);
}

/**
 * Get the insert children method definition for a paper group object.
 * The insert children method emits events when children are added to the paper group object.
 * @private
 * @returns {function} The insert children method that emits events when children are added.
 */
function getInsertChildrenDef(){
    let origInsertChildren = paper.Group.prototype.insertChildren.original || paper.Group.prototype.insertChildren;
    function insertChildren(){ 
        let output = origInsertChildren.apply(this,arguments); 
        let index = arguments[0], children=Array.from(arguments[1]);
        children&&children.forEach((child,i)=>{
            if(child.isGeoJSONFeature){
                let idx = typeof index !== 'undefined' ? index+1 : -1; 
                this.emit('child-added',{item:child,index:idx});
            } 
        });
        return output;
    }
    insertChildren.original = origInsertChildren;
    return insertChildren;
}

/**
 * Define the fill opacity property for a paper style object.
 *  @private
 *  @returns {object} The property descriptor object with the following properties:
 * - get: A function that returns the text of the item.
 * - set: A function that sets the text of the item and causes the 'content-changed' event to be fired.
 */
function textItemContentPropertyDef(){
    let _set = paper.TextItem.prototype._setContent || Object.getOwnPropertyDescriptor(paper.TextItem.prototype, 'content').set;
    paper.TextItem.prototype._setContent = _set;
    return{
        get: function() {
            return this._content;
        },
        set: function(content) {
            _set.call(this, content);
            this.emit('content-changed');
        },
    }
}