@abi-software/flatmap-viewer 2.5.0-a.2 → 2.5.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.
@@ -18,66 +18,133 @@ limitations under the License.
18
18
 
19
19
  ******************************************************************************/
20
20
 
21
- import {colord} from 'colord'
22
21
  import {ArcLayer} from '@deck.gl/layers'
23
22
  import {MapboxOverlay as DeckOverlay} from '@deck.gl/mapbox'
24
- import {PathStyleExtension} from '@deck.gl/extensions'
23
+ import {Model, Geometry} from '@luma.gl/core'
24
+ import GL from '@luma.gl/constants'
25
25
 
26
26
  //==============================================================================
27
27
 
28
- import {pathColour} from '../pathways'
28
+ import {pathColourArray} from '../pathways'
29
29
 
30
- // Should this be in `pathways.js` ??
31
- function pathColourRGB(pathType, alpha=255)
32
- //=========================================
30
+ //==============================================================================
31
+
32
+ const transparencyCheck = '|| length(vColor) == 0.0'
33
+
34
+ class ArcMapLayer extends ArcLayer
33
35
  {
34
- const rgb = colord(pathColour(pathType)).toRgb()
35
- return [rgb.r, rgb.g, rgb.b, alpha]
36
+ static layerName = 'ArcMapLayer'
37
+
38
+ #dirty = false
39
+ #pathData
40
+
41
+ constructor(...args)
42
+ {
43
+ super(...args)
44
+ this.#pathData = new Map([...this.props.data].map(ann => [+ann.featureId, ann]))
45
+ this.#pathData.forEach(ann => delete ann['hidden'])
46
+ }
47
+
48
+ get featureIds()
49
+ //==============
50
+ {
51
+ return [...this.#pathData.keys()]
52
+ }
53
+
54
+ getShaders()
55
+ //==========
56
+ {
57
+ const shaders = super.getShaders()
58
+ shaders.fs = `#version 300 es\n${shaders.fs}`
59
+ .replace('isValid == 0.0', `isValid == 0.0 ${transparencyCheck}`)
60
+ shaders.vs = `#version 300 es\n${shaders.vs}`
61
+ return shaders
62
+ }
63
+ setDataProperty(featureId, key, enabled)
64
+ //======================================
65
+ {
66
+ const properties = this.#pathData.get(+featureId)
67
+ if (properties) {
68
+ if (!(key in properties) || properties[key] !== enabled) {
69
+ properties[key] = enabled
70
+ this.#dirty = true
71
+ }
72
+ }
73
+ }
74
+
75
+ redraw(force=false)
76
+ //=================
77
+ {
78
+ if (force || this.#dirty) {
79
+ this.internalState.changeFlags.dataChanged = true
80
+ this.setNeedsUpdate()
81
+ this.#dirty = false
82
+ }
83
+ }
36
84
  }
37
85
 
38
86
  //==============================================================================
39
87
 
40
- /*
41
- TODO:
42
-
43
- * Dashed paths
44
- * Control by SCKAN status
45
- * Control by taxon visibility
46
- * Control by visible layer
88
+ const makeDashedTriangles = ` float alpha = floor(fract(float(gl_VertexID)/12.0)+0.5);
89
+ if (vColor.a != 0.0) vColor.a *= alpha;
90
+ `
47
91
 
92
+ class ArcDashedLayer extends ArcMapLayer
93
+ {
94
+ static layerName = 'ArcDashedLayer'
48
95
 
49
- Also see https://alasarr.github.io/deck.gl/docs/api-reference/core/layer#updatetriggers
96
+ constructor(...args)
97
+ {
98
+ super(...args)
99
+ }
50
100
 
101
+ getShaders()
102
+ //==========
103
+ {
104
+ const shaders = super.getShaders()
105
+ shaders.vs = shaders.vs.replace('DECKGL_FILTER_COLOR(', `${makeDashedTriangles}\n DECKGL_FILTER_COLOR(`)
106
+ return shaders
107
+ }
51
108
 
109
+ _getModel(gl)
110
+ //===========
52
111
  {
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]
112
+ const {numSegments} = this.props
113
+ let positions = []
114
+ for (let i = 0; i < numSegments; i++) {
115
+ positions = positions.concat([i, 1, 0, i, -1, 0, i+1, 1, 0,
116
+ i, -1, 0, i+1, 1, 0, i+1, -1, 0])
117
+ }
118
+ const model = new Model(gl, {
119
+ ...this.getShaders(),
120
+ id: this.props.id,
121
+ geometry: new Geometry({
122
+ drawMode: GL.TRIANGLES,
123
+ attributes: {
124
+ positions: new Float32Array(positions)
125
+ }
126
+ }),
127
+ isInstanced: true,
128
+ })
129
+ model.setUniforms({numSegments: numSegments})
130
+ return model
69
131
  }
70
- */
132
+ }
71
133
 
