@abi-software/flatmap-viewer 2.2.12-b.4 → 2.2.13-b.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.2.12-b.4``
41
+ * ``npm install @abi-software/flatmap-viewer@2.2.13-b.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.2.12-b.4",
3
+ "version": "2.2.13-b.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",
package/src/controls.js CHANGED
@@ -324,6 +324,123 @@ export class LayerControl
324
324
 
325
325
  //==============================================================================
326
326
 
327
+ export class Control
328
+ {
329
+ constructor(flatmap, id, name)
330
+ {
331
+ this.__flatmap = flatmap;
332
+ this.__id = id;
333
+ this.__name = name;
334
+ this.__map = undefined;
335
+ this.__prefix = `${this.__id}-`
336
+ }
337
+
338
+ getDefaultPosition()
339
+ //==================
340
+ {
341
+ return 'top-right';
342
+ }
343
+
344
+ __innerLinesHTML()
345
+ //================
346
+ {
347
+ return [];
348
+ }
349
+
350
+ __enableAll(enable)
351
+ //=================
352
+ {
353
+
354
+ }
355
+
356
+ onAdd(map)
357
+ //========
358
+ {
359
+ this.__map = map;
360
+ this.__container = document.createElement('div');
361
+ this.__container.className = 'maplibregl-ctrl flatmap-control';
362
+ this.__control = document.createElement('div');
363
+ this.__control.className = 'flatmap-control-grid';
364
+
365
+ const innerHTML = this.__innerLinesHTML();
366
+ this.__totalCount = innerHTML.length;
367
+ innerHTML.splice(0, 0, `<label for="control-all-${this.__id}">ALL ${this.__name.toUpperCase()}:</label><input id="control-all-${this.__id}" type="checkbox" checked/>`);
368
+ this.__control.innerHTML = innerHTML.join('\n');
369
+
370
+ this.__checkedCount = this.__totalCount;
371
+ this.__halfCount = Math.trunc(this.__checkedCount/2);
372
+
373
+ this.__button = document.createElement('button');
374
+ this.__button.id = `flatmap-${this.__id}-button`;
375
+ this.__button.className = 'control-button text-button';
376
+ this.__button.setAttribute('type', 'button');
377
+ this.__button.setAttribute('aria-label', `Show/hide map's ${this.__name}`);
378
+ this.__button.setAttribute('control-visible', 'false');
379
+ this.__button.textContent = this.__name.toUpperCase().substring(0, 6);
380
+ this.__button.title = `Show/hide map's ${this.__name}`;
381
+ this.__container.appendChild(this.__button);
382
+
383
+ this.__container.addEventListener('click', this.onClick_.bind(this));
384
+ return this.__container;
385
+ }
386
+
387
+ onRemove()
388
+ //========
389
+ {
390
+ this.__container.parentNode.removeChild(this.__container);
391
+ this.__map = undefined;
392
+ }
393
+
394
+ onClick_(event)
395
+ //=============
396
+ {
397
+ if (event.target.id === `flatmap-${this.__id}-button`) {
398
+ if (this.__button.getAttribute('control-visible') === 'false') {
399
+ this.__container.appendChild(this.__control);
400
+ this.__button.setAttribute('control-visible', 'true');
401
+ this.__control.focus();
402
+ } else {
403
+ this.__control = this.__container.removeChild(this.__control);
404
+ this.__button.setAttribute('control-visible', 'false');
405
+ }
406
+ } else if (event.target.tagName === 'INPUT') {
407
+ if (event.target.id === `control-all-${this.__id}`) {
408
+ if (event.target.indeterminate) {
409
+ event.target.checked = (this.__checkedCount >= this.__halfCount);
410
+ event.target.indeterminate = false;
411
+ }
412
+ if (event.target.checked) {
413
+ this.__checkedCount = this.__totalCount;
414
+ } else {
415
+ this.__checkedCount = 0;
416
+ }
417
+ this.__enableAll(event.target.checked);
418
+ } else if (event.target.id.startsWith(`${this.__id}-`)) {
419
+ this.__enableControl(event.target.id.substring(this.__prefix.length),
420
+ event.target.checked);
421
+ if (event.target.checked) {
422
+ this.__checkedCount += 1;
423
+ } else {
424
+ this.__checkedCount -= 1;
425
+ }
426
+ const allCheckbox = document.getElementById(`control-all-${this.__id}`);
427
+ if (this.__checkedCount === 0) {
428
+ allCheckbox.checked = false;
429
+ allCheckbox.indeterminate = false;
430
+ } else if (this.__checkedCount === this.__totalCount) {
431
+ allCheckbox.checked = true;
432
+ allCheckbox.indeterminate = false;
433
+ } else {
434
+ allCheckbox.indeterminate = true;
435
+ }
436
+ }
437
+ }
438
+ event.stopPropagation();
439
+ }
440
+ }
441
+
442
+ //==============================================================================
443
+
327
444
  const SCKAN_STATES = [
328
445
  {
329
446
  'id': 'VALID',
@@ -797,6 +797,20 @@ class FlatMap
797
797
  return this._userInteractions.getSystems();
798
798
  }
799
799
  }
