PMA.UI Documentation by Pathomation

components/js/aiImageAnalysisResults.js

import $ from 'jquery';
import { createTree } from 'jquery.fancytree/dist/modules/jquery.fancytree';
import 'jquery.fancytree/dist/modules/jquery.fancytree.dnd5';
import 'jquery.fancytree/dist/skin-win8/ui.fancytree.min.css';
import 'font-awesome/css/font-awesome.css';
import { Resources } from '../../resources/resources';
import { Events } from './components';
import '../../../lib/jscolor/jscolor';
/* global JSColor:readonly */

function formatArea(area) {
    var supscript = String.fromCharCode("2".charCodeAt(0) + 128);
    var output;
    if (area >= 100000) {
        output = Math.round((area * 10) / 1000000) / 10 + " mm" + supscript;
    } else {
        output = Math.round(area * 10) / 10 + " μm" + supscript;
    }

    return output;
}

export
    /**
     * Represents a UI component that allows the manipulation of AI analysis annotations.
     * @memberof PMA.UI.Components
     * @alias AIImageAnalysisResults
     * @param {object} options - Configuration options
     * @param {AnnotationManager} options.annotationManager - The annotation manager to use.
     * @param {string|HTMLElement} options.element - Selector or HTML element where the UI will be rendered.
     * @param {boolean} [options.zoomOnNavigation=false] - Whether or not zoom to or just highlight annotation on focus.
     * @param {boolean} [options.focusOnSelection=false] - Whether or not focus on selected annotation.
     */
    class AIImageAnalysisResults {
    constructor(options) {
        if (!options.annotationManager) {
            console.error("No annotation manager provided!");
            return;
        }

        this.annotationManager = options.annotationManager;

        if (!options.element) {
            console.error("No element provided!");
            return;
        }

        if (options.statusElement instanceof HTMLElement) {
            this.element = options.element;
        } else if (typeof options.element == "string") {
            let el = document.querySelector(options.element);
            if (!el) {
                console.error("Invalid selector for element");
                return;
            }
            else {
                this.element = el;
            }
        } else {
            console.error("Invalid element");
            return;
        }

        this.zoomOnNavigation = options.zoomOnNavigation === true;
        this.focusOnSelection = options.focusOnSelection === true;
        this.id = Array.from(Array(20), () => Math.floor(Math.random() * 36).toString(36)).join('');
        this.navigationCurrentAnnotation = null;
        this.navigationAnnotations = [];
        this.buttonClass = options.buttonClass ? options.buttonClass : "";

        $(this.element).append(`<div class="pma-ui-ai-image-analysis image-analysis-${this.id}">
        <h4 style="width: 100%; text-align: center;">AI Image Analysis Results</h4>
        <hr style="width: 80%;margin: 0 auto; margin-bottom: 8px;">
        <div class="ai-analysis-tree" style="flex: 1; overflow: auto;"></div>
        <div class="ai-analysis-navigation" style="display: flex; padding: 8px; border-top: 1px solid #0001; margin-top: 8px; align-items: center;">
            <button class="navigation-btn navigate-previous-btn ${this.buttonClass}" style="width: 25%; margin: 0 8px;">Previous</button>
            <div class="navigate-info" style="width: 50%; display: flex; justify-content: center; align-items: center; margin: 0 8px; text-align: center;">No annotation is currently focused</div>
            <button class="navigation-btn navigate-next-btn ${this.buttonClass}" style="width: 25%; margin: 0 8px;">Next</button></div>
        </div>`);

        const self = this;
        const aiAnnotations = this.annotationManager.viewport.getAnnotations().filter(x => x.metaData.Context).map(x => {
            return {
                id: x.metaData.AnnotationID,
                color: x.metaData.Color,
                area: x.metaData.Area,
                points: x.getGeometry().getType() === "MultiPoint" ? x.getGeometry().getPoints().length : 0,
                data: JSON.parse(x.metaData.Context)
            }
        });
        const groupBy = keys => array =>
            array.reduce((objectsByKeyValue, obj) => {
                const value = keys.map(key => obj.data[key]).join('_');
                objectsByKeyValue[value] = (objectsByKeyValue[value] || []).concat(obj);
                return objectsByKeyValue;
            }, {});

        const aiGroups = groupBy(['AIService', 'AIAnalysisOn'])(aiAnnotations);

        var sourceData = Object.entries(aiGroups).map(analysis => {
            const classifications = groupBy(['AIClassification'])(analysis[1]);
            const f = analysis[1][0];
            return {
                title: `<strong>${f.data.AIVendor} on ${(new Date(f.data.AIAnalysisOn)).toLocaleString('en-GB', { year: "numeric", month: "numeric", day: "numeric", hour: "2-digit", minute: '2-digit', second: '2-digit', hour12: false })}</strong>
                <div class="pma-ui-ai-analysis-info">
                ${f.data.AIService ? 'Analysis type: <i>' + f.data.AIAnalysisType + '</i><br />' : ""}
                ${f.data.AIService ? 'AI service: <i>' + f.data.AIService + '</i><br />' : ""}
                ${f.data.AIScenarioType ? 'AI scenario: <i>' + f.data.AIScenarioType + (f.data.AIScenarioVersion ? ` (v.${f.data.AIScenarioVersion})` : "") + '</i>' : ""}
                </div>`,
                key: f.data.AIModel + "_" + f.data.AIAnalysisOn,
                id: Object.entries(classifications).flatMap(classification => classification[1].map(x => x.id)),
                type: "analysis",
                selected: true,
                children: Object.entries(classifications).map(classification => {
                    const area = classification[1].map(x => x.area).reduce((accumulator, currentValue) => {
                        return accumulator + currentValue
                    }, 0);
                    return {
                        title: `<strong style="text-transform: capitalize;">${classification[0]}</strong> (${classification[1].length}) <span class="color-switcher-wrapper"><button class="color-switcher" data-value="${classification[1][0].color}" style="height: 19px; padding: 0; margin: 0; border: none; border-radius: 25%; margin-left: 4px;"></button></span>
                        ${(area && area > 0) ? '<div>Area: <i>' + formatArea(area) + '</i></div>' : ""}`,
                        key: f.data.AIModel + "_" + f.data.AIAnalysisOn + "_" + classification[0],
                        id: classification[1].map(x => x.id),
                        type: "classification",
                        selected: true,
                        children: classification[1].map((annot, idx) => {
                            return {
                                title: `<strong style="text-transform: capitalize;">${classification[0]} ${idx + 1}</strong>
                                ${annot.points > 1 ? '<div>No of points: <i>' + annot.points + '</i></div>' : ""}
                                ${annot.data.AIIntensity ? '<div>Intensity: <i>' + annot.data.AIIntensity + '</i></div>' : ""}
                                ${(area && area > 0) ? '<div>Area: <i>' + formatArea(annot.area) + '</i></div>' : ""}`,
                                key: annot.id,
                                id: [annot.id],
                                type: "annotation",
                                selected: true,
                            }
                        })
                    }
                })
            }
        });

        this.fancytree = createTree(`.image-analysis-${this.id} .ai-analysis-tree`, {
            checkbox: true,
            selectMode: 3,
            toggleEffect: false,
            source: sourceData,
            select: function (event, data) {
                data.node.data.id.forEach(id => {
                    self.annotationManager.viewport.showAnnotation(id, data.node.isSelected());
                });
            },
            icon: function (event, data) {
                if (data.node.type === "analysis" || data.node.type === "classification" || data.node.type === "annotation") {
                    return "fa fa-lg fa-crosshairs highlighter";
                }
                else {
                    return "";
                }
            },
            createNode: function (event, data) {
                const ht = $(data.node.li).find(".highlighter");
                if (ht) {
                    ht.off("mouseenter");
                    ht.off("mouseleave");
                    ht.on("mouseenter", e => {
                        data.node.data.id.forEach(id => {
                            var f = self.annotationManager.viewport.annotationsLayer.getSource().getFeatureById(id);
                            if (f) {
                                var feats = self.annotationManager.hoverInteraction.getFeatures();
                                feats.push(f);
                            }
                        });
                    });
                    ht.on("mouseleave", e => {
                        self.annotationManager.hoverInteraction.getFeatures().clear();
                    });
                    if (data.node.type === "annotation" && data.node.data.id.length === 1) {
                        ht.prop('title', 'Click to focus');
                        ht.off("click");
                        ht.on("click", e => {
                            var c = self.fancytree.findAll(n => {
                                return n.extraClasses === "current-node";
                            });

                            c.forEach(cn => {
                                cn.extraClasses = "";
                                cn.render();
                            });

                            data.node.extraClasses = "current-node";
                            self.navigationCurrentAnnotation = self.annotationManager.viewport.getAnnotations().find(x => x.getId() === data.node.data.id[0]);
                            self.navigationAnnotations = data.node.parent.data.id;
                            var curIdx = data.node.parent.data.id.findIndex(x => self.navigationCurrentAnnotation.getId() === x);
                            $(`.image-analysis-${self.id} .navigate-info`).text(self.navigationCurrentAnnotation.metaData.Classification + " " + (curIdx + 1));
                            self.annotationManager.viewport.focusToAnnotation(data.node.data.id[0], self.zoomOnNavigation, [50, 50, 50, 50]);
                        });
                    }
                }

                const swr = $(data.node.li).find(".color-switcher");
                if (swr && swr.length > 0) {
                    var myPicker = new JSColor(swr[0], {
                        format: "hexa",
                        value: $(swr).data("value"),
                        closable: true,
                        previewSize: 19,
                        closeText: Resources.translate("Close"),
                        onChange: () => {
                            const annots = self.annotationManager.viewport.getAnnotations().filter(x => data.node.data.id.toString().includes(x.getId().toString()));
                            annots.forEach(ann => {
                                const md = ann.metaData;
                                md.Color = swr[0].jscolor.targetElement.dataset.currentColor;
                                self.annotationManager.setMetadata(ann, md);
                            });
                            self.annotationManager.fireEvent(Events.AnnotationModified, null);
                        },
                    });
                }
            },
        });

        function navigatePrevious() {
            if (!self.navigationCurrentAnnotation || !self.navigationCurrentAnnotation) {
                return;
            }
            var curIdx = self.navigationAnnotations.findIndex(x => x === self.navigationCurrentAnnotation.getId());
            var previousIdx = curIdx - 1;
            if (curIdx === 0) {
                previousIdx = self.navigationAnnotations.length - 1;
            }
            self.navigationCurrentAnnotation = self.annotationManager.viewport.getAnnotations().find(x => x.getId() === self.navigationAnnotations[previousIdx]);
            while (self.annotationManager.viewport.hiddenAnnotations && self.annotationManager.viewport.hiddenAnnotations.findIndex(x => x.getId() === self.navigationCurrentAnnotation.getId()) !== -1) {
                if (curIdx === previousIdx) {
                    $(`.image-analysis-${self.id} .navigate-info`).text("No annotation is currently focused");
                    return;
                }
                previousIdx--;
                if (previousIdx === -1) {
                    previousIdx = self.navigationAnnotations.length - 1;
                }
                self.navigationCurrentAnnotation = self.annotationManager.viewport.getAnnotations().find(x => x.getId() === self.navigationAnnotations[previousIdx]);
            }

            var c = self.fancytree.findAll(n => {
                return n.extraClasses === "current-node";
            });
            c.forEach(cn => {
                cn.extraClasses = "";
                cn.render();
            });

            var n = self.fancytree.findFirst(n => {
                return n.key.toString() === self.navigationCurrentAnnotation.getId().toString();
            });

            if (!n) {
                return;
            }

            n.extraClasses = "current-node";
            n.render();
            n.li.scrollIntoView({ block: "center", inline: "nearest" });

            $(`.image-analysis-${self.id} .navigate-info`).text(self.navigationCurrentAnnotation.metaData.Classification + " " + (previousIdx + 1));
            self.annotationManager.viewport.focusToAnnotation(self.navigationCurrentAnnotation.getId(), self.zoomOnNavigation, [50, 50, 50, 50]);
            self.annotationManager.highlightAnnotation(self.navigationCurrentAnnotation.getId());
        }

        function navigateNext() {
            if (!self.navigationCurrentAnnotation || !self.navigationCurrentAnnotation) {
                return;
            }
            var curIdx = self.navigationAnnotations.findIndex(x => x === self.navigationCurrentAnnotation.getId());
            var nextIdx = curIdx + 1;
            if (curIdx === self.navigationAnnotations.length - 1) {
                nextIdx = 0;
            }
            self.navigationCurrentAnnotation = self.annotationManager.viewport.getAnnotations().find(x => x.getId() === self.navigationAnnotations[nextIdx]);
            while (self.annotationManager.viewport.hiddenAnnotations && self.annotationManager.viewport.hiddenAnnotations.findIndex(x => x.getId() === self.navigationCurrentAnnotation.getId()) !== -1) {
                if (curIdx === nextIdx) {
                    $(`.image-analysis-${self.id} .navigate-info`).text("No annotation is currently focused");
                    return;
                }
                nextIdx++;
                if (nextIdx === self.navigationAnnotations.length) {
                    nextIdx = 0;
                }
                self.navigationCurrentAnnotation = self.annotationManager.viewport.getAnnotations().find(x => x.getId() === self.navigationAnnotations[nextIdx]);
            }

            var c = self.fancytree.findAll(n => {
                return n.extraClasses === "current-node";
            });
            c.forEach(cn => {
                cn.extraClasses = "";
                cn.render();
            });

            var n = self.fancytree.findFirst(n => {
                return n.key.toString() === self.navigationCurrentAnnotation.getId().toString();
            });

            if (!n) {
                return;
            }

            n.extraClasses = "current-node";
            n.render();
            n.li.scrollIntoView({ block: "center", inline: "nearest" });

            $(`.image-analysis-${self.id} .navigate-info`).text(self.navigationCurrentAnnotation.metaData.Classification + " " + (nextIdx + 1));
            self.annotationManager.viewport.focusToAnnotation(self.navigationCurrentAnnotation.getId(), self.zoomOnNavigation, [50, 50, 50, 50]);
            self.annotationManager.highlightAnnotation(self.navigationCurrentAnnotation.getId());
        }

        $(`.image-analysis-${this.id} .navigate-previous-btn`).on("click", navigatePrevious);
        $(`.image-analysis-${this.id} .navigate-next-btn`).on("click", navigateNext);

        this.annotationManager.listen("annotationsSelectionChanged", () => {
            var sel = self.annotationManager.getSelection();
            if (!sel || sel.length === 0) {
                return;
            }

            var c = self.fancytree.findAll(n => {
                return n.extraClasses === "current-node";
            });
            c.forEach(cn => {
                cn.extraClasses = "";
                cn.render();
            });

            var n = self.fancytree.findFirst(n => {
                return n.key.toString() === sel[0].getId().toString();
            });

            if (!n) {
                $(`.image-analysis-${self.id} .navigate-info`).text("No annotation is currently focused");
                return;
            }

            n.extraClasses = "current-node";
            n.render();
            n.makeVisible({ scrollIntoView: true }).then(() => {
                n.li.scrollIntoView({ block: "center", inline: "nearest" });
            });

            self.navigationCurrentAnnotation = sel[0];
            self.navigationAnnotations = n.parent.data.id;
            var curIdx = n.parent.data.id.findIndex(x => self.navigationCurrentAnnotation.getId() === x);
            $(`.image-analysis-${self.id} .navigate-info`).text(self.navigationCurrentAnnotation.metaData.Classification + " " + (curIdx + 1));
            if (self.focusOnSelection) {
                self.annotationManager.viewport.focusToAnnotation(self.navigationCurrentAnnotation.getId(), self.zoomOnNavigation, [50, 50, 50, 50]);
                self.annotationManager.highlightAnnotation(self.navigationCurrentAnnotation.getId());
            }
        })
    }

    /**
     * Gets the state of the zoomOnNavigation option
     * @returns {boolean}
     */
    getZoomOnNavigation() {
        return this.zoomOnNavigation;
    }

    /**
     * Toggles the zoomOnNavigation option
     * @returns {boolean}
     */
    setZoomOnNavigation(zoomOnNavigation) {
        this.zoomOnNavigation = zoomOnNavigation === true;
    }

    /**
     * Gets the state of the focusOnSelection option
     * @returns {boolean}
     */
    getFocusOnSelection() {
        return this.focusOnSelection;
    }

    /**
     * Toggles the focusOnSelection option
     * @returns {boolean}
     */
    setFocusOnSelection(focusOnSelection) {
        this.focusOnSelection = focusOnSelection === true;
    }
}