72
134
  //==============================================================================
73
135
 
74
136
  export class Paths3DLayer
75
137
  {
138
+ #arcLayers = new Map()
76
139
  #deckOverlay = null
140
+ #dimmed = false
77
141
  #enabled = false
142
+ #featureToLayer = new Map()
143
+ #knownTypes = []
78
144
  #map
79
145
  #pathData
80
146
  #pathManager
147
+ #pathStyles
81
148
  #ui
82
149
 
83
150
  constructor(flatmap, ui)
@@ -86,85 +153,182 @@ export class Paths3DLayer
86
153
  this.#map = flatmap.map
87
154
  this.#pathManager = ui.pathManager
88
155
  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)
156
+ this.#pathData = new Map([...flatmap.annotations.values()]
157
+ .filter(ann => ann['tile-layer'] === 'pathways'
158
+ && ann['geometry'] === 'LineString'
159
+ && 'type' in ann && ann['type'].startsWith('line')
160
+ && 'kind' in ann
161
+ && 'pathStartPosition' in ann
162
+ && 'pathEndPosition' in ann)
163
+ .map(ann => [ann.featureId, ann]))
164
+ this.#pathStyles = new Map(this.#pathManager.pathStyles().map(s => [s.type, s]))
165
+ this.#knownTypes = [...this.#pathStyles.keys()].filter(t => t !== 'other')
96
166
  }
97
167
 
98
168
  enable(enable=true)
99
169
  //=================
100
170
  {
101
171
  if (enable && !this.#enabled) {
102
- this.#setDeckOverlay()
172
+ this.#setupDeckOverlay()
103
173
  this.#map.addControl(this.#deckOverlay)
104
174
  } else if (!enable && this.#enabled) {
105
175
  if (this.#deckOverlay) {
106
176
  this.#map.removeControl(this.#deckOverlay)
177
+ this.#deckOverlay.finalize()
107
178
  this.#deckOverlay = null
108
179
  }
180
+ this.#featureToLayer = new Map()
109
181
  }
110
182
  this.#enabled = enable
111
183
  }
112
184
 
