<commit_before>/**
 * conjoon
 * (c) 2002-2009 siteartwork.de/conjoon.org
 * licensing@conjoon.org
 *
 * $Author$
 * $Id$
 * $Date$
 * $Revision$
 * $LastChangedDate$
 * $LastChangedBy$
 * $URL$
 */

Ext.namespace('com.conjoon.service.twitter');

/**
 * A custom implementation of  {com.conjoon.service.twitter.DataView} to render the
 * contents of a {com.conjoon.service.twitter.data.TweetStore}, i.e. rendering Tweets.
 *
 * @class com.conjoon.service.twitter.TweetList
 * @extends com.conjoon.service.twitter.DataView
 *
 * @author Thorsten Suckow-Homberg <ts@siteartwork.de>
 */
com.conjoon.service.twitter.TweetList = Ext.extend(com.conjoon.service.twitter.DataView, {

    /**
     * @cfg {com.conjoon.service.twitter.data.TweetStore} store The store
     * which data is rendered into a visual representation by this view.
     */

    /**
     * @type {com.conjoon.service.twitter.data.AccountRecord} _accountRecord
     * The Twitter-account for which this data was rendered, i.e. the user who
     * requested to view the tweets.
     */

    /**
     * @cfg {String} cls
     */
    cls : 'com-conjoon-service-twitter-TweetList',

    /**
     * @cfg {Boolean} multiSelect
     */
    multiSelect : false,

    /**
     * @cfg {Boolean} singleSelect
     */
    singleSelect : true,

    /**
     * @cfg {String} overClass
     */
    overClass : 'over',

    /**
     * @cfg {String} itemSelector
     */
    itemSelector : 'div.tweet',

    /**
     * Inits this component.
     *
     */
    initComponent : function()
    {
        if (!this.emptyText) {
            this.emptyText = com.conjoon.Gettext.gettext("No Tweets available");
        }

        var my = this;

        Ext.applyIf(this, {
            loadingText : com.conjoon.Gettext.gettext("Loading tweets..."),
            tpl         : new Ext.XTemplate('<tpl for=".">' +
                          '
'+
                          '
'+
                          '<tpl if="!this.isCurrentAccount(userId)">'+
                              '

'+
                          '</tpl>'+
                          '<tpl if="this.isCurrentAccount(userId)">'+
                              '

'+
                          '</tpl>'+

                          '<tpl if="favorited == 0">'+
                              '

'+
                          '</tpl>'+
                          '<tpl if="favorited != 0">'+
                              '

'+
                          '</tpl>'+

                          '
'+
                          ''+
                          '
'+
                          '{screenName}'+
                          '
'+
                          '
'+
                          '{text} '+
                          '{when}'+
                          ' from '+

                          '<tpl if="sourceUrl != 0">'+
                              '{source}'+
                          '</tpl>'+
                          '<tpl if="sourceUrl == 0">'+
                              '{source}'+
                          '</tpl>'+

                          '<tpl if="inReplyToScreenName != 0">'+
                              ' in reply to {inReplyToScreenName}'+
                          '</tpl>'+

                          ''+
                          '
'+
                          '

'+
                          '
'+
                          '</tpl>', {
                              isCurrentAccount : function(accountId) {
                                  if (!my._accountRecord) {
                                      return false;
                                  }
                                  return my._accountRecord.get('twitterId') == accountId;
                              }
                          })
        });

        com.conjoon.service.twitter.TweetList.superclass.initComponent.call(this);
    },

    /**
     * Function which can be overridden to provide custom formatting for each
     * Record that is used by this DataView's {@link #tpl template} to render
     * each node.
     * This implementation will take care of computing the posted time based
     * on the record's "createdAt" property and save the generated string in the
     * return value's property "when".
     * The text of the status message will also be parsed. If a username referenced
     * with "@" is found, this username will be replaced with a link.
     * The method will also take care of looking up urls in the text and wrap them
     * with a-tags, so the link is highlited and clickable.
     *
     * @param {Array/Object} data The raw data object that was used to create the
     * Record. Notice, that this is the original data of the record, thus a reference.
     * Changes to this object will be reflected in teh record itself.
     * @param {Number} recordIndex the index number of the Record being prepared
     * for rendering.
     * @param {Record} record The Record being prepared for rendering.
     *
     * @return {Array/Object} The formatted data in a format expected by the internal
     * {@link #tpl template}'s overwrite() method. (either an array if your params
     * are numeric (i.e. {0}) or an object (i.e. {foo: 'bar'}))
     */
    prepareData : function(data, recordIndex, record)
    {
        var when = this._getTimeString(record);

        // replace all usernames referenced with an "@" with appropriate links
        var text = record.get("text").replace(
            /@(.*?)(\.|\)|:|$|\s)/ig,
            '@$1$2'
        );

        text = com.conjoon.util.Format.formatUrls(text, {
            'class' : 'tweetUrl'
        });

        var f = Ext.apply({}, data);

        return Ext.apply(f, {
            when : when,
            text : text
        });
    },

    /**
     * Calls parent's implementation and adjusts the css-classes for the entris.
     *
     * @param {Ext.data.Store} ds
     * @param {Array} records
     * @param {Number} index
     */
    onAdd : function(ds, records, index)
    {
        com.conjoon.service.twitter.TweetList.superclass.onAdd.call(this, ds, records, index);

        this._updateRows();
    },

    /**
     * Called when an element gets removed from the store.
     * Highlights the element to remove, then calls its parents
     * implementation afterwards.
     *
     * @param {Ext.data.Store} ds
     * @param {Array} records
     * @param {Number} index
     */
    onRemove : function(ds, record, index)
    {
        var all = this.all;
        var el = all.item(index);

        this.deselect(index);

        el.fadeOut({
            endOpacity : 0,
            easing     : 'easeOut',
            duration   : .2,
            remove     : false,
            useDisplay : true,
            callback   : function() {
                com.conjoon.service.twitter.TweetList.superclass.onRemove.call(this, ds, record, index);
                this._updateRows();
            },
            scope  : this,
            // make sure all blocking fx get removed so we can remove this
            // element
            // when switching between tweet lists, removing an element in the view
            // without specifying this property might fail.
            // Example:
            // 1 remove this property
            // 2 go to Account A
            // 3 Add tweet X
            // 4 go to Account B
            // 5 switch back to Account A
            // 6 Delete tweet X
            // As of Ext 3.0.0, without specifying stopFx == true, the tweet won't
            // be removed from the view (though its clearly not available anymore in
            // the store)
            stopFx : true
        });

    },

    /**
     * Resets the css classes for the entries in this view. Should
     * be called whenever a record gets added or removed from the store.
     * Will also recalculate the time string, i.e. the entry when the
     * tweet was posted (see tpl "when" placeholder),
     *
     * @protected
     */
    _updateRows : function()
    {
        var i = 0;
        var all = this.all;
        var el = all.item(i);

        while(el) {
            el.removeClass('even');
            el.removeClass('odd');
            el.addClass((i % 2 === 0) ? 'even' : 'odd');
            i++;
            el = all.item(i);
        }
    },

    /**
     * Returns a textual representation of the time difference between
     * the date stored in the createdAt property of the passed tweetRecord
     * and the current time.
     *
     * @param {com.conjoon.service.twitter.data.TweetRecord} record
     *
     * @return {String}
     *
     * @protected
     */
    _getTimeString : function(record)
    {
        // compute the time difference
        var now     = Math.floor(((new Date()).getTime()/1000));
        var posted  = Math.floor((record.get('createdAt').getTime()/1000));
        var minutes = (now-posted)/60;
        var when    = "";

        switch (true) {
            // seconds
            case (minutes < 0.05):
                when = String.format(
                    com.conjoon.Gettext.gettext("less than 5 seconds ago"),
                    Math.floor(minutes)
                );
            break;

            // minutes
            case (minutes <= 60):
                if (minutes <= 0.5) {
                    when = com.conjoon.Gettext.gettext("half a minute ago");
                } else if (minutes < 2) {
                    when = com.conjoon.Gettext.gettext("1 minute ago");
                } else {
                    when = String.format(
                        com.conjoon.Gettext.gettext("{0} minutes ago"),
                        Math.floor(minutes)
                    );
                }
            break;

            // hours
            case (minutes < 1440):
                if (minutes < 120) {
                    when = com.conjoon.Gettext.gettext("about 1 hour ago");
                } else {
                    minutes = Math.floor(minutes/60);
                    when = String.format(
                        com.conjoon.Gettext.gettext("about {0} hours ago"),
                        minutes
                    );
                }
            break;

            // days
            case (minutes < 44640):
                if (minutes < 2880) {
                    when = com.conjoon.Gettext.gettext("1 day ago");
                } else {
                    minutes = Math.floor(minutes/1440);
                    when = String.format(
                        com.conjoon.Gettext.gettext("{0} days ago"),
                        minutes
                    );
                }
            break;

            // months
            case (minutes >= 44640):
                if (minutes < 89280) {
                    when = com.conjoon.Gettext.gettext("about 1 month ago");
                } else {
                    minutes = Math.floor(minutes/44640);
                    when = String.format(
                        com.conjoon.Gettext.gettext("about {0} months ago"),
                        minutes
                    );
                }
            break;
        }

        return when;
    },

// -------- public API

    /**
     * Forces this view to update some html nodes, but not to re-render itself.
     * Updated text will be for example the timestamp "when" in the template".
     * This method should be called from a controller that checks if the tweetPoller
     * triggered the updateempty event, so the contents can be refreshed in a
     * frequent interval.
     *
     * Developers note:
     * The implementation will be used in other methods in this class without calling
     * this method directly. If you change anything here, make sure you update/refactore
     * other implementation too.
     */
    updateMetaInfo : function()
    {
        var i = 0;
        var all = this.all;
        var el = all.item(i);

        while(el) {
            // returns a composite element
            /**
             * @todo think about performance enhancement
             */
            el.select('p span a.when').update(
                this._getTimeString(this.store.getAt(i))
            );

            i++;
            el = all.item(i);
        }
    },

    /**
     * Removes the tweetRecord with the specified id out of the store.
     *
     * @param {Number} id the Id of the tweet to remove.
     */
    removeTweet : function(id)
    {
        var rec = this.store.getById(id);

        if (!rec) {
            return;
        }

        this.store.remove(rec);
    },

    /**
     * Sets the account record for this tweet list.
     *
     * @param {com.conjoon.service.twitter.data.AccountRecord} accountRecord
     */
    setAccountRecord : function(accountRecord)
    {
        this._accountRecord = accountRecord;
    }

});<commit_msg> - fixed: enhanced screen name parser to properly create a link for "@SCREENNAME!" without including "!" (closes CN-290)<commit_after>/**
 * conjoon
 * (c) 2002-2009 siteartwork.de/conjoon.org
 * licensing@conjoon.org
 *
 * $Author$
 * $Id$
 * $Date$
 * $Revision$
 * $LastChangedDate$
 * $LastChangedBy$
 * $URL$
 */

