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

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.4.2-b.5``
41
+ * ``npm install @abi-software/flatmap-viewer@2.4.3``
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.4.2-b.5",
3
+ "version": "2.4.3",
4
4
  "description": "Flatmap viewer using Maplibre GL",
5
5
  "repository": "https://github.com/AnatomicMaps/flatmap-viewer.git",
6
6
  "main": "src/main.js",
@@ -19,14 +19,13 @@
19
19
  "dependencies": {
20
20
  "@babel/runtime": "^7.10.4",
21
21
  "@fortawesome/fontawesome-free": "^6.4.0",
22
- "@turf/area": "^6.0.1",
23
- "@turf/bbox": "^6.0.1",
24
- "@turf/helpers": "^6.1.4",
22
+ "@turf/area": "^6.5.0",
23
+ "@turf/bbox": "^6.5.0",
24
+ "@turf/helpers": "^6.5.0",
25
25
  "@turf/projection": "^6.5.0",
26
26
  "bezier-js": "^6.1.0",
27
27
  "html-es6cape": "^2.0.2",
28
- "jspanel4": "^4.16.1",
29
- "maplibre-gl": ">=3.0.0",
28
+ "maplibre-gl": ">=3.6.0",
30
29
  "minisearch": "^2.2.1",
31
30
  "polylabel": "^1.1.0"
32
31
  },
@@ -526,73 +526,6 @@ export class NerveControl
526
526
 
527
527
  //==============================================================================
528
528
 
529
- export class AnnotatedControl
530
- {
531
- constructor(ui, options={excludeAnnotated: false})
532
- {
533
- this.__ui = ui;
534
- this.__map = undefined;
535
- this.__exclude = options.excludeAnnotated || false;
536
- }
537
-
538
- getDefaultPosition()
539
- //==================
540
- {
541
- return 'top-right';
542
- }
543
-
544
- onAdd(map)
545
- //========
546
- {
547
- this.__map = map;
548
- this.__container = document.createElement('div');
549
- this.__container.className = 'maplibregl-ctrl';
550
-
551
- this.__button = document.createElement('button');
552
- this.__button.id = 'map-annotated-button';
553
- this.__button.className = 'control-button text-button';
554
- this.__button.setAttribute('type', 'button');
555
- this.__button.setAttribute('aria-label', 'Show/hide annotated paths');
556
- this.__button.textContent = 'UNANN';
557
- this.__button.title = 'Show/hide annotated paths';
558
- this.__container.appendChild(this.__button);
559
-
560
- this.__container.addEventListener('click', this.onClick_.bind(this));
561
- this.__setBackground();
562
- return this.__container;
563
- }
564
-
565
- __setBackground()
566
- //===============
567
- {
568
- if (this.__exclude) {
569
- this.__button.setAttribute('style', 'background: red');
570
- } else {
571
- this.__button.removeAttribute('style');
572
- }
573
- }
574
-
575
- onRemove()
576
- //========
577
- {
578
- this.__container.parentNode.removeChild(this.__container);
579
- this.__map = undefined;
580
- }
581
-
582
- onClick_(event)
583
- //=============
584
- {
585
- if (event.target.id === 'map-annotated-button') {
586
- this.__exclude = !this.__exclude;
587
- this.__setBackground();
588
- this.__ui.excludeAnnotated(this.__exclude);
589
- }
590
- event.stopPropagation();
591
- }
592
- }
593
-
594
- //==============================================================================
595
-
596
529
  export class BackgroundControl
597
530
  {
598
531
  constructor(flatmap)
@@ -95,9 +95,6 @@ class FlatMap
95
95
  this.__addAnnotation(featureId, annotation);
96
96
  this.__searchIndex.indexMetadata(featureId, annotation);
97
97
  }
98
- if (this.options.annotator) {
99
- this.__addAnnotatedComments();
100
- }
101
98
 
102
99
  // Set base of source URLs in map's style
103
100
 
@@ -223,25 +220,6 @@ class FlatMap
223
220
  });
224
221
  }
225
222
 
