@abi-software/flatmap-viewer 2.4.4 → 2.5.0-a.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.4.4``
41
+ * ``npm install @abi-software/flatmap-viewer@2.5.0-a.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.4.4",
3
+ "version": "2.5.0-a.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",
@@ -18,12 +18,17 @@
18
18
  "license": "MIT",
19
19
  "dependencies": {
20
20
  "@babel/runtime": "^7.10.4",
21
+ "@deck.gl/core": "^8.9.33",
22
+ "@deck.gl/extensions": "^8.9.34",
23
+ "@deck.gl/layers": "^8.9.33",
24
+ "@deck.gl/mapbox": "^8.9.33",
21
25
  "@fortawesome/fontawesome-free": "^6.4.0",
22
26
  "@turf/area": "^6.5.0",
23
27
  "@turf/bbox": "^6.5.0",
24
28
  "@turf/helpers": "^6.5.0",
25
29
  "@turf/projection": "^6.5.0",
26
30
  "bezier-js": "^6.1.0",
31
+ "colord": "^2.9.3",
27
32
  "html-es6cape": "^2.0.2",
28
33
  "maplibre-gl": ">=3.6.0",
29
34
  "mathjax-full": "^3.2.2",
@@ -0,0 +1,76 @@
1
+ /******************************************************************************
2
+
3
+ Flatmap viewer and annotation tool
4
+
5
+ Copyright (c) 2019 - 2024 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
+ export class Path3DControl
22
+ {
23
+ #button
24
+ #container
25
+ #map = null
26
+ #flatmap
27
+
28
+ constructor(flatmap)
29
+ {
30
+ this.#flatmap = flatmap
31
+ }
32
+
33
+ getDefaultPosition()
34
+ //==================
35
+ {
36
+ return 'top-right'
37
+ }
38
+
39
+ onAdd(map)
40
+ //========
41
+ {
42
+ this.#map = map
43
+ this.#container = document.createElement('div')
44
+ this.#container.className = 'maplibregl-ctrl'
45
+ this.#button = document.createElement('button')
46
+ this.#button.className = 'control-button text-button'
47
+ this.#button.setAttribute('type', 'button')
48
+ this.#button.setAttribute('aria-label', 'Show 3D paths')
49
+ this.#button.textContent = '3D'
50
+ this.#button.title = 'Show/hide 3D paths'
51
+ this.#container.appendChild(this.#button)
52
+ this.#container.addEventListener('click', this.onClick.bind(this))
53
+ return this.#container
54
+ }
55
+
56
+ onRemove()
57
+ //========
58
+ {
59
+ this.#container.parentNode.removeChild(this.#container)
60
+ this.#map = undefined
61
+ }
62
+
63
+ onClick(_event)
64
+ //=============
65
+ {
66
+ if (this.#button.classList.contains('control-active')) {
67
+ this.#flatmap.enable3dPaths(false)
68
+ this.#button.classList.remove('control-active')
69
+ } else {
70
+ this.#flatmap.enable3dPaths(true)
71
+ this.#button.classList.add('control-active')
72
+ }
73
+ }
74
+ }
75
+
76
+ //==============================================================================
@@ -170,8 +170,8 @@ class FlatMap
170
170
 
171
171
  // Disable map rotation
172
172
 
173
- this._map.dragRotate.disable();
174
- this._map.touchZoomRotate.disableRotation();
173
+ //this._map.dragRotate.disable();
174
+ //this._map.touchZoomRotate.disableRotation();
175
175
 
176
176
  // Add navigation controls if option set
177
177
 
@@ -898,6 +898,19 @@ class FlatMap
898
898
  }
899
899
  }
900
900
 
901
+ /**
902
+ * Show/hide 3D path view.
903
+ *
904
+ * @param {boolean} [enable=true]
905
+ */
906
+ enable3dPaths(enable=true)
907
+ //========================
908
+ {
909
+ if (this._userInteractions !== null) {
910
+ this._userInteractions.enable3dPaths(enable)
911
+ }
912
+ }
913
+
901
914
  //==========================================================================
902
915
 
903
916
  /**
@@ -35,13 +35,15 @@ import polylabel from 'polylabel';
35
35
 
36
36
  import {LayerManager} from './layers';
37
37
  import {PATHWAYS_LAYER, PathManager} from './pathways';
38
- import {VECTOR_TILES_SOURCE} from './styling';
38
+ import {VECTOR_TILES_SOURCE} from './layers/styling';
39
+ import {Paths3DLayer} from './layers/paths3d'
39
40
  import {SystemsManager} from './systems';
40
41
 
41
42
  import {displayedProperties, InfoControl} from './controls/info';
42
43
  import {BackgroundControl, LayerControl, NerveControl,
43
44
  SCKANControl} from './controls/controls';
44
45
  import {PathControl} from './controls/paths';
46
+ import {Path3DControl} from './controls/paths3d'
45
47
  import {SearchControl} from './controls/search';
46
48
  import {SystemsControl} from './controls/systems';
47
49
  import {TaxonsControl} from './controls/taxons';
@@ -120,6 +122,8 @@ function getRenderedLabel(properties)
120
122
 
121
123
  export class UserInteractions
122
124
  {
125
+ #paths3dLayer
126
+
123
127
  constructor(flatmap)
124
128
  {
125
129
  this._flatmap = flatmap;
@@ -183,6 +187,9 @@ export class UserInteractions
183
187
  // All taxons of connectivity paths are enabled by default
184
188
  this.__enabledConnectivityTaxons = new Set(this._flatmap.taxonIdentifiers);
185
189
 
190
+ // Support 3D path view
191
+ this.#paths3dLayer = new Paths3DLayer(flatmap, this)
192
+
186
193
  // Add various controls when running standalone
187
194
  if (flatmap.options.standalone) {
188
195
  // Add a control to search annotations if option set
@@ -214,6 +221,8 @@ export class UserInteractions
214
221
  // Connectivity taxon control for AC maps
215
222
  this._map.addControl(new TaxonsControl(flatmap));
216
223
  }
224
+
225
+ this._map.addControl(new Path3DControl(this));
217
226
  }
218
227
 
219
228
  // Handle mouse events
@@ -288,6 +297,12 @@ export class UserInteractions
288
297
  this._layerManager.activate(layerId, enable);
289
298
  }
290
299
 
300
+ enable3dPaths(enable=true)
301
+ //========================
302
+ {
303
+ this.#paths3dLayer.enable(enable)
304
+ }
305
+
291
306
  getSystems()
292
307
  //==========
293
308
  {
@@ -459,8 +474,8 @@ export class UserInteractions
459
474
  this._layerManager.setPaint({...this.__colourOptions, dimmed: false});
460
475
  }
461
476
 
462
- __activateFeature(feature)
463
- //========================
477
+ activateFeature(feature)
478
+ //======================
464
479
  {
465
480
  if (feature !== undefined) {
466
481
  this._map.setFeatureState(feature, { active: true });
@@ -515,7 +530,6 @@ export class UserInteractions
515
530
  this.__clearModal();
516
531
  this.__clearActiveMarker();
517
532
  this.unselectFeatures();
518
- this.__enablePathFeatures(this.__pathManager.allFeatureIds(), true);
519
533
  }
520
534
 
521
535
  clearSearchResults(reset=true)
@@ -809,7 +823,7 @@ export class UserInteractions
809
823
  let tooltip = '';
810
824
  if (displayInfo) {
811
825
  if (!('tooltip' in features[0].properties)) {
812
- this.__activateFeature(features[0]);
826
+ this.activateFeature(features[0]);
813
827
  }
814
828
  info = this._infoControl.featureInformation(features, event.lngLat);
815
829
  }
@@ -822,11 +836,11 @@ export class UserInteractions
822
836
  tooltipFeature = lineFeatures[0];
823
837
  for (const lineFeature of lineFeatures) {
824
838
  const lineFeatureId = +lineFeature.properties.featureId; // Ensure numeric
825
- this.__activateFeature(lineFeature);
839
+ this.activateFeature(lineFeature);
826
840
  const lineIds = new Set(lineFeatures.map(f => f.properties.featureId));
827
841
  for (const featureId of this.__pathManager.lineFeatureIds(lineIds)) {
828
842
  if (+featureId !== lineFeatureId) {
829
- this.__activateFeature(this.mapFeature(featureId));
843
+ this.activateFeature(this.mapFeature(featureId));
830
844
  }
831
845
  }
832
846
  }
@@ -876,7 +890,7 @@ export class UserInteractions
876
890
  info = `<div id="info-control-info">${htmlList.join('\n')}</div>`;
877
891
  }
878
892
  }
879
- this.__activateFeature(feature);
893
+ this.activateFeature(feature);
880
894
  this.__activateRelatedFeatures(feature);
881
895
  if ('hyperlink' in feature.properties) {
882
896
  this._map.getCanvas().style.cursor = 'pointer';
@@ -996,25 +1010,19 @@ export class UserInteractions
996
1010
  if ('nerveId' in feature.properties) {
997
1011
  const nerveId = feature.properties.nerveId;
998
1012
  if (nerveId !== feature.id) {
999
- this.__activateFeature(this.mapFeature(nerveId));
1013
+ this.activateFeature(this.mapFeature(nerveId));
1000
1014
  }
1001
1015
  for (const featureId of this.__pathManager.nerveFeatureIds(nerveId)) {
1002
- this.__activateFeature(this.mapFeature(featureId));
1016
+ this.activateFeature(this.mapFeature(featureId));
1003
1017
  }
1004
1018
  }
1005
1019
  if ('nodeId' in feature.properties) {
1006
1020
  for (const featureId of this.__pathManager.pathFeatureIds(feature.properties.nodeId)) {
1007
- this.__activateFeature(this.mapFeature(featureId));
1021
+ this.activateFeature(this.mapFeature(featureId));
1008
1022
  }
1009
1023
  }
1010
1024
  }
1011
1025
 
1012
- enablePath(pathId, enable=true)
1013
- //=============================
1014
- {
1015
- this.__pathManager.enablePath(pathId, enable);
1016
- }
1017
-
1018
1026
  enablePathsBySystem(system, enable=true, force=false)
1019
1027
  //===================================================
1020
1028
  {
@@ -1239,7 +1247,7 @@ export class UserInteractions
1239
1247
  if (event.type === 'mouseenter') {
1240
1248
  // Highlight on mouse enter
1241
1249
  this.resetActiveFeatures_();
1242
- this.__activateFeature(feature);
1250
+ this.activateFeature(feature);
1243
1251
  } else {
1244
1252
  this.selectionEvent_(event, feature)
1245
1253
  }
@@ -22,10 +22,10 @@ limitations under the License.
22
22
 
23
23
  //==============================================================================
24
24
 
25
- import {PATHWAYS_LAYER} from './pathways.js';
25
+ import {PATHWAYS_LAYER} from '../pathways.js';
26
+ import * as utils from '../utils.js';
26
27
 
27
28
  import * as style from './styling.js';
28
- import * as utils from './utils.js';
29
29
 
30
30
  const FEATURES_LAYER = 'features';
31
31
  const RASTER_LAYERS_NAME = 'Background image layer';
@@ -0,0 +1,170 @@
1
+ /******************************************************************************
2
+
3
+ Flatmap viewer and annotation tool
4
+
5
+ Copyright (c) 2019 - 2024 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
+ import {colord} from 'colord'
22
+ import {ArcLayer} from '@deck.gl/layers'
23
+ import {MapboxOverlay as DeckOverlay} from '@deck.gl/mapbox'
24
+ import {PathStyleExtension} from '@deck.gl/extensions'
25
+
26
+ //==============================================================================
27
+
28
+ import {pathColour} from '../pathways'
29
+
30
+ // Should this be in `pathways.js` ??
31
+ function pathColourRGB(pathType, alpha=255)
32
+ //=========================================
33
+ {
34
+ const rgb = colord(pathColour(pathType)).toRgb()
35
+ return [rgb.r, rgb.g, rgb.b, alpha]
36
+ }
37
+
38
+ //==============================================================================
39
+
40
+ /*
41
+ TODO:
42
+
43
+ * Dashed paths
44
+ * Control by SCKAN status
45
+ * Control by taxon visibility
46
+ * Control by visible layer
47
+
48
+
49
+ Also see https://alasarr.github.io/deck.gl/docs/api-reference/core/layer#updatetriggers
50
+
51
+
52
+ {
53
+ "featureId": 37,
54
+ "id": "ilxtr_neuron-type-keast-10",
55
+ "kind": "sensory",
56
+ "label": "L6-S1 sensory neuron innervating bladder",
57
+ "models": "ilxtr:neuron-type-keast-10",
58
+ "sckan": true,
59
+ "source": "https://apinatomy.org/uris/models/keast-bladder",
60
+ "taxons": ["NCBITaxon:10116"],
61
+ "tile-layer": "pathways",
62
+ "type": "line",
63
+ "bounds": [1.5454426659162825, -1.6254174813389017, 1.7478459571498208, -1.3632864333949712],
64
+ "markerPosition": [1.6466443115330516, -1.4943519573669364],
65
+ "geometry": "LineString",
66
+ "layer": "neural-routes",
67
+ "pathStartPosition": [1.7478459571498208, -1.3632864333949712],
68
+ "pathEndPosition": [1.5454426659162825, -1.6254174813389017]
69
+ }
70
+ */
71
+
72
+ //==============================================================================
73
+
74
+ export class Paths3DLayer
75
+ {
76
+ #deckOverlay = null
77
+ #enabled = false
78
+ #map
79
+ #pathData
80
+ #pathManager
81
+ #ui
82
+
83
+ constructor(flatmap, ui)
84
+ {
85
+ this.#ui = ui
86
+ this.#map = flatmap.map
87
+ this.#pathManager = ui.pathManager
88
+ this.#pathManager.addWatcher(this.#pathStateChanged.bind(this))
89
+ this.#pathData = [...flatmap.annotations.values()]
90
+ .filter(ann => ann['tile-layer'] === 'pathways'
91
+ && ann['geometry'] === 'LineString'
92
+ && 'type' in ann && ann['type'].startsWith('line')
93
+ && 'kind' in ann // && !ann['kind'].includes('arterial') && !ann['kind'].includes('venous')
94
+ && 'pathStartPosition' in ann
95
+ && 'pathEndPosition' in ann)
96
+ }
97
+
98
+ enable(enable=true)
99
+ //=================
100
+ {
101
+ if (enable && !this.#enabled) {
102
+ this.#setDeckOverlay()
103
+ this.#map.addControl(this.#deckOverlay)
104
+ } else if (!enable && this.#enabled) {
105
+ if (this.#deckOverlay) {
106
+ this.#map.removeControl(this.#deckOverlay)
107
+ this.#deckOverlay = null
108
+ }
109
+ }
110
+ this.#enabled = enable
111
+ }
112
+
113
+ #pathStateChanged()
114
+ //=================
115
+ {
116
+ if (this.#deckOverlay) {
117
+ this.#map.removeControl(this.#deckOverlay)
118
+ this.#setDeckOverlay()
119
+ this.#map.addControl(this.#deckOverlay)
120
+ }
121
+ }
122
+
123
+ #setDeckOverlay()
124
+ //===============
125
+ {
126
+ this.#deckOverlay = new DeckOverlay({
127
+ layers: [
128
+ // Need to have two layers, one with dashed lines, one without
129
+ //
130
+ // Better, one layer per pathType and set/clear layer.visible...
131
+ //
132
+ new ArcLayer({
133
+ id: 'arcs',
134
+ data: this.#pathData
135
+ .filter(f => this.#pathManager.pathTypeEnabled(f.kind)),
136
+ pickable: true,
137
+ autoHighlight: true,
138
+ numSegments: 100,
139
+ onHover: (i, e) => {
140
+ //console.log('hover', i, e)
141
+ if (i.object) {
142
+ const lineFeatureId = +i.object.featureId
143
+ this.#ui.activateFeature(this.#ui.mapFeature(lineFeatureId))
144
+ for (const featureId of this.#pathManager.lineFeatureIds([lineFeatureId])) {
145
+ if (+featureId !== lineFeatureId) {
146
+ this.#ui.activateFeature(this.#ui.mapFeature(featureId))
147
+ }
148
+ }
149
+ }
150
+ },
151
+ onClick: (i, e) => {
152
+ console.log('click', i, e)
153
+ },
154
+ // Styles
155
+ getSourcePosition: f => f.pathStartPosition,
156
+ getTargetPosition: f => f.pathEndPosition,
157
+ getSourceColor: f => pathColourRGB(f.kind, 160),
158
+ getTargetColor: f => pathColourRGB(f.kind, 160),
159
+ highlightColor: o => pathColourRGB(o.object.kind),
160
+ getWidth: 3,
161
+ extensions: [new PathStyleExtension({dash: true})],
162
+ getDashArray: [3, 2],
163
+ dashJustified: true,
164
+ dashGapPickable: true,
165
+ })
166
+ ],
167
+ getTooltip: ({object}) => object && object.label
168
+ })
169
+ }
170
+ }
@@ -26,8 +26,8 @@ export const VECTOR_TILES_SOURCE = 'vector-tiles';
26
26
 
