Source: media/thumbnail/thumbnailGalleryCover.js

import { WebVTTThumbnail } from "./WebVTTThumbnail.js";
import { WebVTTThumbnailUtils } from "./WebVTTThumbnailUtils.js";
import { IntervalTimeout, TimeConversion } from '../utils/helperTools.js'
export { createCoverOverlay, TextOverlay, ThumbnailPositionOnElementHover, ThumbnailLoopOnImageHover, ThumbnailPositionOnElementHoverViaTimestamp, ThumbnailPositionOnImageHover, ThumbnailPositionOnSeekbarHover };

/**
 * Create an overlay to a cover image providing media related information like duration and resolution.
 * @param {Object} target - Target element to attach overlay as child element.
 * @param {Object} dataSourceElement - Element that provides the information via data attributes.
 */
function createCoverOverlay(target, dataSourceElement) {
    const div = document.createElement('div');
    div.className = "video-cover-text-container";
    div.innerHTML = '<i class="overlay-item fas fa-film"></i>';
    new TextOverlay(div, dataSourceElement, 'data-text-duration');
    new TextOverlay(div, dataSourceElement, 'data-text-resolution');
    target.append(div);
}

/**
 * Create a text overlay.
 * @author ckraemme <ckraemme@awi.de>
 * @private
 */
class TextOverlay {
    /**
     * Append data attributes as text overlay to a target element.
     * @param {Object} target - Target element to append text overlay as child.
     * @param {Object} dataSourceElement - Element that contains data attributes.
     * @param {RegExp} dataSourceSelector - Regex expression to select the data attribute.
     */
    constructor(target, dataSourceElement, dataSourceSelector) {
        this.target = target;
        this.dataSourceElement = dataSourceElement;
        this.dataSourceSelector = dataSourceSelector;
        this.addOverlayTextElements_();
    }

    /**
     * Get specific data attributes in an HTML element.
     * @param {Object} target - Target element with data attributes.
     * @param {RegExp} regexSelector - Regex expression to select the data attribute.
     * @returns {Array} Contains the filtered attributes.
     */
    static filterAttributesByName(target, regexSelector) {
        const re = new RegExp(regexSelector);
        let collectAttributes = target.attributes;
        const filteredAttributes = [...collectAttributes].filter(item => re.test(item.name));
        return filteredAttributes
    }

    /**
     * For each filtered data attribute, create a overlay text.
     * @private
     */
    addOverlayTextElements_() {
        const textDataAttributes = TextOverlay.filterAttributesByName(this.dataSourceElement, this.dataSourceSelector);
        textDataAttributes.forEach((item) => {
            this.addOverlayText_(item);
        });
    }

    /**
     * Create the overlay text element and append it as child to the target element.
     * @param {String} item - Specific data attribute.
     * @private
     */
    addOverlayText_(item) {
        const re = new RegExp(this.dataSourceSelector);
        const attributeName = item.name;
        const attributeValue = item.value;
        if (attributeValue) {
            const textElement = document.createElement('span');
            textElement.className = "overlay-item " + attributeName.replace(re, 'overlay-text-');
            textElement.innerText = attributeValue;
            textElement.style.cursor = 'default';
            this.target.appendChild(textElement);
        }
    }
}


/**
 * Show thumbnails of a webVTT file relative to the position of an element that is hovered.
 * @author ckraemme <ckraemme@awi.de>
 * @private
 */
class ThumbnailPositionOnElementHover {

