PMA.UI Documentation by Pathomation

components/js/slideLoader.js

import { checkBrowserCompatibility } from '../../view/helpers';
import { Viewport } from '../../view/viewport';
import { Events, DragDropMimeType, parseDragData, _sessionList } from './components';

function clone(obj) {
    if (null === obj || "object" != typeof obj) {
        return obj;
    }

    var copy = obj.constructor();
    for (var attr in obj) {
        if (obj.hasOwnProperty(attr)) {
            copy[attr] = obj[attr];
        }
    }

    return copy;
}

function checkReloadImage(serverUrl, path, doneCb, dropped) {
    if (!this.lastLoadImageRequest) {
        return;
    }

    if (this.lastLoadImageRequest.serverUrl !== serverUrl || this.lastLoadImageRequest.path !== path || this.lastLoadImageRequest.doneCb !== doneCb) {
        var ref = this.lastLoadImageRequest;
        this.lastLoadImageRequest = null;

        this.load(ref.serverUrl, ref.path, ref.doneCb, ref.dropped);
    }
}

function onDragOver(ev) {
    ev.preventDefault();
    var types = ev.dataTransfer.types;
    if (types) {
        var hasData = false;
        if (types.indexOf) {
            hasData = types.indexOf("application/x-fancytree-node") > -1 || types.indexOf(DragDropMimeType) > -1;
        }
        else if (types.contains) {
            // in IE and EDGE types is DOMStringList
            hasData = types.contains("application/x-fancytree-node") || types.contains(DragDropMimeType);
        }

        if (hasData) {
            ev.dataTransfer.dropEffect = "link";
            return;
        }
    }

    ev.dataTransfer.dropEffect = "none";
}

function onDrop(ev) {
    ev.preventDefault();
    var nodeData = parseDragData(ev.dataTransfer);
    if (nodeData && !nodeData.isFolder && nodeData.path && nodeData.serverUrl) {
        var cancels = this.fireEvent(Events.BeforeDrop, { serverUrl: nodeData.serverUrl, path: nodeData.path, node: nodeData });
        if (cancels.filter(function (c) { return c == false; }).length == 0) {
            this.load(nodeData.serverUrl, nodeData.path, null, true);
        }
    }
}

function initializeDropZone() {
    if (this.element) {
        this.element.addEventListener("drop", onDrop.bind(this), false);
        this.element.addEventListener("dragover", onDragOver.bind(this), false);
    }
}

