PMA.UI Documentation by Pathomation

components/js/components.js

/**
 * PMA.UI.Components contains UI and utility components that interact with a viewport and PMA.core
 * @namespace PMA.UI.Components
 */

import { Resources } from "../../resources/resources";
import { loginSupportsPost } from "../../view/version";

/**
 * Events fired by components
 * @memberof PMA.UI.Components
 * @namespace PMA.UI.Components.Events
 */

/**
 * Events fired by components
 * @readonly
 * @enum {string}
 * @ignore
 */
export const Events = {
    /**
     * Fires when a directory has been selected by a PMA.UI.Components.Tree instance
     * @event PMA.UI.Components.Events.DirectorySelected
     * @param {Object} args
     * @param {string} args.serverUrl
     * @param {string} args.path
     */
    DirectorySelected: "directorySelected",

    /**
     * Fires when a slide image has been selected by a PMA.UI.Components.Tree or PMA.UI.Components.Gallery or a PMA.UI.Components.MetadataTree instance
     * @event PMA.UI.Components.Events.SlideSelected
     * @param {Object} args
     * @param {string} args.serverUrl - The server url this slide belongs to
     * @param {string} args.path - The path to the slide
     * @param {Number} [args.index] - The index of the slide selected in the gallery components (PMA.UI.Components.Gallery only)
     * @param {boolean} [args.userInteraction] - Whether this event was fired by a user interaction or programmatically (PMA.UI.Components.Gallery only)
     */
    SlideSelected: "slideSelected",

    /**
     * Fires when a server node has been selected by a PMA.UI.Components.Tree instance
     * @event PMA.UI.Components.Events.ServerSelected
     * @param {Object} args
     * @param {string} args.serverUrl
     */
    ServerSelected: "serverSelected",

    /**
     * Fires when a server node has been expanded by a PMA.UI.Components.Tree instance
     * @event PMA.UI.Components.Events.ServerExpanded
     * @param {Object} args
     * @param {string} args.serverUrl
     */
    ServerExpanded: "serverExpanded",

    /**
     * Fires when a folder node has been expanded by a PMA.UI.Components.Tree instance
     * @event PMA.UI.Components.Events.DirectoryExpanded
     * @param {Object} args
     * @param {string} args.serverUrl
     * @param {string} args.path
     */
    DirectoryExpanded: "directoryExpanded",

    /**
     * Fires when slide images have been checked by a PMA.UI.Components.Tree
     * @event PMA.UI.Components.Events.MultiSelectionChanged
     * @param {Object[]} args
     * @param {string} args.serverUrl
     * @param {string} args.path
     */
    MultiSelectionChanged: "multiSelectionChanged",

    /**
     * Fires when a slide image has been deselected by a PMA.UI.Components.Tree or PMA.UI.Components.Gallery instance
     * @event PMA.UI.Components.Events.SlideDeSelected
     */
    SlideDeSelected: "slideDeSelected",

    /**
     * Fires when a form has been saved to PMA.core
     * @event PMA.UI.Components.Events.TreeNodeDoubleClicked
     * @param {Object} args
     * @param {string} args.serverUrl
     * @param {string} args.path
     * @param {boolean} isSlide
     */
    TreeNodeDoubleClicked: "treeNodeDoubleClicked",

    /**
     * Fires when image info could not be loaded
     * @event PMA.UI.Components.Events.SlideInfoError
     */
    SlideInfoError: "SlideInfoError",

    /**
     * Fires before a viewport attempts to load an image
     * @event PMA.UI.Components.Events.BeforeSlideLoad
     */
    BeforeSlideLoad: "BeforeSlideLoad",

    /**
     * Fires after a viewport has finished loading an image
     * @event PMA.UI.Components.Events.SlideLoaded
     * @param {Object} args - The slide loaded arguments
     * @param {string} args.serverUrl - The server the slide belongs to
     * @param {string} args.path - The slide path
     * @param {boolean} args.dropped - Whether the slide was loaded via a drag and drop operation
     */
    SlideLoaded: "SlideLoaded",

    /**
     * Fires after when a new annotation has been added
     * @event PMA.UI.Components.Events.AnnotationAdded
     * @param {Object} args
     * @param {Object} args.feature - The annotation object
     */
    AnnotationAdded: "annotationAdded",

    /**
     * Fires when annotation drawing begins
     * @event PMA.UI.Components.Events.AnnotationDrawing
     */
    AnnotationDrawing: "annotationDrawing",

    /**
     * Fires when an annotation has been deleted
     * @event PMA.UI.Components.Events.AnnotationDeleted
     * @param {Object}  args
     * @param {Number}  args.annotationId
     * @param {Object}  args.feature
     */
    AnnotationDeleted: "annotationDeleted",

    /**
     * Fires when an annotation has been modified
     * @event PMA.UI.Components.Events.AnnotationModified
     */
    AnnotationModified: "annotationModified",

    /**
     * Fires when the annotations have been saved to PMA.core
     * @event PMA.UI.Components.Events.AnnotationsSaved
     * @param {Object} args
     * @param {boolean} args.success
     */
    AnnotationsSaved: "annotationsSaved",

    /**
     * Fires when the currently selected annotation(s) have changed
     * @event PMA.UI.Components.Events.AnnotationsSelectionChanged
     * @param {Object[]} args - The selected annotation objects
     */
    AnnotationsSelectionChanged: "annotationsSelectionChanged",

    /**
     * Fires when annotation editing begins
     * @event PMA.UI.Components.Events.AnnotationEditingStarted
     */
    AnnotationEditingStarted: "annotationEditingStarted",

    /**
     * Fires when annotation editing ends
     * @event PMA.UI.Components.Events.AnnotationEditingEnded
     */
    AnnotationEditingEnded: "annotationEditingEnded",

    /**
     * Fires when a form has been saved to PMA.core
     * @event PMA.UI.Components.Events.FormSaved
     * @param {Object} args - The result of the save operation
     * @param {string} args.message - The error message if any
     * @param {boolean} args.success - True if save was successful
     */
    FormSaved: "formSaved",

    /**
     * Fires when a the edit button of a read only form is clicked
     * @event PMA.UI.Components.Events.FormEditClick
     */
    FormEditClick: "formEditClick",

    /**
     * Fires when the state of the synchronization of viewports has changed (enabled/disabled)
     * @event PMA.UI.Components.Events.SyncChanged
     * @param {boolean} enabled - Whether syncronization was enabled
     */
    SyncChanged: "syncChanged",

    /**
     * Fires when a search has started
     * @event PMA.UI.Components.Events.SearchStarted
     */
    SearchStarted: "searchStarted",

    /**
     * Fires when a search has finished
     * @event PMA.UI.Components.Events.SearchFinished
     * @param {Object.<string, string[]>} results - The object containing the result for each server available
     */
    SearchFinished: "searchFinished",

    /**
     * Fires when a search has failed
     * @event PMA.UI.Components.Events.SearchFailed
     * @param {Tree~server} server - The object containing the server failed to fetch search results for
     */
    SearchFailed: "searchFailed",

    /** 
     * Fires when a form value is expanded by a PMA.UI.Components.MetadataTree
     * @event PMA.UI.Components.Events.ValueExpanded
     * @param {Object} args - The arguments passed to this event
     * @param {string} args.serverUrl - The server url that the form value belongs to
     * @param {string[]} args.slides - An array of the paths for the slides belonging to the formvalue expanded
     */
    ValueExpanded: "valueExpanded",

    /** 
     * Fires when data are dropped on a PMA.UI.Components.Gallery
     * @event PMA.UI.Components.Events.Dropped
     * @param {Object} args - The arguments passed to this event
     * @param {string} args.serverUrl - The server url
     * @param {string} args.path - The path to a slide or directory dropped 
     * @param {boolean} args.isFolder - Whether a directory was dropped  
     * @param {boolean} args.append - Whether the slide/directory was appended or replaced(alt key was pressed)
     */
    Dropped: "dropped",

    /**
     * Fires when a session id authentication fails
     * @event PMA.UI.Components.Events.SessionIdLoginFailed
     * @param {Object} args - The arguments passed to this event
     * @param {string} args.serverUrl - The server url
     */
    SessionIdLoginFailed: "sessionIdLoginFailed",

    /** 
     * Fires when a slide is dropped in the {@link SlideLoader}
     * @event PMA.UI.Components.Events.BeforeDrop
     * @param {Object} args - The arguments passed to this event
     * @param {string} args.serverUrl - The serverUrl
     * @param {string} args.path - The path to the slide  dropped
     * @param {Object} args.node - The node metadata object
     * @returns {boolean} - Whether to cancel the slide loading or not
     */
    BeforeDrop: "beforeDrop",

    /** 
     * Fires when there are changes in the {@link ImageUpload} component
     * @event PMA.UI.Components.Events.OnProgress
     * @param {Object} args - The arguments passed to this event
     * @param {string} args.type - Type of OnProgress event
     * @param {ImageUpload~Slide | null} args.slide - The slide this event is about or null
     * @param {ImageUpload~Slide[]} args.slides - The list of processed slides
     */
    OnProgress: "onProgress"
};

