PMA.UI Examples 2.43.3by Pathomation

Annotations

expertslide loaderannotations
Console
PMA.UI version: 2.43.3
Annotations example with annotation operations and available events.
annotations.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/annotations.js"></script>
23    <link href="./css/annotations.css" type="text/css" rel="stylesheet">
24
25    <title>Annotations</title>
26</head>
27
28<body>
29    <div class="container-fluid">
30        <div class="row">
31            <!-- The element that will host annotation actions -->
32            <div class="col-auto annotations">
33                <div class="btn-group-vertical btn-group-md">
34                    <button type="button" class="btn btn-light" disabled data-action="draw" data-type="Freehand"
35                        title="Freehand">
36                        <i class="fas fa-pencil-alt"></i>
37                    </button>
38                    <button type="button" class="btn btn-light" disabled data-action="draw" data-type="CompoundFreehand"
39                        title="Compound polygon">
40                        <i class="fab fa-pushed"></i>
41                    </button>
42                    <button type="button" class="btn btn-light" disabled data-action="draw" data-type="ClosedFreehand"
43                        title="Closed freehand polygon">
44                        <i class="fas fa-draw-polygon"></i>
45                    </button>
46                    <button type="button" class="btn btn-light" disabled data-action="draw" data-type="Arrow"
47                        title="Arrow">
48                        <i class="fas fa-arrow-right"></i>
49                    </button>
50                    <button type="button" class="btn btn-light" disabled data-action="draw" data-type="Point"
51                        title="Point">
52                        <i class="far fa-dot-circle"></i>
53                    </button>
54                    <button type="button" class="btn btn-light" disabled data-action="draw" data-type="Line"
55                        title="Line">
56                        <i class="fas fa-minus"></i>
57                    </button>
58                    <button type="button" class="btn btn-light" disabled data-action="draw" data-type="MultiPoint"
59                        title="MultiPoint">
60                        <i class="fas fa-braille"></i>
61                    </button>
62                    <div class="dropdown-divider horizontal"></div>
63                    <button type="button" class="btn btn-light" disabled data-action="edit" data-type="Subtract"
64                        title="Subtract">
65                        <span class="fa-stack">
66                            <i class="fas fa-pencil-alt fa-stack-1x fa-flip-horizontal"></i>
67                            <i class="fas fa-minus fa-stack-1x small"></i>
68                        </span>
69                    </button>
70                    <button type="button" class="btn btn-light" disabled data-action="edit" data-type="Addition"
71                        title="Addition">
72                        <span class="fa-stack">
73                            <i class="fas fa-pencil-alt fa-stack-1x fa-flip-horizontal"></i>
74                            <i class="fas fa-plus fa-stack-1x small"></i>
75                        </span>
76                    </button>
77                    <div class="dropdown-divider horizontal"></div>
78                    <button type="button" class="btn btn-light" disabled data-action="edit" data-type="Modify"
79                        title="Modify">
80                        <i class="fas fa-expand"></i>
81                    </button>
82                    <button type="button" class="btn btn-light" disabled data-action="edit" data-type="Transform"
83                        title="Transform">
84                        <i class="fas fa-arrows-alt"></i>
85                    </button>
86                    <div class="dropdown-divider horizontal"></div>
87                    <button type="button" class="btn btn-light" disabled data-action="union" data-type=""
88                        title="Union - Combine 2 or more annotations into a single annotation. In this example only annotations of type Compound polygon and Arrow are allowed. Disabled if less than 2 annotations selected or incompatible annotations selected!">
89                        <i class="fas fa-plus-square"></i>
90                    </button>
91                    <button type="button" class="btn btn-light" disabled data-action="difference" data-type=""
92                        title="Difference - Remove 2nd annotation from the 1st one. In this example only annotations of type Compound polygon and Arrow are allowed. Disabled if less or greater than 2 annotations selected or incompatible annotations selected!">
93                        <i class="fas fa-minus-square"></i>
94                    </button>
95                    <div class="dropdown-divider horizontal"></div>
96                    <input id="color-picker-native" type='color' value='#78eb10' class="color-picker px-2" />
97                    <input id="color-picker-js" type='button' value=' ' class="btn btn-light" style="width: 50px" />
98                </div>
99                <input id="color-picker-hidden" type='hidden' value='#78eb10' class="color-picker" />
100            </div>
101            <div class="col d-flex flex-column vh-100">
102                <div class="row top-row p-1 g-2 align-items-center">
103                    <div class="col-auto">
104                        <div class="btn-group btn-group-sm">
105                            <button type="button" class="btn btn-secondary" disabled data-action="delete" data-type
106                                title="Delete">
107                                <i class="fas fa-times"></i>
108                            </button>
109                            <button type="button" class="btn btn-secondary" disabled data-action="deleteall" data-type
110                                title="Delete all">
111                                <i class="fas fa-times-circle"></i>
112                            </button>
113                            <button type="button" class="btn btn-secondary" disabled data-action="save" data-type
114                                title="Save">
115                                <i class="fas fa-save"></i>
116                            </button>
117                        </div>
118                    </div>
119                    <div class="col-auto">
120                        <div class="btn-group btn-group-sm justify-content-around"
121                            style="width: 350px; min-width: fit-content;">
122                            <div class="custom-control custom-switch">
123                                <input type="checkbox" class="custom-control-input" checked disabled id="chkLabels">
124                                <label class="custom-control-label" for="chkLabels">
125                                    <i class="fas fa-font"></i>&nbsp;Annotation labels
126                                </label>
127                            </div>
128                            <div class="custom-control custom-switch">
129                                <input type="checkbox" class="custom-control-input" checked disabled
130                                    id="chkMeasurements">
131                                <label class="custom-control-label" for="chkMeasurements">
132                                    <i class="fas fa-ruler"></i>&nbsp;Measurements
133                                </label>
134                            </div>
135                        </div>
136                    </div>
137                    <div class="col-auto">
138                        <div class="form-group mb-0">
139                            <div class="row g-2">
140                                <div class="col-auto">
141                                    <div class="form-floating" id="annotation-text-div" style="display: none">
142                                        <input type="text" class="form-control form-control-sm" disabled
143                                            id="annotation-text" placeholder="Label" />
144                                        <label for="annotation-text">Label</label>
145                                    </div>
146                                </div>
147                                <div class="col-auto">
148                                    <div class="form-floating" id="classification-text-div" style="display: none">
149                                        <input type="text" class="form-control form-control-sm" disabled
150                                            id="classification-text" placeholder="Classification" />
151                                        <label for="classification-text">Classification</label>
152                                    </div>
153                                </div>
154                                <div class="col-auto">
155                                    <div class="form-floating" id="txt-length-div" style="display: none">
156                                        <input type="text" class="form-control form-control-sm" disabled
157                                            id="txt-length" />
158                                        <label for="txt-length">Length</label>
159                                    </div>
160                                </div>
161                                <div class="col-auto">
162                                    <div class="form-floating" id="txt-area-div" style="display: none">
163                                        <input type="text" class="form-control form-control-sm" disabled
164                                            id="txt-area" />
165                                        <label for="txt-area">Area</label>
166                                    </div>
167                                </div>
168                                <div class="col-auto d-flex justify-content-center align-items-center">
169                                    <span class="annotation-helper-icon">
170                                        <i class="fas fa-spinner fa-spin"></i>
171                                        <span class="badge bg-success"><i class="fas fa-check"></i> Saved</span>
172                                        <span class="badge bg-danger"><i class="fas fa-times"></i> Error</span>
173                                    </span>
174                                </div>
175                            </div>
176                        </div>
177                    </div>
178                </div>
179                <div class="row viewer-row flex-grow-1">
180                    <div class="col p-0">
181                        <!-- The element that will host the slide loader -->
182                        <div id="viewer"></div>
183                    </div>
184                </div>
185            </div>
186        </div>
187</body>
188
189</html>
annotations.css
1html,
2body {
3    height: 100%;
4    padding: 0px;
5    margin: 0px;
6}
7
8body .pma-ui-viewport-container {
9    height: 100%;
10    width: 100%;
11}
12
13.aligned-text {
14    display: inline-block;
15    text-align: right;
16    width: 70px;
17}
18
19.annotation-helper-icon i,
20.annotation-helper-icon span {
21    display: none;
22}
23
24.annotation-helper-icon.loading i {
25    display: initial;
26}
27
28.annotation-helper-icon.loading span {
29    display: none;
30}
31
32.annotation-helper-icon.saved i,
33.annotation-helper-icon.saved span.badge.bg-danger {
34    display: none;
35}
36
37.annotation-helper-icon.saved span.badge.bg-success {
38    display: initial;
39}
40
41.annotation-helper-icon.error i,
42.annotation-helper-icon.error span.badge.bg-success {
43    display: none;
44}
45
46.annotation-helper-icon.error span.badge.bg-danger {
47    display: block;
48}
49
50.annotations {
51    margin: 0 auto;
52    padding: 0;
53}
54
55.color-picker {
56    height: 35px;
57    margin-bottom: 1px;
58    border: none;
59}
60
61.top-row,
62#color-picker-native,
63.annotations {
64    background-color: #f8f9fa;
65}
66
67.top-row {
68    min-height: 74px;
69}
70
71.btn.btn-light.active {
72    background-color: #d2d2d2;
73}
74
75.btn:disabled {
76    cursor: not-allowed;
77    pointer-events: unset;
78}
79
80.dropdown-divider.horizontal {
81    height: 1px !important;
82    width: 80% !important;
83    margin: 0.5rem auto !important;
84}
85
86.fa-stack-1x.small {
87    left: 1em;
88    top: -1em;
89    font-size: 0.5em;
90}
annotations.js
1// Initial declarations
2var serverUrl = "https://host.pathomation.com/pma.core.2/";
3var serverUsername = "PMA_UI_demo";
4var serverPassword = "PMA_UI_demo";
5var caller = "DemoPortal";
6var slideLoaderElementSelector = "#viewer";
7var imagePath = "Reference/Annotations/CMU-1/CMU-1.svs";
8var context = null;
9var slideLoader = null;
10var annotationManager = null;
11
12function supportsColorPicker() {
13    var colorInput;
14    colorInput = $('<input type="color" value="!" />')[0];
15    return colorInput.type === 'color' && colorInput.value !== '!';
16}
17
18function drawCommands(action, type) {
19    if (action) {
20        if (action === "draw") {
21            var color = $(".color-picker").val();
22
23            var f = annotationManager.getSelection();
24
25            if (type === "MultiPoint") {
26                var mpAnnot = slideLoader.mainViewport.getAnnotations().find(x => x.metaData.Geometry.toLowerCase().indexOf(type.toLowerCase()) !== -1);
27                if (mpAnnot) {
28                    f = [mpAnnot];
29                } else {
30                    f = [];
31                }
32            }
33
34            annotationManager.startDrawing({
35                type: type,
36                color: color,
37                fillColor: "rgba(0,0,0,0)",
38                lineThickness: Math.floor(Math.random() * 4) + 1,
39                iconRelativePath: null,
40                feature: type === "MultiPoint" && f.length > 0 ? f[0] : undefined,
41                notes: $("#annotation-text").val() ? $("#annotation-text").val() : ""
42            });
43        } else if (action === "edit") {
44            var f = annotationManager.getSelection();
45            if (type === "Subtract" || type === "Addition") {
46                f = null;
47                annotationManager.clearSelection();
48            }
49            annotationManager.startTool({
50                type: type,
51                brushType: 'circle',
52                brushSize: 1000,
53                iconRelativePath: null,
54                feature: annotationManager.getSelection()[0],
55                color: $(".color-picker").val(),
56                fillColor: "rgba(33,44,55, 0.6)",
57                lineThickness: Math.floor(Math.random() * 4) + 1,
58                notes: $("#annotation-text").val() ? $("#annotation-text").val() : ""
59            });
60        } else if (action === "measure") {
61            if (type === "clear") {
62                slideLoader.mainViewport.stopMeasuring();
63                return;
64            }
65
66            slideLoader.mainViewport.startMeasuring(type);
67        } else if (action === "save") {
68            saveAnnotations();
69        } else if (action === "delete") {
70            var ann = annotationManager.getSelection();
71
72            if (ann && ann.length > 0) {
73                annotationManager.deleteAnnotation(ann[0].getId());
74            }
75        } else if (action === "deleteall") {
76            var allAnnotations = slideLoader.mainViewport.getAnnotations();
77
78            for (var i = 0; i < allAnnotations.length; i++) {
79                annotationManager.deleteAnnotation(allAnnotations[i].getId());
80            }
81        } else if (action === "merge") {
82            annotationManager.mergeSelection();
83        } else if (action === "union") {
84            annotationManager.booleanUnion(annotationManager.getSelection());
85        } else if (action === "difference") {
86            annotationManager.booleanDifference(annotationManager.getSelection()[0], annotationManager.getSelection()[1]);
87        }
88    }
89}
90
91function saveAnnotations(e) {
92    if (e) {
93        var metadata = e.hasOwnProperty("feature") ? e.feature.metaData : (e.hasOwnProperty("length") && e.length !== 0 ? e[0].metaData : null);
94        if (metadata) {
95            metadata.Notes = $("#annotation-text").val() ? $("#annotation-text").val() : "";
96        }
97    }
98
99    $(".annotation-helper-icon").addClass("loading");
100    annotationManager.saveAnnotations();
101}
102
103jQuery(function () {
104    var jsColorPicker = null;
105    if (supportsColorPicker()) {
106        $("#color-picker-js").remove();
107        $("#color-picker-hidden").remove();
108    } else {
109        $("#color-picker-native").remove();
110        jsColorPicker = new jscolor($("#color-picker-js")[0], {
111            valueElement: $("#color-picker-hidden")[0],
112            hash: true,
113            closable: true,
114            closeText: "Close"
115        });
116    }
117
118    $("button.color-btn").on("click", function (e) {
119        var clr = $(this).data("color");
120        $("#color-picker-native").val(clr);
121        if (jsColorPicker) {
122            jsColorPicker.fromString(clr);
123        }
124    });
125
126    $(".color-picker").on("change", function (e) {
127        var annots = annotationManager.getSelection();
128        if (annots.length > 0) {
129            annots.forEach((ann) => {
130                var md = ann.metaData;
131                md.Color = e.target.value;
132                annotationManager.setMetadata(ann, md);
133            });
134        }
135    })
136
137    $("button[data-action][data-type], a[data-action][data-type]").on("click", function (e) {
138        var shouldReturn = $("button.active").data("type") === $(this).data("type");
139        e.preventDefault();
140        $(this).trigger("blur");
141        if ($("button.active")?.data("action")?.toLowerCase() === "edit") {
142            annotationManager.stopTool();
143        }
144        if ($("button.active")?.data("action")?.toLowerCase() === "draw") {
145            annotationManager.finishDrawing(false, $("button.active").data("type"));
146        }
147        $("button.active").removeClass("active");
148        if (shouldReturn) return;
149
150        $("#annotation-text").val("");
151        drawCommands($(this).data("action"), $(this).data("type"));
152
153        if ($(this).data("type")) {
154            $(this).addClass("active");
155        }
156    });
157
158    console.log(`PMA.UI version: ${PMA.UI.getVersion()}`);
159
160    // Create a context
161    context = new PMA.UI.Components.Context({ caller: caller });
162
163    // Add an autologin authentication provider
164    new PMA.UI.Authentication.AutoLogin(context, [{ serverUrl: serverUrl, username: serverUsername, password: serverPassword }]);
165
166    // Create an image loader that will allow us to load images easily
167    slideLoader = new PMA.UI.Components.SlideLoader(context, {
168        element: slideLoaderElementSelector,
169        annotations: {},
170        filename: false,
171        rotationControl: false,
172        barcode: false,
173        scaleLine: false,
174    });
175
176    // Listen for the slide loaded event by the slide loader
177    slideLoader.listen(PMA.UI.Components.Events.SlideLoaded, function (args) {
178        $("button, input").removeAttr("disabled");
179        $("#txt-length").attr("disabled", "");
180        $("#txt-area").attr("disabled", "");
181        annotationManager = new PMA.UI.Components.Annotations({
182            context: context,
183            element: null,
184            viewport: slideLoader.mainViewport,
185            serverUrl: args.serverUrl,
186            path: args.path,
187            enabled: true
188        });
189
190        annotationManager.listen(PMA.UI.Components.Events.AnnotationsSelectionChanged, function (e) {
191            if (e) {
192                $("button[data-action='delete']").attr("disabled", false);
193                var metadata = e.feature ? e.feature.metaData : (e.hasOwnProperty("length") && e.length !== 0 ? e[0].metaData : null);
194                if (metadata) {
195                    console.log("selection: ", metadata);
196                    $("#annotation-text").val(metadata.Notes);
197                    $("#classification-text").val(metadata.Classification);
198                    $("#txt-area").val(metadata.FormattedArea || " - ");
199                    $("#txt-length").val(metadata.FormattedLength || " - ");
200                } else {
201                    $("button[data-action='delete']").attr("disabled", true);
202                    $("#annotation-text").val("");
203                    $("#classification-text").val("");
204                    $("#txt-area").val(" - ");
205                    $("#txt-length").val(" - ");
206                }
207            }
208        });
209
210        annotationManager.listen(PMA.UI.Components.Events.AnnotationsSaved, function (e) {
211            if (e && e.success) {
212                $(".annotation-helper-icon").removeClass("loading").addClass("saved");
213            } else {
214                $(".annotation-helper-icon").removeClass("loading").addClass("error");
215            }
216
217            $("button[data-action=save][data-type]").removeClass("active");
218
219            setTimeout(function () {
220                $(".annotation-helper-icon").removeClass("loading error saved");
221            }, 1000);
222        });
223
224        annotationManager.listen(PMA.UI.Components.Events.AnnotationAdded, function (e) {
225            console.log("Annotation added", e.feature);
226            $("button.active").removeClass("active");
227            annotationManager.fireEvent(PMA.UI.Components.Events.AnnotationsSelectionChanged, { feature: e.feature });
228            setTimeout(() => annotationManager.selectAnnotation(e.feature.getId()), 150);
229        });
230
231        annotationManager.listen(PMA.UI.Components.Events.AnnotationDrawing, function (e) {
232            // Event called while drawing annotations
233        });
234
235        annotationManager.listen(PMA.UI.Components.Events.AnnotationDeleted, function (e) {
236            console.log("Annotation deleted", e.feature);
237            $("button[data-action='delete']").attr("disabled", true);
238            $("#annotation-text").val("");
239            $("#classification-text").val("");
240            $("#txt-area").val(" - ");
241            $("#txt-length").val(" - ");
242        });
243
244        annotationManager.listen(PMA.UI.Components.Events.AnnotationModified, function (e) {
245            console.log("Annotation modified", e.feature);
246            $("button.active").removeClass("active");
247        });
248
249        annotationManager.listen(PMA.UI.Components.Events.AnnotationEditingEnded, function (e) {
250            console.log("Annotation editing ended", e.feature);
251            $("button.active").removeClass("active");
252            if (e.feature) {
253                annotationManager.fireEvent(PMA.UI.Components.Events.AnnotationsSelectionChanged, { feature: e.feature });
254                setTimeout(() => annotationManager.selectAnnotation(e.feature.getId()), 150);
255            }
256        });
257    });
258
259    // Load the image with the context
260    slideLoader.load(serverUrl, imagePath);
261
262    $("#annotation-text").on("keydown focusout", function (event) {
263        if (event.type === "keydown" && event.key !== "Enter") {
264            return;
265        }
266        if (annotationManager && annotationManager.getSelection().length !== 0) {
267            var sel = annotationManager.getSelection()[0];
268            if (sel.metaData) {
269                sel.metaData.Notes = $(this).val() ? $(this).val() : "";
270                annotationManager.setMetadata(sel, sel.metaData);
271            }
272        }
273    });
274
275    $("#classification-text").on("keydown focusout", function (event) {
276        if (event.type === "keydown" && event.key !== "Enter") {
277            return;
278        }
279        if (annotationManager && annotationManager.getSelection().length !== 0) {
280            var sel = annotationManager.getSelection()[0];
281            if (sel.metaData) {
282                sel.metaData.Classification = $(this).val() ? $(this).val() : "";
283                annotationManager.setMetadata(sel, sel.metaData);
284            }
285        }
286    });
287
288    $("body").on("keydown", function (event) {
289        if (!$(event.target).is('input') && (event.key === "Delete" || event.key === "Del")) {
290            drawCommands("delete", null);
291        }
292    });
293
294    $("#chkLabels").on("change", function () {
295        var selectedAnnotations = annotationManager.getSelection().map(ann => ann);
296        annotationManager.clearSelection();
297        var labels = $("#chkLabels").prop("checked");
298        var measurements = $("#chkMeasurements").prop("checked");
299        console.log(`Show labels: ${labels} - Show measurements: ${measurements}`);
300        slideLoader.mainViewport.showAnnotationsLabels(labels, measurements);
301        selectedAnnotations.forEach(ann => {
302            var feats = annotationManager.selectInteraction.getFeatures();
303            feats.push(ann);
304        });
305    });
306
307    $("#chkMeasurements").on("change", function () {
308        var selectedAnnotations = annotationManager.getSelection().map(ann => ann);
309        annotationManager.clearSelection();
310        var labels = $("#chkLabels").prop("checked");
311        var measurements = $("#chkMeasurements").prop("checked");
312        console.log(`Show labels: ${labels} - Show measurements: ${measurements}`);
313        slideLoader.mainViewport.showAnnotationsLabels(labels, measurements);
314        selectedAnnotations.forEach(ann => {
315            var feats = annotationManager.selectInteraction.getFeatures();
316            feats.push(ann);
317        });
318    });
319
320    setInterval(function () {
321        if (!annotationManager) return;
322        let anns = annotationManager.getSelection();
323        if (anns.length === 1) {
324            $("#annotation-text-div").css("display", "block");
325            $("#classification-text-div").css("display", "block");
326            if ($("#txt-length").val() !== " - ") {
327                $("#txt-length-div").css("display", "block");
328            } else {
329                $("#txt-length-div").css("display", "none");
330            }
331            if ($("#txt-area").val() !== " - ") {
332                $("#txt-area-div").css("display", "block");
333            } else {
334                $("#txt-area-div").css("display", "none");
335            }
336        } else {
337            $("#annotation-text-div").css("display", "none");
338            $("#classification-text-div").css("display", "none");
339            $("#txt-length-div").css("display", "none");
340            $("#txt-area-div").css("display", "none");
341        }
342
343        // if (anns.length === 1 && anns[0].metaData.Geometry.startsWith("POLYGON")) {
344        //     $('button[data-type="Subtract"]').removeAttr("disabled");
345        // } else {
346        //     $('button[data-type="Subtract"]').attr("disabled", "");
347        // }
348
349        if (anns.length > 1 && anns.every(x => x.metaData.Geometry.startsWith("POLYGON"))) {
350            $('button[data-action="union"]').removeAttr("disabled");
351            if (anns.length === 2) {
352                $('button[data-action="difference"]').removeAttr("disabled");
353            } else {
354                $('button[data-action="difference"]').attr("disabled", "");
355            }
356        } else {
357            $('button[data-action="union"]').attr("disabled", "");
358            $('button[data-action="difference"]').attr("disabled", "");
359        }
360
361        if ($("button.active")?.data("action")?.toLowerCase() === "draw" && annotationManager.snapInteraction?.getActive()) {
362            annotationManager.snapInteraction.setActive(false);
363        }
364
365        if ($(annotationManager.cancelDrawingButton).hasClass("bound")) return;
366        $(annotationManager.cancelDrawingButton).addClass("bound")
367        $(annotationManager.cancelDrawingButton).on("click", function (e) {
368            $("button.active").removeClass("active");
369        });
370    }, 100);
371});