Ext.namespace('com.conjoon.service.twitter');

/**
 * A custom implementation of  {com.conjoon.service.twitter.DataView} to render the
 * contents of a {com.conjoon.service.twitter.data.TweetStore}, i.e. rendering Tweets.
 *
 * @class com.conjoon.service.twitter.TweetList
 * @extends com.conjoon.service.twitter.DataView
 *
 * @author Thorsten Suckow-Homberg <ts@siteartwork.de>
 */
com.conjoon.service.twitter.TweetList = Ext.extend(com.conjoon.service.twitter.DataView, {

    /**
     * @cfg {com.conjoon.service.twitter.data.TweetStore} store The store
     * which data is rendered into a visual representation by this view.
     */

    /**
     * @type {com.conjoon.service.twitter.data.AccountRecord} _accountRecord
     * The Twitter-account for which this data was rendered, i.e. the user who
     * requested to view the tweets.
     */

    /**
     * @cfg {String} cls
     */
    cls : 'com-conjoon-service-twitter-TweetList',

    /**
     * @cfg {Boolean} multiSelect
     */
    multiSelect : false,

    /**
     * @cfg {Boolean} singleSelect
     */
    singleSelect : true,

    /**
     * @cfg {String} overClass
     */
    overClass : 'over',

    /**
     * @cfg {String} itemSelector
     */
    itemSelector : 'div.tweet',

    /**
     * Inits this component.
     *
     */
    initComponent : function()
    {
        if (!this.emptyText) {
            this.emptyText = com.conjoon.Gettext.gettext("No Tweets available");
        }

        var my = this;

        Ext.applyIf(this, {
            loadingText : com.conjoon.Gettext.gettext("Loading tweets..."),
            tpl         : new Ext.XTemplate('<tpl for=".">' +
                          '
'+
                          '
'+
                          '<tpl if="!this.isCurrentAccount(userId)">'+
                              '

'+
                          '</tpl>'+
                          '<tpl if="this.isCurrentAccount(userId)">'+
                              '

'+
                          '</tpl>'+

                          '<tpl if="favorited == 0">'+
                              '

'+
                          '</tpl>'+
                          '<tpl if="favorited != 0">'+
                              '

'+
                          '</tpl>'+

                          '
'+
                          ''+
                          '
'+
                          '{screenName}'+
                          '
'+
                          '
'+
                          '{text} '+
                          '{when}'+
                          ' from '+

                          '<tpl if="sourceUrl != 0">'+
                              '{source}'+
                          '</tpl>'+
                          '<tpl if="sourceUrl == 0">'+
                              '{source}'+
                          '</tpl>'+

                          '<tpl if="inReplyToScreenName != 0">'+
                              ' in reply to {inReplyToScreenName}'+
                          '</tpl>'+

                          ''+
                          '
'+
                          '

'+
                          '
'+
                          '</tpl>', {
                              isCurrentAccount : function(accountId) {
                                  if (!my._accountRecord) {
                                      return false;
                                  }
                                  return my._accountRecord.get('twitterId') == accountId;
                              }
                          })
        });

        com.conjoon.service.twitter.TweetList.superclass.initComponent.call(this);
    },

    /**
     * Function which can be overridden to provide custom formatting for each
     * Record that is used by this DataView's {@link #tpl template} to render
     * each node.
     * This implementation will take care of computing the posted time based
     * on the record's "createdAt" property and save the generated string in the
     * return value's property "when".
     * The text of the status message will also be parsed. If a username referenced
     * with "@" is found, this username will be replaced with a link.
     * The method will also take care of looking up urls in the text and wrap them
     * with a-tags, so the link is highlited and clickable.
     *
     * @param {Array/Object} data The raw data object that was used to create the
     * Record. Notice, that this is the original data of the record, thus a reference.
     * Changes to this object will be reflected in teh record itself.
     * @param {Number} recordIndex the index number of the Record being prepared
     * for rendering.
     * @param {Record} record The Record being prepared for rendering.
     *
     * @return {Array/Object} The formatted data in a format expected by the internal
     * {@link #tpl template}'s overwrite() method. (either an array if your params
     * are numeric (i.e. {0}) or an object (i.e. {foo: 'bar'}))
     */
    prepareData : function(data, recordIndex, record)
    {
        var when = this._getTimeString(record);

        // replace all usernames referenced with an "@" with appropriate links
        var text = record.get("text").replace(
            /@(.*?)(\.|\)|:|!|$|\s)/ig,
            '@$1$2'
        );

        text = com.conjoon.util.Format.formatUrls(text, {
            'class' : 'tweetUrl'
        });

        var f = Ext.apply({}, data);

        return Ext.apply(f, {
            when : when,
            text : text
        });
    },

    /**
     * Calls parent's implementation and adjusts the css-classes for the entris.
     *
     * @param {Ext.data.Store} ds
     * @param {Array} records
     * @param {Number} index
     */
    onAdd : function(ds, records, index)
    {
        com.conjoon.service.twitter.TweetList.superclass.onAdd.call(this, ds, records, index);

        this._updateRows();
    },

    /**
     * Called when an element gets removed from the store.
     * Highlights the element to remove, then calls its parents
     * implementation afterwards.
     *
     * @param {Ext.data.Store} ds
     * @param {Array} records
     * @param {Number} index
     */
    onRemove : function(ds, record, index)
    {
        var all = this.all;
        var el = all.item(index);

        this.deselect(index);

        el.fadeOut({
            endOpacity : 0,
            easing     : 'easeOut',
            duration   : .2,
            remove     : false,
            useDisplay : true,
            callback   : function() {
                com.conjoon.service.twitter.TweetList.superclass.onRemove.call(this, ds, record, index);
                this._updateRows();
            },
            scope  : this,
            // make sure all blocking fx get removed so we can remove this
            // element
            // when switching between tweet lists, removing an element in the view
            // without specifying this property might fail.
            // Example:
            // 1 remove this property
            // 2 go to Account A
            // 3 Add tweet X
            // 4 go to Account B
            // 5 switch back to Account A
            // 6 Delete tweet X
            // As of Ext 3.0.0, without specifying stopFx == true, the tweet won't
            // be removed from the view (though its clearly not available anymore in
            // the store)
            stopFx : true
        });

    },

    /**
     * Resets the css classes for the entries in this view. Should
     * be called whenever a record gets added or removed from the store.
     * Will also recalculate the time string, i.e. the entry when the
     * tweet was posted (see tpl "when" placeholder),
     *
     * @protected
     */
    _updateRows : function()
    {
        var i = 0;
        var all = this.all;
        var el = all.item(i);

        while(el) {
            el.removeClass('even');
            el.removeClass('odd');
            el.addClass((i % 2 === 0) ? 'even' : 'odd');
            i++;
            el = all.item(i);
        }
    },

    /**
     * Returns a textual representation of the time difference between
     * the date stored in the createdAt property of the passed tweetRecord
     * and the current time.
     *
     * @param {com.conjoon.service.twitter.data.TweetRecord} record
     *
     * @return {String}
     *
     * @protected
     */
    _getTimeString : function(record)
    {
        // compute the time difference
        var now     = Math.floor(((new Date()).getTime()/1000));
        var posted  = Math.floor((record.get('createdAt').getTime()/1000));
        var minutes = (now-posted)/60;
        var when    = "";

        switch (true) {
            // seconds
            case (minutes < 0.05):
                when = String.format(
                    com.conjoon.Gettext.gettext("less than 5 seconds ago"),
                    Math.floor(minutes)
                );
            break;

            // minutes
            case (minutes <= 60):
                if (minutes <= 0.5) {
                    when = com.conjoon.Gettext.gettext("half a minute ago");
                } else if (minutes < 2) {
                    when = com.conjoon.Gettext.gettext("1 minute ago");
                } else {
                    when = String.format(
                        com.conjoon.Gettext.gettext("{0} minutes ago"),
                        Math.floor(minutes)
                    );
                }
            break;

            // hours
            case (minutes < 1440):
                if (minutes < 120) {
                    when = com.conjoon.Gettext.gettext("about 1 hour ago");
                } else {
                    minutes = Math.floor(minutes/60);
                    when = String.format(
                        com.conjoon.Gettext.gettext("about {0} hours ago"),
                        minutes
                    );
                }
            break;

            // days
            case (minutes < 44640):
                if (minutes < 2880) {
                    when = com.conjoon.Gettext.gettext("1 day ago");
                } else {
                    minutes = Math.floor(minutes/1440);
                    when = String.format(
                        com.conjoon.Gettext.gettext("{0} days ago"),
                        minutes
                    );
                }
            break;

            // months
            case (minutes >= 44640):
                if (minutes < 89280) {
                    when = com.conjoon.Gettext.gettext("about 1 month ago");
                } else {
                    minutes = Math.floor(minutes/44640);
                    when = String.format(
                        com.conjoon.Gettext.gettext("about {0} months ago"),
                        minutes
                    );
                }
            break;
        }

        return when;
    },

// -------- public API

    /**
     * Forces this view to update some html nodes, but not to re-render itself.
     * Updated text will be for example the timestamp "when" in the template".
     * This method should be called from a controller that checks if the tweetPoller
     * triggered the updateempty event, so the contents can be refreshed in a
     * frequent interval.
     *
     * Developers note:
     * The implementation will be used in other methods in this class without calling
     * this method directly. If you change anything here, make sure you update/refactore
     * other implementation too.
     */
    updateMetaInfo : function()
    {
        var i = 0;
        var all = this.all;
        var el = all.item(i);

        while(el) {
            // returns a composite element
            /**
             * @todo think about performance enhancement
             */
            el.select('p span a.when').update(
                this._getTimeString(this.store.getAt(i))
            );

            i++;
            el = all.item(i);
        }
    },

    /**
     * Removes the tweetRecord with the specified id out of the store.
     *
     * @param {Number} id the Id of the tweet to remove.
     */
    removeTweet : function(id)
    {
        var rec = this.store.getById(id);

        if (!rec) {
            return;
        }

        this.store.remove(rec);
    },

    /**
     * Sets the account record for this tweet list.
     *
     * @param {com.conjoon.service.twitter.data.AccountRecord} accountRecord
     */
    setAccountRecord : function(accountRecord)
    {
        this._accountRecord = accountRecord;
    }

});<|endoftext|>


