@abi-software/flatmap-viewer 2.3.0-a.4 → 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.4``
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.4",
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,61 +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
- return response.json();
117
- } else {
118
- callback({error: `${response.status} ${response.statusText}`});
119
- }
120
- }).then((response) => {
121
- if ('error' in response) {
122
- callback({error: response.error});
123
- } else {
124
- this.__setUser(response);
125
- this.__authorised = true;
126
- callback(response);
127
- }
128
- });
129
126
  setTimeout((panel) => {
130
127
  if (this.user === 'undefined') {
131
128
  console.log("Aborting login...");
132
129
  abortController.abort();
133
- panel.headerlogo.innerHTML = '';
130
+ stopSpinner(panel);
134
131
  this.__setStatusMessage('Unable to login...');
135
132
  }
136
133
  },
137
134
  LOGIN_TIMEOUT, panel);
138
- }
139
135
 
140
- __unauthorise()
141
- //=============
142
- {
143
- this.__clearUser();
144
- const abortController = new AbortController();
145
- const url = `${this.__flatmap._baseUrl}logout`;
146
- fetch(url, {
136
+ const url = `${this.__flatmap._baseUrl}login`;
137
+ startSpinner(panel);
138
+ const response = await fetch(url, {
147
139
  headers: { "Content-Type": "application/json; charset=utf-8" },
148
140
  signal: abortController.signal
149
- }).then((response) => {
150
- if (response.ok) {
151
- this.__authorised = false;
152
- return 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});
153
147
  } else {
154
- console.log('Annotator logout:', `${response.status} ${response.statusText}`);
148
+ this.__setUser(user_data);
149
+ this.__authorised = true;
150
+ return user_data;
155
151
  }
156
- }).then((response) => {
157
- console.log('Annotator logout:', response);
158
- });
152
+ } else {
153
+ return Promise.resolve({error: `${response.status} ${response.statusText}`});
154
+ }
155
+ }
156
+
157
+ async __unauthorise()
158
+ //===================
159
+ {
160
+ const abortController = new AbortController();
159
161
  setTimeout(() => {
160
162
  if (this.__authorised) {
161
163
  console.log("Aborting logout...");
@@ -164,6 +166,18 @@ export class Annotator
164
166
  }
165
167
  },
166
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
+ }
167
181
  }
168
182
 
169
183
  __setStatusMessage(message, timeout=STATUS_MESSAGE_TIMEOUT)
@@ -223,15 +237,15 @@ export class Annotator
223
237
  return html.join('\n');
224
238
  }
225
239
 
