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;
}
}