<commit_before>/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global define, $, brackets */

define(function (require, exports, module) {
    'use strict';

    var EditorManager       = brackets.getModule("editor/EditorManager"),
        QuickOpen           = brackets.getModule("search/QuickOpen"),
        DocumentManager     = brackets.getModule("document/DocumentManager");

   /**
    * FileLocation class
    * @constructor
    * @param {string} fullPath
    * @param {number} line
    * @param {number} chFrom column start position
    * @param {number} chTo column end position
    * @param {string} functionName
    */
    function FileLocation(fullPath, line, chFrom, chTo, functionName) {
        this.fullPath = fullPath;
        this.line = line;
        this.chFrom = chFrom;
        this.chTo = chTo;
        this.functionName = functionName;
    }

    /**
     * Contains a list of information about functions for a single document. This array is populated
     * by createFunctionList()
     * @type {?Array.<FileLocation>}
     */
    var functionList = null;

    function done() {
        functionList = null;
    }

    /**
     * Retrieves the FileLocation object stored in the functionsList for a given function name
     * @param {string} functionName
     * @returns {FileLocation}
     */
    function getLocationFromFunctionName(functionName) {
        if (!functionList) {
            return null;
        }

        // TODO: doesn't handle case where two functions have same name
        var i, fileLocation;
        for (i = 0; i < functionList.length; i++) {
            var functionInfo = functionList[i];
            if (functionInfo.functionName === functionName) {
                fileLocation = functionInfo;
                break;
            }
        }

        return fileLocation;
    }

    /**
     * Populates the functionList array
     */
    function createFunctionList() {
        var doc = DocumentManager.getCurrentDocument();
        if (!doc) {
            return;
        }

        if (!functionList) {
            functionList = [];
            var docText = doc.getText();

            // TODO: use a shared JS language intelligence module
            // TODO: this doesn't handle functions with params that spread across lines
            var regexA = new RegExp(/(function\b)(.+)\b\(.*?\)/gi);  // recognizes the form: function functionName()
            var regexB = new RegExp(/(\w+)\s*=\s*function\s*(\(.*?\))/gi); // recognizes the form: functionName = function()
            var regexC = new RegExp(/((\w+)\s*:\s*function\s*\(.*?\))/gi); // recognizes the form: functionName: function()
            var infoA, infoB, infoC, i, line;
            var funcName, chFrom, chTo;

            var lines = docText.split("\n");

            for (i = 0; i < lines.length; i++) {
                line = lines[i];

                infoA = regexA.exec(line);
                if (infoA) {
                    funcName = $.trim(infoA[0]);
                    chFrom = line.indexOf(funcName);
                    chTo = chFrom + funcName.length;
                    functionList.push(new FileLocation(null, i, chFrom, chTo, funcName));
                }

                infoB = regexB.exec(line);
                if (infoB) {
                    var pattern = $.trim(infoB[1]);
                    var params = infoB[2];
                    var dotIndex = pattern.lastIndexOf(".");
                    if (dotIndex !== -1) {
                        funcName = pattern.slice(dotIndex + 1, pattern.length);
                    } else {
                        funcName = pattern;
                    }

                    chFrom = line.indexOf(funcName);
                    chTo = chFrom + funcName.length;
                    functionList.push(new FileLocation(null, i, chFrom, chTo, funcName + params));
                }

                infoC = regexC.exec(line);
                if (infoC) {
                    funcName = $.trim(infoC[1]);
                    chFrom = line.indexOf(funcName);
                    chTo = chFrom + funcName.length;
                    functionList.push(new FileLocation(null, i, chFrom, chTo, funcName));
                }
            }
        }
    }

    /**
     * @param {string} query what the user is searching for
     * @returns {Array.<string>} sorted and filtered results that match the query
     */
    function search(query) {
        createFunctionList();

        query = query.slice(query.indexOf("@") + 1, query.length);
        var filteredList = $.map(functionList, function (itemInfo) {

            var functionName = itemInfo.functionName;
            if (functionName.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
                return functionName;
            }
        }).sort(function (a, b) {
            a = a.toLowerCase();
            b = b.toLowerCase();
            if (a > b) {
                return -1;
            } else if (a < b) {
                return 1;
            } else {
                return 0;
            }
        });

        return filteredList;
    }

    /**
     * @param {string} query what the user is searching for
     * @param {boolean} returns true if this plug-in wants to provide results for this query
     */
    function match(query) {
        // only match @ at beginning of query for now
        // TODO: match any location of @ when QuickOpen._handleItemFocus() is modified to
        // dynamic open files
        //if (query.indexOf("@") !== -1) {
        if (query.indexOf("@") === 0) {
            return true;
        }
    }

    /**
     * Select the selected item in the current document
     * @param {HTMLLIElement} selectedItem
     */
    function itemFocus(selectedItem) {
        var fileLocation = getLocationFromFunctionName($(selectedItem).text());

        if (fileLocation) {
            var from = {line: fileLocation.line, ch: fileLocation.chFrom};
            var to = {line: fileLocation.line, ch: fileLocation.chTo};
            EditorManager.getCurrentFullEditor().setSelection(from, to);
        }
    }

    /**
     * TODO: selectedItem is currently a <LI> item from smart auto complete container. It should just be data
     */
    function itemSelect(selectedItem) {
        itemFocus(selectedItem);
    }

    QuickOpen.addQuickOpenPlugin(
        {
            name: "JavaScript functions",
            fileTypes: ["js"],
            done: done,
            search: search,
            match: match,
            itemFocus: itemFocus,
            itemSelect: itemSelect,
            resultsFormatter: null // use default
        }
    );

});<commit_msg>changed lazy matches to use negative matching instead removed global flag since algorithm doesn't iterate through multiple matches per line<commit_after>/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global define, $, brackets */