export
    /**
     * PMA.core API methods
     * @readonly
     * @enum {string}
     * @memberof PMA.UI.Components
     */
    const ApiMethods = {
        Authenticate: "Authenticate",
        GetFiles: "GetFiles",
        GetDirectories: "GetDirectories",
        GetImageInfo: "GetImageInfo",
        GetImagesInfo: "GetImagesInfo",
        DeAuthenticate: "DeAuthenticate",
        GetForms: "GetForms",
        GetFormDefinitions: "GetFormDefinitions",
        GetFormSubmissions: "GetFormSubmissions",
        GetForm: "GetForm",
        SaveFormDefinition: "SaveFormDefinition",
        DeleteFormDefinition: "DeleteFormDefinition",
        SaveFormData: "SaveFormData",
        GetFormData: "GetFormData",
        GetAnnotations: "GetAnnotations",
        AddAnnotation: "AddAnnotation",
        UpdateAnnotation: "UpdateAnnotation",
        DeleteAnnotation: "DeleteAnnotation",
        GetVersionInfo: "GetVersionInfo",
        QueryFilename: "Filename",
        SaveAnnotations: "SaveAnnotations",
        DistinctValues: "DistinctValues",
        Metadata: "Metadata",
        GetEvents: "GetEvents",
        RunScripts: "Run",
        CreateDirectory: "CreateDirectory",
        DeleteDirectory: "DeleteDirectory",
        RenameDirectory: "RenameDirectory",
        DeleteSlide: "DeleteSlide",
        Upload: "Upload"
    };

