PMA.UI Documentation by Pathomation

view/viewportHelpers.js

import { ajax } from './helpers';
import { Events, ButtonLocations, Themes, Controls as ControlTypes } from './definitions';
import { ol } from './definitionsOl';
import Style from 'ol/style/Style';
import Kinetic from 'ol/Kinetic';
import * as olEventsCondition from 'ol/events/condition';
import { View, Map } from 'ol';
import { Resources } from '../resources/resources';
import { PrevZoom } from './controls/prevZoom';
import { RotationControl } from './controls/rotationControl';
import { Overview } from './controls/overview';
import { AssociatedImage } from './controls/associatedImage';
import { Snapshot } from './controls/snapshot';
import { Filename } from './controls/filename';
import { DimensionSelector } from './controls/dimensionSelector';
import { PathomationAttribution } from './controls/pathomationAttribution';
import { LayerSwitch } from './controls/layerSwitch';
import { ColorAdjustment } from './controls/colorAdjustment';
import { Magnifier } from './controls/magnifier';
import { PmaMouseWheelZoom } from './interactions/customMouseWheelZoom';
import { loginSupportsPost } from "./version";

let stateManager = {};

export function initialize() {
    if (!this.imageInfo) {
        findServerUrl.call(this, 0);
    } else {
        this.serviceUrl = this.imageInfo.BaseUrl + "api/json/";
        this.imagesUrl = this.imageInfo.BaseUrl;
        if (!this.sessionID) {
            login.call(this, loadImageInfo);
        } else {
            loadImageInfo.call(this);
        }
    }
}

export function addEvent(element, eventName, fn) {
    if (element.addEventListener) {
        element.addEventListener(eventName, fn, false);
    } else if (element.attachEvent) {
        element.attachEvent('on' + eventName, fn);
    }
}

export function hasClass(element, className) {
    return (" " + element.className + " ").indexOf(" " + className + " ") !== -1;
}

export function addClass(element, className) {
    if (!element) {
        return;
    }

    if (hasClass(element, className)) {
        return;
    }

    element.className += " " + className;
}

export function removeClass(element, className) {
    if (!element || !hasClass(element, className)) {
        return;
    }

    element.className = element.className.replace(new RegExp(className, "g"), "");
}

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

    var obj = JSON.parse(response);

    if (obj.hasOwnProperty("d")) {
        return obj.d;
    } else {
        return obj;
    }
}

export function tileLoad(imageTile, src) {
    if (this.options.flip.horizontally !== true &&
        this.options.flip.vertically !== true &&
        this.imageAdjustments.brightness === 0 &&
        this.imageAdjustments.contrast === 1 &&
        this.imageAdjustments.gamma === 1 &&
        (this.imageAdjustments.rgb === null ||
            (this.imageAdjustments.rgb[0] == 1 && this.imageAdjustments.rgb[1] == 1 && this.imageAdjustments.rgb[2] == 1)) &&
        (!this.imageAdjustments.tileTransformers || this.imageAdjustments.tileTransformers.length === 0)) {

        imageTile.getImage().src = src;
        return;
    }

    var tmpimg = new Image();
    tmpimg.crossOrigin = '';
    var self = this;
    tmpimg.onload = function() {
        var c = document.createElement("canvas");
        c.width = tmpimg.width;
        c.height = tmpimg.height;
        var ctx = c.getContext('2d');

        var tx = self.options.flip.horizontally ? tmpimg.width : 0;
        var ty = self.options.flip.vertically ? tmpimg.height : 0;
        var sx = self.options.flip.horizontally ? -1 : 1;
        var sy = self.options.flip.vertically ? -1 : 1;

        ctx.translate(tx, ty);
        ctx.scale(sx, sy);

        ctx.drawImage(tmpimg, 0, 0);

        if (self.imageAdjustments.brightness !== 0 ||
            self.imageAdjustments.contrast !== 1 ||
            self.imageAdjustments.gamma !== 1 ||
            (self.imageAdjustments.rgb !== null &&
                (self.imageAdjustments.rgb[0] != 1 || self.imageAdjustments.rgb[1] != 1 || self.imageAdjustments.rgb[2] != 1)) ||
            (self.imageAdjustments.tileTransformers && self.imageAdjustments.tileTransformers.length > 0)) {

            var pixels = ctx.getImageData(0, 0, c.width, c.height);
            if (self.imageAdjustments.tileTransformers) {
                for (var i = 0; i < self.imageAdjustments.tileTransformers.length; i++) {
                    self.imageAdjustments.tileTransformers[i](pixels);
                }
            }

            var brightness = self.imageAdjustments.brightness,
                contrast = self.imageAdjustments.contrast;
            brightnessContrastFilter(pixels.data, brightness, contrast, self.imageAdjustments.gamma);
            if (self.imageAdjustments.rgb !== null &&
                (self.imageAdjustments.rgb[0] != 1 || self.imageAdjustments.rgb[1] != 1 || self.imageAdjustments.rgb[2] != 1)) {
                colorBalanceFilter(pixels.data, self.imageAdjustments.rgb[0], self.imageAdjustments.rgb[1], self.imageAdjustments.rgb[2]);
            }

            ctx.putImageData(pixels, 0, 0);
        }

        imageTile.getImage().src = c.toDataURL("image/jpeg");
    };

    tmpimg.src = src;
}

export function getTileUrl(coord, pixelRatio, projection) {
    let maxZoom = this.imageInfo.MaxZoomLevel;
    let pow = Math.pow(2, coord[0]);

    let x = coord[1];
    if (this.options.flip.horizontally === true) {
        x = (pow - coord[1] - 1);
    }

    let y = coord[2];
    if (this.options.flip.vertically === true) {
        y = pow - coord[2] - 1;
    }

    return this.imagesUrl + "tile?sessionID=" +
        encodeURIComponent(this.sessionID) +
        "&channels=" + this.channelsString +
        "&channelClipping=" + this.channelClippingString +
        "&gamma=" + this.channelGammaString +
        "&timeframe=" + this.selectedTimeFrame + "&layer=" + this.selectedLayer + "&pathOrUid=" + encodeURIComponent(this.image) + "&x=" + x + "&y=" + y + "&z=" + coord[0];
}

// // export function getTileUrlPattern() {
// //     return this.imagesUrl + "tile?sessionID=" + encodeURIComponent(this.sessionID) + "&channels=" + this.channelsString + "&timeframe=" + this.selectedTimeFrame + "&layer=" + this.selectedLayer + "&pathOrUid=" + encodeURIComponent(this.image) + "&x={x}&y={y}&z={z}";
// // }

export function getAnnotatedTileUrlPattern(layerName) {
    return this.imagesUrl + "annotatedTile?sessionID=" + encodeURIComponent(this.sessionID) + "&pathOrUid=" + encodeURIComponent(this.image) + "&type=" + encodeURIComponent(layerName) + "&x={x}&y={y}&z={z}";
}

export function getAnnotatedTileUrl(layerName, coord, pixelRatio, projection) {
    var maxZoom = this.imageInfo.MaxZoomLevel;
    var pow = Math.pow(2, coord[0]);

    let x = coord[1];
    if (this.options.flip.horizontally === true) {
        x = (pow - coord[1] - 1);
    }

    let y = coord[2];
    if (this.options.flip.vertically === true) {
        y = pow - coord[2] - 1;
    }

    return this.imagesUrl + "annotatedTile?sessionID=" + encodeURIComponent(this.sessionID) + "&pathOrUid=" + encodeURIComponent(this.image) + "&type=" + encodeURIComponent(layerName) + "&x=" + x + "&y=" + y + "&z=" + coord[0];
}

export function annotatedTileLoad(imageTile, src) {
    if (this.options.flip.horizontally !== true &&
        this.options.flip.vertically !== true) {

        imageTile.getImage().src = src;
        return;
    }

    var tmpimg = new Image();
    tmpimg.crossOrigin = '';
    var self = this;
    tmpimg.onload = function() {
        var c = document.createElement("canvas");
        c.width = tmpimg.width;
        c.height = tmpimg.height;
        var ctx = c.getContext('2d');

        var tx = self.options.flip.horizontally ? tmpimg.width : 0;
        var ty = self.options.flip.vertically ? tmpimg.height : 0;
        var sx = self.options.flip.horizontally ? -1 : 1;
        var sy = self.options.flip.vertically ? -1 : 1;

        ctx.translate(tx, ty);
        ctx.scale(sx, sy);

        ctx.drawImage(tmpimg, 0, 0);

        imageTile.getImage().src = c.toDataURL("image/png");
    };

    tmpimg.src = src;
}

