@abi-software/flatmap-viewer 2.3.0-a.4 → 2.3.0-a.6

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.6``
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.6",
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,23 +237,25 @@ 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] || '' : '';
235
248
  if (field.kind === 'textbox') {
249
+ const value = field.update ? provenanceData[field.key] || '' : '';
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') {
252
+ const value = field.update ? provenanceData[field.key] || '' : '';
238
253
  html.push(` <input type="text" size="40" id="${field.key}" name="${field.key}" value="${value.trim()}"/>`)
239
254
  } else { // field.kind === 'list'
255
+ const listValues = field.update ? provenanceData[field.key] || [] : [];
240
256
  html.push(' <div class="multiple">')
241
257
  for (let n = 1; n <= field.size; n++) {
242
- const fieldValue = (n <= value.length) ? value[n-1].trim() : '';
258
+ const fieldValue = (n <= listValues.length) ? listValues[n-1].trim() : '';
243
259
  html.push(` <input type="text" size="40" id="${field.key}_${n}" name="${field.key}" value="${fieldValue}"/>`)
244
260
  }
245
261
  html.push(' </div>')
@@ -251,30 +267,52 @@ export class Annotator
251
267
  return html.join('\n');
252
268
  }
253
269
 
254
- __changedAnnotation(lastAnnotation)
270
+ __provenanceData(annotations)
271
+ //===========================
272
+ {
273
+ const provenanceData = {};
274
+ for (const annotation of annotations) { // In order of most recent to oldest
275
+ if (annotation['rdf:type'] === 'prov:Entity') {
276
+ for (const field of ANNOTATION_FIELDS) {
277
+ if (field.update) {
278
+ const value = annotation[field.key];
279
+ if (value !== undefined && !(field.key in provenanceData)) {
280
+ provenanceData[field.key] = value;
281
+ }
282
+ }
283
+ }
284
+ }
285
+ }
286
+ return provenanceData;
287
+ }
288
+
289
+ __changedAnnotation(provenanceData)
255
290
  //=================================
256
291
  {
257
292
  const newProperties = {};
258
293
  let propertiesChanged = false;
259
294
  for (const field of ANNOTATION_FIELDS) {
260
- const lastValue = field.update ? lastAnnotation[field.key] || '' : '';
261
295
  if (!('kind' in field) || field.kind !== 'list') {
296
+ const lastValue = field.update ? provenanceData[field.key] || '' : '';
262
297
  const inputField = document.getElementById(field.key);
263
- newProperties[field.key] = inputField.value.trim();
264
- if (!propertiesChanged && newProperties[field.key] !== lastValue.trim()) {
298
+ const newValue = inputField.value.trim();
299
+ if (newValue !== lastValue.trim()) {
300
+ newProperties[field.key] = newValue;
265
301
  propertiesChanged = true;
266
302
  }
267
303
  } else { // field.kind === 'list'
268
- newProperties[field.key] = [];
269
- const changedList = false;
304
+ const listValues = [];
270
305
  for (let n = 1; n <= field.size; n++) {
271
- const lastListValue = (n <= lastValue.length) ? lastValue[n-1].trim() : '';
272
306
  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
- }
307
+ listValues.push(inputField.value.trim());
308
+ }
309
+ const lastValue = field.update ? provenanceData[field.key] || [] : [];
310
+ const oldValues = lastValue.map(v => v.trim()).filter(v => (v !== '')).sort();
311
+ const newValues = listValues.map(v => v.trim()).filter(v => (v !== '')).sort();
312
+ if (oldValues.length !== newValues.length
313
+ || oldValues.filter(v => !newValues.includes(v)).length > 0) {
314
+ newProperties[field.key] = newValues;
315
+ propertiesChanged = true;
278
316
  }
279
317
  }
280
318
  }
@@ -284,32 +322,38 @@ export class Annotator
284
322
  }
285
323
  }
286
324
 