/**
 * Helper class that wraps around the {@link PMA.UI.View.Viewport} class. It's purpose is mainly to automatically handle slide reloading and authentication, via the provided {@link PMA.UI.Components.Context} instance.
 * @param {PMA.UI.Components.Context} context
 * @param {Object} slideLoaderOptions - Initialization options passed to each {@link PMA.UI.View.Viewport} that is created during a {@link PMA.UI.Components.SlideLoader#load} call. This is the same struct as the one accepted by the {@link PMA.UI.View.Viewport} constructor, omitting server URLs, credentials and specific slide paths. The omitted information is either available via the {@link PMA.UI.Components.Context} instance, or supplied during the {@link PMA.UI.Components.SlideLoader#load} call.
 * @param {string|HTMLElement} slideLoaderOptions.element - The element that hosts the viewer. It can be either a valid CSS selector or an HTMLElement instance
 * @param {string} slideLoaderOptions.image - The path or UID of the image to load
 * @param {Number} [slideLoaderOptions.keyboardPanFactor=0.5] - A factor to calculate pan delta when pressing a keyboard arrow. The actual pan in pixels is calculated as keyboardPanFactor * viewportWidth.
 * @param {PMA.UI.View.Themes} [slideLoaderOptions.theme="default"] - The theme to use
 * @param {boolean|Object} [slideLoaderOptions.overview=true] - Whether or not to display an overview map
 * @param {boolean} [slideLoaderOptions.overview.collapsed] - Whether or not to start the overview in collapsed state
 * @param {boolean|Object} [slideLoaderOptions.dimensions=true] - Whether or not to display the dimensions selector for images that have more than one channel, z-stack or timeframe
 * @param {boolean} [slideLoaderOptions.dimensions.collapsed] - Whether or not to start the dimensions selector in collapsed state
 * @param {boolean|Object} [slideLoaderOptions.barcode=false] - Whether or not to display the image's barcode if it exists
 * @param {boolean} [slideLoaderOptions.barcode.collapsed=undefined] - Whether or not to start the barcode in collapsed state
 * @param {Number} [slideLoaderOptions.barcode.rotation=undefined] - Rotation in steps of 90 degrees
 * @param {boolean|Object} [slideLoaderOptions.loadingBar=true] - Whether or not to display a loading bar while the image is loading
 * @param {PMA.UI.View.Viewport~position} [slideLoaderOptions.position] - The initial position of the viewport within the image
 * @param {boolean} [slideLoaderOptions.snapshot=false] - Whether or not to display a button that generates a snapshot image
 * @param {PMA.UI.View.Viewport~annotationOptions} [slideLoaderOptions.annotations] - Annotation options
 * @param {Number} [slideLoaderOptions.digitalZoomLevels=0] - The number of digital zoom levels to add
 * @param {boolean} [slideLoaderOptions.scaleLine=true] - Whether or not to display a scale line when resolution information is available
 * @param {boolean} [slideLoaderOptions.colorAdjustments=false] - Whether or not to add a control that allows color adjustments
 * @param {string|PMA.UI.View.Viewport~filenameCallback} [slideLoaderOptions.filename] - A string to display as the file name or a callback function. If no value is supplied, no file name is displayed.
 * @param {boolean|PMA.UI.View.Viewport~attributionOptions}[slideLoaderOptions.attributions=undefined] - Whether or not to display Pathomation attribution in the viewer
 * @param {Array<PMA.UI.View.Viewport~customButton>} [slideLoaderOptions.customButtons] - An array of one or more custom buttons to add to the viewer
 * @param {Object|boolean} [options.magnifier=false] - Whether or not to show the magnifier control
 * @param {Object|boolean} [options.magnifier.collapsed=undefined] - Whether or not to show the magnifier control in collapsed state
 * @param {Object} [options.grid] - Options for measurement grid
 * @param {Array<number>} [options.grid.size] - Grid cell width and height in micrometers
 * @fires PMA.UI.Components.Events#SlideInfoError
 * @fires PMA.UI.Components.Events#BeforeSlideLoad
 * @fires PMA.UI.Components.Events#SlideLoaded
 * @fires PMA.UI.Components.Events#BeforeDrop
 * @tutorial 03-gallery
 * @tutorial 04-tree
 * @tutorial 05-annotations
 */