export function findServerUrl(index) {
    var _this = this;
    ajax.call(this, this.serverUrls[index] + "api/json/GetVersionInfo", "GET", null, function(http) {
        if (http.status == 200) {
            var s = this.serverUrls[index];
            if (s[s.length - 1] != "/") {
                s = s + "/";
            }

            const pmaCoreVersion = JSON.parse(http.responseText);

            this.serviceUrl = s + "api/json/";
            this.imagesUrl = s;

            if (!this.sessionID) {
                login.call(this, loadImageInfo, pmaCoreVersion);
            } else {
                loadImageInfo.call(this);
            }
        } else if (index < this.serverUrls.length - 1) {
            findServerUrl.call(this, index + 1);
        } else {
            if (typeof _this.failCallback === "function") {
                _this.failCallback();
            }

            throw "No accessible server URL found.";
        }
    });
}

export function login(callback, pmaCoreVersion) {
    var _this = this;
    _this.userInfo = null;

    let usePost = loginSupportsPost(pmaCoreVersion);

    ajax.call(this, this.serviceUrl + "Authenticate", usePost ? "POST" : "GET", { username: this.username, password: this.password, caller: this.options.caller },
        function(http) {
            if (http.status == 200) {
                var response = parseJson(http.responseText);
                if (response && response.Success === true) {
                    _this.sessionID = response.SessionId;
                    _this.userInfo = response;
                    if (callback) {
                        callback.call(_this);
                    }
                } else {
                    if (typeof _this.failCallback === "function") {
                        _this.failCallback();
                    }

                    throw "Login failed. " + response.Reason;
                }
            } else {
                if (typeof _this.failCallback === "function") {
                    _this.failCallback();
                }

                console.log(http);
                throw "Login failed with status " + http.status + " " + http.statusText;
            }
        }, { contentType: "application/json", dataEncodeCallback: usePost ? JSON.stringify : null });
}

export function loadImageInfo() {
    if (this.imageInfo) {
        // skip getting image info, since it was already provided
        createOlViewer.call(this);
    } else {
        var _this = this;
        ajax.call(this, this.serviceUrl + "GetImageInfo", "GET", { sessionID: this.sessionID, pathOrUid: this.image }, function(http) {
            if (http.status == 200) {
                var response = parseJson(http.responseText);
                if (response) {
                    _this.imageInfo = response;
                    createOlViewer.call(_this);
                } else {
                    if (typeof _this.failCallback === "function") {
                        _this.failCallback();
                    }

                    _this.fireEvent(Events.SlideLoadError, _this);
                    console.error("Server found but could not get image info");
                }
            } else {
                if (typeof _this.failCallback === "function") {
                    _this.failCallback();
                }

                var errorCode = 0; // Unknown error
                var errorMessage = Resources.translate("Unknown Error");
                try {
                    if (http.responseText && http.responseText.length !== 0) {
                        var errorResponse = parseJson(http.responseText);
                        if (errorResponse && errorResponse.hasOwnProperty("Code")) {
                            errorCode = errorResponse.Code;
                            errorMessage = Resources.translate(errorResponse.Message);
                        }
                    }
                } catch (e) {}

                if (this.element) {
                    this.element.innerHTML = "Cannot load slide. " + errorMessage;
                }

                _this.Error = { Code: errorCode, Message: errorMessage };
                _this.fireEvent(Events.SlideLoadError, _this);
                console.error("Server responded with status " + http.status + " and code " + errorCode);
                console.log(http);
            }
        });
    }
}

export function annotationTransform(input, output, dimension) {
    for (var i = 0; i < input.length; i += dimension) {
        var x = input[i];
        var y = input[i + 1];

        if (this.flip.vertically !== true) {
            y = this.extent[3] - y;
        }

        if (this.flip.horizontally === true) {
            x = this.extent[2] - x;
        }

        output[i] = x;
        output[i + 1] = y;
    }
}

export function loadAnnotations(vectorSource, projection) {
    if (typeof this.options.annotations === "object" && "loadAnnotationsByFingerprint" in this.options.annotations) {
        if (this.options.annotations.loadAnnotationsByFingerprint) {
            getFingerprint.call(this, (fingerprint) => {
                getAnnotations.call(this, vectorSource, projection, fingerprint, this.options.annotations.filter);
            });

            return;
        }
    }

    getAnnotations.call(this, vectorSource, projection, null, this.options.annotations.filter);
}

function getAnnotations(vectorSource, projection, fingerprint, filterCb) {
    var _this = this;

    ajax.call(this, this.serviceUrl + "GetAnnotations", "GET", { sessionID: this.sessionID, pathOrUid: this.image, currentUserOnly: false, refresh: Math.random(), ...(fingerprint ? { fingerprint: fingerprint } : {}) }, function(http) {
        if (http.status == 200) {
            var response = parseJson(http.responseText);
            if (response) {
                var annotations = response;
                if (typeof filterCb === "function") {
                    var annotations = [];
                    response.forEach(annot => {
                        if (filterCb(annot)) {
                            annotations.push(annot);
                        }
                    });
                }
                var features = _this.initializeFeatures(annotations, projection);
                vectorSource.addFeatures(features);
            }
        }

        if (typeof _this.readyCallback === "function") {
            _this.readyCallback();
        }
    });
}

export function getFingerprint(callback) {
    ajax.call(this, this.serviceUrl + "GetFingerprint", "GET", { sessionID: this.sessionID, pathOrUid: this.image }, function(http) {
        if (http.status == 200) {
            var response = parseJson(http.responseText);
            if (response) {
                callback(response);
            } else {
                callback(null);
            }
        } else {
            callback(null);
        }
    });
}

export function getKeyboardPanDelta(factor, viewportWidth) {
    var pxDelta = factor * viewportWidth;
    if (isNaN(pxDelta) || pxDelta < 0 || !isFinite(pxDelta)) {
        pxDelta = 50;
    }

    return pxDelta;
}

export function addOrUpdateArrowPanInteraction() {
    if (this.arrowPanInteraction !== null) {
        this.map.removeInteraction(this.arrowPanInteraction);
    }

    this.arrowPanInteraction = new ol.interaction.KeyboardPan({ pixelDelta: getKeyboardPanDelta(this.options.keyboardPanFactor, this.element.offsetWidth), duration: 0 });
    this.map.addInteraction(this.arrowPanInteraction);
}

export function createProjection() {
    var maxZoom = this.imageInfo.MaxZoomLevel;
    var pow = Math.pow(2, maxZoom);

    var xPadding = 0,
        yPadding = 0;
    var boxSize = pow * this.imageInfo.TileSize;

    if (this.options.flip.horizontally === true) {
        xPadding = boxSize - this.imageInfo.Width;
    }

    if (this.options.flip.vertically !== true) {
        yPadding = boxSize - this.imageInfo.Height;
    }

    var pixelPerUMeter = 1;
    if (this.imageInfo.MicrometresPerPixelX && this.imageInfo.MicrometresPerPixelX > 0) {
        pixelPerUMeter = this.imageInfo.MicrometresPerPixelX;
    }

    var pixelProjection = new ol.proj.Projection({
        code: 'pixel',
        units: 'pixels',
        extent: [xPadding, yPadding, xPadding + this.imageInfo.Width, yPadding + this.imageInfo.Height],
        metersPerUnit: pixelPerUMeter * 0.000001,
        getPointResolution: function(resolution, coord) {
            return resolution;
        }
    });

    return pixelProjection;
}

export function createMainView(projection, center, zoom, rotation) {
    var tilesPerBoxSide = Math.pow(2, this.imageInfo.MaxZoomLevel);

    var digitalZoomLevels = parseInt(this.options.digitalZoomLevels);
    if (isNaN(digitalZoomLevels) || digitalZoomLevels <= 0) {
        digitalZoomLevels = 0;
    }

    this.options.digitalZoomLevels = digitalZoomLevels;

    var startZoom = this.imageInfo.MaxZoomLevel;
    var iw = this.imageInfo.Width;
    var ih = this.imageInfo.Height;

    // find a zoom level that fits the whole image inside the viewport - or stop at zoom level 0
    while (startZoom > 0 && (iw > this.element.offsetWidth || ih > this.element.offsetHeight)) {
        iw /= 2;
        ih /= 2;
        startZoom--;
    }

    let allowedResolution = [];
    if (this.imageInfo.MicrometresPerPixelX) {
        let maxResolution = tilesPerBoxSide;
        let minResolution = 1 / Math.pow(2, digitalZoomLevels);
        let mmpx = this.imageInfo.MicrometresPerPixelX;
        allowedResolution = [1, 2, 5, 10, 20, 40, 80, 160].map(function(v) {
            return { objective: v, resolution: 10 / mmpx / v };
        });

        while (allowedResolution[0].resolution < maxResolution) {
            allowedResolution = [{ objective: 0, resolution: 2 * allowedResolution[0].resolution }].concat(allowedResolution);
        }

        if (allowedResolution[allowedResolution.length - 1] > minResolution) {
            allowedResolution.push({ objective: "MAX", resolution: minResolution });
        }
    }

    var view = new View({
        projection: projection,
        center: center ? center : ol.extent.getCenter(projection.getExtent()),
        // extent: projection.getExtent(),
        showFullExtent: true,
        maxResolution: tilesPerBoxSide,
        minResolution: 1 / Math.pow(2, digitalZoomLevels),
        zoom: zoom ? zoom : startZoom,
        rotation: rotation ? rotation : 0,
        resolutions: allowedResolution.length == 0 ? undefined : allowedResolution.map(function(a) { return a.resolution }),
        constrainResolution: false,
        zoomFactor: 2
    });

    var self = this;

    function fireEvent() {
        self.fireEvent(Events.ViewChanged, self);
    }

    view.on("change:center", fireEvent.bind(this));
    view.on("change:resolution", fireEvent.bind(this));
    view.on("change:rotation", fireEvent.bind(this));

    return view;
}