226
- __editFormHtml(annotation)
227
- //========================
240
+ __editFormHtml(provenanceData)
241
+ //============================
228
242
  {
229
243
  const html = [];
230
244
  html.push('<div id="flatmap-annotation-formdata">');
231
245
  for (const field of ANNOTATION_FIELDS) {
232
246
  html.push('<div class="flatmap-annotation-entry">');
233
247
  html.push(` <label for="${field.key}">${field.prompt}:</label>`);
234
- const value = field.update ? annotation[field.key] || '' : '';
248
+ const value = field.update ? provenanceData[field.key] || '' : '';
235
249
  if (field.kind === 'textbox') {
236
250
  html.push(` <textarea rows="5" cols="40" id="${field.key}" name="${field.key}">${value.trim()}</textarea>`)
237
251
  } else if (!('kind' in field) || field.kind !== 'list') {
@@ -251,30 +265,51 @@ export class Annotator
251
265
  return html.join('\n');
252
266
  }
253
267
 
254
- __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)
255
288
  //=================================
256
289
  {
257
290
  const newProperties = {};
258
291
  let propertiesChanged = false;
259
292
  for (const field of ANNOTATION_FIELDS) {
260
- const lastValue = field.update ? lastAnnotation[field.key] || '' : '';
293
+ const lastValue = field.update ? provenanceData[field.key] || '' : '';
261
294
  if (!('kind' in field) || field.kind !== 'list') {
262
295
  const inputField = document.getElementById(field.key);
263
- newProperties[field.key] = inputField.value.trim();
264
- if (!propertiesChanged && newProperties[field.key] !== lastValue.trim()) {
296
+ const newValue = inputField.value.trim();
297
+ if (newValue !== lastValue.trim()) {
298
+ newProperties[field.key] = newValue;
265
299
  propertiesChanged = true;
266
300
  }
267
301
  } else { // field.kind === 'list'
268
- newProperties[field.key] = [];
269
- const changedList = false;
302
+ const listValues = [];
270
303
  for (let n = 1; n <= field.size; n++) {
271
- const lastListValue = (n <= lastValue.length) ? lastValue[n-1].trim() : '';
272
304
  const inputField = document.getElementById(`${field.key}_${n}`);
273
- const newListValue = inputField.value.trim();
274
- newProperties[field.key].push(newListValue);
275
- if (!propertiesChanged && newListValue !== lastListValue) {
276
- propertiesChanged = true;
277
- }
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;
278
313
  }
279
314
  }
280
315
  }
@@ -284,32 +319,38 @@ export class Annotator
284
319
  }
285
320
  }
286
321
 
287
- __updateRemoteAnnotation(annotation, callback)
288
- //============================================
322
+ async __updateRemoteAnnotation(panel, annotation)
323
+ //===============================================
289
324
  {
290
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
+
291
336
  const url = this.__flatmap.addBaseUrl_(`/annotations/${this.__currentFeatureId}`);
292
- fetch(url, {
337
+ const response = await fetch(url, {
293
338
  headers: { "Content-Type": "application/json; charset=utf-8" },
294
339
  method: 'POST',
295
340
  body: JSON.stringify(annotation),
296
341
  signal: abortController.signal
297
- }).then((response) => {
298
- if (response.ok) {
299
- return response.json();
300
- } else {
301
- callback({error: `${response.status} ${response.statusText}`});
302
- }
303
- }).then((response) => {
304
- callback(response);
305
342
  });
306
- return abortController;
343
+ if (response.ok) {
344
+ return await response.json();
345
+ } else {
346
+ return Promise.resolve({error: `${response.status} ${response.statusText}`});
347
+ }
307
348
  }
308
349
 
309
- __saveAnnotation(panel, lastAnnotation)
310
- //=====================================
350
+ async __saveAnnotation(panel, provenanceData)
351
+ //===========================================
311
352
  {
312
- const changedProperties = this.__changedAnnotation(lastAnnotation);
353
+ const changedProperties = this.__changedAnnotation(provenanceData);
313
354
  if (this.__currentFeatureId !== undefined && changedProperties.changed) {
314
355
  const annotation = {
315
356
  ...changedProperties.properties,
@@ -317,26 +358,15 @@ export class Annotator
317
358
  'dct:subject': `flatmaps:${this.__flatmap.uuid}/${this.__currentFeatureId}`,
318
359
  'dct:creator': this.user
319
360
  }
320
- panel.headerlogo.innerHTML = '<span class="fa fa-spinner fa-spin ml-2"></span>';
321
- const remoteUpdate = this.__updateRemoteAnnotation(annotation,
322
- (response) => {
323
- if ('error' in response) {
324
- panel.headerlogo.innerHTML = response.error;
325
- } else {
326
- panel.headerlogo.innerHTML = '';
327
- panel.close();
328
- }
329
- });
330
- setTimeout((panel) => {
331
- if (panel.status !== 'closed') {
332
- console.log("Aborting remote update...");
333
- remoteUpdate.abort();
334
- panel.headerlogo.innerHTML = '';
335
- this.__setStatusMessage('Cannot update annotation...');
336
- }
337
- }, 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
+ }
338
369
  } else {
339
- this.__
340
370
  this.__setStatusMessage('No changes to save...');
341
371
  }
342
372
  }
@@ -345,9 +375,9 @@ export class Annotator
345
375
  //====================================
346
376
  {
347
377
  this.__haveAnnotation = true;
378
+ const provenanceData = this.__provenanceData(response);
348
379
  this.__existingAnnotation.innerHTML = this.__annotationHtml(response);
349
- const lastAnnotation = response.length ? response[0] : {};
350
- this.__annotationForm.innerHTML = this.__editFormHtml(lastAnnotation);
380
+ this.__annotationForm.innerHTML = this.__editFormHtml(provenanceData);
351
381
 
352
382
  // Lock focus to focusable elements within the panel
353
383
  const inputElements = panel.content.querySelectorAll('input, textarea, button');
@@ -370,16 +400,52 @@ export class Annotator
370
400
  }
371
401
  } else if (e.key === 'Enter') {
372
402
  if (e.target === saveButton) {
373
- this.__saveAnnotation(panel, lastAnnotation);
403
+ this.__saveAnnotation(panel, provenanceData);
374
404
  }
375
405
  }
376
406
  }.bind(this));