export
    /**
     * PMA.core scopes for the GetSlides API method
     * @readonly
     * @memberof PMA.UI.Components
     * @enum {string}
     */
    const GetSlidesScope = {
        Normal: 0,
        OneLevel: 1,
        Recursive: 2
    };

export
    /**
     * Available components to render
     * @readonly
     * @enum {string}
     * @memberof PMA.UI.Components
     */
    const GalleryRenderOptions = {
        /** Render both thumbnail and barcode*/
        All: "all",
        /** Render thumbnail only */
        Thumbnail: "thumbnail",
        /** Render barcode only */
        Barcode: "barcode",
    };

export
    /** 
     * The mime type used for drag n drop data transfer
     * @typedef {string} PMA.UI.Components~DragDropMimeType
     */
    const DragDropMimeType = "application/x-pma-node";

/**
 * An object expected for drag and drop features 
 * @typedef {Object} PMA.UI.Components~dragDropObject
 * @property {string} serverUrl - The serverUrl
 * @property {string} path - The path to a slide or directory
 * @property {boolean} isFolder - Whether this is a path or a directory
 */

// static variables

//export let _sessionList = {};
export const _sessionList = {
    _list: {},
    set(key, value) { this._list[key] = value; },
    get() { return this._list; },
    clear() { this._list = {}; },
};
var alertedForIncompatibility = false;

function combinePath(parts) {
    var rx = /^\/+|\/+$/g; // trim left and right trailing slash
    var rxLeftOnly = /^\/+/g; // trim left trailing slash
    for (var i = 0, max = parts.length; i < max; i++) {
        if (i === max - 1) {
            parts[i] = parts[i].replace(rxLeftOnly, "");
        } else {
            parts[i] = parts[i].replace(rx, "");
        }
    }

    return parts.join("/");
}

export const parseJson = function (response) {
    if (response === null || response === undefined || response === "") {
        return null;
    }

    var obj = JSON.parse(response);

    // if (obj.hasOwnProperty("d")) {
    if (Object.prototype.hasOwnProperty.call(obj, "d")) {
        return obj.d;
    } else {
        return obj;
    }
};