export class SlideLoader {
    constructor(context, slideLoaderOptions) {
        if (!checkBrowserCompatibility()) {
            return;
        }

        this.loadingImage = false;
        this.lastLoadImageRequest = null;
        this.context = context;
        this.slideLoaderOptions = slideLoaderOptions || {};

        this.listeners = {};
        this.listeners[Events.SlideInfoError] = [];
        this.listeners[Events.BeforeSlideLoad] = [];
        this.listeners[Events.SlideLoaded] = [];
        this.listeners[Events.BeforeDrop] = [];

        /**
         * The currently loaded {@link PMA.UI.View.Viewport} instance, or null
         * @public
         */
        this.mainViewport = null;

        // try to get the element
        if (this.slideLoaderOptions.element) {
            if (this.slideLoaderOptions.element instanceof HTMLElement) {
                this.element = this.slideLoaderOptions.element;
            }
            else if (typeof this.slideLoaderOptions.element == "string") {
                var el = document.querySelector(this.slideLoaderOptions.element);
                if (!el) {
                    throw "Invalid selector for element";
                }
                else {
                    this.element = el;
                }
            }
        }

        initializeDropZone.call(this);
    }
    /**
             * Sets or overrides the value of an option. Useful when it is required to modify a viewer option before loading as slide.
             * @param  {string} option
             * @param  {any} value
             */
    setOption(option, value) {
        this.slideLoaderOptions[option] = value;
    }
    /**
             * Gets the value of a viewer option.
             * @param  {string} option
             * @param  {any} value
             * @return {any} The value of the option or undefined
             */
    getOption(option) {
        return this.slideLoaderOptions[option];
    }
    /**
             * Creates a {@link PMA.UI.View.Viewport} instance that loads the requested slide
             * @param  {string} serverUrl - PMA.core server URL
             * @param  {string} path - Path or UID of the slide load
             * @param  {function} [doneCb] - Called when the slide has finished loading
             * @param {boolean} [dropped] - Whether this slide was loaded by a drag and drop operation
             * @fires PMA.UI.Components.Events#BeforeSlideLoad
             * @fires PMA.UI.Components.Events#SlideLoaded
             * @fires PMA.UI.Components.Events#SlideInfoError
             */
    load(serverUrl, path, doneCb, dropped) {
        if (this.loadingImage === true) {
            //console.error("SlideLoader.loadImage: Last load image call hasn't finished yet");
            this.lastLoadImageRequest = {
                serverUrl: serverUrl,
                path: path,
                doneCb: doneCb,
                dropped: dropped === true ? true : false
            };

            return;
        }

        if (dropped !== true) {
            dropped = false;
        }

        this.loadingImage = true;

        if (this.mainViewport && this.mainViewport.map) {
            while (this.mainViewport.map.getInteractions().getLength() > 0) {
                this.mainViewport.map.removeInteraction(this.mainViewport.map.getInteractions().item(0));
            }

            while (this.mainViewport.map.getLayers().getLength() > 0) {
                this.mainViewport.map.removeLayer(this.mainViewport.map.getLayers().item(0));
            }

            while (this.mainViewport.map.getControls().getLength() > 0) {
                this.mainViewport.map.removeControl(this.mainViewport.map.getControls().item(0));
            }
        }

        var beforeLoadEa = { serverUrl: serverUrl, path: path, cancel: false };
        this.fireEvent(Events.BeforeSlideLoad, beforeLoadEa);

        if (beforeLoadEa.cancel) {
            this.loadingImage = false;
            return;
        }

        var _this = this;
        if (!serverUrl || !path) {
            if (this.mainViewport) {
                this.mainViewport.element.innerHTML = "";
                this.mainViewport = null;
            }

            this.loadingImage = false;
            this.fireEvent(Events.SlideLoaded, { serverUrl: serverUrl, path: path, dropped: dropped });
            if (typeof doneCb === "function") {
                doneCb();
            }

            checkReloadImage.call(_this, serverUrl, path, doneCb, dropped);
            return;
        }

        this.context.getSession(serverUrl, function (sessionId) {
            var opts = clone(_this.slideLoaderOptions);

            opts.serverUrls = [serverUrl];
            opts.image = path;
            opts.sessionID = sessionId;
            opts.caller = _this.context.getCaller();

            _this.mainViewport = new Viewport(opts, function () {
                _this.loadingImage = false;
                _this.fireEvent(Events.SlideLoaded, { serverUrl: serverUrl, path: path, dropped: dropped });

                if (typeof doneCb === "function") {
                    doneCb();
                }

                checkReloadImage.call(_this, serverUrl, path, doneCb, dropped);
            }, function () {
                _this.loadingImage = false;
                console.error("Error loading slide");
            });

            var count = 0;
            _this.mainViewport.listen("tileserror", function () {
                if (count === 0) {
                    count++;
                    _sessionList.set(serverUrl, null);

                    _this.context.getSession(serverUrl, function (newSessionId) {
                        count = -1;
                        _this.mainViewport.sessionID = newSessionId;
                        _this.mainViewport.redraw();
                    }, function () {
                        _this.fireEvent(Events.SlideInfoError, {});
                    });
                }
                else if (count === -1) {
                    count = 0;
                    _this.fireEvent(Events.SlideInfoError, {});
                }
            });

            _this.mainViewport.listen("SlideLoadError", function () {
                _this.loadingImage = false;
                _this.fireEvent(Events.SlideInfoError, {});
                checkReloadImage.call(_this, serverUrl, path, doneCb, dropped);
            });
        },
            function () {
                _this.loadingImage = false;
                _this.fireEvent(Events.SlideInfoError, {});
                checkReloadImage.call(_this, serverUrl, path, doneCb, dropped);
            });
    }
    /**
             * Gets the image info of the currently loaded image
             * @return {object|null}
             */
    getLoadedImageInfo() {
        if (this.mainViewport && this.mainViewport.map && this.mainViewport.imageInfo) {
            return this.mainViewport.imageInfo;
        }

        return null;
    }
    /**
             * Reloads annotations from the server
             * @param {function} [readyCallback] - Called when the annotations have finished loading
             */
    reloadAnnotations(readyCallback) {
        if (this.mainViewport && this.mainViewport.map && this.mainViewport.imageInfo) {
            this.mainViewport.reloadAnnotations(readyCallback);
            return;
        }

        if (typeof readyCallback === "function") {
            readyCallback.call(this);
        }
    }
    /**
     * Attaches an event listener
     * @param {PMA.UI.Components.Events} eventName - The name of the event to listen to
     * @param {function} callback - The function to call when the event occurs
     */
    listen(eventName, callback) {
        if (!this.listeners.hasOwnProperty(eventName)) {
            console.error(eventName + " is not a valid event");
        }

        this.listeners[eventName].push(callback);
    }
    // fires an event
    fireEvent(eventName, eventArgs) {
        if (!this.listeners.hasOwnProperty(eventName)) {
            console.error(eventName + " does not exist");
            return;
        }

        var returns = [];
        for (var i = 0, max = this.listeners[eventName].length; i < max; i++) {
            returns.push(this.listeners[eventName][i].call(this, eventArgs));
        }

        return returns;
    }