export function createMagnifierControl(element, collapsed) {
    return new Magnifier({ target: element /* or null */ , collapsed: collapsed });
}

export function createOverviewControl() {
    var tilesPerBoxSide = Math.pow(2, this.imageInfo.MaxZoomLevel);
    this.element.querySelector(".ol-overview") && this.element.querySelector(".ol-overview").remove();
    return new Overview({
        maxResolution: tilesPerBoxSide,
        tipLabel: Resources.translate("Overview"),
        collapsed: this.options.overview && this.options.overview.collapsed === true,
        tracking: this.options.overview && this.options.overview.tracking === true,
        stateManager: stateManager
    });
}

export function createOlViewer() {
    // PHP image info case, if there is just one time frame, PHP encodes it as an object and not as an array
    if (this.imageInfo.TimeFrames && this.imageInfo.TimeFrames.TimeFrame) {
        this.imageInfo.TimeFrames = [this.imageInfo.TimeFrames.TimeFrame];
    }

    if (!this.imageInfo.TimeFrames || this.imageInfo.TimeFrames.length === 0) {
        this.imageInfo.TimeFrames = [];
        this.imageInfo.TimeFrames.push({ Layers: [{ LayerID: 0, Channels: [{ ChannelID: 0, Color: "ffffffff", Name: "Default" }] }], TimeID: 0 });
    }

    // PHP image info case, if there is just one layer, PHP encodes it as an object and not as an array
    for (var i = 0; i < this.imageInfo.TimeFrames.length; i++) {
        var tf = this.imageInfo.TimeFrames[i];
        if (tf.Layers.ImageLayer) {
            tf.Layers = [tf.Layers.ImageLayer];
        }

        // PHP image info case, if there is just one channel, PHP encodes it as an object and not as an array
        for (var j = 0; j < this.imageInfo.TimeFrames[i].Layers.length; j++) {
            var l = this.imageInfo.TimeFrames[i].Layers[j];
            if (l.Channels.Channel) {
                l.Channels = [l.Channels.Channel];
            }
        }
    }

    this.selectedTimeFrame = this.options.fov && this.options.fov.timeframe ? this.options.fov.timeframe : 0;
    this.selectedLayer = this.options.fov && this.options.fov.layer ? this.options.fov.layer : 0;

    for (i = 0; i < this.imageInfo.TimeFrames[0].Layers[0].Channels.length; i++) {
        var c = this.imageInfo.TimeFrames[0].Layers[0].Channels[i];
        if (this.options.fov && this.options.fov.channels) {
            c.Active = this.options.fov.channels.includes(i);
        } else if (!c.hasOwnProperty("Active")) {
            c.Active = true;
        }
        if (!c.DefaultGamma) {
            c.Gamma = 1.0;
        } else {
            c.Gamma = c.DefaultGamma;
        }
    }

    this.channelsString = this.getActiveChannels().join(",");
    this.channelGammaString = this.getChannelGammaString();

    addClass(this.element, "pma-ui-viewport-container");

    if (this.element.tabIndex < 0) {
        this.element.tabIndex = 0;
    }

    // if (this.element.tabIndex) {
    //     this.element.removeAttribute("tabindex");
    // }

    // remove any previously added theme classes
    for (var key in Themes) {
        if (!Themes.hasOwnProperty(key)) {
            continue;
        }

        removeClass(this.element, Themes[key]);
    }

    addClass(this.element, this.options.theme);

    this.element.innerHTML = "";
    if (!this.imageInfo.BackgroundColor) {
        this.imageInfo.BackgroundColor = "ffffff";
    }

    if (this.imageInfo.TimeFrames[0].Layers[0].Channels.length < 2 && this.imageInfo.TimeFrames.length < 2 && this.imageInfo.TimeFrames[0].Layers.length < 2) {
        this.options.dimensions = false;
    }

    this.element.style.backgroundColor = "#" + this.imageInfo.BackgroundColor;

    if (this.imageInfo.NumberOfZoomLevels) {
        this.imageInfo.MaxZoomLevel = this.imageInfo.NumberOfZoomLevels;
    }

    var maxZoom = this.imageInfo.MaxZoomLevel;

    if (!this.imageInfo.MicrometresPerPixelX || this.imageInfo.MicrometresPerPixelX <= 0) {
        this.options.scaleLine = false;
    }

    var pixelProjection = createProjection.call(this);

    var tilesPerBoxSide = Math.pow(2, maxZoom);

    var layer = new ol.layer.Tile({
        source: new ol.source.XYZ({
            tileUrlFunction: getTileUrl.bind(this),
            tileLoadFunction: tileLoad.bind(this),
            projection: pixelProjection,
            wrapX: false,
            attributions: "",
            crossOrigin: "PMA.UI",
            cacheSize: 100,
            tileGrid: ol.tilegrid.createXYZ({
                tileSize: [this.imageInfo.TileSize, this.imageInfo.TileSize],
                extent: [0, 0, tilesPerBoxSide * this.imageInfo.TileSize, tilesPerBoxSide * this.imageInfo.TileSize],
                maxZoom: maxZoom
            }),
        }),
        preload: 2,
        className: "ol-layer main-layer",
        extent: [0, 0, tilesPerBoxSide * this.imageInfo.TileSize, tilesPerBoxSide * this.imageInfo.TileSize]
    });

    this.mainLayer = layer;

    this.loading = 0;
    this.loaded = 0;
    this.progressEl = null;

    var olViewer = this;

    // Add progress bar if user requested
    if (this.options.loadingBar) {
        this.loading = 0;
        this.loaded = 0;
        this.progressEl = document.createElement('div');
        this.progressEl.className = "ol-progress";
        if (this.imageInfo.TimeFrames[0].Layers[0].Channels.length > 1) {
            this.progressEl.className = "ol-progress dark";
        }

        this.element.appendChild(this.progressEl);

        layer.getSource().on('tileloadstart', function(event) {
            if (olViewer.loading === 0) {
                olViewer.progressEl.style.visibility = 'visible';
            }

            ++olViewer.loading;
            updateProgress.call(olViewer);
        });

        layer.getSource().on('tileloadend', function() {
            setTimeout(function() {
                ++olViewer.loaded;
                updateProgress.call(olViewer);
            }, 100);
        });

        layer.getSource().on('tileloaderror', function() {
            setTimeout(function() {
                ++olViewer.loaded;
                updateProgress.call(olViewer);
            }, 100);
        });
    }

    layer.getSource().on('tileloaderror', function(event) {
        olViewer.fireEvent(Events.TilesError, olViewer);
    });

    var dragZoom = new ol.interaction.DragZoom({ condition: olEventsCondition.shiftKeyOnly, duration: 0 });
    var dragRotate = new ol.interaction.DragRotate({ condition: olEventsCondition.altKeyOnly });

    var controls = [
        new ol.control.Zoom({ zoomInTipLabel: Resources.translate("Zoom in"), zoomOutTipLabel: Resources.translate("Zoom out") }),
        new PrevZoom({ tipLabel: Resources.translate("Previous view"), dragZoom: dragZoom }),
    ];

    controls.push(new ol.control.FullScreen({ tipLabel: Resources.translate("Toggle fullscreen"), source: this.options.fullScreenElement, label: '\u2195', labelActive: '\u2195' }));


    var layerList = [layer];

    // Initialize server side annotation layers if any
    if (this.options.annotationsLayers && this.imageInfo.AnnotationsLayers && this.imageInfo.AnnotationsLayers.length > 0) {
        var groupList = [];

        for (var a = 0; a < this.imageInfo.AnnotationsLayers.length; a++) {
            var aLayerName = this.imageInfo.AnnotationsLayers[a];
            var alayer = new ol.layer.Tile({
                displayInLayerSwitcher: true,
                visible: this.options.annotationsLayers.loadLayers === true,
                title: aLayerName,
                source: new ol.source.XYZ({
                    tileUrlFunction: getAnnotatedTileUrl.bind(this, aLayerName),
                    tileLoadFunction: annotatedTileLoad.bind(this),
                    projection: pixelProjection,
                    wrapX: false,
                    attributions: '',
                    crossOrigin: "PMA.UI"
                }),
                extent: [0, 0, tilesPerBoxSide * this.imageInfo.TileSize, tilesPerBoxSide * this.imageInfo.TileSize],
                className: "ol-layer annotations-layer"
            });

            alayer.getSource().tileGrid = ol.tilegrid.createXYZ({
                tileSize: [this.imageInfo.TileSize, this.imageInfo.TileSize],
                extent: [0, 0, tilesPerBoxSide * this.imageInfo.TileSize, tilesPerBoxSide * this.imageInfo.TileSize],
                maxZoom: maxZoom
            });

            groupList.push(alayer);
        }

        if (groupList.length > 0) {
            var group = new ol.layer.Group({
                title: Resources.translate("Annotation layers"),
                layers: groupList
            });

            layerList.push(group);
        }
    }

    // try get the annotations
    if (this.options.annotations) {
        var vectorSource = new ol.source.Vector({
            projection: pixelProjection
        });

        this.annotationsLayer = new ol.layer.Vector({ source: vectorSource, updateWhileAnimating: true, updateWhileInteracting: true, className: "ol-layer annotations-layer" });

        loadAnnotations.call(this, vectorSource, pixelProjection);
        layerList.push(this.annotationsLayer);
        this.showAnnotations(this.options.annotations.visible !== false);
    }

    var measureVectorSource = new ol.source.Vector({
        projection: pixelProjection
    });

    this.measureLayer = new ol.layer.Vector({
        source: measureVectorSource,
        updateWhileAnimating: true,
        updateWhileInteracting: true,
        style: new Style({
            fill: new ol.style.Fill({
                color: 'rgba(255, 255, 255, 0.2)'
            }),
            stroke: new ol.style.Stroke({
                color: '#ffcc33',
                width: 2
            }),
            image: new ol.style.Circle({
                radius: 7,
                fill: new ol.style.Fill({ color: 'rgba(255, 255, 255, 0.2)' }),
                stroke: new ol.style.Stroke({ color: '#ff0000', width: 2 }),
            }),
            text: new ol.style.Text({
                font: '12px Calibri,sans-serif',
                fill: new ol.style.Fill({ color: '#000' }),
                stroke: new ol.style.Stroke({
                    color: '#fff',
                    width: 2
                }),
            })
        })
    });

    this.gridLayer = new ol.layer.Vector({
        updateWhileAnimating: true,
        updateWhileInteracting: true,
        source: new ol.source.Vector({
            projection: pixelProjection
        }),
        style: new ol.style.Style({
            fill: new ol.style.Fill({
                color: 'rgba(255, 255, 255, 0.2)'
            }),
            stroke: new ol.style.Stroke({
                color: '#bbcc33',
                width: 1
            }),
        })
    });

    this.measureTooltips = [];
    layerList.push(this.measureLayer);
    layerList.push(this.gridLayer);

    if (!this.options.keyboardPanFactor) {
        this.options.keyboardPanFactor = 0.5;
    }

    var dragPanInteraction = new ol.interaction.DragPan({ kinetic: new Kinetic(-0.005, 0.05, 100), onFocusOnly: false });
    this.mouseWheelInteraction = new PmaMouseWheelZoom({
        duration: 0,
        maxDelta: 1,
        deltaPerZoom: 100,
        timeout: 0,
        onFocusOnly: false,
        condition: olEventsCondition.always,
        constrainResolution: false
    });

    this.map = new Map({
        interactions: [
            dragPanInteraction,
            dragRotate,
            dragZoom,
            this.mouseWheelInteraction,
            new ol.interaction.PinchZoom({ constrainResolution: false }),
            new ol.interaction.PinchRotate(),
            new ol.interaction.DoubleClickZoom(),
            new ol.interaction.KeyboardZoom()
        ],
        loadTilesWhileAnimating: this.options.highQuality === true,
        loadTilesWhileInteracting: this.options.highQuality === true,
        layers: layerList,
        target: this.element,
        controls: controls,
        view: createMainView.call(this, pixelProjection)
    });

    setControlVisibility.call(this, this.element.querySelector(".ol-full-screen"), this.options.fullscreenControl);
    addOrUpdateArrowPanInteraction.call(this);

    if (this.options.customButtons) {
        createCustomButtons.call(this, this.options.customButtons);
    }

    if (this.options.position) {
        this.setPosition(this.options.position);
    }

    if (this.options.fov && this.options.fov.extent) {
        this.fitToExtent(this.options.fov.extent, this.options.fov.constrainResolution == true ? true : false);
    }

    if (this.options.fov && this.options.fov.rotation) {
        this.setPosition({ rotation: this.options.fov.rotation });
    }

    initializeControls.call(this);

    if (this.options.digitalZoomLevels > 1) {
        var totalLevels = this.options.digitalZoomLevels + maxZoom;
        var percent = 100 - Math.round(this.options.digitalZoomLevels * 100 / totalLevels);
        if (percent > 0 && percent <= 100) {
            var zsElement = this.element.querySelector(".ol-zoomslider");
            if (zsElement) {
                var digitalLevelsBgColor = "rgba(127, 127, 127, 0.4)";
                var zsStyle = window.getComputedStyle(zsElement, null);

                var testContainer = document.createElement("div");
                testContainer.className = "pma-ui-viewport-container " + this.options.theme;

                var testEl = document.createElement("div");
                testEl.className = this.options.theme + " ol-zoomslider digital-zoom-levels";

                testContainer.appendChild(testEl);
                zsElement.appendChild(testContainer);

                digitalLevelsBgColor = window.getComputedStyle(testEl, null).getPropertyValue('background-color');

                zsElement.removeChild(testContainer);

                zsElement.style.background = "linear-gradient(to top, " + zsStyle.getPropertyValue('background-color') + " " + percent + "%, " + digitalLevelsBgColor + " " + percent + "%, " + digitalLevelsBgColor + " 100%)";
            }
        }
    }

    if (this.options.grid) {
        this.showGrid(this.options.grid.size);
    }

    if (this.map) {
        this.map.on("change:size", mapResize.bind(this));

        setMapSizeClass.call(this);
    }

    if (!isNaN(this.imageInfo.DefaultGamma)) {
        this.setGamma(this.imageInfo.DefaultGamma);
    }

    // fire ready callback only if we are not going to load annotations
    // otherwise let the annotations load export function to do it
    if (!this.options.annotations && typeof this.readyCallback === "function") {
        this.readyCallback();
    }
}

