PMA.UI Examples 2.43.3by Pathomation

Counter tool

advancedviewportcounter
Console
PMA.UI version: 2.43.3
Counter tool example
counter.html
1<!doctype html>
2<html lang="en">
3
4<head>
5    <meta charset="utf-8">
6    <meta http-equiv="X-UA-Compatible" content="IE=10">
7    <meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
8
9    <!-- Include PMA.UI required libraries downloaded or from CDN -->
10    <script src="./pma.ui/jquery-3.1.0.js"></script>
11    <link href="./pma.ui/font-awesome.min.css" type="text/css" rel="stylesheet">
12
13    <!-- Include optional libraries downloaded or from CDN -->
14    <link rel="stylesheet" href="./pma.ui/bootstrap5.min.css">
15    <script src="./pma.ui/bootstrap5.bundle.min.js"></script>
16
17    <!-- Include PMA.UI script & css -->
18    <script src="./pma.ui/pma.ui.js"></script>
19    <link href="./pma.ui/pma.ui.css" type="text/css" rel="stylesheet">
20
21    <!-- Include custom script & css -->
22    <script src="./js/counter.js"></script>
23    <link href="./css/counter.css" type="text/css" rel="stylesheet">
24
25    <title>Counter tool</title>
26</head>
27
28<body>
29    <div class="container-fluid">
30        <div class="row">
31            <div class="col px-0">
32                <!-- The element that will host the viewport -->
33                <div id="viewer"></div>
34            </div>
35        </div>
36    </div>
37    <button type="button" class="btn btn-light fullscreen-btn mt-2 me-2 rounded" title="Fullscreen"
38        onClick="toggleFullscreen()">
39        <i class="fas fa-expand-arrows-alt"></i>
40    </button>
41    <div class="counter-actions">
42        <div class="card-body p-1 border border-light rounded bg-white">
43            <button type="button" class="btn btn-sm btn-light remove-selected-btn d-none"
44                title="Remove selected point(s)" onClick="removeSelectedPoints()">
45                Remove selected
46            </button>
47            <button type="button" class="btn btn-sm btn-light remove-last-btn d-none" title="Remove last point"
48                onClick="removeLastPoint()">
49                Remove last
50            </button>
51            <button type="button" class="btn btn-sm btn-light finish-drawing-btn d-none" title="Finish drawing points"
52                onClick="finishDrawing()">
53                Finish
54            </button>
55        </div>
56    </div>
57    <div class="card m-2 counter-main bg-light bg-gradient d-none">
58        <div class="card-body p-1">
59            <div class="card-title clearfix mb-0">
60                <h5 class="float-start">Counter tool</h5>
61                <button type="button" class="btn btn-sm btn-secondary float-end dropdown-toggle" title="Add new counter"
62                    data-bs-toggle="dropdown">
63                    <i class="fas fa-plus"></i>
64                </button>
65                <ul class="dropdown-menu">
66                    <li><a class="dropdown-item add-new-counter" data-shape="Point" data-fixed="0" href="#">Point</a>
67                    </li>
68                    <li><a class="dropdown-item add-new-counter" data-shape="Arrow" data-fixed="0" href="#">Arrow</a>
69                    </li>
70                    <li><a class="dropdown-item add-new-counter" data-shape="Rectangle" data-fixed="0"
71                            href="#">Rectangle</a></li>
72                    <li><a class="dropdown-item add-new-counter" data-shape="Ellipse" data-fixed="0"
73                            href="#">Ellipse</a></li>
74                    <li><a class="dropdown-item add-new-counter" data-shape="Square" data-fixed="1" href="#">Fixed
75                            Size Square</a>
76                    </li>
77                    <li><a class="dropdown-item add-new-counter" data-shape="Rectangle" data-fixed="1" href="#">Fixed
78                            Size Rectangle</a>
79                    </li>
80                    <li><a class="dropdown-item add-new-counter" data-shape="Circle" data-fixed="1" href="#">Fixed Size
81                            Circle</a></li>
82                </ul>
83                <button type="button" class="btn btn-sm btn-info float-end save-counters me-2 text-white"
84                    title="Save counters">
85                    <i class="fas fa-save"></i>
86                </button>
87            </div>
88            <ul class="list-group counter-tools-list">
89            </ul>
90        </div>
91    </div>
92</body>
93
94</html>
counter.css
1html,
2body {
3    height: 100%;
4    padding: 0px;
5    margin: 0px;
6}
7
8#viewer {
9    height: 100vh;
10}
11
12.counter-main {
13    position: absolute;
14    bottom: 110px;
15    left: 0;
16    width: 350px;
17    height: 300px;
18    opacity: 0.5;
19}
20
21.counter-main:hover {
22    opacity: 1 !important;
23}
24
25.counter-tools-list {
26    max-height: 250px;
27    overflow-y: auto;
28}
29
30.list-group-item.active {
31    background-color: rgb(211, 212, 213);
32    border: none;
33}
34
35.counter-actions {
36    position: absolute;
37    top: 20px;
38    width: auto;
39    height: auto;
40    transform: translateX(calc(50vw - 50%));
41    opacity: 0.5;
42}
43
44.counter-actions:hover {
45    opacity: 1 !important;
46}
47
48.fullscreen-btn {
49    position: absolute;
50    top: 0;
51    right: 0;
52}
53
54.ol-control.pma-ui-viewport-annotations-drawing {
55    display: none;
56}
counter.js
1// Initial declarations
2var serverUrl = "https://host.pathomation.com/pma.core.3/";
3var serverUsername = "PMA_UI_demo";
4var serverPassword = "PMA_UI_demo";
5var imagePath = "wsiformats/brightfield/Leica/Leica-1.scn";
6var viewerElementSelector = "#viewer";
7var caller = "DemoPortal";
8var context = null;
9var slideLoader = null;
10var annotationManager = null;
11var tools = [];
12var toolsToHide = [];
13var activeTool = -1;
14
15const getCounterName = (classification) => {
16    return classification.split("__")[2];
17}
18const getCounterShape = (classification) => {
19    return classification.split("__")[1];
20}
21
22const getCounterPointIndex = (classification) => {
23    return classification.split("__$")[1];
24}
25
26const getCounterClassification = (classification) => {
27    return classification.split("__$")[0];
28}
29
30const getToolIndexByClassification = (classification) => {
31    return tools.findIndex(t => t?.classification === classification);
32}
33
34const finishDrawing = () => {
35    $(".counter-tools-list").children().removeClass("active");
36    $(".remove-last-btn").addClass("d-none");
37    $(".finish-drawing-btn").addClass("d-none");
38    annotationManager.finishDrawing(false, "Point");
39    activeTool = -1;
40    updateCounts();
41}
42
43const removeSelectedPoints = () => {
44    $(".remove-selected-btn").addClass("d-none");
45    const anns = [...annotationManager.getSelection()];
46    removePoints(anns);
47}
48
49const removeHighlightedPoints = () => {
50    const anns = [...annotationManager.hoverInteraction.getFeatures().getArray()];
51    removePoints(anns);
52}
53
54const removePoints = (annotations) => {
55    var toolsToUpdate = [];
56    for (var i = 0; i < annotations.length; i++) {
57        var classification = getCounterClassification(annotations[i].metaData.Classification);
58        const pointId = annotations[i].getId();
59        annotationManager.deleteAnnotation(pointId);
60        const toolIndex = getToolIndexByClassification(classification);
61        var tool = tools[toolIndex];
62        const pointIndex = tool.points.findIndex(p => p.getId() === pointId);
63        tool.points.splice(pointIndex, 1);
64        if (toolsToUpdate.findIndex(ti => ti === toolIndex) === -1) {
65            toolsToUpdate.push(toolIndex);
66        }
67    }
68
69    toolsToUpdate.forEach(ti => reindexToolPoints(ti));
70    updateCounts();
71}
72
73const reindexToolPoints = (idx) => {
74    var tool = tools[idx];
75    tool.points.forEach((p, i) => {
76        var md = p.metaData;
77        md.Classification = tool.classification + "__$" + i;
78        annotationManager.setMetadata(p, md);
79    });
80}
81
82const removeLastPoint = () => {
83    if (activeTool < 0) return;
84    var tool = tools[activeTool];
85    if (tool.points.length === 0) return;
86    annotationManager.deleteAnnotation(tool.points.pop().getId());
87    updateCounts();
88}
89
90const updateCounts = () => {
91    tools.filter(x => x).forEach(x => {
92        $(x.countElement).val(x.points.length);
93    });
94}
95
96const drawCounter = (idx, recursive = false) => {
97    $(".counter-tools-list").children().removeClass("active");
98    $(".remove-last-btn").addClass("d-none");
99    $(".finish-drawing-btn").addClass("d-none");
100    if (!recursive) {
101        annotationManager.finishDrawing(false, "Point");
102        if (activeTool === idx) {
103            activeTool = -1;
104            return;
105        }
106    }
107
108    const tool = tools[idx];
109    const color = tool.color;
110    const shape = tool.shape === "Square" ? "Rectangle" : tool.shape;
111    activeTool = idx;
112    $(".counter-tools-list").find(`[data-id='${idx}']`).addClass("active");
113    $(".remove-last-btn").removeClass("d-none");
114    $(".finish-drawing-btn").removeClass("d-none");
115
116    annotationManager.startDrawing({
117        type: shape,
118        color: color,
119        fillColor: "rgba(0,0,0,0)",
120        lineThickness: shape === "Arrow" ? 3 : 6,
121        notes: "",
122        size: tool.width && tool.height ? [tool.width, tool.height] : undefined,
123    });
124}
125
126const removeTool = (idx) => {
127    if (activeTool === idx) {
128        $(".counter-tools-list").children().removeClass("active");
129        $(".remove-last-btn").addClass("d-none");
130        $(".finish-drawing-btn").addClass("d-none");
131        annotationManager.finishDrawing(false, "Point");
132        activeTool = -1;
133    }
134    var tool = tools[idx];
135    var len = tool.points.length;
136    while (len--) {
137        annotationManager.deleteAnnotation(tool.points[len].getId());
138    }
139    tools[idx] = null;
140    $(".counter-tools-list").find(`[data-id='${idx}']`).remove();
141}
142
143const changeColor = (el, idx) => {
144    var tool = tools[idx];
145    var len = tool.points.length;
146    const newColor = $(el).val();
147    while (len--) {
148        var md = tool.points[len].metaData;
149        md.Color = newColor;
150        annotationManager.setMetadata(tool.points[len], md);
151    }
152    tool.color = newColor;
153}
154
155const classificationChange = (el, idx) => {
156    var tool = tools[idx];
157    var len = tool.points.length;
158    const newClassification = "@counter__" + getCounterShape(tool.classification) + "__" + $(el).val();
159    while (len--) {
160        var md = tool.points[len].metaData;
161        md.Classification = md.Classification.replace(tool.classification, newClassification);
162        annotationManager.setMetadata(tool.points[len], md);
163    }
164    tool.classification = newClassification;
165}
166
167const toggleVisibility = (el, idx) => {
168    finishDrawing();
169    var tool = tools[idx];
170    var len = tool.points.length;
171    if (!toolsToHide.includes(idx)) {
172        toolsToHide.push(idx);
173        $(el.firstElementChild).removeClass("fa-eye");
174        $(el.firstElementChild).addClass("fa-eye-slash");
175        $(".draw-counter-" + idx).prop("disabled", true);
176        while (len--) {
177            annotationManager.viewport.showAnnotation(tool.points[len].getId(), false);
178        }
179    } else {
180        toolsToHide = toolsToHide.filter(x => x !== idx);
181        $(el.firstElementChild).removeClass("fa-eye-slash");
182        $(el.firstElementChild).addClass("fa-eye");
183        $(".draw-counter-" + idx).prop("disabled", false);
184        while (len--) {
185            annotationManager.viewport.showAnnotation(tool.points[len].getId(), true);
186        }
187    }
188}
189
190const groupBy = (list, keyGetter) => {
191    const map = new Map();
192    list.forEach((item) => {
193        const key = keyGetter(item);
194        const collection = map.get(key);
195        if (!collection) {
196            map.set(key, [item]);
197        } else {
198            collection.push(item);
199        }
200    });
201    return map;
202}
203
204const saveCounters = () => {
205    annotationManager.saveAnnotations();
206}
207
208const toggleFullscreen = () => {
209    if (document.fullscreenElement) {
210        document.exitFullscreen();
211    } else {
212        document.body.requestFullscreen();
213    }
214}
215
216const initializeNewCounter = (shape, width, height, color, classification, annotations = []) => {
217    if (!shape) {
218        shape = "Point";
219    }
220    if (!color) {
221        color = "#" + Math.floor(Math.random() * 16777215).toString(16);
222    }
223    if (!classification) {
224        classification = `@counter__${shape}[${width},${height}]__Counter${tools.length}`;
225    }
226
227    let shapeIcon = "";
228    switch (shape) {
229        case "Point":
230            shapeIcon += `<i class="fas fa-circle fa-xs"></i>`;
231            break;
232        case "Arrow":
233            shapeIcon += `<i class="fas fa-long-arrow-alt-up fa-xs" style="transform: rotate(45deg);"></i>`;
234            break;
235        case "Rectangle":
236            shapeIcon += height ? `<div style="positon: absolute;height: 0;"><div style="font-size: x-small;font-weight: bold;letter-spacing: -1px;rotate: 90deg;right: -10px;position: relative;top: -18px;">${height}</div></div>` : "";
237            shapeIcon += `<i class="far fa-square" style="transform: scaleY(0.5);align-self: ${height ? "baseline" : "unset"};"></i>`;
238            shapeIcon += width ? `<div style="font-size: x-small;font-weight: bold;letter-spacing: -1px;align-self: baseline;">${width}</div>` : "";
239            break;
240        case "Square":
241            shapeIcon += `<div style="font-size: x-small;font-weight: bold;letter-spacing: -1px;">${width}</div>`;
242            shapeIcon += `<i class="far fa-square fa-xs"></i>`;
243            break;
244        case "Ellipse":
245            shapeIcon += `<i class="far fa-circle" style="transform: scaleY(0.5);"></i>`;
246            break;
247        case "Circle":
248            shapeIcon += `<div style="font-size: x-small;font-weight: bold;letter-spacing: -1px;">${width}</div>`;
249            shapeIcon += `<i class="far fa-circle fa-xs"></i>`;
250            break;
251    }
252
253    $(".counter-tools-list").append(`<li class="list-group-item py-1 px-2 rounded my-1" data-id="${tools.length}">
254        <div class="row g-1 flex-nowrap align-items-center">
255            <div class="col-auto">
256                <button type="button" class="btn btn-sm btn-light" title="Hide / unhide counter" onClick="toggleVisibility(this, ${tools.length})">
257                    <i class="fas fa-eye"></i>
258                </button>
259            </div>
260            <div class="col-1">
261                <input type="color"
262                    class="form-control form-control-sm form-control-color p-0 bg-transparent w-100 counter-color-${tools.length}"
263                    value="${color}" title="Choose your color" onChange="changeColor(this, ${tools.length})" />
264            </div>
265            <div class="col-1">
266                <div class="d-flex justify-content-center align-items-center" style="flex-direction: column-reverse;">
267                    ${shapeIcon}
268                </div>
269            </div>
270            <div class="col-4">
271                <input class="form-control form-control-sm counter-classification-${tools.length}" type="text" value="${getCounterName(classification)}" onChange="classificationChange(this, ${tools.length})" />
272            </div>
273            <div class="col-2">
274                <input class="form-control form-control-sm counter-count-${tools.length}" type="text" value="0" disabled readonly />
275            </div>
276            <div class="col-auto">
277                <button type="button" class="btn btn-sm btn-light draw-counter-${tools.length}" title="Add annotations" onClick="drawCounter(${tools.length})">
278                    <i class="fas fa-plus-circle"></i>
279                </button>
280            </div>
281            <div class="col-auto">
282                <button type="button" class="btn btn-sm btn-light" title="Delete counter" onClick="removeTool(${tools.length})">
283                    <i class="fas fa-trash"></i>
284                </button>
285            </div>
286        </div>
287    </li>`);
288
289    tools.push({
290        id: tools.length,
291        shape: shape,
292        classification: classification,
293        color: color,
294        points: annotations,
295        width: width,
296        height: height,
297        classificationElement: $(`.counter-classification-${tools.length}`)[0],
298        countElement: $(`.counter-count-${tools.length}`)[0],
299        colorElement: $(`.counter-color-${tools.length}`)[0],
300    });
301};
302
303const loadCounters = () => {
304    var anns = annotationManager.viewport.getAnnotations();
305    var counters = groupBy(anns.filter(a => a.metaData.Classification.startsWith("@counter__")), (a) => getCounterShape(a.metaData.Classification) + "__" + getCounterName(a.metaData.Classification));
306
307    counters.forEach((v, k) => {
308        var classification = "@counter__" + k;
309        if (Array.isArray(v) && v.length > 0) {
310            const color = v[0].metaData.Color;
311            const drawing = getCounterShape(v[0].metaData.Classification).slice(0, -1).split('[');
312            let drawingType = drawing[0];
313            let dimensions = drawing[1].split(',');
314            let width = parseInt(dimensions[0]) ?? 0;
315            let height = parseInt(dimensions[1]) ?? 0;
316            initializeNewCounter(drawingType, width, height, color, classification, [...v.sort((a, b) => getCounterPointIndex(a.metaData.Classification) - getCounterPointIndex(b.metaData.Classification))]);
317        } else {
318            initializeNewCounter(undefined, 0, 0, undefined, classification, []);
319        }
320    });
321
322    updateCounts();
323}
324
325jQuery(function () {
326    console.log(`PMA.UI version: ${PMA.UI.getVersion()}`);
327
328    // Create a context
329    context = new PMA.UI.Components.Context({ caller: caller });
330
331    // Add an autologin authentication provider
332    new PMA.UI.Authentication.AutoLogin(context, [{ serverUrl: serverUrl, username: serverUsername, password: serverPassword }]);
333
334    // Create an image loader that will allow us to load images easily
335    slideLoader = new PMA.UI.Components.SlideLoader(context, {
336        element: viewerElementSelector,
337        annotations: {},
338        filename: false,
339        barcode: false,
340        annotationsLayers: false,
341        fullscreenControl: false,
342    });
343
344    // Listen for the slide loaded event by the slide loader
345    slideLoader.listen(PMA.UI.Components.Events.SlideLoaded, function (args) {
346        slideLoader.mainViewport.showAnnotationsLabels(false, false);
347        $(".counter-main").removeClass("d-none");
348        annotationManager = new PMA.UI.Components.Annotations({
349            context: context,
350            element: null,
351            viewport: slideLoader.mainViewport,
352            serverUrl: args.serverUrl,
353            path: args.path,
354            enabled: true
355        });
356
357        loadCounters();
358
359        annotationManager.listen(PMA.UI.Components.Events.AnnotationAdded, function (e) {
360            console.log("Annotation added", e.feature);
361            var tool = tools[activeTool];
362            var md = e.feature.metaData;
363            md.Classification = tool.classification + "__$" + tool.points.length;
364            annotationManager.setMetadata(e.feature, md);
365            tool.points.push(e.feature);
366            updateCounts();
367            setTimeout(() => drawCounter(activeTool, true), 150);
368        });
369
370        annotationManager.listen(PMA.UI.Components.Events.AnnotationsSelectionChanged, function (e) {
371            if (!e || e.length === 0) {
372                $(".remove-selected-btn").addClass("d-none");
373            } else {
374                $(".remove-selected-btn").removeClass("d-none");
375            }
376        });
377
378        document.addEventListener('keydown', function (event) {
379            const key = event.key;
380            if (key === "Delete") {
381                removeHighlightedPoints();
382            }
383        });
384    });
385
386    // Load the image with the context
387    slideLoader.load(serverUrl, imagePath);
388
389    $(".add-new-counter").on("click", (e) => {
390        let width = 0;
391        let height = 0;
392        let shape = $(e.target).data("shape");
393        if ($(e.target).data("fixed")) {
394            if (shape === "Circle") {
395                width = Number(window.prompt("Provide diameter in micrometers", ""));
396                height = width;
397            } else if (shape === "Square") {
398                width = Number(window.prompt("Provide length in micrometers", ""));
399                height = width;
400            } else {
401                width = Number(window.prompt("Provide width in micrometers", ""));
402                height = Number(window.prompt("Provide height in micrometers", ""));
403            }
404            if (width <= 0 || height <= 0) return;
405        }
406        initializeNewCounter(shape, width, height);
407    });
408    $(".save-counters").on("click", () => saveCounters());
409});