<commit_before>/*
  Map browser based on OpenLayers 5.
  Popup windows

  Copyright (C) 2017-2019 Øyvind Hanssen, LA7ECA, ohanssen@acm.org

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU Affero General Public License as published
  by the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public License
  along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

var isMobile = false;

/**
 * @classdesc
 * Popup window manager class.
 *
 * @constructor
 * @param {pol.core.MapBrowser} mb - Map browser instance.
 */

pol.core.Popup = class {

    constructor(mb) {
        this.mb            = mb;
        this.onDiv         = document.getElementById("map");
        this.activepopup   = null;
        this.psubdiv       = null;
        this.allowedPopups = 1;
        this.onCallback    = null;
        this.offCallback   = null;
        this.geoPos        = false;
        this.image         = null;
        const t = this;

        this.mb.view.on('change:center', onChangeCenter);
        this.mb.map.on('click', onClick);
        this.mb.map.on('change:view', onChangeView);

        function onChangeCenter() {
           if (t.geoPos && t.geoPos != null) t.setPositionGeo(t.geoPos);
        }

        function onClick() {
           t.removePopup();
        }

        function onChangeView() {
           t.removePopup();
           t.mb.view.on('change:center', onChangeCenter);
           // FIXME: Unregister handler on previous view?
        }
    } /* constructor */

    /**
     * Deactivate popup.
     */
    removePopup() {
        if (this.activepopup == null)
            return;
        if (this.offCallback != null)
            this.offCallback();
        this.isMenu = false;
        this.allowedPopups++;

        this.activepopup.style.display = "none" ;
        this.activepopup.parentNode.removeChild(this.activepopup);
        this.activepopup = null;
        this.geoPos = null;
    }

    /**
     * Return true if popup is active.
     * @returns {boolean}
     */
    popupActive()
       { return (this.activepopup != null); }

    /**
     * Register callback functions
     * For popup activation (on) and deactivation (off)
     * @param {function} on - Callback to be invoked when popup is activated.
     * @param {function} off - Callback to be invoked when popup is deactivated.
     */
    onPopup(on, off)
       { this.onCallback = on; this.offCallback = off; }

    /**
     * Activate popup.
     * @param {Object.<string,*>} props - Options
     * @param {string|undefined} props.html - HTML code to render inside the popup.
     * @param {ol.Pixel|undefined} props.pixPos - Pixel position of upper left corner of popup.
     * @param {ol.Coordinate|undefined} props.geoPos - LatLong position of upper left corner of popup.
     * @param {boolean|undefined} props.image - true if we want a cross to be displayed at the position.
     * @param {boolean|undefined} props.draggable - true if we want the popup to be draggable and pinnable.
     * @param {string|undefined} props.id - unique identifier (used as id of element).
     * @param {function|undefined} props.onclose - handler to be called when window closes.
     *
     */
    showPopup(props)
    {
        const t = this;
        let x, y;
        if (props.id && props.id != null && document.getElementById(props.id) != null) {
            $('#'+props.id).effect('bounce');
            return null;
        }

        let pdiv = ((props.elem && props.elem!=null)
            ? props.elem : document.createElement('div'));
        if (props.html)
            pdiv.innerHTML = props.html;
        if (props.vnode)
            m.mount(pdiv, props.vnode);

        if (props.id && props.id != null) pdiv.id = props.id;

        pdiv.className = 'POPUP' +
           ((props.cclass && props.cclass != null) ? " "+props.cclass : "");

            if (props.geoPos && props.geoPos != null) {
                t.geoPos = props.geoPos;
                props.pixPos = t.mb.map.getPixelFromCoordinate
                    (ol.proj.fromLonLat(props.geoPos, t.mb.view.getProjection()));
            }
            else
                t.geoPos = null;

            if (props.pixPos && props.pixPos != null)
                { x = props.pixPos[0]; y=props.pixPos[1]; }
            t.popup_(pdiv, x, y, props.image);

            if (props.label)
                t.allowedPopups++;

            if (props.draggable) {
                if (props.pinned)
                    pdiv._pinned = true;

                if (pdiv._pinned) {
                    t.allowedPopups++;
                    t.activepopup = null;
                    if (props.pin)
                        props.pin(pdiv._pinned); // Pin callback

                    /* Close icon */
                    setTimeout( ()=> {
                        const closeimage = document.createElement('img');
                        closeimage.className = "popup_close";
                        closeimage.src = "images/16px/close.png";
                        pdiv.appendChild(closeimage);

                        /* close click handler */
                        closeimage.onclick = (e)=> pdiv.close()
                    }, 200);
                }
            }

        pdiv.onmousedown = function(e)
            { e = (e)?e:((event)?event:null); e.stopPropagation(); return null; };
        pdiv.onmouseup = function(e)
            { e = (e)?e:((event)?event:null); return null; };
        pdiv.onclick = function(e)
            { e = (e)?e:((event)?event:null); e.stopPropagation(); return null; };

        if (props.resizable)
            $(pdiv).resizable();
        if (props.draggable)
            $(pdiv).draggable(
                { handle: "h1,h2,.handle", delay: 100, opacity: 0.7,
                    start: props.dragStart, stop: props.dragStop }  );

        pdiv.close = ()=> {
            if (props.vnode)
                m.mount(pdiv, null);
            if (props.onclose)
                props.onclose();
            pdiv._pinned = false;

            const tmp = t.activepopup;
            t.activepopup = pdiv;
            t.allowedPopups--;
            t.removePopup();
            t.activepopup = tmp;
            t.removePopup();
            if (props.pin)
                props.pin(pdiv._pinned); // Pin callback
        }
        return pdiv;
    }

    /**
     * Activate popup with image content
     * @param {string} title - heading text.
     * @param {string} href - URL of image
     */
    imagePopup(title, href, props)
    {
        props.html = '
'+title+'
' +
                      '';
        const d =  this.showPopup(props);
        return d;
    }

    /**
     * Activate popup with content from remote server.
     * @param {string} url - URL of remote content.
     * @param {Object.<string,*>} props - Options, see showPopup()
     */
    remotePopup(srv, service, data, props)
    {
        const d =  this.showPopup(props);
        srv.GET(service, data,
            txt => {d.innerHTML = txt;});
        return d;
    }

    /**
     * Activate popup with content from remote server and with
     * a stylesheet.
     * @param {string} url - URL of remote content.
     * @param {string} css - CSS-class to add to the popup.
     * @param {Object.<string,*>} props - Options, see showPopup()
     *
     */
    remotePopupCSS(url, css, props)
    {
        const d = this.remotePopup(url, props);
        if (css != null)
            setTimeout( () => {
                div.className = css;
            }, 900);
    }

    /**
     * Show a cross at a given map position.
     * @param {ol.Coordinate} geoPos - LatLong position of where to put upper left corner of popup.
     */
    showImageGeo(geoPos) {
        setTimeout( () => {
            this.geoPos = geoPos;
            var pixPos = this.mb.map.getPixelFromCoordinate
                (ol.proj.fromLonLat(geoPos, this.mb.view.getProjection()));
            this.popup_(null, pixPos[0], pixPos[1], true);
        }, 900);
    }

    /**
     * Change the position of a popup or image.
     * @param {ol.Coordinate} geoPos - LatLong position of upper left corner of popup.
     */
    setPositionGeo(geoPos) {
        if (this.activepopup && this.activepopup != null) {
            var pixPos = this.mb.map.getPixelFromCoordinate
                (ol.proj.fromLonLat(geoPos, this.mb.view.getProjection()));
            this.setPosition_(pixPos[0], pixPos[1]);
        }
    }

    /**
     * Change the position of a popup or image. Pixel position on screen.
     * @param {ol.Coordinate} pixPos - Pixel position of upper left corner of popup.
     */
    setPositionPix(pixPos) {
        if (this.activepopup && this.activepopup != null)
            this.setPosition_(pixPos[0], pixPos[1]);
    }

    /**
     * Set the position of a popup or image. Pixel position on screen.
     * @private
     */

    setPosition_(x, y)
    {
        let xoff=0;
        let yoff=0;
        let xoffs = false, yoffs = false;

        if (this.image != null) {
            this.image.style.left= -9+'px';
            this.image.style.top= -9+'px';
        }
        else {
           if (x<0) x=0;
           if (y<0) y=0;
        }

        /*
         * If part if window is outside the viewport, we may adjust the
         * position of the window. It may be easier to just move the map, but
         * sometimes it is desirable not to..
         */
        xoff = x + 4 + this.psubdiv.clientWidth - this.onDiv.clientWidth;
        if (xoff > 0) {
            xoffs = true;
            x -= xoff;
            if (x < 1) x=1;
            if (this.image!=null)
                this.image.style.left = (xoff-9)+'px';
        }

        yoff = y + 4 + this.psubdiv.clientHeight - this.onDiv.clientHeight;
        if (yoff > 0) {
            yoffs = true;
            y -= yoff;
            if (y < 1) y=1;
            if (this.image!=null)
                this.image.style.top =(yoff-9)+'px';
        }

        this.activepopup.style.left = x-3+"px";
        this.activepopup.style.top  = y-3+"px";

        if (xoffs && yoffs && this.image != null)
            this.image.style.display = "none";
        return [x,y];
    }

    /**
     * Activate a popup - show it on the screen.
     * @private
     */
    popup_(elem, x, y, img)
    {
        var t = this;
        if (this.allowedPopups <= 0)
            return;

        if (elem == null)
            elem = document.createElement('div');

        this.psubdiv = this.activepopup = elem;
        if (img != null && img) {
            this.image = document.createElement('img');
            this.activepopup.appendChild(this.image);
            this.image.src='images/cross.png';
            this.image.style.position='absolute';
            this.image.className="marker";
        }

        /* Add the popup window div to the viewport */
        this.onDiv.appendChild(this.activepopup);

        this.activepopup.style.position   = 'absolute';
        this.activepopup.style.display    = 'block';
        this.activepopup.style.padding    = '2px';
        this.activepopup.style.cursor     = 'default';

        /* If viewport is less than 500 pixels wide and content is
         * wider than the viewport, let popup be as wide as the viewport
         */
        if (this.onDiv.clientWidth < 500 && elem.clientWidth > this.onDiv.clientWidth)
            this.activepopup.style.minWidth = this.onDiv.clientWidth+'px';

        /* Is the height of the content more than the available height?
         * Then we need a scroller
         */
        if (elem.clientHeight+10 > this.onDiv.clientHeight) {
            this.activepopup.style.maxHeight = this.onDiv.clientHeight-5 + "px";
            elem.id = 'wrapper'; // FIXME?
            this.activepopup.style.overflowY  = 'scroll';
        }
        else
            this.activepopup.style.overflowY = 'visible';

        /* Hack to set the popup position even if it takes some time to load its content */
        setAdjustedPos(x,y);
        setTimeout( ()=> { setAdjustedPos(x,y) }, 300);
        setTimeout( ()=> { setAdjustedPos(x,y) }, 2000);

        this.allowedPopups--;
        if (this.onCallback != null)
            this.onCallback();

        function setAdjustedPos(x,y) {
            if (t.activepopup != null)
                t.activepopup.adjustedPos = t.setPosition_(x, y);
        }
    }

} /* class */