27
27
  //==============================================================================
28
28
 
29
- import {UNCLASSIFIED_TAXON_ID} from './flatmap-viewer';
30
- import {PATH_STYLE_RULES} from './pathways';
29
+ import {UNCLASSIFIED_TAXON_ID} from '../flatmap-viewer';
30
+ import {PATH_STYLE_RULES} from '../pathways';
31
31
 
32
32
  //==============================================================================
33
33
 
package/src/pathways.js CHANGED
@@ -52,6 +52,14 @@ const PATH_TYPES = [
52
52
  export const PATH_STYLE_RULES =
53
53
  PATH_TYPES.flatMap(pathType => [['==', ['get', 'kind'], pathType.type], pathType.colour]);
54
54
 
55
+ export const PATH_COLOURS =
56
+ Object.fromEntries(PATH_TYPES.flatMap(pathType => [[pathType.type, pathType.colour]]));
57
+
58
+ export function pathColour(pathType)
59
+ {
60
+ return PATH_COLOURS[pathType] || '#FF0';
61
+ }
62
+
55
63
  //==============================================================================
56
64
 
57
65
  export class PathManager
@@ -291,6 +299,7 @@ export class PathManager
291
299
  enablePathsBySystem(system, enable, force=false)
292
300
  //==============================================
293
301
  {
302
+ let changed = false;
294
303
  for (const pathId of system.pathIds) {
295
304
  const path = this.__paths[pathId];
296
305
  if (this.__pathtypeEnabled[path.pathType]
@@ -303,6 +312,7 @@ export class PathManager
303
312
  for (const featureId of featureIds) {
304
313
  this.__ui.enableFeature(featureId, enable, force);
305
314
  }
315
+ changed = true
306
316
  }
307
317
  path.systemCount += (enable ? 1 : -1);
308
318
  if (path.systemCount < 0) {
@@ -310,6 +320,9 @@ export class PathManager
310
320
  }
311
321
  // TODO? Show connectors and parent components of these paths??
312
322
  }
323
+ if (changed) {
324
+ this.#notifyWatchers()
325
+ }
313
326
  }
314
327
 
315
328
  enablePathsByType(pathType, enable, force=false)
@@ -322,9 +335,16 @@ export class PathManager
322
335
  this.__ui.enableFeature(featureId, enable, force);
323
336
  }
