@abi-software/flatmap-viewer 2.5.0-a.2 → 2.5.2

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.5.0-a.2``
41
+ * ``npm install @abi-software/flatmap-viewer@2.5.2``
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.5.0-a.2",
3
+ "version": "2.5.2",
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,10 +19,9 @@
19
19
  "dependencies": {
20
20
  "@babel/runtime": "^7.10.4",
21
21
  "@deck.gl/core": "^8.9.33",
22
- "@deck.gl/extensions": "^8.9.34",
23
22
  "@deck.gl/layers": "^8.9.33",
24
23
  "@deck.gl/mapbox": "^8.9.33",
25
- "@fortawesome/fontawesome-free": "^6.4.0",
24
+ "@mapbox/mapbox-gl-draw": "^1.4.3",
26
25
  "@turf/area": "^6.5.0",
27
26
  "@turf/bbox": "^6.5.0",
28
27
  "@turf/helpers": "^6.5.0",
@@ -0,0 +1,246 @@
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
+ /*
22
+ * Annotation drawing mode is enabled/disabled by:
23
+ *
24
+ * 1. A call to ``Flatmap.enableAnnotation()``
25
+ * 2. An on-map control button calls this when in standalone viewing mode.
26
+ *
27
+ * Drawn features include a GeoJSON geometry. Existing geometries of annotated
28
+ * features are added to the MapboxDraw control when the map is loaded. These
29
+ * should only be visible on the map when the draw control is active.
30
+ *
31
+ * We listen for drawn features being created, updated and deleted, and notify
32
+ * the external annotator, first assigning new features and ID wrt the flatmap.
33
+ * The external annotator may reject a new feature (the user's cancelled the
34
+ * resulting dialog) which results in the newly drawn feature being removed from
35
+ * the control.
36
+ *
37
+ * The external annotator is responsible for saving/obtaining drawn geometries
38
+ * from an annotation service.
39
+ *
40
+ */
41
+
42
+ //==============================================================================
43
+
44
+ import MapboxDraw from "@mapbox/mapbox-gl-draw"
45
+ import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'
46
+
47
+
48
+ //==============================================================================
49
+
50
+ const drawStyleIds = MapboxDraw.lib.theme.map(s => s.id)
51
+
52
+ export const DRAW_ANNOTATION_LAYERS = [...drawStyleIds.map(id => `${id}.cold`),
53
+ ...drawStyleIds.map(id => `${id}.hot`)]
54
+
55
+ //==============================================================================
56
+
57
+ export class AnnotationDrawControl
58
+ {
59
+ constructor(flatmap, visible=false)
60
+ {
61
+ MapboxDraw.constants.classes.CONTROL_BASE = 'maplibregl-ctrl'
62
+ MapboxDraw.constants.classes.CONTROL_PREFIX = 'maplibregl-ctrl-'
63
+ MapboxDraw.constants.classes.CONTROL_GROUP = 'maplibregl-ctrl-group'
64
+
65
+ this.__flatmap = flatmap
66
+ this.__committedFeatures = new Map()
67
+ this.__uncommittedFeatureIds = new Set()
68
+ this.__visible = visible
69
+ this.__draw = new MapboxDraw({
70
+ displayControlsDefault: false,
71
+ controls: {
72
+ point: true,
73
+ line_string: true,
74
+ polygon: true,
75
+ trash: true
76
+ },
77
+ userProperties: true,
78
+ keybindings: true
79
+ })
80
+ this.__map = null
81
+ }
82
+
83
+ onAdd(map)
84
+ //========
85
+ {
86
+ this.__map = map
87
+ this.__container = this.__draw.onAdd(map)
88
+
89
+ // Fix to allow deletion with Del Key when default trash icon is not shown.
90
+ // See https://github.com/mapbox/mapbox-gl-draw/issues/989
91
+ this.__draw.options.controls.trash = true
92
+
93
+ // Prevent firefox menu from appearing on Alt key up
94
+ window.addEventListener('keyup', function (e) {
95
+ if (e.key === "Alt") {
96
+ e.preventDefault();
97
+ }
98
+ }, false)
99
+ map.on('draw.create', this.createdFeature.bind(this))
100
+ map.on('draw.delete', this.deletedFeature.bind(this))
101
+ map.on('draw.update', this.updatedFeature.bind(this))
102
+ this.show(this.__visible)
103
+ return this.__container
104
+ }
105
+
106
+ onRemove()
107
+ //========
108
+ {
109
+ this.__container.parentNode.removeChild(this.__container)
110
+ this.__container = null
111
+ this.__map = null
112
+ }
113
+
114
+ show(visible=true)
115
+ //================
116
+ {
117
+ if (this.__container) {
118
+ this.__container.style.display = visible ? 'block' : 'none'
119
+ if (visible && !this.__visible) {
120
+ for (const layerId of DRAW_ANNOTATION_LAYERS) {
121
+ this.__map.setLayoutProperty(layerId, 'visibility', 'visible')
122
+ }
123
+ } else if (!visible && this.__visible) {
124
+ for (const layerId of DRAW_ANNOTATION_LAYERS) {
125
+ this.__map.setLayoutProperty(layerId, 'visibility', 'none')
126
+ }
127
+ }
128
+ }
129
+ this.__visible = visible
130
+ }
131
+
132
+ #cleanFeature(event)
133
+ //==================
134
+ {
135
+ const features = event.features.filter(f => f.type === 'Feature')
136
+ .map(f => {
137
+ return {
138
+ id: f.id,
139
+ type: 'Feature',
140
+ geometry: f.geometry
141
+ }
142
+ })
143
+ return features.length ? features[0] : null
144
+ }
145
+
146
+ #sendEvent(type, feature)
147
+ //=======================
148
+ {
149
+ this.__uncommittedFeatureIds.add(feature.id)
150
+ this.__flatmap.annotationEvent(type, feature)
151
+ }
152
+
153
+ createdFeature(event)
154
+ //===================
155
+ {
156
+ const feature = this.#cleanFeature(event)
157
+ if (feature) {
158
+ // Set properties to indicate that this is a drawn annotation
159
+ this.__draw.setFeatureProperty(feature.id, 'drawn', true)
160
+ this.__draw.setFeatureProperty(feature.id, 'label', 'Drawn annotation')
161
+ // They need to be on the feature passed to the annotator for storage
162
+ feature.properties = {
163
+ user_drawn: true,
164
+ user_label: 'Drawn annotation'
165
+ }
166
+ this.#sendEvent('created', feature)
167
+ }
168
+ }
169
+
170
+ deletedFeature(event)
171
+ //===================
172
+ {
173
+ const feature = this.#cleanFeature(event)
174
+ if (feature) {
175
+ if (this.__uncommittedFeatureIds.has(feature.id)) {
176
+ // Ignore delete on an uncommitted create or update
177
+ } else {
178
+ this.#sendEvent('deleted', feature)
179
+ }
180
+ }
181
+ }
182
+
183
+ updatedFeature(event)
184
+ //===================
185
+ {
186
+ const feature = this.#cleanFeature(event)
187
+ if (feature) {
188
+ if (this.__uncommittedFeatureIds.has(feature.id)) {
189
+ // Ignore updates on an uncommitted create or update
190
+ } else {
191
+ this.#sendEvent('updated', feature)
192
+ }
193
+ }
194
+ }
195
+
196
+ commitEvent(event)
197
+ //================
198
+ {
199
+ const feature = event.feature
200
+ if (event.type === 'deleted') {
201
+ this.__committedFeatures.delete(feature.id)
202
+ } else {
203
+ this.__committedFeatures.set(feature.id, feature)
204
+ }
205
+ this.__uncommittedFeatureIds.delete(feature.id)
206
+ }
207
+
208
+ rollbackEvent(event)
209
+ //==================
210
+ {
211
+ const feature = event.feature
212
+ if (event.type === 'created') {
213
+ this.__draw.delete(feature.id)
214
+ this.__committedFeatures.delete(feature.id)
215
+ this.__uncommittedFeatureIds.delete(feature.id)
216
+ } else if (event.type === 'deleted') {
217
+ this.__draw.add(feature)
218
+ this.__committedFeatures.set(feature.id, feature)
219
+ this.__uncommittedFeatureIds.delete(feature.id)
220
+ } else if (event.type === 'updated') {
221
+ const savedFeature = this.__committedFeatures.get(feature.id)
222
+ if (savedFeature) {
223
+ this.__draw.delete(feature.id)
224
+ this.__draw.add(savedFeature)
225
+ this.__uncommittedFeatureIds.delete(feature.id)
226
+ }
227
+ }
228
+ }
229
+
230
+ addFeature(feature)
231
+ //=================
232
+ {
233
+ feature = Object.assign({}, feature, {type: 'Feature'})
234
+ const ids = this.__draw.add(feature)
235
+ this.__committedFeatures.set(ids[0], feature)
236
+ this.__uncommittedFeatureIds.delete(ids[0])
237
+ }
238
+
239
+ refreshGeometry(feature)
240
+ //======================
241
+ {
242
+ return this.__draw.get(feature.id) || null
243
+ }
244
+ }
245
+
246
+ //==============================================================================
@@ -526,6 +526,74 @@ export class NerveControl
526
526
 
527
527
  //==============================================================================
528
528
 
529
+ export class AnnotatorControl
530
+ {
531
+ #enabled = false
532
+
533
+ constructor(flatmap)
534
+ {
535
+ this.__flatmap = flatmap
536
+ this.__map = null
537
+ }
538
+
539
+ getDefaultPosition()
540
+ //==================
541
+ {
542
+ return 'top-right'
543
+ }
544
+
545
+ onAdd(map)
546
+ //========
547
+ {
548
+ this.__map = map;
549
+ this.__container = document.createElement('div');
550
+ this.__container.className = 'maplibregl-ctrl';
551
+
552
+ this.__button = document.createElement('button');
553
+ this.__button.id = 'map-annotated-button';
554
+ this.__button.className = 'control-button text-button';
555
+ this.__button.setAttribute('type', 'button');
556
+ this.__button.setAttribute('aria-label', 'Draw on map for annotation');
557
+ this.__button.textContent = 'DRAW';
558
+ this.__button.title = 'Draw on map for annotation';
559
+ this.__container.appendChild(this.__button);
560
+
561
+ this.__container.addEventListener('click', this.onClick_.bind(this));
562
+ this.__setBackground();
563
+ return this.__container;
564
+ }
565
+
566
+ __setBackground()
567
+ //===============
568
+ {
569
+ if (this.#enabled) {
570
+ this.__button.setAttribute('style', 'background: red');
571
+ } else {
572
+ this.__button.removeAttribute('style');
573
+ }
574
+ }
575
+
576
+ onRemove()
577
+ //========
578
+ {
579
+ this.__container.parentNode.removeChild(this.__container)
580
+ this.__map = null
581
+ }
582
+
583
+ onClick_(event)
584
+ //=============
585
+ {
586
+ if (event.target.id === 'map-annotated-button') {
587
+ this.#enabled = !this.#enabled
588
+ this.__setBackground()
589
+ this.__flatmap.showAnnotator(this.#enabled)
590
+ }
591
+ event.stopPropagation();
592
+ }
593
+ }
594
+
595
+ //==============================================================================
596
+
529
597
  export class BackgroundControl
530
598
  {
531
599
  constructor(flatmap)
@@ -151,13 +151,8 @@ export class MinimapControl
151
151
  container: container,
152
152
  style: map.getStyle(),
153
153
  bounds: map.getBounds()
154
-
155
154
  });
156
155
 
157
- // Finish initialising once the map has loaded
158
-
159
- this._miniMap.on('load', this.load_.bind(this));
160
-
161
156
  return this._container;
162
157
  }