export function mapResize() {
    setMapSizeClass.call(this);
    addOrUpdateArrowPanInteraction.call(this);
}

export function setMapSizeClass() {
    var size = this.map.getSize();
    this.element.className = this.element.className.replace(/ xlg\b| lg\b| md\b| sm\b| xs\b/g, '');
    if (size[0] > 1200) {
        this.element.className += ' xlg';
        removeClass(this.element.querySelector(".ol-scale-line"), "collapsed");
        removeClass(this.element.querySelector(".ol-filename"), "ol-collapsed");
    } else if (size[0] > 992) {
        this.element.className += ' lg';
        removeClass(this.element.querySelector(".ol-scale-line"), "collapsed");
        removeClass(this.element.querySelector(".ol-filename"), "ol-collapsed");
    } else if (size[0] > 768) {
        this.element.className += ' md';
        removeClass(this.element.querySelector(".ol-scale-line"), "collapsed");
        removeClass(this.element.querySelector(".ol-filename"), "ol-collapsed");
    } else if (size[0] > 576) {
        this.element.className += ' sm';
        // addClass(this.element.querySelector(".ol-scale-line"), "collapsed");
        // addClass(this.element.querySelector(".ol-filename"), "ol-collapsed");
    } else {
        this.element.className += ' xs';
        // addClass(this.element.querySelector(".ol-scale-line"), "collapsed");
        // addClass(this.element.querySelector(".ol-filename"), "ol-collapsed");
    }

    this.element.className = this.element.className.replace(/ xlgh| lgh| mdh| smh| xsh/g, '');
    if (size[1] > 1200) {
        this.element.className += ' xlgh';
    } else if (size[1] > 992) {
        this.element.className += ' lgh';
    } else if (size[1] > 768) {
        this.element.className += ' mdh';
    } else if (size[1] > 576) {
        this.element.className += ' smh';
    } else {
        this.element.className += ' xsh';
    }

    if (this.overviewControl) {
        // 300-500px: resize overview image 1/6 of the number of horizontal pixels in the viewport
        // 500-750px: resize  overview image 1/7 of the number of horizontal pixels in the viewport
        // 750-1000px: resize overview image 1/8 of the number of horizontal pixels in the viewport
        // 1000-1250px:resize  overview image 1/9 of the number of horizontal pixels in the viewport
        // 1250-1500px: resize overview image 1/10 of the number of horizontal pixels in the viewport
        // 1500-1750px:resize overview image 1/11 of the number of horizontal pixels in the viewport
        // 1750-2000px:resize overview image 1/12 of the number of horizontal pixels in the viewport
        // 2000-3000px:resize overview image 1/13 of the number of horizontal pixels in the viewport
        // 3000-4000px: resize overview image 1/14 of the number of horizontal pixels in the viewport 
        if (size[0] < 300 || size[1] < 300) {
            // this.overviewControl.setCollapsed(true);
        } else {
            var factor = 1 / 14;
            if (size[0] < 500) {
                factor = 1 / 6;
            } else if (size[0] < 750) {
                factor = 1 / 7;
            } else if (size[0] < 1000) {
                factor = 1 / 8;
            } else if (size[0] < 1250) {
                factor = 1 / 9;
            } else if (size[0] < 1500) {

                factor = 1 / 10;
            } else if (size[0] < 1750) {
                factor = 1 / 11;
            } else if (size[0] < 2000) {
                factor = 1 / 12;
            } else if (size[0] < 3000) {
                factor = 1 / 13;
            }

            this.overviewControl.changeOverviewSizePx(Math.sqrt(size[0] * size[0]) * factor);
        }
    }
}

