@abi-software/flatmap-viewer 2.5.0-a.1 → 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.5.0-a.1``
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.5.0-a.1",
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",
@@ -19,6 +19,7 @@
19
19
  "dependencies": {
20
20
  "@babel/runtime": "^7.10.4",
21
21
  "@deck.gl/core": "^8.9.33",
22
+ "@deck.gl/extensions": "^8.9.34",
22
23
  "@deck.gl/layers": "^8.9.33",
23
24
  "@deck.gl/mapbox": "^8.9.33",
24
25
  "@fortawesome/fontawesome-free": "^6.4.0",
@@ -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
+ //==============================================================================
@@ -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
  /**
@@ -22,10 +22,6 @@ limitations under the License.
22
22
 
23
23
  //==============================================================================
24
24
 
25
- import {colord} from "colord";
26
-
27
- import {MapboxOverlay as DeckOverlay} from '@deck.gl/mapbox';
28
- import {ArcLayer} from '@deck.gl/layers';
29
25
  import maplibregl from 'maplibre-gl';
30
26
 
31
27
  import {default as turfArea} from '@turf/area';
@@ -38,14 +34,16 @@ import polylabel from 'polylabel';
38
34
  //==============================================================================
39
35
 
40
36
  import {LayerManager} from './layers';
41
- import {pathColour, PATHWAYS_LAYER, PathManager} from './pathways';
42
- import {VECTOR_TILES_SOURCE} from './styling';
37
+ import {PATHWAYS_LAYER, PathManager} from './pathways';
38
+ import {VECTOR_TILES_SOURCE} from './layers/styling';
39
+ import {Paths3DLayer} from './layers/paths3d'
43
40
  import {SystemsManager} from './systems';
44
41
 
45
42
  import {displayedProperties, InfoControl} from './controls/info';
46
43
  import {BackgroundControl, LayerControl, NerveControl,
47
44
  SCKANControl} from './controls/controls';
48
45
  import {PathControl} from './controls/paths';
46
+ import {Path3DControl} from './controls/paths3d'
49
47
  import {SearchControl} from './controls/search';
50
48
  import {SystemsControl} from './controls/systems';
51
49
  import {TaxonsControl} from './controls/taxons';
@@ -120,17 +118,12 @@ function getRenderedLabel(properties)
120
118
  return properties.renderedLabel;
121
119
  }
122
120
 
123
- // Should this be in `pathways.js` ??
124
- function pathColourRGB(pathType, alpha=255)
125
- {
126
- const rgb = colord(pathColour(pathType)).toRgb();
127
- return [rgb.r, rgb.g, rgb.b, alpha];
128
- }
129
-
130
121
  //==============================================================================
131
122
 
132
123
  export class UserInteractions
133
124
  {
125
+ #paths3dLayer
126
+
134
127
  constructor(flatmap)
135
128
  {
136
129
  this._flatmap = flatmap;
@@ -194,6 +187,9 @@ export class UserInteractions
194
187
  // All taxons of connectivity paths are enabled by default
195
188
  this.__enabledConnectivityTaxons = new Set(this._flatmap.taxonIdentifiers);
196
189
 
190
+ // Support 3D path view
191
+ this.#paths3dLayer = new Paths3DLayer(flatmap, this)
192
+
197
193
  // Add various controls when running standalone
198
194
  if (flatmap.options.standalone) {
199
195
  // Add a control to search annotations if option set
@@ -225,73 +221,9 @@ export class UserInteractions
225
221
  // Connectivity taxon control for AC maps
226
222
  this._map.addControl(new TaxonsControl(flatmap));
227
223
  }
228
- }
229
-
230
- const pathData = [...flatmap.annotations.values()]
231
- .filter(ann => ann['tile-layer'] === 'pathways'
232
- && ann['geometry'] === 'LineString'
233
- && 'type' in ann && ann['type'].startsWith('line')
234
- && 'kind' in ann && !ann['kind'].includes('arterial') && !ann['kind'].includes('venous')
235
- && 'pathStartPosition' in ann
236
- && 'pathEndPosition' in ann)
237
224
 
238
- /*
239
- {
240
- "featureId": 37,
241
- "id": "ilxtr_neuron-type-keast-10",
242
- "kind": "sensory",
243
- "label": "L6-S1 sensory neuron innervating bladder",
244
- "models": "ilxtr:neuron-type-keast-10",
245
- "sckan": true,
246
- "source": "https://apinatomy.org/uris/models/keast-bladder",
247
- "taxons": ["NCBITaxon:10116"],
248
- "tile-layer": "pathways",
249
- "type": "line",
250
- "bounds": [1.5454426659162825, -1.6254174813389017, 1.7478459571498208, -1.3632864333949712],
251
- "markerPosition": [1.6466443115330516, -1.4943519573669364],
252
- "geometry": "LineString",
253
- "layer": "neural-routes",
254
- "pathStartPosition": [1.7478459571498208, -1.3632864333949712],
255
- "pathEndPosition": [1.5454426659162825, -1.6254174813389017]
256
- }
257
- */
258
-
259
- const deckOverlay = new DeckOverlay({
260
- layers: [
261
- new ArcLayer({
262
- id: 'arcs',
263
- data: pathData,
264
- pickable: true,
265
- autoHighlight: true,
266
- numSegments: 100,
267
- onHover: (i, e) => {
268
- //console.log('hover', i, e)
269
- if (i.object) {
270
- const lineFeatureId = +i.object.featureId;
271
- this.__activateFeature(this.mapFeature(lineFeatureId));
272
- for (const featureId of this.__pathManager.lineFeatureIds([lineFeatureId])) {
273
- if (+featureId !== lineFeatureId) {
274
- this.__activateFeature(this.mapFeature(featureId));
275
- }
276
- }
225
+ this._map.addControl(new Path3DControl(this));
277
226
  }
278
- },
279
- onClick: (i, e) => {
280
- console.log('click', i, e)
281
- },
282
- // Styles
283
- getSourcePosition: f => f.pathStartPosition,
284
- getTargetPosition: f => f.pathEndPosition,
285
- getSourceColor: f => pathColourRGB(f.kind, 160),
286
- getTargetColor: f => pathColourRGB(f.kind, 160),
287
- highlightColor: o => pathColourRGB(o.object.kind),
288
- getWidth: 3
289
- })
290
- ],
291
- getTooltip: ({object}) => object && object.label
292
- });
293
-
294
- this._map.addControl(deckOverlay);
295
227
 
296
228
  // Handle mouse events
297
229
 
@@ -365,6 +297,12 @@ this._map.addControl(deckOverlay);
365
297
  this._layerManager.activate(layerId, enable);
366
298
  }