define(function (require, exports, module) {
    'use strict';

    var EditorManager       = brackets.getModule("editor/EditorManager"),
        QuickOpen           = brackets.getModule("search/QuickOpen"),
        DocumentManager     = brackets.getModule("document/DocumentManager");

   /**
    * FileLocation class
    * @constructor
    * @param {string} fullPath
    * @param {number} line
    * @param {number} chFrom column start position
    * @param {number} chTo column end position
    * @param {string} functionName
    */
    function FileLocation(fullPath, line, chFrom, chTo, functionName) {
        this.fullPath = fullPath;
        this.line = line;
        this.chFrom = chFrom;
        this.chTo = chTo;
        this.functionName = functionName;
    }

    /**
     * Contains a list of information about functions for a single document. This array is populated
     * by createFunctionList()
     * @type {?Array.<FileLocation>}
     */
    var functionList = null;

    function done() {
        functionList = null;
    }

    /**
     * Retrieves the FileLocation object stored in the functionsList for a given function name
     * @param {string} functionName
     * @returns {FileLocation}
     */
    function getLocationFromFunctionName(functionName) {
        if (!functionList) {
            return null;
        }

        // TODO: doesn't handle case where two functions have same name
        var i, fileLocation;
        for (i = 0; i < functionList.length; i++) {
            var functionInfo = functionList[i];
            if (functionInfo.functionName === functionName) {
                fileLocation = functionInfo;
                break;
            }
        }

        return fileLocation;
    }

    /**
     * Populates the functionList array
     */
    function createFunctionList() {
        var doc = DocumentManager.getCurrentDocument();
        if (!doc) {
            return;
        }

        if (!functionList) {
            functionList = [];
            var docText = doc.getText();

            // TODO: use a shared JS language intelligence module
            // TODO: this doesn't handle functions with params that spread across lines
            //
            // Note, the global switch on the regex expressions below is NOT used because this code assumes
            // one function per line.  exec() is not called repeatedly for each line.
            var regexA = new RegExp(/(function\b)([^)]+)\b\([^)]*\)/i);  // recognizes the form: function functionName()
            var regexB = new RegExp(/(\w+)\s*=\s*function\s*(\([^)]*\))/i); // recognizes the form: functionName = function()
            var regexC = new RegExp(/((\w+)\s*:\s*function\s*\([^)]*\))/i); // recognizes the form: functionName: function()
            var infoA, infoB, infoC, i, line;
            var funcName, chFrom, chTo;

            var lines = docText.split("\n");

            for (i = 0; i < lines.length; i++) {
                line = lines[i];

                infoA = regexA.exec(line);
                if (infoA) {
                    funcName = $.trim(infoA[0]);
                    chFrom = line.indexOf(funcName);
                    chTo = chFrom + funcName.length;
                    functionList.push(new FileLocation(null, i, chFrom, chTo, funcName));
                }

                infoB = regexB.exec(line);
                if (infoB) {
                    var pattern = $.trim(infoB[1]);
                    var params = infoB[2];
                    var dotIndex = pattern.lastIndexOf(".");
                    if (dotIndex !== -1) {
                        funcName = pattern.slice(dotIndex + 1, pattern.length);
                    } else {
                        funcName = pattern;
                    }

                    chFrom = line.indexOf(funcName);
                    chTo = chFrom + funcName.length;
                    functionList.push(new FileLocation(null, i, chFrom, chTo, funcName + params));
                }

                infoC = regexC.exec(line);
                if (infoC) {
                    funcName = $.trim(infoC[1]);
                    chFrom = line.indexOf(funcName);
                    chTo = chFrom + funcName.length;
                    functionList.push(new FileLocation(null, i, chFrom, chTo, funcName));
                }
            }
        }
    }

    /**
     * @param {string} query what the user is searching for
     * @returns {Array.<string>} sorted and filtered results that match the query
     */
    function search(query) {
        createFunctionList();

        query = query.slice(query.indexOf("@") + 1, query.length);
        var filteredList = $.map(functionList, function (itemInfo) {

            var functionName = itemInfo.functionName;
            if (functionName.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
                return functionName;
            }
        }).sort(function (a, b) {
            a = a.toLowerCase();
            b = b.toLowerCase();
            if (a > b) {
                return -1;
            } else if (a < b) {
                return 1;
            } else {
                return 0;
            }
        });

        return filteredList;
    }

    /**
     * @param {string} query what the user is searching for
     * @param {boolean} returns true if this plug-in wants to provide results for this query
     */
    function match(query) {
        // only match @ at beginning of query for now
        // TODO: match any location of @ when QuickOpen._handleItemFocus() is modified to
        // dynamic open files
        //if (query.indexOf("@") !== -1) {
        if (query.indexOf("@") === 0) {
            return true;
        }
    }

    /**
     * Select the selected item in the current document
     * @param {HTMLLIElement} selectedItem
     */
    function itemFocus(selectedItem) {
        var fileLocation = getLocationFromFunctionName($(selectedItem).text());

        if (fileLocation) {
            var from = {line: fileLocation.line, ch: fileLocation.chFrom};
            var to = {line: fileLocation.line, ch: fileLocation.chTo};
            EditorManager.getCurrentFullEditor().setSelection(from, to);
        }
    }

    /**
     * TODO: selectedItem is currently a <LI> item from smart auto complete container. It should just be data
     */
    function itemSelect(selectedItem) {
        itemFocus(selectedItem);
    }

    QuickOpen.addQuickOpenPlugin(
        {
            name: "JavaScript functions",
            fileTypes: ["js"],
            done: done,
            search: search,
            match: match,
            itemFocus: itemFocus,
            itemSelect: itemSelect,
            resultsFormatter: null // use default
        }
    );

});<|endoftext|>


<commit_before>/*
 * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, describe, it, expect, beforeFirst, afterLast, beforeEach, afterEach, runs, brackets, waitsFor, waitsForDone, spyOn */

define(function (require, exports, module) {
	"use strict";

	var SpecRunnerUtils = brackets.getModule("spec/SpecRunnerUtils"),
		FileUtils       = brackets.getModule("file/FileUtils"),
		KeyEvent        = brackets.getModule("utils/KeyEvent"),
		_               = brackets.getModule("thirdparty/lodash");

	describe("Recent Projects", function () {
		var extensionPath = FileUtils.getNativeModuleDirectoryPath(module),
			testWindow,
			$,
			CommandManager,
			PreferencesManager;

		beforeFirst(function () {
			runs(function () {
				SpecRunnerUtils.createTestWindowAndRun(this, function (w) {
					testWindow = w;
					$ = testWindow.$;
					CommandManager  = testWindow.brackets.test.CommandManager;
					PreferencesManager = testWindow.brackets.test.PreferencesManager;
				});
			});
		});

		afterLast(function () {
			testWindow = null;
			SpecRunnerUtils.closeTestWindow();
		});

		function openRecentProjectDropDown() {
			var flag = false;
			CommandManager.execute("recentProjects.toggle");
			testWindow.setTimeout(function () {
				flag = true;
			}, 100);

			waitsFor(function () { return flag; });
		}

		function setupRecentProjectsSpy(howManyProjects) {
			spyOn(PreferencesManager, "getViewState").andCallFake(function (prefId) {
				if (prefId === "recentProjects") {
					// return howManyProjects recent projects
					return _.map(_.range(1, howManyProjects + 1), function (num) { return extensionPath + "/Test-Project-" + num; });
				} else {
					return [];
				}
			});
		}

		describe("UI", function () {
			it("should open the recent projects list with only the getting started project", function () {
				runs(function () {
					openRecentProjectDropDown();
				});

				runs(function () {
					var $dropDown = $("#project-dropdown");
					expect($("#project-dropdown").is(":visible")).toEqual(true);
					expect($dropDown.children().length).toEqual(1);
				});
			});

			it("should open the recent project list and show 5 recent projects", function () {
				setupRecentProjectsSpy(5);

				runs(function () {
					openRecentProjectDropDown();
				});

				runs(function () {
					var $dropDown = $("#project-dropdown");
					expect($dropDown.is(":visible")).toEqual(true);
					expect($dropDown.find(".recent-folder-link").length).toEqual(5);
				});
			});

			it("should delete one project from recent project list when delete key is pressed on", function () {
				setupRecentProjectsSpy(5);

				runs(function () {
					openRecentProjectDropDown();
				});

				runs(function () {
					var $dropDown = $("#project-dropdown");
					SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_DOWN, "keydown", $dropDown[0]);
					SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_DELETE, "keydown", $dropDown[0]);
				});

				runs(function () {
					var $dropDown = $("#project-dropdown");
					expect($dropDown.is(":visible")).toEqual(true);
					expect($dropDown.find(".recent-folder-link").length).toEqual(4);
				});
			});
		});
	});
});
<commit_msg>- converted all tabs to spaces (to have the same formatting like all files in the project)<commit_after>/*
 * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, describe, it, expect, beforeFirst, afterLast, beforeEach, afterEach, runs, brackets, waitsFor, waitsForDone, spyOn */