<commit_msg>Popup fixes..<commit_after>/*
  Map browser based on OpenLayers 5.
  Popup windows

  Copyright (C) 2017-2021 Øyvind Hanssen, LA7ECA, ohanssen@acm.org

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU Affero General Public License as published
  by the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public License
  along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

var isMobile = false;

/**
 * @classdesc
 * Popup window manager class.
 *
 * @constructor
 * @param {pol.core.MapBrowser} mb - Map browser instance.
 */

pol.core.Popup = class {

    constructor(mb) {
        this.mb            = mb;
        this.onDiv         = document.getElementById("map");
        this.activepopup   = null;
        this.allowedPopups = 1;
        this.onCallback    = null;
        this.offCallback   = null;
        this.image         = null;
        const t = this;

       // this.mb.view.on('change:center', onChangeCenter);
        this.mb.map.on('click', onClick);
        this.mb.map.on('change:view', onChangeView);

        function onClick() {
           t.removePopup();
        }

        function onChangeView() {
           t.removePopup();
        }
    } /* constructor */

    /**
     * Deactivate popup.
     */
    removePopup() {
        if (this.activepopup == null)
            return;
        if (this.offCallback != null)
            this.offCallback();
        this.isMenu = false;
        this.allowedPopups++;

        this.activepopup.style.display = "none" ;
        this.activepopup.parentNode.removeChild(this.activepopup);
        this.activepopup = null;
    }

    /**
     * Return true if popup is active.
     * @returns {boolean}
     */
    popupActive()
       { return (this.activepopup != null); }

    /**
     * Register callback functions
     * For popup activation (on) and deactivation (off)
     * @param {function} on - Callback to be invoked when popup is activated.
     * @param {function} off - Callback to be invoked when popup is deactivated.
     */
    onPopup(on, off)
       { this.onCallback = on; this.offCallback = off; }

    /**
     * Activate popup.
     * @param {Object.<string,*>} props - Options
     * @param {string|undefined} props.html - HTML code to render inside the popup.
     * @param {ol.Pixel|undefined} props.pixPos - Pixel position of upper left corner of popup.
     * @param {ol.Coordinate|undefined} props.geoPos - LatLong position of upper left corner of popup.
     * @param {boolean|undefined} props.image - true if we want a cross to be displayed at the position.
     * @param {boolean|undefined} props.draggable - true if we want the popup to be draggable and pinnable.
     * @param {string|undefined} props.id - unique identifier (used as id of element).
     * @param {function|undefined} props.onclose - handler to be called when window closes.
     *
     * @returns div element or null
     *
     */
    showPopup(props)
    {
        const t = this;
        let x, y;
        if (t.activepopup != null && t.allowedPopups <= 1)
            return null;
        if (props.id && props.id != null && document.getElementById(props.id) != null) {
            $('#'+props.id).effect('bounce');
            return null;
        }
        let pdiv = ((props.elem && props.elem!=null)
            ? props.elem : document.createElement('div'));
        if (props.html)
            pdiv.innerHTML = props.html;
        if (props.vnode)
            m.mount(pdiv, props.vnode);

        if (props.id && props.id != null) pdiv.id = props.id;

        pdiv.className = 'POPUP' +
           ((props.cclass && props.cclass != null) ? " "+props.cclass : "");

        if (props.geoPos && props.geoPos != null) {
            props.pixPos = t.mb.map.getPixelFromCoordinate
                (ol.proj.fromLonLat(props.geoPos, t.mb.view.getProjection()));
        }

        if (props.pixPos && props.pixPos != null)
            { x = props.pixPos[0]; y=props.pixPos[1]; }
        t.popup_(pdiv, x, y, props.image);

        if (props.label)
            t.allowedPopups++;

        if (props.draggable && props.pinned) {
            /*
             * If we activate a pinned and draggable popup we allow more popups to be
             * created and we add a close icon.
             */
            pdiv._pinned = true;
            t.allowedPopups++;
            t.activepopup = null;
            if (props.pin)
                props.pin(pdiv._pinned); // Call pin callback

            /* Close icon */
            setTimeout( ()=> {
                const closeimage = document.createElement('img');
                closeimage.className = "popup_close";
                closeimage.src = "images/16px/close.png";
                pdiv.appendChild(closeimage);

                /* close click handler */
                closeimage.onclick = (e)=> pdiv.close()
            }, 200);
        }

        /* Drag and resize setup */
        if (props.resizable)
            $(pdiv).resizable();
        if (props.draggable)
            $(pdiv).draggable(
                { handle: "h1,h2,.handle", delay: 100, opacity: 0.7,
                    start: props.dragStart, stop: props.dragStop }  );

        /*
         * Mouse event handlers
         */
        pdiv.onmousedown = function(e)
            { e = (e)?e:((event)?event:null); e.stopPropagation(); return null; };
        pdiv.onmouseup = function(e)
            { e = (e)?e:((event)?event:null); return null; };
        pdiv.onclick = function(e)
            { e = (e)?e:((event)?event:null); e.stopPropagation(); return null; };

        /* Close handler */
        pdiv.close = ()=> {
            if (props.vnode)
                m.mount(pdiv, null);
            if (props.onclose)
                props.onclose();
            pdiv._pinned = false;

            const tmp = t.activepopup;
            t.activepopup = pdiv;
            t.allowedPopups--;
            t.removePopup();
            t.activepopup = tmp;
            t.removePopup();
            if (props.pin)
                props.pin(pdiv._pinned); // Pin callback
        }
        return pdiv;
    }

    /**
     * Activate popup with image content
     * @param {string} title - heading text.
     * @param {string} href - URL of image
     */
    imagePopup(title, href, props)
    {
        props.html = '
'+title+'
' +
                      '';
        const d =  this.showPopup(props);
        return d;
    }

    /**
     * Activate popup with content from remote server.
     * @param {string} url - URL of remote content.
     * @param {Object.<string,*>} props - Options, see showPopup()
     */
    remotePopup(srv, service, data, props)
    {
        const d =  this.showPopup(props);
        srv.GET(service, data,
            txt => {d.innerHTML = txt;});
        return d;
    }

    /**
     * Activate popup with content from remote server and with
     * a stylesheet.
     * @param {string} url - URL of remote content.
     * @param {string} css - CSS-class to add to the popup.
     * @param {Object.<string,*>} props - Options, see showPopup()
     *
     */
    remotePopupCSS(url, css, props)
    {
        const d = this.remotePopup(url, props);
        if (css != null)
            setTimeout( () => {
                div.className = css;
            }, 900);
    }

    /**
     * Show a cross at a given map position.
     * @param {ol.Coordinate} geoPos - LatLong position of where to put upper left corner of popup.
     */
    showImageGeo(geoPos) {
        setTimeout( () => {
            var pixPos = this.mb.map.getPixelFromCoordinate
                (ol.proj.fromLonLat(geoPos, this.mb.view.getProjection()));
            this.popup_(null, pixPos[0], pixPos[1], true);
        }, 900);
    }

    /**
     * Change the position of a popup or image.
     * @param {ol.Coordinate} geoPos - LatLong position of upper left corner of popup.
     */
    setPositionGeo(geoPos) {
        if (this.activepopup && this.activepopup != null) {
            var pixPos = this.mb.map.getPixelFromCoordinate
                (ol.proj.fromLonLat(geoPos, this.mb.view.getProjection()));
            this.setPosition_(this.activepopup, this.image, pixPos[0], pixPos[1]);
        }
    }

    /**
     * Change the position of a popup or image. Pixel position on screen.
     * @param {ol.Coordinate} pixPos - Pixel position of upper left corner of popup.
     */
    setPositionPix(pixPos) {
        if (this.activepopup && this.activepopup != null)
            this.setPosition_(this.activepopup, this.image, pixPos[0], pixPos[1]);
    }

    /**
     * Set the position of a popup or image. Pixel position on screen.
     * @private
     */

    setPosition_(pdiv, img, x, y)
    {
        let xoff=0;
        let yoff=0;
        let xoffs = false, yoffs = false;

        if (img != null) {
            img.style.left= -9+'px';
            img.style.top= -9+'px';
        }
        else {
           if (x<0) x=0;
           if (y<0) y=0;
        }

        /*
         * If part if window is outside the viewport, we may adjust the
         * position of the window. It may be easier to just move the map, but
         * sometimes it is desirable not to..
         */
        xoff = x + 20 + pdiv.clientWidth - this.onDiv.clientWidth;
        if (xoff > 0) {
            xoffs = true;
            x -= xoff;
            if (x < 1) x=1;
            if (img!=null)
                img.style.left = (xoff-9)+'px';
        }

        yoff = y + 20 + pdiv.clientHeight - this.onDiv.clientHeight;
        if (yoff > 0) {
            yoffs = true;
            y -= yoff;
            if (y < 1) y=1;
            if (img!=null)
                img.style.top =(yoff-9)+'px';
        }

        pdiv.style.left = x+"px";
        pdiv.style.top  = y+"px";

        if (xoffs && yoffs && img != null)
            img.style.display = "none";
        return [x,y];
    }

    /**
     * Activate a popup - show it on the screen.
     * @private
     */
    popup_(elem, x, y, img)
    {
        var t = this;
        if (this.allowedPopups <= 0)
            return;

        if (elem == null)
            elem = document.createElement('div');

        this.activepopup = elem;
        if (img != null && img) {
            this.image = document.createElement('img');
            this.activepopup.appendChild(this.image);
            this.image.src='images/cross.png';
            this.image.style.position='absolute';
            this.image.className="marker";
        }

        /* Add the popup window div to the viewport */
        this.onDiv.appendChild(this.activepopup);

        this.activepopup.style.position   = 'absolute';
        this.activepopup.style.display    = 'block';
        this.activepopup.style.padding    = '2px';
        this.activepopup.style.cursor     = 'default';

        /* If viewport is less than 500 pixels wide and content is
         * wider than the viewport, let popup be as wide as the viewport
         */
        if (this.onDiv.clientWidth < 500 && elem.clientWidth > this.onDiv.clientWidth)
            this.activepopup.style.minWidth = this.onDiv.clientWidth+'px';

        /* Is the height of the content more than the available height?
         * Then we need a scroller
         */
        if (elem.clientHeight+10 > this.onDiv.clientHeight) {
            this.activepopup.style.maxHeight = this.onDiv.clientHeight-5 + "px";
            elem.id = 'wrapper'; // FIXME?
            this.activepopup.style.overflowY  = 'scroll';
        }
        else
            this.activepopup.style.overflowY = 'visible';

        /* Hack to set the popup position even if it takes some time to load its content */
        const pd = t.activepopup;
        const pimg = t.image;
        setAdjustedPos(pd, pimg, x,y);
        setTimeout( ()=> { setAdjustedPos(pd, pimg, x,y) }, 300);
        setTimeout( ()=> { setAdjustedPos(pd, pimg, x,y) }, 2000);

        this.allowedPopups--;
        if (this.onCallback != null)
            this.onCallback();

        function setAdjustedPos(pd, pimg, x,y) {
            if (pd != null)
                pd.adjustedPos = t.setPosition_(pd, pimg, x, y);
        }
    }

} /* class */