163
158
 
@@ -169,8 +164,8 @@ export class MinimapControl
169
164
  this._container = null;
170
165
  }
171
166
 
172
- load_()
173
- //=====
167
+ initialise()
168
+ //==========
174
169
  {
175
170
  const opts = this._options;
176
171
  const parentMap = this._map;
@@ -184,7 +179,7 @@ export class MinimapControl
184
179
  ];
185
180
  interactions.forEach(i => miniMap[i].disable());
186
181
 
187
- // Set background if specified (defaults is the parent map's)
182
+ // Set background if specified (default is the parent map's)
188
183
 
189
184
  if (this._background !== null) {
190
185
  miniMap.setPaintProperty('background', 'background-color', this._background);
@@ -22,6 +22,7 @@ export class Path3DControl
22
22
  {
23
23
  #button
24
24
  #container
25
+ #enabled = false
25
26
  #map = null
26
27
  #flatmap
27
28
 
@@ -60,16 +61,29 @@ export class Path3DControl
60
61
  this.#map = undefined
61
62
  }
62
63
 
64
+ __setBackground()
65
+ //===============
66
+ {
67
+ if (this.#enabled) {
68
+ this.#button.setAttribute('style', 'background: red');
69
+ } else {
70
+ this.#button.removeAttribute('style');
71
+ }
72
+ }
73
+
63
74
  onClick(_event)
64
75
  //=============
65
76
  {
66
77
  if (this.#button.classList.contains('control-active')) {
67
78
  this.#flatmap.enable3dPaths(false)
68
79
  this.#button.classList.remove('control-active')
80
+ this.#enabled = false
69
81
  } else {
70
82
  this.#flatmap.enable3dPaths(true)
71
83
  this.#button.classList.add('control-active')
84
+ this.#enabled = true
72
85
  }
86
+ this.__setBackground()
73
87
  }
74
88
  }
