@abi-software/flatmap-viewer 2.2.8 → 2.2.10-devel

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.2.8``
41
+ * ``npm install @abi-software/flatmap-viewer@2.2.9``
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.2.8",
3
+ "version": "2.2.10-devel",
4
4
  "description": "Flatmap viewer using Maplibre GL",
5
5
  "repository": "https://github.com/AnatomicMaps/flatmap-viewer.git",
6
6
  "main": "src/main.js",
package/src/controls.js CHANGED
@@ -26,6 +26,20 @@ import * as pathways from './pathways.js';
26
26
 
27
27
  //==============================================================================
28
28
 
29
+ // Make sure colour string is in `#rrggbb` form.
30
+ // Based on https://stackoverflow.com/a/47355187
31
+
32
+ function standardise_color(str){
33
+ const canvas = document.createElement("canvas");
34
+ const ctx = canvas.getContext("2d");
35
+ ctx.fillStyle = str;
36
+ const colour = ctx.fillStyle;
37
+ canvas.remove()
38
+ return colour;
39
+ }
40
+
41
+ //==============================================================================
42
+
29
43
  export class NavigationControl
30
44
  {
31
45
  constructor(flatmap)
@@ -102,9 +116,9 @@ export class PathControl
102
116
  this._legend.className = 'flatmap-nerve-grid';
103
117
 
104
118
  const innerHTML = [];
105
- innerHTML.push(`<label for="path-all-paths">ALL PATHS:</label><input id="path-all-paths" type="checkbox" checked/><div class="nerve-line"></div>`);
119
+ innerHTML.push(`<label for="path-all-paths">ALL PATHS:</label><div class="nerve-line"></div><input id="path-all-paths" type="checkbox" checked/>`);
106
120
  for (const path of pathways.PATH_TYPES) {
107
- innerHTML.push(`<label for="path-${path.type}">${path.label}</label><input id="path-${path.type}" type="checkbox" checked/><div class="nerve-line nerve-${path.type}"></div>`);
121
+ innerHTML.push(`<label for="path-${path.type}">${path.label}</label><div class="nerve-line nerve-${path.type}"></div><input id="path-${path.type}" type="checkbox" checked/>`);
108
122
  }
109
123
  this._legend.innerHTML = innerHTML.join('\n');
110
124
  this.__checkedCount = pathways.PATH_TYPES.length;
@@ -112,10 +126,10 @@ export class PathControl
112
126
 
113
127
  this._button = document.createElement('button');
114
128
  this._button.id = 'nerve-key-button';
115
- this._button.className = 'control-button';
129
+ this._button.className = 'control-button text-button';
116
130
  this._button.setAttribute('type', 'button');
117
131
  this._button.setAttribute('aria-label', 'Nerve paths legend');
118
- this._button.setAttribute('legend-visible', 'false');
132
+ this._button.setAttribute('control-visible', 'false');
119
133
  this._button.textContent = 'PATHS';
120
134
  this._button.title = 'Show/hide neuron paths';
121
135
  this._container.appendChild(this._button);
@@ -135,13 +149,13 @@ export class PathControl
135
149
  //=============
136
150
  {
137
151
  if (event.target.id === 'nerve-key-button') {
138
- if (this._button.getAttribute('legend-visible') === 'false') {
152
+ if (this._button.getAttribute('control-visible') === 'false') {
139
153
  this._container.appendChild(this._legend);
140
- this._button.setAttribute('legend-visible', 'true');
154
+ this._button.setAttribute('control-visible', 'true');
141
155
  this._legend.focus();
142
156
  } else {
143
157
  this._legend = this._container.removeChild(this._legend);
144
- this._button.setAttribute('legend-visible', 'false');
158
+ this._button.setAttribute('control-visible', 'false');
145
159
  }
146
160
  } else if (event.target.tagName === 'INPUT') {
147
161
  if (event.target.id === 'path-all-paths') {
@@ -186,3 +200,168 @@ export class PathControl
186
200
  }
187
201
 
188
202
  //==============================================================================
203
+
204
+ export class LayerControl
205
+ {
206
+ constructor(flatmap, layerManager)
207
+ {
208
+ this.__flatmap = flatmap;
209
+ this.__manager = layerManager;
210
+ this.__map = undefined;
211
+ }
212
+
213
+ getDefaultPosition()
214
+ //==================
215
+ {
216
+ return 'top-right';
217
+ }
218
+
219
+ onAdd(map)
220
+ //========
221
+ {
222
+ this.__map = map;
223
+ this.__container = document.createElement('div');
224
+ this.__container.className = 'maplibregl-ctrl';
225
+ this.__container.id = 'flatmap-layer-control';
226
+
227
+ this.__layers = document.createElement('div');
228
+ this.__layers.id = 'layer-control-text';
229
+ this.__layers.className = 'flatmap-layer-grid';
230
+
231
+ const innerHTML = [];
232
+ innerHTML.push(`<label for="layer-all-layers">ALL LAYERS:</label><input id="layer-all-layers" type="checkbox" checked/>`);
233
+ for (const layer of this.__manager.layers) {
234
+ innerHTML.push(`<label for="layer-${layer.id}">${layer.description}</label><input id="layer-${layer.id}" type="checkbox" checked/>`);
235
+ }
236
+ this.__layers.innerHTML = innerHTML.join('\n');
237
+
238
+ this.__layersCount = this.__manager.layers.length;
239
+ this.__checkedCount = this.__layersCount;
240
+ this.__halfCount = Math.trunc(this.__checkedCount/2);
241
+
242
+ this.__button = document.createElement('button');
243
+ this.__button.id = 'map-layers-button';
244
+ this.__button.className = 'control-button text-button';
245
+ this.__button.setAttribute('type', 'button');
246
+ this.__button.setAttribute('aria-label', 'Show/hide map layers');
247
+ this.__button.setAttribute('control-visible', 'false');
248
+ this.__button.textContent = 'LAYERS';
249
+ this.__button.title = 'Show/hide map layers';
250
+ this.__container.appendChild(this.__button);
251
+
252
+ this.__container.addEventListener('click', this.onClick_.bind(this));
253
+ return this.__container;
254
+ }
255
+
256
+ onRemove()
257
+ //========
258
+ {
259
+ this.__container.parentNode.removeChild(this.__container);
260
+ this.__map = undefined;
261
+ }
262
+
263
+ onClick_(event)
264
+ //=============
265
+ {
266
+ if (event.target.id === 'map-layers-button') {
267
+ if (this.__button.getAttribute('control-visible') === 'false') {
268
+ this.__container.appendChild(this.__layers);
269
+ this.__button.setAttribute('control-visible', 'true');
270
+ this.__layers.focus();
271
+ } else {
272
+ this.__layers = this.__container.removeChild(this.__layers);
273
+ this.__button.setAttribute('control-visible', 'false');
274
+ }
275
+ } else if (event.target.tagName === 'INPUT') {
276
+ if (event.target.id === 'layer-all-layers') {
277
+ if (event.target.indeterminate) {
278
+ event.target.checked = (this.__checkedCount >= this.__halfCount);
279
+ event.target.indeterminate = false;
280
+ }
281
+ if (event.target.checked) {
282
+ this.__checkedCount = this.__layersCount;
283
+ } else {
284
+ this.__checkedCount = 0;
285
+ }
286
+ for (const layer of this.__manager.layers) {
287
+ const layerCheckbox = document.getElementById(`layer-${layer.id}`);
288
+ if (layerCheckbox) {
289
+ layerCheckbox.checked = event.target.checked;
290
+ this.__manager.activate(layer.id, event.target.checked);
291
+ }
292
+ }
293
+ } else if (event.target.id.startsWith('layer-')) {
294
+ const layerId = event.target.id.substring(6);
295
+ this.__manager.activate(layerId, event.target.checked);
296
+ if (event.target.checked) {
297
+ this.__checkedCount += 1;
298
+ } else {
299
+ this.__checkedCount -= 1;
300
+ }
301
+ const allLayersCheckbox = document.getElementById('layer-all-layers');
302
+ if (this.__checkedCount === 0) {
303
+ allLayersCheckbox.checked = false;
304
+ allLayersCheckbox.indeterminate = false;
305
+ } else if (this.__checkedCount === this.__layersCount) {
306
+ allLayersCheckbox.checked = true;
307
+ allLayersCheckbox.indeterminate = false;
308
+ } else {
309
+ allLayersCheckbox.indeterminate = true;
310
+ }
311
+ }
312
+ }
313
+ event.stopPropagation();
314
+ }
315
+ }
316
+
317
+ //==============================================================================
318
+
319
+ export class BackgroundControl
320
+ {
321
+ constructor(flatmap)
322
+ {
323
+ this.__flatmap = flatmap;
324
+ this.__map = undefined;
325
+ }
326
+
327
+ getDefaultPosition()
328
+ //==================
329
+ {
330
+ return 'top-right';
331
+ }
332
+
333
+ onAdd(map)
334
+ //========
335
+ {
336
+ this.__map = map;
337
+ this.__container = document.createElement('div');
338
+ this.__container.className = 'maplibregl-ctrl';
339
+ this.__colourDiv = document.createElement('div');
340
+ this.__colourDiv.setAttribute('aria-label', 'Change background colour');
341
+ this.__colourDiv.title = 'Change background colour';
342
+ const background = standardise_color(this.__flatmap.getBackgroundColour());
343
+ this.__colourDiv.innerHTML = `<input type="color" id="colourPicker" value="${background}">`;
344
+ this.__container.appendChild(this.__colourDiv);
345
+ this.__colourDiv.addEventListener('input', this.__updateColour.bind(this), false);
346
+ this.__colourDiv.addEventListener('change', this.__updateColour.bind(this), false);
347
+ return this.__container;
348
+ }
349
+
350
+ onRemove()
351
+ //========
352
+ {
353
+ this.__container.parentNode.removeChild(this.__container);
354
+ this.__map = undefined;
355
+ }
356
+
357
+ __updateColour(event)
358
+ //===================
359
+ {
360
+ const colour = event.target.value;
361
+ this.__flatmap.setBackgroundColour(colour);
362
+ this.__flatmap.controlEvent('change', 'background', colour)
363
+ event.stopPropagation();
364
+ }
365
+ }
366
+
367
+ //==============================================================================
@@ -621,12 +621,6 @@ class FlatMap
621
621
  this._map.resize();
622
622
  }
623
623
 
624
- mapLayerId(name)
625
- //==============
626
- {
627
- return `${this.uniqueId}/${name}`;
628
- }
629
-
630
624
  getIdentifier()
631
625
  //=============
632
626
  {
@@ -872,6 +866,23 @@ class FlatMap
872
866
  });
873
867
  }
874
868
 
869
+ /**
870
+ * Generate a callback as a result of some event in a control.
871
+ *
872
+ * @param {string} eventType The event type
873
+ * @param {string} control The name of the control
874
+ * @param {string} value The value of the control
875
+ */
876
+ controlEvent(eventType, control, value)
877
+ //=====================================
878
+ {
879
+ this.callback(eventType, {
880
+ type: 'control',
881
+ control: control,
882
+ value: value
883
+ });
884
+ }
885
+
875
886
  /**
876
887
  * Generate callbacks as a result of panning/zooming the map.
877
888
  *
@@ -1211,6 +1222,12 @@ export class MapManager
1211
1222
  mapOptions['pathControls'] = true;
1212
1223
  }
1213
1224
 
1225
+ // Default is to show layer controls
1226
+
1227
+ if (!('layerControl' in mapOptions)) {
1228
+ mapOptions['layerControl'] = true;
1229
+ }
1230
+
1214
1231
  // Mapmaker's changed the name of the field to indicate that indicates if
1215
1232
  // there are raster layers
1216
1233
  if (!('image-layers' in mapIndex) && ('image_layer' in mapIndex)) {
@@ -1285,9 +1302,8 @@ export class MapManager
1285
1302
  outline: true
1286
1303
  };
1287
1304
  }
1288
- if ('authoring' in mapIndex) {
1289
- mapOptions.layerOptions.style == 'authoring'
1290
- } else if ('style' in mapIndex) {
1305
+ mapOptions.layerOptions.authoring = ('authoring' in mapIndex) ? mapIndex.authoring : false;
1306
+ if ('style' in mapIndex) {
1291
1307
  mapOptions.layerOptions.style = mapIndex.style;
1292
1308
  } else {
1293
1309
  mapOptions.layerOptions.style = 'flatmap';
@@ -38,10 +38,11 @@ import {displayedProperties} from './info.js';
38
38
  import {InfoControl} from './info.js';
39
39
  import {LayerManager} from './layers.js';
40
40
  import {PATHWAYS_LAYER, Pathways} from './pathways.js';
41
- import {PathControl} from './controls.js';
41
+ import {BackgroundControl, LayerControl, PathControl} from './controls.js';
42
42
  import {SearchControl} from './search.js';
43
43
  import {VECTOR_TILES_SOURCE} from './styling.js';
44
44
 
45
+ import * as pathways from './pathways.js';
45
46
  import * as utils from './utils.js';
46
47
 
47
48
  //==============================================================================
@@ -144,20 +145,32 @@ export class UserInteractions
144
145
  }
145
146
  }
146
147
 
148
+ // Add and manage our layers
149
+
150
+ this._layerManager = new LayerManager(flatmap);
151
+
152
+ // Control background colour (NB. this depends on having map layers created)
153
+
154
+ if (flatmap.options.backgroundControl) {
155
+ this._map.addControl(new BackgroundControl(flatmap));
156
+ }
157
+
147
158
  // Neural pathways which are either controlled externally
148
159
  // or by our local controls
149
160
 
150
161
  this._pathways = new Pathways(flatmap);
151
162
 
152
- if (flatmap.options.pathControls) {
153
- // Add controls to manage our pathways
163
+ // Add a control to manage our pathways
154
164
 
165
+ if (flatmap.options.pathControls) {
155
166
  this._map.addControl(new PathControl(flatmap));
156
167
  }
157
168
 
158
- // Manage our layers
169
+ // Add a control to manage our layers
159
170
 
160
- this._layerManager = new LayerManager(flatmap);
171
+ if (flatmap.options.layerControl) {
172
+ this._map.addControl(new LayerControl(flatmap, this._layerManager));
173
+ }
161
174
 
162
175
  // Flag features that have annotations
163
176
  // Also flag those features that are models of something
@@ -649,17 +662,17 @@ export class UserInteractions
649
662
  if (('label' in properties || 'hyperlink' in properties)
650
663
  && (forceLabel || !('tooltip' in properties) || properties.tooltip)
651
664
  && !('labelled' in properties)) {
652
- let tooltip = '';
653
- if ('label' in properties) {
654
- const label = properties.label;
655
- tooltip = (label.substr(0, 1).toUpperCase() + label.substr(1)).replaceAll("\n", "<br/>");
656
- } else {
657
- tooltip = properties.hyperlink
658
- }
665
+ const label = ('label' in properties) ? (properties.label.substr(0, 1).toUpperCase()
666
+ + properties.label.substr(1)).replaceAll("\n", "<br/>")
667
+ : '';
659
668
  if ('hyperlink' in properties) {
660
- return `<div class='flatmap-feature-label'><a href='{properties.hyperlink}'>${tooltip}</a></div>`;
669
+ if (label === '') {
670
+ return `<div class='flatmap-feature-label'><a href='{properties.hyperlink}'>${properties.hyperlink}</a></div>`;
671
+ } else {
672
+ return `<div class='flatmap-feature-label'><a href='{properties.hyperlink}'>${properties.hyperlink}</a><br/>${label}</div>`;
673
+ }
661
674
  } else {
662
- return `<div class='flatmap-feature-label'>${tooltip}</div>`;
675
+ return `<div class='flatmap-feature-label'>${label}</div>`;
663
676
  }
664
677
  }
665
678
  return '';
@@ -842,31 +855,34 @@ export class UserInteractions
842
855
  selectionEvent_(event, feature)
843
856
  //=============================
844
857
  {
845
- const multipleSelect = event.ctrlKey || event.metaKey;
846
- if (!multipleSelect) {
847
- this.__unselectFeatures();
848
- }
849
858
  if (feature !== undefined) {
850
- const featureId = feature.id;
851
- const selecting = !this.featureSelected_(featureId);
852
- if ('properties' in feature
853
- && 'type' in feature.properties
854
- && feature.properties.type.startsWith('line')) {
859
+ const clickedFeatureId = feature.id;
860
+ const dim = !('properties' in feature
861
+ && 'kind' in feature.properties
862
+ && ['cell-type', 'scaffold', 'tissue'].indexOf(feature.properties.kind) >= 0);
863
+ if (!(event.ctrlKey || event.metaKey)) {
864
+ let selecting = true;
865
+ for (const featureId of this._selectedFeatureIds.keys()) {
866
+ if (featureId === clickedFeatureId) {
867
+ selecting = false;
868
+ break;
869
+ }
870
+ }
871
+ this.__unselectFeatures();
872
+ if (selecting) {
873
+ for (const feature of this._activeFeatures) {
874
+ this.selectFeature_(feature.id, dim);
875
+ }
876
+ }
877
+ } else {
878
+ const clickedSelected = this.featureSelected_(clickedFeatureId);
855
879
  for (const feature of this._activeFeatures) {
856
- const featureId = feature.id;
857
- if (selecting) {
858
- this.selectFeature_(featureId);
880
+ if (clickedSelected) {
881
+ this.unselectFeature_(feature.id);
859
882
  } else {
860
- this.unselectFeature_(featureId);
883
+ this.selectFeature_(feature.id, dim);
861
884
  }
862
885
  }
863
- } else if (selecting) {
864
- const dim = !('properties' in feature
865
- && 'kind' in feature.properties
866
- && ['cell-type', 'scaffold', 'tissue'].indexOf(feature.properties.kind) >= 0);
867
- this.selectFeature_(featureId, dim);
868
- } else {
869
- this.unselectFeature_(featureId);
870
886
  }
871
887
  }
872
888
  }
@@ -875,24 +891,14 @@ export class UserInteractions
875
891
  //================
876
892
  {
877
893
  this.clearActiveMarker_();
878
- const clickedFeatures = this._map.queryRenderedFeatures(event.point)
879
- .filter(feature => this.__enabledFeature(feature));
894
+ const clickedFeatures = this._map.queryRenderedFeatures(event.point);
880
895
  if (clickedFeatures.length == 0){
896
+ this.__unselectFeatures();
881
897
  return;
882
898
  }
883
899
  const clickedFeature = clickedFeatures[0];
884
900
  const originalEvent = event.originalEvent;
885
- if (clickedFeature === undefined || this._activeFeatures.length === 1) {
886
- this.selectionEvent_(originalEvent, clickedFeature);
887
- } else if (this._activeFeatures.length > 1) {
888
- const multipleSelect = originalEvent.ctrlKey || originalEvent.metaKey;
889
- if (!multipleSelect) {
890
- this.__unselectFeatures();
891
- }
892
- for (const feature of this._activeFeatures) {
893
- this.selectFeature_(feature.id);
894
- }
895
- }
901
+ this.selectionEvent_(originalEvent, clickedFeature);
896
902
  if (this._modal) {
897
903
  // Remove tooltip, reset active features, etc
898
904
  this.__resetFeatureDisplay();
@@ -976,18 +982,23 @@ export class UserInteractions
976
982
  //===============================
977
983
  {
978
984
  // Disable/enable all paths except those with `pathTypes`
979
-
980
- this.enablePathFeatures_(!enable, this._pathways.allFeatureIds());
981
-
982
985
  if (Array.isArray(pathTypes)) {
983
- for (const pathType of pathTypes) {
984
- this.enablePath(pathType, enable);
986
+ for (const pathType of pathways.PATH_TYPES) {
987
+ if (pathTypes.indexOf(pathType.type) >= 0) {
988
+ this.enablePath(pathType.type, enable)
989
+ } else {
990
+ this.enablePath(pathType.type, !enable)
991
+ }
985
992
  }
986
993
  } else {
987
- this.enablePath(pathTypes, enable);
994
+ for (const pathType of pathways.PATH_TYPES) {
995
+ if (pathType.type === pathTypes) {
996
+ this.enablePath(pathType.type, enable)
997
+ } else {
998
+ this.enablePath(pathType.type, !enable)
999
+ }
1000
+ }
988
1001
  }
989
-
990
- this._disabledPathFeatures = true;
991
1002
  }
992
1003
 
993
1004
  pathwaysFeatureIds(externalIds)
package/src/layers.js CHANGED
@@ -28,113 +28,174 @@ import * as style from './styling.js';
28
28
  import * as utils from './utils.js';
29
29
 
30
30
  const FEATURES_LAYER = 'features'
31
+ const RASTER_LAYERS_NAME = 'Background image layer'
31
32
 
32
33
  //==============================================================================
33
34
 
34
- class MapFeatureLayer
35
+ class MapStylingLayers
35
36
  {
36
- constructor(flatmap, layer, background_layers=true)
37
+ constructor(flatmap, layerId)
37
38
  {
38
39
  this.__map = flatmap.map;
40
+ this.__id = layerId;
41
+ this.__layers = [];
42
+ this.__active = true;
43
+ this.__layerOptions = flatmap.options.layerOptions;
39
44
  this.__separateLayers = flatmap.options.separateLayers;
40
- this.__id = layer.id;
41
- this.__rasterLayers = [];
42
- this.__styleLayers = [];
45
+ }
43
46
 
44
- const layerOptions = flatmap.options.layerOptions;
47
+ get id()
48
+ //======
49
+ {
50
+ return this.__id;
51
+ }
52
+
53
+ get active()
54
+ //==========
55
+ {
56
+ return this.__active;
57
+ }
58
+
59
+ addLayer(styleLayer, options)
60
+ //==========================
61
+ {
62
+ this.__map.addLayer(styleLayer.style(options));
63
+ this.__layers.push(styleLayer);
64
+ }
65
+
66
+ __showLayer(layer, visible=true)
67
+ //===============================
68
+ {
69
+ this.__map.setLayoutProperty(layer.id, 'visibility', visible ? 'visible' : 'none');
70
+ }
71
+
72
+ activate(enable=true)
73
+ //===================
74
+ {
75
+ for (const layer of this.__layers) {
76
+ this.__showLayer(layer, enable);
77
+ }
78
+ this.__active = enable;
79
+ }
80
+
81
+ vectorSourceId(sourceLayer)
82
+ //=========================
83
+ {
84
+ return this.__separateLayers ? `${this.__id}_${sourceLayer}`
85
+ : sourceLayer;
86
+ }
87
+ }
88
+
89
+ //==============================================================================
90
+
91
+ class MapFeatureLayers extends MapStylingLayers
92
+ {
93
+ constructor(flatmap, layer)
94
+ {
95
+ super(flatmap, layer.id);
45
96
  const vectorTileSource = this.__map.getSource('vector-tiles');
46
97
  const haveVectorLayers = (typeof vectorTileSource !== 'undefined');
47
- const featuresVectorLayerId = this.__separateLayers
48
- ? `${this.__id}_${FEATURES_LAYER}`
49
- : FEATURES_LAYER;
50
- const vectorFeatures = haveVectorLayers
51
- && vectorTileSource.vectorLayerIds.indexOf(featuresVectorLayerId) >= 0;
52
- if (background_layers) {
53
- if (vectorFeatures) {
54
- this.__addStyleLayer(style.BodyLayer, layerOptions);
55
- }
56
- if (flatmap.details['image-layers']) {
57
- for (const raster_layer_id of layer['image-layers']) {
58
- this.__addRasterLayer(raster_layer_id, layerOptions);
59
- }
60
- }
61
- }
98
+
62
99
  // if no image layers then make feature borders (and lines?) more visible...??
63
100
  if (haveVectorLayers) {
101
+ const featuresVectorSource = this.vectorSourceId(FEATURES_LAYER);
102
+ const vectorFeatures = vectorTileSource.vectorLayerIds
103
+ .indexOf(featuresVectorSource) >= 0;
64
104
  if (vectorFeatures) {
65
- this.__addStyleLayer(style.FeatureFillLayer, layerOptions);
66
- this.__addStyleLayer(style.FeatureDashLineLayer, layerOptions);
67
- this.__addStyleLayer(style.FeatureLineLayer, layerOptions);
68
- this.__addStyleLayer(style.FeatureBorderLayer, layerOptions);
105
+ this.__addStyleLayer(style.FeatureFillLayer);
106
+ this.__addStyleLayer(style.FeatureDashLineLayer);
107
+ this.__addStyleLayer(style.FeatureLineLayer);
108
+ this.__addStyleLayer(style.FeatureBorderLayer);
69
109
  }
70
- this.__addPathwayStyleLayers(layerOptions);
110
+ this.__addPathwayStyleLayers(this.__layerOptions);
71
111
  if (vectorFeatures) {
72
- this.__addStyleLayer(style.FeatureLargeSymbolLayer, layerOptions);
112
+ this.__addStyleLayer(style.FeatureLargeSymbolLayer);
73
113
  if (!flatmap.options.tooltips) {
74
- this.__addStyleLayer(style.FeatureSmallSymbolLayer, layerOptions);
114
+ this.__addStyleLayer(style.FeatureSmallSymbolLayer);
75
115
  }
76
116
  }
77
117
  }
78
118
 
79
119
  // Make sure our colour options are set properly, in particular raster layer visibility
80
120
 
81
- this.setColour(layerOptions);
121
+ this.setColour(this.__layerOptions);
82
122
  }
83
123
 
84
- get id()
85
- //======
124
+ __addStyleLayer(styleClass, sourceLayer=FEATURES_LAYER)
125
+ //=====================================================
86
126
  {
87
- return this.__id;
127
+ const styleLayer = new styleClass(`${this.__id}_${sourceLayer}`,
128
+ this.vectorSourceId(sourceLayer));
129
+ this.__map.addLayer(styleLayer.style(this.__layerOptions));
130
+ this.__layers.push(styleLayer);
88
131
  }
89
132
 
90
- __addRasterLayer(raster_layer_id, options)
91
- //========================================
133
+ __addPathwayStyleLayers()
134
+ //=======================
92
135
  {
93
- const rasterLayer = new style.RasterLayer(raster_layer_id);
94
- this.__map.addLayer(rasterLayer.style(options));
95
- this.__rasterLayers.push(rasterLayer);
136
+ const pathwaysVectorSource = this.vectorSourceId(PATHWAYS_LAYER);
137
+ if (this.__map.getSource('vector-tiles')
138
+ .vectorLayerIds
139
+ .indexOf(pathwaysVectorSource) >= 0) {
140
+ this.__addStyleLayer(style.PathLineLayer, PATHWAYS_LAYER);
141
+ this.__addStyleLayer(style.PathDashlineLayer, PATHWAYS_LAYER);
142
+ this.__addStyleLayer(style.NervePolygonBorder, PATHWAYS_LAYER);
143
+ this.__addStyleLayer(style.NervePolygonFill, PATHWAYS_LAYER);
144
+ this.__addStyleLayer(style.FeatureNerveLayer, PATHWAYS_LAYER);
145
+ }
96
146
  }
97
147
 
98
- __addPathwayStyleLayers(options)
99
- //==============================
148
+ setColour(options)
149
+ //================
100
150
  {
101
- const pathwaysVectorLayerId = this.__separateLayers
102
- ? `${this.__id}_${PATHWAYS_LAYER}`
103
- : PATHWAYS_LAYER;
104
- if (this.__map.getSource('vector-tiles')
105
- .vectorLayerIds
106
- .indexOf(pathwaysVectorLayerId) >= 0) {
107
- this.__addStyleLayer(style.PathLineLayer, options, PATHWAYS_LAYER);
108
- this.__addStyleLayer(style.PathDashlineLayer, options, PATHWAYS_LAYER);
109
- this.__addStyleLayer(style.NervePolygonBorder, options, PATHWAYS_LAYER);
110
- this.__addStyleLayer(style.NervePolygonFill, options, PATHWAYS_LAYER);
111
- this.__addStyleLayer(style.FeatureNerveLayer, options, PATHWAYS_LAYER);
151
+ for (const layer of this.__layers) {
152
+ const paintStyle = layer.paintStyle(options, true);
153
+ for (const [property, value] of Object.entries(paintStyle)) {
154
+ this.__map.setPaintProperty(layer.id, property, value, {validate: false});
155
+ }
112
156
  }
113
157
  }
158
+ }
114
159
 
115
- __addStyleLayer(styleClass, options, sourceLayer=FEATURES_LAYER)
116
- //==============================================================
160
+ //==============================================================================
161
+
162
+ class MapRasterLayers extends MapStylingLayers
163
+ {
164
+ constructor(flatmap, bodyLayerId=null)
117
165
  {
118
- const layerId = `${this.__id}_${sourceLayer}`;
119
- const source = this.__separateLayers ? layerId : sourceLayer;
120
- const styleLayer = new styleClass(layerId, source);
121
- this.__map.addLayer(styleLayer.style(options));
122
- this.__styleLayers.push(styleLayer);
166
+ super(flatmap, 'background-image-layer');
167
+ if (bodyLayerId !== null) {
168
+ const layerId = `${bodyLayerId}_${FEATURES_LAYER}`;
169
+ const source = flatmap.options.separateLayers ? layerId : FEATURES_LAYER;
170
+ const styleLayer = new style.BodyLayer(layerId, source);
171
+ this.__map.addLayer(styleLayer.style(this.__layerOptions));
172
+ this.__layers.push(styleLayer);
173
+ }
174
+ // Make sure our colour options are set properly, in particular raster layer visibility
175
+ this.setColour(this.__layerOptions);
176
+ }
177
+
178
+ addLayer(layer)
179
+ //=============
180
+ {
181
+ for (const layer_id of layer['image-layers']) {
182
+ const rasterLayer = new style.RasterLayer(layer_id);
183
+ this.__map.addLayer(rasterLayer.style(this.__layerOptions));
184
+ this.__layers.push(rasterLayer);
185
+ }
186
+ // Make sure our colour options are set properly, in particular raster layer visibility
187
+ this.setColour(this.__layerOptions);
123
188
  }
124
189
 
125
190
  setColour(options)
126
191
  //================
127
192
  {
128
193
  const coloured = !('colour' in options) || options.colour;
129
- for (const rasterLayer of this.__rasterLayers) {
130
- this.__map.setLayoutProperty(rasterLayer.id, 'visibility', coloured ? 'visible' : 'none',
131
- {validate: false});
132
- }
133
- for (const styleLayer of this.__styleLayers) {
134
- const paintStyle = styleLayer.paintStyle(options, true);
135
- for (const [property, value] of Object.entries(paintStyle)) {
136
- this.__map.setPaintProperty(styleLayer.id, property, value, {validate: false});
137
- }
194
+ for (const layer of this.__layers) {
195
+ // Check active status when resetting to visible....
196
+ this.__map.setLayoutProperty(layer.id, 'visibility',
197
+ (coloured && this.active) ? 'visible' : 'none',
198
+ {validate: false});
138
199
  }
139
200
  }
140
201
  }
@@ -143,89 +204,73 @@ class MapFeatureLayer
143
204
 
144
205
  export class LayerManager
145
206
  {
146
- constructor(flatmap, switcher=false)
207
+ constructor(flatmap)
147
208
  {
148
209
  this.__flatmap = flatmap;
149
210
  this.__map = flatmap.map;
150
211
  this.__layers = new Map;
151
212
  this.__mapLayers = new Map;
152
- this.__activeLayers = [];
153
- this.__activeLayerNames = [];
154
- this.__rasterLayers = [];
155
213
  const layerOptions = flatmap.options.layerOptions;
156
- const fcDiagram = ('style' in layerOptions && layerOptions.style == 'fcdiagram');
157
214
  const backgroundLayer = new style.BackgroundLayer();
158
- if (fcDiagram) {
159
- this.__map.addLayer(backgroundLayer.style('black', 1));
160
- }
161
- else if ('background' in flatmap.options) {
215
+ if ('background' in flatmap.options) {
162
216
  this.__map.addLayer(backgroundLayer.style(flatmap.options.background));
163
217
  } else {
164
218
  this.__map.addLayer(backgroundLayer.style('white'));
165
219
  }
220
+
166
221
  // Add the map's layers
167
- if (fcDiagram && flatmap.details['image-layers']) {
222
+ if (flatmap.details['image-layers']) {
223
+ // Image layers are below all feature layers
224
+
225
+ const bodyLayer = flatmap.layers[0];
226
+
227
+ const rasterLayers = new MapRasterLayers(this.__flatmap, bodyLayer.id); // body layer if not FC??
228
+
168
229
  for (const layer of flatmap.layers) {
169
- for (const raster_layer_id of layer['image-layers']) {
170
- const rasterLayer = new style.RasterLayer(raster_layer_id);
171
- this.__map.addLayer(rasterLayer.style(layerOptions));
172
- this.__rasterLayers.push(rasterLayer);
173
- }
230
+ rasterLayers.addLayer(layer);
174
231
  }
232
+ this.__layers.set(RASTER_LAYERS_NAME, {
233
+ id: RASTER_LAYERS_NAME,
234
+ description: RASTER_LAYERS_NAME
235
+ });
236
+ this.__mapLayers.set(RASTER_LAYERS_NAME, rasterLayers);
175
237
  }
176
238
  for (const layer of flatmap.layers) {
177
- this.addLayer(layer, !fcDiagram);
239
+ this.__addFeatureLayer(layer);
178
240
  }
179
241
  }
180
242
 
181
243
  get activeLayerNames()
182
244
  //====================
183
245
  {
184
- return this.__activeLayerNames;
246
+ const activeNames = [];
247
+ for (const mapLayer of this.__mapLayers.values()) {
248
+ if (mapLayer.active) {
249
+ activeNames.push(mapLayer.id);
250
+ }
251
+ }
252
+ return activeNames;
185
253
  }
186
254
 
187
- addLayer(layer, background_layers=true)
188
- //=====================================
255
+ __addFeatureLayer(layer)
256
+ //======================
189
257
  {
190
- this.__mapLayers.set(layer.id, layer);
191
-
192
- const layers = new MapFeatureLayer(this.__flatmap, layer, background_layers);
193
- const layerId = this.__flatmap.mapLayerId(layer.id);
194
- this.__layers.set(layerId, layers);
258
+ this.__layers.set(layer.id, layer);
259
+ this.__mapLayers.set(layer.id, new MapFeatureLayers(this.__flatmap, layer));
195
260
  }
196
261
 
197
262
  get layers()
198
263
  //==========
199
264
  {
200
- return this.__layers;
201
- }
202
-
203
- activate(layerId)
204
- //===============
205
- {
206
- const layer = this.__layers.get(layerId);
207
- if (layer !== undefined) {
208
- layer.activate();
209
- if (this.__activeLayers.indexOf(layer) < 0) {
210
- this.__activeLayers.push(layer);
211
- this.__activeLayerNames.push(layer.id);
212
- }
213
- }
265
+ return Array.from(this.__layers.values());
214
266
  }
215
267
 
216
- deactivate(layerId)
217
- //=================
268
+ activate(layerId, enable=true)
269
+ //============================
218
270
  {
219
- const layer = this.__layers.get(layerId);
220
- if (layer !== undefined) {
221
- layer.deactivate();
222
- const index = this.__activeLayers.indexOf(layer);
223
- if (index >= 0) {
224
- delete this.__activeLayers[index];
225
- this.__activeLayers.splice(index, 1);
226
- delete this.__activeLayerNames[index];
227
- this.__activeLayerNames.splice(index, 1);
228
- }
271
+ const mapLayer = this.__mapLayers.get(layerId);
272
+ if (mapLayer !== undefined) {
273
+ mapLayer.activate(enable);
229
274
  }
230
275
  }
231
276
 
@@ -233,35 +278,10 @@ export class LayerManager
233
278
  //=====================
234
279
  {
235
280
  options = utils.setDefaultOptions(options, {colour: true, outline: true});
236
- for (const layer of this.__layers.values()) {
237
- layer.setColour(options)
281
+ for (const mapLayer of this.__mapLayers.values()) {
282
+ mapLayer.setColour(options)
238
283
  }
239
284
  }
240
-
241
- makeUppermost(layerId)
242
- //====================
243
- {
244
- // position before top layer
245
- }
246
-
247
- makeLowest(layerId)
248
- //=================
249
- {
250
- // position after bottom layer (before == undefined)
251
- }
252
-
253
-
254
- lower(layerId)
255
- //============
256
- {
257
- // position before second layer underneath...
258
- }
259
-
260
- raise(layerId)
261
- //============
262
- {
263
- // position before layer above...
264
- }
265
285
  }
266
286
 
267
287
  //==============================================================================
package/src/main.js CHANGED
@@ -27,7 +27,7 @@ export { MapManager };
27
27
 
28
28
  //==============================================================================
29
29
 
30
- export async function standaloneViewer(map_endpoint=null, map_options={})
30
+ export async function standaloneViewer(map_endpoint=null, options={})
31
31
  {
32
32
  const requestUrl = new URL(window.location.href);
33
33
  if (map_endpoint == null) {
@@ -52,6 +52,18 @@ export async function standaloneViewer(map_endpoint=null, map_options={})
52
52
  });
53
53
 
54
54
  let currentMap = null;
55
+ let defaultBackground = 'black';
56
+
57
+ const mapOptions = Object.assign({
58
+ tooltips: true,
59
+ background: defaultBackground,
60
+ backgroundControl: true,
61
+ debug: false,
62
+ minimap: false,
63
+ searchable: true,
64
+ featureInfo: true,
65
+ showPosition: false
66
+ }, options);
55
67
 
56
68
  function loadMap(id, taxon, sex)
57
69
  //==============================
@@ -71,20 +83,15 @@ export async function standaloneViewer(map_endpoint=null, map_options={})
71
83
  }
72
84
  requestUrl.searchParams.delete('id');
73
85
  }
86
+
87
+ // Update address bar URL to current map
74
88
  window.history.pushState('data', document.title, requestUrl);
75
89
 
76
- const options = Object.assign({
77
- tooltips: true,
78
- background: '#EEF',
79
- debug: false,
80
- minimap: false,
81
- navigationControl: 'top-right',
82
- searchable: true,
83
- featureInfo: true,
84
- showPosition: false
85
- }, map_options);
86
-
87
- mapManager.loadMap(id, 'map-canvas', (...args) => console.log(...args), options)
90
+ mapManager.loadMap(id, 'map-canvas', (eventType, ...args) => {
91
+ if (args[0].type === 'control' && args[0].control === 'background') {
92
+ mapOptions.background = args[0].value;
93
+ }
94
+ }, mapOptions)
88
95
  .then(map => {
89
96
  map.addMarker('UBERON:0000948'); // Heart
90
97
  map.addMarker('UBERON:0002048'); // Lung
@@ -129,7 +136,7 @@ export async function standaloneViewer(map_endpoint=null, map_options={})
129
136
  let mapId = null;
130
137
  let mapTaxon = null;
131
138
  let mapSex = null;
132
- const options = [];
139
+ const mapList = [];
133
140
  for (const [name, map] of sortedMaps.entries()) {
134
141
  const text = [ name, map.created ];
135
142
  let selected = '';
@@ -145,12 +152,12 @@ export async function standaloneViewer(map_endpoint=null, map_options={})
145
152
  mapSex = viewMapSex;
146
153
  selected = 'selected';
147
154
  }
148
- options.push(`<option value="${id}" ${selected}>${text.join(' -- ')}</option>`);
155
+ mapList.push(`<option value="${id}" ${selected}>${text.join(' -- ')}</option>`);
149
156
  }
150
- options.splice(0, 0, '<option value="">Select flatmap...</option>');
157
+ mapList.splice(0, 0, '<option value="">Select flatmap...</option>');
151
158
 
152
159
  const selector = document.getElementById('map-selector');
153
- selector.innerHTML = options.join('');
160
+ selector.innerHTML = mapList.join('');
154
161
  selector.onchange = (e) => {
155
162
  if (e.target.value !== '') {
156
163
  loadMap(e.target.value);
package/src/search.js CHANGED
@@ -158,7 +158,10 @@ export class SearchIndex
158
158
 
159
159
  addTerm_(featureId, text)
160
160
  //=======================
161
- { if (text) {
161
+ {
162
+ text = text.replace(new RegExp('<br/>', 'g'), ' ')
163
+ .replace('\n', ' ');
164
+ if (text) {
162
165
  this._searchEngine.add({
163
166
  id: this._featureIds.length,
164
167
  text: text
package/src/styling.js CHANGED
@@ -226,8 +226,7 @@ export class FeatureBorderLayer extends VectorStyleLayer
226
226
  'type': 'line',
227
227
  'filter': [
228
228
  'all',
229
- ['==', '$type', 'Polygon'],
230
- ['has', 'id']
229
+ ['==', '$type', 'Polygon']
231
230
  ],
232
231
  'paint': this.paintStyle(options)
233
232
  };
@@ -268,7 +267,7 @@ export class FeatureLineLayer extends VectorStyleLayer
268
267
  ['boolean', ['feature-state', 'active'], false], coloured ? '#888' : '#CCC',
269
268
  ['==', ['get', 'type'], 'network'], '#AFA202',
270
269
  ['has', 'centreline'], '#888',
271
- ('style' in options && options.style === 'authoring') ? '#C44' : '#444'
270
+ options.authoring ? '#C44' : '#444'
272
271
  ],
273
272
  'line-opacity': [
274
273
  'case',
@@ -284,7 +283,7 @@ export class FeatureLineLayer extends VectorStyleLayer
284
283
  ['==', ['get', 'type'], 'network'], 1.2,
285
284
  ['boolean', ['feature-state', 'selected'], false], 1.2,
286
285
  ['boolean', ['feature-state', 'active'], false], 1.2,
287
- ('style' in options && options.style === 'authoring') ? 0.7 : 0.5
286
+ options.authoring ? 0.7 : 0.5
288
287
  ], [
289
288
  'interpolate',
290
289
  ['exponential', 2],
@@ -357,6 +356,7 @@ export class PathLineLayer extends VectorStyleLayer
357
356
  const paintStyle = {
358
357
  'line-color': [
359
358
  'case',
359
+ ['boolean', ['feature-state', 'selected'], false], '#0F0',
360
360
  ['boolean', ['feature-state', 'hidden'], false], '#CCC',
361
361
  ['==', ['get', 'type'], 'bezier'], 'red',
362
362
  ['==', ['get', 'kind'], 'error'], '#FFFE0E',
@@ -683,7 +683,7 @@ export class BackgroundLayer
683
683
  return this.__id;
684
684
  }
685
685
 
686
- style(backgroundColour, opacity=0.1)
686
+ style(backgroundColour, opacity=1.0)
687
687
  {
688
688
  return {
689
689
  'id': this.__id,
@@ -51,6 +51,10 @@ li.flatmap-contextmenu-item:hover {
51
51
  margin-top: 2px;
52
52
  }
53
53
 
54
+ .text-button {
55
+ width: 64px;
56
+ }
57
+
54
58
  /* Search box */
55
59
 
56
60
  #search-control-input {
@@ -176,7 +180,7 @@ li.flatmap-contextmenu-item:hover {
176
180
  .flatmap-nerve-grid {
177
181
  margin-top: 10px;
178
182
  display: grid;
179
- grid-template-columns: 3fr 0.2fr 0.8fr;
183
+ grid-template-columns: 3fr 0.8fr 0.2fr;
180
184
  column-gap: 10px;
181
185
  row-gap: 0.2em;
182
186
  width: 300px;
@@ -226,6 +230,32 @@ label[for=path-all-paths] {
226
230
  background: repeating-linear-gradient(to right,#EA3423 0,#EA3423 6px,transparent 6px,transparent 9px)
227
231
  }
228
232
 
233
+ /* Layer control */
234
+
235
+ #flatmap-layer-control {
236
+ text-align: right;
237
+ }
238
+ .flatmap-layer-grid {
239
+ margin-top: 10px;
240
+ display: grid;
241
+ grid-template-columns: 3.8fr 0.2fr;
242
+ column-gap: 10px;
243
+ row-gap: 0.2em;
244
+ width: 300px;
245
+ font-size: 10pt;
246
+ cursor: pointer;
247
+ text-align: left;
248
+ background: white;
249
+ border: 1px solid gray;
250
+ padding: 4px;
251
+ opacity: 0.8;
252
+ }
253
+ .flatmap-layer-grid input {
254
+ height: 1.1em;
255
+ }
256
+ label[for=layer-all-layers] {
257
+ font-weight: bold;
258
+ }
229
259
 
230
260
  /* Markers */
231
261