<|endoftext|>


<commit_before>function ServiceContainer(){
	/**
	 * Contains service handlers, indexed by name.
	 */
	this._serviceFunctions = {};
	/**
	 * Stores static resources, passed as the first argument to every service called (via bind, when requesting the service).
	 */
	this._resources = {};
}

ServiceContainer.prototype.addService = function addService(serviceName, serviceFunction){
	this._serviceFunctions[serviceName] = serviceFunction;
};

ServiceContainer.prototype.addResource = function addResource(resourceName, resourceValue){
	this._resources[resourceName] = resourceValue;
};

ServiceContainer.prototype._getBoundService = function _getBoundService(providedResources, generatorFunction, serviceName){
	if(typeof(this._serviceFunctions[serviceName]) === 'function'){
		if(typeof(generatorFunction) === 'function'){
			// If dynamic resource generation is required, we call the generator and overwrite the static resources with it.
			var generatedResources = generatorFunction();
			for(var k in generatedResources){
				if(Object.prototype.hasOwnProperty.call(generatedResources, k)){
					providedResources[k] = generatedResources[k];
				}
			}
		}
		return this._serviceFunctions[serviceName].bind(undefined, providedResources);
	}
	else{
		throw new Error('Service does not exist: ' + serviceName);
	}
};

ServiceContainer.prototype.createServiceGetter = function createServiceGetter(contextResources){
	var completeResourcesObject = {};
	// The context's resources shall contain our original resources...
	for(var staticResourceName in this._resources){
		completeResourcesObject[staticResourceName] = this._resources[staticResourceName];
	}
	if(typeof(contextResources) === 'object'){
		// ... as well as the resources passed into this function.
		for(var dynamicResourceName in contextResources){
			completeResourcesObject[dynamicResourceName] = contextResources[dynamicResourceName];
		}
		return this._getBoundService.bind(this, completeResourcesObject, null);
	}
	else if(typeof(contextResources) === 'function'){
		// In case the resources were a dynamic generator, the generator is passed for further execution to the generated service getter.
		return this._getBoundService.bind(this, completeResourcesObject, contextResources);
	}
	else{
		throw new Error('Context resources need to be either a ready-to-use object or an object-generating function');
	}
};

ServiceContainer.prototype.service = function service(serviceName){
	return this._getBoundService(this._resources, null, serviceName);
};

ServiceContainer.prototype.getResources = function getResources(){
	return this._resources;
};

ServiceContainer.prototype.getServices = function getServices(){
	return this._serviceFunctions;
};

module.exports.ServiceContainer = ServiceContainer;<commit_msg>The service container now overlays resources dynamically (in the generated function) rather than statically (at the time of function creation). This allows dynamic propagation of context variables, such as user IDs, sessions etc.<commit_after>function ServiceContainer(){
	/**
	 * Contains service handlers, indexed by name.
	 */
	this._serviceFunctions = {};
	/**
	 * Stores static resources, passed as the first argument to every service called (via bind, when requesting the service).
	 */
	this._resources = {};
}

ServiceContainer.prototype.addService = function addService(serviceName, serviceFunction){
	this._serviceFunctions[serviceName] = serviceFunction;
};

ServiceContainer.prototype.addResource = function addResource(resourceName, resourceValue){
	this._resources[resourceName] = resourceValue;
};

ServiceContainer.prototype._getBoundService = function _getBoundService(providedResources, generatorFunction, serviceName){
	var activeResources = {};
	for(var resourceName in this._resources){
		activeResources[resourceName] = this._resources[resourceName];
	}
	for(var resourceName in providedResources){
		activeResources[resourceName] = providedResources[resourceName];
	}
	if(typeof(this._serviceFunctions[serviceName]) === 'function'){
		if(typeof(generatorFunction) === 'function'){
			// If dynamic resource generation is required, we call the generator and overwrite the static resources with it.
			var generatedResources = generatorFunction();
			for(var k in generatedResources){
				if(Object.prototype.hasOwnProperty.call(generatedResources, k)){
					activeResources[k] = generatedResources[k];
				}
			}
		}
		return this._serviceFunctions[serviceName].bind(undefined, activeResources);
	}
	else{
		throw new Error('Service does not exist: ' + serviceName);
	}
};

ServiceContainer.prototype.createServiceGetter = function createServiceGetter(contextResources){
	if(typeof(contextResources) === 'object'){
		return this._getBoundService.bind(this, contextResources, null);
	}
	else if(typeof(contextResources) === 'function'){
		// In case the resources were a dynamic generator, the generator is passed for further execution to the generated service getter.
		return this._getBoundService.bind(this, {}, contextResources);
	}
	else{
		throw new Error('Context resources need to be either a ready-to-use object or an object-generating function');
	}
};

ServiceContainer.prototype.service = function service(serviceName){
	return this._getBoundService(this._resources, null, serviceName);
};

ServiceContainer.prototype.getResources = function getResources(){
	return this._resources;
};

ServiceContainer.prototype.getServices = function getServices(){
	return this._serviceFunctions;
};

module.exports.ServiceContainer = ServiceContainer;<|endoftext|>


<commit_before>$(function() {
	$('#btnGenarate').click(ReportTemplate.generate);
	$('#btnShowChart').click(ReportTemplate.showChart);
	$('#btnToggleSkins').click(ReportTemplate.toggleSkin);
	$('#btnShowComment').mouseover(ReportTemplate.showComment);
	$('#btnShowComment').mouseleave(ReportTemplate.closeComment);
	$('#checkAllStatColumn').click(ReportTemplate.checkedAllStatColumn);
	$('#btnExportToExcel').click(ReportTemplate.exportToExcel);
	$('#btnFullScreen').click(ReportTemplate.fullScreen);

	ReportTemplate.generate(ReportTemplate.Mode.classic,null);
});

var ReportTemplate = function() {
};

ReportTemplate.Mode = {
	classic : 'classic',// 经典表格模式
	datatables : 'dt'// datatables控件表格模式
};

ReportTemplate.URL = {
	generate : WebAppRequest.getContextPath() + '/report/generate',
	exportToExcel : WebAppRequest.getContextPath() + '/report/exportExcel'
};

ReportTemplate.generate = function(mode, callback) {
	$('#isRowSpan').val($('#isMergeRow').prop('checked'));
	$.ajax({
		type : "POST",
		url : ReportTemplate.URL.generate,
		data : $("#templateFrom").serialize(),
		dataType : "json",
		beforeSend : function() {
			$('#loadingText').html("报表正在生成中, 请稍等...");
			$('#loading').show();
		},
		success : function(result) {
			$('#reportDiv').html(result.htmlTable);
			ReportTemplate.initTable(mode || ReportTemplate.Mode.classic);
			ReportTemplate.filterTable = ReportTemplate.renderFilterTable(result);
			if (callback instanceof Function) {
				callback();
			}
		},
		complete : function() {
			$('#loading').hide();
		}
	});
};

ReportTemplate.initTable = function(mode) {
	var table = $("#easyreport");
	return ReportTemplate.renderClassicReport(table);

	/*if (mode == ReportTemplate.Mode.classic) {
		return ReportTemplate.renderClassicReport(table);
	}
	// 如果为dt模式但是表格存在跨行
	// 则转为经典表格模式,因为datatables控件不支持跨行
	if (ReportTemplate.hasRowSpan()) {
		return ReportTemplate.renderClassicReport(table);
	}
	return ReportTemplate.renderDatatablesReport(table);*/
};

// 表格中是否跨行
ReportTemplate.hasRowSpan = function() {
	var rowspans = $("#easyreport>tbody>tr>td[rowspan]");
	return (rowspans && rowspans.length > 0);
};

ReportTemplate.renderClassicReport = function(table) {
	$("#easyreport>tbody>tr").click(function() {
		$('#easyreport .selected').removeClass('selected').removeAttr('title');
		$(this).addClass('selected');
	});
	$('#easyreport>tbody>tr').mouseover(function(e) {
		$(this).addClass('selected');
	});
	$('#easyreport>tbody>tr').mouseleave(function(e) {
		$('#easyreport .selected').removeClass('selected').removeAttr('title');
	});

	var noRowSpan = !ReportTemplate.hasRowSpan();
	table.data('isSort', noRowSpan).fixScroll();

	//如果表格中没有跨行rowspan(暂不支持跨行)
	if (noRowSpan) {
		table.tablesorter({
			sortInitialOrder : 'desc'
		});
		table.find('>thead>tr').attr({
			title : "点击可以排序"
		}).css({
			cursor : "pointer"
		});
	}
};