367
299
 
300
+ enable3dPaths(enable=true)
301
+ //========================
302
+ {
303
+ this.#paths3dLayer.enable(enable)
304
+ }
305
+
368
306
  getSystems()
369
307
  //==========
370
308
  {
@@ -536,8 +474,8 @@ this._map.addControl(deckOverlay);
536
474
  this._layerManager.setPaint({...this.__colourOptions, dimmed: false});
537
475
  }
538
476
 
539
- __activateFeature(feature)
540
- //========================
477
+ activateFeature(feature)
478
+ //======================
541
479
  {
542
480
  if (feature !== undefined) {
543
481
  this._map.setFeatureState(feature, { active: true });
@@ -885,7 +823,7 @@ this._map.addControl(deckOverlay);
885
823
  let tooltip = '';
886
824
  if (displayInfo) {
887
825
  if (!('tooltip' in features[0].properties)) {
888
- this.__activateFeature(features[0]);
826
+ this.activateFeature(features[0]);
889
827
  }
890
828
  info = this._infoControl.featureInformation(features, event.lngLat);
891
829
  }
@@ -898,11 +836,11 @@ this._map.addControl(deckOverlay);
898
836
  tooltipFeature = lineFeatures[0];
899
837
  for (const lineFeature of lineFeatures) {
900
838
  const lineFeatureId = +lineFeature.properties.featureId; // Ensure numeric
901
- this.__activateFeature(lineFeature);
839
+ this.activateFeature(lineFeature);
902
840
  const lineIds = new Set(lineFeatures.map(f => f.properties.featureId));
903
841
  for (const featureId of this.__pathManager.lineFeatureIds(lineIds)) {
904
842
  if (+featureId !== lineFeatureId) {
905
- this.__activateFeature(this.mapFeature(featureId));
843
+ this.activateFeature(this.mapFeature(featureId));
906
844
  }
907
845
  }
908
846
  }
@@ -952,7 +890,7 @@ this._map.addControl(deckOverlay);
952
890
  info = `<div id="info-control-info">${htmlList.join('\n')}</div>`;
953
891
  }
954
892
  }