export function printObjectivesInZoomBar() {
    var zsElement = this.element.querySelector(".ol-zoomslider");
    if (zsElement) {

        var oldEl = zsElement.querySelector(".objectives-scale");
        if (oldEl) {
            zsElement.removeChild(oldEl);
        }

        var digitalLevelsBgColor = "rgba(127, 127, 127, 0.4)";
        var zsStyle = window.getComputedStyle(zsElement, null);

        if (this.imageInfo.MicrometresPerPixelX !== 0) {
            var verticalOffset = parseFloat(zsStyle.getPropertyValue("padding-top")) +
                parseFloat(zsStyle.getPropertyValue("margin-top")) +
                parseFloat(zsStyle.getPropertyValue("border-top-width")) +
                parseFloat(zsStyle.getPropertyValue("padding-bottom")) +
                parseFloat(zsStyle.getPropertyValue("margin-bottom")) +
                parseFloat(zsStyle.getPropertyValue("border-bottom-width"));

            var thumbButton = this.element.querySelector(".ol-zoomslider-thumb");
            var thumbButtonStyle = window.getComputedStyle(thumbButton, null);
            var thumbButtonHeight = thumbButton.offsetHeight +
                parseFloat(thumbButtonStyle.getPropertyValue("padding-top")) +
                parseFloat(thumbButtonStyle.getPropertyValue("margin-top")) +
                parseFloat(thumbButtonStyle.getPropertyValue("border-top-width")) +
                parseFloat(thumbButtonStyle.getPropertyValue("padding-bottom")) +
                parseFloat(thumbButtonStyle.getPropertyValue("margin-bottom")) +
                parseFloat(thumbButtonStyle.getPropertyValue("border-bottom-width"));

            var zoomBarHeight = zsElement.offsetHeight - thumbButtonHeight;
            zoomBarHeight += (thumbButtonHeight - thumbButton.offsetHeight);

            ////var factor = (this.imageInfo.MicrometresPerPixelX - 0.75) / -0.0125;
            var factor = 10 / this.imageInfo.MicrometresPerPixelX;
            var totalLevels = this.options.digitalZoomLevels + this.imageInfo.MaxZoomLevel;
            var zoomLevelHeight = zoomBarHeight / totalLevels;

            var objectiveHeights = [];
            for (var obji = 0; obji <= totalLevels; obji++) {
                var curObj = factor / Math.pow(2, (totalLevels - obji - this.options.digitalZoomLevels));
                curObj = displayObjective(curObj);
                if (curObj !== false) {
                    objectiveHeights.push({ name: curObj + "X", height: zoomLevelHeight * obji });
                }
            }

            if (objectiveHeights.length > 0) {
                var objectivesEl = document.createElement("div");
                objectivesEl.className = "objectives-scale";
                zsElement.appendChild(objectivesEl);

                var html = "";
                for (var t = 0; t < objectiveHeights.length; t++) {
                    html += '<div style="bottom: ' + objectiveHeights[t].height + 'px"><div>' + objectiveHeights[t].name + '</div></div>';
                }

                objectivesEl.innerHTML = html;
                objectivesEl.style.height = zoomBarHeight + "px";
                objectivesEl.style.top = (thumbButtonHeight / 2.0) + "px";
            }
        }
    }
}

export function displayObjective(objective) {
    if (objective > 1) {
        objective |= 0; // convert to int
        if (objective > 10) {
            objective = (Math.round(objective / 10.0) * 10.0) | 0; // round to closest 10 multiple
        }

        var valid = [1, 2, 5, 10, 20, 40, 80, 160];
        for (var i = 0; i < valid.length; i++) {
            if (valid[i] === objective) {
                return valid[i];
            }
        }
    }

    return false;
}

export function findClosestObjectiveValue(v) {
    var allowedValues = [1, 2, 5, 10, 20, 40, 80, 160];
    return allowedValues[allowedValues.map(function(av) {
        return Math.abs(1 - av / v);
    }).reduce(function(iMin, x, i, arr) {
        return x < arr[iMin] ? i : iMin;
    }, 0)];
}

export function calculateObjective(element) {
    var objective = 10 / this.imageInfo.MicrometresPerPixelX / this.map.getView().getResolution();
    if (objective > 1) {
        objective |= 0; // convert to int
        // if (objective > 10) {
        //     objective = (Math.round(objective / 10.0) * 10.0) | 0; // round to closest 10 multiple
        // }

        element.innerHTML = objective + "X"; //findClosestObjectiveValue(objective) + "X";
        element.style.display = "block";
    } else {
        element.style.display = "none";
    }
}

export function updateProgress() {
    var w = this.loaded / this.loading * 100;
    if (w > 100) {
        w = 100;
    }
    var width = w.toFixed(1) + '%';
    this.progressEl.style.width = width;
    if (this.loading <= this.loaded) {
        this.loading = 0;
        this.loaded = 0;
        var this_ = this;
        setTimeout(function() {
            if (this_.loading === this_.loaded) {
                this_.progressEl.style.visibility = 'hidden';
                this_.progressEl.style.width = 0;
            }
        }, 500);
    }
}

export function applyImageAdjustments() {
    if (this.colorAdjustmentsControl != null) {
        this.colorAdjustmentsControl.update(this.imageAdjustments.brightness, this.imageAdjustments.contrast, this.imageAdjustments.gamma);
    }

    this.mainLayer.getSource().refresh();
}

export function drawScalebar(canvasCtx, imageInfo, scale, location, font) {
    /** @type {CanvasRenderingContext2D} */
    var ctx = canvasCtx;
    if (imageInfo.MicrometresPerPixelX == 0) {
        return;
    }

    if (!font) {
        font = '24px serif';
    }

    if (!location) {
        location = "TopLeft";
    }

    var pixelsPerUnit = scale / imageInfo.MicrometresPerPixelX;
    var maxWidth = 80;
    var totalUnits = 0,
        pow = 1;
    var width = 0;

    let iw = ctx.canvas.width;
    let ih = ctx.canvas.height;

    while (width < maxWidth) {
        var firstChar = totalUnits / pow;

        if (firstChar == 0) {
            totalUnits = 1;
        } else if (firstChar == 1) {
            totalUnits = 2;
        } else if (firstChar == 2) {
            totalUnits = 5;
        } else {
            totalUnits = 1;
            pow *= 10;
        }

        totalUnits *= pow;
        width = totalUnits * pixelsPerUnit;
    }

    var units = "";
    if (totalUnits % 1000 == 0) {
        units = " mm";
        totalUnits /= 1000;
    } else {
        units = " μm";
    }

    var height = 25;
    var startpixely = location.toLowerCase().startsWith("bottom") ? (ih - height - 20) : 20;
    var startpixelx = location.toLowerCase().endsWith("right") ? (iw - (2 * maxWidth) - 20) : 20;
    var rectdiff = 0.2 * 20;
    ctx.fillStyle = 'white';
    ctx.fillRect(startpixelx - rectdiff, startpixely - rectdiff, width + (2 * rectdiff), height + (2 * rectdiff));

    ctx.beginPath();
    ctx.moveTo(startpixelx, startpixely);
    ctx.lineTo(startpixelx, startpixely + height);
    ctx.lineTo(startpixelx + width, startpixely + height);
    ctx.lineTo(startpixelx + width, startpixely);
    ctx.stroke();
    ctx.font = font;
    ctx.fillStyle = 'red';
    var text = ctx.measureText(`${totalUnits} ${units}`);
    let x = startpixelx + (width / 2.0) - (text.width / 2.0);
    x = x < startpixelx ? startpixelx : x;
    ctx.textBaseline = "top";
    ctx.fillText(`${totalUnits} ${units}`, x, startpixely);
}