ReportTemplate.renderDatatablesReport = function(table) {
	$('#easyreport').removeClass("easyreport");
	$('#easyreport').addClass('table table-striped table-bordered');
	var dt = $('#easyreport').dataTable({
		"scrollY" : "758",
		"scrollX" : true,
		"scrollCollapse" : true,
		"searching" : false,
		"pageLength": 100,
		"lengthMenu": [ 50, 100, 200, 500, 1000 ],
		"language" : {
			processing : "数据正在加载中...",
			search : "查询:",
			lengthMenu : "每页显示 _MENU_ 条记录",
			info : "从 _START_ 到 _END_ /共 _TOTAL_ 条记录",
			infoEmpty : "从 0 到  0  共 0  条记录",
			infoFiltered : "(从 _MAX_ 条数据中检索)",
			infoPostFix : "",
			thousands : ",",
			loadingRecords : "数据加载中...",
			zeroRecords : "没有检索到数据",
			emptyTable : "没有数据",
			paginate : {
				first : "首页",
				previous : "前一页",
				next : "后一页",
				last : "尾页"
			},
			aria : {
				sortAscending : ": 升序",
				sortDescending : ": 降序"
			}
		}
	});
	$('#easyreport tbody').on( 'click', 'tr', function () {
        if ( $(this).hasClass('selected') ) {
            $(this).removeClass('selected');
        }
        else {
        	dt.$('tr.selected').removeClass('selected');
            $(this).addClass('selected');
        }
    } );
};

// 将报表上面的过滤信息拼成table，用于写入excel中
ReportTemplate.renderFilterTable = function(result) {
	var html = '
';
	html += '

' + $('#rpTitle').text() + '

';
	html += '

导出时间:' + currentTime()+ '

';
	$('#templateFrom .j-item').each(function() {
		var type = $(this).attr('data-type');
		if (type === 'date-range') {
			var input = $(this).find('.combo-text');
			html += '
时间范围:' + input.eq(0).val() + '~' + input.eq(1).val() + '
';
		} else if (type === 'checkbox') {
			html += '
筛选统计列:';
			var rowChoose = [];
			$(this).find('input[type="checkbox"]:checked').each(function() {
				rowChoose.push($(this).attr('data-name'));
			})
			html += rowChoose.join('、');
			html += '
';
		} else {
			var label = $(this).find('label').text().replace(':', '');
			var val = $(this).find('.combo-text').val();
			html += '
' + label + '	' + val + '
';
		}
	})
	html += '

';
	return html;
};

ReportTemplate.exportToExcel = function(e) {
	var htmlText = '';
	htmlText += (ReportTemplate.filterTable || '');
	htmlText += '
' + $('#easyreport').html() + '
';
	var bytes = ReportTemplate.getBytes(htmlText);
	if (bytes > 2000000) {
		htmlText = "large";
	}
	var postUrl = ReportTemplate.URL.exportToExcel;
	var postData = $('#templateFrom').serializeObject();
	postData["htmlText"] = htmlText;

	$('#loadingText').html("正在导出Excel中, 请稍等...");
	$('#loading').show();
	$.fileDownload(postUrl, {
		httpMethod : "POST",
		data : postData
	}).done(function() {
		$('#loading').hide();
	}).fail(function() {
		$('#loading').hide();
	});
	e.preventDefault();
};

ReportTemplate.showComment = function(e) {
	$('#rpCommentTip').css({
		"display" : "block",
		"backgroundColor" : '#666',
		"borderColor" : '#666'
	});
};

ReportTemplate.closeComment = function(e) {
	$('#rpCommentTip').css({
		"display" : "none"
	});
};

ReportTemplate.toggleSkin = function(e) {
	var regex = /default/g;
	var href = WebAppRequest.getContextPath() + "/assets/modules/report/themes/default.css";
	if (regex.test($('#skin').attr("href"))) {
		href = WebAppRequest.getContextPath() + "/assets/modules/report/themes/clear.css";
	}
	$('#skin').attr({
		"href" : href
	});
};

ReportTemplate.fullScreen = function() {
	var uid = $('#rpUid').val();
	var url = WebAppRequest.getContextPath() + '/report/uid/' + uid;
	ReportTemplate.winOpen(url, uid);
};

ReportTemplate.showChart = function(e) {
	var title = $('#rpName').val() + "(图表)";
	var id = $('#rpId').val();
	var uid = $('#rpUid').val();
	parent.showChart(title, id, uid);
};

ReportTemplate.checkedAllStatColumn = function(e) {
	var checked = $("input[name='checkAllStatColumn']").prop("checked");
	$("input[name='statColumns']").prop("checked", checked);
};

ReportTemplate.winOpen = function(url, name) {
	var width = window.screen.availWidth - 10;
	var height = window.screen.availHeight - 60;
	window.open(url, name, 'width=' + width + ',height=' + height
			+ ',top=0,left=0,toolbar=no,menubar=no,scrollbars=yes, resizable=yes,location=no, status=no');
};

ReportTemplate.getBytes = function(str) {
	var totalLength = 0;
	var i;
	var charCode;
	for (i = 0; i < str.length; i++) {
		charCode = str.charCodeAt(i);
		if (charCode < 0x007f) {
			totalLength = totalLength + 1;
		} else if ((0x0080 <= charCode) && (charCode <= 0x07ff)) {
			totalLength += 2;
		} else if ((0x0800 <= charCode) && (charCode <= 0xffff)) {
			totalLength += 3;
		}
	}
	return totalLength;
};

function currentTime(){
	var d = new Date(),str = '';
	str += d.getFullYear()+'-';
	str  += d.getMonth() + 1+'-';
	str  += d.getDate()+' ';
	str += d.getHours()+':';
	str  += d.getMinutes()+':';
	str+= d.getSeconds()+'';
	return str;
};<commit_msg>导出excel 日期参数值，为undefined<commit_after>$(function() {
	$('#btnGenarate').click(ReportTemplate.generate);
	$('#btnShowChart').click(ReportTemplate.showChart);
	$('#btnToggleSkins').click(ReportTemplate.toggleSkin);
	$('#btnShowComment').mouseover(ReportTemplate.showComment);
	$('#btnShowComment').mouseleave(ReportTemplate.closeComment);
	$('#checkAllStatColumn').click(ReportTemplate.checkedAllStatColumn);
	$('#btnExportToExcel').click(ReportTemplate.exportToExcel);
	$('#btnFullScreen').click(ReportTemplate.fullScreen);

	ReportTemplate.generate(ReportTemplate.Mode.classic,null);
});

var ReportTemplate = function() {
};

ReportTemplate.Mode = {
	classic : 'classic',// 经典表格模式
	datatables : 'dt'// datatables控件表格模式
};

ReportTemplate.URL = {
	generate : WebAppRequest.getContextPath() + '/report/generate',
	exportToExcel : WebAppRequest.getContextPath() + '/report/exportExcel'
};

ReportTemplate.generate = function(mode, callback) {
	$('#isRowSpan').val($('#isMergeRow').prop('checked'));
	$.ajax({
		type : "POST",
		url : ReportTemplate.URL.generate,
		data : $("#templateFrom").serialize(),
		dataType : "json",
		beforeSend : function() {
			$('#loadingText').html("报表正在生成中, 请稍等...");
			$('#loading').show();
		},
		success : function(result) {
			$('#reportDiv').html(result.htmlTable);
			ReportTemplate.initTable(mode || ReportTemplate.Mode.classic);
			ReportTemplate.filterTable = ReportTemplate.renderFilterTable(result);
			if (callback instanceof Function) {
				callback();
			}
		},
		complete : function() {
			$('#loading').hide();
		}
	});
};

ReportTemplate.initTable = function(mode) {
	var table = $("#easyreport");
	return ReportTemplate.renderClassicReport(table);

	/*if (mode == ReportTemplate.Mode.classic) {
		return ReportTemplate.renderClassicReport(table);
	}
	// 如果为dt模式但是表格存在跨行
	// 则转为经典表格模式,因为datatables控件不支持跨行
	if (ReportTemplate.hasRowSpan()) {
		return ReportTemplate.renderClassicReport(table);
	}
	return ReportTemplate.renderDatatablesReport(table);*/
};

// 表格中是否跨行
ReportTemplate.hasRowSpan = function() {
	var rowspans = $("#easyreport>tbody>tr>td[rowspan]");
	return (rowspans && rowspans.length > 0);
};

ReportTemplate.renderClassicReport = function(table) {
	$("#easyreport>tbody>tr").click(function() {
		$('#easyreport .selected').removeClass('selected').removeAttr('title');
		$(this).addClass('selected');
	});
	$('#easyreport>tbody>tr').mouseover(function(e) {
		$(this).addClass('selected');
	});
	$('#easyreport>tbody>tr').mouseleave(function(e) {
		$('#easyreport .selected').removeClass('selected').removeAttr('title');
	});

	var noRowSpan = !ReportTemplate.hasRowSpan();
	table.data('isSort', noRowSpan).fixScroll();

	//如果表格中没有跨行rowspan(暂不支持跨行)
	if (noRowSpan) {
		table.tablesorter({
			sortInitialOrder : 'desc'
		});
		table.find('>thead>tr').attr({
			title : "点击可以排序"
		}).css({
			cursor : "pointer"
		});
	}
};

ReportTemplate.renderDatatablesReport = function(table) {
	$('#easyreport').removeClass("easyreport");
	$('#easyreport').addClass('table table-striped table-bordered');
	var dt = $('#easyreport').dataTable({
		"scrollY" : "758",
		"scrollX" : true,
		"scrollCollapse" : true,
		"searching" : false,
		"pageLength": 100,
		"lengthMenu": [ 50, 100, 200, 500, 1000 ],
		"language" : {
			processing : "数据正在加载中...",
			search : "查询:",
			lengthMenu : "每页显示 _MENU_ 条记录",
			info : "从 _START_ 到 _END_ /共 _TOTAL_ 条记录",
			infoEmpty : "从 0 到  0  共 0  条记录",
			infoFiltered : "(从 _MAX_ 条数据中检索)",
			infoPostFix : "",
			thousands : ",",
			loadingRecords : "数据加载中...",
			zeroRecords : "没有检索到数据",
			emptyTable : "没有数据",
			paginate : {
				first : "首页",
				previous : "前一页",
				next : "后一页",
				last : "尾页"
			},
			aria : {
				sortAscending : ": 升序",
				sortDescending : ": 降序"
			}
		}
	});
	$('#easyreport tbody').on( 'click', 'tr', function () {
        if ( $(this).hasClass('selected') ) {
            $(this).removeClass('selected');
        }
        else {
        	dt.$('tr.selected').removeClass('selected');
            $(this).addClass('selected');
        }
    } );
};