    /**
     * Provide a mouse controlled thumbnail playback. Construct the object and attach all required event listeners to the element.
     * @param {Object} hoverElement - Hover element where event listeners should be attached.
     * @param {Object} targetElement - Target element that should contain timestamp and canvas element.
     * @param {Number} canvasWidth - Canvas width in pixel.
     * @param {Number} canvasHeight - Canvas height in pixel.
     * @param {String} vttUrl - URL to the webVTT file.
     * @param {Object} timestampTextElement - Text element to show timestamps as inner text.
     * @param {String} timestampTextOnPointerLeave - Timestamp text that is shown when the pointer leaves the element.
     * @param {Number} showNextThumbnailEveryNthPixel - Skip thumbnails that are to close in order to avoid a too sensitive mouse pointer.
     */
    constructor(hoverElement, targetElement, canvasWidth, canvasHeight, vttUrl, timestampTextElement = null, timestampTextOnPointerLeave, showNextThumbnailEveryNthPixel = 10) {
        this.manifestData, this.canvasElement;
        this.canvasWidth = canvasWidth;
        this.canvasHeight = canvasHeight;
        this.vttUrl = vttUrl;
        this.targetElement = targetElement;
        this.showNextThumbnailEveryNthPixel = showNextThumbnailEveryNthPixel;
        this.stateToPreventDuplicateBuilds = 0;
        this.hoverElement = hoverElement;
        this.timestampTextElement = timestampTextElement;
        this.timestampTextOnPointerLeave = timestampTextOnPointerLeave;
        this.hoverElement.addEventListener('pointerenter', this.coverPointerEnter_.bind(this));
        this.hoverElement.addEventListener('pointerleave', this.coverPointerLeave_.bind(this));
    }

    /**
     * Read the manifest file and attach an event lister when the element is hovered.
     * @param {Event} event - Pointerenter event.
     * @private 
     */
    coverPointerEnter_(event) {
        if (this.stateToPreventDuplicateBuilds === 0) {
            this.stateToPreventDuplicateBuilds = 1;
            let webVTT = new WebVTTThumbnail(this.vttUrl);
            webVTT.init(-1, false).then(() => {
                this.manifestData = webVTT.manifestData;
                this.coverPointerHover_(event);
                this.pointerMoveListener = this.coverPointerHover_.bind(this);
                this.hoverElement.addEventListener('pointermove', this.pointerMoveListener);
            })
        }
    }

    /**
     * Create or update the thumbnail frame in the canvas and timestamp.
     * @param {Event} event - Pointermove event.
     * @private 
     */
    coverPointerHover_(event) {
        const reducedManifestObjectNumber = Math.round(event.target.clientWidth / this.showNextThumbnailEveryNthPixel);
        const manifestDataReduced = WebVTTThumbnailUtils.reduceManifestEvenlyDistributed(this.manifestData, reducedManifestObjectNumber);
        const [manifestDataAtTimestamp,] = WebVTTThumbnailUtils.getManifestObjectOnHover(event, manifestDataReduced);
        this.generateThumbnail_(manifestDataAtTimestamp, manifestDataAtTimestamp['time_start']);
    }

    generateThumbnail_(manifestDataAtTimestamp, timestampTextElementText) {
        if (typeof this.canvasElement == 'undefined')
            this.canvasElement = ThumbnailCanvas.createThumbnailCanvas(this.targetElement);

        if (this.timestampTextElement != null)
            this.timestampTextElement.innerText = timestampTextElementText;

        ThumbnailCanvas.paintThumbnail(manifestDataAtTimestamp, this.canvasElement, this.canvasWidth, this.canvasHeight);
    }

    /**
     * Remove all attached elements and events.
     * @param {Event} event - Pointerleave event.
     * @private 
     */
    coverPointerLeave_(event) {
        if (this.stateToPreventDuplicateBuilds === 1) {
            if (ThumbnailCanvas.removeThumbnailCanvas(this.targetElement)) {
                this.canvasElement = undefined;
                this.hoverElement.removeEventListener('pointermove', this.pointerMoveListener);
                if (this.timestampTextElement != null && this.timestampTextOnPointerLeave != null)
                    this.timestampTextElement.innerText = this.timestampTextOnPointerLeave;
                this.stateToPreventDuplicateBuilds = 0;
            }
            else
                setTimeout(() => { this.coverPointerLeave_(event) }, 100);
        }
    }
}