define(function (require, exports, module) {
    "use strict";

    var SpecRunnerUtils = brackets.getModule("spec/SpecRunnerUtils"),
        FileUtils       = brackets.getModule("file/FileUtils"),
        KeyEvent        = brackets.getModule("utils/KeyEvent"),
        _               = brackets.getModule("thirdparty/lodash");

    describe("Recent Projects", function () {
        var extensionPath = FileUtils.getNativeModuleDirectoryPath(module),
            testWindow,
            $,
            CommandManager,
            PreferencesManager;

        beforeFirst(function () {
            runs(function () {
                SpecRunnerUtils.createTestWindowAndRun(this, function (w) {
                    testWindow = w;
                    $ = testWindow.$;
                    CommandManager  = testWindow.brackets.test.CommandManager;
                    PreferencesManager = testWindow.brackets.test.PreferencesManager;
                });
            });
        });

        afterLast(function () {
            testWindow = null;
            SpecRunnerUtils.closeTestWindow();
        });

        function openRecentProjectDropDown() {
            var flag = false;
            CommandManager.execute("recentProjects.toggle");
            testWindow.setTimeout(function () {
                flag = true;
            }, 100);

            waitsFor(function () { return flag; });
        }

        function setupRecentProjectsSpy(howManyProjects) {
            spyOn(PreferencesManager, "getViewState").andCallFake(function (prefId) {
                if (prefId === "recentProjects") {
                    // return howManyProjects recent projects
                    return _.map(_.range(1, howManyProjects + 1), function (num) { return extensionPath + "/Test-Project-" + num; });
                } else {
                    return [];
                }
            });
        }

        describe("UI", function () {
            it("should open the recent projects list with only the getting started project", function () {
                runs(function () {
                    openRecentProjectDropDown();
                });

                runs(function () {
                    var $dropDown = $("#project-dropdown");
                    expect($("#project-dropdown").is(":visible")).toEqual(true);
                    expect($dropDown.children().length).toEqual(1);
                });
            });

            it("should open the recent project list and show 5 recent projects", function () {
                setupRecentProjectsSpy(5);

                runs(function () {
                    openRecentProjectDropDown();
                });

                runs(function () {
                    var $dropDown = $("#project-dropdown");
                    expect($dropDown.is(":visible")).toEqual(true);
                    expect($dropDown.find(".recent-folder-link").length).toEqual(5);
                });
            });

            it("should delete one project from recent project list when delete key is pressed on", function () {
                setupRecentProjectsSpy(5);

                runs(function () {
                    openRecentProjectDropDown();
                });

                runs(function () {
                    var $dropDown = $("#project-dropdown");
                    SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_DOWN, "keydown", $dropDown[0]);
                    SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_DELETE, "keydown", $dropDown[0]);
                });

                runs(function () {
                    var $dropDown = $("#project-dropdown");
                    expect($dropDown.is(":visible")).toEqual(true);
                    expect($dropDown.find(".recent-folder-link").length).toEqual(4);
                });
            });
        });
    });
});
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2019 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.nanos.crunch',
  name: 'UserCapabilityJunctionDAO',
  extends: 'foam.dao.ProxyDAO',

  documentation: `Authenticated DAO decorator to only show capabilities owned by a user. Updates can only be performed by system.`,

  javaImports: [
    'foam.core.FObject',
    'foam.dao.DAO',
    'foam.nanos.auth.*',
    'static foam.mlang.MLang.*'
  ],

  messages: [
    { name: 'ERROR_ONE', message: 'UserCapabilityJunctions should be disabled via status change.' },
    { name: 'ERROR_TWO', message: 'User\'s capability cannot be reassigned.' },
    { name: 'ERROR_THREE', message: 'Capability cannot be changed.' },
    { name: 'ERROR_FOUR', message: 'Capability data provided is not of the correct. CapabilityId: ' },
  ],

  methods: [
    {
      name: 'checkOwnership',
      args: [
        {
          name: 'x',
          type: 'Context'
        },
        {
          name: 'obj',
          type: 'foam.nanos.crunch.UserCapabilityJunction'
        }
      ],
      documentation: `Check if current user has permission to add this junction`,
      javaCode: `
        Subject subject = (Subject) x.get("subject");
        User user = subject.getUser();
        User realUser = subject.getRealUser();

        AuthService auth = (AuthService) x.get("auth");
        boolean isOwner = obj.getSourceId() == user.getId() || obj.getSourceId() == realUser.getId();
        if ( ! isOwner && ! auth.check(x, "usercapabilityjunction.read.*") ) throw new AuthorizationException();
      `
    },
    {
      name: 'getFilteredDAO',
      args: [
        {
          name: 'x',
          type: 'Context'
        }
      ],
      type: 'foam.dao.DAO',
      documentation: `Return list of junctions the current user has read access to`,
      javaCode: `
      Subject subject = (Subject) x.get("subject");
      User user = (User) subject.getUser();
      User realUser = (User) subject.getRealUser();

      AuthService auth = (AuthService) x.get("auth");
      if ( auth.check(x, "*") ) return getDelegate();
      return getDelegate().where(
        OR(
          EQ(UserCapabilityJunction.SOURCE_ID, user.getId()),
          EQ(UserCapabilityJunction.SOURCE_ID, realUser.getId())
        )
      );
      `
    },
    {
      name: 'remove_',
      javaCode: `
        throw new UnsupportedOperationException(this.ERROR_ONE);
      `
    },
    {
      name: 'removeAll_',
      javaCode: `
        throw new UnsupportedOperationException(this.ERROR_ONE);
      `
    },
    {
      name: 'select_',
      javaCode: `
        DAO dao = getFilteredDAO(x);
        return dao.select_(x, sink, skip, limit, order, predicate);
      `
    },
    {
      name: 'find_',
      javaCode:`
        FObject result = super.find_(x, id);
        if ( result != null ) checkOwnership(x, (UserCapabilityJunction) result);
        return result;
      `
    },
    {
      name: 'put_',
      javaCode: `
        UserCapabilityJunction ucj = (UserCapabilityJunction) obj;
        UserCapabilityJunction old = (UserCapabilityJunction) super.find_(x, ucj.getId());

        // do not allow updates to sourceId/targetId properties
        if ( old != null && ucj.getSourceId() != old.getSourceId() ) throw new RuntimeException(this.ERROR_TWO);
        if ( old != null && ! ucj.getTargetId().equals(old.getTargetId()) ) throw new RuntimeException(this.ERROR_THREE);

        // if ucj data is set but does not match expected data, do not put
        Capability capability = (Capability) ucj.findTargetId(x);
        if (
          capability == null ||
          (
            ! ( capability.getOf() == null || ucj.getData() == null ) &&
            ! ( ucj.getData().getClassInfo().equals(capability.getOf()) )
          )
        ) throw new RuntimeException(this.ERROR_FOUR + ucj.getTargetId());

        return super.put_(x, obj);
      `
    }
  ]
});
<commit_msg>Update src/foam/nanos/crunch/UserCapabilityJunctionDAO.js<commit_after>/**
 * @license
 * Copyright 2019 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.nanos.crunch',
  name: 'UserCapabilityJunctionDAO',
  extends: 'foam.dao.ProxyDAO',

  documentation: `Authenticated DAO decorator to only show capabilities owned by a user. Updates can only be performed by system.`,

  javaImports: [
    'foam.core.FObject',
    'foam.dao.DAO',
    'foam.nanos.auth.*',
    'static foam.mlang.MLang.*'
  ],

  messages: [
    { name: 'ERROR_ONE', message: 'UserCapabilityJunctions should be disabled via status change.' },
    { name: 'ERROR_TWO', message: 'User\'s capability cannot be reassigned.' },
    { name: 'ERROR_THREE', message: 'Capability cannot be changed.' },
    { name: 'ERROR_FOUR', message: 'Capability data provided is not of the correct type. CapabilityId: ' },
  ],

  methods: [
    {
      name: 'checkOwnership',
      args: [
        {
          name: 'x',
          type: 'Context'
        },
        {
          name: 'obj',
          type: 'foam.nanos.crunch.UserCapabilityJunction'
        }
      ],
      documentation: `Check if current user has permission to add this junction`,
      javaCode: `
        Subject subject = (Subject) x.get("subject");
        User user = subject.getUser();
        User realUser = subject.getRealUser();

        AuthService auth = (AuthService) x.get("auth");
        boolean isOwner = obj.getSourceId() == user.getId() || obj.getSourceId() == realUser.getId();
        if ( ! isOwner && ! auth.check(x, "usercapabilityjunction.read.*") ) throw new AuthorizationException();
      `
    },
    {
      name: 'getFilteredDAO',
      args: [
        {
          name: 'x',
          type: 'Context'
        }
      ],
      type: 'foam.dao.DAO',
      documentation: `Return list of junctions the current user has read access to`,
      javaCode: `
      Subject subject = (Subject) x.get("subject");
      User user = (User) subject.getUser();
      User realUser = (User) subject.getRealUser();

      AuthService auth = (AuthService) x.get("auth");
      if ( auth.check(x, "*") ) return getDelegate();
      return getDelegate().where(
        OR(
          EQ(UserCapabilityJunction.SOURCE_ID, user.getId()),
          EQ(UserCapabilityJunction.SOURCE_ID, realUser.getId())
        )
      );
      `
    },
    {
      name: 'remove_',
      javaCode: `
        throw new UnsupportedOperationException(this.ERROR_ONE);
      `
    },
    {
      name: 'removeAll_',
      javaCode: `
        throw new UnsupportedOperationException(this.ERROR_ONE);
      `
    },
    {
      name: 'select_',
      javaCode: `
        DAO dao = getFilteredDAO(x);
        return dao.select_(x, sink, skip, limit, order, predicate);
      `
    },
    {
      name: 'find_',
      javaCode:`
        FObject result = super.find_(x, id);
        if ( result != null ) checkOwnership(x, (UserCapabilityJunction) result);
        return result;
      `
    },
    {
      name: 'put_',
      javaCode: `
        UserCapabilityJunction ucj = (UserCapabilityJunction) obj;
        UserCapabilityJunction old = (UserCapabilityJunction) super.find_(x, ucj.getId());

        // do not allow updates to sourceId/targetId properties
        if ( old != null && ucj.getSourceId() != old.getSourceId() ) throw new RuntimeException(this.ERROR_TWO);
        if ( old != null && ! ucj.getTargetId().equals(old.getTargetId()) ) throw new RuntimeException(this.ERROR_THREE);

        // if ucj data is set but does not match expected data, do not put
        Capability capability = (Capability) ucj.findTargetId(x);
        if (
          capability == null ||
          (
            ! ( capability.getOf() == null || ucj.getData() == null ) &&
            ! ( ucj.getData().getClassInfo().equals(capability.getOf()) )
          )
        ) throw new RuntimeException(this.ERROR_FOUR + ucj.getTargetId());

        return super.put_(x, obj);
      `
    }
  ]
});
<|endoftext|>


<commit_before>import Renderer from "./renderer.js";
import {SVG} from "../../core/svg.js";
import FeatureBinarySearchTree from "../feature-binary-search-tree.js";
import GenomeBrowserUtils from "../genome-browser-utils.js";
import GenomeBrowserConstants from "../genome-browser-constants.js";

export default class AlignmentRenderer extends Renderer {

    render(data, options) {
        // Get data to render
        const coverage = data[0] || []; // Coverage data is in the first position
        const alignments = data[1] || null; // Alignments data is in the second position (if provided)

        // Define the height of the coverage track
        const regionSize = options.requestedRegion.end - options.requestedRegion.start + 1;
        const parentHeight = options.svgCanvasFeatures.parentElement.clientHeight;
        const coverageHeight = regionSize < this.config.alignmentsMaxRegionSize ? 50 : parentHeight;

        const coverageParent = SVG.addChild(options.svgCanvasFeatures, "g", {});
        const alignmentsParent = SVG.addChild(options.svgCanvasFeatures, "g", {});

        // Render coverage data
        coverage.forEach(item => {
            this.#renderCoverage(coverageParent, item, coverageHeight, options);
        });

        // Group and render alignments
        if (alignments && regionSize < this.config.alignmentsMaxRegionSize) {
            this.#groupAlignments(alignments, options).forEach(group => {
                this.#renderAlignments(alignmentsParent, group, coverageHeight, options);
            });
        }
    }

    #groupAlignments(reads, options) {
        const groups = {};

        // Filter reads to avoid duplications
        const nonDuplicatedReads = reads.filter(read => {
            return !options.renderedFeatures.has(read.id);
        });

        // Group only non duplicated reads
        nonDuplicatedReads.forEach(read => {
            const id = read.id;
            if (!groups[id]) {
                groups[id] = [read];
            } else {
                const newReadPosition = read.alignment.position.position;
                const storedReadPosition = groups[id][0].alignment.position.position;

                // FIXME: For some reason, the webservice is some times returning the exactly same read more than once.
                if (newReadPosition !== storedReadPosition) {
                    // Order the alignments to be rendered properly
                    if (newReadPosition > storedReadPosition) {
                        groups[id].push(read);
                    } else {
                        groups[id].unshift(read);
                    }
                }
            }

            // Mark this read as rendered
            options.renderedFeatures.add(read.id);
        });

        return Object.values(groups);
    }

    #analyseRead(feature) {
        const differences = [];
        const start = feature.alignment.position.position;

        const cigar = [];
        let relativePosition = 0;
        const insertions = [];

        (feature.alignment.cigar || []).forEach(item => {
            cigar.push(item.operationLength);
            const length = parseInt(item.operationLength);
            switch (item.operation) {
                case "CLIP_SOFT":
                    cigar.push("S");
                    break;
                case "ALIGNMENT_MATCH":
                    cigar.push("M");
                    relativePosition = relativePosition + length;
                    break;
                case "INSERT":
                    cigar.push("I");

                    // We put this here because it will be read to calculate the position of the mismatches
                    insertions.push({
                        pos: relativePosition,
                        length: length,
                    });
                    differences.push({
                        pos: relativePosition,
                        seq: feature.alignedSequence.slice(relativePosition, relativePosition + length),
                        op: "I",
                        length: length,
                    });
                    break;
                case "DELETE":
                    cigar.push("D");
                    differences.push({
                        pos: relativePosition,
                        op: "D",
                        length: length,
                    });
                    relativePosition = relativePosition + length;
                    break;
                case "SKIP":
                    cigar.push("N");
                    relativePosition = relativePosition + length;
                    break;
                default:
            }
        });

        // Assign feature information (cigar, start and end positions)
        Object.assign(feature, {
            cigar: cigar.join(""),
            start: start,
            end: start + relativePosition - 1,
        });

        if (feature.info?.["MD"]) {
            const md = feature.info["MD"][1];
            const matches = md.match(/([0-9]+)|([^0-9]+)/g);
            let position = 0;

            if (feature.alignment.cigar[0].operation === "CLIP_SOFT") {
                position = parseInt(feature.alignment.cigar[0].operationLength);
            }

            // This variable will contain the offset between the insertion and the position where the mismatch is
            // Imagine we have this sequence ACTGCT, and we have an insertion in position 3 and a mismatch in position 5
            // The mismatch will be in position 5 of the sequence, but located in position 4 when showed relative to the
            // reference genome.
            let offset = position;
            for (let i = 0; i < matches.length; i++) {
                if (i % 2 === 0) {
                    // Number
                    position += parseInt(matches[i]);
                } else {
                    if (insertions.length > 0) {
                        for (let j = 0; j < insertions.length; j++) {
                            if (insertions[j].pos < position) {
                                position += insertions[j].length;
                                offset += insertions[j].length;
                                insertions[j].pos = Infinity;
                            } else {
                                break;
                            }
                        }
                    }

                    // Not deletion
                    if (matches[i][0] !== "^") {
                        // Reference nucleotide
                        if (matches[i] === feature.alignedSequence[position]) {
                            console.error("Something strange happened. The mismatch matches the nucleotide of the reference genome?");
                        }

                        differences.push({
                            pos: position - offset,
                            seq: feature.alignedSequence[position],
                            quality: feature.alignedQuality?.[position] || 0,
                            op: "M",
                            length: 1,
                        });

                        position += 1;
                    } else {
                        // -1 because we should not count the ^
                        offset -= matches[i].length - 1;
                    }
                }
            }
        }

        return [differences, relativePosition];
    }

    #renderCoverage(group, coverage, height, options) {
        const start = parseInt(coverage.start);
        const end = parseInt(coverage.end);
        const pixelWidth = (end - start + 1) * options.pixelBase;
        const maximumValue = coverage.stats.max; // Math.max.apply(null, coverageList);
        const points = [];

        const startPoint = GenomeBrowserUtils.getFeatureX(start, options);
        const endPoint = GenomeBrowserUtils.getFeatureX(end, options);

        // First starting point
        points.push(`${startPoint},${height}`);

        if (maximumValue > 0) {
            const maxValueRatio = height / maximumValue;
            let prevCoverage = -1;
            let prevPosition = -1;

            (coverage.values || []).forEach((value, index) => {
                const pos = index * coverage.windowSize;

                if (value !== prevCoverage || index === coverage.values.length - 1) {
                    if (prevCoverage > -1 && prevPosition + 1 < index) {
                        // We need to add the previous position as well to make a flat line between positions with equal coverage
                        const prevX = GenomeBrowserUtils.getFeatureX(start + prevPosition * coverage.windowSize, options);
                        const prevY = height - (prevCoverage * maxValueRatio);
                        points.push(`${prevX},${prevY}`);
                    }
                    prevCoverage = value;
                    prevPosition = index;

                    const x = GenomeBrowserUtils.getFeatureX(start + pos, options);
                    const y = height - (value * maxValueRatio);
                    points.push(`${x},${y}`);
                }
            });
        }

        // Last point
        points.push(`${endPoint},${height}`);

        SVG.addChild(group, "polyline", {
            points: points.join(" "),
            stroke: "lightgrey",
            fill: "lightgrey",
            width: pixelWidth,
            height: height,
            cursor: "pointer",
        });
    }

    #renderDifferences(parent, differences, readStart, readHeight, rowY, options) {
        (differences || []).forEach(diff => {
            const start = GenomeBrowserUtils.getFeatureX(diff.pos + readStart, options);

            if (diff.op === "M") {
                const end = start + options.pixelBase;
                const color = GenomeBrowserConstants.SEQUENCE_COLORS[diff.seq];
                SVG.addChild(parent, "path", {
                    "d": `M${start} ${rowY} V${rowY + readHeight} H${end} V${rowY} H${start}`,
                    "stroke": color,
                    "stroke-width": 0.7,
                    "fill": color,
                    "fill-opacity": diff.quality < this.config.minMappingQuality ? 0.2 : 0.7,
                });
            } else if (diff.op === "I") {
                const text = SVG.addChild(parent, "text", {
                    y: rowY + readHeight,
                    x: start - (options.pixelBase / 2),
                    class: "ocb-font-ubuntumono ocb-font-size-15",
                    // textLength: diff.size,
                });
                text.textContent = "|";
            } else if (diff.op === "D") {
                const end = start + options.pixelBase * diff.length;
                SVG.addChild(parent, "path", {
                    "d": `M${start} ${rowY + (readHeight / 2)} H${end} H${start}`,
                    "stroke": "#000",
                    "stroke-width": 0.7,
                    "fill": "#000",
                    "fill-opacity": 0.5,
                });
            }
        });
    }

    #renderAlignments(parent, alignments, startHeight, options) {
        const differences = [];
        const starts = [];
        const ends = [];

        alignments.forEach(read => {
            const [diffs, length] = this.#analyseRead(read, options);
            differences.push(diffs);
            starts.push(read.alignment.position.position);
            ends.push(read.alignment.position.position + length - 1);
        });

        // transform to pixel position
        const alignmentStart = GenomeBrowserUtils.getFeatureX(Math.min.apply(null, starts), options);
        const alignmentEnd = GenomeBrowserUtils.getFeatureX(Math.max.apply(null, ends), options);

        const height = this.getValueFromConfig("height", []);
        const rowHeight = height + 5;
        let rowY = startHeight + 10;
        let fitted = false;

        while (!fitted) {
            if (!options.renderedArea[rowY]) {
                // eslint-disable-next-line no-param-reassign
                options.renderedArea[rowY] = new FeatureBinarySearchTree();
            }

            const foundRow = options.renderedArea[rowY].add({
                start: alignmentStart,
                end: alignmentEnd,
            });

            if (foundRow) {
                let prevAlignmentEnd = null;
                const connectorsPoints = [];

                alignments.forEach((read, index) => {
                    const strand = this.getValueFromConfig("strand", [read]) || "Forward";
                    const start = GenomeBrowserUtils.getFeatureX(starts[index], options);
                    const end = GenomeBrowserUtils.getFeatureX(ends[index], options);
                    let points = []; // To save read points

                    if (strand.toUpperCase() === "REVERSE") {
                        points = [
                            `M${start},${rowY + (height / 2)}`,
                            `L${start + 5},${rowY}`,
                            `H${end}`,
                            `V${rowY + height}`,
                            `H${start + 5}`,
                            "Z",
                        ];
                    } else {
                        points = [
                            `M${start},${rowY}`,
                            `H${end - 5}`,
                            `L${end},${rowY + (height / 2)}`,
                            `L${end - 5},${rowY + height}`,
                            `H${start}`,
                            "Z",
                        ];
                    }

                    // Get read attributes
                    const readColor = this.getValueFromConfig("color", [read, this.config.minMappingQuality]);
                    const readOpacity = this.getValueFromConfig("opacity", [read, this.config.minMappingQuality]);

                    // Render this read
                    const readElement = SVG.addChild(parent, "path", {
                        "d": points.join(" "),
                        "stroke": "black",
                        "stroke-width": 0.5,
                        "fill": readColor,
                        "fill-opacity": readOpacity,
                        "cursor": "pointer",
                    });

                    // Display read tooltip
                    $(readElement).qtip({
                        content: {
                            title: this.getValueFromConfig("tooltipTitle", [read]),
                            text: this.getValueFromConfig("tooltipText", [read]),
                        },
                        position: {
                            viewport: $(window),
                            target: "mouse",
                            adjust: {x: 25, y: 15},
                        },
                        style: {
                            width: true,
                            classes: `${this.config.toolTipfontClass} ui-tooltip ui-tooltip-shadow`,
                        },
                    });

                    // Render differences
                    if (options.regionSize < 1000) {
                        this.#renderDifferences(parent, differences[index], starts[index], height, rowY, options);
                    }

                    // Check for rendering a line connecting reads
                    // NOTE: the second condition is to avoud drawing the connector in overlapped reads
                    if (prevAlignmentEnd && prevAlignmentEnd < start) {
                        connectorsPoints.push(`M${prevAlignmentEnd} ${rowY + (height / 2)} H${start}`);
                    }
                    prevAlignmentEnd = end;
                });

                // Check for rendering connectors points
                if (connectorsPoints.length > 0) {
                    SVG.addChild(parent, "path", {
                        "d": connectorsPoints.join(" "),
                        "stroke": "black",
                        "stroke-width": 0.5,
                        "fill": "transparent",
                    });
                }

                // Read fittend
                fitted = true;
            }
            rowY = rowY + rowHeight;
        }
    }

    getDefaultConfig() {
        return {
            alignmentsMaxRegionSize: 50000,
            minMappingQuality: 20, // Reads with a mapping quality under 20 will have a transparency
            strand: GenomeBrowserUtils.alignmentStrandParser,
            // Displayed reads style
            color: "darkgrey",
            opacity: (read, minMappingQuality) => {
                return read.alignment.mappingQuality > minMappingQuality ? 1 : 0.5;
            },
            height: 10,
            tooltipTitle: GenomeBrowserUtils.alignmentTooltipTitleFormatter,
            tooltipText: GenomeBrowserUtils.alignmentTooltipTextFormatter,
        };
    }

}
<commit_msg>gb: use default alignments color formatter in alignments renderer #TASK-855<commit_after>import Renderer from "./renderer.js";
import {SVG} from "../../core/svg.js";
import FeatureBinarySearchTree from "../feature-binary-search-tree.js";
import GenomeBrowserUtils from "../genome-browser-utils.js";
import GenomeBrowserConstants from "../genome-browser-constants.js";

export default class AlignmentRenderer extends Renderer {

    render(data, options) {
        // Get data to render
        const coverage = data[0] || []; // Coverage data is in the first position
        const alignments = data[1] || null; // Alignments data is in the second position (if provided)

        // Define the height of the coverage track
        const regionSize = options.requestedRegion.end - options.requestedRegion.start + 1;
        const parentHeight = options.svgCanvasFeatures.parentElement.clientHeight;
        const coverageHeight = regionSize < this.config.alignmentsMaxRegionSize ? 50 : parentHeight;

        const coverageParent = SVG.addChild(options.svgCanvasFeatures, "g", {});
        const alignmentsParent = SVG.addChild(options.svgCanvasFeatures, "g", {});

        // Render coverage data
        coverage.forEach(item => {
            this.#renderCoverage(coverageParent, item, coverageHeight, options);
        });

        // Group and render alignments
        if (alignments && regionSize < this.config.alignmentsMaxRegionSize) {
            this.#groupAlignments(alignments, options).forEach(group => {
                this.#renderAlignments(alignmentsParent, group, coverageHeight, options);
            });
        }
    }

    #groupAlignments(reads, options) {
        const groups = {};

        // Filter reads to avoid duplications
        const nonDuplicatedReads = reads.filter(read => {
            return !options.renderedFeatures.has(read.id);
        });

        // Group only non duplicated reads
        nonDuplicatedReads.forEach(read => {
            const id = read.id;
            if (!groups[id]) {
                groups[id] = [read];
            } else {
                const newReadPosition = read.alignment.position.position;
                const storedReadPosition = groups[id][0].alignment.position.position;

                // FIXME: For some reason, the webservice is some times returning the exactly same read more than once.
                if (newReadPosition !== storedReadPosition) {
                    // Order the alignments to be rendered properly
                    if (newReadPosition > storedReadPosition) {
                        groups[id].push(read);
                    } else {
                        groups[id].unshift(read);
                    }
                }
            }

            // Mark this read as rendered
            options.renderedFeatures.add(read.id);
        });

        return Object.values(groups);
    }

    #analyseRead(feature) {
        const differences = [];
        const start = feature.alignment.position.position;

        const cigar = [];
        let relativePosition = 0;
        const insertions = [];

        (feature.alignment.cigar || []).forEach(item => {
            cigar.push(item.operationLength);
            const length = parseInt(item.operationLength);
            switch (item.operation) {
                case "CLIP_SOFT":
                    cigar.push("S");
                    break;
                case "ALIGNMENT_MATCH":
                    cigar.push("M");
                    relativePosition = relativePosition + length;
                    break;
                case "INSERT":
                    cigar.push("I");

                    // We put this here because it will be read to calculate the position of the mismatches
                    insertions.push({
                        pos: relativePosition,
                        length: length,
                    });
                    differences.push({
                        pos: relativePosition,
                        seq: feature.alignedSequence.slice(relativePosition, relativePosition + length),
                        op: "I",
                        length: length,
                    });
                    break;
                case "DELETE":
                    cigar.push("D");
                    differences.push({
                        pos: relativePosition,
                        op: "D",
                        length: length,
                    });
                    relativePosition = relativePosition + length;
                    break;
                case "SKIP":
                    cigar.push("N");
                    relativePosition = relativePosition + length;
                    break;
                default:
            }
        });

        // Assign feature information (cigar, start and end positions)
        Object.assign(feature, {
            cigar: cigar.join(""),
            start: start,
            end: start + relativePosition - 1,
        });

        if (feature.info?.["MD"]) {
            const md = feature.info["MD"][1];
            const matches = md.match(/([0-9]+)|([^0-9]+)/g);
            let position = 0;

            if (feature.alignment.cigar[0].operation === "CLIP_SOFT") {
                position = parseInt(feature.alignment.cigar[0].operationLength);
            }

            // This variable will contain the offset between the insertion and the position where the mismatch is
            // Imagine we have this sequence ACTGCT, and we have an insertion in position 3 and a mismatch in position 5
            // The mismatch will be in position 5 of the sequence, but located in position 4 when showed relative to the
            // reference genome.
            let offset = position;
            for (let i = 0; i < matches.length; i++) {
                if (i % 2 === 0) {
                    // Number
                    position += parseInt(matches[i]);
                } else {
                    if (insertions.length > 0) {
                        for (let j = 0; j < insertions.length; j++) {
                            if (insertions[j].pos < position) {
                                position += insertions[j].length;
                                offset += insertions[j].length;
                                insertions[j].pos = Infinity;
                            } else {
                                break;
                            }
                        }
                    }

                    // Not deletion
                    if (matches[i][0] !== "^") {
                        // Reference nucleotide
                        if (matches[i] === feature.alignedSequence[position]) {
                            console.error("Something strange happened. The mismatch matches the nucleotide of the reference genome?");
                        }

                        differences.push({
                            pos: position - offset,
                            seq: feature.alignedSequence[position],
                            quality: feature.alignedQuality?.[position] || 0,
                            op: "M",
                            length: 1,
                        });

                        position += 1;
                    } else {
                        // -1 because we should not count the ^
                        offset -= matches[i].length - 1;
                    }
                }
            }
        }

        return [differences, relativePosition];
    }

    #renderCoverage(group, coverage, height, options) {
        const start = parseInt(coverage.start);
        const end = parseInt(coverage.end);
        const pixelWidth = (end - start + 1) * options.pixelBase;
        const maximumValue = coverage.stats.max; // Math.max.apply(null, coverageList);
        const points = [];

        const startPoint = GenomeBrowserUtils.getFeatureX(start, options);
        const endPoint = GenomeBrowserUtils.getFeatureX(end, options);

        // First starting point
        points.push(`${startPoint},${height}`);

        if (maximumValue > 0) {
            const maxValueRatio = height / maximumValue;
            let prevCoverage = -1;
            let prevPosition = -1;

            (coverage.values || []).forEach((value, index) => {
                const pos = index * coverage.windowSize;

                if (value !== prevCoverage || index === coverage.values.length - 1) {
                    if (prevCoverage > -1 && prevPosition + 1 < index) {
                        // We need to add the previous position as well to make a flat line between positions with equal coverage
                        const prevX = GenomeBrowserUtils.getFeatureX(start + prevPosition * coverage.windowSize, options);
                        const prevY = height - (prevCoverage * maxValueRatio);
                        points.push(`${prevX},${prevY}`);
                    }
                    prevCoverage = value;
                    prevPosition = index;

                    const x = GenomeBrowserUtils.getFeatureX(start + pos, options);
                    const y = height - (value * maxValueRatio);
                    points.push(`${x},${y}`);
                }
            });
        }

        // Last point
        points.push(`${endPoint},${height}`);

        SVG.addChild(group, "polyline", {
            points: points.join(" "),
            stroke: "lightgrey",
            fill: "lightgrey",
            width: pixelWidth,
            height: height,
            cursor: "pointer",
        });
    }

    #renderDifferences(parent, differences, readStart, readHeight, rowY, options) {
        (differences || []).forEach(diff => {
            const start = GenomeBrowserUtils.getFeatureX(diff.pos + readStart, options);

            if (diff.op === "M") {
                const end = start + options.pixelBase;
                const color = GenomeBrowserConstants.SEQUENCE_COLORS[diff.seq];
                SVG.addChild(parent, "path", {
                    "d": `M${start} ${rowY} V${rowY + readHeight} H${end} V${rowY} H${start}`,
                    "stroke": color,
                    "stroke-width": 0.9,
                    "fill": color,
                    "fill-opacity": diff.quality < this.config.minMappingQuality ? 0.3 : 0.7,
                });
            } else if (diff.op === "I") {
                const text = SVG.addChild(parent, "text", {
                    y: rowY + readHeight,
                    x: start - (options.pixelBase / 2),
                    class: "ocb-font-ubuntumono ocb-font-size-15",
                    // textLength: diff.size,
                });
                text.textContent = "|";
            } else if (diff.op === "D") {
                const end = start + options.pixelBase * diff.length;
                SVG.addChild(parent, "path", {
                    "d": `M${start} ${rowY + (readHeight / 2)} H${end} H${start}`,
                    "stroke": "#000",
                    "stroke-width": 0.7,
                    "fill": "#000",
                    "fill-opacity": 0.5,
                });
            }
        });
    }

    #renderAlignments(parent, alignments, startHeight, options) {
        const differences = [];
        const starts = [];
        const ends = [];

        alignments.forEach(read => {
            const [diffs, length] = this.#analyseRead(read, options);
            differences.push(diffs);
            starts.push(read.alignment.position.position);
            ends.push(read.alignment.position.position + length - 1);
        });

        // transform to pixel position
        const alignmentStart = GenomeBrowserUtils.getFeatureX(Math.min.apply(null, starts), options);
        const alignmentEnd = GenomeBrowserUtils.getFeatureX(Math.max.apply(null, ends), options);

        const height = this.getValueFromConfig("height", []);
        const rowHeight = height + 5;
        let rowY = startHeight + 10;
        let fitted = false;

        while (!fitted) {
            if (!options.renderedArea[rowY]) {
                // eslint-disable-next-line no-param-reassign
                options.renderedArea[rowY] = new FeatureBinarySearchTree();
            }

            const foundRow = options.renderedArea[rowY].add({
                start: alignmentStart,
                end: alignmentEnd,
            });

            if (foundRow) {
                let prevAlignmentEnd = null;
                const connectorsPoints = [];

                alignments.forEach((read, index) => {
                    const strand = this.getValueFromConfig("strand", [read]) || "Forward";
                    const start = GenomeBrowserUtils.getFeatureX(starts[index], options);
                    const end = GenomeBrowserUtils.getFeatureX(ends[index], options);
                    let points = []; // To save read points

                    if (strand.toUpperCase() === "REVERSE") {
                        points = [
                            `M${start},${rowY + (height / 2)}`,
                            `L${start + 5},${rowY}`,
                            `H${end}`,
                            `V${rowY + height}`,
                            `H${start + 5}`,
                            "Z",
                        ];
                    } else {
                        points = [
                            `M${start},${rowY}`,
                            `H${end - 5}`,
                            `L${end},${rowY + (height / 2)}`,
                            `L${end - 5},${rowY + height}`,
                            `H${start}`,
                            "Z",
                        ];
                    }

                    // Get read attributes
                    const readColor = this.getValueFromConfig("color", [read, alignments, this.config.minMappingQuality]);
                    const readOpacity = this.getValueFromConfig("opacity", [read, this.config.minMappingQuality]);

                    // Render this read
                    const readElement = SVG.addChild(parent, "path", {
                        "d": points.join(" "),
                        "stroke": "black",
                        "stroke-width": 0.5,
                        "fill": readColor,
                        "fill-opacity": readOpacity,
                        "cursor": "pointer",
                    });

                    // Display read tooltip
                    $(readElement).qtip({
                        content: {
                            title: this.getValueFromConfig("tooltipTitle", [read]),
                            text: this.getValueFromConfig("tooltipText", [read]),
                        },
                        position: {
                            viewport: $(window),
                            target: "mouse",
                            adjust: {x: 10, y: 10},
                        },
                        style: {
                            width: true,
                            classes: `${this.config.toolTipfontClass} ui-tooltip ui-tooltip-shadow`,
                        },
                    });

                    // Render differences
                    if (options.regionSize < 1000) {
                        this.#renderDifferences(parent, differences[index], starts[index], height, rowY, options);
                    }

                    // Check for rendering a line connecting reads
                    // NOTE: the second condition is to avoud drawing the connector in overlapped reads
                    if (prevAlignmentEnd && prevAlignmentEnd < start) {
                        connectorsPoints.push(`M${prevAlignmentEnd} ${rowY + (height / 2)} H${start}`);
                    }
                    prevAlignmentEnd = end;
                });

                // Check for rendering connectors points
                if (connectorsPoints.length > 0) {
                    SVG.addChild(parent, "path", {
                        "d": connectorsPoints.join(" "),
                        "stroke": "black",
                        "stroke-width": 0.5,
                        "fill": "transparent",
                    });
                }

                // Read fittend
                fitted = true;
            }
            rowY = rowY + rowHeight;
        }
    }

    getDefaultConfig() {
        return {
            alignmentsMaxRegionSize: 50000,
            minMappingQuality: 20, // Reads with a mapping quality under 20 will have a transparency
            strand: GenomeBrowserUtils.alignmentStrandParser,
            color: GenomeBrowserUtils.alignmentColorFormatter,
            opacity: (read, minMappingQuality) => {
                return read.alignment.mappingQuality > minMappingQuality ? 0.7 : 0.3;
            },
            height: 10,
            tooltipTitle: GenomeBrowserUtils.alignmentTooltipTitleFormatter,
            tooltipText: GenomeBrowserUtils.alignmentTooltipTextFormatter,
        };
    }

}
<|endoftext|>