// 将报表上面的过滤信息拼成table，用于写入excel中
ReportTemplate.renderFilterTable = function(result) {
	var html = '
';
	html += '

' + $('#rpTitle').text() + '

';
	html += '

导出时间:' + currentTime()+ '

';
	$('#templateFrom .j-item').each(function() {
		var type = $(this).attr('data-type');
		if (type === 'date-range') {
			var input = $(this).find('.combo-text');
			html += '
时间范围:' + input.eq(0).val() + '~' + input.eq(1).val() + '
';
		} else if (type === 'checkbox') {
			html += '
筛选统计列:';
			var rowChoose = [];
			$(this).find('input[type="checkbox"]:checked').each(function() {
				rowChoose.push($(this).attr('data-name'));
			})
			html += rowChoose.join('、');
			html += '
';
		}
		else if (new RegExp('datebox').test($(this).find("input").attr("class"))) {
			var label = $(this).find('label').text().replace(':', '');
			var val = $(this).find("input").attr("value");
			if(!val){
				val = $(this).find('.combo-text').val();
			}
			html += '
' + label + '	' + val + '
';
		}
		else {
			var label = $(this).find('label').text().replace(':', '');
			var val = $(this).find('.combo-text').val();
			if(!val){
				val = $(this).find("input").attr("value");
			}
			html += '
' + label + '	' + val + '
';
		}
	})
	html += '

';
	return html;
};

ReportTemplate.exportToExcel = function(e) {
	var htmlText = '';
	htmlText += (ReportTemplate.filterTable || '');
	htmlText += '
' + $('#easyreport').html() + '
';
	var bytes = ReportTemplate.getBytes(htmlText);
	if (bytes > 2000000) {
		htmlText = "large";
	}
	var postUrl = ReportTemplate.URL.exportToExcel;
	var postData = $('#templateFrom').serializeObject();
	postData["htmlText"] = htmlText;

	$('#loadingText').html("正在导出Excel中, 请稍等...");
	$('#loading').show();
	$.fileDownload(postUrl, {
		httpMethod : "POST",
		data : postData
	}).done(function() {
		$('#loading').hide();
	}).fail(function() {
		$('#loading').hide();
	});
	e.preventDefault();
};

ReportTemplate.showComment = function(e) {
	$('#rpCommentTip').css({
		"display" : "block",
		"backgroundColor" : '#666',
		"borderColor" : '#666'
	});
};

ReportTemplate.closeComment = function(e) {
	$('#rpCommentTip').css({
		"display" : "none"
	});
};

ReportTemplate.toggleSkin = function(e) {
	var regex = /default/g;
	var href = WebAppRequest.getContextPath() + "/assets/modules/report/themes/default.css";
	if (regex.test($('#skin').attr("href"))) {
		href = WebAppRequest.getContextPath() + "/assets/modules/report/themes/clear.css";
	}
	$('#skin').attr({
		"href" : href
	});
};

ReportTemplate.fullScreen = function() {
	var uid = $('#rpUid').val();
	var url = WebAppRequest.getContextPath() + '/report/uid/' + uid;
	ReportTemplate.winOpen(url, uid);
};

ReportTemplate.showChart = function(e) {
	var title = $('#rpName').val() + "(图表)";
	var id = $('#rpId').val();
	var uid = $('#rpUid').val();
	parent.showChart(title, id, uid);
};

ReportTemplate.checkedAllStatColumn = function(e) {
	var checked = $("input[name='checkAllStatColumn']").prop("checked");
	$("input[name='statColumns']").prop("checked", checked);
};

ReportTemplate.winOpen = function(url, name) {
	var width = window.screen.availWidth - 10;
	var height = window.screen.availHeight - 60;
	window.open(url, name, 'width=' + width + ',height=' + height
			+ ',top=0,left=0,toolbar=no,menubar=no,scrollbars=yes, resizable=yes,location=no, status=no');
};

ReportTemplate.getBytes = function(str) {
	var totalLength = 0;
	var i;
	var charCode;
	for (i = 0; i < str.length; i++) {
		charCode = str.charCodeAt(i);
		if (charCode < 0x007f) {
			totalLength = totalLength + 1;
		} else if ((0x0080 <= charCode) && (charCode <= 0x07ff)) {
			totalLength += 2;
		} else if ((0x0800 <= charCode) && (charCode <= 0xffff)) {
			totalLength += 3;
		}
	}
	return totalLength;
};

function currentTime(){
	var d = new Date(),str = '';
	str += d.getFullYear()+'-';
	str  += d.getMonth() + 1+'-';
	str  += d.getDate()+' ';
	str += d.getHours()+':';
	str  += d.getMinutes()+':';
	str+= d.getSeconds()+'';
	return str;
};<|endoftext|>


<commit_before>var Crafty = require('../core/core.js');

/**@
 * #Text
 * @category Graphics
 * @kind Component
 *
 * @trigger Invalidate - when the text is changed
 * @requires Canvas or DOM
 * Component to make a text entity.
 *
 * By default, text will have the style "10px sans-serif".
 *
 * @note An entity with the text component is just text! If you want to write text
 * inside an image, you need one entity for the text and another entity for the image.
 * More tips for writing text inside an image: (1) Use the z-index (from 2D component)
 * to ensure that the text is on top of the image, not the other way around; (2)
 * use .attach() (from 2D component) to glue the text to the image so they move and
 * rotate together.
 *
 * @note For DOM (but not canvas) text entities, various font settings (like
 * text-decoration and text-align) can be set using `.css()` (see DOM component). But
 * you cannot use `.css()` to set the properties which are controlled by `.textFont()`
 * or `.textColor()` -- the settings will be ignored.
 *
 * @note If you use canvas text with glyphs that are taller than standard letters, portions of the glyphs might be cut off.
 */