955
- this.__activateFeature(feature);
893
+ this.activateFeature(feature);
956
894
  this.__activateRelatedFeatures(feature);
957
895
  if ('hyperlink' in feature.properties) {
958
896
  this._map.getCanvas().style.cursor = 'pointer';
@@ -1072,25 +1010,19 @@ this._map.addControl(deckOverlay);
1072
1010
  if ('nerveId' in feature.properties) {
1073
1011
  const nerveId = feature.properties.nerveId;
1074
1012
  if (nerveId !== feature.id) {
1075
- this.__activateFeature(this.mapFeature(nerveId));
1013
+ this.activateFeature(this.mapFeature(nerveId));
1076
1014
  }
1077
1015
  for (const featureId of this.__pathManager.nerveFeatureIds(nerveId)) {
1078
- this.__activateFeature(this.mapFeature(featureId));
1016
+ this.activateFeature(this.mapFeature(featureId));
1079
1017
  }
1080
1018
  }
1081
1019
  if ('nodeId' in feature.properties) {
1082
1020
  for (const featureId of this.__pathManager.pathFeatureIds(feature.properties.nodeId)) {
1083
- this.__activateFeature(this.mapFeature(featureId));
1021
+ this.activateFeature(this.mapFeature(featureId));
1084
1022
  }
1085
1023
  }
1086
1024
  }
1087
1025
 
1088
- enablePath(pathId, enable=true)
1089
- //=============================
1090
- {
1091
- this.__pathManager.enablePath(pathId, enable);
1092
- }
1093
-
1094
1026
  enablePathsBySystem(system, enable=true, force=false)
1095
1027
  //===================================================
1096
1028
  {
@@ -1315,7 +1247,7 @@ this._map.addControl(deckOverlay);
1315
1247
  if (event.type === 'mouseenter') {
1316
1248
  // Highlight on mouse enter
1317
1249
  this.resetActiveFeatures_();
1318
- this.__activateFeature(feature);
1250
+ this.activateFeature(feature);
1319
1251
  } else {
1320
1252
  this.selectionEvent_(event, feature)
1321
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
@@ -299,6 +299,7 @@ export class PathManager
299
299
  enablePathsBySystem(system, enable, force=false)
300
300
  //==============================================
301
301
  {
302
+ let changed = false;
302
303
  for (const pathId of system.pathIds) {
303
304
  const path = this.__paths[pathId];
304
305
  if (this.__pathtypeEnabled[path.pathType]
@@ -311,6 +312,7 @@ export class PathManager
311
312
  for (const featureId of featureIds) {
312
313
  this.__ui.enableFeature(featureId, enable, force);
313
314
  }
315
+ changed = true
314
316
  }
315
317
  path.systemCount += (enable ? 1 : -1);
316
318
  if (path.systemCount < 0) {
@@ -318,6 +320,9 @@ export class PathManager
318
320
  }
319
321
  // TODO? Show connectors and parent components of these paths??
320
322
  }
323
+ if (changed) {
324
+ this.#notifyWatchers()
325
+ }
321
326
  }
322
327
 
323
328
  enablePathsByType(pathType, enable, force=false)
@@ -330,9 +335,16 @@ export class PathManager
330
335
  this.__ui.enableFeature(featureId, enable, force);
331
336
  }
332
337
  this.__pathtypeEnabled[pathType] = enable;
338
+ this.#notifyWatchers()
333
339
  }
334
340
  }
335
341
 
342
+ pathTypeEnabled(pathType)
343
+ //=======================
344
+ {
345
+ return this.__pathtypeEnabled[pathType] || false
346
+ }
347
+
336
348
  nodePathModels(nodeId)
337
349
  //====================
338
350
  {
@@ -360,6 +372,31 @@ export class PathManager
360
372
  }
361
373
  return nodeIds;
362
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
+ }
363
400
  }
364
401
 
365
402
  //==============================================================================