@abi-software/flatmap-viewer 2.4.2-b.5 → 2.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/annotation.js DELETED
@@ -1,665 +0,0 @@
1
- /******************************************************************************
2
-
3
- Flatmap viewer and annotation tool
4
-
5
- Copyright (c) 2019 - 2023 David Brooks
6
-
7
- Licensed under the Apache License, Version 2.0 (the "License");
8
- you may not use this file except in compliance with the License.
9
- You may obtain a copy of the License at
10
-
11
- http://www.apache.org/licenses/LICENSE-2.0
12
-
13
- Unless required by applicable law or agreed to in writing, software
14
- distributed under the License is distributed on an "AS IS" BASIS,
15
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- See the License for the specific language governing permissions and
17
- limitations under the License.
18
-
19
- ******************************************************************************/
20
-
21
- 'use strict';
22
-
23
- //==============================================================================
24
-
25
- // We use Font Awesome icons
26
- import '@fortawesome/fontawesome-free/css/all.css';
27
- import escape from 'html-es6cape';
28
- import { jsPanel } from 'jspanel4';
29
- import 'jspanel4/dist/jspanel.css';
30
-
31
- //==============================================================================
32
-
33
- const FETCH_TIMEOUT = 3000; // 3 seconds
34
- const UPDATE_TIMEOUT = 3000; // 5 seconds
35
- const LOGIN_TIMEOUT = 30000; // 30 seconds
36
- const LOGOUT_TIMEOUT = 3000; // 5 seconds
37
-
38
- const STATUS_MESSAGE_TIMEOUT = 3000;
39
-
40
- //==============================================================================
41
-
42
- const FEATURE_DISPLAY_PROPERTIES = {
43
- 'id': 'Feature',
44
- 'label': 'Tooltip',
45
- 'models': 'Models',
46
- 'name': 'Name',
47
- 'sckan': 'SCKAN valid',
48
- 'fc-class': 'FC class',
49
- 'fc-kind': 'FC kind',
50
- 'layer': 'Map layer',
51
- }
52
-
53
- const ANNOTATION_FIELDS = [
54
- {
55
- prompt: 'Feature derived from',
56
- key: 'prov:wasDerivedFrom',
57
- update: true,
58
- kind: 'list',
59
- size: 6
60
- },
61
- {
62
- prompt: 'Comment',
63
- key: 'rdfs:comment',
64
- update: false,
65
- kind: 'textbox'
66
- },
67
- ];
68
-
69
- //==============================================================================
70
-
71
- function startSpinner(panel)
72
- {
73
- panel.headerlogo.innerHTML = '<span class="fa fa-spinner fa-spin ml-2"></span>';
74
- }
75
-
76
- function stopSpinner(panel)
77
- {
78
- panel.headerlogo.innerHTML = '';
79
- }
80
-
81
- //==============================================================================
82
-
83
- export class Annotator
84
- {
85
- constructor(flatmap, ui)
86
- {
87
- this.__flatmap = flatmap;
88
- this.__ui = ui;
89
- this.__haveAnnotation = false;
90
- this.__user = undefined;
91
- this.__savedStatusMessage = '';
92
- this.__authorised = false;
93
- }
94
-
95
- get user()
96
- {
97
- return this.__user;
98
- }
99
-
100
- __creatorName(creator)
101
- {
102
- return creator.name || creator.email || creator.login || creator.company || creator;
103
- }
104
-
105
- __setUser(creator)
106
- {
107
- this.__user = creator;
108
- this.__setStatusMessage(`Annotating as ${this.__creatorName(creator)}`, 0)
109
- }
110
-
111
- __clearUser()
112
- {
113
- this.__user = undefined;
114
- this.__setStatusMessage('', 0);
115
- }
116
-
117
- async __authorise(panel)
118
- //======================
119
- {
120
- const abortController = new AbortController();
121
- setTimeout((panel) => {
122
- if (this.user === 'undefined') {
123
- console.log("Aborting login...");
124
- abortController.abort();
125
- stopSpinner(panel);
126
- this.__setStatusMessage('Unable to login...');
127
- }
128
- },
129
- LOGIN_TIMEOUT, panel);
130
-
131
- const url = `${this.__flatmap._baseUrl}login`;
132
- startSpinner(panel);
133
- const response = await fetch(url, {
134
- headers: { "Content-Type": "application/json; charset=utf-8" },
135
- signal: abortController.signal
136
- });
137
- stopSpinner(panel);
138
- if (response.ok) {
139
- const user_data = await response.json();
140
- if ('error' in user_data) {
141
- return Promise.resolve({error: response.error});
142
- } else {
143
- this.__setUser(user_data);
144
- this.__authorised = true;
145
- return Promise.resolve(user_data);
146
- }
147
- } else {
148
- return Promise.resolve({error: `${response.status} ${response.statusText}`});
149
- }
150
- }
151
-
152
- async __unauthorise()
153
- //===================
154
- {
155
- const abortController = new AbortController();
156
- setTimeout(() => {
157
- if (this.__authorised) {
158
- console.log("Aborting logout...");
159
- abortController.abort();
160
- this.__setStatusMessage('Unable to logout...');
161
- }
162
- },
163
- LOGOUT_TIMEOUT);
164
-
165
- const url = `${this.__flatmap._baseUrl}logout`;
166
- const response = fetch(url, {
167
- headers: { "Content-Type": "application/json; charset=utf-8" },
168
- signal: abortController.signal
169
- });
170
- if (response.ok) {
171
- this.__authorised = false;
172
- return response.json();
173
- } else {
174
- return Promise.resolve({error: `${response.status} ${response.statusText}`});
175
- }
176
- }
177
-
178
- __setStatusMessage(message, timeout=STATUS_MESSAGE_TIMEOUT)
179
- //=========================================================
180
- {
181
- if (timeout == 0) {
182
- this.__savedStatusMessage = message;
183
- }
184
- this.__statusMessage.innerHTML = message;
185
- if (+timeout > 0) {
186
- setTimeout(() => {
187
- this.__statusMessage.innerHTML = this.__savedStatusMessage;
188
- }, +timeout);
189
- }
190
- }
191
-
192
- __featureHtml(featureProperties)
193
- //==============================
194
- {
195
- // Feature properties
196
- const html = [];
197
- for (const [key, prompt] of Object.entries(FEATURE_DISPLAY_PROPERTIES)) {
198
- const value = featureProperties[key];
199
- if (value !== undefined && value !== '') {
200
- const escapedValue = escape(value).replaceAll('\n', '<br/>');
201
- html.push(`<div><span class="flatmap-annotation-prompt">${prompt}:</span><span class="flatmap-annotation-value">${escapedValue}</span></div>`)
202
- }
203
- }
204
- return html;
205
- }
206
-
207
- __annotationHtml(annotations)
208
- //===========================
209
- {
210
- const html = [];
211
- let firstBlock = true;
212
- for (const annotation of annotations) {
213
- if (firstBlock) {
214
- firstBlock = false;
215
- } else {
216
- html.push('<hr/>')
217
- }
218
- if (annotation['rdf:type'] === 'prov:Entity') {
219
- const annotator = this.__creatorName(annotation['dct:creator']);
220
- html.push(`<div><span class="flatmap-annotation-prompt">${annotation['dct:created']}</span><span class="flatmap-annotation-value">${annotator}</span></div>`);
221
- for (const field of ANNOTATION_FIELDS) {
222
- const value = annotation[field.key];
223
- if (value !== undefined && value !== '') {
224
- const escapedValue = (field.kind === 'list')
225
- ? value.filter(v => v.trim()).map(v => escape(v.trim())).join(', ')
226
- : escape(value).replaceAll('\n', '<br/>');
227
- html.push(`<div><span class="flatmap-annotation-prompt">${field.prompt}:</span><span class="flatmap-annotation-value">${escapedValue}</span></div>`);
228
- }
229
- }
230
- }
231
- }
232
- return html.join('\n');
233
- }
234
-
235
- __editFormHtml(provenanceData)
236
- //============================
237
- {
238
- const html = [];
239
- html.push('<div id="flatmap-annotation-formdata">');
240
- for (const field of ANNOTATION_FIELDS) {
241
- html.push('<div class="flatmap-annotation-entry">');
242
- html.push(` <label for="${field.key}">${field.prompt}:</label>`);
243
- if (field.kind === 'textbox') {
244
- const value = field.update ? provenanceData[field.key] || '' : '';
245
- html.push(` <textarea rows="5" cols="40" id="${field.key}" name="${field.key}">${value.trim()}</textarea>`)
246
- } else if (!('kind' in field) || field.kind !== 'list') {
247
- const value = field.update ? provenanceData[field.key] || '' : '';
248
- html.push(` <input type="text" size="40" id="${field.key}" name="${field.key}" value="${value.trim()}"/>`)
249
- } else { // field.kind === 'list'
250
- const listValues = field.update ? provenanceData[field.key] || [] : [];
251
- html.push(' <div class="multiple">')
252
- for (let n = 1; n <= field.size; n++) {
253
- const fieldValue = (n <= listValues.length) ? listValues[n-1].trim() : '';
254
- html.push(` <input type="text" size="40" id="${field.key}_${n}" name="${field.key}" value="${fieldValue}"/>`)
255
- }
256
- html.push(' </div>')
257
- }
258
- html.push('</div>');
259
- }
260
- html.push(' <div><input id="annotation-save-button" type="button" value="Save"/></div>');
261
- html.push('</div>');
262
- return html.join('\n');
263
- }
264
-
265
- __provenanceData(annotations)
266
- //===========================
267
- {
268
- const provenanceData = {};
269
- for (const annotation of annotations) { // In order of most recent to oldest
270
- if (annotation['rdf:type'] === 'prov:Entity') {
271
- for (const field of ANNOTATION_FIELDS) {
272
- if (field.update) {
273
- const value = annotation[field.key];
274
- if (value !== undefined && !(field.key in provenanceData)) {
275
- provenanceData[field.key] = value;
276
- }
277
- }
278
- }
279
- }
280
- }
281
- return provenanceData;
282
- }
283
-
284
- __changedAnnotation(provenanceData)
285
- //=================================
286
- {
287
- const newProperties = {};
288
- let propertiesChanged = false;
289
- for (const field of ANNOTATION_FIELDS) {
290
- if (!('kind' in field) || field.kind !== 'list') {
291
- const lastValue = field.update ? provenanceData[field.key] || '' : '';
292
- const inputField = document.getElementById(field.key);
293
- const newValue = inputField.value.trim();
294
- if (newValue !== lastValue.trim()) {
295
- newProperties[field.key] = newValue;
296
- propertiesChanged = true;
297
- }
298
- } else { // field.kind === 'list'
299
- const listValues = [];
300
- for (let n = 1; n <= field.size; n++) {
301
- const inputField = document.getElementById(`${field.key}_${n}`);
302
- listValues.push(inputField.value.trim());
303
- }
304
- const lastValue = field.update ? provenanceData[field.key] || [] : [];
305
- const oldValues = lastValue.map(v => v.trim()).filter(v => (v !== '')).sort(Intl.Collator().compare);
306
- const newValues = listValues.map(v => v.trim()).filter(v => (v !== '')).sort(Intl.Collator().compare);
307
- if (oldValues.length !== newValues.length
308
- || oldValues.filter(v => !newValues.includes(v)).length > 0) {
309
- newProperties[field.key] = newValues;
310
- propertiesChanged = true;
311
- }
312
- }
313
- }
314
- return {
315
- changed: propertiesChanged,
316
- properties: newProperties
317
- }
318
- }
319
-
320
- async __updateRemoteAnnotation(panel, annotation)
321
- //===============================================
322
- {
323
- const abortController = new AbortController();
324
-
325
- setTimeout((panel) => {
326
- if (panel.status !== 'closed') {
327
- console.log("Aborting remote update...");
328
- abortController.abort();
329
- stopSpinner(panel);
330
- this.__setStatusMessage('Cannot update annotation...');
331
- }
332
- }, UPDATE_TIMEOUT, panel);
333
-
334
- const url = this.__flatmap.makeServerUrl(this.__currentFeatureId, 'annotator/');
335
- const response = await fetch(url, {
336
- headers: { "Content-Type": "application/json; charset=utf-8" },
337
- method: 'POST',
338
- body: JSON.stringify(annotation),
339
- signal: abortController.signal
340
- });
341
- if (response.ok) {
342
- return response.json();
343
- } else {
344
- return Promise.resolve({error: `${response.status} ${response.statusText}`});
345
- }
346
- }
347
-
348
- async __saveAnnotation(panel, provenanceData)
349
- //===========================================
350
- {
351
- const changedProperties = this.__changedAnnotation(provenanceData);
352
- if (this.__currentFeatureId !== undefined && changedProperties.changed) {
353
- const annotation = {
354
- ...changedProperties.properties,
355
- 'rdf:type': 'prov:Entity',
356
- 'dct:subject': `flatmaps:${this.__flatmap.uuid}/${this.__currentFeatureId}`,
357
- 'dct:creator': this.user
358
- }
359
- startSpinner(panel);
360
- const response = await this.__updateRemoteAnnotation(panel, annotation);
361
- stopSpinner(panel);
362
- if ('error' in response) {
363
- this.__setStatusMessage(response.error);
364
- } else {
365
- this.__flatmap.setFeatureAnnotated(this.__currentFeatureId);
366
- panel.close();
367
- }
368
- } else {
369
- this.__setStatusMessage('No changes to save...');
370
- }
371
- }
372
-
373
- __finishPanelContent(panel, response)
374
- //====================================
375
- {
376
- this.__haveAnnotation = true;
377
- const provenanceData = this.__provenanceData(response);
378
- this.__existingAnnotation.innerHTML = this.__annotationHtml(response);
379
- this.__annotationForm.innerHTML = this.__editFormHtml(provenanceData);
380
-
381
- // Lock focus to focusable elements within the panel
382
- const inputElements = panel.content.querySelectorAll('input, textarea, button');
383
- this.__firstInputField = inputElements[0];
384
- const lastInput = inputElements[inputElements.length - 1];
385
- const saveButton = document.getElementById('annotation-save-button');
386
-
387
- panel.addEventListener('keydown', function (e) {
388
- if (e.key === 'Tab') {
389
- if ( e.shiftKey ) /* shift + tab */ {
390
- if (document.activeElement === this.__firstInputField) {
391
- lastInput.focus();
392
- e.preventDefault();
393
- }
394
- } else /* tab */ {
395
- if (document.activeElement === lastInput) {
396
- this.__firstInputField.focus();
397
- e.preventDefault();
398
- }
399
- }
400
- } else if (e.key === 'Enter') {
401
- if (e.target === saveButton) {
402
- this.__saveAnnotation(panel, provenanceData);
403
- }
404
- }
405
- }.bind(this));
406
-
407
- saveButton.addEventListener('mousedown', function (e) {
408
- this.__saveAnnotation(panel, provenanceData);
409
- }.bind(this));
410
- }
411
-
412
- __panelCallback(panel)
413
- //====================
414
- {
415
- this.__annotationForm = document.getElementById('flatmap-annotation-form');
416
- // Data entry only once authorised
417
- this.__annotationForm.hidden = true;
418
-
419
- // Populate once we have content from server
420
- this.__existingAnnotation = document.getElementById('flatmap-annotation-existing');
421
- this.__statusMessage = document.getElementById('flatmap-annotation-status');
422
-
423
- this.__authoriseLock = document.getElementById('flatmap-annotation-lock');
424
- this.__authoriseLock.addEventListener('click', (e) => {
425
- const lockClasses = this.__authoriseLock.classList;
426
- if (lockClasses.contains('fa-lock')) {
427
- this.__authorise(panel).then((response) => {
428
- if ('error' in response) {
429
- this.__setStatusMessage(response.error);
430
- } else {
431
- this.__annotationForm.hidden = false;
432
- this.__firstInputField.focus();
433
- lockClasses.remove('fa-lock');
434
- lockClasses.add('fa-unlock');
435
- }
436
- });
437
- } else {
438
- this.__unauthorise().then((response) => {
439
- console.log(`Annotator logout: ${response}`);
440
- });
441
- this.__annotationForm.hidden = true;
442
- lockClasses.remove('fa-unlock');
443
- lockClasses.add('fa-lock');
444
- }
445
- });
446
- }
447
-
448
- __chooseFeatureProperties(features, callback)
449
- //===========================================
450
- {
451
- this.__ui.selectFeature(features[0].id);
452
-
453
- // Feature chooser is only for multiple selections
454
- if (features.length === 1
455
- || features[0].properties['cd-class'] !== 'celldl:Connection'
456
- || (features.length === 2
457
- && features[1].properties['cd-class'] !== 'celldl:Connection')) {
458
- callback(features[0].properties);
459
- return;
460
- }
461
- const featureList = [];
462
- const featureProperties = new Map();
463
- const featureSeen = new Set();
464
- let selected = 'selected'; // Select the first entry
465
- for (const feature of features) {
466
- if (feature.properties['cd-class'] !== 'celldl:Connection'
467
- || feature.properties['id'] == undefined
468
- || featureSeen.has(feature.properties['id'])) {
469
- continue;
470
- }
471
- const mapFeature = this.__ui.mapFeature(feature.id);
472
- const annotated = (mapFeature !== undefined)
473
- ? this.__ui._map.getFeatureState(mapFeature)['annotated']
474
- : false;
475
- let label = '';
476
- if (feature.properties.models) {
477
- label = ` -- ${feature.properties.label.split('\n')[0]} (${feature.properties.models})`;
478
- } else if (feature.properties.label) {
479
- label = ` -- ${feature.properties.label.split('\n')[0]}`;
480
- }
481
- featureList.push(`<option value="${feature.id}" ${selected}>${annotated ? '*' : '&nbsp;'} ${feature.properties.id} -- ${feature.properties.kind}${label}</option>`);
482
- featureProperties.set(+feature.id, feature.properties);
483
- featureSeen.add(feature.properties['id']);
484
- selected = '';
485
- }
486
- if (featureList.length == 0) {
487
- callback(undefined);
488
- return;
489
- } else if (featureList.length == 1) {
490
- callback(featureProperties.values().next().value);
491
- return;
492
- }
493
- const panelContent = `
494
- <div id="annotation-feature-selection">
495
- <div>
496
- <label for="annotation-feature-selector">Select feature:</label>
497
- <select id="annotation-feature-selector" size="${Math.min(featureList.length, 7)}">
498
- ${featureList.join('\n')}
499
- </select>
500
- </div>
501
- <div id="annotation-feature-buttons">
502
- <input id="annotation-feature-cancel" type="button" value="Cancel"/>
503
- <input id="annotation-feature-annotate" type="button" value="Annotate"/>
504
- </div>
505
- </div>`;
506
- this.__panel = jsPanel.create({
507
- theme: 'light',
508
- border: '2px solid #080',
509
- borderRadius: '.5rem',
510
- panelSize: 'auto auto',
511
- position: 'left-top 50 70',
512
- content: panelContent,
513
- data: features[0].properties,
514
- closeOnEscape: true,
515
- closeOnBackdrop: false,
516
- headerTitle: 'Select feature to annotate',
517
- headerControls: 'closeonly xs',
518
- callback: ((panel) => {
519
- const selector = document.getElementById('annotation-feature-selector');
520
- selector.onchange = (e) => {
521
- if (e.target.value !== '') {
522
- this.__ui.unselectFeatures();
523
- this.__ui.selectFeature(e.target.value);
524
- this.__panel.options.data = featureProperties.get(+e.target.value);
525
- }
526
- };
527
- selector.ondblclick = (e) => {
528
- if (e.target.value !== '') {
529
- const properties = this.__panel.options.data;
530
- this.__panel.close();
531
- callback(properties);
532
- }
533
- }
534
- selector.focus();
535
- document.getElementById('annotation-feature-cancel')
536
- .onclick = (e) => {
537
- this.__panel.close();
538
- callback(undefined);
539
- };
540
- document.getElementById('annotation-feature-annotate')
541
- .onclick = (e) => {
542
- const properties = this.__panel.options.data;
543
- this.__panel.close();
544
- callback(properties);
545
- };
546
- }).bind(this)
547
- });
548
- document.addEventListener('jspanelcloseduser', (e) => { callback(undefined) }, false);
549
- }
550
-
551
- annotate(features, closedCallback)
552
- //================================
553
- {
554
- // provide a list of features so dialog needs to first provide selection list
555
- // and highlight current one as user scrolls...
556
-
557
- this.__chooseFeatureProperties(features, (featureProperties) => {
558
- if (featureProperties) {
559
- this.__annotateFeature(featureProperties, closedCallback);
560
- } else {
561
- closedCallback();
562
- }
563
- });
564
- }
565
-
566
- __annotateFeature(featureProperties, callback)
567
- //============================================
568
- {
569
- this.__currentFeatureId = featureProperties['id'];
570
- if (this.__currentFeatureId === undefined) {
571
- callback();
572
- return;
573
- }
574
- const panelContent = [];
575
- panelContent.push('<div id="flatmap-annotation-panel">');
576
- panelContent.push(' <div id="flatmap-annotation-feature">');
577
- panelContent.push(...this.__featureHtml(featureProperties));
578
- panelContent.push(' </div>');
579
- panelContent.push(' <form id="flatmap-annotation-form"></form>');
580
- panelContent.push(' <div id="flatmap-annotation-existing"></div>');
581
- panelContent.push('</div>');
582
-
583
- const annotator = this; // To use in panel creation code
584
- const flatmap = this.__flatmap; // To use in panel creation code
585
- const contentFetchAbort = new AbortController();
586
- this.__panel = jsPanel.create({
587
- theme: 'light',
588
- border: '2px solid #080',
589
- borderRadius: '.5rem',
590
- panelSize: '725px auto',
591
- position: 'left-top 50 70',
592
- data: {
593
- flatmap: this.__flatmap
594
- },
595
- content: panelContent.join('\n'),
596
- closeOnEscape: true,
597
- closeOnBackdrop: false,
598
- headerTitle: 'Feature annotations',
599
- headerControls: 'closeonly xs',
600
- footerToolbar: [
601
- '<span id="flatmap-annotation-status" class="flex-auto"></span>',
602
- '<span id="flatmap-annotation-lock" class="jsPanel-ftr-btn fa fa-lock"></span>',
603
- ],
604
- contentFetch: {
605
- resource: flatmap.makeServerUrl(this.__currentFeatureId, 'annotator/'),
606
- fetchInit: {
607
- method: 'GET',
608
- mode: 'cors',
609
- headers: {
610
- "Accept": "application/json; charset=utf-8",
611
- "Cache-Control": "no-store"
612
- },
613
- signal: contentFetchAbort.signal
614
- },
615
- bodyMethod: 'json',
616
- beforeSend: (fetchConfig, panel) => {
617
- startSpinner(panel);
618
- setTimeout((panel) => {
619
- if (!annotator.__haveAnnotation) {
620
- console.log("Aborting content fetch...");
621
- contentFetchAbort.abort();
622
- stopSpinner(panel);
623
- annotator.__setStatusMessage('Cannot fetch annotation...');
624
- annotator.__authoriseLock.className = '';
625
- }
626
- }, FETCH_TIMEOUT, panel);
627
- },
628
- done: (response, panel) => {
629
- annotator.__finishPanelContent(panel, response);
630
- stopSpinner(panel);
631
- }
632
- },
633
- callback: annotator.__panelCallback.bind(annotator)
634
- });
635
-
636
- // should we warn if unsaved changes when closing??
637
- document.addEventListener('jspanelclosed', callback, false);
638
- }
639
-
640
- async annotated_features()
641
- //========================
642
- {
643
- const url = this.__flatmap.makeServerUrl('', 'annotator/');
644
- try {
645
- const response = await fetch(url, {
646
- headers: {
647
- "Accept": "application/json; charset=utf-8",
648
- "Cache-Control": "no-store"
649
- }
650
- });
651
- if (response.ok) {
652
- return response.json();
653
- } else {
654
- console.error(`Annotated features: ${response.status} ${response.statusText}`);
655
- return Promise.resolve([]);
656
- }
657
- } catch {
658
- console.error(`Fetch failed -- is annotator available at ${this.__flatmap._baseUrl} ?`);
659
- return Promise.resolve([]);
660
- }
661
- }
662
-
663
- }
664
-
665
- //==============================================================================