Crafty.c("Text", {
    _text: "",
    defaultSize: "10px",
    defaultFamily: "sans-serif",
    defaultVariant: "normal",
    defaultLineHeight: "normal",
    defaultTextAlign: "left",
    ready: true,

    init: function () {
        this.requires("2D");
        this._textFont = {
            "type": "",
            "weight": "",
            "size": this.defaultSize,
            "lineHeight":this.defaultLineHeight,
            "family": this.defaultFamily,
            "variant": this.defaultVariant
        };
        this._textAlign = this.defaultTextAlign;
    },

    events: {
        "Draw": function (e) {
            var font = this._fontString();

            if (e.type === "DOM") {
                var el = this._element,
                    style = el.style;

                style.color = this._textColor;
                style.font = font;
                style.textAlign = this._textAlign;
                el.innerHTML = this._text;
            } else if (e.type === "canvas") {
                var context = e.ctx;

                context.save();

                context.textBaseline = "top";
                context.fillStyle = this._textColor || "rgb(0,0,0)";
                context.font = font;
                context.textAlign = this._textAlign;

                context.fillText(this._text, e.pos._x, e.pos._y);

                context.restore();
            }
        }
    },

    remove: function(){
        // Clean up the dynamic text update
        this.unbind(this._textUpdateEvent, this._dynamicTextUpdate);
    },

    // takes a CSS font-size string and gets the height of the resulting font in px
    _getFontHeight: (function(){
        // regex for grabbing the first string of letters
        var re = /([a-zA-Z]+)\b/;
        // From the CSS spec.  "em" and "ex" are undefined on a canvas.
        var multipliers = {
            "px": 1,
            "pt": 4/3,
            "pc": 16,
            "cm": 96/2.54,
            "mm": 96/25.4,
            "in": 96,
            "em": undefined,
            "ex": undefined
        };
        return function (font){
            var number = parseFloat(font);
            var match = re.exec(font);
            var unit =  match ? match[1] : "px";
            if (multipliers[unit] !== undefined)
                return Math.ceil(number * multipliers[unit]);
            else
                return Math.ceil(number);
        };
    })(),

    /**@
     * #.text
     * @comp Text
     * @kind Method
     *
     * @sign public this .text(String text)
     * @param text - String of text that will be inserted into the DOM or Canvas element.
     *
     * @sign public this .text(Function textGenerator)
     * @param textGenerator - A function that returns a string.
     *        It will be immediately invoked in the context of the entity, with the result used as the text to display.
     *
     * This method will update the text inside the entity.
     *
     * If you need to reference attributes on the entity itself you can pass a function instead of a string.
     *
     * If dynamic text generation is turned on, the function will then be reevaluated as necessary.
     *
     * @see .dynamicTextGeneration
     *
     * @example
     * ~~~
     * Crafty.e("2D, DOM, Text").attr({ x: 100, y: 100 }).text("Look at me!!");
     *
     * Crafty.e("2D, DOM, Text").attr({ x: 100, y: 100 })
     *     .text(function () { return "My position is " + this._x });
     *
     * Crafty.e("2D, Canvas, Text").attr({ x: 100, y: 100 }).text("Look at me!!");
     *
     * Crafty.e("2D, Canvas, Text").attr({ x: 100, y: 100 })
     *     .text(function () { return "My position is " + this._x });
     * ~~~
     */
    _textGenerator: null,
    text: function (text) {
        if (!(typeof text !== "undefined" && text !== null)) return this._text;
        if (typeof (text) === "function"){
            this._text = text.call(this);
            this._textGenerator = text;
        } else {
            this._text = text;
            this._textGenerator = null;
        }

        if (this.has("Canvas") )
            this._resizeForCanvas();

        this.trigger("Invalidate");
        return this;
    },

    /**@
     * #.dynamicTextGeneration
     * @comp Text
     * @kind Method
     *
     * @sign public this .dynamicTextGeneration(bool dynamicTextOn[, string textUpdateEvent])
     * @param dynamicTextOn - A flag that indicates whether dyanamic text should be on or off.
     * @param textUpdateEvent - The name of the event which will trigger text to be updated.  Defaults to "EnterFrame".  (This parameter does nothing if dynamicTextOn is false.)
     *
     * Turns on (or off) dynamic text generation for this entity.  While dynamic text generation is on,
     * if the `.text()` method is called with a text generating function, the text will be updated each frame.
     *
     * If textUpdateEvent is provided, text generation will be bound to that event instead of "EnterFrame".
     *
     * @note Dynamic text generation could cause performance issues when the entity is attached to a Canvas layer.
     *
     * @example
     * ~~~
     * Crafty.e("2D, DOM, Text, Motion").attr({ x: 100, y: 100, vx: 10 })
     *     .text(function () { return "My position is " + this._x })
     *     .dynamicTextGeneration(true)
     * ~~~
     * The above example will update the text with the entities position as it changes.
     */
    _dynamicTextOn: false,
    _textUpdateEvent: null,
    _dynamicTextUpdate: function(){
        if (!this._textGenerator) return;
        this.text(this._textGenerator);
    },
    dynamicTextGeneration: function(dynamicTextOn, textUpdateEvent) {
        this.unbind(this._textUpdateEvent, this._dynamicTextUpdate);
        if (dynamicTextOn) {
            this._textUpdateEvent = textUpdateEvent || "EnterFrame";
            this.bind(this._textUpdateEvent, this._dynamicTextUpdate);
        }
        return this;
    },

    // Calculates the height and width of text on the canvas
    // Width is found by using the canvas measureText function
    // Height is only estimated -- it calculates the font size in pixels, and sets the height to 110% of that.
    _resizeForCanvas: function(){
        var ctx = this._drawContext;
        ctx.font = this._fontString();
        this.w = ctx.measureText(this._text).width;

        var size = (this._textFont.size || this.defaultSize);
        this.h = 1.1 * this._getFontHeight(size);

        /* Offset the MBR for text alignment*/
        if (this._textAlign === 'left' || this._textAlign === 'start') {
            this.offsetBoundary(0, 0, 0, 0);
        } else if (this._textAlign === 'center') {
            this.offsetBoundary(this.w/2, 0, -this.w/2, 0);
        } else if (this._textAlign === 'end' || this._textAlign === 'right') {
            this.offsetBoundary(this.w, 0, -this.w, 0);
        }
    },

    // Returns the font string to use
    _fontString: function(){
        return this._textFont.type + ' ' + this._textFont.variant  + ' ' + this._textFont.weight + ' ' + this._textFont.size  + ' / ' + this._textFont.lineHeight + ' ' + this._textFont.family;
    },
    /**@
     * #.textColor
     * @comp Text
     * @kind Method
     *
     * @sign public this .textColor(String color)
     * @param color - The color in name, hex, rgb or rgba
     *
     * Change the color of the text. You can use HEX, rgb and rgba colors.
     *
     * If you want the text to be transparent, you should use rgba where you can define alphaChannel.
     *
     * @example
     * ~~~
     * Crafty.e("2D, DOM, Text").attr({ x: 100, y: 100 }).text("Look at me!!")
     *   .textColor('#FF0000');
     *
     * Crafty.e("2D, Canvas, Text").attr({ x: 100, y: 100 }).text('Look at me!!')
     *   .textColor('rgba(0, 255, 0, 0.5)');
     *
     * Crafty.e("2D, Canvas, Text").attr({ x: 100, y: 100 }).text('Look at me!!')
     *   .textColor('white');
     * ~~~
     * @see Crafty.assignColor
     */
    textColor: function (color) {
        Crafty.assignColor(color, this);
        this._textColor = "rgba(" + this._red + ", " + this._green + ", " + this._blue + ", " + this._strength + ")";
        this.trigger("Invalidate");
        return this;
    },

    /**@
     * @comp Text
     * @kind Method
     *
     * @sign public this .textAlign(String alignment)
     * @param alignment - The new alignment of the text.
     *
     * Change the alignment of the text. Valid values are 'start', 'end, 'left', 'center', or 'right'.
     */
    textAlign: function(alignment) {
        this._textAlign = alignment;
        if (this.has("Canvas"))
            this._resizeForCanvas();
        this.trigger("Invalidate");
        return this;
    },

    /**@
     * #.textFont
     * @comp Text
     * @kind Method
     *
     * @triggers Invalidate
     * @sign public this .textFont(String key, * value)
     * @param key - Property of the entity to modify
     * @param value - Value to set the property to
     *
     * @sign public this .textFont(Object map)
     * @param map - Object where the key is the property to modify and the value as the property value
     *
     * Use this method to set font property of the text entity.  Possible values are: type, weight, size, family, lineHeight, and variant.
     *
     * When rendered by the canvas, lineHeight and variant will be ignored.
     *
     * @example
     * ~~~
     * Crafty.e("2D, DOM, Text").textFont({ type: 'italic', family: 'Arial' });
     * Crafty.e("2D, Canvas, Text").textFont({ size: '20px', weight: 'bold' });
     *
     * Crafty.e("2D, Canvas, Text").textFont("type", "italic");
     * Crafty.e("2D, Canvas, Text").textFont("type"); // italic
     * ~~~
     */
    textFont: function (key, value) {
        if (arguments.length === 1) {
            //if just the key, return the value
            if (typeof key === "string") {
                return this._textFont[key];
            }

            if (typeof key === "object") {
                for (var propertyKey in key) {
                    if(propertyKey === 'family'){
                        this._textFont[propertyKey] = "'" + key[propertyKey] + "'";
                    } else {
                        this._textFont[propertyKey] = key[propertyKey];
                    }
                }
            }
        } else {
            this._textFont[key] = value;
        }

        if (this.has("Canvas") )
            this._resizeForCanvas();

        this.trigger("Invalidate");
        return this;
    },
    /**@
     * #.unselectable
     * @comp Text
     * @kind Method
     *
     * @triggers Invalidate
     * @sign public this .unselectable()
     *
     * This method sets the text so that it cannot be selected (highlighted) by dragging.
     * (Canvas text can never be highlighted, so this only matters for DOM text.)
     * Works by changing the css property "user-select" and its variants.
     *
     * Likewise, this sets the mouseover cursor to be "default" (arrow), not "text" (I-beam)
     *
     * @example
     * ~~~
     * Crafty.e("2D, DOM, Text").text('This text cannot be highlighted!').unselectable();
     * ~~~
     */
    unselectable: function () {
        // http://stackoverflow.com/questions/826782/css-rule-to-disable-text-selection-highlighting
        if (this.has("DOM")) {
            this.css({
                '-webkit-touch-callout': 'none',
                '-webkit-user-select': 'none',
                '-khtml-user-select': 'none',
                '-moz-user-select': 'none',
                '-ms-user-select': 'none',
                'user-select': 'none',
                'cursor': 'default'
            });
            this.trigger("Invalidate");
        }
        return this;
    }

});
<commit_msg>Doc fixes and consistency for Text<commit_after>var Crafty = require('../core/core.js');

/**@
 * #Text
 * @category Graphics
 * @kind Component
 *
 * @trigger Invalidate - when the text is changed
 * @requires Canvas or DOM
 * Component to make a text entity.
 *
 * By default, text will have the style "10px sans-serif".
 *
 * @note An entity with the text component is just text! If you want to write text
 * inside an image, you need one entity for the text and another entity for the image.
 * More tips for writing text inside an image: (1) Use the z-index (from 2D component)
 * to ensure that the text is on top of the image, not the other way around; (2)
 * use .attach() (from 2D component) to glue the text to the image so they move and
 * rotate together.
 *
 * @note For DOM (but not canvas) text entities, various font settings (such as
 * text-decoration) can be set using `.css()` (see DOM component). But
 * you cannot use `.css()` to set the properties which are controlled by `.textFont()`,
 *  `.textColor()`, or `.textAlign()` -- the settings will be ignored.
 *
 * @note If you use canvas text with glyphs that are taller than standard letters, portions of the glyphs might be cut off.
 */
