@abi-software/flatmap-viewer 2.2.12-b.3 → 2.2.13-b.1

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.3``
41
+ * ``npm install @abi-software/flatmap-viewer@2.2.13-b.1``
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.3",
3
+ "version": "2.2.13-b.1",
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,24 +797,40 @@ 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
  /**
803
817
  * Add a marker to the map.
804
818
  *
805
- * @param {string} anatomicalId The anatomical identifier of the feature on which
806
- * to place the marker
807
- * @param {string} [htmlElement=null] An optional parameter giving the DOM element to
808
- * use as a marker. The default is a light blue,
809
- * droplet-shaped SVG marker.
810
- * @return {integer} The identifier for the resulting marker. -1 is returned if the
811
- * map doesn't contain a feature with the given anatomical identifier
819
+ * @param {string} anatomicalId The anatomical identifier of the feature on which
820
+ * to place the marker.
821
+ * @arg {Object} options Configurable options for the marker.
822
+ * @arg {string} options.colour Colour of the default marker. Defaults to ``'#005974'``
823
+ * (dark blue).
824
+ * @arg {string} options.element The DOM element to use as a marker. The default is
825
+ * a dark blue droplet-shaped SVG marker.
826
+ * @return {integer} The identifier for the resulting marker. -1 is returned if the
827
+ * map doesn't contain a feature with the given anatomical identifier
812
828
  */
813
- addMarker(anatomicalId, htmlElement=null)
814
- //========================================
829
+ addMarker(anatomicalId, options={})
830
+ //==================================
815
831
  {
816
832
  if (this._userInteractions !== null) {
817
- return this._userInteractions.addMarker(anatomicalId, htmlElement);
833
+ return this._userInteractions.addMarker(anatomicalId, options);
818
834
  }
819
835
  return -1;
820
836
  }
@@ -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,48 +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
- // Display a context menu on right-click
197
-
198
- this._lastContextTime = 0;
199
- this._contextMenu = new ContextMenu(flatmap, this.__clearModal.bind(this));
200
- this._map.on('contextmenu', this.contextMenuEvent_.bind(this));
201
-
202
- // Display a context menu with a touch longer than 0.5 second
203
-
204
- this._lastTouchTime = 0;
205
- this._map.on('touchstart', (e) => { this._lastTouchTime = Date.now(); });
206
- this._map.on('touchend', (e) => {
207
- if (Date.now() > (this._lastTouchTime + 500)) {
208
- this.contextMenuEvent_(e);
209
- }
210
- });
211
-
212
201
  // Handle mouse events
213
202
 
214
203
  this._map.on('click', this.clickEvent_.bind(this));
@@ -284,7 +273,61 @@ export class UserInteractions
284
273
  getSystems()
285
274
  //==========
286
275
  {
287
- 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
+ }
288
331
  }
289
332
 
290
333
  mapFeature_(featureId)
@@ -297,9 +340,11 @@ export class UserInteractions
297
340
  source: VECTOR_TILES_SOURCE,
298
341
  sourceLayer: this._flatmap.options.separateLayers
299
342
  ? `${ann['layer']}_${ann['tile-layer']}`
300
- : ann['tile-layer']
343
+ : ann['tile-layer'],
344
+ children: ann.children || []
301
345
  };
302
346
  }
347
+ return undefined;
303
348
  }
304
349
 
305
350
  featureSelected_(featureId)
@@ -424,45 +469,6 @@ export class UserInteractions
424
469
  return smallestFeature;
425
470
  }
426
471
 
427
- contextMenuEvent_(event)
428
- //======================
429
- {
430
- event.preventDefault();
431
-
432
- // Chrome on Android sends both touch and contextmenu events
433
- // so ignore duplicate
434
-
435
- if (Date.now() < (this._lastContextTime + 100)) {
436
- return;
437
- }
438
- this._lastContextTime = Date.now();
439
-
440
- if (this._activeFeatures.length > 0) {
441
- const feature = this._activeFeatures[0];
442
-
443
- // Remove any tooltip
444
- this.removeTooltip_();
445
-
446
- const featureId = feature.id;
447
- if (this._pathways.isNode(featureId)) {
448
- const items = [
449
- {
450
- featureId: featureId,
451
- prompt: 'Show paths',
452
- action: this.enablePaths_.bind(this, true)
453
- },
454
- {
455
- featureId: featureId,
456
- prompt: 'Hide paths',
457
- action: this.enablePaths_.bind(this, false)
458
- }
459
- ];
460
- this.setModal_();
461
- this._contextMenu.show(event.lngLat, items, feature.properties.label);
462
- }
463
- }
464
- }
465
-
466
472
  setModal_(event)
467
473
  //==============
468
474
  {
@@ -982,7 +988,6 @@ export class UserInteractions
982
988
  enablePaths_(enable, event)
983
989
  //=========================
984
990
  {
985
- this._contextMenu.hide();
986
991
  const nodeId = event.target.getAttribute('featureId');
987
992
  this.enablePathFeatures_(enable, this._pathways.pathFeatureIds(nodeId));
988
993
  this.__clearModal();
@@ -1086,8 +1091,8 @@ export class UserInteractions
1086
1091
  return position;
1087
1092
  }
1088
1093
 
1089
- addMarker(anatomicalId, htmlElement=null)
1090
- //=======================================
1094
+ addMarker(anatomicalId, options={})
1095
+ //=================================
1091
1096
  {
1092
1097
  const featureIds = this._flatmap.modelFeatureIds(anatomicalId);
1093
1098
  let markerId = -1;
@@ -1106,13 +1111,15 @@ export class UserInteractions
1106
1111
  // MapLibre dynamically sets a transform on marker elements so in
1107
1112
  // order to apply a scale transform we need to create marker icons
1108
1113
  // inside the marker container <div>.
1109
- const markerHTML = htmlElement ? new maplibre.Marker({element: htmlElement})
1110
- : new maplibre.Marker({color: '#005974'});
1114
+ const colour = options.colour || '#005974';
1115
+ const markerHTML = options.element ? new maplibre.Marker({element: options.element})
1116
+ : new maplibre.Marker({color: colour});
1111
1117
 
1112
1118
  const markerElement = document.createElement('div');
1113
1119
  const markerIcon = document.createElement('div');
1114
1120
  markerIcon.innerHTML = markerHTML.getElement().innerHTML;
1115
1121
  markerIcon.className = 'flatmap-marker';
1122
+ markerElement.id = `marker-${markerId}`;
1116
1123
  markerElement.appendChild(markerIcon);
1117
1124
 
1118
1125
  const markerPosition = this.__markerPosition(featureId, annotation);
@@ -1129,6 +1136,7 @@ export class UserInteractions
1129
1136
  this.markerMouseEvent_.bind(this, marker, anatomicalId));
1130
1137
 
1131
1138
  this.__markerIdByMarker.set(marker, markerId);
1139
+ this.__markerIdByFeatureId.set(+featureId, markerId);
1132
1140
  this.__annotationByMarkerId.set(markerId, annotation);
1133
1141
  }
1134
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/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
+ }