113
- #pathStateChanged()
114
- //=================
185
+ queryFeaturesAtPoint(point)
186
+ //=========================
115
187
  {
116
188
  if (this.#deckOverlay) {
117
- this.#map.removeControl(this.#deckOverlay)
118
- this.#setDeckOverlay()
119
- this.#map.addControl(this.#deckOverlay)
189
+ return this.#deckOverlay
190
+ .pickMultipleObjects(point)
191
+ .map(o => this.#makeMapFeature(o.object))
192
+ }
193
+ return []
194
+ }
195
+
196
+ redraw(force=false)
197
+ //=================
198
+ {
199
+ for (const layer of this.#arcLayers.values()) {
200
+ layer.redraw(force)
120
201
  }
121
202
  }
122
203
 
123
- #setDeckOverlay()
204
+ removeFeatureState(featureId, key)
205
+ //================================
206
+ {
207
+ const layer = this.#featureToLayer.get(+featureId)
208
+ if (layer) {
209
+ layer.setDataProperty(featureId, key, false)
210
+ layer.redraw()
211
+ }
212
+ }
213
+
214
+ setFeatureState(featureId, state)
215
+ //===============================
216
+ {
217
+ const layer = this.#featureToLayer.get(+featureId)
218
+ if (layer) {
219
+ for (const [key, value] of Object.entries(state)) {
220
+ layer.setDataProperty(featureId, key, value)
221
+ }
222
+ layer.redraw()
223
+ }
224
+ }
225
+
226
+ setPaint(options)
124
227
  //===============
125
228
  {
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,
229
+ const dimmed = options.dimmed || false
230
+ if (this.#dimmed !== dimmed) {
231
+ this.#dimmed = dimmed
232
+ this.redraw(true)
233
+ }
234
+ }
235
+
236
+ #addArcLayer(pathType)
237
+ //====================
238
+ {
239
+ const layer = this.#pathStyles.get(pathType).dashed
240
+ ? new ArcDashedLayer(this.#layerOptions(pathType))
241
+ : new ArcMapLayer(this.#layerOptions(pathType))
242
+ layer.featureIds.forEach(id => this.#featureToLayer.set(+id, layer))
243
+ this.#arcLayers.set(pathType, layer)
244
+ }
245
+
246
+ #removeArcLayer(pathType)
247
+ //=======================
248
+ {
249
+ const layer = this.#arcLayers.get(pathType)
250
+ if (layer) {
251
+ layer.featureIds.forEach(id => this.#featureToLayer.delete(+id))
252
+ this.#arcLayers.delete(pathType)
253
+ }
254
+ }
255
+
256
+ #pathColour(properties)
257
+ //=====================
258
+ {
259
+ if (properties.hidden) {
260
+ return [0, 0, 0, 0]
261
+ }
262
+ return pathColourArray(properties.kind,
263
+ properties.active || properties.selected ? 255
264
+ : this.#dimmed ? 20 : 160)
265
+ }
266
+
267
+ #pathStateChanged(changes={})
268
+ //===========================
269
+ {
270
+ if (this.#deckOverlay) {
271
+ if ('pathType' in changes) {
272
+ const pathType = changes.pathType
273
+ const enabled = this.#pathManager.pathTypeEnabled(pathType)
274
+ if (enabled && !this.#arcLayers.has(pathType)) {
275
+ this.#addArcLayer(pathType)
276
+ } else if (!enabled && this.#arcLayers.has(pathType)) {
277
+ this.#removeArcLayer(pathType)
278
+ }
279
+ this.#deckOverlay.setProps({
280
+ layers: [...this.#arcLayers.values()]
165
281
  })
166
- ],
167
- getTooltip: ({object}) => object && object.label
282
+ }
283
+ }
284
+ }
285
+
286
+
287
+ #layerOptions(pathType)
288
+ //=====================
289
+ {
290
+ const pathData = [...this.#pathData.values()]
291
+ .filter(ann => (this.#knownTypes.includes(ann.kind) && (ann.kind === pathType)
292
+ || !this.#knownTypes.includes(ann.kind) && (pathType === 'other')))
293
+ return {
294
+ id: `arc-${pathType}`,
295
+ data: pathData,
296
+ pickable: true,
297
+ autoHighlight: true,
298
+ numSegments: 400,
299
+ // Styles
300
+ getSourcePosition: f => f.pathStartPosition,
301
+ getTargetPosition: f => f.pathEndPosition,
302
+ getSourceColor: this.#pathColour.bind(this),
303
+ getTargetColor: this.#pathColour.bind(this),
304
+ highlightColor: o => this.#pathColour(o.object),
305
+ opacity: 1.0,
306
+ getWidth: 3,
307
+ }
308
+ }
309
+
310
+ #makeMapFeature(pickedObject)
311
+ //===========================
312
+ {
313
+ // Mock up a map vector feature
314
+ return {
315
+ id: pickedObject.featureId,
316
+ source: 'vector-tiles',
317
+ sourceLayer: `${pickedObject.layer}_${pickedObject['tile-layer']}`,
318
+ properties: pickedObject,
319
+ arc3dLayer: true
320
+ }
321
+ }
322
+
323
+ #setupDeckOverlay()
324
+ //=================
325
+ {
326
+ [...this.#pathStyles.values()].filter(style => this.#pathManager.pathTypeEnabled(style.type))
327
+ .forEach(style => this.#addArcLayer(style.type))
328
+ this.#deckOverlay = new DeckOverlay({
329
+ layers: [...this.#arcLayers.values()],
168
330
  })
169
331
  }
170
332
  }
333
+
334
+ //==============================================================================
package/src/main.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Flatmap viewer and annotation tool
4
4
 
5
- Copyright (c) 2019 David Brooks
5
+ Copyright (c) 2019 - 2024 David Brooks
6
6
 
7
7
  Licensed under the Apache License, Version 2.0 (the "License");
8
8
  you may not use this file except in compliance with the License.
@@ -27,6 +27,52 @@ export { MapManager };
27
27
 
28
28
  //==============================================================================
29
29
 
30
+ class DrawControl
31
+ {
32
+ constructor(flatmap)
33
+ {
34
+ this._flatmap = flatmap
35
+ this._lastEvent = null
36
+ this._idField = document.getElementById('drawing-id')
37
+
38
+ this._okBtn = document.getElementById('drawing-ok')
39
+ if (this._okBtn) {
40
+ this._okBtn.addEventListener('click', e => {
41
+ if (this._lastEvent) {
42
+ const feature = this._flatmap.refreshAnnotationFeatureGeometry(this._lastEvent.feature)
43
+ this._flatmap.commitAnnotationEvent(this._lastEvent)
44
+ this._idField.innerText = ''
45
+ this._lastEvent = null
46
+ // Send `feature`, along with user comments, to the annotation service
47
+ }
48
+ })
49
+ }
50
+
51
+ this._cancelBtn = document.getElementById('drawing-cancel')
52
+ if (this._cancelBtn) {
53
+ this._cancelBtn.addEventListener('click', e => {
54
+ if (this._lastEvent) {
55
+ this._flatmap.rollbackAnnotationEvent(this._lastEvent)
56
+ this._idField.innerText = ''
57
+ this._lastEvent = null
58
+ }
59
+ })
60
+ }
61
+ }
62
+
63
+ handleEvent(event)
64
+ //================
65
+ {
66
+ console.log(event)
67
+ if (this._idField) {
68
+ this._idField.innerText = `Annotation ${event.type}, Id: ${event.feature.id}`
69
+ this._lastEvent = event
70
+ }
71
+ }
72
+ }
73
+
74
+ //==============================================================================
75
+
30
76
  export async function standaloneViewer(map_endpoint=null, options={})
31
77
  {
32
78
  const requestUrl = new URL(window.location.href);
@@ -52,6 +98,7 @@ export async function standaloneViewer(map_endpoint=null, options={})
52
98
  });
53
99
 
54
100
  let currentMap = null;
101
+ let drawControl = null;
55
102
  let defaultBackground = localStorage.getItem('flatmap-background-colour') || 'black';
56
103
 
57
104
  const mapOptions = Object.assign({
@@ -62,6 +109,7 @@ export async function standaloneViewer(map_endpoint=null, options={})
62
109
  showId: true,
63
110
  showPosition: false,
64
111
  standalone: true,
112
+ annotator: true,
65
113
  }, options);
66
114
 
67
115
  function loadMap(id, taxon, sex)
@@ -89,6 +137,8 @@ export async function standaloneViewer(map_endpoint=null, options={})
89
137
  mapManager.loadMap(id, 'map-canvas', (eventType, ...args) => {
90
138
  if (args[0].type === 'control' && args[0].control === 'background') {
91
139
  mapOptions.background = args[0].value;
140
+ } else if (eventType === 'annotation') {
141
+ drawControl.handleEvent(...args)
92
142
  }
93
143
  }, mapOptions)
94
144
  .then(map => {
@@ -98,6 +148,7 @@ export async function standaloneViewer(map_endpoint=null, options={})
98
148
  map.addMarker('UBERON:0001155'); // Colon
99
149
  map.addMarker('UBERON:0001255'); // Bladder
100
150
  currentMap = map;
151
+ drawControl = new DrawControl(map)
101
152
  })
102
153
  .catch(error => {
103
154
  console.log(error);
@@ -179,3 +230,6 @@ export async function standaloneViewer(map_endpoint=null, options={})
179
230
 
180
231
  loadMap(mapId, mapTaxon, mapSex);
181
232
  }
233
+
234
+ //==============================================================================
235
+
package/src/pathways.js CHANGED
@@ -20,6 +20,8 @@ limitations under the License.
20
20
 
21
21
  'use strict';
22
22
 
23
+ import {colord} from 'colord'
24
+
23
25
  //==============================================================================
24
26
 
25
27
  import { reverseMap } from './utils';
@@ -49,15 +51,18 @@ const PATH_TYPES = [
49
51
  { type: "error", label: "Paths with errors or warnings", colour: "#FF0", enabled: false}
50
52
  ];
51
53
 
54
+ const PathTypeMap = new Map(PATH_TYPES.map(t => [t.type, t]))
55
+
52
56
  export const PATH_STYLE_RULES =
53
57
  PATH_TYPES.flatMap(pathType => [['==', ['get', 'kind'], pathType.type], pathType.colour]);
54
58
 
55
- export const PATH_COLOURS =
56
- Object.fromEntries(PATH_TYPES.flatMap(pathType => [[pathType.type, pathType.colour]]));
57
-
58
- export function pathColour(pathType)
59
+ export function pathColourArray(pathType, alpha=255)
60
+ //==================================================
59
61
  {
60
- return PATH_COLOURS[pathType] || '#FF0';
62
+ const rgb = colord(PathTypeMap.has(pathType)
63
+ ? PathTypeMap.get(pathType).colour
64
+ : '#FF0').toRgb()
65
+ return [rgb.r, rgb.g, rgb.b, alpha]
61
66
  }
62
67
 
63
68
  //==============================================================================
@@ -162,6 +167,21 @@ export class PathManager
162
167
  }
163
168
  }
164
169
 
170
+ pathStyles()
171
+ //==========
172
+ {
173
+ const styles = []
174
+ for (const mapType of this.pathTypes()) {
175
+ const defn = PathTypeMap.get(mapType.type)
176
+ styles.push({
177
+ type: defn.type,
178
+ colour: defn.colour,
179
+ dashed: defn.dashed || false
180
+ })
181
+ }
182
+ return styles
183
+ }
184
+
165
185
  pathTypes()
166
186
  //=========
167
187
  {
@@ -335,7 +355,7 @@ export class PathManager
335
355
  this.__ui.enableFeature(featureId, enable, force);
336
356
  }
337
357
  this.__pathtypeEnabled[pathType] = enable;
338
- this.#notifyWatchers()
358
+ this.#notifyWatchers({pathType})
339
359
  }
340
360
  }
341
361
 
@@ -390,11 +410,11 @@ export class PathManager
390
410
  this.#watcherCallbacks.delete(watcherId)
391
411
  }
392
412
 
393
- #notifyWatchers()
394
- //===============
413
+ #notifyWatchers(changes={})
414
+ //=========================
395
415
  {
396
416
  for (const callback of this.#watcherCallbacks.values()) {
397
- callback()
417
+ callback(changes)
398
418
  }
399
419
  }
400
420
  }