75
89
 
@@ -37,8 +37,6 @@ import {MapServer, loadJSON} from './mapserver.js';
37
37
  import {SearchIndex} from './search.js';
38
38
  import {UserInteractions} from './interactions.js';
39
39
 
40
- import {MinimapControl} from './controls/minimap.js';
41
- import {NavigationControl} from './controls/controls.js';
42
40
  import {APINATOMY_PATH_PREFIX} from './pathways';
43
41
 
44
42
  import * as images from './images.js';
@@ -162,33 +160,16 @@ class FlatMap
162
160
 
163
161
  this._map.setRenderWorldCopies(false);
164
162
 
165
- // Do we want a fullscreen control?
166
-
167
- if (mapDescription.options.fullscreenControl === true) {
168
- this._map.addControl(new maplibregl.FullscreenControl(), 'top-right');
169
- }
170
-
171
163
  // Disable map rotation
172
164
 
173
165
  //this._map.dragRotate.disable();
174
166
  //this._map.touchZoomRotate.disableRotation();
175
167
 
176
- // Add navigation controls if option set
177
-
178
- if (mapDescription.options.navigationControl) {
179
- const value = mapDescription.options.navigationControl;
180
- const position = ((typeof value === 'string')
181
- && ['top-left', 'top-right', 'bottom-right', 'bottom-left'].includes(value))
182
- ? value : 'bottom-right';
183
- this._map.addControl(new NavigationControl(this), position);
184
- }
185
-
186
168
  // Finish initialisation when all sources have loaded