/**
 * 
 * @param {CanvasRenderingContext2D} canvasCtx 
 * @param {string} title 
 */
export function drawTitle(canvasCtx, title) {
    if (!title || !canvasCtx) {
        return;
    }

    canvasCtx.font = '32px serif'
    canvasCtx.fillStyle = 'black';
    canvasCtx.strokeStyle = 'white';
    canvasCtx.lineWidth = 4;

    var text = canvasCtx.measureText(title);
    let x = (canvasCtx.canvas.width / 2.0) - (text.width / 2.0);
    canvasCtx.textBaseline = "top";
    canvasCtx.strokeText(title, x, 20);
    canvasCtx.fillText(title, x, 20);
}

/**
 * 
 * @param {CanvasRenderingContext2D} canvasCtx 
 * @param {PMA.UI.Viewport} viewport
 * @param {0 | 90 | 180 | 270} [rotation] - The rotation of the barcode
 */
export function drawBarcode(canvasCtx, viewport, rotation) {
    return new Promise((resolve, reject) => {
        let imageInfo = viewport && viewport.imageInfo;
        if (!viewport || !canvasCtx || !imageInfo) {
            resolve();
            return;
        }

        if (imageInfo.AssociatedImageTypes.indexOf("Barcode") < 0) {
            resolve();
            return;
        }

        var img = new Image;
        img.crossOrigin = "anonymous";
        img.onload = function() {
            let fw = canvasCtx.canvas.width * 0.12;
            let fh = fw * (img.height / img.width);
            let x = canvasCtx.canvas.width - fw - 20;
            let y = 20;

            canvasCtx.lineWidth = 2;
            canvasCtx.drawImage(img, x, y, fw, fh);
            canvasCtx.strokeStyle = 'black';
            canvasCtx.strokeRect(x, y, fw + 2, fh + 2);
            canvasCtx.strokeStyle = 'white';
            canvasCtx.strokeRect(x + 2, y + 2, fw - 2, fh - 2);
            resolve();
        };

        img.onerror = reject
        img.src = getBarcodeUrl(viewport, rotation);
    });
}

/**
 * 
 * @param {CanvasRenderingContext2D} canvasCtx 
 * @param {PMA.UI.Viewport} viewport
 */
export function drawOverview(canvasCtx, viewport) {
    return new Promise((resolve, reject) => {
        let scale = (canvasCtx.canvas.width * 0.2) / viewport.imageInfo.Width;

        var url =
            viewport.imagesUrl +
            "region?pathOrUid=" +
            encodeURIComponent(viewport.image) +
            "&format=jpg" +
            "&timeframe=" +
            viewport.selectedTimeFrame +
            "&layer=" +
            viewport.selectedLayer +
            "&channels=" +
            viewport.channelsString +
            "&channelClipping=" +
            viewport.channelClippingString +
            "&sessionID=" +
            encodeURIComponent(viewport.sessionID) +
            "&drawScaleBar=false" +
            "&x=0" + "&y=0" +
            "&width=" + viewport.imageInfo.Width + "&height=" + viewport.imageInfo.Height +
            "&scale=" + scale;
        var img = new Image;
        img.crossOrigin = "anonymous";
        img.onload = function() {
            let fw = canvasCtx.canvas.width * 0.2;
            let fh = fw * (img.height / img.width);
            let x = canvasCtx.canvas.width - fw - 20;
            let y = canvasCtx.canvas.height - fh - 20;
            canvasCtx.drawImage(img, x, y, fw, fh);

            canvasCtx.lineWidth = 2;
            canvasCtx.strokeStyle = 'black';
            canvasCtx.strokeRect(x, y, fw + 2, fh + 2);
            canvasCtx.strokeStyle = 'white';
            canvasCtx.strokeRect(x + 2, y + 2, fw - 2, fh - 2);
            resolve();
        };

        img.onerror = reject
        img.src = url;
    });
}

/**
 * @param {PMA.UI.Viewport} viewport - The main viewport
 * @param {0 | 90 | 180 | 270} [rotation] - The rotation of the barcode
 * @returns {string} - The barcode url
 */
export function getBarcodeUrl(viewport, rotation) {
    return viewport.getActiveServerUrl() + "barcode" +
        "?sessionID=" + encodeURIComponent(viewport.getSessionID()) +
        "&pathOrUid=" + encodeURIComponent(viewport.imageInfo.Filename) +
        (rotation ? ("&rotation=" + rotation) : "");
}

export function brightnessContrastFilter(pixels, brightness, contrast, gamma) {
    var lookupTable = [];
    if (!gamma || gamma < 0) {
        gamma = 0;
    }

    for (var i = 0; i < 256; i++) {
        var value = contrast * (i - 128) + 128 + brightness;
        if (gamma != 1) {
            value = 255 * Math.pow(value / 255, 1 / gamma);
        }

        if (value[i] < 0) {
            value[i] = 0;
        }
        if (value[i] > 255) {
            value[i] = 255;
        }
        lookupTable.push(value);
    }

    for (i = 0; i < pixels.length; i += 4) {
        pixels[i] = lookupTable[pixels[i]];
        pixels[i + 1] = lookupTable[pixels[i + 1]];
        pixels[i + 2] = lookupTable[pixels[i + 2]];
        //pixels[i + 3] = 0;
    }
}

export function colorBalanceFilter(pixels, red, green, blue) {
    for (let i = 0; i < pixels.length; i += 4) {
        pixels[i] = Math.max(0, Math.min(255, pixels[i] * red));
        pixels[i + 1] = Math.max(0, Math.min(255, pixels[i + 1] * green));
        pixels[i + 2] = Math.max(0, Math.min(255, pixels[i + 2] * blue));
        //pixels[i + 3] = 0;
    }
}

export function createCustomButtons(buttons) {
    if (buttons != null && buttons.length > 0 && this.element) {
        var defaultLocation = ButtonLocations.S;
        var groupByLocations = {};
        groupByLocations[defaultLocation] = [];

        for (var i = 0; i < buttons.length; i++) {
            if (!buttons[i].location) {
                groupByLocations[defaultLocation].push(buttons[i]);
            } else {
                var l = buttons[i].location;
                (groupByLocations[l] = groupByLocations[l] || []).push(buttons[i]);
            }
        }

        for (var location in groupByLocations) {
            if (groupByLocations.hasOwnProperty(location)) {

                var div = document.createElement("div");
                div.className = "ol-unselectable ol-control ol-pma-custom-btn ol-custom-control" + " " + location.toString();

                for (i = 0; i < groupByLocations[location].length; i++) {
                    var b = groupByLocations[location][i];
                    var btn = document.createElement("button");
                    btn.title = b.title ? b.title : "";
                    btn.innerHTML = b.content ? b.content : "";
                    btn.className = b.class ? b.class : "";
                    if (typeof b.callback === "function") {
                        btn.addEventListener("click", b.callback.bind(this));
                    }

                    div.appendChild(btn);
                }

                this.element.querySelector(".ol-overlaycontainer-stopevent").appendChild(div);
            }
        }
    }
}

export function setControlVisibility(controlDiv, visible) {
    if (controlDiv) {
        if (visible === false) {
            addClass(controlDiv, "ol-hidden");
        } else {
            if (hasClass(controlDiv, "ol-dimension-selector") &&
                this.imageInfo.TimeFrames[0].Layers[0].Channels.length < 2 && this.imageInfo.TimeFrames.length < 2 && this.imageInfo.TimeFrames[0].Layers.length < 2) {
                return;
            }

            removeClass(controlDiv, "ol-hidden");
        }
    }
}

export function getControlVisibility(controlDiv) {
    if (controlDiv) {
        return !(hasClass(controlDiv, "ol-hidden"));
    }

    return false;
}

export function setScaleLineConfiguration(conf) {
    var scaleLineDiv = this.element.querySelector(".ol-scale-line");
    if (scaleLineDiv) {
        setControlVisibility.call(this, scaleLineDiv, conf.visible);

        if (conf.collapsed === true) {
            addClass(scaleLineDiv, "collapsed");
            stateManager.scaleLine.collapsed = true;
        } else {
            removeClass(scaleLineDiv, "collapsed");
            stateManager.scaleLine.collapsed = false;
        }
    }
}