export
    /**
     * Tries to parse the data passed from a drag and drop operation
     * @param {Object} dataTransfer - The browser dataTransfer object
     * @returns {PMA.UI.Components~dragDropObject} - The parsed drag and drop object or null
     * @ignore
     */
    const parseDragData = function (dataTransfer) {
        try {
            var d1 = dataTransfer.getData(PMA.UI.Components.DragDropMimeType); // eslint-disable-line no-undef
            return JSON.parse(d1);
        } catch (e) { } // eslint-disable-line no-empty

        //Try the text for IE compatibility
        try {
            var d2 = dataTransfer.getData("text");
            return JSON.parse(d2);
        } catch (e) { } // eslint-disable-line no-empty

        return null;
    };

/**
 * Encodes an object so that it can be passed as an ajax data object
 * @param {object} data 
 * @returns a www-encoded serialized string of the data object 
 * @ignore
 */
export const formify = function (data) {
    if (data && typeof data === "object") {
        var sData = "";

        var i = 0;
        for (var property in data) {
            // if (data.hasOwnProperty(property)) {
            if (Object.prototype.hasOwnProperty.call(data, property)) {
                if (i > 0) {
                    sData += "&";
                }

                sData += encodeURIComponent(property) + "=" + encodeURIComponent(data[property]);
                i++;
            }
        }

        return sData;
    }

    return data;
};

/**
 * @param {string} url 
 * @param {string} method 
 * @param {string} contentType 
 * @param {object} data 
 * @param {function} success 
 * @param {function} failure 
 * @ignore
 */
export const ajax = function (url, method, contentType, data, success, failure) {
    method = method.toUpperCase();
    var sData = null;

    if (contentType && contentType.toLowerCase && contentType.toLowerCase() === "application/json") {
        sData = JSON.stringify(data);
    } else {
        sData = formify(data);
    }

    if (sData && method == "GET") {
        url = url + "?" + sData;
        sData = null;
    }

    var http = new XMLHttpRequest();
    http.open(method, url, true);

    if (method == "POST" && !contentType) {
        http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    } else if (contentType) {
        http.setRequestHeader('Content-Type', contentType);
    }

    http.onreadystatechange = function () {
        if (http.readyState === 4) {
            if (http.status === 200) {
                if (typeof success === "function") {
                    success(http);
                }
            } else if (typeof failure === "function") {
                failure(http);
            }
        }
    };

    if (sData) {
        http.send(sData);
    } else {
        http.send();
    }
};

/**
 * Invokes a PMA.core API method. This method will properly encode all provided parameters.
 * @param {Object} options
 * @param {string} options.serverUrl - The URL of the PMA.core instance
 * @param {string} options.method - The API method to call
 * @param {string} options.httpMethod - The HTTP verb
 * @param {object} options.data - The parameters to pass to the API method
 * @param {string} [options.apiPath="api"] - The API path to append to the server URL
 * @param {string} [options.contentType=""]
 * @param {boolean} [options.webapi=false] - Whether the api call is a webapi call
 * @param {function} [options.success] - Function to call upon successful method invocation
 * @param {function} [options.failure] - Function to call upon unsuccessful method invocation
 * @ignore
 */
export const callApiMethod = function (options) {
    var httpMethod = "GET";
    if (options.httpMethod) {
        httpMethod = options.httpMethod;
    }

    if (!options.apiPath) {
        options.apiPath = "api";
    }

    let infix = options.webapi === true ? "/" : "/json/";
    if (options.webapi && httpMethod == "POST" && options.data) {
        options.method += "?sessionId=" + options.data.sessionID;
    }

    ajax(combinePath([options.serverUrl, options.apiPath + infix, options.method]), httpMethod, options.contentType, options.data, options.success, options.failure);
};

export const callApiMethodPromise = function (options) {
    return new Promise(function (resolve, reject) {
        const localOptions = {
            ...options,
            success: resolve,
            failure: reject
        };

        callApiMethod(localOptions)
    });
}

/**
 * Authenticates against a PMA.core server
 * @param  {string} serverUrl
 * @param  {string} username
 * @param  {string} password
 * @param  {string} caller
 * @param  {function} success
 * @param  {function} failure
 * @ignore
 */