/**
 * Overwrite the default ThumbnailPositionOnElementHover class and replace the referenced element.
 * Here, the seek bar is used to show thumbnails referenced in a webVTT file. It is based on the position of a pointer that hovers the seek bar.
 * @extends ThumbnailPositionOnElementHover
 * @author ckraemme <ckraemme@awi.de>
 */
class ThumbnailPositionOnSeekbarHover extends ThumbnailPositionOnElementHover {

    /**
     * Note: This class is based on class inheritance.
     * Only new initialized or removed parameters are described here.
     * Observe: showNextThumbnailEveryNthPixel can be omitted, as it is not used in the coverPointerHover_() method defined here.
     */
    constructor(hoverElement, targetElement, canvasWidth, canvasHeight, vttUrl, timestampTextElement = null, timestampTextOnPointerLeave) {
        super(hoverElement, targetElement, canvasWidth, canvasHeight, vttUrl, timestampTextElement, timestampTextOnPointerLeave);
    }

    /**
     * Overwrites ThumbnailPositionOnElementHover.coverPointerHover_() due to the required seek bar specificity.
     * @param {Event} event - Pointermove event.
     * @private 
     */
    coverPointerHover_(event) {
        const isSeekbar = true;
        const [manifestDataAtTimestamp, currentTimeHHMMSSms] = WebVTTThumbnailUtils.getManifestObjectOnHover(event, this.manifestData, isSeekbar);
        this.generateThumbnail_(manifestDataAtTimestamp, currentTimeHHMMSSms.split('.')[0]);
    }

}

/**
 * Overwrite the default ThumbnailPositionOnElementHover class and replace the referenced element.
 * Here, a time element is used to show thumbnails referenced in a webVTT file. It is based on the inner text of an html element that indicates the time when hovering the seek bar.
 * @extends ThumbnailPositionOnElementHover
 * @author ckraemme <ckraemme@awi.de>
 */
class ThumbnailPositionOnElementHoverViaTimestamp extends ThumbnailPositionOnElementHover {

    /**
     * Note: This class is based on class inheritance.
     * Only new initialized or removed parameters are described here.
     * Observe: showNextThumbnailEveryNthPixel can be omitted, as it is not used in the coverPointerHover_() method defined here.
     * @param {Object} timeElementSelector - Selector of time element that contains the timestamp as innerText.
     */

    constructor(hoverElement, targetElement, timeElementSelector, canvasWidth, canvasHeight, vttUrl, timestampTextElement = null, timestampTextOnPointerLeave) {
        // observe: timeElementSelector as additional input argument
        super(hoverElement, targetElement, canvasWidth, canvasHeight, vttUrl, timestampTextElement, timestampTextOnPointerLeave);
        this.timeElementSelector = timeElementSelector;
    }

    /**
     * Overwrites ThumbnailPositionOnElementHover.coverPointerHover_() due to the required seek bar specificity.
     * @param {Event} event - Pointermove event.
     * @private 
     */
    coverPointerHover_(event) {
        const delayReadingOfTimeElement_ms = 20; // wait until the time element changed - could be replaced by a mutation observer in the future
        setTimeout(() => {
            const currentTimeHHMMSS = document.querySelector(this.timeElementSelector)?.innerText;
            const currentTimeHHMMSSms = TimeConversion.time2IsoDayTime(currentTimeHHMMSS)
            const manifestDataAtTimestamp = WebVTTThumbnailUtils.getManifestObjectViaTimestamp(currentTimeHHMMSSms, this.manifestData);
            this.generateThumbnail_(manifestDataAtTimestamp, currentTimeHHMMSSms.split('.')[0]);
        }, delayReadingOfTimeElement_ms);
    }
}

/**
 * Show thumbnails referenced in a webVTT file relative to the hover position of an image element.
 * @extends ThumbnailPositionOnElementHover
 * @author ckraemme <ckraemme@awi.de>
 */
class ThumbnailPositionOnImageHover extends ThumbnailPositionOnElementHover {