377
407
 
378
408
  saveButton.addEventListener('mousedown', function (e) {
379
- this.__saveAnnotation(panel, lastAnnotation);
409
+ this.__saveAnnotation(panel, provenanceData);
380
410
  }.bind(this));
381
411
  }
382
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
+
383
449
  annotate(feature, closedCallback)
384
450
  //===============================
385
451
  {
@@ -399,8 +465,8 @@ export class Annotator
399
465
  panelContent.push(' <div id="flatmap-annotation-existing"></div>');
400
466
  panelContent.push('</div>');
401
467
 
402
- const annotator = this; // To use in panel code
403
- 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
404
470
  const contentFetchAbort = new AbortController();
405
471
  this.__panel = jsPanel.create({
406
472
  theme: 'light',
@@ -433,12 +499,12 @@ export class Annotator
433
499
  },
434
500
  bodyMethod: 'json',
435
501
  beforeSend: (fetchConfig, panel) => {
436
- panel.headerlogo.innerHTML = '<span class="fa fa-spinner fa-spin ml-2"></span>';
502
+ startSpinner(panel);
437
503
  setTimeout((panel) => {
438
504
  if (!annotator.__haveAnnotation) {
439
505
  console.log("Aborting content fetch...");
440
506
  contentFetchAbort.abort();
441
- panel.headerlogo.innerHTML = '';
507
+ stopSpinner(panel);
442
508
  annotator.__setStatusMessage('Cannot fetch annotation...');
443
509
  annotator.__authoriseLock.className = '';
444
510
  }
@@ -446,45 +512,14 @@ export class Annotator
446
512
  },
447
513
  done: (response, panel) => {
448
514
  annotator.__finishPanelContent(panel, response);
449
- panel.headerlogo.innerHTML = '';
515
+ stopSpinner(panel);
450
516
  }
451
517
  },
452
- callback: (panel) => {
453
- annotator.__annotationForm = document.getElementById('flatmap-annotation-form');
454
- // Data entry only once authorised
455
- annotator.__annotationForm.hidden = true;
456
-
457
- // Populate once we have content from server
458
- annotator.__existingAnnotation = document.getElementById('flatmap-annotation-existing');
459
- annotator.__statusMessage = document.getElementById('flatmap-annotation-status');
460
-
461
- annotator.__authoriseLock = document.getElementById('flatmap-annotation-lock');
462
- annotator.__authoriseLock.addEventListener('click', (e) => {
463
- const lockClasses = annotator.__authoriseLock.classList;
464
- if (lockClasses.contains('fa-lock')) {
465
- annotator.__authorise(panel, (response) => {
466
- if ('error' in response) {
467
- annotator.__setStatusMessage(response.error);
468
- } else {
469
- annotator.__annotationForm.hidden = false;
470
- annotator.__firstInputField.focus();
471
- lockClasses.remove('fa-lock');
472
- lockClasses.add('fa-unlock');
473
- }
474
- });
475
- } else {
476
- annotator.__unauthorise();
477
- annotator.__annotationForm.hidden = true;
478
- lockClasses.remove('fa-unlock');
479
- lockClasses.add('fa-lock');
480
- }
481
- });
482
-
483
- // should we warn if unsaved changes when closing??
484
- document.addEventListener('jspanelclosed', closedCallback, false);
485
- }
518
+ callback: annotator.__panelCallback.bind(annotator)
486
519
  });
487
520
 
521
+ // should we warn if unsaved changes when closing??
522
+ document.addEventListener('jspanelclosed', closedCallback, false);
488
523
  }
489
524
 
490
525
  }