export function setControlsConfiguration(configuration) {
    for (var i = 0; i < configuration.length; i++) {
        var conf = configuration[i];

        if (conf.control == ControlTypes.ZoomSlider) {
            setControlVisibility.call(this, this.element.querySelector(".ol-zoomslider"), conf.visible);
        } else if (conf.control == ControlTypes.ScaleLine) {
            setScaleLineConfiguration.call(this, conf);
        } else if (conf.control == ControlTypes.Overview) {
            setControlVisibility.call(this, this.element.querySelector(".ol-overview"), conf.visible);
            this.overviewControl.setCollapsed(conf.collapsed);
        } else if (conf.control == ControlTypes.Barcode) {
            setControlVisibility.call(this, this.element.querySelector(".ol-associated-image"), conf.visible);
            if (this.barcodeControl) {
                this.barcodeControl.setCollapsed(conf.collapsed);
                // do rotation
            }
        } else if (conf.control == ControlTypes.Magnifier) {
            if (this.magnifierControl) {
                this.magnifierControl.setCollapsed(conf.visible);
            }
        } else if (conf.control == ControlTypes.ColorAdjustments) {
            setControlVisibility.call(this, this.element.querySelector(".ol-brightness-contrast"), conf.visible);
        } else if (conf.control == ControlTypes.LayerSwitch) {
            setControlVisibility.call(this, this.element.querySelector(".ol-layerswitch"), conf.visible);
            if (this.layerSwitcher) {
                this.layerSwitcher.setCollapsed(conf.collapsed);
            }
        } else if (conf.control == ControlTypes.DimensionSelector) {
            setControlVisibility.call(this, this.element.querySelector(".ol-dimension-selector"), conf.visible);
            if (this.dimensionsControl) {
                this.dimensionsControl.setCollapsed(conf.collapsed);
            }
        } else if (conf.control == ControlTypes.Filename) {
            setControlVisibility.call(this, this.element.querySelector(".ol-filename"), conf.visible);
            if (this.filenameControl) {
                this.filenameControl.setCollapsed(conf.collapsed);
            }
        } else if (conf.control == ControlTypes.Snapshot) {
            setControlVisibility.call(this, this.element.querySelector(".ol-snapshot"), conf.visible);
        } else if (conf.control == ControlTypes.RotationControl) {
            setControlVisibility.call(this, this.element.querySelector(".ol-rotation"), conf.visible);

            if (this.rotationControl) {
                this.rotationControl.setCollapsed(conf.collapsed);
            }
        } else if (conf.control == ControlTypes.Attribution) {
            setControlVisibility.call(this, this.element.querySelector(".ol-attr"), conf.visible);
        } else if (conf.control == ControlTypes.Fullscreen) {
            setControlVisibility.call(this, this.element.querySelector(".ol-full-screen"), conf.visible);
        }
    }
}

export function getControlsConfiguration() {
    var scaleLineDiv = this.element.querySelector(".ol-scale-line");

    return [{
            control: ControlTypes.ZoomSlider,
            visible: getControlVisibility.call(this, this.element.querySelector(".ol-zoomslider"))
        },
        {
            control: ControlTypes.ScaleLine,
            visible: this.scaleLineControl && getControlVisibility.call(this, this.element.querySelector(".ol-scale-line")),
            collapsed: scaleLineDiv ? hasClass(scaleLineDiv, "collapsed") : false
        },
        {
            control: ControlTypes.Overview,
            visible: getControlVisibility.call(this, this.element.querySelector(".ol-overview")),
            collapsed: this.overviewControl && this.overviewControl.getCollapsed()
        },
        {
            control: ControlTypes.Barcode,
            visible: getControlVisibility.call(this, this.element.querySelector(".ol-associated-image")),
            collapsed: this.barcodeControl && this.barcodeControl.getCollapsed(),
            rotation: this.barcodeControl ? this.barcodeControl.rotation : 0
        },
        {
            control: ControlTypes.Magnifier,
            visible: this.magnifierControl && this.magnifierControl.getCollapsed()
        },
        {
            control: ControlTypes.ColorAdjustments,
            visible: getControlVisibility.call(this, this.element.querySelector(".ol-brightness-contrast"))
        },
        {
            control: ControlTypes.LayerSwitch,
            visible: this.layerSwitcher && getControlVisibility.call(this, this.element.querySelector(".ol-layerswitch")),
            collapsed: this.layerSwitcher ? this.layerSwitcher.getCollapsed() : false
        },
        {
            control: ControlTypes.DimensionSelector,
            visible: this.dimensionsControl && getControlVisibility.call(this, this.element.querySelector(".ol-dimension-selector")),
            collapsed: this.dimensionsControl ? this.dimensionsControl.getCollapsed() : false
        },
        {
            control: ControlTypes.Filename,
            visible: this.filenameControl && getControlVisibility.call(this, this.element.querySelector(".ol-filename")),
            collapsed: this.filenameControl ? this.filenameControl.getCollapsed() : false,
            filename: null // {string|PMA.UI.View.Viewport~filenameCallback}
        },
        {
            control: ControlTypes.Snapshot,
            visible: this.snapShotControl && getControlVisibility.call(this, this.element.querySelector(".ol-snapshot"))
        },
        {
            control: ControlTypes.RotationControl,
            visible: this.rotationControl && getControlVisibility.call(this, this.element.querySelector(".ol-rotation"))
        },
        {
            control: ControlTypes.Attribution,
            visible: this.attributionControl && getControlVisibility.call(this, this.element.querySelector(".ol-attr")),
            options: null // {boolean|PMA.UI.View.Viewport~attributionOptions}
        }
    ];
}