    /**
     * Provide a mouse controlled thumbnail playback. Construct the object and attach all required event listeners to the image element.
     * @param {Object} imageElement - Image element where event listeners should be attached.
     * @param {String} imageElementDataAttributeWebVTTUrl - Name of the data attribute in the image element.
     * @param {Object} timestampTextElement - Text element to show timestamps as inner text.
     * @param {String} timestampTextOnPointerLeave - Timestamp text that is shown when the pointer leaves the element.
     */
    constructor(imageElement, imageElementDataAttributeWebVTTUrl = "data-src-webvtt", timestampTextElement = null, timestampTextOnPointerLeave) {
        const vttUrl = imageElement.getAttribute(imageElementDataAttributeWebVTTUrl);
        const imageContainer = imageElement.parentElement;
        const coverContainer = document.createElement('div');
        coverContainer.className = 'cover-container';
        imageContainer.appendChild(coverContainer);
        const canvasWidth = imageElement.width;
        const canvasHeight = imageElement.height;
        const hoverElement = coverContainer;
        super(hoverElement, coverContainer, canvasWidth, canvasHeight, vttUrl, timestampTextElement, timestampTextOnPointerLeave);
    }
}

/**
 * Play thumbnails referenced in a webVTT file in a loop on top of an image element that is hovered.
 * @author ckraemme <ckraemme@awi.de>
 */
class ThumbnailLoopOnImageHover {
    /**
     * Create a canvas element on top of an image element where thumbnails are shown when the image is hovered.
     * @param {Object} imageElement - Image element that provides the url to the webVTT file as data attribute.
     * @param {string} imageElementDataAttributeWebVTTUrl - Name of the data attribute in the image element.
     * @param {Number} timeIntervalMilliseconds - Time interval between thumbnail images. Allows playback speed control.
     */
    constructor(imageElement, imageElementDataAttributeWebVTTUrl = "data-src-webvtt", timeIntervalMilliseconds = 100) {

        this.playback;
        this.timeIntervalMilliseconds = timeIntervalMilliseconds;
        this.stateToPreventDuplicateBuilds = 0;
        this.imageElement = imageElement;
        this.vttUrl = imageElement.getAttribute(imageElementDataAttributeWebVTTUrl);

        const coverContainer = imageElement.parentElement;
        coverContainer.addEventListener('mouseenter', this.coverMouseEnter_.bind(this));
        coverContainer.addEventListener('mouseleave', this.coverMouseLeave_.bind(this));
    }

    /**
     * Play a loop of thumbnail images provided via a webVTT file.
     * @param {Object} imageElement - Image element where thumbnail loop is attached as sibling.
     * @param {String} vttUrl - URL to the webVTT file.
     * @param {Number} currentFrame - Current thumbnail frame. Starts with zero.
     * @param {Number} timeIntervalMilliseconds - Time interval between thumbnail images. Allows playback speed control.
     * @param {Boolean} reversePlayback - If true play thumbnails in reverse order.
     * @param {Object} timestampTextElement - Text element to show timestamps as inner text.
     * @param {String} timestampTextOnRemove - Timestamp text that is shown when the thumbnail loop is removed.
     */
    static playThumbnailLoop(imageElement, vttUrl, currentFrame = 0, timeIntervalMilliseconds = 100, reversePlayback = false, timestampTextElement = null, timestampTextOnRemove) {
        let webVTT = new WebVTTThumbnail(vttUrl);
        return webVTT.init(-1, false).then(() => {
            return new ThumbnailLoop(imageElement.parentElement, webVTT.manifestData, imageElement.width, imageElement.height, currentFrame, timeIntervalMilliseconds, reversePlayback, timestampTextElement, timestampTextOnRemove);
        });
    }