Crafty.c("Text", {
    _text: "",
    defaultSize: "10px",
    defaultFamily: "sans-serif",
    defaultVariant: "normal",
    defaultLineHeight: "normal",
    defaultTextAlign: "left",
    ready: true,

    init: function () {
        this.requires("2D");
        this._textFont = {
            "type": "",
            "weight": "",
            "size": this.defaultSize,
            "lineHeight":this.defaultLineHeight,
            "family": this.defaultFamily,
            "variant": this.defaultVariant
        };
        this._textAlign = this.defaultTextAlign;
    },

    events: {
        "Draw": function (e) {
            var font = this._fontString();

            if (e.type === "DOM") {
                var el = this._element,
                    style = el.style;

                style.color = this._textColor;
                style.font = font;
                style.textAlign = this._textAlign;
                el.innerHTML = this._text;
            } else if (e.type === "canvas") {
                var context = e.ctx;

                context.save();

                context.textBaseline = "top";
                context.fillStyle = this._textColor || "rgb(0,0,0)";
                context.font = font;
                context.textAlign = this._textAlign;

                context.fillText(this._text, e.pos._x, e.pos._y);

                context.restore();
            }
        }
    },

    remove: function(){
        // Clean up the dynamic text update
        this.unbind(this._textUpdateEvent, this._dynamicTextUpdate);
    },

    // takes a CSS font-size string and gets the height of the resulting font in px
    _getFontHeight: (function(){
        // regex for grabbing the first string of letters
        var re = /([a-zA-Z]+)\b/;
        // From the CSS spec.  "em" and "ex" are undefined on a canvas.
        var multipliers = {
            "px": 1,
            "pt": 4/3,
            "pc": 16,
            "cm": 96/2.54,
            "mm": 96/25.4,
            "in": 96,
            "em": undefined,
            "ex": undefined
        };
        return function (font){
            var number = parseFloat(font);
            var match = re.exec(font);
            var unit =  match ? match[1] : "px";
            if (multipliers[unit] !== undefined)
                return Math.ceil(number * multipliers[unit]);
            else
                return Math.ceil(number);
        };
    })(),

    /**@
     * #.text
     * @comp Text
     * @kind Method
     *
     * @sign public this .text(String text)
     * @param text - String of text that will be inserted into the DOM or Canvas element.
     *
     * @sign public this .text(Function textGenerator)
     * @param textGenerator - A function that returns a string.
     *        It will be immediately invoked in the context of the entity, with the result used as the text to display.
     *
     * This method will update the text inside the entity.
     *
     * If you need to reference attributes on the entity itself you can pass a function instead of a string.
     *
     * If dynamic text generation is turned on, the function will then be reevaluated as necessary.
     *
     * @see .dynamicTextGeneration
     *
     * @example
     * ~~~
     * Crafty.e("2D, DOM, Text").attr({ x: 100, y: 100 }).text("Look at me!!");
     *
     * Crafty.e("2D, DOM, Text").attr({ x: 100, y: 100 })
     *     .text(function () { return "My position is " + this._x });
     *
     * Crafty.e("2D, Canvas, Text").attr({ x: 100, y: 100 }).text("Look at me!!");
     *
     * Crafty.e("2D, Canvas, Text").attr({ x: 100, y: 100 })
     *     .text(function () { return "My position is " + this._x });
     * ~~~
     */
    _textGenerator: null,
    text: function (text) {
        if (!(typeof text !== "undefined" && text !== null)) return this._text;
        if (typeof (text) === "function"){
            this._text = text.call(this);
            this._textGenerator = text;
        } else {
            this._text = text;
            this._textGenerator = null;
        }

        if (this.has("Canvas") )
            this._resizeForCanvas();

        this.trigger("Invalidate");
        return this;
    },

    /**@
     * #.dynamicTextGeneration
     * @comp Text
     * @kind Method
     *
     * @sign public this .dynamicTextGeneration(bool dynamicTextOn[, string textUpdateEvent])
     * @param dynamicTextOn - A flag that indicates whether dyanamic text should be on or off.
     * @param textUpdateEvent - The name of the event which will trigger text to be updated.  Defaults to "EnterFrame".  (This parameter does nothing if dynamicTextOn is false.)
     *
     * Turns on (or off) dynamic text generation for this entity.  While dynamic text generation is on,
     * if the `.text()` method is called with a text generating function, the text will be updated each frame.
     *
     * If textUpdateEvent is provided, text generation will be bound to that event instead of "EnterFrame".
     *
     * @note Dynamic text generation could cause performance issues when the entity is attached to a Canvas layer.
     *
     * @example
     * ~~~
     * Crafty.e("2D, DOM, Text, Motion").attr({ x: 100, y: 100, vx: 10 })
     *     .text(function () { return "My position is " + this._x })
     *     .dynamicTextGeneration(true)
     * ~~~
     * The above example will update the text with the entities position as it changes.
     */
    _dynamicTextOn: false,
    _textUpdateEvent: null,
    _dynamicTextUpdate: function(){
        if (!this._textGenerator) return;
        this.text(this._textGenerator);
    },
    dynamicTextGeneration: function(dynamicTextOn, textUpdateEvent) {
        this.unbind(this._textUpdateEvent, this._dynamicTextUpdate);
        if (dynamicTextOn) {
            this._textUpdateEvent = textUpdateEvent || "EnterFrame";
            this.bind(this._textUpdateEvent, this._dynamicTextUpdate);
        }
        return this;
    },

    // Calculates the height and width of text on the canvas
    // Width is found by using the canvas measureText function
    // Height is only estimated -- it calculates the font size in pixels, and sets the height to 110% of that.
    _resizeForCanvas: function(){
        var ctx = this._drawContext;
        ctx.font = this._fontString();
        this.w = ctx.measureText(this._text).width;

        var size = (this._textFont.size || this.defaultSize);
        this.h = 1.1 * this._getFontHeight(size);

        /* Offset the MBR for text alignment*/
        if (this._textAlign === 'left' || this._textAlign === 'start') {
            this.offsetBoundary(0, 0, 0, 0);
        } else if (this._textAlign === 'center') {
            this.offsetBoundary(this.w/2, 0, -this.w/2, 0);
        } else if (this._textAlign === 'end' || this._textAlign === 'right') {
            this.offsetBoundary(this.w, 0, -this.w, 0);
        }
    },

    // Returns the font string to use
    _fontString: function(){
        return this._textFont.type + ' ' + this._textFont.variant  + ' ' + this._textFont.weight + ' ' + this._textFont.size  + ' / ' + this._textFont.lineHeight + ' ' + this._textFont.family;
    },
    /**@
     * #.textColor
     * @comp Text
     * @kind Method
     *
     * @sign public this .textColor(String color)
     * @param color - The color in name, hex, rgb or rgba
     *
     * Change the color of the text. You can use HEX, rgb and rgba colors.
     *
     * If you want the text to be transparent, you should use rgba where you can define alphaChannel.
     *
     * @example
     * ~~~
     * Crafty.e("2D, DOM, Text").attr({ x: 100, y: 100 }).text("Look at me!!")
     *   .textColor('#FF0000');
     *
     * Crafty.e("2D, Canvas, Text").attr({ x: 100, y: 100 }).text('Look at me!!')
     *   .textColor('rgba(0, 255, 0, 0.5)');
     *
     * Crafty.e("2D, Canvas, Text").attr({ x: 100, y: 100 }).text('Look at me!!')
     *   .textColor('white');
     * ~~~
     * @see Crafty.assignColor
     */
    textColor: function (color) {
        Crafty.assignColor(color, this);
        this._textColor = "rgba(" + this._red + ", " + this._green + ", " + this._blue + ", " + this._strength + ")";
        this.trigger("Invalidate");
        return this;
    },

    /**@
     * #.textAlign
     * @comp Text
     * @kind Method
     *
     * @sign public this .textAlign(String alignment)
     * @param alignment - The new alignment of the text.
     *
     * Change the alignment of the text. Valid values are 'start', 'end, 'left', 'center', or 'right'.
     */
    textAlign: function(alignment) {
        this._textAlign = alignment;
        if (this.has("Canvas"))
            this._resizeForCanvas();
        this.trigger("Invalidate");
        return this;
    },

    /**@
     * #.textFont
     * @comp Text
     * @kind Method
     *
     * @triggers Invalidate
     * @sign public this .textFont(String key, * value)
     * @param key - Property of the entity to modify
     * @param value - Value to set the property to
     *
     * @sign public this .textFont(Object map)
     * @param map - Object where the key is the property to modify and the value as the property value
     *
     * Use this method to set font property of the text entity.  Possible values are: type, weight, size, family, lineHeight, and variant.
     *
     * When rendered by the canvas, lineHeight and variant will be ignored.
     *
     * @example
     * ~~~
     * Crafty.e("2D, DOM, Text").textFont({ type: 'italic', family: 'Arial' });
     * Crafty.e("2D, Canvas, Text").textFont({ size: '20px', weight: 'bold' });
     *
     * Crafty.e("2D, Canvas, Text").textFont("type", "italic");
     * Crafty.e("2D, Canvas, Text").textFont("type"); // italic
     * ~~~
     */
    textFont: function (key, value) {
        if (arguments.length === 1) {
            //if just the key, return the value
            if (typeof key === "string") {
                return this._textFont[key];
            }

            if (typeof key === "object") {
                for (var propertyKey in key) {
                    if(propertyKey === 'family'){
                        this._textFont[propertyKey] = "'" + key[propertyKey] + "'";
                    } else {
                        this._textFont[propertyKey] = key[propertyKey];
                    }
                }
            }
        } else {
            this._textFont[key] = value;
        }

        if (this.has("Canvas") )
            this._resizeForCanvas();

        this.trigger("Invalidate");
        return this;
    },
    /**@
     * #.unselectable
     * @comp Text
     * @kind Method
     *
     * @triggers Invalidate
     * @sign public this .unselectable()
     *
     * This method sets the text so that it cannot be selected (highlighted) by dragging.
     * (Canvas text can never be highlighted, so this only matters for DOM text.)
     * Works by changing the css property "user-select" and its variants.
     *
     * Likewise, this sets the mouseover cursor to be "default" (arrow), not "text" (I-beam)
     *
     * @example
     * ~~~
     * Crafty.e("2D, DOM, Text").text('This text cannot be highlighted!').unselectable();
     * ~~~
     */
    unselectable: function () {
        // http://stackoverflow.com/questions/826782/css-rule-to-disable-text-selection-highlighting
        if (this.has("DOM")) {
            this.css({
                '-webkit-touch-callout': 'none',
                '-webkit-user-select': 'none',
                '-khtml-user-select': 'none',
                '-moz-user-select': 'none',
                '-ms-user-select': 'none',
                'user-select': 'none',
                'cursor': 'default'
            });
            this.trigger("Invalidate");
        }
        return this;
    }

});
<|endoftext|>


<commit_before>/*jslint node: true */
'use strict';
var _ = require("lodash");
var util = require('util');

var GraphWrapper = require('./objects/graph');
var PipelineWrapper = require('./objects/pipeline');
var Argument = require('./arguments/argument');
var GremlinFunction = require('./functions/function');

