Counter tool
advancedviewportcounter
Console
PMA.UI version: 2.43.3
Counter tool example
counter.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/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});