    /**
     * Start thumbnail playback loop.
     * @param {Event} event - Pointerenter event.
     * @private
     */
    coverMouseEnter_(event) {
        if (this.stateToPreventDuplicateBuilds === 0) {
            this.stateToPreventDuplicateBuilds = 1;
            const currentFrame = 0;
            ThumbnailLoopOnImageHover.playThumbnailLoop(this.imageElement, this.vttUrl, currentFrame, this.timeIntervalMilliseconds).then(playback => this.playback = playback);
        }
    }

    /**
     * Stop and destroy thumbnail playback loop.
     * @param {Event} event - Pointerleave event.
     * @private
     */
    coverMouseLeave_(event) {
        if (this.stateToPreventDuplicateBuilds === 1) {
            if (typeof this.playback !== 'undefined') {
                this.playback.remove();
                this.stateToPreventDuplicateBuilds = 0;
            }
            else
                setTimeout(() => { this.coverMouseLeave_(event) }, 1000);
        }
    }
}

/**
 * Create an thumbnail loop and provide methods for controlling the playback.
 * @author ckraemme <ckraemme@awi.de>
 * @private
 */
class ThumbnailLoop {

    /**
     * Create a thumbnail loop playback.
     * @param {Object} targetElement - Target element.
     * @param {Object} manifestData - Manifest data object providing all thumbnail related information.
     * @param {Number} containerWidth - Container width in pixel.
     * @param {Number} containerHeight - Container height in pixel.
     * @param {Number} currentFrame - Current thumbnail frame. Starts with zero.
     * @param {Number} timeIntervalMilliseconds - Time interval between thumbnail images. Allows playback speed control.
     * @param {Boolean} reversePlayback - If true play thumbnails in reverse order.
     * @param {Object} timestampTextElement - Text element to show timestamps as inner text.
     * @param {String} timestampTextOnRemove - Timestamp text that is shown when the thumbnail loop is removed.
     */
    constructor(targetElement, manifestData, containerWidth, containerHeight, currentFrame = 0, timeIntervalMilliseconds = 100, reversePlayback = false, timestampTextElement = null, timestampTextOnRemove) {
        this.targetElement = targetElement;
        this.manifestData = manifestData;
        this.containerWidth = containerWidth;
        this.containerHeight = containerHeight;
        this.currentFrame = currentFrame;
        this.timeIntervalMilliseconds = timeIntervalMilliseconds;
        this.reversePlayback = reversePlayback;
        this.timestampTextElement = timestampTextElement;
        this.timestampTextOnRemove = timestampTextOnRemove;

        this.canvasElement = ThumbnailCanvas.createThumbnailCanvas(targetElement);
        this.intervalTimer = new IntervalTimeout(this.looping_.bind(this), timeIntervalMilliseconds);
        this.intervalTimer.setInterval();

    }

    /**
     * Create the thumbnail playback loop effect by controlling and painting the frames.
     * @private 
     */
    looping_() {
        this.intervalTimer.delayMS = this.timeIntervalMilliseconds;
        const boundary = false;
        if (this.reversePlayback === false)
            this.skipFrame(1, boundary);
        else
            this.skipFrame(-1, boundary);
        this.paintCurrentFrame();
    }

    /**
     * Paint current thumbnail frame to the canvas.
     */
    paintCurrentFrame() {
        ThumbnailCanvas.paintThumbnail(this.manifestData[this.currentFrame], this.canvasElement, this.containerWidth, this.containerHeight);
        if (this.timestampTextElement != null)
            this.timestampTextElement.innerText = this.manifestData[this.currentFrame]['time_start'];
    }

    /**
     * Pause thumbnail loop playback.
     */
    pause() {
        this.intervalTimer.clearInterval();
        this.intervalTimer.timeoutID = null;
    }

