@abi-software/flatmap-viewer 2.3.0-a.3 → 2.3.0-a.5

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/README.rst CHANGED
@@ -38,7 +38,7 @@ The map server endpoint is specified as ``MAP_ENDPOINT`` in ``src/main.js``. It
38
38
  Package Installation
39
39
  ====================
40
40
 
41
- * ``npm install @abi-software/flatmap-viewer@2.3.0-a.3``
41
+ * ``npm install @abi-software/flatmap-viewer@2.3.0-a.5``
42
42
 
43
43
  Documentation
44
44
  -------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abi-software/flatmap-viewer",
3
- "version": "2.3.0-a.3",
3
+ "version": "2.3.0-a.5",
4
4
  "description": "Flatmap viewer using Maplibre GL",
5
5
  "repository": "https://github.com/AnatomicMaps/flatmap-viewer.git",
6
6
  "main": "src/main.js",
@@ -41,6 +41,7 @@
41
41
  "css-loader": "^6.5.1",
42
42
  "eslint": "^8.7.0",
43
43
  "express": "^4.17.1",
44
+ "file-loader": "^6.2.0",
44
45
  "html-webpack-plugin": "^4.5.2",
45
46
  "strip-ansi": "^7.0.1",
46
47
  "style-loader": "^1.0.0",
package/src/annotation.js CHANGED
@@ -31,9 +31,9 @@ import 'jspanel4/dist/jspanel.css';
31
31
  //==============================================================================
32
32
 
33
33
  const FETCH_TIMEOUT = 3000; // 3 seconds
34
- const UPDATE_TIMEOUT = 5000; // 5 seconds
34
+ const UPDATE_TIMEOUT = 3000; // 5 seconds
35
35
  const LOGIN_TIMEOUT = 30000; // 30 seconds
36
- const LOGOUT_TIMEOUT = 5000; // 5 seconds
36
+ const LOGOUT_TIMEOUT = 3000; // 5 seconds
37
37
 
38
38
  const STATUS_MESSAGE_TIMEOUT = 3000;
39
39
 