    /**
     * Draw annotations for each slide in collage mode
     * @param {string} serverUrl - The server used to load the images
     * @param {function} slidePathTransform - A function to transform each filename to a slide name
     */

    drawCollageAnnotations(serverUrl, slidePathTransform) {
        let collagePath = this.mainViewport.image;
        if (!collagePath.startsWith("{")) {
            return;
        }

        let collageObj = JSON.parse(collagePath);
        let sources = collageObj["Sources"];
        let collectionRows = collageObj["CollectionRows"];
        let tileMargin = collageObj["CollectionTileMargin"];
        let layout = collageObj["CollectionLayout"];
        if (!sources || sources.length == 0) {
            return;
        }

        this.context.getImagesInfo({
            serverUrl: serverUrl,
            images: sources,
            success: (sessionId, images) => {
                let annotations = new PMA.UI.Components.Annotations({
                    context: this.context,
                    viewport: this.mainViewport,
                    serverUrl: serverUrl,
                    path: "",
                    enabled: true
                });

                let minresX = Math.min.apply(Math, images.map(im => im.MicrometresPerPixelX));
                let minresY = Math.min.apply(Math, images.map(im => im.MicrometresPerPixelY));

                let c = Math.round(images.length / collectionRows);
                c = Math.max(1, c);
                let sx = 0;
                let sy = 0;
                let maxHeight = 0;
                for (let i = 0; i < images.length; i++) {
                    let mX = images[i].MicrometresPerPixelX / minresX;
                    let mY = images[i].MicrometresPerPixelY / minresY;

                    let w = Math.round(images[i].Width * mX);
                    let h = Math.round(images[i].Height * mY);

                    annotations.addAnnotation({
                        LayerID: 0,
                        Geometry: `POLYGON ((${sx} ${sy}, ${sx} ${sy + h}, ${sx + w} ${sy + h},${sx + w} ${sy}, ${sx} ${sy}))`,
                        Notes: slidePathTransform(images[i].Filename)
                    });

                    if (layout == 0) {
                        sx += w + tileMargin;
                        maxHeight = Math.max(maxHeight, h);
                    }
                    else {
                        sy += h + tileMargin;
                        maxHeight = Math.max(maxHeight, w);
                    }

                    if (((i + 1) % c) == 0) {
                        if (layout == 0) {
                            sx = 0;
                            sy += maxHeight + tileMargin;
                            maxHeight = 0;
                        }
                        else {
                            sy = 0;
                            sx += maxHeight + tileMargin;
                            maxHeight = 0;
                        }
                    }
                }
            }
        });
    }
}