Annotations
expertslide loaderannotations
Console
PMA.UI version: 2.43.3
Annotations example with annotation operations and available events.
annotations.html
1<!doctype >
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> 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> 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});