324
337
  this.__pathtypeEnabled[pathType] = enable;
338
+ this.#notifyWatchers()
325
339
  }
326
340
  }
327
341
 
342
+ pathTypeEnabled(pathType)
343
+ //=======================
344
+ {
345
+ return this.__pathtypeEnabled[pathType] || false
346
+ }
347
+
328
348
  nodePathModels(nodeId)
329
349
  //====================
330
350
  {
@@ -352,6 +372,31 @@ export class PathManager
352
372
  }
353
373
  return nodeIds;
354
374
  }
375
+
376
+ #lastWatcherId = 0
377
+ #watcherCallbacks = new Map()
378
+
379
+ addWatcher(callback)
380
+ //==================
381
+ {
382
+ this.#lastWatcherId += 1
383
+ this.#watcherCallbacks.set(this.#lastWatcherId, callback)
384
+ return this.#lastWatcherId
385
+ }
386
+
387
+ removeWatcher(watcherId)
388
+ //======================
389
+ {
390
+ this.#watcherCallbacks.delete(watcherId)
391
+ }
392
+
393
+ #notifyWatchers()
394
+ //===============
395
+ {
396
+ for (const callback of this.#watcherCallbacks.values()) {
397
+ callback()
398
+ }
399
+ }
355
400
  }
356
401
 
357
402
  //==============================================================================