export function initializeControls() {
    this.zoomSliderControl = new ol.control.ZoomSlider();
    this.map.addControl(this.zoomSliderControl);
    if (this.options.zoomSlider === false) {
        setControlVisibility.call(this, this.element.querySelector(".ol-zoomslider"), false);
    }

    var meta = this.imageInfo.MetaData;
    var isJp2k = false;

    for (var m = 0; m < meta.length; m++) {
        if (meta[m].Name === "compression") {
            if (meta[m].Value === "Jpeg2000YCbCr" ||
                meta[m].Value === "Jpeg2000RGB" ||
                meta[m].Value === "Jpeg2000" ||
                meta[m].Value.indexOf("JPEG 2000") !== -1 ||
                meta[m].Value === "JP2") {
                isJp2k = true;
            }

            break;
        }
    }

    isJp2k = false; // Disables powered by Kakadu message

    if (isJp2k) {
        this.attributionControl = new PathomationAttribution({ className: "ol-attr-kakadu", html: '<a href="http://kakadusoftware.com/" target="_blank">&nbsp;</a>' });
        this.map.addControl(this.attributionControl);
    } else if (this.options.attributions) {
        this.attributionControl = new PathomationAttribution(this.options.attributions);
        this.map.addControl(this.attributionControl);
    }

    this.colorAdjustmentsControl = new ColorAdjustment({ layer: this.mainLayer, viewer: this });
    this.map.addControl(this.colorAdjustmentsControl);
    setControlVisibility.call(this, this.element.querySelector(".ol-brightness-contrast"), this.options.colorAdjustments);

    if (this.options.annotationsLayers && this.imageInfo.AnnotationsLayers && this.imageInfo.AnnotationsLayers.length > 0) {
        this.layerSwitcher = new LayerSwitch({
            pmaViewport: this,
            tipLabel: Resources.translate("Layers"),
            collapsed: this.options.annotationsLayers && this.options.annotationsLayers.collapsed ? true : false,
            stateManager: stateManager
        });

        this.map.addControl(this.layerSwitcher);
    }

    this.overviewControl = createOverviewControl.call(this);
    this.map.addControl(this.overviewControl);
    setControlVisibility.call(this, this.element.querySelector(".ol-overview"), this.options.overview);

    if (this.imageInfo.AssociatedImageTypes) {
        for (var k = 0; k < this.imageInfo.AssociatedImageTypes.length; k++) {
            if (this.imageInfo.AssociatedImageTypes[k].toLowerCase() == "barcode") {
                let rotation = 0;
                let collapsed = false;
                if (this.options.barcode) {
                    if (this.options.barcode.rotation) {
                        rotation = this.options.barcode.rotation;
                    }

                    if (this.options.barcode.collapsed) {
                        collapsed = this.options.barcode.collapsed;
                    }
                }

                this.barcodeControl = new AssociatedImage({ pmaViewport: this, rotation: rotation, collapsed: collapsed, imageType: "barcode", tipLabel: Resources.translate("Barcode"), stateManager: stateManager });

                this.map.addControl(this.barcodeControl);
                setControlVisibility.call(this, this.element.querySelector(".ol-associated-image"), this.options.barcode);
                break;
            }
        }
    }

    this.dimensionsControl = new DimensionSelector({ pmaViewport: this, tipLabel: Resources.translate("Channels"), collapsed: this.options.dimensions && this.options.dimensions.collapsed, stateManager: stateManager });
    this.map.addControl(this.dimensionsControl);
    this.dimensionsControl.renderSliders();
    setControlVisibility.call(this, this.element.querySelector(".ol-dimension-selector"), this.options.dimensions);

    var self = this;
    var filenameOptions = {
        //filename: (typeof this.options.filename === "string" ? this.options.filename : this.imageInfo.Filename),
        onClick: function() {
            self.fireEvent(Events.FilenameClick, self);
        },
        stateManager: stateManager
    };

    if (typeof this.options.filename === "string") {
        filenameOptions.filename = self.options.filename;
    } else if (typeof this.options.filename === "function") {
        filenameOptions.filename = self.options.filename.call(self, { serverUrl: self.getActiveServerUrl(), path: self.imageInfo.Filename });
    } else {
        filenameOptions.filename = self.imageInfo.Filename;
    }

    this.filenameControl = new Filename(filenameOptions);
    this.map.addControl(this.filenameControl);
    setControlVisibility.call(this, this.element.querySelector(".ol-filename"), this.options.filename);

    if (!stateManager.scaleLine) {
        stateManager.scaleLine = {};
    }

    this.scaleLineControl = new ol.control.ScaleLine();
    this.map.addControl(this.scaleLineControl);
    var scaleLineDiv = this.element.querySelector(".ol-scale-line");
    if (scaleLineDiv) {
        if (this.options.scaleLine == false) {
            setScaleLineConfiguration.call(this, { visible: false, collapsed: stateManager.scaleLine.collapsed });
        }

        if (stateManager.scaleLine.collapsed === true) {
            addClass(scaleLineDiv, "collapsed");
        }

        var objectiveIndicator = document.createElement("div");
        objectiveIndicator.className = 'scanResolution';
        calculateObjective.call(this, objectiveIndicator);
        scaleLineDiv.appendChild(objectiveIndicator);

        var _this = this;
        this.map.getView().on("change:resolution", function() {
            calculateObjective.call(_this, objectiveIndicator);
        });

        addEvent(scaleLineDiv, "click", function() {
            if (hasClass(scaleLineDiv, "collapsed")) {
                removeClass(scaleLineDiv, "collapsed");
                stateManager.scaleLine.collapsed = false;
            } else {
                addClass(scaleLineDiv, "collapsed");
                stateManager.scaleLine.collapsed = true;
            }
        });
    }

    this.snapShotControl = new Snapshot({ pmaViewport: this, tipLabel: Resources.translate("Snapshot") });
    this.map.addControl(this.snapShotControl);
    setControlVisibility.call(this, this.element.querySelector(".ol-snapshot"), this.options.snapshot == true);

    let rotCollapsed;
    try {
        if (this.options.rotationControl.collapsed !== true) {
            rotCollapsed = false;
        } else {
            rotCollapsed = true;
        }
    } catch {
        rotCollapsed = false;
    }
    this.rotationControl = new RotationControl({ pmaViewport: this, resetTipLabel: Resources.translate("Reset rotation"), flipHorizontallyTipLabel: Resources.translate("Flip horizontally"), flipVerticallyTipLabel: Resources.translate("Flip vertically"), collapsed: rotCollapsed });
    this.map.addControl(this.rotationControl);
    setControlVisibility.call(this, this.element.querySelector(".ol-rotation"), !(this.options.rotationControl === false));

    this.magnifierControl = createMagnifierControl.call(this, null, !this.options.magnifier || (this.options.magnifier && this.options.magnifier.collapsed === true));
    this.map.addControl(this.magnifierControl);

    printObjectivesInZoomBar.call(this);
}
//#endregion

//#region documentation objects
/**
 * Receives pixel data and applies an image transformation to it
 * @callback PMA.UI.View.tileTransformer
 * @param {ImageData} pixels - Represents the underlying pixel data of an area of a canvas element
 */

/**
 * Viewport position
 * @typedef {Object} PMA.UI.View.Viewport~position
 * @property {Array} center - The x and y coordinates of the center point
 * @property {number} zoom - The zoom level. Can be omitted if resolution is specified
 * @property {number} [resolution] - Can be omitted if zoom is specified
 * @property {number} [rotation=0]
 */

/**
 * Viewport Field of view
 * @typedef {Object} PMA.UI.View.Viewport~fov
 * @property {Array} extent - The extent of the viewport [minx, miny, maxx, maxy]
 * @property {number} rotation - The rotation of the viewport
 * @property {bool} [constrainResolution] - Whether to contain the resolution to allowed values when fitting to extent
 * @property {Array} [channels] - The selected channels
 * @property {number} [layer=0] - The selected layer
 * @property {number} [timeframe=0] - The selected timeframe
 */

/**
 * Annotation display options
 * @typedef {Object} PMA.UI.View.Viewport~annotationOptions
 * @property {boolean} [visible=true] - Whether or not to display the loaded annotations
 * @property {boolean} [labels=false] - Whether or not to render the text label of each annotation in the viewer
 * @property {boolean} [imageBaseUrl=""] - The base URL from which to load images
 * @property {number} [imageScale=NaN] - Scale factor for images
 * @property {boolean} [alwaysDisplayInMicrons=false] - Whether or not to automatically select the appropriate units for annotations (μm(^2) or mm(^2)) depending on the value
 * @property {boolean} [showMeasurements=true] - Whether to show the length and area of an annotation
 * @property {boolean} [loadAnnotationsByFingerprint=false] - Whether to load annotations based on image's fingerprint
 * @property {function} [filter] - A function that takes a PMA.core annotation and returns true if annotation should load or false if shouldn't
 */

/**
 * Attribution display options
 * @typedef {Object} PMA.UI.View.Viewport~attributionOptions
 * @property {string} html - The HTML contents to add inside the attribution container element
 * @property {string} [className="ol-attr"] - The CSS class to assign to the attribution container element
 */

/**
 * Image flip options
 * @typedef {Object} PMA.UI.View.Viewport~flipOptions
 * @property {boolean} [horizontally=false] - Whether or not to flip the image horizontally
 * @property {boolean} [vertically=false] - Whether or not to flip the image vertically
 */

/**
 * export function that returns a file name to display in the viewer
 * @callback PMA.UI.View.Viewport~filenameCallback
 * @param {Object} options
 * @param {string} options.serverUrl - The URL of the current PMA.core server
 * @param {string} options.fileName - The full path of the currently loaded image
 * @returns {string}
 */

/**
 * A custom button to be added to the viewer
 * @typedef {Object} PMA.UI.View.Viewport~customButton
 * @property {string} title - The title of the button
 * @property {string} content - The inner html of the button
 * @property {string} class - The class of the button
 * @property {PMA.UI.View.ButtonLocations} [location=PMA.UI.View.ButtonLocations.S] - The location in the viewport of the custom button
 * @property {function} callback - The callback to call when the button is clicked with this referring to the viewer
 */

/**
 * An annotation as returned by pma.core
 * @typedef PMA.UI.View.Viewport~annotation
 * @property {Number} AnnotationID - The annotation id
 * @property {Number} LayerID - The layer id
 * @property {string} Geometry - The annotation geometry in wkt format
 * @property {string} [Notes] - Optional notes for the annotation
 * @property {string} [Classification] - Optional classification string (Necrosis, tumor etc)
 * @property {string} [Color] - Optional color
 * @property {string} [CreatedBy] - Optional created by string
 * @property {string} [UpdateInfo] - Optional update info
 * @property {string} [Updatedby] - Optional updated by info
 * @property {string} [FillColor] - Optional fill color
 * @property {Number} [Dimensions] - Optional dimensionality of the annotation
 */

/**
 * An object for configuring the visible and collapse state of a control
 * @typedef PMA.UI.View.Viewport~ControlConfiguration
 * @property {PMA.UI.Types.Controls} control - The control to configure
 * @property {boolean} - visible - The visibility of the control
 * @property {boolean} - collapsed - Whether the control is collapsed or not
 */

/**
 * An object for configuring the visible and collapse state of a control
 * @typedef PMA.UI.View.Viewport~SnapshotCoordinates
 * @property {Integer} x - The x coordinate
 * @property {Integer} y - The y coordinate
 * @property {Integer} w - The width
 * @property {Integer} h - The height
 * @property {Number} scale - The scale of the current view in radians
 * @property {Number} rotation - The rotation of the current view in radians
 * @property {PMA.UI.View.Viewport~flipOptions} flip - Whether the image is flipped
 */