    /**
     * Pause thumbnail loop playback.
     * @param {Number} numberOfFrames - Amount of frames that should be skipped starting from the current playback position (can be positive and negative).
     * @param {Boolean} boundary - If true, limit the amount of skipped frames to the number of available frames. Else, start at the other end like in a loop.
     */
    skipFrame(numberOfFrames, boundary = true) {
        var newPosition;
        if (boundary) {
            const firstBoundaryValue = Math.max(0, this.currentFrame + numberOfFrames);
            newPosition = Math.min(firstBoundaryValue, this.manifestData.length - 1)
        }
        else {
            newPosition = (this.currentFrame + numberOfFrames) % this.manifestData.length
            if (newPosition < 0)
                newPosition = this.manifestData.length + newPosition;
        }
        this.currentFrame = newPosition;
    }

    /**
     * Resume thumbnail loop playback.
     */
    resume() {
        if (this.intervalTimer.timeoutID == null)
            this.intervalTimer.setInterval();
    }

    /**
     * Remove thumbnail loop object containing all child elements.
     * @param {Boolean} removeTargetCompletely - Indicates to remove target element too.
     */
    remove(removeTargetCompletely = false) {
        if (ThumbnailCanvas.removeThumbnailCanvas(this.targetElement)) {
            if (removeTargetCompletely)
                this.targetElement.remove();
            this.intervalTimer.clearInterval();
            if (this.timestampTextElement != null && this.timestampTextOnRemove != null)
                this.timestampTextElement.innerText = this.timestampTextOnRemove;
        }
        else
            setTimeout(() => { this.remove(removeTargetCompletely) }, 100);
    }
}

/**
 * Class that provides tools to create, paint images on and remove canvas in the context of the webVTT thumbnails. 
 * @author ckraemme <ckraemme@awi.de>
 */
class ThumbnailCanvas {
    /**
     * Create canvas element, set default canvas settings and append it to a target element.
     * @param {Object} targetElement - Target element to append canvas as child.
     * @returns {Object} Canvas element.
     */
    static createThumbnailCanvas(targetElement) {
        const canvasElement = document.createElement('canvas');
        const context = canvasElement.getContext('2d');
        context.imageSmoothingEnabled = true;
        context.imageSmoothingQuality = "high";
        targetElement.append(canvasElement);
        return canvasElement
    }

    /**
     * Remove the first canvas element found in a target element.
     * @param {Object} targetElement - Target element that contain a canvas.
     * @returns {Boolean} Return true when canvas could be removed, else false.
     */
    static removeThumbnailCanvas(targetElement) {
        const canvas = targetElement.querySelector('canvas');
        if (canvas)
            canvas.remove();
        else
            return false
        return true
    }

    /**
     * Paint the image from a thumbnail object to a canvas element.
     * @param {Object} thumbnailObject - Thumbnail object, i.e.,  Object { time_start: "00:00:00.000", time_end: "00:00:00.280", data: "thumb.jpeg#xywh=0,0,250,141", url: "https://localhost/Desktop/marine-data/thumbnail-hover/thumb.jpeg", image: img }.
     * @param {Object} canvasElement - Target canvas element.
     * @param {Object} canvasWidth - Canvas width in pixel.
     * @param {Object} canvasHeight - Canvas height in pixel.
     */
    static paintThumbnail(thumbnailObject, canvasElement, canvasWidth, canvasHeight) {
        const imageSprite = thumbnailObject.data.includes('#xywh')
        if (imageSprite)
            var [x, y, frameWidth, frameHeight] = thumbnailObject.data.split('#xywh=')[1].split(',');
        else
            var [x, y, frameWidth, frameHeight] = [0, 0, thumbnailObject.image.width, thumbnailObject.image.height];

        if (canvasHeight === undefined)
            canvasHeight = canvasWidth * frameHeight / frameWidth;
        if (canvasWidth === undefined)
            canvasWidth = canvasHeight * frameWidth / frameHeight;
        canvasElement.width = canvasWidth;
        canvasElement.height = canvasHeight;

        const context = canvasElement.getContext('2d');
        context.clearRect(0, 0, canvasElement.width, canvasElement.height);
        context.drawImage(thumbnailObject.image, x, y, frameWidth, frameHeight, 0, 0, canvasWidth, canvasHeight);
    }
}