187
169
  // and map has rendered
188
170
 
189
171
  this._userInteractions = null;
190
172
  this._initialState = null;
191
- this._minimap = null;
192
173
 
193
174
  this._map.on('idle', () => {
194
175
  if (this._userInteractions === null) {
@@ -207,14 +188,9 @@ class FlatMap
207
188
  this._userInteractions.setState(this._options.state);
208
189
  }
209
190
  this._initialState = this.getState();
210
-
211
- // Add a minimap if option set
212
-
213
- if (this.options.minimap) {
214
- this._minimap = new MinimapControl(this, this.options.minimap);
215
- this._map.addControl(this._minimap);
216
- }
217
-
191
+ if (this._userInteractions.minimap) {
192
+ this._userInteractions.minimap.initialise()
193
+ }
218
194
  this._resolve(this);
219
195
  }
220
196
  });
@@ -224,13 +200,11 @@ class FlatMap
224
200
  //============================
225
201
  {
226
202
  // Load any images required by the map
227
-
228
203
  for (const image of this._options.images) {
229
204
  await this.addImage(image.id, image.url, '', image.options);
230
205
  }
231
206
 
232
207
  // Layers have now loaded so finish setting up
233
-
234
208
  this._userInteractions = new UserInteractions(this);
235
209
  }
236
210
 
@@ -836,8 +810,8 @@ class FlatMap
836
810
 
837
811
  this._map.setPaintProperty('background', 'background-color', colour);
838
812
 
839
- if (this._minimap) {
840
- this._minimap.setBackgroundColour(colour);
813
+ if (this._userInteractions.minimap) {
814
+ this._userInteractions.minimap.setBackgroundColour(colour);
841
815
  }
842
816
  }
843
817
 