800
+
801
+ /**
802
+ * @param {string} systemName The name of the system to enable
803
+ * @param {boolean} enable Show or hide the system. Defaults to ``true`` (show)
804
+ *
805
+ */
806
+ enableSystem(systemName, enable=true)
807
+ //===================================
808
+ {
809
+ if (this._userInteractions !== null) {
810
+ return this._userInteractions.enableSystem(systemName, enable);
811
+ }
812
+ }
813
+
800
814
  //==========================================================================
801
815
 
802
816
  /**
@@ -38,9 +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 {BackgroundControl, LayerControl, NerveControl, PathControl, SCKANControl} from './controls.js';
41
+ import {BackgroundControl, LayerControl, NerveControl,
42
+ PathControl, SCKANControl} from './controls.js';
42
43
  import {SearchControl} from './search.js';
43
44
  import {VECTOR_TILES_SOURCE} from './styling.js';
45
+ import {SystemsControl, SystemsManager} from './systems';
44
46
 
45
47
  import * as pathways from './pathways.js';
46
48
  import * as utils from './utils.js';
@@ -115,6 +117,7 @@ export class UserInteractions
115
117
  this.__activeMarker = null;
116
118
  this.__lastMarkerId = 900000;
117
119
  this.__markerIdByMarker = new Map();
120
+ this.__markerIdByFeatureId = new Map();
118
121
  this.__annotationByMarkerId = new Map();
119
122
 
120
123
  // Where to put labels and popups on a feature
@@ -142,6 +145,27 @@ export class UserInteractions
142
145
  }
143
146
  }
144
147
 
148
+ // Flag features that have annotations and note which are FC systems
149
+
150
+ this.__systems = new Map();
151
+ for (const [id, ann] of flatmap.annotations) {
152
+ const feature = this.mapFeature_(id);
153
+ if (feature !== undefined) {
154
+ this._map.setFeatureState(feature, { 'annotated': true });
155
+ }
156
+ if (ann['fc-class'] === 'fc-class:System') {
157
+ if (this.__systems.has(ann.name)) {
158
+ this.__systems.get(ann.name).featureIds.push(ann.featureId)
159
+ } else {
160
+ this.__systems.set(ann.name, {
161
+ id: ann.name.replaceAll(' ', '_'),
162
+ colour: ann.colour,
163
+ featureIds: [ ann.featureId ]
164
+ });
165
+ }
166
+ }
167
+ }
168
+
145
169
  // Add various controls when running standalone
146
170
 
147
171
  if (flatmap.options.standalone) {
@@ -167,32 +191,13 @@ export class UserInteractions
167
191
  this.enableCentrelines(false);
168
192
  }
169
193
 
170
- // A SCKAN path control for FC maps
194
+ // SCKAN path and SYSTEMS controls for FC maps
171
195
  if (flatmap.options.style === 'functional') {
196
+ this._map.addControl(new SystemsControl(flatmap, this.__systems));
172
197
  this._map.addControl(new SCKANControl(flatmap, flatmap.options.layerOptions));
173
198
  }
174
199
  }
175
200
 
176
- // Flag features that have annotations and not which are FC systems
177
-
178
- this.__systems = [];
179
- const seenSystems = [];
180
- for (const [id, ann] of flatmap.annotations) {
181
- const feature = this.mapFeature_(id);
182
- if (feature !== undefined) {
183
- this._map.setFeatureState(feature, { 'annotated': true });
184
- }
185
- if (ann['fc-class'] === 'FC_CLASS.SYSTEM') {
186
- if (seenSystems.indexOf(ann['name']) < 0) {
187
- seenSystems.push(ann['name']);
188
- this.__systems.push({
189
- name: ann['name'],
190
- colour: ann['colour']
191
- });
192
- }
193
- }
194
- }
195
-
196
201
  // Handle mouse events
197
202
 
198
203
  this._map.on('click', this.clickEvent_.bind(this));
@@ -268,7 +273,61 @@ export class UserInteractions
268
273
  getSystems()
269
274
  //==========
270
275
  {
271
- return this.__systems;
276
+ const systems = [];
277
+ for (const system of this.__systems.values()) {
278
+ systems.push({
279
+ name: system.name,
280
+ colour: system.colour,
281
+ });
282
+ }
283
+ return systems;
284
+ }
285
+
286
+ enableSystem(systemName, enable=true)
287
+ //===================================
288
+ {
289
+ if (this.__systems.has(systemName)) {
290
+ for (const featureId of this.__systems.get(systemName).featureIds) {
291
+ this.__enableFeatureWithChildren(featureId, enable);
292
+ }
293
+ }
294
+ }
295
+
296
+ __enableFeatureWithChildren(featureId, enable=true)
297
+ //=================================================
298
+ {
299
+ const feature = this.mapFeature_(featureId);
300
+ if (feature !== undefined) {
301
+ this.__enableFeature(feature, enable);
302
+ for (const childFeatureId of feature.children) {
303
+ this.__enableFeatureWithChildren(childFeatureId, enable);
304
+ }
305
+ }
306
+ }
307
+
308
+ __enableFeatureMarker(featureId, enable=true)
309
+ //===========================================
310
+ {
311
+ const markerId = this.__markerIdByFeatureId.get(+featureId);
312
+ if (markerId !== undefined) {
313
+ const markerDiv = document.getElementById(`marker-${markerId}`);
314
+ if (markerDiv) {
315
+ markerDiv.style.visibility = enable ? 'visible' : 'hidden';
316
+ }
317
+ }
318
+ }
319
+
320
+ __enableFeature(feature, enable=true)
321
+ //===================================
322
+ {
323
+ if (feature !== undefined) {
324
+ if (enable) {
325
+ this._map.removeFeatureState(feature, 'hidden');
326
+ } else {
327
+ this._map.setFeatureState(feature, { 'hidden': true });
328
+ }
329
+ this.__enableFeatureMarker(feature.id, enable);
330
+ }
272
331
  }
273
332
 
274
333
  mapFeature_(featureId)
@@ -281,9 +340,11 @@ export class UserInteractions
281
340
  source: VECTOR_TILES_SOURCE,
282
341
  sourceLayer: this._flatmap.options.separateLayers
283
342
  ? `${ann['layer']}_${ann['tile-layer']}`
284
- : ann['tile-layer']
343
+ : ann['tile-layer'],
344
+ children: ann.children || []
285
345
  };
286
346
  }
347
+ return undefined;
287
348
  }
288
349
 
289
350
  featureSelected_(featureId)
@@ -1058,6 +1119,7 @@ export class UserInteractions
1058
1119
  const markerIcon = document.createElement('div');
1059
1120
  markerIcon.innerHTML = markerHTML.getElement().innerHTML;
1060
1121
  markerIcon.className = 'flatmap-marker';
1122
+ markerElement.id = `marker-${markerId}`;
1061
1123
  markerElement.appendChild(markerIcon);
1062
1124
 
1063
1125
  const markerPosition = this.__markerPosition(featureId, annotation);
@@ -1074,6 +1136,7 @@ export class UserInteractions
1074
1136
  this.markerMouseEvent_.bind(this, marker, anatomicalId));
1075
1137
 
1076
1138
  this.__markerIdByMarker.set(marker, markerId);
1139
+ this.__markerIdByFeatureId.set(+featureId, markerId);
1077
1140
  this.__annotationByMarkerId.set(markerId, annotation);
1078
1141
  }
1079
1142
  }
package/src/layers.js CHANGED
@@ -65,7 +65,7 @@ class MapStylingLayers
65
65
  }
66
66
 
67
67
  addLayer(styleLayer, options)
68
- //==========================
68
+ //===========================
69
69
  {
70
70
  this.__map.addLayer(styleLayer.style(options));
71
71
  this.__layers.push(styleLayer);
@@ -89,8 +89,8 @@ class MapStylingLayers
89
89
  vectorSourceId(sourceLayer)
90
90
  //=========================
91
91
  {
92
- return this.__separateLayers ? `${this.__id}_${sourceLayer}`
93
- : sourceLayer;
92
+ return (this.__separateLayers ? `${this.__id}_${sourceLayer}`
93
+ : sourceLayer).replaceAll('/', '_');
94
94
  }
95
95
 
96
96
  setPaint(options)
@@ -144,8 +144,7 @@ class MapFeatureLayers extends MapStylingLayers
144
144
  {
145
145
  const styleLayer = new styleClass(`${this.__id}_${sourceLayer}`,
146
146
  this.vectorSourceId(sourceLayer));
147
- this.__map.addLayer(styleLayer.style(this.__layerOptions));
148
- this.__layers.push(styleLayer);
147
+ this.addLayer(styleLayer, this.__layerOptions);
149
148
  }
150
149
 
151
150
  __addPathwayStyleLayers()
package/src/pathways.js CHANGED
@@ -32,6 +32,7 @@ const PATH_TYPES = [
32
32
  { type: "para-pre", label: "Parasympathetic pre-ganglionic", colour: "#3F8F4A"},
33
33
  { type: "para-post", label: "Parasympathetic post-ganglionic", colour: "#3F8F4A", dashed: true},
34
34
  { type: "sensory", label: "Sensory (afferent) neuron", colour: "#2A62F6"},
35
+ { type: "motor", label: "Somatic lower motor", colour: "#98561D"},
35
36
  { type: "somatic", label: "Somatic lower motor", colour: "#98561D"},
36
37
  { type: "symp-pre", label: "Sympathetic pre-ganglionic", colour: "#EA3423"},
37
38
  { type: "symp-post", label: "Sympathetic post-ganglionic", colour: "#EA3423", dashed: true},
package/src/styling.js CHANGED
@@ -123,31 +123,21 @@ export class FeatureFillLayer extends VectorStyleLayer
123
123
  {
124
124
  const coloured = !('colour' in options) || options.colour;
125
125
  const dimmed = 'dimmed' in options && options.dimmed;
126
- const activeRasterLayer = 'activeRasterLayer' in options && options.activeRasterLayer;
127
126
  const paintStyle = {
128
127
  'fill-color': [
129
128
  'case',
130
129
  ['boolean', ['feature-state', 'selected'], false], '#0F0',
131
130
  ['has', 'colour'], ['get', 'colour'],
132
131
  ['boolean', ['feature-state', 'active'], false], coloured ? '#D88' : '#CCC',
133
- ['any',
134
- ['==', ['get', 'kind'], 'scaffold']
135
- ], 'white',
136
- ['has', 'node'], '#AFA202',
137
132
  'white' // background colour? body colour ??
138
133
  ],
139
134
  'fill-opacity': [
140
135
  'case',
136
+ ['boolean', ['feature-state', 'hidden'], false], 0.01,
141
137
  ['boolean', ['feature-state', 'selected'], false], 0.7,
142
138
  ['has', 'opacity'], ['get', 'opacity'],
143
139
  ['has', 'colour'], 1.0,
144
140
  ['boolean', ['feature-state', 'active'], false], 0.7,
145
- ['has', 'node'], 0.3,
146
- ['any',
147
- ['==', ['get', 'kind'], 'scaffold'],
148
- ['==', ['get', 'kind'], 'tissue'],
149
- ['==', ['get', 'kind'], 'cell-type'],
150
- ], 0.1,
151
141
  (coloured && !dimmed) ? 0.01 : 0.1
152
142
  ]
153
143
  };
@@ -196,14 +186,12 @@ export class FeatureBorderLayer extends VectorStyleLayer
196
186
  lineColour.push('blue');
197
187
  }
198
188
  lineColour.push(['has', 'colour']);
199
- lineColour.push('#000');
200
- lineColour.push(['has', 'node']);
201
- lineColour.push('#AFA202');
189
+ lineColour.push(['get', 'colour']);
202
190
  lineColour.push('#444');
203
191
 
204
192
  const lineOpacity = [
205
193
  'case',
206
- ['boolean', ['get', 'invisible'], false], 0.05,
194
+ ['boolean', ['feature-state', 'hidden'], false], 0.05,
207
195
  ];
208
196
  if (coloured && outlined) {
209
197
  lineOpacity.push(['boolean', ['feature-state', 'active'], false]);
package/src/systems.js ADDED
@@ -0,0 +1,76 @@
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
+ import { Control } from './controls';
23
+
24
+ //==============================================================================
25
+
26
+ export class SystemsManager
27
+ {
28
+ constructor()
29
+ {
30
+
31
+ }
32
+ }
33
+
34
+ //==============================================================================
35
+
36
+ export class SystemsControl extends Control
37
+ {
38
+ constructor(flatmap, systems)
39
+ {
40
+ super(flatmap, 'system', 'systems');
41
+ this.__systems = systems;
42
+ }
43
+
44
+ __innerLinesHTML()
45
+ //================
46
+ {
47
+ const html = [];
48
+ for (const [name, system] of this.__systems.entries()) {
49
+ html.push(`<label for="${this.__prefix}${system.id}" style="background: ${system.colour};">${name}</label><input id="${this.__prefix}${system.id}" type="checkbox" checked/>`);
50
+ }
51
+ return html;
52
+ }
53
+
54
+ __enableAll(enable)
55
+ //=================
56
+ {
57
+ for (const [name, system] of this.__systems.entries()) {
58
+ const checkbox = document.getElementById(`${this.__prefix}${system.id}`);
59
+ if (checkbox) {
60
+ checkbox.checked = enable;
61
+ this.__flatmap.enableSystem(name, enable);
62
+ }
63
+ }
64
+ }
65
+
66
+ __enableControl(id, enable)
67
+ //=========================
68
+ {
69
+ for (const [name, system] of this.__systems.entries()) {
70
+ if (id === system.id) {
71
+ this.__flatmap.enableSystem(name, enable);
72
+ }
73
+ }
74
+ }
75
+
76
+ }