export const login = function (serverUrl, username, password, caller, success, failure) {
    if (typeof caller !== "string") {
        throw "Caller parameter not supplied";
    }

    function authFailure(http) {
        if (typeof failure === "function") {
            if (!http.responseText || http.responseText.length === 0 || http.responseType === "") {
                failure({ Message: Resources.translate("Authentication failed") });
            } else {
                try {
                    const response = parseJson(http.responseText);
                    failure(response);
                } catch {
                    failure({ Message: Resources.translate("Authentication failed") });
                }
            }
        }
    }

    callApiMethodPromise({
        serverUrl: serverUrl,
        method: ApiMethods.GetVersionInfo
    })
        .then((versionHttp) => {
            let usePost = loginSupportsPost(JSON.parse(versionHttp.responseText));

            callApiMethod({
                serverUrl: serverUrl,
                method: ApiMethods.Authenticate,
                httpMethod: usePost ? "POST" : "GET",
                contentType: usePost ? "application/json" : null,
                data: { username: username, password: password, caller: caller },
                success: function (http) {
                    var response = parseJson(http.responseText);
                    if (response && response.Success === true) {
                        // store session ID for this server in global cache
                        _sessionList.set(serverUrl, response);

                        if (typeof success === "function") {
                            success(response.SessionId);
                        }
                    } else {
                        authFailure(http);
                    }
                },
                failure: authFailure
            });
        }, authFailure);
};
/**
 * Returns a URL that points to the thumbnail of a slide image
 * @name getThumbnailUrl
 * @memberof PMA.UI.Components
 * @function
 * @param {string} serverUrl The PMA.core server URL
 * @param {string} sessionId The session id acquired by authenticating to the server
 * @param {string} pathOrUid The path or UID of the slide 
 * @param {Number} [orientation=0] The desired rotation of the thumbnail in degrees
 * @param {Number} [width=0] The desired width of the thumbnail in pixels
 * @param {Number} [height=0] The desired height of the thumbnail in pixels
 */
export const getThumbnailUrl = function (serverUrl, sessionId, pathOrUid, orientation, width, height) {
    return combinePath([serverUrl, "thumbnail"]) +
        "?sessionID=" + encodeURIComponent(sessionId) +
        "&pathOrUid=" + encodeURIComponent(pathOrUid) +
        "&orientation=" + (orientation ? orientation : 0) +
        "&w=" + (width ? width : 0) +
        "&h=" + (height ? height : 0);
};

/**
 * snapshot parameters
 * @memberof PMA.UI.Components
 * @typedef {Object} PMA.UI.Components~snapshotParameters
 * @property {number} x - The x coordinate of the top left point
 * @property {number} y - The y coordinate of the top left point
 * @property {number} width - The width of the viewport
 * @property {number} height - The height of the viewport
 * @property {number} rotation - The rotation of the viewport (in degrees)
 * @property {boolean} flipHorizontally - Wheter the snapshot is flipped horizontally
 * @property {boolean} flipVertically - Wheter the snapshot is flipped vertically
 * @property {Array} [channels] - The selected channels
 * @property {string} [channelClipping] - A pair of values [0..100] to clip and scale pixel values for each active channel (comma separated)
 * @property {string} [channelColor] - Color in css format for each active channel (comma separated)
 * @property {string} [gamma] - A value of tha gamma correction factor for each active channel (comma separated)
 * @property {number} [layer=0] - The selected layer
 * @property {number} [timeframe=0] - The selected timeframe
 * @property {number} [scale=null] - A factor to scale the image by. Default is the current viewport scale
 * @property {number} [postGamma=null] - A value of the gamma correction factor
 * @property {number} [brightness=null] - A value of the brightness
 * @property {number} [contrast=null] - A value of the contrast
 * @property {number} [dpi=null] - Dots per inch to set to the image
 */

