PMA.UI Documentation by Pathomation

components/js/syncView.js

import { Events } from "./components";
import { Events as ViewEvents } from "../../view/definitions";
import { rotate } from "ol/coordinate";

export /**
 * Automatically handles syncronization of views between slides
 * @memberof PMA.UI.Components
 * @alias SyncView
 * @param  {SlideLoader[]} slideLoaders - Array of PMA.UI.Components.SlideLoaders.
 */
class SyncView {
    constructor(slideLoaders) {
        if (!slideLoaders.length) {
            console.error("Expected array of PMA.UI.Components.SlideLoader");
            return;
        }

        this.slideLoaders = slideLoaders;

        // for syncing viewers
        this.eventsKeys = [];
        this.posSync = [];

        this.listeners = {};
        this.listeners[Events.SyncChanged] = [];

        for (var i = 0; i < this.slideLoaders.length; i++) {
            this.slideLoaders[i].listen(Events.BeforeSlideLoad, this.disableSync.bind(this));
        }
    }
    /**
     * Enables synchronization on the slides
     * @fires PMA.UI.Components.Events.SyncChanged
     */
    enableSync() {
        this.posSync = [];

        for (var i = 0; i < this.slideLoaders.length; i++) {
            var slideLoader = this.slideLoaders[i];

            if (slideLoader.mainViewport && slideLoader.mainViewport.map) {
                this.posSync.push(slideLoader.mainViewport.getPosition());

                var parameter = { index: i, self: this, slideLoaders: this.slideLoaders };
                this.eventsKeys.push({ viewport: slideLoader.mainViewport, callback: viewChanged.bind(this, parameter) });
                slideLoader.mainViewport.listen(ViewEvents.ViewChanged, this.eventsKeys[this.eventsKeys.length - 1].callback);
            }
        }

        this.fireEvent(Events.SyncChanged, true);
    }
    /**
     * Disables synchronization on the slides
     * @fires PMA.UI.Components.Events.SyncChanged
     */
    disableSync() {
        if (this.eventsKeys) {
            while (this.eventsKeys.length > 0) {
                var ek = this.eventsKeys.pop();
                ek.viewport.unlisten(ViewEvents.ViewChanged, ek.callback);
            }
        }

        this.posSync = [];

        this.fireEvent(Events.SyncChanged, false);
    }
    /**
     * Returns a value indicating whether slides are synchronized
     * @fires PMA.UI.Components.Events.SyncChanged
     */
    getStatus() {
        return this.eventsKeys && this.eventsKeys.length > 0;
    }
    /**
     * 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)) {
        if (!Object.prototype.hasOwnProperty.call(this.listeners, 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)) {
        if (!Object.prototype.hasOwnProperty.call(this.listeners, eventName)) {
            console.error(eventName + " does not exist");
            return;
        }

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

// to avoid circular updates when syncing views
var isBusy = false;
function viewChanged(parameters) {
    if (!parameters || isBusy) {
        return;
    }

    var self = parameters.self;
    var slideLoader = parameters.slideLoaders[parameters.index];
    let flipState = slideLoader.mainViewport.getFlip();
    for (let i = 0; i < parameters.slideLoaders.length; i++) {
        if (parameters.index == i) {
            continue;
        }

        let sl = parameters.slideLoaders[i];

        let flipStateI = sl.mainViewport.getFlip();
        if (flipStateI.horizontally != flipState.horizontally || flipStateI.vertically != flipState.vertically) {
            sl.mainViewport.setFlip(flipState.horizontally, flipState.vertically);
        }
    }

    var pos = slideLoader.mainViewport.getPosition();
    var oldPos = self.posSync[parameters.index];

    if (isNaN(pos.zoom)) {
        return;
    }

    if (oldPos) {
        var diff = {
            zoom: pos.zoom - oldPos.zoom,
            rotation: pos.rotation - oldPos.rotation,
            center: [pos.center[0] - oldPos.center[0], pos.center[1] - oldPos.center[1]],
        };

        self.posSync[parameters.index] = pos;
        oldPos = self.posSync[parameters.index];

        isBusy = true;
        for (var i = 0; i < parameters.slideLoaders.length; i++) {
            if (i == parameters.index) {
                continue;
            }

            const scale = slideLoader.mainViewport.imageInfo.MicrometresPerPixelX / parameters.slideLoaders[i].mainViewport.imageInfo.MicrometresPerPixelX;

            var p = self.posSync[i];
            if (p) {
                p.zoom += diff.zoom;
                p.rotation += diff.rotation;
                if (p.rotation != oldPos.rotation) {
                    // different rotation
                    var d = rotate([diff.center[0], diff.center[1]], p.rotation - oldPos.rotation);
                    p.center[0] += d[0] * scale;
                    p.center[1] += d[1] * scale;
                } else {
                    p.center[0] += diff.center[0] * scale;
                    p.center[1] += diff.center[1] * scale;
                }

                parameters.slideLoaders[i].mainViewport.setPosition(p, true);
                parameters.slideLoaders[i].mainViewport.map.getView().dispatchEvent("change:center");
            }

            self.posSync[i] = p;
            isBusy = false;
        }
    }
}