@@ -851,8 +825,8 @@ class FlatMap
851
825
  {
852
826
  this._map.setPaintProperty('background', 'background-opacity', opacity);
853
827
 
854
- if (this._minimap) {
855
- this._minimap.setBackgroundOpacity(opacity);
828
+ if (this._userInteractions.minimap) {
829
+ this._userInteractions.minimap.setBackgroundOpacity(opacity);
856
830
  }
857
831
  }
858
832
 
@@ -864,8 +838,8 @@ class FlatMap
864
838
  showMinimap(show)
865
839
  //===============
866
840
  {
867
- if (this._minimap) {
868
- this._minimap.show(show);
841
+ if (this._userInteractions.minimap) {
842
+ this._userInteractions.minimap.show(show);
869
843
  }
870
844
 
871
845
  }
@@ -1063,6 +1037,100 @@ class FlatMap
1063
1037
  return data;
1064
1038
  }
1065
1039
 
1040
+ /**
1041
+ * Show or hide a tool for drawing regions to annotate on the map.
1042
+ *
1043
+ * @param {boolean} [visible=true]
1044
+ */
1045
+ showAnnotator(visible=true)
1046
+ //=========================
1047
+ {
1048
+ if (this._userInteractions !== null) {
1049
+ this._userInteractions.showAnnotator(visible)
1050
+ }
1051
+ }
1052
+
1053
+ /**
1054
+ * Generate an ``annotation`` callback event when a drawn annotation has been created
1055
+ * a modified.
1056
+ *
1057
+ * @param eventType {string} Either ``created``, ``updated`` or ``deleted``
1058
+ * @param feature {Object} A feature object with ``id``, ``type``, and ``geometry``
1059
+ * fields of a feature that has been created, updated or
1060
+ * deleted.
1061
+ */
1062
+ annotationEvent(eventType, feature)
1063
+ //=================================
1064
+ {
1065
+ this.callback('annotation', {
1066
+ type: eventType,
1067
+ feature: feature
1068
+ });
1069
+ }
1070
+
1071
+ /**
1072
+ * Mark a drawn/changed annotation as having been accepted by the user.
1073
+ *
1074
+ * @param event {Object} The object as received in an annotation callback
1075
+ * @param event.type {string} Either ``created``, ``updated`` or ``deleted``
1076
+ * @param event.feature {Object} A feature object.
1077
+ */
1078
+ commitAnnotationEvent(event)
1079
+ //==========================
1080
+ {
1081
+ if (this._userInteractions) {
1082
+ this._userInteractions.commitAnnotationEvent(event)
1083
+ }
1084
+ }
1085
+
1086
+ /**
1087
+ * Mark a drawn/changed annotation as having been rejected by the user.
1088
+ *
1089
+ * @param event {Object} The object as received in an annotation callback
1090
+ * @param event.type {string} Either ``created``, ``updated`` or ``deleted``
1091
+ * @param event.feature {Object} A feature object.
1092
+ */
1093
+ rollbackAnnotationEvent(event)
1094
+ //============================
1095
+ {
1096
+ if (this._userInteractions) {
1097
+ this._userInteractions.rollbackAnnotationEvent(event)
1098
+ }
1099
+ }
1100
+
1101
+ /**
1102
+ * Add a drawn feature to the annotation drawing tool.
1103
+ *
1104
+ * @param feature {Object} The feature to add
1105
+ * @param feature.id {string} The feature's id
1106
+ * @param feature.geometry {Object} The feature's geometry as GeoJSON
1107
+ */
1108
+ addAnnotationFeature(feature)
1109
+ //===========================
1110
+ {
1111
+ if (this._userInteractions) {
1112
+ this._userInteractions.addAnnotationFeature(feature)
1113
+ }
1114
+ }
1115
+
1116
+ /**
1117
+ * Return the feature as it is currently drawn. This is so
1118
+ * the correct geometry can be saved with a feature should
1119
+ * a user make changes before submitting dialog provided
1120
+ * by an external annotator.
1121
+ *
1122
+ * @param feature {Object} The drawn feature to refresh.
1123
+ * @returns {Object|null} The feature with currently geometry or ``null``
1124
+ * if the feature has been deleted.
1125
+ */
1126
+ refreshAnnotationFeatureGeometry(feature)
1127
+ //=======================================
1128
+ {
1129
+ if (this._userInteractions) {
1130
+ this._userInteractions.refreshAnnotationFeatureGeometry(feature)
1131
+ }
1132
+ }
1133
+
1066
1134
  /**
1067
1135
  * Generate a callback as a result of some event with a flatmap feature.
1068
1136
  *