287
- __updateRemoteAnnotation(annotation, callback)
288
- //============================================
325
+ async __updateRemoteAnnotation(panel, annotation)
326
+ //===============================================
289
327
  {
290
328
  const abortController = new AbortController();
329
+
330
+ setTimeout((panel) => {
331
+ if (panel.status !== 'closed') {
332
+ console.log("Aborting remote update...");
333
+ abortController.abort();
334
+ stopSpinner(panel);
335
+ this.__setStatusMessage('Cannot update annotation...');
336
+ }
337
+ }, UPDATE_TIMEOUT, panel);
338
+
291
339
  const url = this.__flatmap.addBaseUrl_(`/annotations/${this.__currentFeatureId}`);
292
- fetch(url, {
340
+ const response = await fetch(url, {
293
341
  headers: { "Content-Type": "application/json; charset=utf-8" },
294
342
  method: 'POST',
295
343
  body: JSON.stringify(annotation),
296
344
  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
345
  });
306
- return abortController;
346
+ if (response.ok) {
347
+ return await response.json();
348
+ } else {
349
+ return Promise.resolve({error: `${response.status} ${response.statusText}`});
350
+ }
307
351
  }
308
352
 
309
- __saveAnnotation(panel, lastAnnotation)
310
- //=====================================
353
+ async __saveAnnotation(panel, provenanceData)
354
+ //===========================================
311
355
  {
312
- const changedProperties = this.__changedAnnotation(lastAnnotation);
356
+ const changedProperties = this.__changedAnnotation(provenanceData);
313
357
  if (this.__currentFeatureId !== undefined && changedProperties.changed) {
314
358
  const annotation = {
315
359
  ...changedProperties.properties,
@@ -317,26 +361,15 @@ export class Annotator
317
361
  'dct:subject': `flatmaps:${this.__flatmap.uuid}/${this.__currentFeatureId}`,
318
362
  'dct:creator': this.user
319
363
  }
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);
364
+ startSpinner(panel);
365
+ const response = await this.__updateRemoteAnnotation(panel, annotation);
366
+ stopSpinner(panel);
367
+ if ('error' in response) {
368
+ this.__setStatusMessage(response.error);
369
+ } else {
370
+ panel.close();
371
+ }
338
372
  } else {
339
- this.__
340
373
  this.__setStatusMessage('No changes to save...');
341
374
  }
342
375
  }
@@ -345,9 +378,9 @@ export class Annotator
345
378
  //====================================
346
379
  {
347
380
  this.__haveAnnotation = true;
381
+ const provenanceData = this.__provenanceData(response);
348
382
  this.__existingAnnotation.innerHTML = this.__annotationHtml(response);
349
- const lastAnnotation = response.length ? response[0] : {};
350
- this.__annotationForm.innerHTML = this.__editFormHtml(lastAnnotation);
383
+ this.__annotationForm.innerHTML = this.__editFormHtml(provenanceData);
351
384
 
352
385
  // Lock focus to focusable elements within the panel
353
386
  const inputElements = panel.content.querySelectorAll('input, textarea, button');
@@ -370,16 +403,52 @@ export class Annotator
370
403
  }
371
404
  } else if (e.key === 'Enter') {
372
405
  if (e.target === saveButton) {
373
- this.__saveAnnotation(panel, lastAnnotation);
406
+ this.__saveAnnotation(panel, provenanceData);
374
407
  }
375
408
  }
376
409
  }.bind(this));
377
410
 
378
411
  saveButton.addEventListener('mousedown', function (e) {
379
- this.__saveAnnotation(panel, lastAnnotation);
412
+ this.__saveAnnotation(panel, provenanceData);
380
413
  }.bind(this));
381
414
  }
382
415
 
416
+ __panelCallback(panel)
417
+ //====================
418
+ {
419
+ this.__annotationForm = document.getElementById('flatmap-annotation-form');
420
+ // Data entry only once authorised
421
+ this.__annotationForm.hidden = true;
422
+
423
+ // Populate once we have content from server
424
+ this.__existingAnnotation = document.getElementById('flatmap-annotation-existing');
425
+ this.__statusMessage = document.getElementById('flatmap-annotation-status');
426
+
427
+ this.__authoriseLock = document.getElementById('flatmap-annotation-lock');
428
+ this.__authoriseLock.addEventListener('click', (e) => {
429
+ const lockClasses = this.__authoriseLock.classList;
430
+ if (lockClasses.contains('fa-lock')) {
431
+ this.__authorise(panel).then((response) => {
432
+ if ('error' in response) {
433
+ this.__setStatusMessage(response.error);
434
+ } else {
435
+ this.__annotationForm.hidden = false;
436
+ this.__firstInputField.focus();
437
+ lockClasses.remove('fa-lock');
438
+ lockClasses.add('fa-unlock');
439
+ }
440
+ });
441
+ } else {
442
+ this.__unauthorise().then((response) => {
443
+ console.log(`Annotator logout: ${response}`);
444
+ });
445
+ this.__annotationForm.hidden = true;
446
+ lockClasses.remove('fa-unlock');
447
+ lockClasses.add('fa-lock');
448
+ }
449
+ });
450
+ }
451
+
383
452
  annotate(feature, closedCallback)
384
453
  //===============================
385
454
  {
@@ -399,8 +468,8 @@ export class Annotator
399
468
  panelContent.push(' <div id="flatmap-annotation-existing"></div>');
400
469
  panelContent.push('</div>');
401
470
 
402
- const annotator = this; // To use in panel code
403
- const flatmap = this.__flatmap; // To use in panel code
471
+ const annotator = this; // To use in panel creation code
472
+ const flatmap = this.__flatmap; // To use in panel creation code
404
473
  const contentFetchAbort = new AbortController();
405
474
  this.__panel = jsPanel.create({
406
475
  theme: 'light',
@@ -433,12 +502,12 @@ export class Annotator
433
502
  },
434
503
  bodyMethod: 'json',
435
504
  beforeSend: (fetchConfig, panel) => {
436
- panel.headerlogo.innerHTML = '<span class="fa fa-spinner fa-spin ml-2"></span>';
505
+ startSpinner(panel);
437
506
  setTimeout((panel) => {
438
507
  if (!annotator.__haveAnnotation) {
439
508
  console.log("Aborting content fetch...");
440
509
  contentFetchAbort.abort();
441
- panel.headerlogo.innerHTML = '';
510
+ stopSpinner(panel);
442
511
  annotator.__setStatusMessage('Cannot fetch annotation...');
443
512
  annotator.__authoriseLock.className = '';
444
513
  }
@@ -446,45 +515,14 @@ export class Annotator
446
515
  },
447
516
  done: (response, panel) => {
448
517
  annotator.__finishPanelContent(panel, response);
449
- panel.headerlogo.innerHTML = '';
518
+ stopSpinner(panel);
450
519
  }
451
520
  },
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
- }
521
+ callback: annotator.__panelCallback.bind(annotator)
486
522
  });
487
523
 
524
+ // should we warn if unsaved changes when closing??
525
+ document.addEventListener('jspanelclosed', closedCallback, false);
488
526
  }
489
527
 
490
528
  }