export
    /**
     * Returns a URL that points to the snapshot of a slide image
     * @funtion
     * @memberof PMA.UI.Components
     * @param  {string} serverUrl
     * @param  {string} sessionId
     * @param  {string} pathOrUid
     * @param {PMA.UI.Components~snapshotParameters} snapshotParameters - The parameters required for the snapshot
     * @param {Number} thumbWidth - The final snapshot width (this may be smaller due to aspect ratio)
     * @param {Number} thumbHeight - The final snapshot height (this may be smaller due to aspect ratio)
     * @param {string} [format=jpg] - The snapshot image format
     * @param {boolean} [drawScaleBar=false] - Whether or not to draw a scalebar
     * @param {string} [filename="snapshot"] - The filename of the image
     * @param {boolean} [download] - True to prompt the user to save the snapshot instead of viewing it within the browser
     */
    const getSnapshotUrl = function (serverUrl, sessionId, pathOrUid, snapshotParameters, thumbWidth, thumbHeight, drawScaleBar, format, filename = "snapshot", download) {
        if (!format) {
            format = "jpg";
        }

        thumbWidth = thumbWidth ? thumbWidth : 150;
        thumbHeight = thumbHeight ? thumbHeight : 150;

        var channelsString = "0";
        if (snapshotParameters.channels) {
            channelsString = snapshotParameters.channels.join(",");
        }

        var channelsClippingString = "";
        if (snapshotParameters.channelClipping) {
            channelsClippingString = snapshotParameters.channelClipping;
        } else {
            channelsClippingString = "0,100"
        }

        var channelsColorString = "";
        if (snapshotParameters.channelColor) {
            channelsColorString = snapshotParameters.channelColor;
        } else {
            channelsColorString = "ffffffff"
        }

        var channelsGammaString = "";
        if (snapshotParameters.gamma) {
            channelsGammaString = snapshotParameters.gamma;
        } else {
            channelsGammaString = "1"
        }

        var selectedTimeFrame = snapshotParameters.timeframe ? snapshotParameters.timeframe : 0;
        var selectedLayer = snapshotParameters.layer ? snapshotParameters.layer : 0;

        var scale = snapshotParameters.scale ? snapshotParameters.scale : Math.max(thumbWidth / snapshotParameters.width, thumbHeight / snapshotParameters.height);
        if (snapshotParameters.width * scale < 1 || snapshotParameters.height < 1) {
            scale = 1 / Math.max(snapshotParameters.width, snapshotParameters.height);
        }

        var rotation = snapshotParameters.rotation ? snapshotParameters.rotation : 0;

        return combinePath([serverUrl, "region"]) +
            "?sessionID=" + encodeURIComponent(sessionId) +
            "&pathOrUid=" + encodeURIComponent(pathOrUid) +
            "&format=" + encodeURIComponent(format) +
            "&x=" + Math.floor(snapshotParameters.x) +
            "&y=" + Math.floor(snapshotParameters.y) +
            "&width=" + Math.floor(snapshotParameters.width) +
            "&height=" + Math.floor(snapshotParameters.height) +
            "&rotation=" + rotation +
            "&flipHorizontal=" + snapshotParameters.flipHorizontally +
            "&flipVertical=" + snapshotParameters.flipVertically +
            "&timeframe=" + selectedTimeFrame +
            "&layer=" + selectedLayer +
            "&channels=" + channelsString +
            "&channelClipping=" + channelsClippingString +
            "&channelColor=" + channelsColorString +
            "&gamma=" + channelsGammaString +
            "&scale=" + scale +
            (snapshotParameters.dpi ? "&dpi=" + snapshotParameters.dpi : "") +
            (snapshotParameters.postGamma ? "&postGamma=" + snapshotParameters.postGamma : "") +
            (snapshotParameters.brightness ? "&brightness=" + snapshotParameters.brightness : "") +
            (snapshotParameters.contrast ? "&contrast=" + snapshotParameters.contrast : "") +
            (drawScaleBar ? "&drawScaleBar=true" : "") +
            (download ? "&downloadInsteadOfDisplay=true&filename=" + filename : "");
    };

/**
 * Returns a URL that points to the barcode of a slide image. This method doesn't guarantee that a barcode image actually exists.
 * @name getBarcodeUrl
 * @memberof PMA.UI.Components
 * @function
 * @param {string} serverUrl The PMA.core server URL
 * @param {string} sessionId The session id acquired by authenticating to the server
 * @param {string} pathOrUid The path or UID of the slide 
 * @param {Number} [rotation=0] The initial rotation for this associated image
 */
export const getBarcodeUrl = function (serverUrl, sessionId, pathOrUid, rotation) {
    return combinePath([serverUrl, "barcode"]) +
        "?sessionID=" + encodeURIComponent(sessionId) +
        "&pathOrUid=" + encodeURIComponent(pathOrUid) +
        "&rotation=" + (rotation ? rotation : 0);
};