module.exports = (function() {
  function GremlinScript() {
    this.script = '';
    this.params = {};
    this.paramCount = 0;
    this.identifierCount = 0;
  }

  /**
   * Append an arbitrary string to the script as a new line.
   *
   * @private
   * @param {String} line
   */
  GremlinScript.prototype.line = function(line) {
    this.script += line + '\n';
  };

  /**
   * Add bound parameters to the script. This currently only works when
   * using the formatted string. It does not work with gRex helpers/wrappers.
   *
   * @private
   * @param {Array} boundParams
   */
  GremlinScript.prototype.addBoundParams = function(boundParams) {
    var currentParamNames = [];
    var identifier;

    _.each(boundParams, function(boundParam) {
      identifier = 'p'+ this.paramCount++;
      this.params[identifier] = boundParam;
      currentParamNames.push(identifier);
    }, this);

    return currentParamNames;
  };

  /**
   * Handle a string statement using Node util.format() function.
   *
   * @private
   * @param {String} statement
   */
  GremlinScript.prototype.handleString = function(statement) {
    var currentParams = [statement];
    currentParams = currentParams.concat(this.addBoundParams(_.rest(arguments)));

    this.line(util.format.apply(util.format, currentParams));
  };

  /**
   * Handle a helper statement wrapped in one of gRex Wrapper classes
   *
   * @private
   * @param {ObjectWrapper} wrapper
   * @return {ObjectWrapper}
   */
  GremlinScript.prototype.handleHelper = function(wrapper) {
    this.line(wrapper.toGroovy());

    return wrapper;
  };

  /**
   * Identify a statement within the script with the provided optional
   * identifier. Will assign an automatica identifier instead.
   *
   * @public
   * @param {ObjectWrapper} wrapper
   * @param {String} identifier - an optional identifier
   */
  GremlinScript.prototype.var = function(wrapper, identifier) {
    identifier = identifier || 'i'+ this.identifierCount++;
    wrapper.identifier = identifier;
    var prefix = identifier + '=';

    var groovyCode = wrapper.toGroovy ? wrapper.toGroovy() : wrapper;

    this.script += prefix + groovyCode + '\n';

    return wrapper;
  };

  GremlinScript.prototype.appendStatement = function(statement) {
    if (arguments.length > 1) {
      // Assume query('g(%s)', 1) signature
      this.handleString.apply(this, arguments);
    } else if (_.isString(statement)) {
      // Assume query('g.v(1)') signature
      this.line(statement);
    } else if (statement) {
      // Assume query(g.v(1)) signature
      this.handleHelper(statement);
    }

    return this;
  };

  /**
   * Returns a function responsible for handling statements and ultimately
   * appending bits of Gremlin-Groovy to this GremlinScript.
   *
   * @private
   * @return {Function}
   */
  GremlinScript.prototype.getAppender = function() {
    var self = this;

    function GremlinAppender() {
      self.appendStatement.apply(self, arguments);
    }

    /**
     * Proxy some GremlinScript methods/getters to the appender
     */
    GremlinAppender.var = GremlinScript.prototype.var.bind(this);

    Object.defineProperty(GremlinAppender, 'script', {
      get: function() {
        return this.script;
      }.bind(this)
    });

    Object.defineProperty(GremlinAppender, 'params', {
      get: function() {
        return this.params;
      }.bind(this)
    });

    return GremlinAppender;
  };

  return GremlinScript;
})();
<commit_msg>Use self = this; closure in GremlinAppender<commit_after>/*jslint node: true */
'use strict';
var _ = require("lodash");
var util = require('util');

var GraphWrapper = require('./objects/graph');
var PipelineWrapper = require('./objects/pipeline');
var Argument = require('./arguments/argument');
var GremlinFunction = require('./functions/function');

module.exports = (function() {
  function GremlinScript() {
    this.script = '';
    this.params = {};
    this.paramCount = 0;
    this.identifierCount = 0;
  }

  /**
   * Append an arbitrary string to the script as a new line.
   *
   * @private
   * @param {String} line
   */
  GremlinScript.prototype.line = function(line) {
    this.script += line + '\n';
  };

  /**
   * Add bound parameters to the script. This currently only works when
   * using the formatted string. It does not work with gRex helpers/wrappers.
   *
   * @private
   * @param {Array} boundParams
   */
  GremlinScript.prototype.addBoundParams = function(boundParams) {
    var currentParamNames = [];
    var identifier;

    _.each(boundParams, function(boundParam) {
      identifier = 'p'+ this.paramCount++;
      this.params[identifier] = boundParam;
      currentParamNames.push(identifier);
    }, this);

    return currentParamNames;
  };

  /**
   * Handle a string statement using Node util.format() function.
   *
   * @private
   * @param {String} statement
   */
  GremlinScript.prototype.handleString = function(statement) {
    var currentParams = [statement];
    currentParams = currentParams.concat(this.addBoundParams(_.rest(arguments)));

    this.line(util.format.apply(util.format, currentParams));
  };

  /**
   * Handle a helper statement wrapped in one of gRex Wrapper classes
   *
   * @private
   * @param {ObjectWrapper} wrapper
   * @return {ObjectWrapper}
   */
  GremlinScript.prototype.handleHelper = function(wrapper) {
    this.line(wrapper.toGroovy());

    return wrapper;
  };

  /**
   * Identify a statement within the script with the provided optional
   * identifier. Will assign an automatica identifier instead.
   *
   * @public
   * @param {ObjectWrapper} wrapper
   * @param {String} identifier - an optional identifier
   */
  GremlinScript.prototype.var = function(wrapper, identifier) {
    identifier = identifier || 'i'+ this.identifierCount++;
    wrapper.identifier = identifier;
    var prefix = identifier + '=';

    var groovyCode = wrapper.toGroovy ? wrapper.toGroovy() : wrapper;

    this.script += prefix + groovyCode + '\n';

    return wrapper;
  };

  GremlinScript.prototype.appendStatement = function(statement) {
    if (arguments.length > 1) {
      // Assume query('g(%s)', 1) signature
      this.handleString.apply(this, arguments);
    } else if (_.isString(statement)) {
      // Assume query('g.v(1)') signature
      this.line(statement);
    } else if (statement) {
      // Assume query(g.v(1)) signature
      this.handleHelper(statement);
    }

    return this;
  };

  /**
   * Returns a function responsible for handling statements and ultimately
   * appending bits of Gremlin-Groovy to this GremlinScript.
   *
   * @private
   * @return {Function}
   */
  GremlinScript.prototype.getAppender = function() {
    var self = this;

    function GremlinAppender() {
      self.appendStatement.apply(self, arguments);
    }

    /**
     * Proxy some GremlinScript methods/getters to the appender
     */
    GremlinAppender.var = GremlinScript.prototype.var.bind(this);

    Object.defineProperty(GremlinAppender, 'script', {
      get: function() {
        return self.script;
      }
    });

    Object.defineProperty(GremlinAppender, 'params', {
      get: function() {
        return self.params;
      }
    });

    return GremlinAppender;
  };

  return GremlinScript;
})();
<|endoftext|>


<commit_before>/**
 * @depends ../core/AudioletNode.js
 */

var CrossFade = new Class({
    Extends: AudioletNode,
    initialize: function(audiolet, position) {
        AudioletNode.prototype.initialize.apply(this, [audiolet, 3, 1]);
        this.linkNumberOfOutputChannels(0, 0);
        this.position = new AudioletParameter(this, 2, position || 0.5);
    },

    generate: function(inputBuffers, outputBuffers) {
        var inputBufferA = inputBuffers[0];
        var inputBufferB = inputBuffers[1];
        var outputBuffer = outputBuffers[0];

        var inputChannelsA = inputBufferA.channels;
        var inputChannelsB = inputBufferB.channels;
        var outputChannels = outputBuffer.channels;

        if (inputBufferA.isEmpty && inputBufferB.isEmpty) {
            outputBuffer.isEmpty = true;
            return;
        }

        // Local processing variables
        var positionParameter = this.position;
        var position, positionChannel;
        if (positionParameter.isStatic()) {
            position = positionParameter.getValue();
        }
        else {
            positionChannel = positionParameter.getChannel();
        }

        var bufferLength = outputBuffer.length;
        for (var i = 0; i < bufferLength; i++) {
            if (positionChannel) {
                position = positionChannel[i];
            }
            var scaledPosition = position * Math.PI / 2;
            // TODO: Use sine/cos tables?
            var gainA = Math.cos(scaledPosition);
            var gainB = Math.sin(scaledPosition);

            var numberOfChannels = inputBufferA.numberOfChannels;
            for (var j = 0; j < numberOfChannels; j++) {
                var inputChannelA = inputChannelsA[j];
                var inputChannelB = inputChannelsB[j];
                var outputChannel = outputChannels[j];

                var valueA, valueB;
                if (!inputBufferA.isEmpty) {
                    valueA = inputChannelA[i];
                }
                else {
                    valueA = 0;
                }

                if (!inputBufferB.isEmpty) {
                    valueB = inputChannelB[i];
                }
                else {
                    valueB = 0;
                }
                outputChannel[i] = valueA * gainA +
                                   valueB * gainB;
            }
        }
    },

    toString: function() {
        return 'Cross Fader';
    }
});
<commit_msg>Use Sine table for calculating crossfade multipliers<commit_after>/**
 * @depends ../core/AudioletNode.js
 * @depends Sine.js
 */

var CrossFade = new Class({
    Extends: AudioletNode,
    initialize: function(audiolet, position) {
        AudioletNode.prototype.initialize.apply(this, [audiolet, 3, 1]);
        this.linkNumberOfOutputChannels(0, 0);
        this.position = new AudioletParameter(this, 2, position || 0.5);
    },

    generate: function(inputBuffers, outputBuffers) {
        var inputBufferA = inputBuffers[0];
        var inputBufferB = inputBuffers[1];
        var outputBuffer = outputBuffers[0];

        var inputChannelsA = inputBufferA.channels;
        var inputChannelsB = inputBufferB.channels;
        var outputChannels = outputBuffer.channels;

        if (inputBufferA.isEmpty && inputBufferB.isEmpty) {
            outputBuffer.isEmpty = true;
            return;
        }

        // Local processing variables
        var positionParameter = this.position;
        var position, positionChannel;
        if (positionParameter.isStatic()) {
            position = positionParameter.getValue();
        }
        else {
            positionChannel = positionParameter.getChannel();
        }

        var bufferLength = outputBuffer.length;
        for (var i = 0; i < bufferLength; i++) {
            if (positionChannel) {
                position = positionChannel[i];
            }

            var tableLength = Sine.TABLE.length / 4;
            var scaledPosition = Math.floor(position * tableLength);
            // TODO: Use sine/cos tables?
            var gainA = Sine.TABLE[scaledPosition + tableLength];
            var gainB = Sine.TABLE[scaledPosition];

            var numberOfChannels = inputBufferA.numberOfChannels;
            for (var j = 0; j < numberOfChannels; j++) {
                var inputChannelA = inputChannelsA[j];
                var inputChannelB = inputChannelsB[j];
                var outputChannel = outputChannels[j];

                var valueA, valueB;
                if (!inputBufferA.isEmpty) {
                    valueA = inputChannelA[i];
                }
                else {
                    valueA = 0;
                }

                if (!inputBufferB.isEmpty) {
                    valueB = inputChannelB[i];
                }
                else {
                    valueB = 0;
                }
                outputChannel[i] = valueA * gainA +
                                   valueB * gainB;
            }
        }
    },

    toString: function() {
        return 'Cross Fader';
    }
});
<|endoftext|>