@@ -68,6 +68,18 @@ const ANNOTATION_FIELDS = [
68
68
 
69
69
  //==============================================================================
70
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
+
71
83
  export class Annotator
72
84
  {
73
85
  constructor(flatmap)
@@ -101,58 +113,51 @@ export class Annotator
101
113
  this.__setStatusMessage('', 0);
102
114
  }
103
115
 
104
- __authorise(panel, callback)
105
- //==========================
116
+ async __authorise(panel)
117
+ //======================
106
118
  {
119
+ /*
120
+ const testUser = {name: 'Testing...'};
121
+ this.__setUser(testUser);
122
+ callback(testUser);
123
+
124
+ */
107
125
  const abortController = new AbortController();
108
- const url = `${this.__flatmap._baseUrl}login`;
109
- panel.headerlogo.innerHTML = '<span class="fa fa-spinner fa-spin ml-2"></span>';
110
- fetch(url, {
111
- headers: { "Content-Type": "application/json; charset=utf-8" },
112
- signal: abortController.signal
113
- }).then((response) => {
114
- panel.headerlogo.innerHTML = '';
115
- if (response.ok) {
116
- const creator = response.json();
117
- if ('error' in creator) {
118
- callback({error: creator.error});
119
- } else {
120
- this.__setUser(creator);
121
- this.__authorised = true;
122
- callback(creator);
123
- }
124
- } else {
125
- callback({error: `${response.status} ${response.statusText}`});
126
- }
127
- });
128
126
  setTimeout((panel) => {
129
127
  if (this.user === 'undefined') {
130
128
  console.log("Aborting login...");
131
129
  abortController.abort();
132
- panel.headerlogo.innerHTML = '';
130
+ stopSpinner(panel);
133
131
  this.__setStatusMessage('Unable to login...');
134
132
  }
135
133
  },
136
134
  LOGIN_TIMEOUT, panel);
137
- }
138
135
 
139
- __unauthorise()
140
- //=============
141
- {
142
- this.__clearUser();
143
- const abortController = new AbortController();
144
- const url = `${this.__flatmap._baseUrl}logout`;
145
- fetch(url, {
136
+ const url = `${this.__flatmap._baseUrl}login`;
137
+ startSpinner(panel);
138
+ const response = await fetch(url, {
146
139
  headers: { "Content-Type": "application/json; charset=utf-8" },
147
140
  signal: abortController.signal
148
- }).then((response) => {
149
- if (response.ok) {
150
- this.__authorised = false;
151
- console.log('Annotator logout:', response.json());
141
+ });
142
+ stopSpinner(panel);
143
+ if (response.ok) {
144
+ const user_data = await response.json();
145
+ if ('error' in user_data) {
146
+ return Promise.resolve({error: response.error});
152
147
  } else {
153
- console.log('Annotator logout:', `${response.status} ${response.statusText}`);
148
+ this.__setUser(user_data);
149
+ this.__authorised = true;
150
+ return user_data;
154
151
  }
155
- });
152
+ } else {
153
+ return Promise.resolve({error: `${response.status} ${response.statusText}`});
154
+ }
155
+ }
156
+
157
+ async __unauthorise()
158
+ //===================
159
+ {
160
+ const abortController = new AbortController();
156
161
  setTimeout(() => {
157
162
  if (this.__authorised) {
158
163
  console.log("Aborting logout...");
@@ -161,6 +166,18 @@ export class Annotator
161
166
  }
162
167
  },
163
168
  LOGOUT_TIMEOUT);
169
+
170
+ const url = `${this.__flatmap._baseUrl}logout`;
171
+ const response = fetch(url, {
172
+ headers: { "Content-Type": "application/json; charset=utf-8" },
173
+ signal: abortController.signal
174
+ });
175
+ if (response.ok) {
176
+ this.__authorised = false;
177
+ return await response.json();
178
+ } else {
179
+ return Promise.resolve({error: `${response.status} ${response.statusText}`});
180
+ }
164
181
  }
165
182
 
166
183
  __setStatusMessage(message, timeout=STATUS_MESSAGE_TIMEOUT)
@@ -220,15 +237,15 @@ export class Annotator
220
237
  return html.join('\n');
221
238
  }
222
239
 
223
- __editFormHtml(annotation)
224
- //========================
240
+ __editFormHtml(provenanceData)
241
+ //============================
225
242
  {
226
243
  const html = [];
227
244
  html.push('<div id="flatmap-annotation-formdata">');
228
245
  for (const field of ANNOTATION_FIELDS) {
229
246
  html.push('<div class="flatmap-annotation-entry">');
230
247
  html.push(` <label for="${field.key}">${field.prompt}:</label>`);
231
- const value = field.update ? annotation[field.key] || '' : '';
248
+ const value = field.update ? provenanceData[field.key] || '' : '';
232
249
  if (field.kind === 'textbox') {
233
250
  html.push(` <textarea rows="5" cols="40" id="${field.key}" name="${field.key}">${value.trim()}</textarea>`)
234
251
  } else if (!('kind' in field) || field.kind !== 'list') {
@@ -248,30 +265,51 @@ export class Annotator
248
265
  return html.join('\n');
249
266
  }
250
267
 
251
- __changedAnnotation(lastAnnotation)
268
+ __provenanceData(annotations)
269
+ //===========================
270
+ {
271
+ const provenanceData = {};
272
+ for (const annotation of annotations) { // In order of most recent to oldest
273
+ if (annotation['rdf:type'] === 'prov:Entity') {
274
+ for (const field of ANNOTATION_FIELDS) {
275
+ if (field.update) {
276
+ const value = annotation[field.key];
277
+ if (value !== undefined && !(field.key in provenanceData)) {
278
+ provenanceData[field.key] = value;
279
+ }
280
+ }
281
+ }
282
+ }
283
+ }
284
+ return provenanceData;
285
+ }
286
+
287
+ __changedAnnotation(provenanceData)
252
288
  //=================================
253
289
  {
254
290
  const newProperties = {};
255
291
  let propertiesChanged = false;
256
292
  for (const field of ANNOTATION_FIELDS) {
257
- const lastValue = field.update ? lastAnnotation[field.key] || '' : '';
293
+ const lastValue = field.update ? provenanceData[field.key] || '' : '';
258
294
  if (!('kind' in field) || field.kind !== 'list') {
259
295
  const inputField = document.getElementById(field.key);
260
- newProperties[field.key] = inputField.value.trim();
261
- if (!propertiesChanged && newProperties[field.key] !== lastValue.trim()) {
296
+ const newValue = inputField.value.trim();
297
+ if (newValue !== lastValue.trim()) {
298
+ newProperties[field.key] = newValue;
262
299
  propertiesChanged = true;
263
300
  }
264
301
  } else { // field.kind === 'list'
265
- newProperties[field.key] = [];
266
- const changedList = false;
302
+ const listValues = [];
267
303
  for (let n = 1; n <= field.size; n++) {
268
- const lastListValue = (n <= lastValue.length) ? lastValue[n-1].trim() : '';
269
304
  const inputField = document.getElementById(`${field.key}_${n}`);
270
- const newListValue = inputField.value.trim();
271
- newProperties[field.key].push(newListValue);
272
- if (!propertiesChanged && newListValue !== lastListValue) {
273
- propertiesChanged = true;
274
- }
305
+ listValues.push(inputField.value.trim());
306
+ }
307
+ const oldValues = lastValue.map(v => v.trim()).filter(v => (v !== '')).sort();
308
+ const newValues = listValues.map(v => v.trim()).filter(v => (v !== '')).sort();
309
+ if (oldValues.length !== newValues.length
310
+ || oldValues.filter(v => !newValues.includes(v)).length > 0) {
311
+ newProperties[field.key] = newValues;
312
+ propertiesChanged = true;
275
313
  }
276
314
  }
277
315
  }
@@ -281,30 +319,38 @@ export class Annotator
281
319
  }
282
320
  }
283
321
 
284
- __updateRemoteAnnotation(annotation, callback)
285
- //============================================
322
+ async __updateRemoteAnnotation(panel, annotation)
323
+ //===============================================
286
324
  {
287
325
  const abortController = new AbortController();
326
+
327
+ setTimeout((panel) => {
328
+ if (panel.status !== 'closed') {
329
+ console.log("Aborting remote update...");
330
+ abortController.abort();
331
+ stopSpinner(panel);
332
+ this.__setStatusMessage('Cannot update annotation...');
333
+ }
334
+ }, UPDATE_TIMEOUT, panel);
335
+
288
336
  const url = this.__flatmap.addBaseUrl_(`/annotations/${this.__currentFeatureId}`);
289
- fetch(url, {
337
+ const response = await fetch(url, {
290
338
  headers: { "Content-Type": "application/json; charset=utf-8" },
291
339
  method: 'POST',
292
340
  body: JSON.stringify(annotation),
293
341
  signal: abortController.signal
294
- }).then((response) => {
295
- if (response.ok) {
296
- callback(response.json());
297
- } else {
298
- callback({error: `${response.status} ${response.statusText}`});
299
- }
300
342
  });
301
- return abortController;
343
+ if (response.ok) {
344
+ return await response.json();
345
+ } else {
346
+ return Promise.resolve({error: `${response.status} ${response.statusText}`});
347
+ }
302
348
  }
303
349
 
304
- __saveAnnotation(panel, lastAnnotation)
305
- //=====================================
350
+ async __saveAnnotation(panel, provenanceData)
351
+ //===========================================
306
352
  {
307
- const changedProperties = this.__changedAnnotation(lastAnnotation);
353
+ const changedProperties = this.__changedAnnotation(provenanceData);
308
354
  if (this.__currentFeatureId !== undefined && changedProperties.changed) {
309
355
  const annotation = {
310
356
  ...changedProperties.properties,
@@ -312,26 +358,15 @@ export class Annotator
312
358
  'dct:subject': `flatmaps:${this.__flatmap.uuid}/${this.__currentFeatureId}`,
313
359
  'dct:creator': this.user
314
360
  }
315
- panel.headerlogo.innerHTML = '<span class="fa fa-spinner fa-spin ml-2"></span>';
316
- const remoteUpdate = this.__updateRemoteAnnotation(annotation,
317
- (response) => {
318
- if ('error' in response) {
319
- panel.headerlogo.innerHTML = response.error;
320
- } else {
321
- panel.headerlogo.innerHTML = '';
322
- panel.close();
323
- }
324
- });
325
- setTimeout((panel) => {
326
- if (panel.status !== 'closed') {
327
- console.log("Aborting remote update...");
328
- remoteUpdate.abort();
329
- panel.headerlogo.innerHTML = '';
330
- this.__setStatusMessage('Cannot update annotation...');
331
- }
332
- }, UPDATE_TIMEOUT, panel);
361
+ startSpinner(panel);
362
+ const response = await this.__updateRemoteAnnotation(panel, annotation);
363
+ stopSpinner(panel);
364
+ if ('error' in response) {
365
+ this.__setStatusMessage(response.error);
366
+ } else {
367
+ panel.close();
368
+ }
333
369
  } else {
334
- this.__
335
370
  this.__setStatusMessage('No changes to save...');
336
371
  }
337
372
  }
@@ -340,9 +375,9 @@ export class Annotator
340
375
  //====================================
341
376
  {
342
377
  this.__haveAnnotation = true;
378
+ const provenanceData = this.__provenanceData(response);
343
379
  this.__existingAnnotation.innerHTML = this.__annotationHtml(response);
344
- const lastAnnotation = response.length ? response[0] : {};
345
- this.__annotationForm.innerHTML = this.__editFormHtml(lastAnnotation);
380
+ this.__annotationForm.innerHTML = this.__editFormHtml(provenanceData);
346
381
 
347
382
  // Lock focus to focusable elements within the panel
348
383
  const inputElements = panel.content.querySelectorAll('input, textarea, button');
@@ -365,16 +400,52 @@ export class Annotator
365
400
  }
366
401
  } else if (e.key === 'Enter') {
367
402
  if (e.target === saveButton) {
368
- this.__saveAnnotation(panel, lastAnnotation);
403
+ this.__saveAnnotation(panel, provenanceData);
369
404
  }
370
405
  }
371
406
  }.bind(this));
372
407
 
373
408
  saveButton.addEventListener('mousedown', function (e) {
374
- this.__saveAnnotation(panel, lastAnnotation);
409
+ this.__saveAnnotation(panel, provenanceData);
375
410
  }.bind(this));
376
411
  }
377
412
 
413
+ __panelCallback(panel)
414
+ //====================
415
+ {
416
+ this.__annotationForm = document.getElementById('flatmap-annotation-form');
417
+ // Data entry only once authorised
418
+ this.__annotationForm.hidden = true;
419
+
420
+ // Populate once we have content from server
421
+ this.__existingAnnotation = document.getElementById('flatmap-annotation-existing');
422
+ this.__statusMessage = document.getElementById('flatmap-annotation-status');
423
+
424
+ this.__authoriseLock = document.getElementById('flatmap-annotation-lock');
425
+ this.__authoriseLock.addEventListener('click', (e) => {
426
+ const lockClasses = this.__authoriseLock.classList;
427
+ if (lockClasses.contains('fa-lock')) {
428
+ this.__authorise(panel).then((response) => {
429
+ if ('error' in response) {
430
+ this.__setStatusMessage(response.error);
431
+ } else {
432
+ this.__annotationForm.hidden = false;
433
+ this.__firstInputField.focus();
434
+ lockClasses.remove('fa-lock');
435
+ lockClasses.add('fa-unlock');
436
+ }
437
+ });
438
+ } else {
439
+ this.__unauthorise().then((response) => {
440
+ console.log(`Annotator logout: ${response}`);
441
+ });
442
+ this.__annotationForm.hidden = true;
443
+ lockClasses.remove('fa-unlock');
444
+ lockClasses.add('fa-lock');
445
+ }
446
+ });
447
+ }
448
+
378
449
  annotate(feature, closedCallback)
379
450
  //===============================
380
451
  {
@@ -394,8 +465,8 @@ export class Annotator
394
465
  panelContent.push(' <div id="flatmap-annotation-existing"></div>');
395
466
  panelContent.push('</div>');
396
467
 
397
- const annotator = this; // To use in panel code
398
- const flatmap = this.__flatmap; // To use in panel code
468
+ const annotator = this; // To use in panel creation code
469
+ const flatmap = this.__flatmap; // To use in panel creation code
399
470
  const contentFetchAbort = new AbortController();
400
471
  this.__panel = jsPanel.create({
401
472
  theme: 'light',
@@ -428,12 +499,12 @@ export class Annotator
428
499
  },
429
500
  bodyMethod: 'json',
430
501
  beforeSend: (fetchConfig, panel) => {
431
- panel.headerlogo.innerHTML = '<span class="fa fa-spinner fa-spin ml-2"></span>';
502
+ startSpinner(panel);
432
503
  setTimeout((panel) => {
433
504
  if (!annotator.__haveAnnotation) {
434
505
  console.log("Aborting content fetch...");
435
506
  contentFetchAbort.abort();
436
- panel.headerlogo.innerHTML = '';
507
+ stopSpinner(panel);
437
508
  annotator.__setStatusMessage('Cannot fetch annotation...');
438
509
  annotator.__authoriseLock.className = '';
439
510
  }
@@ -441,45 +512,14 @@ export class Annotator
441
512
  },
442
513
  done: (response, panel) => {
443
514
  annotator.__finishPanelContent(panel, response);
444
- panel.headerlogo.innerHTML = '';
515
+ stopSpinner(panel);
445
516
  }
446
517
  },
447
- callback: (panel) => {
448
- annotator.__annotationForm = document.getElementById('flatmap-annotation-form');
449
- // Data entry only once authorised
450
- annotator.__annotationForm.hidden = true;
451
-
452
- // Populate once we have content from server
453
- annotator.__existingAnnotation = document.getElementById('flatmap-annotation-existing');
454
- annotator.__statusMessage = document.getElementById('flatmap-annotation-status');
455
-
456
- annotator.__authoriseLock = document.getElementById('flatmap-annotation-lock');
457
- annotator.__authoriseLock.addEventListener('click', (e) => {
458
- const lockClasses = annotator.__authoriseLock.classList;
459
- if (lockClasses.contains('fa-lock')) {
460
- annotator.__authorise(panel, (response) => {
461
- if ('error' in response) {
462
- annotator.__setStatusMessage(response.error);
463
- } else {
464
- annotator.__annotationForm.hidden = false;
465
- annotator.__firstInputField.focus();
466
- lockClasses.remove('fa-lock');
467
- lockClasses.add('fa-unlock');
468
- }
469
- });
470
- } else {
471
- annotator.__unauthorise();
472
- annotator.__annotationForm.hidden = true;
473
- lockClasses.remove('fa-unlock');
474
- lockClasses.add('fa-lock');
475
- }
476
- });
477
-
478
- // should we warn if unsaved changes when closing??
479
- document.addEventListener('jspanelclosed', closedCallback, false);
480
- }
518
+ callback: annotator.__panelCallback.bind(annotator)
481
519
  });
482
520
 
521
+ // should we warn if unsaved changes when closing??
522
+ document.addEventListener('jspanelclosed', closedCallback, false);
483
523
  }
484
524
 
485
525
  }