226
- async __addAnnotatedComments()
227
- //============================
228
- {
229
- const url = this.makeServerUrl('', 'annotator/')
230
- const annotatedFeatures = await loadJSON(url);
231
- for (const annotatedId of annotatedFeatures) {
232
- const featureId = this.__annIdToFeatureId.get(annotatedId);
233
- if (featureId) {
234
- const url = this.makeServerUrl(annotatedId, 'annotator/')
235
- const annotations = await loadJSON(url);
236
- for (const annotation of annotations) { // In order of most recent to oldest
237
- if ('rdfs:comment' in annotation) {
238
- this.__searchIndex.indexText(featureId, annotation['rdfs:comment']);
239
- }
240
- }
241
- }
242
- }
243
- }
244
-
245
223
  async setupUserInteractions_()
246
224
  //============================
247
225
  {
@@ -1237,20 +1215,6 @@ class FlatMap
1237
1215
 
1238
1216
  //==========================================================================
1239
1217
 
1240
- /**
1241
- * Highlight features on the map.
1242
- *
1243
- * @param {Array.<string>} externalIds An array of anaotomical terms identifing features to highlight
1244
- */
1245
- highlightFeatures(externalIds)
1246
- //============================
1247
- {
1248
- if (this._userInteractions !== null) {
1249
- const featureIds = this.modelFeatureIdList(externalIds);
1250
- this._userInteractions.highlightFeatures(featureIds);
1251
- }
1252
- }
1253
-
1254
1218
  /**
1255
1219
  * Select features on the map.
1256
1220
  *
@@ -1435,7 +1399,6 @@ export class MapManager
1435
1399
  * @arg options.showPosition {boolean} Show ``position`` of tooltip.
1436
1400
  * @arg options.standalone {boolean} Viewer is running ``standalone``, as opposed to integrated into
1437
1401
  * another application so show a number of controls. Defaults to ``false``.
1438
- * @arg options.annotator {boolean} Allow interactive annotation of features and paths.
1439
1402
  * @example
1440
1403
  * const humanMap1 = mapManager.loadMap('humanV1', 'div-1');
1441
1404
  *
@@ -33,14 +33,13 @@ import polylabel from 'polylabel';
33
33
 
34
34
  //==============================================================================
35
35
 
36
- import {Annotator} from './annotation';
37
36
  import {LayerManager} from './layers';
38
37
  import {PATHWAYS_LAYER, PathManager} from './pathways';
39
38
  import {VECTOR_TILES_SOURCE} from './styling';
40
39
  import {SystemsManager} from './systems';
41
40
 
42
41
  import {displayedProperties, InfoControl} from './controls/info';
43
- import {AnnotatedControl, BackgroundControl, LayerControl, NerveControl,
42
+ import {BackgroundControl, LayerControl, NerveControl,
44
43
  SCKANControl} from './controls/controls';
45
44
  import {PathControl} from './controls/paths';
46
45
  import {SearchControl} from './controls/search';
@@ -152,13 +151,6 @@ export class UserInteractions
152
151
  this.enableCentrelines(this.__pathManager.enabledCentrelines, true);
153
152
  }
154
153
 
155
- // Add annotation capability
156
- if (flatmap.options.annotator) {
157
- this.__setupAnnotation();
158
- } else {
159
- this.__annotator = null;
160
- }
161
-
162
154
  // Note features that are FC systems
163
155
  this.__systemsManager = new SystemsManager(this._flatmap, this, featuresEnabled);
164
156
 
@@ -196,11 +188,6 @@ export class UserInteractions
196
188
  // Connectivity taxon control for AC maps
197
189
  this._map.addControl(new TaxonsControl(flatmap));
198
190
  }
199
-
200
- if (flatmap.options.annotator) {
201
- // Show/hide annotated paths
202
- this._map.addControl(new AnnotatedControl(this, flatmap.options.layerOptions));
203
- }
204
191
  }
205
192
 
206
193
  // Handle mouse events
@@ -256,41 +243,6 @@ export class UserInteractions
256
243
  }
257
244
  }
258
245
 
259
- async __setupAnnotation()
260
- //=======================
261
- {
262
- // Add annotation capability
263
-
264
- this.__annotator = new Annotator(this._flatmap, this);
265
- const annotated_features = await this.__annotator.annotated_features();
266
-
267
- // Flag features that have annotations
268
- this.__featureIdToMapId = new Map();
269
- for (const [mapId, ann] of this._flatmap.annotations) {
270
- this.__featureIdToMapId.set(ann.id, mapId);
271
- const feature = this.mapFeature(mapId);
272
- if (feature !== undefined) {
273
- this._map.setFeatureState(feature, { 'map-annotation': true });
274
- if (annotated_features.includes(ann.id)) {
275
- this._map.setFeatureState(feature, { 'annotated': true });
276
- }
277
- }
278
- }
279
- }
280
-
281
- setFeatureAnnotated(featureId)
282
- //============================
283
- {
284
- if (this.__annotator) {
285
- // featureId v's geoJSON id
286
- const mapId = this.__featureIdToMapId.get(featureId);
287
- const feature = this.mapFeature(mapId);
288
- if (feature !== undefined) {
289
- this._map.setFeatureState(feature, { 'annotated': true });
290
- }
291
- }
292
- }
293
-
294
246
  setPaint(options)
295
247
  //===============
296
248
  {
@@ -495,19 +447,6 @@ export class UserInteractions
495
447
  }
496
448
  }
497
449
 
498
- highlightFeature_(featureId)
499
- //==========================
500
- {
501
- featureId = +featureId; // Ensure numeric
502
- this.__activateFeature(this.mapFeature(featureId));
503
- }
504
-
505
- unhighlightFeatures_()
506
- //====================
507
- {
508
- this.resetActiveFeatures_();
509
- }
510
-
511
450
  smallestAnnotatedPolygonFeature_(features)
512
451
  //========================================
513
452
  {
@@ -556,30 +495,6 @@ export class UserInteractions
556
495
  this.unselectFeatures();
557
496
  }
558
497
 
559
- /**
560
- * Highlight features on the map.
561
- *
562
- * @param {Array.<string>} featureIds An array of feature identifiers to highlight
563
- */
564
- highlightFeatures(featureIds)
565
- //===========================
566
- {
567
- if (featureIds.length) {
568
- this.unhighlightFeatures_();
569
- for (const featureId of featureIds) {
570
- const annotation = this._flatmap.annotation(featureId);
571
- if (annotation) {
572
- this.highlightFeature_(featureId);
573
- if ('type' in annotation && annotation.type.startsWith('line')) {
574
- for (const pathFeatureId of this.__pathManager.lineFeatureIds([featureId])) {
575
- this.highlightFeature_(pathFeatureId);
576
- }
577
- }
578
- }
579
- }
580
- }
581
- }
582
-
583
498
  /**
584
499
  * Select features on the map.
585
500
  *
@@ -1018,28 +933,6 @@ export class UserInteractions
1018
933
  }
1019
934
  }
1020
935
 
1021
- __annotationEvent(features)
1022
- //=========================
1023
- {
1024
- if (!this.__annotator) {
1025
- return;
1026
- }
1027
-
1028
- event.preventDefault();
1029
-
1030
- // Remove any tooltip
1031
- this.removeTooltip_();
1032
-
1033
- // Don't respond to mouse events while the dialog is open
1034
- this.setModal_();
1035
-
1036
- // The annotation dialog...
1037
- this.__annotator.annotate(features, () => {
1038
- this.unselectFeatures();
1039
- this.__clearModal();
1040
- });
1041
- }
1042
-
1043
936
  clickEvent_(event)
1044
937
  //================
1045
938
  {
@@ -1054,14 +947,8 @@ export class UserInteractions
1054
947
  this.unselectFeatures();
1055
948
  return;
1056
949
  }
1057
- const originalEvent = event.originalEvent;
1058
- if (originalEvent.altKey) {
1059
- this.__annotationEvent(clickedFeatures);
1060
- return;
1061
- }
1062
-
1063
950
  const clickedFeature = clickedFeatures[0];
1064
- this.selectionEvent_(originalEvent, clickedFeature);
951
+ this.selectionEvent_(event.originalEvent, clickedFeature);
1065
952
  if (this._modal) {
1066
953
  // Remove tooltip, reset active features, etc
1067
954
  this.__resetFeatureDisplay();
package/src/main.js CHANGED
@@ -62,7 +62,6 @@ export async function standaloneViewer(map_endpoint=null, options={})
62
62
  showId: true,
63
63
  showPosition: false,
64
64
  standalone: true,
65
- annotator: true,
66
65
  }, options);
67
66
 
68
67
  function loadMap(id, taxon, sex)
package/src/styling.js CHANGED
@@ -52,7 +52,7 @@ const STROKE_INTERPOLATION = [
52
52
  ['zoom'],
53
53
  2, ["*", ['var', 'width'], ["^", 2, -0.5]],
54
54
  7, ["*", ['var', 'width'], ["^", 2, 2.5]],
55
- 9, ["*", ['var', 'width'], ["^", 2, 4.0]]
55
+ 9, ["*", ['var', 'width'], ["^", 2, 3.0]]
56
56
  ];
57
57
 
58
58
  //==============================================================================
@@ -162,7 +162,7 @@ export class FeatureFillLayer extends VectorStyleLayer
162
162
  'fill-opacity': [
163
163
  'case',
164
164
  ['boolean', ['feature-state', 'hidden'], false], 0.1,
165
- ['boolean', ['feature-state', 'selected'], false], 0.5,
165
+ ['boolean', ['feature-state', 'selected'], false], 0.2,
166
166
  ['has', 'opacity'], ['get', 'opacity'],
167
167
  ['has', 'colour'], 1.0,
168
168
  ['boolean', ['feature-state', 'active'], false], 0.7,
@@ -206,53 +206,37 @@ export class FeatureBorderLayer extends VectorStyleLayer
206
206
  const outlined = !('outline' in options) || options.outline;
207
207
  const dimmed = 'dimmed' in options && options.dimmed;
208
208
  const activeRasterLayer = 'activeRasterLayer' in options && options.activeRasterLayer;
209
- const lineColour = [ 'case' ];
210
- lineColour.push(['boolean', ['feature-state', 'hidden'], false]);
211
- lineColour.push(COLOUR_HIDDEN);
212
- lineColour.push(['boolean', ['feature-state', 'selected'], false]);
213
- lineColour.push(FEATURE_SELECTED_BORDER);
209
+ const lineColour = ['case'];
210
+ lineColour.push(['boolean', ['feature-state', 'hidden'], false], COLOUR_HIDDEN);
211
+ lineColour.push(['boolean', ['feature-state', 'selected'], false], FEATURE_SELECTED_BORDER);
214
212
  if (coloured && outlined) {
215
- lineColour.push(['boolean', ['feature-state', 'active'], false]);
216
- lineColour.push(COLOUR_ACTIVE);
213
+ lineColour.push(['boolean', ['feature-state', 'active'], false], COLOUR_ACTIVE);
217
214
  }
218
- lineColour.push(['boolean', ['feature-state', 'annotated'], false]);
219
- lineColour.push(COLOUR_ANNOTATED);
220
- lineColour.push(['has', 'colour']);
221
- lineColour.push(['get', 'colour']);
215
+ lineColour.push(['boolean', ['feature-state', 'annotated'], false], COLOUR_ANNOTATED);
216
+ lineColour.push(['has', 'colour'], ['get', 'colour']);
222
217
  lineColour.push('#444');
223
218
 
224
- const lineOpacity = [
225
- 'case',
226
- ['boolean', ['feature-state', 'hidden'], false], 0.05,
227
- ];
219
+ const lineOpacity = ['case'];
220
+ lineOpacity.push(['boolean', ['feature-state', 'hidden'], false], 0.05);
228
221
  if (coloured && outlined) {
229
- lineOpacity.push(['boolean', ['feature-state', 'active'], false]);
230
- lineOpacity.push(0.9);
222
+ lineOpacity.push(['boolean', ['feature-state', 'active'], false], 0.9);
231
223
  }
232
- lineOpacity.push(['boolean', ['feature-state', 'selected'], false]);
233
- lineOpacity.push(0.9);
234
- lineOpacity.push(['boolean', ['feature-state', 'annotated'], false]);
235
- lineOpacity.push(0.9);
224
+ lineOpacity.push(['boolean', ['feature-state', 'selected'], false], 0.9);
225
+ lineOpacity.push(['boolean', ['feature-state', 'annotated'], false], 0.9);
236
226
  if (activeRasterLayer) {
237
227
  lineOpacity.push((outlined && !dimmed) ? 0.3 : 0.1);
238
228
  } else {
239
229
  lineOpacity.push(0.5);
240
230
  }
241
231
 
242
- const lineWidth = [
243
- 'case',
244
- ['boolean', ['get', 'invisible'], false], 0.2,
245
- ];
246
- lineWidth.push(['boolean', ['feature-state', 'selected'], false]);
247
- lineWidth.push(2.5);
232
+ const lineWidth = ['case'];
233
+ lineWidth.push(['boolean', ['get', 'invisible'], false], 0.2);
234
+ lineWidth.push(['boolean', ['feature-state', 'selected'], false], 1.5);
248
235
  if (coloured && outlined) {
249
- lineWidth.push(['boolean', ['feature-state', 'active'], false]);
250
- lineWidth.push(1.5);
236
+ lineWidth.push(['boolean', ['feature-state', 'active'], false], 1.5);
251
237
  }
252
- lineWidth.push(['boolean', ['feature-state', 'annotated'], false]);
253
- lineWidth.push(3.5);
254
- lineWidth.push(['has', 'colour']);
255
- lineWidth.push(0.7);
238
+ lineWidth.push(['boolean', ['feature-state', 'annotated'], false], 3.5);
239
+ lineWidth.push(['has', 'colour'], 0.7);
256
240
  lineWidth.push((coloured && outlined) ? 0.5 : 0.1);
257
241
 
258
242
  return super.changedPaintStyle({
@@ -293,14 +277,14 @@ export class FeatureLineLayer extends VectorStyleLayer
293
277
  return this.__dashed ? [
294
278
  'all',
295
279
  ['==', '$type', 'LineString'],
296
- ['==', 'type', `line-dash`]
280
+ ['==', 'type', 'line-dash']
297
281
  ] : [
298
282
  'all',
299
283
  ['==', '$type', 'LineString'],
300
284
  [
301
285
  'any',
302
286
  ['==', 'type', 'bezier'],
303
- ['==', 'type', `line`]
287
+ ['==', 'type', 'line']
304
288
  ]
305
289
  ];
306
290
  }
@@ -328,12 +312,17 @@ export class FeatureLineLayer extends VectorStyleLayer
328
312
  ],
329
313
  'line-width': [
330
314
  'let',
331
- 'width', [
332
- 'case',
333
- ['==', ['get', 'type'], 'network'], 1.2,
334
- ['boolean', ['feature-state', 'selected'], false], 1.2,
335
- ['boolean', ['feature-state', 'active'], false], 1.2,
336
- options.authoring ? 0.7 : 0.5
315
+ 'width', [
316
+ '*',
317
+ ['case',
318
+ ['has', 'stroke-width'], ['get', 'stroke-width'],
319
+ 1.0
320
+ ],
321
+ ['case',
322
+ ['boolean', ['feature-state', 'selected'], false], 1.2,
323
+ ['boolean', ['feature-state', 'active'], false], 1.2,
324
+ options.authoring ? 0.7 : 0.5
325
+ ]
337
326
  ],
338
327
  STROKE_INTERPOLATION
339
328
  ]
@@ -526,10 +515,6 @@ export class PathLineLayer extends VectorStyleLayer
526
515
  'line-color': [
527
516
  'let', 'active', ['to-number', ['feature-state', 'active'], 0],
528
517
  [ 'case',
529
- ['all',
530
- ['==', ['var', 'active'], 0],
531
- ['boolean', ['feature-state', 'selected'], false],
532
- ], COLOUR_SELECTED,
533
518
  ['boolean', ['feature-state', 'hidden'], false], COLOUR_HIDDEN,
534
519
  ['==', ['get', 'type'], 'bezier'], 'red',
535
520
  ...PATH_STYLE_RULES,
@@ -559,7 +544,9 @@ export class PathLineLayer extends VectorStyleLayer
559
544
  "*",
560
545
  this.__highlight ? ['case',
561
546
  ['boolean', ['get', 'invisible'], false], 0.1,
562
- ['boolean', ['feature-state', 'selected'], false], 0.6,
547
+ ['boolean', ['feature-state', 'selected'], false], [
548
+ 'case', ['boolean', ['feature-state', 'active'], false], 1.2,
549
+ 0.9],
563
550
  ['boolean', ['feature-state', 'active'], false], 0.9,
564
551
  0.0
565
552
  ] : [
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
- //==============================================================================